实现思路:参考微信支付和支付宝支付
用户出示二维码商家进行扫码扣款,可以是用户余额钱包也可以是绑定的会员卡
前端用的uniapp,后端thinkphp6.0,同时兼容微信小程序和app
<template>
<view class="page">
<view class="cu-card">
<view class="cu-item padding shadow" style="padding-bottom: 100upx;">
<view class="cu-bar bg-white solid-bottom">
<view class="action">
<text class="cuIcon-check text-green"></text>
商家进行扫码扣点 {
{ str2 }}
</view>
<view class="action"><text class="cuIcon-moreandroid"></text></view>
</view>
<view class="image qrbar margin-top">
<canvas canvas-id="canvasbar" id="canvasbar" style="width:590rpx; height:180rpx;"></canvas>
<view class="gui-margin-top gui-text-center">
<text class="text-lg">{
{ str1 }}</text>
</view>
</view>
<view class="image qrcode"><canvas canvas-id="canvasqrcode" id="canvasqrcode" style="width:360rpx; height:360rpx;"></canvas></view>
</view>
<view class="cu-list menu sm-border card-menu ">
<view class="cu-item shadow arrow" @click="showModal">
<view class="content flex-sub padding">
<view class="text-bold">{
{ currCard.type_name }}({
{ currCard.type_text }})</view>
<view>{
{ currCard.card_number }}</view>
<view class="text-gray text-sm flex justify-between"><view>优先使用此卡券进行扣点</view></view>
</view>
</view>
</view>
</view>
<view class="cu-modal bottom-modal block" :class="modalShow ? 'show' : ''">
<view class="cu-dialog">
<view class="cu-bar bg-white justify-end">
<view class="content">选择扣点的卡券</view>
<view class="action" @tap="hideModal"><text class="cuIcon-close text-red"></text></view>
</view>
<view class="padding">
<radio-group class="block" @change="RadioChange">
<view class="cu-list menu sm-border text-left">
<view class="cu-item" v-for="(item, index) in cardpaylist" :key="index">
<label class="flex justify-between align-center flex-sub">
<view class="flex-sub">{
{ item.type_name }}({
{ item.type_text }}){
{ item.card_number }}</view>
<radio class="" :checked="item.is_dev == 1 ? true : false" :value="item"></radio>
</label>
</view>
</view>
</radio-group>
</view>
<view class="cu-bar bg-white justify-center">
<view class="action"><button class="cu-btn bg-green margin-left lg" @tap="qudingselectcard">确定选择</button></view>
</view>
</view>
</view>
</view>
</template>
<script>
var QRCode = require('../../utils/qrcode.js');
var barcode = require('../../utils/barcode.js');
const util = require('../../utils/otp/util.js');
const otpapi = require('../../utils/otp/otpapi.js');
const otp = require('../../utils/otp/otp.js');
export default {
data() {
return {
// 二维码绘制对象
qrcode: null,
// 二维码尺寸,单位 rpx
qrcodeSize: 360,
// 二维码数据
qrcodeContent: '',
// 二维码背景颜色
qrcodeBgColor: '#FFFFFF',
// 二维码颜色
qrcodeColor: '#000000',
// 画布 id
qrcodeId: 'canvasqrcode',
// 条形码绘制对象
barcode: null,
// 条形码尺寸,单位 rpx [ 宽度, 高度 ]
barcodeSize: [590, 180],
// 二维码数据
barcodeContent: '',
// 画布 id
canvasId: 'canvasbar',
selected_secret_name: null, // 选中的口令名
selected_secret: null, // 选中的口令
selected_secret_result: null, // 选中的口令的计算结果
totp_secret_list: [], // 口令列表
intervalId: null, // 动态绘制更新口令结果的定时器
timestamp_adj: null, // 用来矫正时间,使得从12点方向开始绘制进度
str2: '',
str1: '',
windowWidth: null,
windowHeight: null,
secret_list_height: '400rpx',
modalShow: false,
cardpaylist: [],
currCard: '',
daicurrCard: ''
};
},
onLoad() {
this.initpage();
},
onHide() {
this.stopUpdateSelectedSecret();
},
onUnload() {
this.stopUpdateSelectedSecret();
},
onReady() {
//this.makeqrcode();
var that = this;
setTimeout(function() {
that.totp_gen_token();
}, 200);
},
methods: {
initpage() {
var that = this;
this.$api.post('user/userpaylist', {}, function(res) {
var data = res.data;
data.forEach((item, index) => {
if (item.is_dev == 1) {
that.currCard = item;
}
});
that.cardpaylist = data;
});
},
makeqrcode(qrcodeContent) {
barcode.code128(uni.createCanvasContext(this.canvasId), qrcodeContent, uni.upx2px(this.barcodeSize[0]), uni.upx2px(this.barcodeSize[1]));
this.qrcode = new QRCode(this.qrcodeId, {
text: qrcodeContent,
width: uni.upx2px(this.qrcodeSize),
height: uni.upx2px(this.qrcodeSize),
colorDark: this.qrcodeColor,
colorLight: this.qrcodeBgColor,
correctLevel: QRCode.CorrectLevel.H
});
},
totp_gen_token() {
//获取默认的卡券
var secret = this.currCard.secret;
this.selected_secret = secret;
this.selected_secret_result = null;
this.startUpdateSelectedSecret();
},
// 停止显示口令
stopUpdateSelectedSecret() {
if (this.intervalId != null) {
clearInterval(this.intervalId);
this.intervalId = null;
this.selected_secret_result = null;
}
this.updateTotpToken(null);
},
// 启动显示口令
restartUpdateSelectedSecret() {
this.stopUpdateSelectedSecret();
this.startUpdateSelectedSecret();
},
startUpdateSelectedSecret() {
// 已启动
if (this.intervalId) return;
// 验证
if (!this.selected_secret) {
this.updateTotpToken(null);
return;
}
// 启动定时计算和绘制口令结果
//console.log('setInterval...');
this.computeTimestampAdj();
var intervalId = setInterval(this.updateSelectedSecret, 100);
this.intervalId = intervalId;
},
// 计算adj,作为补偿,使得从12点钟方向开始显示口令
computeTimestampAdj: function() {
var timestamp = new Date().getTime() / 1000;
var timestamp_adj = timestamp % 30;
if (timestamp_adj > 15) {
timestamp_adj = 30 - timestamp_adj; // 大于15,补到30,用下一轮的
} else {
timestamp_adj = -timestamp_adj; // 不够15,退回到0,用之前的
}
timestamp_adj = timestamp_adj * 1000;
//console.log(timestamp_adj);
this.timestamp_adj=timestamp_adj;
},
// 定时刷新口令并显示
updateSelectedSecret() {
var that = this;
// 验证需要绘制
if (!that.selected_secret) {
that.stopUpdateSelectedSecret();
return;
}
// 使用旧结果绘制,如果旧结果有效的话。
var result = that.selected_secret_result;
if (result) {
// 通过对比当前时间和刷新时间和刷新周期来判断
var timestamp = new Date().getTime() + this.timestamp_adj;
if (timestamp - result.refresh_time <= result.refresh_interval) {
that.updateTotpToken(result.token, timestamp, result.refresh_time, result.refresh_interval);
return;
}
} else {
// 旧结果没效,那应该是重选了另一个口令,或口令删除,需要重计算adj
this.computeTimestampAdj();
}
// 使用新结果绘制
var timestamp = new Date().getTime() + this.timestamp_adj;
otpapi.totp_gen2(that.selected_secret, timestamp).then(function(res) {
//console.log("获取到令牌,设置令牌", res);
var result = res.data;
that.selected_secret_result = result;
that.updateTotpToken(result.token, timestamp, result.refresh_time, result.refresh_interval);
});
},
// ==============================================
// 口令绘制
updateTotpToken: function(token, timestamp, refresh_time, refresh_interval) {
var str1 = '0000000000000000';
//if (token) str1 =String(token);//String(parseInt(this.currCard.card_number)*3+);//String();
if (token) str1 = String(parseInt(this.currCard.id) + parseInt(token) * 10000002527); //String();
//付款码=TOTP * 质数 + 用户ID
//质数取值需要大于最大用户ID,如:当前系统设计最大承受用户有9900人,则质数可以设定必须大于9900的数值(999999991).
var str2 = '- - - - - -';
if (timestamp && refresh_time && refresh_interval) {
var sec = parseInt((refresh_interval + refresh_time - timestamp) / 1000) + 1;
str2 = sec + '秒后刷新';
}
this.str1 = str1;
this.str2 = str2;
//二维码应该能确定用户的身份,能确认卡号,用完一次就失效
this.makeqrcode(str1);
},
qudingselectcard() {
this.hideModal();
if (this.daicurrCard != '') {
this.currCard = this.daicurrCard;
//this.selected_secret_name = this.daicurrCard.card_number;
this.selected_secret = this.daicurrCard.secret;
this.totp_gen_token();
}
},
showModal() {
this.modalShow = true;
},
RadioChange(e) {
this.daicurrCard = e.target.value;
},
hideModal() {
this.modalShow = false;
}
}
};
</script>
<style>
.page {
background-color: #f23030;
height: 100vh;
}
.section {
}
.qrcode {
display: flex;
align-items: center;
flex-direction: column;
padding: 30upx;
}
.qrbar {
display: flex;
align-items: center;
flex-direction: column;
padding: 30upx;
}
</style>