收藏
评论

平台型服务商代实现企业官网小程序(云开发版)官方

一、帐号准备

1 第三方平台类型选择:

平台型服务商

2 权限选择:

1)开发管理与数据分析权限

2)云开发管理权限集

3 开发配置

1)登录授权的发起页域名:w.cloudbase.vip

2)授权事件接收URL:wx.cloudbase.vip/call

注意:在这里只是教程演示的URL,请根据自己的域名设置


请根据以上内容在微信开放平台注册一个第三方平台帐号,具体流程可以参照第一章节的步骤。







二、基础信息设置

1 开发环境搭建

1.1 开通云开发CloudBase,并设定域名

云开发CloudBase是由腾讯云推出的云原生一体化多端研发平台,用户无需管理底层架构,可以直接开展业务开发。我们的第三方平台(注意不是小程序本身)使用云开发来构建。如果你想参照服务器版本,请在教程最后下载服务器版本的DEMO,代码细节一致。

开通云开发CloudBase,创建按量计费环境,进入静态网站托管页面的【基础配置】,添加自定义域名。需要注意,此处的自定义域名需要与第三方平台设置的授权发起页域名保持一致。

在本教程中,设定域名为w.cloudbase.vip

1.2 设定授权事件url域名

在控制台中点击【环境-云接入】,进入云接入配置页,配置第三方平台设置的授权事件接收URL所在的域名,在本教程中,设定的授权事件url域名为wx.cloudbase.vip

云接入是使用标准的 HTTP 协议访问环境内全部资源的一种能力,包括云函数服务,通过对应的触发域名以及云函数的触发路径进行http访问,便可以使用云函数的服务。具体可以参考:云接入文档


2 创建授权事件接收URL的服务监听

安装云开发vscode插件,在vscode中,创建一个云函数,命名为call。

创建后,我们开始进行代码的编写,首先在index.js文件中编写如下代码

const tcb = require("@cloudbase/node-sdk");

const cloud = tcb.init({
    env: "" //需要填写自己的云开发环境id
});
const db = cloud.database();
const _ = db.command;

exports.main = async (event) => {
    //从event中可以获取到HTTP请求的有关信息

    //event.body即为请求体
    let msg_body = event.body;

    /*event.queryStringParameters中可以获得请求参数,取出以下三个内容
     - timestamp 时间戳
     - nonce 随机数
     - msg_signature 消息体签名,用于验证消息体的正确 */
    let { msg_signature, nonce, timestamp } = event.queryStringParameters;

    //判断签名是否不为空,过滤一些非开放平台的无效请求
    if (msg_signature != null) {

        //针对信息进行base64解码
        let encryptedMsg = Buffer.from(msg_body, 'base64').toString();

        //取出加密的encrypt,在这里没有使用XML方式读取
        let encrypt = encryptedMsg.slice(encryptedMsg.indexOf(''));

        //引入util.js文件,命名为WechatEncrypt,此js包含解码的所有逻辑
        const WechatEncrypt = require('./util');

        //引入key.json,在这里存储了第三方平台设置的key,随机码,appid等
        const WXKEY = require('./key.json');

        //将key.json的内容代入,创建WechatEncrypt实例
        const wechatEncrypt = new WechatEncrypt(WXKEY);

        //将timestamp 时间戳、nonce 随机数、加密的encrypt代入gensign函数进行签名处理
        let signature = wechatEncrypt.genSign({ timestamp, nonce, encrypt });

        //判断签名是否和传来的参数签名一致
        if (signature === msg_signature) {

            //将加密的encrypt直接代入decode函数进行解码,返回解码后的明文
            let xml = wechatEncrypt.decode(encrypt);

            //判断明文中是否有ComponentVerifyTicket字段,由此来判断此为验证票据
            if (xml.indexOf('ComponentVerifyTicket') != -1) {

                //取出相应的票据,在这里没有使用XML方式读取
                let ticket = xml.slice(xml.indexOf('ticket@@@'), xml.indexOf(']]>'));
                try {

                    //将票据信息保存到云开发数据库中wxid集合中,component_verify_ticket文档中,在使用前需要在控制台创建集合并设置文档
                    db.collection('wxid').doc('component_verify_ticket').update({
                        time: db.serverDate(),//更新的时间
                        value: ticket
                    });
                }
                catch (e) {
                    console.log('save failed!', e);
                }
            }
            return 'success';
        }
        else {
            return 'error';
        }

    }
    else {
        return 404;
    }
}

在index.js同级目录下,创建util.js,此为解码的主要逻辑文件,编写代码如下:

const crypto = require('crypto')

const ALGORITHM = 'aes-256-cbc'     // 使用的加密算法
const MSG_LENGTH_SIZE = 4           // 存放消息体尺寸的空间大小。单位:字节
const RANDOM_BYTES_SIZE = 16        // 随机数据的大小。单位:字节
const BLOCK_SIZE = 32               // 分块尺寸。单位:字节

let data = {
    appId: '',                      // 微信公众号 APPID
    token: '',                      // 消息校验 token
    key: '',                        // 加密密钥
    iv: ''                          // 初始化向量
}

const Encrypt = function (params) {

    let { appId, encodingAESKey, token } = params
    let key = Buffer.from(encodingAESKey + '=', 'base64')       // 解码密钥
    let iv = key.slice(0, 16)                                   // 初始化向量为密钥的前16字节
    Object.assign(data, { appId, token, key, iv })

}

Encrypt.prototype = {
    /**
     * 加密消息
     * @param {string} msg 待加密的消息体
     */
    encode(msg) {
        let { appId, key, iv } = data
        let randomBytes = crypto.randomBytes(RANDOM_BYTES_SIZE)                     // 生成指定大小的随机数据

        let msgLenBuf = Buffer.alloc(MSG_LENGTH_SIZE)                               // 申请指定大小的空间,存放消息体的大小
        let offset = 0                                                              // 写入的偏移值
        msgLenBuf.writeUInt32BE(Buffer.byteLength(msg), offset)                     // 按大端序(网络字节序)写入消息体的大小

        let msgBuf = Buffer.from(msg)                                               // 将消息体转成 buffer
        let appIdBuf = Buffer.from(appId)                                           // 将 APPID 转成 buffer

        let totalBuf = Buffer.concat([randomBytes, msgLenBuf, msgBuf, appIdBuf])    // 将16字节的随机数据、4字节的消息体大小、若干字节的消息体、若干字节的APPID拼接起来

        let cipher = crypto.createCipheriv(ALGORITHM, key, iv)                      // 创建加密器实例
        cipher.setAutoPadding(false)                                                // 禁用默认的数据填充方式
        totalBuf = this.PKCS7Encode(totalBuf)                                       // 使用自定义的数据填充方式
        let encryptdBuf = Buffer.concat([cipher.update(totalBuf), cipher.final()])  // 加密后的数据

        return encryptdBuf.toString('base64')                                       // 返回加密数据的 base64 编码结果
    },

    /**
     * 解密消息
     * @param {string} encryptdMsg 待解密的消息体
     */
    decode(encryptdMsg) {
        let { key, iv } = data
        let encryptedMsgBuf = Buffer.from(encryptdMsg, 'base64')                                // 将 base64 编码的数据转成 buffer
        let decipher = crypto.createDecipheriv(ALGORITHM, key, iv)                              // 创建解密器实例
        decipher.setAutoPadding(false)                                                          // 禁用默认的数据填充方式
        let decryptdBuf = Buffer.concat([decipher.update(encryptedMsgBuf), decipher.final()])   // 解密后的数据

        decryptdBuf = this.PKCS7Decode(decryptdBuf)                                             // 去除填充的数据

        let msgSize = decryptdBuf.readUInt32BE(RANDOM_BYTES_SIZE)                               // 根据指定偏移值,从 buffer 中读取消息体的大小,单位:字节
        let msgBufStartPos = RANDOM_BYTES_SIZE + MSG_LENGTH_SIZE                                // 消息体的起始位置
        let msgBufEndPos = msgBufStartPos + msgSize                                             // 消息体的结束位置

        let msgBuf = decryptdBuf.slice(msgBufStartPos, msgBufEndPos)                            // 从 buffer 中提取消息体

        return msgBuf.toString()                                                                // 将消息体转成字符串,并返回数据
    },

    /**
     * 生成签名
     * @param {Object} params 待签名的参数
     */
    genSign(params) {
        let { token } = data
        let { timestamp, nonce, encrypt } = params;
        let rawStr = [token,timestamp,nonce,encrypt].sort().join('')                            // 原始字符串
        let signature = crypto.createHash('sha1').update(rawStr).digest('hex')                  // 计算签名
        return signature
    },

    /**
     * 按 PKCS#7 的方式从填充过的数据中提取原数据
     * @param {Buffer} buf 待处理的数据
     */
    PKCS7Decode(buf) {
        let padSize = buf[buf.length - 1]                       // 最后1字节记录着填充的数据大小
        return buf.slice(0, buf.length - padSize)               // 提取原数据
    },

    /**
     * 按 PKCS#7 的方式填充数据结尾
     * @param {Buffer} buf 待填充的数据
     */
    PKCS7Encode(buf) {
        let padSize = BLOCK_SIZE - (buf.length % BLOCK_SIZE)    // 计算填充的大小。
        let fillByte = padSize                                  // 填充的字节数据为填充的大小
        let padBuf = Buffer.alloc(padSize, fillByte)            // 分配指定大小的空间,并填充数据
        return Buffer.concat([buf, padBuf])                     // 拼接原数据和填充的数据
    }
}

module.exports = Encrypt

在index.js同级目录下创建key.json,里面保存有关设置信息

{
    "appId": "第三方平台详情页中展示的appid",
    "encodingAESKey": "在创建过程中设置的消息加解密Key",
    "token": "在创建过程中设置的消息校验Token"
}

按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk crypto

在call文件夹中右键,点击部署并上传全部文件,云函数即可部署成功。

前往云开发CloudBase控制台,在云函数操作页下找到部署的call云函数,点击进入函数详情页,点击右上角编辑,进入设置页,配置云接入路径为/call

于是,我们可以通过wx.cloudbase.vip/call这个地址来接收请求。微信开放平台将每隔10分钟左右就向此url发送请求(因为我们在第三方平台创建时填写的此url),此云函数便可以完成请求的解析和解密存储操作。具体效果如下:


3 使用接收到的验证票据(component_verify_ticket)获取令牌

在vscode中,我们创建云函数getComToken,按照之前创建的步骤正常创建即可。在index.js编写如下代码:

const tcb = require("@cloudbase/node-sdk");
const request = require('request');
//获取相关的第三方平台信息
const { component_appid, component_appsecret } = require('./key.json');

const cloud = tcb.init({
    env: ""//此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的http请求函数
function CallWeb(ticket) {
    return new Promise((resolve, reject) => {
        request({
            url: 'https://api.weixin.qq.com/cgi-bin/component/api_component_token',//请求的API地址
            body: JSON.stringify({
                component_appid,
                component_appsecret,
                component_verify_ticket: ticket
            }),//传递的所需参数
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    console.log(event);
    try {
        //由于令牌有一定时效性,所以我们没必要每一次都要请求,而是将令牌保存重复利用,我们将令牌保存在wxid集合中的component_access_token文档里

        //首先取出文档的信息
        let access_token = (await db.collection('wxid').doc('component_access_token').get()).data[0];

        //以当前时间的往后一分钟来作为上限时间
        let overtime = new Date((new Date()).valueOf() + 60 * 1000);

        //如果文档的令牌超时时间大于上限时间,则证明令牌还有效,直接返回令牌
        if (access_token.time > overtime) {
            return access_token.value;
        }
        else {
            //如果小于则证明令牌过期,需要重新申请
            console.log('token timeover!');
            try {
                //取出ticket票据信息
                let ticket = (await db.collection('wxid').doc('component_verify_ticket').get()).data[0];
                //将票据信息传入http请求函数,等待请求结果
                let result = await CallWeb(ticket.value);

                //结果是一个json字符串,验证是否有component_access_token字样,如果有则没有报错
                if (result.indexOf('component_access_token') != -1) {

                    //解析字符串为json
                    let { component_access_token, expires_in } = JSON.parse(result);
                    try {
                        //更新令牌,并设定超时时间为当前时间的有效时效后,expires_in为有效秒数
                        await db.collection('wxid').doc('component_access_token').update({
                            time: db.serverDate({
                                offset: expires_in * 1000
                            }),
                            value: component_access_token
                        });
                        //返回新的令牌
                        return component_access_token;
                    }
                    catch (e) {
                        console.log('access save failed!', e);
                        return null;
                    }
                }
                else {
                    console.log('wxcall failed!', result);
                    return null;
                }
            } catch (e) {
                console.log('ticket failed!', e);
                return null;
            }
        }
    }
    catch (e) {
        console.log('access get failed!', e);
        return null;
    }
}

在index.js同级目录下创建key.json文件,里面保存需要的信息

{
    "component_appid": "第三方平台 appid",
    "component_appsecret": "第三方平台 appsecret"
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk request

右键上传部署云函数,转到腾讯云官网控制台,进入函数的编辑设置页,按照截图进行设置并保存。

保存后会出现固定的ip地址,如下图所示:

将此处的ip地址填写到第三方平台的白名单中,修改保存

获取令牌的接口云函数,具体效果如下:

注意事项

云开发的云函数可以设置固定IP地址,这样就可以直接将IP地址填写到第三方平台白名单中。在同一环境中所有云函数共用一个IP地址,无需重复设置。

温馨提示:以js来演示消息解密过程,其中util文件中仍然包含加密函数,有需要的同学可以自行研究使用,util无需修改直接可用,如果你想使用其他语言版本,请在官方文档中下载代码示例







三、授权流程配置

1 使用令牌获取预授权码并拼接用户授权链接

根据官方文档-获取预授权码的描述,我们需要component_access_token(第三方平台令牌)、component_appid(第三方平台appid),API接口为:https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=${component_access_token}

使用vscode创建一个云函数,命名为getPreAuth,按照之前正常的创建过程创建即可。在index.js中编写如下代码:

const tcb = require("@cloudbase/node-sdk");
const request = require('request');
//获取相关的第三方平台信息
const { component_appid } = require('./key.json');

const cloud = tcb.init({
    env: ""//此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的http请求函数
function CallWeb(token) {
    return new Promise((resolve, reject) => {
        request({
            url: 'https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=' + token,
            body: JSON.stringify({
                component_appid
            }),
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    //此云函数是由后台管理页面请求的,在调用此云函数时就意味着想要创建一个授权的对象,name是对这个对象的备注
    if(event.name==null||event.name=='') return null;
    try {
        //通过调用getComToken云函数获取第三方令牌
        let access_token = (await cloud.callFunction({ name: 'getComToken' })).result;

        if (access_token != null) {
            //将令牌信息传入http请求函数,等待请求结果
            let result = await CallWeb(access_token);

            //结果是一个json字符串,验证是否有pre_auth_code字样,如果有则没有报错
            if (result.indexOf('pre_auth_code') != -1) {

                //解析字符串为json
                let { pre_auth_code, expires_in } = JSON.parse(result);

                //拼接授权链接,根据此文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Authorization_Process_Technical_Description.html
                //其中redirect_uri填写发起授权的url,这在之后配置
                let auth_url = `https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=${component_appid}&pre_auth_code=${pre_auth_code}&redirect_uri=https://w.cloudbase.vip&auth_type=2`;

                //将授权链接保存到云开发数据库中的mini集合内
                return await db.collection('mini').add({
                    status: 0,
                    name:event.name,
                    url: auth_url,
                    time: db.serverDate({
                        offset: expires_in * 1000
                    })
                })
            }
            else {
                console.log('wxcall failed!', result);
                return null;
            }
        }
        else {
            console.log('token get failed');
            return null;
        }
    }
    catch (e) {
        console.log('get failed!', e);
        return null;
    }
}

在index.js同级目录下创建key.json文件,里面保存需要的信息

{
    "component_appid": "第三方平台 appid"
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk request

右键上传部署云函数,转到腾讯云官网控制台,配置开启固定IP

完成后,可以在控制台按照如下方法进行测试,具体效果如下:

我们可以保存返回结果中的id值,这将在下一步骤中用到。


2 通过授权发起页域名打开授权链接引导用户扫码

由于授权链接需要从设置的发起页域名中跳转才有效,所以我们需要编写一个html文档,部署到静态网站托管服务中,由此来模拟真实业务场景下用户自助授权的整个流程。

我们创建一个html文档,编写如下代码:

    客户授权



    这是微信第三方平台测试——客户端接口

        let msg_text = document.getElementById('text');
        let app = tcb.init({
            env: ''//此处填写自己的云开发环境id
        });
        let auth = app.auth();
        //加载后直接使用云开发匿名登录方式进行登录,此处需要在控制台中开启匿名登录
        auth.anonymousAuthProvider().signIn().then(() => {
            console.log('匿名登录成功!');
            //拉取url中的参数
            let query = getQueryString();
            //如果存在do这个参数,则执行逻辑
            if (query.do != null) {
                msg_text.innerText = "拉取授权页面中……"
                //使用参数信息,其实就是上一步保存数据库返回的文档id,请求云函数authGetOne
                app.callFunction({
                    name: 'authGetOne',
                    data: {
                        id: query.do
                    }
                }).then(res => {
                    console.log(res);
                    //存在数据
                    if (res.result.data.length != 0) {
                        //状态设置0为未授权,1为已授权
                        if(res.result.data[0].status==0){
                            //保存id信息,因为授权后要返回此界面,需要后续解析
                            window.localStorage.setItem('open_auth_id', query.do);
                            //跳转url
                            window.location = res.result.data[0].url;
                        }
                        else{
                            msg_text.innerText = "已授权!"
                        }
                    }
                    else {
                        msg_text.innerText = "授权路径不存在!"
                    }
                });
            }
        });
        // 获取url的请求参数
        function getQueryString() {
            var qs = location.search.substr(1),
                args = {},
                items = qs.length ? qs.split("&") : [],
                item = null,
                len = items.length;
            for (var i = 0; i < len; i++) {
                item = items[i].split("=");
                var name = decodeURIComponent(item[0]),
                    value = decodeURIComponent(item[1]);
                if (name) {
                    args[name] = value;
                }
            }
            return args;
        }
    


将此html命名为index.html,上传至环境的静态网站托管中。并需要在环境配置登录鉴权中开启匿名登录

在vscode中创建云函数,命名为authGetOne,按照之前正常的创建即可。在index.js中编写如下代码

const tcb = require("@cloudbase/node-sdk");

const cloud = tcb.init({
    env: ""//填写自己云开发环境的ID
});
const db = cloud.database();

exports.main = async event => {
    if(event.id!=null){
        //field控制取出哪些变量,敏感信息不外泄
        return await db.collection('mini').where({_id:event.id}).field({
            url:true,
            status:true
        }).get();
    }
    else{
        return null;
    }
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk

右键上传部署云函数。

我们就可以使用w.cloudbase.vip?do=+(上一步保存的id)这个链接来实现授权页面的跳转了。(w.cloudbase.vip为本教程专用,实际实践需要改写为自己的)






3 获取第三方调用令牌

3.1 根据用户授权后的授权码获取用户授权账号的信息

用户授权后,会在回调的页面中回传授权码,具体表现如下:

其中auth_code参数即为授权码的信息,我们改造一下index.html,在if逻辑中添加else if逻辑

//加载后直接使用云开发匿名登录方式进行登录,此处需要在控制台中开启匿名登录
        auth.anonymousAuthProvider().signIn().then(() => {
            console.log('匿名登录成功!');
            //拉取url中的参数
            let query = getQueryString();
            //如果存在do这个参数,则执行逻辑
            if (query.do != null) {
                msg_text.innerText = "拉取授权页面中……"
                //使用参数信息,其实就是上一步保存数据库返回的文档id,请求云函数authGetOne
                app.callFunction({
                    name: 'authGetOne',
                    data: {
                        id: query.do
                    }
                }).then(res => {
                    console.log(res);
                    //存在数据
                    if (res.result.data.length != 0) {
                        //状态设置0为未授权,1为已授权
                        if(res.result.data[0].status==0){
                            //保存id信息,因为授权后要返回此界面,需要后续解析
                            window.localStorage.setItem('open_auth_id', query.do);
                            //跳转url
                            window.location = res.result.data[0].url;
                        }
                        else{
                            msg_text.innerText = "已授权!"
                        }
                    }
                    else {
                        msg_text.innerText = "授权路径不存在!"
                    }
                });
            }
            //如果存在auth_code这个变量
            else if (query.auth_code != null) {
                msg_text.innerText = "校验中……"
                //取出保存的id信息
                let temp_id = window.localStorage.getItem('open_auth_id');
                if (temp_id != null) {
                    //执行authUpdateOne进行授权信息获取
                    app.callFunction({
                        name: 'authUpdateOne',
                        data: {
                            code: query.auth_code,
                            id: temp_id
                        }
                    }).then(res => {
                        console.log(res);
                        if (res.result!= null) {
                            //授权信息获取成功后,删除无用的id信息
                            window.localStorage.removeItem('open_auth_id');
                            console.log(res.result);
                            msg_text.innerText = "校验成功!";
                        }
                        else {
                            msg_text.innerText = "校验失败,请刷新此页面重试!"
                        }
                    });
                }
                else {
                    msg_text.innerText = "路径已经刷新,请联系平台!"
                }
            }
        });

将此html重新上传至静态网站托管

在vscode中创建云函数,命名为authUpdateOne,按照之前正常的创建即可。在index.js中编写如下代码

const tcb = require("@cloudbase/node-sdk");
const request = require('request');
//获取相关的第三方平台信息
const { component_appid } = require('./key.json');

const cloud = tcb.init({
    env: ""//此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的http请求函数
function CallWeb(token,code) {
    return new Promise((resolve, reject) => {
        request({
            //获取授权信息的API
            url: 'https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=' + token,
            body: JSON.stringify({
                component_appid,
                authorization_code:code
            }),
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    if(event.code==null||event.id==null) return null;
    try {
        //通过调用getComToken云函数获取第三方令牌
        let access_token = (await cloud.callFunction({ name: 'getComToken' })).result;

        if (access_token != null) {
             //将令牌信息和授权码传入http请求函数,等待请求结果
            let result = await CallWeb(access_token,event.code);

            console.log(result);

            //结果是一个json字符串,验证是否有authorization_info字样,如果有则没有报错
            if (result.indexOf('authorization_info') != -1) {
                //解析字符串为json
                let { authorization_info } = JSON.parse(result);

                //取出字段
                let { authorizer_access_token,authorizer_appid,authorizer_refresh_token,expires_in,func_info} = authorization_info;

                //存储到mini中相应的文档里,并置状态为1
                return await db.collection('mini').doc(event.id).update({
                    status:1,
                    time:db.serverDate(),
                    func_info,
                    access_token:authorizer_access_token,
                    access_time:db.serverDate({offset: expires_in * 1000}),
                    appid:authorizer_appid,
                    refresh_token:authorizer_refresh_token
                });
            }
            else {
                console.log('wxcall failed!', result);
                return null;
            }
        }
        else {
            console.log('token get failed');
            return null;
        }
    }
    catch (e) {
        console.log('get failed!', e);
        return null;
    }
}

在index.js同级目录下创建key.json文件,里面保存需要的信息

{
    "component_appid": "第三方平台 appid"
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk request

右键上传部署云函数,转到腾讯云官网控制台,配置开启固定IP

具体效果如下图所示:

以上是在用户的角度来看到的,在第三方平台角度看到的信息如下,我们可以获得授权账户的appid、接口令牌、刷新令牌、授权集信息。

由于授权用户可以自主修改授权的信息,所以我们需要判断用户授权的授权集是否满足我们的业务开发要求,在下图字段func_info我们可以获得权限集合的列表,我们应该有相应的判断,来进行复核确认,官方文档中已经清楚的写明了。

一切确定了之后,我们便可以根据这些来进行后续的开发操作了。


3.2 过期的授权令牌如何重新刷新

在官方文档中描述,每次提供令牌时,都附带刷新令牌,我们需要通过刷新令牌来去获取最新的令牌,由此反复。

需要注意的是,要好好保存刷新令牌,如果丢失需要用户重新授权才可以。

以下是关于重复获取令牌的云函数示例,具体部署细则参照前面的云函数

const tcb = require("@cloudbase/node-sdk");
const request = require('request');
const { component_appid } = require('./key.json');

const cloud = tcb.init({
    env: ""
});
const db = cloud.database();
const _ = db.command;

function CallWeb(refresh_token, access_token, auth_appid) {
    return new Promise((resolve, reject) => {
        request({
            url: 'https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=' + access_token,
            body: JSON.stringify({
                component_appid: component_appid,
                authorizer_appid: auth_appid,
                authorizer_refresh_token: refresh_token
            }),
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    console.log(event);
    if(event.appid==null)return;
    try {
        let auth_data = (await db.collection('mini').where({ appid: event.appid }).get()).data;

        if (auth_data.length != 0) {

            let { access_token, access_time, refresh_token } = auth_data[0];
            let overtime = new Date((new Date()).valueOf() + 60 * 1000);

            if (access_time > overtime) {
                return access_token;
            }
            else {
                console.log('token timeover!');
                let access_token = (await cloud.callFunction({ name: 'getComToken' })).result;

                if (access_token != null) {
                    let result = await CallWeb(refresh_token, access_token, event.appid);
                    console.log(result);

                    if (result.indexOf('authorizer_access_token') != -1) {
                        let { authorizer_access_token, authorizer_refresh_token, expires_in } = JSON.parse(result);
                        await db.collection('mini').where({appid:event.appid}).update({
                            access_token: authorizer_access_token,
                            access_time: db.serverDate({ offset: expires_in * 1000 }),
                            refresh_token: authorizer_refresh_token
                        });
                        return authorizer_access_token
                    }
                    else {
                        console.log('wxcall failed!', result);
                        return null;
                    }
                }
                else {
                    console.log('token get failed');
                    return null;
                }
            }
        }
        else {
            console.log('can not get appid!');
            return null;
        }
    }
    catch (e) {
        console.log('access get failed!', e);
        return null;
    }
}

我们可以通过此云函数直接获取令牌,如果令牌过期会自动刷新令牌并保存,以备下一次使用。



四、 使用小程序云开发构建小程序模版

我们开始开发编写一个小程序,为了能够较为完整的演示所有可能性,此小程序是一个使用后端服务的企业展示小程序,具体效果如下:

此小程序用到云开发,所有内容均来自云开发数据库,并且通过云函数进行更新操作。

具体代码可以点击这里下载,其中关键的逻辑信息如下:

小程序页面index.js代码如下:

const app = getApp()
var that = null;

Page({
  onShow() {
    //加载本地存储
    let data = wx.getStorageSync('data');
    //如果没有存储,直接请求云端数据
    if(data==null||data==""){
      this.init();
    }
    else{
      //有数据即直接部署数据
      this.setData(data);
      wx.setNavigationBarTitle({
        title: data.name,
      })
    }
  },
  //云端请求数据,云开发云函数
  init(){
    that = this;
    wx.cloud.callFunction({
      name: "test",//云函数名称为test
      success(res) {
        //部署数据,设置标题栏内容
        if(res.result.name!=null){
          that.setData(res.result);
          wx.setStorageSync('data', res.result);
          wx.setNavigationBarTitle({
            title: res.result.name,
          })
        }
      },
      fail(e){
        wx.showModal({
          title:"测试失败",
          content:JSON.stringify(e)
        });
      }
    })
  },
  telcall(){
    wx.makePhoneCall({
      phoneNumber: this.data.tel,
    })
  }
})

云函数test逻辑代码如下:

const cloud = require('wx-server-sdk');
cloud.init();
const db = cloud.database();
const _ = db.command;

exports.main = async (event, context) => {
  try{
    // 读取数据库中的INFO集合的base_info文档
    let FAS = await db.collection('INFO').doc('base_info').get();
    // 附加的一个计数,每次调用就加一
    await db.collection('INFO').doc('base_info').update({
      data:{
        see:_.inc(1)
      }
    });
    // 返回数据
    return FAS.data;
  }
  catch(e){
    return {
      _id:null
    };
  }
}

在开发完毕后,即可上传到第三方平台的草稿箱,参照第一章节的步骤将其转化为小程序模版,即可使用模版ID进行下面的部署步骤了。







五、小程序模版部署到小程序中(上传代码与提审)

1 小程序模版部署到授权小程序

在这里我们将使用第三方平台的API接口来执行部署过程。由于所有第三方API接口都需要在白名单中才可以调用,所以我们创建一个云函数来实现。

打开vscode,创建一个云函数,命名为uploadCode,按照之前的指引正常操作即可,打开index.js编写如下代码:

const tcb = require("@cloudbase/node-sdk");
const request = require('request');

const cloud = tcb.init({
    env: "" //此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的http请求函数
function CallWeb(access_token, data) {
    return new Promise((resolve, reject) => {
        request({
            //上传小程序代码的API
            url: `https://api.weixin.qq.com/wxa/commit?access_token=${access_token}`,
            body: JSON.stringify(data),
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    //取出传参的appid和企业名称(业务)
    let { appid,name } = event;
    if (appid != null && name != null) {
        //传入appid,获得相应的操作令牌
        let access_token = (await cloud.callFunction({
            name: 'getAuthToken',
            data: {
                appid: appid
            }
        })).result;
        if (access_token != null) {
            //传入令牌,以及参数
            let result = await CallWeb(access_token, {
                template_id : "2",//模板id,要换成自己的真实的
                user_version : "1.0.1",//版本号,自定义
                user_desc : "第三方平台代开发",//描述
                ext_json : `{
                    "extEnable": true,
                    "extAppid": "${appid}",
                    "directCommit": false,
                    "window":{
                      "navigationBarTitleText": "${name}"
                    }}`//这里是特殊配置,会覆盖小程序代码原有配置,比如在这里将标题栏名字设置为企业名字了
            });
            try{
                let res = JSON.parse(result);
                console.log(res);
                return res;
            }
            catch(e){
                console.log(e,result);
                return result;
            }
        }
        else {
            return {
                code: -1,
                msg: 'access_token is null'
            }
        }
    }
    else {
        console.log(event);
        return 404;
    }
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk request

右键上传部署云函数,转到腾讯云官网控制台,配置开启固定IP

我们使用接口调用如下,具体效果如下图所示:

当然我们真实管理不可能以这种方式来进行,所以需要写一个管理平台,利用这个封装好的函数接口来实现业务操作。这就是学习完成之后需要自己搭建的内容了。

本教程提供了一个简易的管理平台,可以予以参考。


2 授权小程序云开发资源的部署

云开发作为后端服务的小程序,第三方平台提供了丰富的API接口来部署各种资源。官方文档-云开发接入指南主要描述了云开发环境层面的一些部署能力,官方文档-云开发HTTP API主要描述了云开发业务层面的一些能力,包括数据库存储等操作。

所有的API接口都是以https://api.weixin.qq.com/tcb/开头,整体的参数都是比较固定,所以我们可以将接口通用化成一个云函数。

打开vscode,我们创建一个云函数,命名为toTCB,编写如下代码:

const tcb = require("@cloudbase/node-sdk");
const request = require('request');

const cloud = tcb.init({
    env: ""//此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的云开发通用http请求函数
function CallWeb(access_token, api_name, data) {
    return new Promise((resolve, reject) => {
        request({
            url: `https://api.weixin.qq.com/tcb/${api_name}?access_token=${access_token}`,//拼接相应的name,以及令牌
            body: JSON.stringify(data),//请求的数据参数
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    //获取appid,相应的云开发接口名、参数
    let { appid, name, data } = event;
    if (appid != null && name != null && data != null) {
        //传入appid,获得相应的操作令牌
        let access_token = (await cloud.callFunction({
            name: 'getAuthToken',
            data: {
                appid: appid
            }
        })).result;
        if (access_token != null) {
            //传入参数,等待数据
            let result = await CallWeb(access_token, name, data);
            try{
                let res = JSON.parse(result);
                console.log(res);
                return res;
            }
            catch(e){
                console.log(e,result);
                return result;
            }
        }
        else {
            return {
                code: -1,
                msg: 'access_token is null'
            }
        }
    }
    else {
        console.log(event);
        return 404;
    }
}

请按照注释指引填写正确的信息后,保存代码文件,并在同级文件夹下使用终端命令行打开执行以下命令安装nodejs依赖

npm i @cloudbase/node-sdk request

右键上传部署云函数,转到腾讯云官网控制台,配置开启固定IP

我们可以在管理平台中直接请求此云函数,来进行云开发相关部署,这是客户端封装的云函数调用,需要在登录后才可以使用

function toTCB(obj) {
    app.callFunction({
        name: 'toTcb',
        data: {
            appid: obj.appid,
            name: obj.name,
            data: obj.data
        }
    }).then(res => {
        obj.success ? obj.success(res.result) : null;
    }).catch(e => {
        obj.fail ? obj.fail(e) : null;
    });
}

以下举几个例子供参考:

1) 查询小程序的云开发环境

toTCB({
    name: 'getenvinfo',
    appid: appid,
    data: {},
    success(res) {
        console.log(res);
        if (res.errcode == 0 && res.info_list != 0) {
            console.log('检测到' + res.info_list.length + '个云开发环境,使用默认环境id:' + res.info_list[0].env);
            //在这里如果没有云开发环境可以使用创建环境的接口来创建,在参考示例中只有创建云开发环境的情况,需要自己改写实现
        }
        else {
            alert('环境检测失败!');
        }
    }
})

2)文件上传至云存储

toTCB({
    name: 'uploadfile',
    appid: input.appid,
    data: {
        env: env,
        path: 'logo.png'
    },
    success(res) {
        console.log(res);
        if (res.errcode == 0) {
            //在这里返回了一个上传链接,需要再使用http请求上传文件。
            //需要注意的是,如果在前端上传文件,需要以微信公众号形式登录腾讯云控制台-添加web安全域名为操作的前端域名才可以
        }
        else {
            alert('文件上传失败!');
        }
    }
})

3)数据库的集合创建

toTCB({
    name: 'databasecollectionadd',
    appid: appid,
    data: {
        env: env,
        collection_name: 'INFO'
    },
    success(res) {
        console.log(res);
        if (res.errcode == 0 || res.errcode == -501000) {
            alert('创建数据库INFO成功!');
            //此处注意-501000是表示数据库已存在,目的达成
        }
        else {
            alert('创建数据库INFO失败!');
        }
    }
})

4)数据库处理数据

toTCB({
    name: 'databaseadd',
    appid: appid,
    data: {
        env: env,
        query: `
            db.collection('INFO').add({
                data:{
                    _id:"base_info",
                    content:'${des}',
                    logo:"${logourl}",
                    name:"${name}",
                    person:"${person}",
                    see:0,
                    tel:"${tel}"
                }
            })
        `//拟定数据库处理语句
    },
    success(res) {
        console.log(res);
        if (res.errcode == 0) {
            alert('数据库数据部署成功!');
        }
        else if (res.errcode == -501000) {
            alert('数据库有原始数据,覆盖新数据!');
            toTCB({
                name: 'databaseupdate',
                appid: appid,
                data: {
                    env: env,
                    query: `
                    db.collection('INFO').doc('base_info').update({
                        data:{
                            content:'${des}',
                            logo:"${logourl}",
                            name:"${name}",
                            person:"${person}",
                            see:0,
                            tel:"${tel}"
                        }
                    })
                `
                },
                success(res) {
                    console.log(res);
                    if (res.errcode == 0) {
                        alert('数据库数据部署成功!');
                    }
                }
            })
        }
    }
})


3 授权小程序提交审核

当小程序模版上传、云开发资源部署完毕之后,就需要提交审核了。

相关代码如下:

const tcb = require("@cloudbase/node-sdk");
const request = require('request');

const cloud = tcb.init({
    env: ""//此处填写自己的云开发环境ID
});
const db = cloud.database();
const _ = db.command;

//封装的http请求函数
function CallWeb(access_token,data) {
    return new Promise((resolve, reject) => {
        request({
            url: `https://api.weixin.qq.com/wxa/submit_audit?access_token=${access_token}`,//拼接相应的name,以及令牌
            body: JSON.stringify(data),//请求的数据参数
            method: 'POST'
        }, (error, response, body) => {
            if (error) {
                reject(error);
            }
            resolve(response.body);
        });
    });
}

exports.main = async (event) => {
    //获取appid,参数
    let { appid } = event;
    if (appid != null && name != null && data != null) {
        //传入appid,获得相应的操作令牌
        let access_token = (await cloud.callFunction({
            name: 'getAuthToken',
            data: {
                appid: appid
            }
        })).result;
        if (access_token != null) {
            //传入参数,等待数据
            let result = await CallWeb(access_token,{
                version_desc:"这是我的小程序,请审核"
            });
            try{
                let res = JSON.parse(result);
                console.log(res);
                return res;
            }
            catch(e){
                console.log(e,result);
                return result;
            }
        }
        else {
            return {
                code: -1,
                msg: 'access_token is null'
            }
        }
    }
    else {
        console.log(event);
        return 404;
    }
}


代码示例

本教程用到的示例代码如下:

最后一次编辑于  2021-09-09
赞 6
收藏

1 个评论

  • 王马利
    王马利
    2020-12-10

    文档里的图片怎么都不显示,1.2“环境-云接入”,在控制台实际环境下面并没有云接入

    2020-12-10
    赞同
    回复 4
    • 嫣儿
      嫣儿
      2021-01-18
      你好,问题已知,正在联系处理中,感谢反馈~
      2021-01-18
      回复
    • 兵
      2021-04-13回复嫣儿
      hi,“文档里的图片不显示”这个问题啥时候能解决呀
      2021-04-13
      回复
    • 嫣儿
      嫣儿
      2021-06-22回复
      你好,已修复~
      2021-06-22
      回复
    • 嫣儿
      嫣儿
      2021-06-22回复
      2021-06-22
      回复
登录 后发表内容
课程标签