接到需求
接到一个银行对接微信免输绑卡的需求,在微信小程序实现页面,用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}×tamp=${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}×tamp=${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>
参考文档
写给银行前端——免输卡号绑卡开发中的坑
微信小程序免输卡号绑定银行卡(前端流程及代码)-爱代码爱编程
使用插件
免输绑卡 | 小程序插件 | 微信公众平台
免输卡号绑卡_跳转银行选卡页面签名验证失败
微信小程序免输入绑卡,大家都是如何调试的?