【学一点儿前端】银行×微信:前端uniapp实现微信免输绑卡需求

接到需求

接到一个银行对接微信免输绑卡的需求,在微信小程序实现页面,用uniapp编写,本来想上网抄作业,结果发现免输绑卡的资料非常少,只有官方文档和微信原生小程序的开发资料,于是我自己进行一段时间的摸索实现了,鉴于网上实现案例太少,所以把本次方案按我理解的实现顺序分享出来。

接入前准备

需要定好测试用小程序以及生产用小程序(测试为在体验版测试,两个小程序可以为同一个),将要求的信息发送邮件给微信支付对接方以获取appkey和测试用微信安装包(appkey在开发需要用到)。微信对接方建议在开发前提前两三周发送邮件,具体参考官方文档:
免输卡号绑卡接入前准备
在这里插入图片描述
还要在上述小程序微信公众平台申请免输绑卡插件,最好提前一两周申请,特殊情况就需要催对接方了
在这里插入图片描述

开发

开发中需要用到sha1加密方式,因此要

npm i sha1

manifest.json引入免输绑卡插件,provider为插件的appid,noInputBindCard为我自己起的插件名:

"mp-weixin": {
    "plugins": {
        "noInputBindCard": {
            "version": "1.0.5",
            "provider": "xxxxxxxxxx"
        }
    }
},

在page.json中,在分包写入调起绑卡插件的路由,其中no-input-bind为文件夹名,noInputBind为文件名,在style里写入usingComponents,写入"animation-cards": "plugin://noInputBindCard/animation-cards"使用免输绑卡插件
注意:noInputBindCard是上面在manifest.json起的插件名

"subPackages": [
	{
        "root": "pages/no-input-bind",
        "pages": [
            {
                "path": "noInputBind",
                "name": "noInputBind",
                "style": {
                    "navigationBarTitleText": "免输绑卡",
                    "navigationStyle": "custom",
                    "usingComponents": {
                        "animation-cards": "plugin://noInputBindCard/animation-cards"
                    }
                }
            }
        ]
    }
],

随后就是noInputBind.vue的代码了
简单梳理一下代码流程:

1.先randomStr()随机生成一串字符串用于点击绑卡时校验用,调getCurrentInstance().proxy.getOpenerEventChannel()获取eventChannel

由于大概还有0.02%的用户不支持this.getOpenerEventChannel方法,会导致小程序跳转插件失败,所以请勿删除js示例中的checkingVersion方法。

2.调compareWXPaysign将从页面传入的参数拼接进行sha1加密成为paysignStr,将加密后的串与传过来的paysign比较,如果一致则进行下一步
3.调getUserEnter获取openid和sessionKey,将请求到的openid与页面传来的openid进行比较,如果一致则进行下一步
4.调getCardInfo调用行内接口请求当前微信用户的卡列表,对请求到的卡列表进行加工
5.调checkingVersion判断eventChannel是否存在,存在则进行下一步
6.调goPlugin携带卡列表和appid前往插件,如果用户点击某张卡并同意绑卡后则调网联一键绑卡接口进行验证,验证后继续调用phoneBindCardVerifySms微信验短api,完毕。
代码如下:

<template>
    <view />
</template>

<script setup>
import { reactive, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getOpenID, setEncData } from '@/utils/Storage';
import sha1 from 'sha1';
import { getWXOpenId, getSm2PublicKey } from '../../services/auth';
import { queryWxCardList, epccCardBinding } from '@/services/wx-info';
import config from '../../../config/config';

const data = reactive({
    param: {}, // 接收微信传来的参数
    noncestr: '', // 生成随机不到32位字符串
    paysign: '', // 生成签名
    openid: '', // 用于银行校验
    sessionKey: '', // Session Key
    timestamp: '', // 随机数
    pluginData: {
        // 银行向插件传的数据
        appId: '', // 银行小程序的appid
        bankCardLists: [],
    },
    // 绑卡接口需要送的字段
    phone: '', // 手机号
    sgntrPrsnIdentTp: '', // 证件类型
    sgntrAcctNm: '', // 签约人账户名称
    sgntrPrsnIdentNo: '', // 证件号
    eventChannel: '',

    testArr: '',
});
const getSm2Key = async () => {
    const res = await getSm2PublicKey({
        bankCode: config.appBankCode,
    });
    const { message } = res;
    //写入缓存
    setEncData({
        publicKey: message.publicKey_04xy,
        iv: message.iv,
    });
};
const goPlugin = () => {
    wx.navigateTo({
        url: 'plugin://noInputBindCard/plugin-index',
        events: {
            agreeBtnClick: async (eventData) => {
                console.log('🚀 ~ file: noInputBind.vue:52 ~ goPlugin ~ eventData:', eventData);

                const appid = data.param.appid;
                const appkey = 'xxxxxxxxxxxxxxxxxxxxxxxx';
                const noncestrValue = data.noncestr;
                const package1 = `bank_type=NCBCB_${eventData.selectedCard.item.cardType}`;
                const sessionid = data.param.sessionid;
                const timestampValue = Date.now().toString().substring(0, 10);
                const paysignStr = `appid=${appid}&appkey=${appkey}&noncestr=${noncestrValue}&package=${package1}&sessionid=${sessionid}&timestamp=${timestampValue}`;
                const paysignSuccess = sha1(paysignStr);
                const encodsndBackURL = encodeURIComponent(`bind_scene=NO_CARD_BIND&sessionid=${data.param.sessionid}`);
                const paramData = {
                    sessionId: data.param.sessionid,
                    sndBackURL: encodsndBackURL,
                    tranTpCd: '0207',
                    acptInstNo: '1',
                    rsrvMblNo: data.phone,
                    sgntrPrsnIdentNo: data.sgntrPrsnIdentNo,
                    sgntrPrsnIdentTp: data.sgntrPrsnIdentTp,
                    sgntrAcctNm: data.sgntrAcctNm,
                    sgntrBnkAcctNo: eventData.selectedCard.item.acctNo,
                    sgntrPrsnAcctTp: eventData.selectedCard.item.sgntrPrsnAcctTp,
                    acctInstIndNo: '银行的机构标识',
                    pymtCnlTp: 'EPCC',
                    uuid: eventData.selectedCard.item.uuid,
                };
                const res = await epccCardBinding(paramData);

                // 假设接口返回中有一个 `status` 字段来判断是否成功
                if (res.result === '200') {
                    uni.phoneBindCardVerifySms({
                        timestamp: timestampValue,
                        noncestr: noncestrValue,
                        package: package1,
                        signtype: 'sha1',
                        paysign: paysignSuccess,
                        sessionid: data.param.sessionid,
                        appid: data.param.appid,
                        success: (smsRes) => {
                            console.log('验短api成功回调', smsRes);
                        },
                        fail: (smsRes) => {
                            console.log('验短api失败回调', smsRes);
                            data.testArr = '验短api失败回调' + smsRes;
                        },
                    });
                }
            },
            disagreeBtnClick: () => {
                console.log('用户点击了协议蒙层的“不同意”按钮');
            },
            openUserProtocol: (protocolData) => {
                console.log('用户点击了某个协议,协议信息为:', protocolData.protocolInfo);
            },
            noCardsStatusBtnClick: () => {
                console.log('没有银行卡场景,用户点击了知道了按钮');
            },
        },
        success: (res) => {
            uni.hideLoading();
            // 通过eventChannel向插件页面传送数据
            res.eventChannel.emit('bankListDataSend', {
                bankData: data.pluginData,
            });
        },
        fail: (err) => {
            uni.hideLoading();
            console.log('跳转插件失败', err);

            uni.showToast({
                title: '跳转插件失败' + err,
                icon: 'none',
                duration: 2000,
            });
        },
    });
};
const checkingVersion = function () {
    if (data.eventChannel) {
        return true;
    } else {
        uni.showModal({
            title: '更新提示',
            content: '当前版本过低,请更新微信到最新版本。',
            showCancel: false,
            confirmText: '知道了',
            success: function () {
                uni.exitMiniProgram();
            },
        });
        return false;
    }
};

const getCardInfo = async () => {
    let requestData = {
        encBankElem: decodeURIComponent(data.param.enc_bankelem),
        sessionId: data.param.sessionid,
        openId: data.openid,
        bindTail: data.param.bind_tail,
        appId: data.param.appid,
        timestamp: data.param.timestamp,
        nonceStr: data.param.noncestr,
        signType: data.param.signtype,
        paySign: data.param.paysign,
    };
    try {
        uni.showLoading({
            title: '正在加载...',
        });
        const res = await queryWxCardList(requestData);

        // 假设接口返回中有一个 `status` 字段来判断是否成功
        if (res.result === '200') {
            let bankCardList = res.message?.bankCardLists?.map((item) => {
                return {
                    cardNumber: item.cardNumber,
                    cardType: item.cardType,
                    uuid: item.uuid,
                };
            });
            data.pluginData.bankCardLists = [...bankCardList];

            console.log(
                '🚀 ~ file: noInputBind.vue:173 ~ awaitqueryWxCardList ~ data.pluginData.bankCardLists:',
                data.pluginData.bankCardLists
            );

            if (checkingVersion()) {
                goPlugin();
            }
        } else {
            // 如果返回的状态不是成功,抛出一个错误
            throw new Error(res.message || '请求失败');
        }
    } catch (e) {
        console.log('🚀 ~ file: noInputBind.vue:195 ~ getCardInfo ~ e:', e);

        // uni.showModal({
        //     content: e.message || '请求发生错误',
        //     showCancel: false,
        //     confirmText: '知道了',
        //     success: function () {
        //         uni.exitMiniProgram();
        //     },
        // });
    }
};
const compareOpenid = (id) => {
    if (data.param.openid === id) {
        getCardInfo();
    } else {
        console.log('🚀 ~ openid不一致');
        uni.showToast({
            title: 'openid不一致',
            icon: 'none',
            duration: 2000,
        });
    }
};

//如果缓存有openId则直接返回,如果没有则重新生成后再返回
const getOpenIdAndSessionKey = async () => {
    return new Promise((resolve) => {
        const openId = getOpenID();
        if (openId) {
            resolve({
                result: '200',
                openId: openId,
            });
        } else {
            uni.login({
                provider: 'weixin', //使用微信登录
                success: async function (loginRes) {
                    const { code } = loginRes;
                    const res = await getWXOpenId({
                        jsCode: code,
                        appId: data.param.appid,
                    });
                    const { result, message } = res;
                    if (result === '200') {
                        console.log('🚀 ~ message:', message);
                        // setOpenID(message.openId);
                        resolve({
                            result: '200',
                            openId: message.openId,
                            sessionKey: message.sessionKey,
                        });
                    }
                },
            });
        }
    });
};
const toCompareOpenid = async () => {
    try {
        uni.showLoading({
            title: '正在加载...',
        });
        const res = await getOpenIdAndSessionKey();
        if (res.result === '200') {
            console.log('🚀 ~ toCompareOpenid ~ res:', res);
            data.openid = res.openId;
            data.sessionKey = res.sessionKey;
            compareOpenid(data.openid);
        } else {
            console.log('🚀 ~ toCompareOpenid ~ 获取openid和sessionKey失败');
        }
    } catch (e) {
        // uni.showModal({
        //     content: '获取openid和sessionKey失败',
        //     showCancel: false,
        //     confirmText: '知道了',
        //     success: function () {
        //         uni.exitMiniProgram();
        //     },
        // });
    }
};

const randomStr = () => {
    const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
    let randomNum = '';
    for (let i = 0; i < 32; i++) {
        randomNum += $chars.charAt(Math.floor(Math.random() * $chars.length));
    }
    data.noncestr = randomNum;
};
const compareWXPaysign = () => {
    const appid = data.param.appid;
    console.log('🚀 ~ compareWXPaysign ~ appid:', appid);
    const appkey =
        'xxxxxxxxxxxxxxxxxxxxxxxxxxxx';
    const paysignStr = `appid=${appid}&appkey=${appkey}&bind_tail=${
        data.param.bind_tail
    }&enc_bankelem=${decodeURIComponent(data.param.enc_bankelem)}&noncestr=${data.param.noncestr}&openid=${
        data.param.openid
    }&sessionid=${data.param.sessionid}&timestamp=${data.param.timestamp}`;
    const paysignFromWX = data.param.paysign;

    console.log(
        '🚀 ~ file: noInputBind.vue:264 ~ compareWXPaysign ~ data.param.enc_bankelem:',
        decodeURIComponent(data.param.enc_bankelem)
    );

    console.log('🚀 ~ compareWXPaysign ~ paysignFromWX:', paysignFromWX);
    console.log('🚀 ~ compareWXPaysign ~ sha1(paysignStr):', sha1(paysignStr));
    if (sha1(paysignStr) === paysignFromWX) {
        toCompareOpenid();
    } else {
        console.log('签名校验没有通过');
        uni.showToast({
            title: '签名校验没有通过',
            icon: 'none',
            duration: 2000,
        });
    }
};

onLoad((option) => {
    uni.showLoading({
        title: '正在加载...',
    });
    // 随机生成一串字符串用于点击绑卡时校验用
    randomStr();
    //生成通讯用的key
    getSm2Key();
    const instance = getCurrentInstance().proxy;
    data.eventChannel = instance?.getOpenerEventChannel();
    if (option) {
        console.log('🚀 ~ onLoad ~ option:', option);
        data.param = option;
        data.pluginData.appId = option.appid;
        compareWXPaysign();
    }
});
</script>

<style lang="scss" scoped></style>

参考文档

写给银行前端——免输卡号绑卡开发中的坑
微信小程序免输卡号绑定银行卡(前端流程及代码)-爱代码爱编程
使用插件
免输绑卡 | 小程序插件 | 微信公众平台
免输卡号绑卡_跳转银行选卡页面签名验证失败
微信小程序免输入绑卡,大家都是如何调试的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值