Sfoglia il codice sorgente

企业微信功能

laiguoran 2 anni fa
parent
commit
c49e65e22a

+ 63 - 26
app/controller/profile_controller.js

@@ -15,6 +15,7 @@ const qr = require('qr-image');
 const path = require('path');
 const sendToWormhole = require('stream-wormhole');
 const loginWay = require('../const/setting').loginWay;
+const wxWork = require('../lib/wx_work');
 
 module.exports = app => {
 
@@ -568,6 +569,9 @@ module.exports = app => {
 
             // 获取账号数据
             const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+            if (accountData.qywx_user_info !== null) {
+                accountData.qywx_user_info = JSON.parse(accountData.qywx_user_info);
+            }
 
             const renderData = {
                 accountData,
@@ -587,34 +591,67 @@ module.exports = app => {
                 const sessionUser = ctx.session.sessionUser;
                 // 获取账号数据
                 const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
-                const result = await ctx.service.projectAccount.bindWx(sessionUser.accountId, null, null);
+                // 判断解绑类型
+                if (ctx.request.body.data_type === 'wxWork') {
+                    const result = await ctx.service.projectAccount.bindWx4Work(sessionUser.accountId, null, null, null);
+                    if (!result) {
+                        throw '解绑企业微信失败!';
+                    }
+                    // 解绑成功通知
+                    const qywx = new wxWork(ctx);
+                    const desc = '您好,纵横云计量与企业微信解绑成功。';
+                    const content = [
+                        {
+                            keyname: '项目编号',
+                            value: ctx.session.sessionProject.code,
+                        },
+                        {
+                            keyname: '账号',
+                            value: sessionUser.account,
+                        },
+                        {
+                            keyname: '绑定时间',
+                            value: moment(new Date()).format('YYYY-MM-DD'),
+                        },
+                        {
+                            keyname: '备注',
+                            value: '感谢您的使用,要接收通知请重新绑定。',
+                        },
+                    ];
+                    const url = ctx.protocol + '://' + ctx.host + '/wx/tips?msg=解绑成功,感谢您的使用。';
+                    await qywx.sendTemplateCard([accountData.qywx_userid], accountData.qywx_corpid, '账号解绑成功通知', desc, content, url);
+
+                    this.setMessage('企业微信解绑成功', this.messageType.SUCCESS);
+                } else {
+                    const result = await ctx.service.projectAccount.bindWx(sessionUser.accountId, null, null);
 
-                if (!result) {
-                    throw '解绑微信失败!';
+                    if (!result) {
+                        throw '解绑微信失败!';
+                    }
+                    // 解绑成功通知
+                    const templateId = '0w0Yp65X4PHccTLeAyE5aQhS-blS-bylwxAPYEGy3CI';
+                    const url = '';
+                    const msgData = {
+                        first: {
+                            value: '您好,纵横云计量与微信解绑成功。',
+                        },
+                        keyword1: {
+                            value: ctx.session.sessionProject.code,
+                        },
+                        keyword2: {
+                            value: sessionUser.account,
+                        },
+                        keyword3: {
+                            value: moment(new Date()).format('YYYY-MM-DD'),
+                        },
+                        remark: {
+                            value: '感谢您的使用,要接收通知请重新绑定。',
+                        },
+                    };
+                    await app.wechat.api.sendTemplate(accountData.wx_openid, templateId, url, '', msgData);
+
+                    this.setMessage('微信解绑成功', this.messageType.SUCCESS);
                 }
-                // 解绑成功通知
-                const templateId = '0w0Yp65X4PHccTLeAyE5aQhS-blS-bylwxAPYEGy3CI';
-                const url = '';
-                const msgData = {
-                    first: {
-                        value: '您好,纵横云计量与微信解绑成功。',
-                    },
-                    keyword1: {
-                        value: ctx.session.sessionProject.code,
-                    },
-                    keyword2: {
-                        value: sessionUser.account,
-                    },
-                    keyword3: {
-                        value: moment(new Date()).format('YYYY-MM-DD'),
-                    },
-                    remark: {
-                        value: '感谢您的使用,要接收通知请重新绑定。',
-                    },
-                };
-                await app.wechat.api.sendTemplate(accountData.wx_openid, templateId, url, '', msgData);
-
-                this.setMessage('微信解绑成功', this.messageType.SUCCESS);
             } catch (error) {
                 console.log(error);
                 this.setMessage(error.toString(), this.messageType.ERROR);

+ 291 - 0
app/controller/wechat_controller.js

@@ -11,9 +11,12 @@
 const moment = require('moment');
 // const Controller = require('egg').Controller;
 const crypto = require('crypto');
+const qywxCrypto = require('@wecom/crypto');
+const getRawBody = require('raw-body');
 const maintainConst = require('../const/maintain');
 const wxConst = require('../const/wechat_template.js');
 const smsTypeConst = require('../const/sms_type');
+const wxWork = require('../lib/wx_work');
 
 module.exports = app => {
     class WechatController extends app.BaseController {
@@ -254,6 +257,294 @@ module.exports = app => {
                 ctx.body = error;
             }
         }
+
+        // 企业微信功能
+        // 回调方法
+        async command(ctx) {
+            try {
+                const msg_signature = ctx.query.msg_signature;
+                const timestamp = ctx.query.timestamp;
+                const nonce = ctx.query.nonce;
+                const echostr = ctx.query.echostr;
+                const signature = qywxCrypto.getSignature(ctx.app.config.qywx.token, timestamp, nonce, echostr);
+                if (signature === msg_signature) {
+                    const aeskey = ctx.app.config.qywx.encodingAESKey;
+                    const { message } = qywxCrypto.decrypt(aeskey, echostr);
+                    ctx.body = message;
+                    // res.send(message);
+                } else {
+                    throw '验证失败';
+                }
+            } catch (e) {
+                console.log(e);
+            }
+        }
+        // 获取suite_ticket方法
+        async postCommand(ctx) {
+            try {
+                // ctx.req才能获取到rawbody
+                const wholeXML = await getRawBody(ctx.req, {
+                    length: ctx.headers['content-length'],
+                    limit: '1mb',
+                    encoding: 'utf-8',
+                });
+                const formatJson = await ctx.helper.parseXML(wholeXML);
+                const messageXML = qywxCrypto.decrypt(ctx.app.config.qywx.encodingAESKey, formatJson.Encrypt);
+                const callbackDataBody = await ctx.helper.parseXML(messageXML.message);
+                console.log('CallbackData', callbackDataBody);
+                const qywx = new wxWork(ctx);
+                switch (callbackDataBody.InfoType) {
+                    case 'suite_ticket':
+                        // 刷新
+                        console.log('SuiteTicket', callbackDataBody.SuiteTicket);
+                        await qywx.setSuiteTicket(callbackDataBody.SuiteTicket);
+                        // await app.redis.set('suite_ticket', callbackDataBody.SuiteTicket, 'EX', 1500);
+                        break;
+                    case 'reset_permanent_code':
+                    case 'create_auth':
+                        console.log('AuthCode', callbackDataBody.AuthCode);
+                        await qywx.savePermanentCode(callbackDataBody.AuthCode);
+                        qywx.setPermanentCode();// 不用马上执行,有执行就行
+                        break;
+                    case 'cancel_auth':
+                        // 企业管理员删除应用
+                        await ctx.service.wxWork.delCorp(callbackDataBody.AuthCorpId);
+                        break;
+                    default:
+                        break;
+                }
+                // 很重要,一定要返回 success 字符串
+                ctx.body = 'success';
+            } catch (e) {
+                console.log(e);
+            }
+        }
+
+        async oauthWxWorkTxt(ctx) {
+            ctx.body = 'CZwGPbI7BRGOBUX1';
+        }
+
+        /**
+         * 企业微信登录验证
+         *
+         * @param {Object} ctx - egg全局页面
+         * @return {void}
+         */
+        async workOauth(ctx) {
+            const corpid = ctx.params.corpid;
+            const redirect_uri = encodeURIComponent(ctx.query.redirect_uri);
+            const corpInfo = await ctx.service.wxWork.getDataByCondition({ corpid });
+            const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${corpid}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_privateinfo&state=STATE&agentid=${corpInfo.agentid}#wechat_redirect`;
+            ctx.redirect(url);
+        }
+
+        async workBind(ctx) {
+            try {
+                const qywx = new wxWork(ctx);
+                const token = await qywx.getCorpAccessToken(ctx.params.corpid);
+                const user = await qywx.getCorpUser(token, ctx.query.code);
+                if (!user) {
+                    throw '获取企业用户信息失败';
+                }
+                const errorMessage = ctx.session.loginError;
+                // 显示完删除
+                ctx.session.loginError = null;
+                // 获取系统维护信息
+                const maintainData = await ctx.service.maintain.getDataById(1);
+                const renderData = {
+                    maintainData,
+                    maintainConst,
+                    errorMessage,
+                    user,
+                    corpid: ctx.params.corpid,
+                };
+                await ctx.render('wechat/work_bind.ejs', renderData);
+            } catch (error) {
+                console.log(error);
+                const renderData = {
+                    status: 1,
+                    msg: error,
+                };
+                await ctx.render('wechat/tips.ejs', renderData);
+            }
+        }
+
+        async workBindwx(ctx) {
+            const corpid = ctx.request.body.corpid ? ctx.request.body.corpid : null;
+            try {
+                const result = await ctx.service.projectAccount.accountCheck(ctx.request.body);
+
+                if (!result) {
+                    throw '用户名或密码错误';
+                }
+
+                if (result === 2) {
+                    throw '该账号已被停用,请联系销售人员';
+                }
+                const accountData = result;
+                const qywx_userid = ctx.request.body.userid;
+                if (!qywx_userid || !corpid) {
+                    throw '参数有误';
+                }
+                if (accountData.qywx_userid || qywx_userid === accountData.qywx_userid) {
+                    throw '该账号已经绑定过企业微信';
+                }
+                const wxAccountData = await ctx.service.projectAccount.getDataByCondition({ project_id: accountData.project_id, qywx_userid });
+                if (wxAccountData) {
+                    throw '该企业微信号已绑定过本项目其它账号';
+                }
+                const qywx = new wxWork(ctx);
+                const token = await qywx.getCorpAccessToken(corpid);
+                const user = await qywx.getCorpUserCommonData(token, qywx_userid, corpid);
+                if (!user) {
+                    throw '获取企业用户信息失败';
+                }
+                user.avatar = ctx.request.body.avatar !== undefined ? ctx.request.body.avatar : null;
+                user.gender = ctx.request.body.gender !== undefined ? ctx.request.body.gender : null;
+
+                const result2 = await ctx.service.projectAccount.bindWx4Work(accountData.id, corpid, qywx_userid, user);
+                if (!result2) {
+                    throw '绑定失败';
+                }
+                const projectData = await ctx.service.project.getDataById(accountData.project_id);
+                const desc = '您好,纵横云计量与企业微信绑定成功。';
+                const content = [
+                    {
+                        keyname: '项目编号',
+                        value: projectData.code,
+                    },
+                    {
+                        keyname: '账号',
+                        value: accountData.account,
+                    },
+                    {
+                        keyname: '绑定时间',
+                        value: moment(new Date()).format('YYYY-MM-DD'),
+                    },
+                    {
+                        keyname: '备注',
+                        value: '感谢您的使用。',
+                    },
+                ];
+                const url = ctx.protocol + '://' + ctx.host + `/wx/work/${corpid}/project`;
+                await qywx.sendTemplateCard([qywx_userid], corpid, '账号绑定成功通知', desc, content, url, '登录项目');
+                const renderData = {
+                    status: 0,
+                    msg: '绑定成功',
+                };
+                await ctx.render('wechat/tips.ejs', renderData);
+            } catch (error) {
+                this.log(error);
+                ctx.session.loginError = error;
+                const returnUrl = corpid ? `/wx/work/${corpid}/bind` : '/';
+                ctx.redirect(returnUrl);
+            }
+        }
+
+        // 设置用户企业微信登录项目,跳转到对应wap页面
+        async url2wap4work(ctx) {
+            try {
+                if (!ctx.query.project || !ctx.query.url) {
+                    throw '参数有误';
+                }
+                const code = ctx.query.project;
+                // 查找项目数据
+                const projectData = await ctx.service.project.getProjectByCode(code.toString().trim());
+                if (projectData === null) {
+                    throw '不存在项目数据';
+                }
+                const qywx = new wxWork(ctx);
+                const token = await qywx.getCorpAccessToken(ctx.params.corpid);
+                const user = await qywx.getCorpUser(token, ctx.query.code);
+                if (!user) {
+                    throw '获取企业用户信息失败';
+                }
+                const pa = await ctx.service.projectAccount.getDataByCondition({ project_id: projectData.id, qywx_userid: user.userid });
+                if (!pa) {
+                    throw '该企业微信号未绑定此项目';
+                }
+                if (pa.enable !== 1) {
+                    throw '该账号已被停用,请联系销售人员';
+                }
+                // 设置项目和用户session记录
+                const result = await ctx.service.projectAccount.accountLogin({ project: projectData, accountData: pa }, 3);
+                if (!result) {
+                    throw '登录出错';
+                }
+                ctx.redirect(ctx.query.url);
+            } catch (error) {
+                const renderData = {
+                    status: 1,
+                    msg: error,
+                };
+                await ctx.render('wechat/tips.ejs', renderData);
+            }
+        }
+
+        async workProject(ctx) {
+            try {
+                // const user = await app.wechat.oauth.getUser(ctx.session.wechatToken.openid);
+                const qywx = new wxWork(ctx);
+                const token = await qywx.getCorpAccessToken(ctx.params.corpid);
+                const user = await qywx.getCorpUser(token, ctx.query.code);
+                if (!user) {
+                    throw '获取企业用户信息失败';
+                }
+                const paList = await ctx.service.projectAccount.getAllDataByCondition({ where: { qywx_userid: user.userid } });
+                const pidList = ctx.app._.uniq(ctx.app._.map(paList, 'project_id'));
+                const pList = [];
+                const isWap = ctx.helper.isMobile(ctx.request.header['user-agent']) ? '/wap' : '';
+                const redirect_url = ctx.protocol + '://' + ctx.host + isWap + '/dashboard';
+                for (const p of pidList) {
+                    const pro = await ctx.service.project.getDataById(p);
+                    pList.push(pro);
+                }
+                if (pList.length === 0) {
+                    throw '该企业微信号未绑定任何项目';
+                }
+                // 获取系统维护信息
+                const maintainData = await ctx.service.maintain.getDataById(1);
+                const renderData = {
+                    maintainData,
+                    maintainConst,
+                    // user,
+                    pList,
+                    redirect_url,
+                    corpid: ctx.params.corpid,
+                };
+                // ctx.body = renderData;
+                await ctx.render('wechat/work_project.ejs', renderData);
+            } catch (e) {
+                const renderData = {
+                    status: 1,
+                    msg: e,
+                };
+                await ctx.render('wechat/tips.ejs', renderData);
+            }
+        }
+
+        async workTest(ctx) {
+            try {
+                // const user = await app.wechat.oauth.getUser(ctx.session.wechatToken.openid);
+                const qywx = new wxWork(ctx);
+                const result = await qywx.getUserList(ctx.params.corpid);
+                ctx.body = result;
+            } catch (e) {
+                const renderData = {
+                    status: 1,
+                    msg: e,
+                };
+                await ctx.render('wechat/tips.ejs', renderData);
+            }
+        }
+
+        async tips(ctx) {
+            const renderData = {
+                status: 0,
+                msg: ctx.query.msg,
+            };
+            await ctx.render('wechat/tips.ejs', renderData);
+        }
     }
 
     return WechatController;

+ 46 - 7
app/extend/helper.js

@@ -25,6 +25,7 @@ const syncApiConst = require('../const/sync_api');
 const crypto = require('crypto');
 const jwt = require('jsonwebtoken');
 const sign = require('../const/sign');
+const xml2js = require('xml2js');
 module.exports = {
     _,
 
@@ -1096,22 +1097,26 @@ module.exports = {
         // }
     },
 
-
+    // 企业微信和公众号微信通知共用
     async sendWechat(userId, type, judge, template, data = {}) {
         const wechats = [];
+        const qywxs = {};
         if (!userId || (userId instanceof Array && userId.length === 0)) return;
         const wxUser = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { id: userId } });
         for (const user of wxUser) {
-            if (!user.wx_openid || user.wx_openid === '') continue;
+            if ((!user.wx_openid || user.wx_openid === '') && !user.qywx_userid) continue;
             if (!user.wx_type || user.wx_type === '') continue;
 
             const wxType = JSON.parse(user.wx_type);
             if (wxType[type] && wxType[type].indexOf(judge) !== -1) {
-                wechats.push(user.wx_openid);
+                if (user.wx_openid) wechats.push(user.wx_openid);
+                if (user.qywx_userid && user.qywx_corpid) {
+                    _.has(qywxs, user.qywx_corpid) ? qywxs[user.qywx_corpid].push(user.qywx_userid) : qywxs[user.qywx_corpid] = [user.qywx_userid];
+                }
             }
         }
 
-        if (wechats.length > 0) {
+        if (wechats.length > 0 || !_.isEmpty(qywxs)) {
             const wx = new WX(this.ctx);
             const tenderName = await wx.contentChange(this.ctx.tender.data.name);
             const projectName = await wx.contentChange(this.ctx.tender.info.deal_info.buildName);
@@ -1120,7 +1125,8 @@ module.exports = {
                 tenderName,
             };
             const postParam = Object.assign(param, data);
-            wx.Send(wechats, template, postParam);
+            if (wechats.length > 0) wx.Send(wechats, template, postParam);
+            if (!_.isEmpty(qywxs)) wx.Send4Work(qywxs, template, postParam);
         }
     },
 
@@ -1235,7 +1241,9 @@ module.exports = {
      * @return {*}
      */
     isMobile(agent) {
-        return agent.match(/(iphone|ipod|android)/i);
+        const ua = new UAParser(agent);
+        const osInfo = ua.getOS();
+        return agent.match(/(iphone|ipod|android)/i) || osInfo.name === 'Android' || osInfo.name === 'iOS';
     },
 
     /**
@@ -1525,5 +1533,36 @@ module.exports = {
         }
         if (result.length > 0) result.unshift(key);
         return result;
-    }
+    },
+
+    // 把一个 xml 格式化成一个 json
+    async parseXML(xml) {
+        const result = await xml2js.parseStringPromise(xml, { trim: true });
+        return this.formatMessage(result.xml);
+    },
+
+    // 将xml2js解析出来的对象转换成直接可访问的对象
+    formatMessage(result) {
+        const message = {};
+        if (typeof result === 'object') {
+            for (const key in result) {
+                if (!(result[key] instanceof Array) || result[key].length === 0) {
+                    continue;
+                }
+                if (result[key].length === 1) {
+                    const val = result[key][0];
+                    if (typeof val === 'object') {
+                        message[key] = this.formatMessage(val);
+                    } else {
+                        message[key] = (val || '').trim();
+                    }
+                } else {
+                    message[key] = result[key].map(function(item) {
+                        return this.formatMessage(item);
+                    });
+                }
+            }
+        }
+        return message;
+    },
 };

+ 238 - 0
app/lib/wechat.js

@@ -12,6 +12,7 @@
 const _ = require('lodash');
 const moment = require('moment');
 const wxConst = require('../const/wechat_template.js');
+const wxWork = require('./wx_work');
 class WX {
 
     /**
@@ -209,6 +210,243 @@ class WX {
     }
 
     /**
+     * 发送微信审批通知模板
+     *
+     * @param {Array} corps - 发送的微信账号
+     * @param {String} template - 模板id
+     * @param {Object} data - 一些模板展示数据
+     * @return {Boolean} - 发送结果
+     */
+    async Send4Work(corps, template, data) {
+        let flag = false;
+        try {
+            // const sck = 'https://scn.ink/';
+            const sck = '';
+            const qywx = new wxWork(this.ctx);
+            for (const c in corps) {
+                let url = '';
+                let title = '';
+                let desc = '';
+                let content = '';
+                let msgData = '';
+                let remark = '';
+                let btntxt = '';
+                const userids = corps[c];
+                switch (template) {
+                    case wxConst.template.stage:
+                        title = '计量申请审批通知';
+                        desc = `您好, 本期计量${data.tips}`;
+                        remark = '查看审批详情';
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '状态',
+                                value: data.status,
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, 本期计量${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">计量期: 第${data.qi}期</div>
+<div class="normal">状态: ${data.status}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = this.ctx.protocol + '://' + this.ctx.host + `/wx/work/${c}/url2wap?project=` + data.code + '&url=' + sck + data.wap_url;
+                        break;
+                    case wxConst.template.change:
+                        title = '工程变更申请审批通知';
+                        desc = `您好, ${data.type ? wxConst.changeType[data.type] : '工程变更申请'}${data.tips}`;
+                        remark = data.status === wxConst.status.check ? (data.type && _.indexOf(['apply', 'project'], data.type) !== -1 ? '暂无法在线审批' : '可快速审批,如需进行详细审批') :
+                            (data.status === wxConst.status.success ? '审批已通过,查看审批结果' :
+                                (data.status === wxConst.status.back ? '审批被退回,查看退回结果' : '审批已终止,查看终止结果'));
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '变更名称',
+                                value: data.c_name,
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, ${data.type ? wxConst.changeType[data.type] : '工程变更申请'}${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">变更名称: ${data.c_name}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = data.wap_url ? this.ctx.protocol + '://' + this.ctx.host + `/wx/work/${c}/url2wap?project=` + data.code + '&url=' + sck + data.wap_url : '';
+                        btntxt = '详情';
+                        break;
+                    case wxConst.template.ledger:
+                        title = '台帐申请审批通知';
+                        desc = `您好, 台账${data.tips}`;
+                        remark = data.status === wxConst.status.check ? '暂无法在线审批' :
+                            (data.status === wxConst.status.success ? '审批已通过,查看审批结果' : '审批被退回,查看退回结果');
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '上报时间',
+                                value: moment(new Date(data.begin_time)).format('YYYY-MM-DD'),
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, 台账${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">上报时间: ${moment(new Date(data.begin_time)).format('YYYY-MM-DD')}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = this.ctx.protocol + '://' + this.ctx.host + `/wx/tips?msg=${remark},请登录PC端系统。`;
+                        break;
+                    case wxConst.template.revise:
+                        title = '台帐修订申请审批通知';
+                        desc = `您好, 台账修订${data.tips}`;
+                        remark = data.status === wxConst.status.check ? '可快速审批,如需进行详细审批' :
+                            (data.status === wxConst.status.success ? '审批已通过,查看审批结果' :
+                                (data.status === wxConst.status.back ? '审批被退回,查看退回结果' : '审批已上报,查看审批结果'));
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '上报时间',
+                                value: moment(new Date(data.begin_time)).format('YYYY-MM-DD'),
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, 台账修订${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">上报时间: ${moment(new Date(data.begin_time)).format('YYYY-MM-DD')}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = this.ctx.protocol + '://' + this.ctx.host + `/wx/work/${c}/url2wap?project=` + data.code + '&url=' + sck + data.wap_url;
+                        btntxt = '详情';
+                        break;
+                    case wxConst.template.material:
+                        title = '材料调差申请审批通知';
+                        desc = `您好, 第${data.qi}期材料调差申请${data.tips}`;
+                        remark = data.status === wxConst.status.check ? '暂无法在线审批' :
+                            (data.status === wxConst.status.success ? '审批已通过,查看审批结果' : '审批被退回,查看退回结果');
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '价差费用',
+                                value: data.m_tp ? data.m_tp.toString() : null,
+                            },
+                            {
+                                keyname: '价差费用含税',
+                                value: data.hs_m_tp ? data.hs_m_tp.toString() : null,
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, 第${data.qi}期材料调差申请${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">上报时间: ${moment(new Date(data.begin_time)).format('YYYY-MM-DD')}</div>
+<div class="normal">价差费用: ${data.m_tp ? data.m_tp.toString() : null}</div>
+<div class="normal">价差费用含税: ${data.hs_m_tp ? data.hs_m_tp.toString() : null}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = this.ctx.protocol + '://' + this.ctx.host + `/wx/tips?msg=${remark},请登录PC端系统。`;
+                        break;
+                    case wxConst.template.advance:
+                        title = '预付款申请审批通知';
+                        desc = `您好, 预付款申请${data.tips}`;
+                        remark = data.status === wxConst.status.check ? '可快速审批,如需进行详细审批' :
+                            (data.status === wxConst.status.success ? '审批已通过,查看审批结果' : '审批被退回,查看退回结果');
+                        content = [
+                            {
+                                keyname: '建设项目',
+                                value: data.projectName,
+                            },
+                            {
+                                keyname: '标段',
+                                value: data.tenderName,
+                            },
+                            {
+                                keyname: '期数',
+                                value: data.qi,
+                            },
+                            {
+                                keyname: '本期支付金额',
+                                value: data.tp ? data.tp.toString() : null,
+                            },
+                            {
+                                keyname: '备注',
+                                value: `${remark},请登录PC端系统。`,
+                            },
+                        ];
+                        msgData = `<div class="gray">您好, 预付款申请${data.tips}</div>
+<div class="normal">建设项目: ${data.projectName}</div>
+<div class="normal">标段: ${data.tenderName}</div>
+<div class="normal">期数: ${data.qi}</div>
+<div class="normal">本期支付金额: ${data.tp ? data.tp.toString() : null}</div>
+<div class="normal">备注: ${remark},请登录PC端系统。</div>`;
+                        url = this.ctx.protocol + '://' + this.ctx.host + `/wx/work/${c}/url2wap?project=` + data.code + '&url=' + sck + data.wap_url;
+                        btntxt = '详情';
+                        break;
+                    default:
+                        break;
+                }
+                if (title !== '') {
+                    console.log(userids, c, title, desc, content, url, btntxt);
+                    // await qywx.sendTextCard(userids, c, title, msgData, url, btntxt);
+                    await qywx.sendTemplateCard(userids, c, title, desc, content, url, btntxt);
+                }
+            }
+            flag = true;
+        } catch (e) {
+            console.log(e);
+            flag = false;
+        }
+        return flag;
+    }
+
+    /**
      * 关键字转换,并限制20个字以内
      *
      * @param {String} content - 内容

+ 255 - 0
app/lib/wx_work.js

@@ -0,0 +1,255 @@
+'use strict';
+// 企业微信一些方法
+
+const axios = require('axios');
+class wxWork {
+    constructor(ctx) {
+        this.ctx = ctx;
+    }
+
+    async setSuiteTicket(suite_ticket) {
+        return await this.ctx.app.redis.set('suite_ticket', suite_ticket, 'EX', 1800);// 最多半小时有效期,企业微信会自动请求刷新获取
+    }
+
+    async getSuiteTicket() {
+        try {
+            const suite_ticket = this.ctx.app.redis.get('suite_ticket');
+            if (!suite_ticket) {
+                throw '还没有设置过';
+            }
+            return suite_ticket;
+        } catch (error) {
+            console.log(error);
+            return false;
+        }
+    }
+
+    async getSuiteAccessToken() {
+        const suite_access_token = await this.ctx.app.redis.get('suite_access_token');
+        if (!suite_access_token) {
+            const suite_ticket = await this.getSuiteTicket();
+            const post_data = {
+                suite_id: this.ctx.app.config.qywx.suiteID,
+                suite_secret: this.ctx.app.config.qywx.suiteSecret,
+                suite_ticket,
+            };
+            // const response = await this.ctx.helper.sendRequest('https://qyapi.work.weixin.qq.com/cgi-bin/service/get_suite_token', post_data, 'POST');
+            // console.log(response);
+            const { data } = await axios.post('https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token', post_data);
+            const { suite_access_token, expires_in } = data;
+            if (suite_access_token) {
+                await this.ctx.app.redis.set('suite_access_token', suite_access_token, 'EX', expires_in);// 最多两个小时有效期
+                return suite_access_token;
+            }
+        }
+        return suite_access_token;
+    }
+
+    async savePermanentCode(temporary_code) {
+        return await this.ctx.app.redis.set('temporary_code', temporary_code, 'EX', 600);// 最多10分钟有效期,企业信息要入库
+    }
+
+    // 设置永久授权码(代开发授权凭证)
+    async setPermanentCode() {
+        const temporary_code = await this.ctx.app.redis.get('temporary_code');
+        if (!temporary_code) {
+            console.log('不存在授权码');
+            return false;
+        }
+        const suite_access_token = await this.getSuiteAccessToken();
+        const { data } = await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=${suite_access_token}`, {
+            auth_code: temporary_code,
+        });
+        const {
+            permanent_code,
+            auth_corp_info,
+            auth_user_info,
+            auth_info,
+        } = data;
+
+        if (permanent_code) {
+            const agentid = auth_info.agent[0].agentid;
+            const result = await this.ctx.service.wxWork.insertCorp(permanent_code, agentid, auth_corp_info, auth_user_info);
+            console.log(result, permanent_code, agentid, auth_corp_info, auth_user_info);
+            console.log(auth_corp_info.corp_name + ' 授权企业成功');
+            await this.ctx.app.redis.del('temporary_code');
+            return result;
+        }
+        console.error('获取永久授权码失败', data);
+        return false;
+    }
+    // 读取企业的通讯录 access_token
+    async getCorpTxlAccessToken(corpID) {
+        try {
+            let token = await this.ctx.app.redis.get(`${corpID}_txl_access_token`);
+            if (!token) {
+                const corpInfo = await this.ctx.service.wxWork.getDataByCondition({ corpid: corpID });
+                if (!corpInfo) {
+                    throw '未绑定该企业';
+                }
+                const { data } = await axios.get(`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpID}&corpsecret=8SZKdgpUICzj9ImEjrqHHVCD6NxEpgWqkoEb0KzzG40`);
+                const { access_token, expires_in } = data;
+                if (!access_token) {
+                    throw '获取 corp_access_token 失败';
+                }
+                await this.ctx.app.redis.set(`${corpID}_txl_access_token`, access_token, 'EX', expires_in);
+                token = access_token;
+            }
+            return token;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+
+    // 读取企业的 access_token
+    async getCorpAccessToken(corpID) {
+        try {
+            let token = await this.ctx.app.redis.get(`${corpID}_access_token`);
+            if (!token) {
+                const corpInfo = await this.ctx.service.wxWork.getDataByCondition({ corpid: corpID });
+                if (!corpInfo) {
+                    throw '未绑定该企业';
+                }
+                const { data } = await axios.get(`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpID}&corpsecret=${corpInfo.permanent_code}`);
+                const { access_token, expires_in } = data;
+                if (!access_token) {
+                    throw '获取 corp_access_token 失败';
+                }
+                await this.ctx.app.redis.set(`${corpID}_access_token`, access_token, 'EX', expires_in);
+                token = access_token;
+            }
+            return token;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+
+    // 读取企业用户信息, 只获取一次
+    async getCorpUser(token, code) {
+        try {
+            const { data: user_data } = await axios.get(`https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=${token}&code=${code}`);
+            if (user_data.errcode !== 0) {
+                throw '获取企业微信用户信息失败';
+            }
+            const user_ticket = user_data.user_ticket;
+            const { data: user_detail_data } = await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=${token}`, {
+                user_ticket,
+            });
+            if (user_detail_data.errcode !== 0) {
+                throw '获取企业微信用户详细信息失败';
+            }
+            const user_info = {
+                userid: user_data.userid,
+                avatar: user_detail_data.avatar !== undefined ? user_detail_data.avatar : null,
+                gender: user_detail_data.gender !== undefined ? user_detail_data.gender : null,
+            };
+            return user_info;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+
+    // 读取企业用户信息
+    async getCorpUserCommonData(token, userid, corpID) {
+        try {
+            const { data: user_common_data } = await axios.get(`https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=${token}&userid=${userid}`);
+            if (user_common_data.errcode !== 0) {
+                throw '获取企业微信用户详细信息失败';
+            }
+            const corpInfo = await this.ctx.service.wxWork.getDataByCondition({ corpid: corpID });
+            const user_info = {
+                userid: user_common_data.userid,
+                name: user_common_data.name,
+                department: user_common_data.department,
+                position: user_common_data.position,
+                company: corpInfo.corp_name,
+            };
+            return user_info;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+
+    // 获取通讯录
+    async getUserList(corpid) {
+        const token = await this.getCorpTxlAccessToken(corpid);
+        const { data: user_data } = await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=${token}`);
+        console.log(user_data, user_data.dept_user);
+        return user_data.dept_user;
+    }
+
+    // 发送模板卡片信息
+    async sendTemplateCard(userids, corpid, title, desc, content, url = '', btntxt = null) {
+        try {
+            const corpInfo = await this.ctx.service.wxWork.getDataByCondition({ corpid });
+            if (!corpInfo) {
+                throw '该企业不存在';
+            }
+            const token = await this.getCorpAccessToken(corpid);
+            const templateCard = {
+                card_type: 'text_notice',
+                main_title: {
+                    title,
+                    desc,
+                },
+                horizontal_content_list: content,
+            };
+            if (btntxt) {
+                templateCard.jump_list = {
+                    type: 1,
+                    title: btntxt,
+                    url,
+                };
+            }
+            templateCard.card_action = {
+                type: 1,
+                url,
+            };
+            const data = await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
+                touser: userids.join('|'),
+                msgtype: 'template_card',
+                agentid: corpInfo.agentid,
+                template_card: templateCard,
+            });
+            console.log(data);
+            return data;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+
+    // 发送文本卡片信息
+    async sendTextCard(userids, corpid, title, content, url = '', btntxt = null) {
+        try {
+            const corpInfo = await this.ctx.service.wxWork.getDataByCondition({ corpid });
+            if (!corpInfo) {
+                throw '该企业不存在';
+            }
+            const token = await this.getCorpAccessToken(corpid);
+            const textCard = {
+                title,
+                description: content,
+            };
+            if (url) textCard.url = url ? url : '';
+            if (btntxt) textCard.btntxt = btntxt;
+            const data = await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`, {
+                touser: userids.join('|'),
+                msgtype: 'textcard',
+                agentid: corpInfo.agentid,
+                textcard: textCard,
+            });
+            console.log(data);
+            return data;
+        } catch (e) {
+            console.log(e);
+            return null;
+        }
+    }
+}
+
+module.exports = wxWork;

+ 6 - 0
app/middleware/session_auth.js

@@ -74,6 +74,12 @@ module.exports = options => {
             const path = yield this.service.settingShow.getDefaultPath(this.session.sessionProject.id);
             path && (this.curListUrl = path);
 
+            // 针对非wap重定向,去掉wap
+            if (this.method === 'GET' && this.url.match(/\/wap\//) && !this.helper.isMobile(this.request.header['user-agent'])) {
+                const returnUrl = this.url.replace(/\/wap/g, '');
+                this.redirect(returnUrl);
+            }
+
         } catch (error) {
             if (this.helper.isAjax(this.request)) {
                 return this.body = {

+ 39 - 0
app/middleware/wx_work_auth.js

@@ -0,0 +1,39 @@
+'use strict';
+
+const wxWork = require('../lib/wx_work');
+module.exports = (options, app) => {
+    /**
+     * session判断中间件
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return async function wxWorkAuth(ctx, next) {
+        try {
+            // 判断session
+            const corpid = ctx.params.corpid || ctx.request.body.corpid;
+            if (!corpid) {
+                throw '参数有误';
+            }
+            const qywx = new wxWork(ctx);
+            const token = await qywx.getCorpAccessToken(corpid);
+            if (!token) {
+                throw '企业微信通信有误';
+            }
+            if (ctx.url.indexOf('/wx/work/bindwx') === -1 && !ctx.query.code) {
+                ctx.redirect(`/wx/work/${ctx.params.corpid}/oauth?redirect_uri=` + encodeURIComponent(ctx.protocol + '://' + ctx.host + ctx.request.url));
+                return;
+            }
+            // 同步系统维护信息
+            await ctx.service.maintain.syncMaintainData();
+            if (ctx.session === null) {
+                throw '系统维护中~';
+            }
+        } catch (error) {
+            console.log(error);
+            ctx.body = error;
+            return;
+        }
+        await next();
+    };
+};

+ 22 - 2
app/router.js

@@ -23,6 +23,8 @@ module.exports = app => {
     const api3managementCheck = app.middlewares.api3managementCheck();
     // 微信验证登录中间件
     const wechatAuth = app.middlewares.wechatAuth();
+    // 企业微信验证登录中间件
+    const wxWorkAuth = app.middlewares.wxWorkAuth();
     // 预付款中间件
     const advanceCheck = app.middlewares.advanceCheck();
     // 变更令中间件
@@ -281,6 +283,7 @@ module.exports = app => {
     // 期计量详细
     // 本期计量台账
     app.get('/tender/:id/measure/stage/:order', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.index');
+    app.get('/tender/:id/stage/:order', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.index');// 针对旧数据wap端跳转web问题
     app.post('/tender/:id/measure/stage/:order/load', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.getStageData');
     app.post('/tender/:id/measure/stage/:order/pos', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.getStagePosData');
     app.post('/tender/:id/measure/stage/:order/update', sessionAuth, tenderCheck, uncheckTenderCheck, stageCheck, 'stageController.updateStageData');
@@ -448,7 +451,6 @@ module.exports = app => {
     app.post('/tender/:id/change/newCode', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.newCode');
     app.post('/tender/:id/change/add', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.add');
     app.post('/tender/:id/change/defaultBills', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.defaultBills');
-    app.get('/tender/:id/change/:cid/info', sessionAuth, tenderCheck, uncheckTenderCheck, changeAuditCheck, 'changeController.info');
     app.post('/tender/:id/change/:cid/info/file/upload', sessionAuth, 'changeController.uploadFile');
     app.get('/change/download/file/:id', sessionAuth, 'changeController.downloadFile');
     app.post('/change/download/file/:id', sessionAuth, 'changeController.checkFile');
@@ -469,6 +471,7 @@ module.exports = app => {
     app.post('/change/update/company', sessionAuth, 'changeController.updateCompany');
 
     // 变更令 - 新版本
+    app.get('/tender/:id/change/:cid/info', sessionAuth, tenderCheck, uncheckTenderCheck, changeCheck, changeAuditCheck, 'changeController.information');// 针对旧数据wap端跳转web问题
     app.get('/tender/:id/change/:cid/information', sessionAuth, tenderCheck, uncheckTenderCheck, changeCheck, changeAuditCheck, 'changeController.information');
     app.post('/tender/:id/change/:cid/information/save', sessionAuth, tenderCheck, uncheckTenderCheck, changeCheck, 'changeController.saveListsData');
     app.post('/tender/:id/change/:cid/information/audit/start', sessionAuth, tenderCheck, uncheckTenderCheck, changeCheck, changeAuditCheck, 'changeController.startAudit');
@@ -517,6 +520,7 @@ module.exports = app => {
     app.get('/tender/:id/change/plan/status/:status', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.planStatus');
     app.post('/tender/:id/change/plan/add', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.planAdd');
     app.post('/tender/:id/change/plan/delete', sessionAuth, tenderCheck, uncheckTenderCheck, 'changeController.planDelete');
+    app.get('/tender/:id/change/plan/:cpid/info', sessionAuth, tenderCheck, uncheckTenderCheck, changePlanCheck, 'changeController.planInformation');// 针对旧数据wap端跳转web问题
     app.get('/tender/:id/change/plan/:cpid/information', sessionAuth, tenderCheck, uncheckTenderCheck, changePlanCheck, 'changeController.planInformation');
     app.post('/tender/:id/change/plan/:cpid/information/save', sessionAuth, tenderCheck, uncheckTenderCheck, changePlanCheck, 'changeController.planInformationSave');
     app.post('/tender/:id/change/plan/:cpid/information/file/upload', sessionAuth, tenderCheck, uncheckTenderCheck, changePlanCheck, 'changeController.uploadPlanFile');
@@ -622,6 +626,10 @@ module.exports = app => {
     app.get('/wap/tender/:id/revise/:rid/info', sessionAuth, tenderCheck, uncheckTenderCheck, 'wapController.revise');
     app.get('/wap/tender/:id/advance', sessionAuth, tenderCheck, uncheckTenderCheck, 'wapController.advance');
     app.get('/wap/tender/:id/advance/:order/detail', sessionAuth, tenderCheck, advanceCheck, 'wapController.advanceDetail');
+    // 针对企业微信访问判断去掉wap就能直达web端
+    app.get('/wap/tender/:id/measure/stage/:order', sessionAuth, tenderCheck, uncheckTenderCheck, 'wapController.stage');
+    app.get('/wap/tender/:id/change/:cid/information', sessionAuth, tenderCheck, uncheckTenderCheck, 'wapController.change');
+    app.get('/wap/tender/:id/change/plan/:cpid/information', sessionAuth, tenderCheck, uncheckTenderCheck, 'wapController.changePlan');
 
     // 微信
     app.get('/wx', 'wechatController.index');
@@ -697,5 +705,17 @@ module.exports = app => {
     app.post('/budget/:id/decimal', sessionAuth, budgetCheck, 'budgetController.decimal');
 
     // 支付审批
-    app.get('/payment/:rid/detail/:id', sessionAuth, 'paymentController.detail');
+    // app.get('/payment/:rid/detail/:id', sessionAuth, 'paymentController.detail');
+
+    // 企业微信回调
+    app.get('/wx/work/callback/command', 'wechatController.command');
+    app.post('/wx/work/callback/command', 'wechatController.postCommand');
+    app.get('/WW_verify_CZwGPbI7BRGOBUX1.txt', 'wechatController.oauthWxWorkTxt');// 可信域名配置
+    app.get('/wx/work/:corpid/oauth', 'wechatController.workOauth');
+    app.get('/wx/work/:corpid/bind', wxWorkAuth, 'wechatController.workBind');
+    app.post('/wx/work/bindwx', wxWorkAuth, 'wechatController.workBindwx');
+    app.get('/wx/work/:corpid/url2wap', wxWorkAuth, 'wechatController.url2wap4work');
+    app.get('/wx/work/:corpid/project', wxWorkAuth, 'wechatController.workProject');
+    app.get('/wx/work/:corpid/test', wxWorkAuth, 'wechatController.workTest');
+    app.get('/wx/tips', 'wechatController.tips');
 };

+ 6 - 6
app/service/change.js

@@ -567,7 +567,7 @@ module.exports = app => {
                             const sms = new SMS(this.ctx);
                             const code = await sms.contentChange(changeInfo.code);
                             const shenpiUrl = await this.ctx.helper.urlToShort(
-                                this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/info#shenpi'
+                                this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/information#shenpi'
                             );
                             await this.ctx.helper.sendAliSms(auditInfo[0], smsTypeConst.const.BG, smsTypeConst.judge.approval.toString(), SmsAliConst.template.change_check, {
                                 biangeng: code,
@@ -827,7 +827,7 @@ module.exports = app => {
                     });
                     // 微信模板通知
                     const shenpiUrl = await this.ctx.helper.urlToShort(
-                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/info#shenpi'
+                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/information#shenpi'
                     );
                     const wechatData = {
                         wap_url: shenpiUrl,
@@ -851,7 +851,7 @@ module.exports = app => {
                     const sms = new SMS(this.ctx);
                     const code = await sms.contentChange(changeData.code);
                     const shenpiUrl = await this.ctx.helper.urlToShort(
-                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/info#shenpi'
+                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/information#shenpi'
                     );
                     await this.ctx.helper.sendAliSms(nextAuditData.uid, smsTypeConst.const.BG, smsTypeConst.judge.approval.toString(), SmsAliConst.template.change_check, {
                         biangeng: code,
@@ -1020,7 +1020,7 @@ module.exports = app => {
                 });
                 // 微信模板通知
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/information#shenpi'
                 );
                 const wechatData = {
                     wap_url: shenpiUrl,
@@ -1162,7 +1162,7 @@ module.exports = app => {
                 const sms = new SMS(this.ctx);
                 const code = await sms.contentChange(changeData.code);
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeData.tid + '/change/' + changeData.cid + '/information#shenpi'
                 );
                 await this.ctx.helper.sendAliSms(lastauditInfo.uid, smsTypeConst.const.BG, smsTypeConst.judge.approval.toString(), SmsAliConst.template.change_check, {
                     biangeng: code,
@@ -1560,7 +1560,7 @@ module.exports = app => {
                 const sms = new SMS(this.ctx);
                 const code = await sms.contentChange(changeInfo.code);
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + changeInfo.tid + '/change/' + changeInfo.cid + '/information#shenpi'
                 );
                 await this.ctx.helper.sendAliSms(auditInfo.uid, smsTypeConst.const.BG, smsTypeConst.judge.approval.toString(), SmsAliConst.template.change_check, {
                     biangeng: code,

+ 1 - 1
app/service/change_audit.js

@@ -650,7 +650,7 @@ module.exports = app => {
                 const sms = new SMS(this.ctx);
                 const code = await sms.contentChange(this.ctx.change.code);
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.change.tid + '/change/' + cid + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.change.tid + '/change/' + cid + '/information#shenpi'
                 );
                 await this.ctx.helper.sendAliSms(audit.uid, smsTypeConst.const.BG, smsTypeConst.judge.approval.toString(), SmsAliConst.template.change_check, {
                     biangeng: code,

+ 4 - 4
app/service/change_plan_audit.js

@@ -272,7 +272,7 @@ module.exports = app => {
                 });
                 // 微信模板通知
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/information#shenpi'
                 );
                 const wechatData = {
                     type: 'plan',
@@ -426,7 +426,7 @@ module.exports = app => {
 
                     // 微信模板通知
                     const shenpiUrl = await this.ctx.helper.urlToShort(
-                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/info#shenpi'
+                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/information#shenpi'
                     );
                     const wechatData = {
                         type: 'plan',
@@ -449,7 +449,7 @@ module.exports = app => {
                     // 微信模板通知
                     const users = this._.uniq(this._.concat(this._.map(auditors, 'aid'), this.ctx.change.uid));
                     const shenpiUrl = await this.ctx.helper.urlToShort(
-                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/info#shenpi'
+                        this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/information#shenpi'
                     );
                     const wechatData = {
                         type: 'plan',
@@ -507,7 +507,7 @@ module.exports = app => {
                 // 微信模板通知
                 const users = this._.uniq(this._.concat(this._.map(auditors, 'aid'), this.ctx.change.uid));
                 const shenpiUrl = await this.ctx.helper.urlToShort(
-                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/info#shenpi'
+                    this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/change/plan/' + cpId + '/information#shenpi'
                 );
                 const wechatData = {
                     type: 'plan',

+ 33 - 0
app/service/project_account.js

@@ -835,6 +835,39 @@ module.exports = app => {
         }
 
         /**
+         * 账号绑定企业微信
+         *
+         * @param {String} id - 账号id
+         * @param {String} corpid - 企业id
+         * @param {String} userid - 企业微信用户id
+         * @param {String} user_info - 用户信息
+         * @return {Boolean} - 返回修改结果
+         */
+        async bindWx4Work(id, corpid, userid, user_info) {
+            const wx_type = {};
+            for (const key in smsTypeConst) {
+                if (smsTypeConst.hasOwnProperty(key)) {
+                    const type = smsTypeConst[key];
+                    wx_type[key] = [`${type.children[0].value}`];
+                }
+            }
+
+            const updateData = {
+                id,
+                qywx_corpid: corpid,
+                qywx_userid: userid,
+                qywx_user_info: user_info ? JSON.stringify(user_info) : null,
+                wx_type: JSON.stringify(wx_type),
+            };
+
+            const operate = await this.db.update(this.tableName, updateData);
+
+            const result = operate.affectedRows > 0;
+
+            return result;
+        }
+
+        /**
          * 获取项目下所有账号
          * @param {String} project_id - 项目id
          * @return {Promise<Array>} - 账号

+ 7 - 7
app/service/stage_audit.js

@@ -320,7 +320,7 @@ module.exports = app => {
 
                 // 添加短信通知-需要审批提醒功能
                 const stageInfo = await this.ctx.service.stage.getDataById(audit.sid);
-                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                 const users = this._.map(this.ctx.stage.auditAssists.filter(x => { return x.user_id === audit.aid }), 'ass_user_id');
                 users.push(audit.aid);
                 await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JL, smsTypeConst.judge.approval.toString(), SmsAliConst.template.stage_check, {
@@ -450,7 +450,7 @@ module.exports = app => {
 
                     // 添加短信通知-需要审批提醒功能
                     const stageInfo = await this.ctx.service.stage.getDataById(nextAudit.sid);
-                    const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                    const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                     const users = this._.map(this._.filter(this.ctx.auditAssists, function (x) { return x.user_id === nextAudit.id; }), 'ass_user_id');
                     users.push(nextAudit.aid);
                     await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JL, smsTypeConst.judge.approval.toString(), SmsAliConst.template.stage_check, {
@@ -501,7 +501,7 @@ module.exports = app => {
                     });
 
                     // 微信模板通知
-                    const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                    const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                     const wechatData = {
                         wap_url: shenpiUrl,
                         qi: stageInfo.order,
@@ -629,7 +629,7 @@ module.exports = app => {
                     status: SmsAliConst.status.back,
                 });
                 // 微信模板通知
-                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                 const wechatData = {
                     wap_url: shenpiUrl,
                     qi: stageInfo.order,
@@ -774,7 +774,7 @@ module.exports = app => {
                     cache_time_r: this.ctx.stage.cache_time_l,
                 });
                 const stageInfo = await this.ctx.service.stage.getDataById(audit.sid);
-                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                 const users = this._.map(this.ctx.stage.auditAssists.filter(x => {return x.user_id === preAuditor.aid}), 'ass_user_id');
                 users.push(preAuditor.aid);
                 await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JL, smsTypeConst.judge.approval.toString(), SmsAliConst.template.stage_check, {
@@ -933,13 +933,13 @@ module.exports = app => {
                 //         const tenderName = await sms.contentChange(tenderInfo.name);
                 //         const projectName = await sms.contentChange(this.ctx.tender.info.deal_info.buildName);
                 //         const ptmsg = projectName !== '' ? '项目「' + projectName + '」标段「' + tenderName + '」' : tenderName;
-                //         const result = await this.ctx.helper.urlToShort('http://' + this.ctx.request.header.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                //         const result = await this.ctx.helper.urlToShort('http://' + this.ctx.request.header.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                 //         const content = '【纵横计量支付】' + ptmsg + '第' + stageInfo.order + '期,需要您审批。' + result;
                 //         sms.send(smsUser.auth_mobile, content);
                 //     }
                 // }
                 const stageInfo = await this.ctx.service.stage.getDataById(audit.sid);
-                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/stage/' + stageInfo.order);
+                const shenpiUrl = await this.ctx.helper.urlToShort(this.ctx.protocol + '://' + this.ctx.host + '/wap/tender/' + this.ctx.tender.id + '/measure/stage/' + stageInfo.order);
                 const users = this._.map(this.ctx.stage.auditAssists.filter(x => { return x.user_id == audit.aid; }), 'ass_user_id');
                 users.push(audit.aid);
                 await this.ctx.helper.sendAliSms(users, smsTypeConst.const.JL, smsTypeConst.judge.approval.toString(), SmsAliConst.template.stage_check, {

+ 76 - 0
app/service/wx_work.js

@@ -0,0 +1,76 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Ellisran
+ * @date 2020/7/6
+ * @version
+ */
+
+
+module.exports = app => {
+    class WxWork extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'wx_work';
+        }
+
+        async insertCorp(permanent_code, agentid, auth_corp_info, auth_user_info) {
+            const corpInfo = await this.getDataByCondition({ corpid: auth_corp_info.corpid });
+            const pushData = {
+                agentid,
+                corpid: auth_corp_info.corpid,
+                corp_name: auth_corp_info.corp_name,
+                permanent_code,
+                auth_user_info: JSON.stringify(auth_user_info),
+                in_time: new Date(),
+            };
+            if (corpInfo) {
+                pushData.id = corpInfo.id;
+                return await this.db.update(this.tableName, pushData);
+            }
+            return this.db.insert(this.tableName, pushData);
+        }
+
+        async delCorp(corpid) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const corpInfo = await this.getDataByCondition({ corpid });
+                if (corpInfo) {
+                    // 删除用户已绑定企业微信信息
+                    const userList = await this.ctx.service.projectAccount.getAllDataByCondition({ where: { qywx_corpid: corpid } });
+                    if (userList.length !== 0) {
+                        const updateUserList = [];
+                        for (const u of userList) {
+                            updateUserList.push({
+                                id: u.id,
+                                qywx_corpid: null,
+                                qywx_userid: null,
+                                qywx_user_info: null,
+                            });
+                        }
+                        await transaction.updateRows(this.ctx.service.projectAccount.tableName, updateUserList);
+                    }
+                    await transaction.delete(this.tableName, { id: corpInfo.id });
+                } else {
+                    throw '该企业已删除';
+                }
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                console.log(err);
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+    return WxWork;
+}

+ 98 - 47
app/view/profile/wechat.ejs

@@ -8,59 +8,110 @@
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
-            <div class="row m-0">
-                <div class="col-5 my-3">
-                    <% if (accountData.wx_openid !== null && accountData.wx_openid !== '') { %>
-                    <!--已绑定手机-->
-                    <div class="form-group">
-                        <label>微信账号</label>
-                        <input class="form-control-plaintext" disabled="" value="<%= accountData.wx_name %>">
-                        <a href="#remove-wechat" class="btn btn-sm btn-outline-primary" data-toggle="modal" data-target="#remove-wechat">解绑</a>
-                    </div>
-                    <% } else { %>
-                    <div class="form-group">
-                        <label>微信账号</label>
-                        <input class="form-control-plaintext" disabled="" value="未绑定">
-                    </div>
-                    <% } %>
-                    <!--二维码-->
-                    <div class="form-group">
-                        <label>扫码或搜索 关注服务号</label>
-                        <div><img class="w-100" src="/public/images/wechat.png"></div>
-                    </div>
-                    <% if (accountData.wx_openid !== null && accountData.wx_openid !== '') { %>
-                    <!--短信通知开关(已有认证手机后显示)-->
-                    <div class="mt-5">
-                        <h4>通知类型</h4>
-                        <p class="text-muted">勾选您需要接收的微信类型。</p>
-                        <form id="sms-form" method="post" action="/profile/sms/type">
-                            <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
-                            <% const user_wxType = accountData.wx_type !== '' ? JSON.parse(accountData.wx_type) : null; %>
-                            <% for (const s in smsType) { %>
-                            <% if (smsType[s].wechat) { %>
-                            <div class="form-group row">
-                                <label class="col-2 col-form-label"><%= smsType[s].name %>
-                                    <!--<a href="#sms-view" data-toggle="modal" data-target="#sms-view" class="ml-2"><i class="fa fa-info-circle"></i></a>-->
-                                </label>
-                                <div class="col-3">
-                                    <% for (const c of smsType[s].children) { %>
-                                    <div class="form-check ">
-                                        <input class="form-check-input" id="<%= s %>_<%- c.value %>" type="checkbox" name="<%= s %>[]" value="<%= c.value %>" <% if (user_wxType !== null && user_wxType[s] !== undefined && user_wxType[s].indexOf(c.value.toString()) !== -1) { %>checked<% } %>>
-                                        <label class="form-check-label" for="<%= s %>_<%- c.value %>"><%= c.title %></label>
-                                    </div>
+                <div class="row m-0 mt-3">
+                    <div class="col-6">
+                        <div class="card mb-3">
+                            <div class="card-body pt-3">
+                                <h6 class="card-title">个人微信</h6>
+                                <div class="ml-3">
+                                    <% if (accountData.wx_openid !== null && accountData.wx_openid !== '') { %>
+                                        <!--已绑定手机-->
+                                        <div class="form-group">
+                                            <label>微信账号</label>
+                                            <input class="form-control-plaintext" disabled="" value="<%= accountData.wx_name %>">
+                                            <a href="#remove-wechat" class="btn btn-sm btn-outline-primary" data-toggle="modal" data-target="#remove-wechat">解绑</a>
+                                        </div>
+                                    <% } else { %>
+                                        <div class="form-group">
+                                            <label>微信账号</label>
+                                            <input class="form-control-plaintext" disabled="" value="未绑定">
+                                        </div>
                                     <% } %>
+                                    <!--二维码-->
+                                    <div class="form-group">
+                                        <label>扫码或搜索 关注服务号</label>
+                                        <div><img class="w-100" src="/public/images/wechat.png"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="card mb-3">
+                            <div class="card-body pt-3">
+                                <h6 class="card-title">企业微信</h6>
+                                <form class="ml-3">
+                                    <!--已绑定手机-->
+                                    <p>打开企业微信,在工作台页面,进入“纵横云计量”应用,绑定项目,即可使用企业微信进行相关计量业务审批。 </p>
+                                    <p>如无“纵横云计量”应用,请先联系贵司企业微信管理员进行添加。</p>
+                                </form>
+                                <% if (accountData.qywx_userid !== null) { %>
+                                    <div class="ml-3">
+                                        <label>企业微信账号</label>
+                                        <div class="form-group row">
+                                            <label for="uname" class="ml-3 col-form-label">姓名:</label>
+                                            <div class="col-sm-10">
+                                                <input type="text" disabled class="form-control-plaintext" value="<%= accountData.qywx_user_info.name %>">
+                                            </div>
+                                        </div>
+                                        <div class="form-group row">
+                                            <label for="uname" class="ml-3 col-form-label">单位:</label>
+                                            <div class="col-sm-10">
+                                                <input type="text" disabled class="form-control-plaintext" value="<%= accountData.qywx_user_info.company %>">
+                                            </div>
+                                        </div>
+                                        <div class="form-group row">
+                                            <label for="uname" class="ml-3 col-form-label">职位:</label>
+                                            <div class="col-sm-10">
+                                                <input type="text" disabled class="form-control-plaintext" value="<%= accountData.qywx_user_info.position %>">
+                                            </div>
+                                        </div>
+                                        <a href="#remove-wxWork" class="btn btn-sm btn-outline-primary" data-toggle="modal" data-target="#remove-wxWork">解绑</a>
+                                    </div>
+                                <% } else { %>
+                                    <div class="form-group ml-3">
+                                        <label>企业微信账号</label>
+                                        <!--<p>未绑定</p>-->
+                                        <input class="form-control-plaintext" disabled="" value="未绑定">
+                                    </div>
+                                <% } %>
+                            </div>
+                        </div>
+                        <% if ((accountData.wx_openid !== null && accountData.wx_openid !== '') || accountData.qywx_userid !== null) { %>
+                            <!--短信通知开关(已有认证手机后显示)-->
+                            <div class="card mb-3">
+                                <div class="card-body pt-3">
+                                    <h6 class="card-title">通知类型</h6>
+                                    <div class="ml-3">
+                                        <p class="text-muted">勾选您需要接收的微信类型。</p>
+                                        <form id="sms-form" method="post" action="/profile/sms/type">
+                                            <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                                            <% const user_wxType = accountData.wx_type !== '' ? JSON.parse(accountData.wx_type) : null; %>
+                                            <% for (const s in smsType) { %>
+                                                <% if (smsType[s].wechat) { %>
+                                                    <div class="form-group form-group-sm row">
+                                                        <label class="col-3 col-form-label"><%= smsType[s].name %>
+                                                            <!--<a href="#sms-view" data-toggle="modal" data-target="#sms-view" class="ml-2"><i class="fa fa-info-circle"></i></a>-->
+                                                        </label>
+                                                        <div class="col-5">
+                                                            <% for (const c of smsType[s].children) { %>
+                                                                <div class="form-check ">
+                                                                    <input class="form-check-input" id="<%= s %>_<%- c.value %>" type="checkbox" name="<%= s %>[]" value="<%= c.value %>" <% if (user_wxType !== null && user_wxType[s] !== undefined && user_wxType[s].indexOf(c.value.toString()) !== -1) { %>checked<% } %>>
+                                                                    <label class="form-check-label" for="<%= s %>_<%- c.value %>"><%= c.title %></label>
+                                                                </div>
+                                                            <% } %>
+                                                        </div>
+                                                    </div>
+                                                <% } %>
+                                            <% } %>
+                                            <input name="type" value="0" type="hidden">
+                                            <button type="submit" class="btn btn-primary btn-sm">确认修改</button>
+                                        </form>
+                                    </div>
                                 </div>
                             </div>
-                            <% } %>
-                            <% } %>
-                            <input name="type" value="0" type="hidden">
-                            <button type="submit" class="btn btn-primary btn-sm">确认修改</button>
-                        </form>
+                        <% } %>
                     </div>
-                    <% } %>
                 </div>
             </div>
-            </div>
         </div>
     </div>
 </div>

+ 24 - 0
app/view/profile/wechat_modal.ejs

@@ -14,6 +14,30 @@
             </div>
             <div class="modal-footer">
                 <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                <input type="hidden" name="data_type" value="wechat">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="submit" class="btn btn-sm btn-primary">确定解绑</button>
+            </div>
+        </form>
+    </div>
+</div>
+<!--短信图示-->
+<div class="modal fade" id="remove-wxWork" >
+    <div class="modal-dialog" role="document">
+        <form class="modal-content" method="post" action="/profile/wechat/remove">
+            <div class="modal-header">
+                <h5 class="modal-title">解绑企业微信账号</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <h6>确认解绑企业微信号 <%- accountData.qywx_userid ? accountData.qywx_user_info.name : '' %>?</h6>
+                <h6>解绑后无法在企业微信接收通知。</h6>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>">
+                <input type="hidden" name="data_type" value="wxWork">
                 <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
                 <button type="submit" class="btn btn-sm btn-primary">确定解绑</button>
             </div>

+ 3 - 3
app/view/wap/dashboard.ejs

@@ -61,7 +61,7 @@
                             </table>
                         </div>
                         <div class="">
-                            <a href="/wap/tender/<%- audit.tid %>/stage/<%- audit.order %>" class="btn btn-block btn-success">审批</a>
+                            <a href="/wap/tender/<%- audit.tid %>/measure/stage/<%- audit.order %>" class="btn btn-block btn-success">审批</a>
                         </div>
                     </div>
                 </div>
@@ -106,7 +106,7 @@
                                 </table>
                             </div>
                             <div class="">
-                                <a href="/wap/tender/<%- change.tid %>/change/<%- change.cid %>/info#shenpi" class="btn btn-block btn-success">审批</a>
+                                <a href="/wap/tender/<%- change.tid %>/change/<%- change.cid %>/information#shenpi" class="btn btn-block btn-success">审批</a>
                             </div>
                         </div>
                     </div>
@@ -128,7 +128,7 @@
                                 </table>
                             </div>
                             <div class="">
-                                <a href="/wap/tender/<%- change.tid %>/change/plan/<%- change.id %>/info#shenpi" class="btn btn-block btn-success">审批</a>
+                                <a href="/wap/tender/<%- change.tid %>/change/plan/<%- change.id %>/information#shenpi" class="btn btn-block btn-success">审批</a>
                             </div>
                         </div>
                     </div>

+ 1 - 1
app/view/wap/tender.ejs

@@ -194,7 +194,7 @@
                             <% if (s.curAuditor && s.status == auditConst.status.checking && s.curAuditor.aid === ctx.session.sessionUser.accountId) { %>
                             <tr>
                                 <td colspan="2">
-                                    <a class="btn btn-block btn-success" href="/wap/tender/<%- s.tid %>/stage/<%- s.order %>">审批本期</a>
+                                    <a class="btn btn-block btn-success" href="/wap/tender/<%- s.tid %>/measure/stage/<%- s.order %>">审批本期</a>
                                 </td>
                             </tr>
                             <% } %>

+ 79 - 0
app/view/wechat/work_bind.ejs

@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>账号绑定-计量支付</title>
+    <link rel="stylesheet" href="/public/css/bootstrap/bootstrap.min.css">
+    <link rel="stylesheet" href="/public/css/wap/main.css">
+    <link rel="stylesheet" href="/public/css/toast.css">
+    <link rel="stylesheet" href="/public/css/font-awesome/font-awesome.min.css">
+    <link rel="shortcut icon" href="/public/images/favicon.ico">
+    <style>
+        html{height:100%;}
+    </style>
+</head>
+<body class="login-body">
+<div class="container">
+    <!--演示版-->
+    <div class="row">
+        <div class="col-12 text-center">
+            <img src="/public/images/loginlogo.png" class="my-3">
+        </div>
+        <div class="col-12">
+            <% if (maintainData.status === maintainConst.status.ongoing) { %>
+            <form class="card m-3 mt-3">
+                <div class="card-body">
+                    <h4 class="text-center mb-3"><i class="fa fa-wrench"></i>系统正在维护</h4>
+                    <h4>预计恢复时间<%- (maintainData.duration !== maintainConst.duration.forever ? '为 ' + ctx.helper.dateTran(parseFloat(maintainData.maintain_time) + ctx.helper.timeAdd(maintainData.duration)) : ' 暂未确定') %></h4>
+                    <h4>造成不便敬请谅解。</h4>
+                </div>
+            </form>
+            <% } else { %>
+            <form class="card m-3 mt-3" method="post" action="/wx/work/bindwx">
+                <div class="card-body">
+                    <h5 class="text-center mb-4 text-muted" id="project_name"></h5>
+                    <h4 class="text-center mb-4">账号绑定</h4>
+                    <div class="form-group mb-3 <% if (errorMessage !== undefined && errorMessage !== null) { %>has-danger<% } %>">
+                        <input id="project" class="form-control" name="project" placeholder="项目编号" autofocus="" />
+                    </div>
+                    <div class="form-group mb-3 <% if (errorMessage !== undefined && errorMessage !== null) { %>has-danger<% } %>">
+                        <input id="account" class="form-control" name="account" placeholder="输入账号" autofocus="" />
+                    </div>
+                    <div class="form-group mb-3 <% if (errorMessage !== undefined && errorMessage !== null) { %>has-danger<% } %>">
+                        <input id="project-password" name="project_password" class="form-control" placeholder="输入密码" type="password" />
+                    </div>
+                    <div class="form-group mb-3">
+                        <div class="alert alert-danger" <% if(errorMessage === undefined || errorMessage === null) { %>style="display: none"<% } %> role="alert" id="error-msg">
+                            <% if(errorMessage !== undefined && errorMessage !== null) { %><strong>绑定失败</strong> <%= errorMessage %><% } %>
+                        </div>
+                    </div>
+                    <div class="form-group mb-3">
+                        <button class="btn btn-primary btn-block" type="submit">绑定企业微信</button>
+                        <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                        <input type="hidden" name="corpid" value="<%= corpid %>">
+                        <input type="hidden" name="userid" value="<%= user.userid %>">
+                        <input type="hidden" name="avatar" value="<%= user.avatar %>">
+                        <input type="hidden" name="gender" value="<%= user.gender %>">
+                    </div>
+                </div>
+            </form>
+            <% } %>
+        </div>
+    </div>
+    <!--项目版-->
+</div>
+<!-- JS. -->
+<div class="toast" style="text-align: center">
+    <i class="icon fa"></i>
+    <span class="message"></span>
+</div>
+<!-- JS. -->
+<script src="/public/js/jquery/jquery-3.2.1.min.js"></script>
+<script src="/public/js/popper/popper.min.js"></script>
+<script src="/public/js/bootstrap/bootstrap.min.js"></script>
+<script src="/public/js/wap/global.js"></script>
+<script src="/public/js/wechat/bind.js"></script>
+</body>
+</html>

+ 69 - 0
app/view/wechat/work_project.ejs

@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>项目列表-计量支付</title>
+    <link rel="stylesheet" href="/public/css/bootstrap/bootstrap.min.css">
+    <link rel="stylesheet" href="/public/css/wap/main.css">
+    <link rel="stylesheet" href="/public/css/toast.css">
+    <link rel="stylesheet" href="/public/css/font-awesome/font-awesome.min.css">
+    <link rel="shortcut icon" href="/public/images/favicon.ico">
+</head>
+<body>
+<div class="container">
+    <!--顶部-->
+    <nav class="fixed-top bg-dark">
+        <div class="my-2 d-flex justify-content-between">
+            <span class="text-white ml-3">项目列表</span>
+            <div class="mr-3">
+                <!-- <div class="dropdown">
+                  <button class="btn btn-sm btn-light dropdown-toggle" type="button" data-toggle="dropdown">
+                    张三
+                  </button>
+                  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                    <a class="dropdown-item" href="#">退出登录</a>
+                  </div>
+                </div> -->
+            </div>
+        </div>
+    </nav>
+    <% if (maintainData.status === maintainConst.status.ongoing) { %>
+        <form class="card m-3 mt-3">
+            <div class="card-body">
+                <h4 class="text-center mb-3"><i class="fa fa-wrench"></i>系统正在维护</h4>
+                <h4>预计恢复时间<%- (maintainData.duration !== maintainConst.duration.forever ? '为 ' + ctx.helper.dateTran(parseFloat(maintainData.maintain_time) + ctx.helper.timeAdd(maintainData.duration)) : ' 暂未确定') %></h4>
+                <h4>造成不便敬请谅解。</h4>
+            </div>
+        </form>
+    <% } else { %>
+    <!--待审批期列表-->
+    <div class="py-6">
+        <p class="text-muted">您的企业微信已绑定以下项目:</p>
+        <% for (const p of pList) { %>
+        <div class="card mb-3">
+            <div class="card-header d-flex justify-content-between">
+                <span><%- p.name %></span>
+            </div>
+            <div class="d-flex justify-content-between">
+                <div class=" p-2 px-3">项目编号:<%- p.code %></div>
+                <div class=""><a href="/wx/work/<%- corpid %>/url2wap?project=<%- p.code %>&url=<%- redirect_url %>" class="btn btn-block btn-link">进入项目</a></div>
+            </div>
+        </div>
+        <% } %>
+    </div>
+    <% } %>
+</div>
+<!-- JS. -->
+<div class="toast" style="text-align: center">
+    <i class="icon fa"></i>
+    <span class="message"></span>
+</div>
+<!-- JS. -->
+<script src="/public/js/jquery/jquery-3.2.1.min.js"></script>
+<script src="/public/js/popper/popper.min.js"></script>
+<script src="/public/js/bootstrap/bootstrap.min.js"></script>
+<script src="/public/js/wap/global.js"></script>
+</body>
+</html>

+ 8 - 0
config/config.default.js

@@ -209,6 +209,14 @@ module.exports = appInfo => {
         appsecret: '457d64c55f48f57cd22eca47e53d15cb',
     };
 
+    // 企业微信代开发模板信息
+    config.qywx = {
+        suiteID: 'dk10a5f6b6a68d6ed4',
+        suiteSecret: 'TWmb3K-74VGv-X-ugox3T3yGPc_aa0ci4uNkIlmMaRA',
+        token: 'NRPyXeKObE3Nesc',
+        encodingAESKey: 'a6zuXvcHQlgdyY8465AbVpMpSKF0HMf0aMMxRthuOiq',
+    };
+
     config.proxy = true;
 
     config.hisOssPath = 'prod/';

+ 8 - 0
config/config.uat.js

@@ -59,6 +59,14 @@ module.exports = appInfo => {
         appsecret: 'ca7c0dbd9e94dc3b1c3b0e73865743f4',
     };
 
+    // 企业微信代开发模板信息(uat版本)
+    config.qywx = {
+        suiteID: 'dka00967439b7dcdad',
+        suiteSecret: 'w_i_VDjKcGquwWCZdpFEcxK52TvaI2z6eXvZB28Hm84',
+        token: 'ab0IV95Hm5exQ',
+        encodingAESKey: 'hNuILriaRQZYaNiUNCCyUphBO38N5VXkP135IsRAEDX',
+    };
+
     config.hisOssPath = 'uat/';
     config.stashOssPath = 'stash/uat/';
     config.oss = {

+ 62 - 17
package-lock.json

@@ -370,6 +370,11 @@
             "integrity": "sha512-OMSKUmZ09gbzITzx4nxnJqhprWC7JqsmlrEsVtb+cv3GXHNpv0kktqxhboKX52FnMggkQvT5ezt8pxTWyKpJHA==",
             "dev": true
         },
+        "@wecom/crypto": {
+            "version": "1.0.1",
+            "resolved": "http://192.168.1.90:4873/@wecom%2fcrypto/-/crypto-1.0.1.tgz",
+            "integrity": "sha512-K4Ilkl1l64ceJDbj/kflx8ND/J88pcl8tKx4Ivp7IiCrshRJU+Uo5uWCjAa+PjUiLIdcQSZ4m4d0t1npMPCX5A=="
+        },
         "a-sync-waterfall": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.0.tgz",
@@ -1050,7 +1055,7 @@
                 },
                 "isarray": {
                     "version": "1.0.0",
-                    "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz?cache=0&sync_timestamp=1562592096220&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fisarray%2Fdownload%2Fisarray-1.0.0.tgz",
+                    "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
                     "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
                 },
                 "normalize-path": {
@@ -1441,6 +1446,41 @@
             "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
             "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
         },
+        "axios": {
+            "version": "1.3.4",
+            "resolved": "http://192.168.1.90:4873/axios/-/axios-1.3.4.tgz",
+            "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
+            "requires": {
+                "follow-redirects": "^1.15.0",
+                "form-data": "^4.0.0",
+                "proxy-from-env": "^1.1.0"
+            },
+            "dependencies": {
+                "combined-stream": {
+                    "version": "1.0.8",
+                    "resolved": "http://192.168.1.90:4873/combined-stream/-/combined-stream-1.0.8.tgz",
+                    "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+                    "requires": {
+                        "delayed-stream": "~1.0.0"
+                    }
+                },
+                "form-data": {
+                    "version": "4.0.0",
+                    "resolved": "http://192.168.1.90:4873/form-data/-/form-data-4.0.0.tgz",
+                    "integrity": "sha1-k5Gdrq82HuUpWEubMWZNwSyfpFI=",
+                    "requires": {
+                        "asynckit": "^0.4.0",
+                        "combined-stream": "^1.0.8",
+                        "mime-types": "^2.1.12"
+                    }
+                },
+                "proxy-from-env": {
+                    "version": "1.1.0",
+                    "resolved": "http://192.168.1.90:4873/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+                    "integrity": "sha1-4QLxbKNVQkhldV0sno6k8k1Yw+I="
+                }
+            }
+        },
         "axobject-query": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz",
@@ -2754,7 +2794,7 @@
         },
         "buffer-crc32": {
             "version": "0.2.13",
-            "resolved": "https://registry.npm.taobao.org/buffer-crc32/download/buffer-crc32-0.2.13.tgz",
+            "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
             "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
         },
         "buffer-equal": {
@@ -2989,7 +3029,7 @@
         },
         "charenc": {
             "version": "0.0.2",
-            "resolved": "https://registry.npm.taobao.org/charenc/download/charenc-0.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
             "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
         },
         "chokidar": {
@@ -3220,12 +3260,12 @@
             "dependencies": {
                 "sax": {
                     "version": "0.6.1",
-                    "resolved": "http://registry.npm.taobao.org/sax/download/sax-0.6.1.tgz",
+                    "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz",
                     "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk="
                 },
                 "xml2js": {
                     "version": "0.4.4",
-                    "resolved": "https://registry.npm.taobao.org/xml2js/download/xml2js-0.4.4.tgz?cache=0&sync_timestamp=1584990425260&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxml2js%2Fdownload%2Fxml2js-0.4.4.tgz",
+                    "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz",
                     "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=",
                     "requires": {
                         "sax": "0.6.x",
@@ -3261,7 +3301,7 @@
         },
         "co-wechat-oauth": {
             "version": "2.0.1",
-            "resolved": "https://registry.npm.taobao.org/co-wechat-oauth/download/co-wechat-oauth-2.0.1.tgz",
+            "resolved": "https://registry.npmjs.org/co-wechat-oauth/-/co-wechat-oauth-2.0.1.tgz",
             "integrity": "sha1-n/pS3fuTWdAwAYiBw/jWIQSKKVQ=",
             "requires": {
                 "httpx": "^2.1.1"
@@ -3269,7 +3309,7 @@
         },
         "co-wechat-payment": {
             "version": "0.2.0",
-            "resolved": "https://registry.npm.taobao.org/co-wechat-payment/download/co-wechat-payment-0.2.0.tgz",
+            "resolved": "https://registry.npmjs.org/co-wechat-payment/-/co-wechat-payment-0.2.0.tgz",
             "integrity": "sha1-6V2uUqYEPHDZADDbHzPQFg0hEkA=",
             "requires": {
                 "md5": "^2.1.0",
@@ -3767,7 +3807,7 @@
         },
         "crypt": {
             "version": "0.0.2",
-            "resolved": "https://registry.npm.taobao.org/crypt/download/crypt-0.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
             "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
         },
         "csrf": {
@@ -6916,6 +6956,11 @@
                 }
             }
         },
+        "follow-redirects": {
+            "version": "1.15.2",
+            "resolved": "http://192.168.1.90:4873/follow-redirects/-/follow-redirects-1.15.2.tgz",
+            "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+        },
         "fontkit": {
             "version": "1.8.0",
             "resolved": "https://registry.npm.taobao.org/fontkit/download/fontkit-1.8.0.tgz",
@@ -9879,17 +9924,17 @@
         },
         "lodash.defaults": {
             "version": "4.2.0",
-            "resolved": "https://registry.npm.taobao.org/lodash.defaults/download/lodash.defaults-4.2.0.tgz",
+            "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
             "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
         },
         "lodash.difference": {
             "version": "4.5.0",
-            "resolved": "https://registry.npm.taobao.org/lodash.difference/download/lodash.difference-4.5.0.tgz",
+            "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
             "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
         },
         "lodash.flatten": {
             "version": "4.4.0",
-            "resolved": "https://registry.npm.taobao.org/lodash.flatten/download/lodash.flatten-4.4.0.tgz",
+            "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
             "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
         },
         "lodash.includes": {
@@ -9914,7 +9959,7 @@
         },
         "lodash.isplainobject": {
             "version": "4.0.6",
-            "resolved": "https://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz",
+            "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
             "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
         },
         "lodash.isstring": {
@@ -9929,7 +9974,7 @@
         },
         "lodash.union": {
             "version": "4.6.0",
-            "resolved": "https://registry.npm.taobao.org/lodash.union/download/lodash.union-4.6.0.tgz",
+            "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
             "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
         },
         "long": {
@@ -10335,7 +10380,7 @@
         },
         "md5": {
             "version": "2.2.1",
-            "resolved": "https://registry.npm.taobao.org/md5/download/md5-2.2.1.tgz",
+            "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
             "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
             "requires": {
                 "charenc": "~0.0.1",
@@ -13711,7 +13756,7 @@
         },
         "sha1": {
             "version": "1.1.1",
-            "resolved": "https://registry.npm.taobao.org/sha1/download/sha1-1.1.1.tgz",
+            "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
             "integrity": "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=",
             "requires": {
                 "charenc": ">= 0.0.1",
@@ -14885,7 +14930,7 @@
         },
         "ueditor": {
             "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/ueditor/-/ueditor-1.0.0.tgz",
+            "resolved": "https://registry.npmjs.org/ueditor/-/ueditor-1.2.3.tgz",
             "integrity": "sha1-HsihLT8VutOkAReGpzpIZJY2twQ=",
             "requires": {
                 "busboy": "^0.2.9",
@@ -15391,7 +15436,7 @@
         },
         "wechat-crypto": {
             "version": "0.0.2",
-            "resolved": "https://registry.npm.taobao.org/wechat-crypto/download/wechat-crypto-0.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/wechat-crypto/-/wechat-crypto-0.0.2.tgz",
             "integrity": "sha1-pVRD7AgQ9MGZKik6J4YIOXAMY2k="
         },
         "whatwg-fetch": {

+ 2 - 0
package.json

@@ -5,9 +5,11 @@
     "private": true,
     "dependencies": {
         "@alicloud/pop-core": "^1.7.9",
+        "@wecom/crypto": "^1.0.1",
         "ali-rds": "^3.3.0",
         "archiver": "^5.0.2",
         "atob": "^2.1.2",
+        "axios": "^1.3.4",
         "bignumber.js": "^8.1.1",
         "decimal.js": "^10.2.0",
         "egg": "^1.13.0",

+ 15 - 0
sql/update.sql

@@ -0,0 +1,15 @@
+ALTER TABLE `zh_project_account` ADD `qywx_corpid` VARCHAR(255) NULL DEFAULT NULL COMMENT '已绑定的企业微信id' AFTER `wx_name`;
+ALTER TABLE `zh_project_account` ADD `qywx_userid` VARCHAR(255) NULL DEFAULT NULL COMMENT '已绑定企业微信用户id' AFTER `qywx_corpid`;
+ALTER TABLE `zh_project_account` ADD `qywx_user_info` VARCHAR(1000) NULL DEFAULT NULL COMMENT '已绑定的企业微信用户信息' AFTER `qywx_userid`;
+
+CREATE TABLE `zh_wx_work` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `agentid` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '应用id',
+  `corpid` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '企业id',
+  `corp_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '企业名称',
+  `permanent_code` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '企业secret',
+  `auth_user_info` varchar(1000) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '授权人信息json',
+  `in_time` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  NIQUE KEY `corpid` (`corpid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='企业微信信息表';