提交 bd8bec0c 编写于 作者: VK1688's avatar VK1688

uni-pay新增苹果虚拟支付

上级 2f557bd4
......@@ -1037,6 +1037,13 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/API/virtual-payment/virtual-payment-uni-pay",
"style": {
"navigationBarTitleText": "苹果虚拟支付(uni-pay)",
"enablePullDownRefresh": false
}
},
// #endif
{
"path": "pages/API/request-payment/request-payment/order-detail",
......@@ -1798,14 +1805,14 @@
{
"path": "uni_modules/uni-pay-x/pages/ad-interactive-webview/ad-interactive-webview",
"style": {
"navigationBarTitleText": "收银台",
"navigationBarTitleText": "ad",
"backgroundColor": "#F8F8F8"
}
},
{
"path": "uni_modules/uni-pay-x/pages/pay-desk/pay-desk",
"style": {
"navigationBarTitleText": "ad",
"navigationBarTitleText": "收银台",
"backgroundColor": "#F8F8F8"
}
},
......
......@@ -27,9 +27,12 @@
<!-- #endif -->
<button class="button" @click="getOrderPopup(true)">查询支付状态</button>
<button class="button" @click="pageTo('/uni_modules/uni-pay-x/pages/success/success?out_trade_no=test2024030501-1&order_no=test2024030501&total_fee=1&adpid=1000000001&return_url=/pages/API/request-payment-uni-pay/order-detail')">支付成功页面示例</button>
<button class="button" @click="pageTo('/uni_modules/uni-pay-x/pages/success/success?out_trade_no=test2024030501-1&order_no=test2024030501&total_fee=1&adpid=1000000001&return_url=/pages/API/request-payment/request-payment/order-detail')">支付成功页面示例</button>
<!-- #ifdef APP-IOS -->
<button class="button" @click="pageTo('/pages/API/virtual-payment/virtual-payment-uni-pay')">苹果虚拟支付示例(iOS内购)</button>
<!-- #endif -->
<!-- 查询支付的弹窗 -->
<uni-pay-popup ref="getOrderPopupRef" type="bottom">
<uni-pay-popup ref="getOrderPopupRef" type="center">
<scroll-view direction="vertical" class="get-order-popup">
<view class="label">插件支付单号:</view>
<view class="mt20">
......@@ -90,7 +93,7 @@
<button class="button" v-if="h5Env === 'h5-weixin'" @click="getWeiXinJsCode('snsapi_base')">公众号获取openid示例</button>
<!-- #endif -->
<!-- 统一支付组件,注意:vue3下ref不可以等于组件名,因此这里ref="pay" 而不能是 ref="uniPay" -->
<uni-pay ref="payRef" :adpid="adpid" height="900rpx" return-url="/pages/API/request-payment-uni-pay/order-detail" logo="/static/logo.png" @success="onSuccess" @create="onCreate"
<uni-pay ref="payRef" :adpid="adpid" height="900rpx" return-url="/pages/API/request-payment/request-payment/order-detail" logo="/static/logo.png" @success="onSuccess" @create="onCreate"
@fail="onFail" @cancel="onCancel"></uni-pay>
</view>
</template>
......@@ -251,7 +254,7 @@
getOrderData['out_trade_no'] = this.out_trade_no;
}
let res = await payInstance.getOrder(getOrderData);
if (res != null && res['errCode'] == 0) {
if (res['errCode'] == 0) {
this.getOrderRes = res.getJSON('pay_order') as UTSJSONObject;
let obj = {
"-1": "已关闭",
......@@ -275,7 +278,7 @@
let res = await payInstance.refund({
out_trade_no: this.out_trade_no, // 插件支付单号
});
if (res != null && res['errCode'] == 0) {
if (res['errCode'] == 0) {
uni.showToast({
title: res['errMsg'] as string,
icon: "none"
......@@ -288,7 +291,7 @@
let res = await payInstance.getRefund({
out_trade_no: this.out_trade_no, // 插件支付单号
});
if (res != null && res['errCode'] == 0) {
if (res['errCode'] == 0) {
uni.showModal({
content: res['errMsg'] as string,
showCancel: false
......@@ -301,7 +304,7 @@
let res = await payInstance.closeOrder({
out_trade_no: this.out_trade_no, // 插件支付单号
});
if (res != null && res['errCode'] == 0) {
if (res['errCode'] == 0) {
uni.showModal({
content: res['errMsg'] as string,
showCancel: false
......@@ -316,7 +319,7 @@
provider: "wxpay",
provider_pay_type: "jsapi"
});
if (res != null && res['appid'] != null && res['appid'] != "") {
if (res['appid'] != null && res['appid'] != "") {
let appid = res['appid'] as string;
let redirect_uri = window.location.href.split("?")[0];
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`;
......@@ -327,7 +330,7 @@
async getOpenid(data:UTSJSONObject) : Promise<void> {
const payInstance = this.$refs["payRef"] as UniPayComponentPublicInstance;
let res = await payInstance.getOpenid(data);
if (res != null && res['openid'] != null && res['openid'] != "") {
if (res['openid'] != null && res['openid'] != "") {
let openid = res['openid'] as string;
let code = data['code'] as string;
this.openid = openid;
......
<template>
<view class="content">
<view class="uni-list">
<radio-group @change="applePriceChange">
<view class="uni-list-cell" v-for="(item, index) in productList" :key="index">
<radio :value="item['product_id']" :checked="product_id == item['product_id']"/>
<view class="price" @click="applePriceClick(item)">{{item['title']}} {{item['goods_price']}}元</view>
</view>
</radio-group>
</view>
<view class="uni-padding-wrap">
<button class="button btn-pay" @click="createOrder" :loading="loading" :disabled="disabled">立即支付</button>
</view>
<!-- 统一支付组件 -->
<uni-pay ref="payRef" :debug="true" :adpid="adpid" return-url="/pages/API/request-payment/request-payment/order-detail" @mounted="onMounted" @success="onSuccess" @fail="onFail" @cancel="onCancel"></uni-pay>
</view>
</template>
<script>
export default {
data() {
return {
order_no: "", // 业务系统订单号(即你自己业务系统的订单表的订单号)
out_trade_no: "", // 插件支付单号
adpid: "1000000001", // uni-ad的广告位id
loading: false, // 支付按钮是否在loading中
disabled: true, // 支付按钮是否禁用
product_id: "", // 用户选择的商品id
// 出售的苹果虚拟商品列表
productList: [
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 1, // 单价(元)
"buy_quantity": 1, // 数量(消耗性类型: 数量默认是1,最大值是10)
"product_id": "uniappx.consumable.sponsor_1",
"title": "消耗性产品:赞助"
},
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 5, // 单价(元)
"buy_quantity": 1, // 数量(消耗性类型: 数量默认是1,最大值是10)
"product_id": "uniappx.consumable.sponsor_50",
"title": "消耗性产品:赞助"
},
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 1, // 单价(元)
"buy_quantity": 1, // 数量(非消耗性: 数量只能是1,且一个该类型产品一个appleId只能购买一次)
"product_id": "uniappx.nonconsumable.sponsorskin_1",
"title": "非消耗性产品: 赞助"
},
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 1, // 单价(元)
"buy_quantity": 1, // 数量(自动续期订阅产品: 数量只能是1)
"product_id": "uniappx.autorenewable.monthly_1",
"title": "自动续期订阅产品:每月定期赞助", // 注意自动续期订阅产品在沙盒模式下,实际周期会缩短到几分钟续期一次(即现实世界几分钟 = 沙盒世界1个月)
},
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 1, // 单价(元)
"buy_quantity": 1, // 数量(非自动续期订阅产品: 数量只能是1)
"product_id": "uniappx.nonrenewable.monthly_1",
"title": "非自动续期订阅产品:月赞助",
},
{
"description": "为DCloud提供的免费软件进行赞助",
"goods_price": 1, // 单价(元)
"buy_quantity": 1, // 数量
"product_id": "uniappx.nonrenewable.none",
"title": "测试不存在的产品"
}
] as Array<UTSJSONObject>,
}
},
onLoad: function() {
},
onShow() {
},
onUnload() {},
methods: {
// 支付组件加载完毕后执行
onMounted(insideData: any){
this.init();
},
// 初始化
init() {
this.product_id = this.productList[0]["product_id"] as string;
this.disabled = false;
let payRef = this.$refs['payRef'] as UniPayComponentPublicInstance;
// 苹果虚拟支付未完成订单检测
payRef.appleiapRestore();
},
/**
* 发起支付
* 在调用此api前,你应该先创建自己的业务系统订单,并获得订单号 order_no,把order_no当参数传给此api,而示例中为了简化跟支付插件无关的代码,这里直接已时间戳生成了order_no
*/
createOrder(){
this.order_no = `test`+Date.now();
this.out_trade_no = this.order_no;
let productInfo: UTSJSONObject = this.productList.find((item: UTSJSONObject) : boolean => {
return item['product_id'] == this.product_id;
});
let buy_quantity = productInfo.getNumber('buy_quantity') || 1;
let goods_price = productInfo.getNumber('goods_price');
// 发起支付
this.$refs.payRef.createOrder({
provider: "appleiap", // 支付供应商(这里固定为appleiap,代表苹果虚拟支付)
order_no: this.order_no, // 业务系统订单号(即你自己业务系统的订单表的订单号)
out_trade_no: this.out_trade_no, // 插件支付单号
type: "appleiap", // 支付回调类型(可自定义,建议填写appleiap)
description: productInfo.description,
total_fee: parseInt((goods_price * 100 * buy_quantity).toFixed(0)), // 插件是以分为单位,故这里需要乘以100
// apple_virtual字段仅苹果虚拟支付生效
apple_virtual: {
product_id: this.product_id, // 产品id
goods_price: goods_price, // 单价
buy_quantity: buy_quantity, // 购买数量
},
// 自定义数据
custom: {}
});
},
// 监听事件 - 支付成功
onSuccess(res){
console.log('success: ', res);
if (res.user_order_success) {
// 代表用户已付款,且你自己写的回调成功并正确执行了
} else {
// 代表用户已付款,但你自己写的回调执行失败(通常是因为你的回调代码有问题)
}
},
onFail(err){
uni.showModal({
content: `${err.errSubject} : ${err.errCode} : ${err.errMsg}`,
showCancel: false,
title: `发起支付失败`,
});
},
onCancel(err){
uni.showToast({
title: "用户取消了支付",
icon: 'none'
});
},
// 监听-多选框选中的值改变
applePriceChange(e) {
this.product_id = e.detail.value;
},
applePriceClick(item: any){
this.product_id = item['product_id'] as string;
}
}
}
</script>
<style>
.content {
padding: 15px;
}
.button {
background-color: #007aff;
color: #ffffff;
}
.uni-list-cell {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.price {
margin-left: 10px;
}
.btn-pay {
margin-top: 30px;
}
</style>
......@@ -109,12 +109,15 @@ module.exports = {
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'), // 支付宝根证书路径
}
},
// ios内购相关(uniapp-x暂不支持)
// 苹果虚拟支付相关
"appleiap": {
// ios内购支付
// 苹果虚拟支付支付,参数获取地址:https://appstoreconnect.apple.com/access/integrations/api/subs
"app": {
"password": "", // App 专用共享密钥,App 专用共享密钥是用于接收此 App 自动续期订阅收据的唯一代码。如果您要将此 App 转让给其他开发者或不想公开主共享密钥,建议使用 App 专用共享密钥。非自动续订场景不需要此参数
"timeout": 10000, // 请求超时时间,单位:毫秒
"appId": "", // 密钥ID
"issuerId": "", // Issuer ID
"bundleId": "", // 正式包名(如果dev包名和正式包名一致,则只填bundleId即可)
"devBundleId": "", // dev包名(如果dev包名和正式包名一致,则devBundleId可不填)
"appCertPath": path.join(__dirname, 'appleiap/apiclient_cert.p8'), // 证书路径
"sandbox": true, // 是否是沙箱环境
},
}
......
<template>
<view class="popup-root" v-if="isOpen" v-show="isShow" @click="clickMask">
<view class="popup-root" :class="'popup-'+type" v-if="isOpen" v-show="isShow" @click="clickMask">
<view @click.stop>
<slot></slot>
</view>
......@@ -20,6 +20,10 @@
type: Boolean,
default: true
},
type: {
type: String,
default: "center"
}
},
watch: {
// 设置show = true 时,如果没有 open 需要设置为 open
......@@ -78,4 +82,7 @@
align-items: center;
z-index: 99;
}
.popup-bottom {
justify-content: flex-end;
}
</style>
\ No newline at end of file
......@@ -264,32 +264,28 @@
options['qr_code'] = false;
options = objectAssign(options, data);
if (options['provider'] == "appleiap") {
// #ifndef APP
// 苹果虚拟支付走特殊逻辑
// #ifdef APP-IOS
return this._appleiapCreateOrder(options);
// #endif
// #ifndef APP-IOS
uni.showModal({
title: "提示",
content: "苹果内购只支持app发起",
content: "请在iOS系统中执行",
showCancel: false
})
// #endif
// #ifdef APP
}
// #ifdef APP
if (options['provider'] == "wxpay") {
// #ifdef uniVersion < 4.11
uni.showModal({
title: "提示",
content: "uni-app x 暂不支持苹果内购",
content: "请先升级HBX至4.11",
showCancel: false
})
// #endif
return;
}
// #ifdef APP
if (options['provider'] == "wxpay") {
// #ifdef uniVersion < 4.11
uni.showModal({
title: "提示",
content: "请先升级HBX至4.11",
showCancel: false
})
return;
// #endif
// #endif
}
// #endif
......@@ -376,15 +372,16 @@
// #ifndef H5
let _order = res.get('order');
let orderStr = typeof _order == "string" ? _order as string : JSON.stringify(_order) as string;
console.log('orderStr: ', orderStr)
uni.requestPayment({
provider: res['provider'] as string,
orderInfo: orderStr,
success: (res : RequestPaymentSuccess) => {
console.log(JSON.stringify(res))
success: (res) => {
console.log("requestPaymentSuccess", JSON.stringify(res))
this._getOrder();
},
fail: (err : RequestPaymentFail) => {
console.log("RequestPaymentFail", JSON.stringify(err))
fail: (err) => {
console.log("requestPaymentFail", JSON.stringify(err))
let errCode = err.errCode;
let errMsg = err.errMsg;
if (errCode == 700713) {
......@@ -399,7 +396,7 @@
this.$emit("fail", err);
}
}
} as RequestPaymentOptions);
});
// #endif
},
// 打开弹窗
......@@ -413,66 +410,99 @@
popupRef.close();
},
// 查询订单(查询支付情况)
async getOrder(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async getOrder(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.getOrder(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 发起退款(此接口需要admin角色才可以访问)
async refund(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async refund(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.refund(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 查询退款(查询退款情况)
async getRefund(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async getRefund(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.getRefund(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 关闭订单
async closeOrder(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async closeOrder(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.closeOrder(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 获取支持的支付供应商
async getPayProviderFromCloud(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async getPayProviderFromCloud(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.getPayProviderFromCloud(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 获取支付配置内的appid(主要用于获取获取微信公众号的appid,用以获取code)
async getProviderAppId(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async getProviderAppId(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.getProviderAppId(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 根据code获取openid
async getOpenid(data : UTSJSONObject) : Promise<UTSJSONObject | null> {
async getOpenid(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.getOpenid(data);
return res;
} catch (err) {
return null
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 验证iosIap苹果内购支付凭据
async verifyReceiptFromAppleiap(data : UTSJSONObject) : Promise<UTSJSONObject> {
try {
let res = await uniPayCo.verifyReceiptFromAppleiap(data);
return res;
} catch (err) {
return {
errCode: -1,
errMsg: (err as Error).message
}
}
},
// 支付成功后的逻辑
......@@ -526,7 +556,7 @@
out_trade_no,
await_notify: true
});
if (res != null) {
if (res['errCode'] == 0) {
let has_paid = res.getBoolean('has_paid');
if (has_paid != null && has_paid == true) {
this.closePopup("qrcodePopup");
......@@ -544,6 +574,173 @@
if (provider != _provider) {
this.createOrder({ provider: provider })
}
},
// 苹果虚拟支付支付逻辑
async _appleiapCreateOrder(options : UTSJSONObject) : Promise<void>{
// #ifndef APP-IOS
uni.showToast({
title: "请在iOS系统中打开",
icon: "none"
})
// #endif
// #ifdef APP-IOS
const virtualPaymentManager = uni.getVirtualPaymentManager();
let createOrderData = {
provider: options.provider,
total_fee: options.total_fee,
order_no: options.order_no,
out_trade_no: options.out_trade_no,
description: options.description,
type: options.type,
apple_virtual: options.apple_virtual,
custom: options.custom,
} as UTSJSONObject;
let res = await uniPayCo.createOrder(createOrderData);
if (res.errCode === 0) {
this.$emit("create", res);
this.res = res;
uni.showLoading({
title: '支付请求中...'
});
try {
// 请求苹果支付
if (this.debug) console.log("正在请求苹果服务器", res.out_trade_no);
uni.requestVirtualPayment({
apple: {
productId: options.getJSON('apple_virtual')!.getString('product_id')!,
appAccountToken: res.appleiap_account_token,
quantity: options.getJSON('apple_virtual')!.getNumber('buy_quantity')! || 1,
},
success: async (requestPaymentRes) => {
uni.hideLoading()
let transaction = requestPaymentRes?.apple;
if (this.debug) console.log('用户支付成功', transaction);
let transactionIdentifier : string = transaction.transactionIdentifier;
let transactionDate : string = transaction.transactionDate;
let outTradeNo : string = res.out_trade_no;
uni.showLoading({
title: '正在处理支付结果...'
});
// 云端请求苹果服务器验证票据
let verifyRes = await this.verifyReceiptFromAppleiap({
out_trade_no: outTradeNo,
transaction_receipt: transaction.jsonRepresentation,
transaction_identifier: transactionIdentifier
});
if (verifyRes.errCode === 0) {
if (verifyRes.repeat) {
uni.showModal({
title: "提示",
content: `当前道具只能购买一次`,
showCancel: false,
confirmText: "好的"
});
} else {
//经过开发者server验证成功后请结束该交易
virtualPaymentManager.finishTransaction({
transaction: transaction,
success: (r) => {
if (this.debug) console.log("关单成功, 该productId= " + transaction.productId)
},
fail: (e) => {
if (this.debug) console.log("关单失败, 该productId= " + transaction.roductId)
}
});
uni.hideLoading();
this.paySuccess(verifyRes);
}
} else {
if (this.debug) console.log('verifyRes: ', verifyRes)
}
},
fail: (err) => {
uni.hideLoading();
if (this.debug) console.log("购买失败:errSubject= " + err.errSubject + ", errCode= " + err.errCode + ", errMsg= " + err.errMsg);
if ([700601].indexOf(err.errCode) > -1 || err.errMsg.indexOf("cancel") > -1) {
this.$emit("cancel", err);
} else {
this.$emit("fail", err);
}
}
});
} catch (err) {
let code = err.errCode || err.code;
if (code === 2) {
// 用户取消支付
if (this.debug) console.log("用户取消支付");
this.$emit("cancel", err);
} else {
// 发起支付失败
console.error("appleiapCreateOrder:fail", err);
this.$emit("fail", err);
}
uni.hideLoading();
}
}
// #endif
},
// 苹果虚拟支付未完成订单检测
appleiapRestore() {
// #ifdef APP-IOS
uni.showLoading({
title: "",
mask: true
});
try {
const virtualPaymentManager = uni.getVirtualPaymentManager();
virtualPaymentManager.getUnfinishedTransactions({
success: async (res) => {
uni.hideLoading()
console.log("获取未结束的订单列表个数:" + res.transactions.length)
res.transactions.forEach(async transaction => {
console.log("getUnfinishedTransactions成功的交易productId= " + transaction.productId);
let appAccountToken : string = transaction.appAccountToken;
let transactionIdentifier : string = transaction.transactionIdentifier;
//let originalTransactionIdentifier : string = transaction.originalTransactionIdentifier;
let transactionDate : string = transaction.transactionDate;
// 云端请求苹果服务器验证票据
let verifyRes = await this.verifyReceiptFromAppleiap({
appleiap_account_token: appAccountToken,
transaction_receipt: transaction.jsonRepresentation,
transaction_identifier: transactionIdentifier,
});
if (verifyRes.errCode === 0 || !appAccountToken) {
// 经过开发者server验证成功后请结束该交易
virtualPaymentManager.finishTransaction({
transaction: transaction,
success: (r) => {
if (this.debug) console.log("关单成功, 该productId= " + transaction.productId)
},
fail: (e) => {
if (this.debug) console.log("关单失败, 该productId= " + transaction.productId)
}
});
uni.hideLoading();
// 如果是自动续期,则不跳页面
if (!verifyRes.is_subscribe && verifyRes.pay_order) {
this.paySuccess(verifyRes);
}
} else {
if (this.debug) console.log('verifyRes: ', verifyRes)
}
})
},
fail: (e) => {
uni.hideLoading()
console.log("获取未结束的订单列表失败:errSubject= " + e.errSubject + ", errCode= " + e.errCode + ", errMsg= " + e.errMsg)
uni.showToast({
title: "获取未结束的订单列表失败:errCode= " + e.errCode,
icon: 'error'
});
}
})
} catch(err){
console.error('err: ', err)
uni.hideLoading()
}
// #endif
}
},
watch: {
......@@ -839,7 +1036,7 @@
margin-bottom: 6rpx;
.qrcode-popup-info-fee {
.text{
.text {
color: red;
font-size: 60rpx;
font-weight: bold;
......
......@@ -15,6 +15,8 @@ const ERROR = {
51009: 51009,
51010: 51010,
51011: 51011,
51012: 51012,
51013: 51013,
// 数据不存在
52001: 52001,
52002: 52002,
......@@ -24,6 +26,8 @@ const ERROR = {
53003: 53003,
53004: 53004,
53005: 53005,
54001: 54001,
54002: 54002,
}
const errSubject = "uni-pay";
......
......@@ -95,6 +95,7 @@ module.exports = {
clientInfo, // 兼容云对象调用云对象模式
cloudInfo, // 兼容云对象调用云对象模式
wxpay_virtual, // 仅用于微信虚拟支付
apple_virtual, // 仅用于苹果虚拟支付
} = data;
if (!clientInfo) clientInfo = this.getClientInfo();
......@@ -118,6 +119,7 @@ module.exports = {
clientInfo,
cloudInfo,
wxpay_virtual,
apple_virtual,
});
// uniappx-特殊处理
if (typeof res.order === "object" && typeof res.order["timestamp"] === "string") {
......@@ -266,13 +268,17 @@ module.exports = {
async verifyReceiptFromAppleiap(data) {
let {
out_trade_no,
appleiap_account_token,
transaction_receipt,
transaction_identifier,
} = data;
const clientInfo = this.getClientInfo();
return await service.pay.verifyReceiptFromAppleiap({
out_trade_no,
appleiap_account_token,
transaction_receipt,
transaction_identifier
transaction_identifier,
clientInfo,
});
},
......@@ -289,5 +295,26 @@ module.exports = {
cloudInfo
});
},
/**
* 请求微信小程序虚拟支付API
*/
async requestWxpayVirtualApi(data) {
const clientInfo = this.getClientInfo();
if (clientInfo.source !== "function") {
throw new Error("requestWxpayVirtualApi只能通过云端调云端的方式调用");
}
let res = await service.pay.requestWxpayVirtualApi(data);
return res;
},
/**
* 测试请求,仅为了确保是否请求能调通
*/
async test(data) {
return {
errCode: 0,
errMsg: "ok"
};
},
}
}
\ No newline at end of file
......@@ -16,6 +16,7 @@ const sentence = {
51010: 'Invalid out_trade_no or transaction_id',
51011: 'Invalid wxpay_virtual',
51012: 'Invalid buy_quantity',
51013: 'Invalid apple_virtual',
52001: 'NotExist payOrder',
52002: 'NotExist notifyUrl',
53001: 'Create payment error',
......
......@@ -15,7 +15,8 @@ const sentence = {
51009: 'cloudInfo不能为空',
51010: '支付单号或第三方交易单号不能同时为空',
51011: '微信虚拟支付参数(wxpay_virtual)不能为空',
51012: '代币购买数量(buy_quantity)不能为空',
51012: '购买数量(buy_quantity)不能为空',
51013: '苹果虚拟支付参数(apple_virtual)不能为空',
52001: '支付订单不存在',
52002: '请先配置正确的异步回调URL',
53001: '获取支付信息失败,请稍后再试',
......@@ -23,7 +24,7 @@ const sentence = {
53003: '查询退款信息失败,请稍后再试',
53004: '关闭订单失败,请稍后再试',
53005: '证书错误,请检查支付证书',
54001: 'ios内购凭据校验不通过',
54001: '苹果虚拟支付凭据校验不通过',
54002: '订单未支付'
};
......
......@@ -82,6 +82,19 @@ util.aes.decrypt = function(obj) {
return decrypted;
};
util.generateUUID = function() {
// 获取当前时间戳
let timestamp = Date.now().toString(16);
while (timestamp.length < 16) {
timestamp = timestamp + "0";
}
// 生成随机数部分
const randomHex = crypto.randomBytes(10).toString('hex');
// 结合时间戳和随机数,并按照UUID格式排列
const uuid = `${timestamp.slice(0, 8)}-${timestamp.slice(8, 12)}-${randomHex.slice(0, 4)}-${randomHex.slice(4, 8)}-${randomHex.slice(8)}`;
return uuid.toLowerCase();
};
module.exports = util;
// aes192算法 - 加密
......
......@@ -3,11 +3,13 @@ const alipay = require('./alipay');
const common = require('./common');
const qrcode = require('./qrcode'); // 此源码为npm i qrcode的压缩版本
const crypto = require('./crypto');
const jsonwebtoken = require('./jsonwebtoken');
module.exports = {
wxpay,
alipay,
common,
qrcode,
crypto
crypto,
jsonwebtoken
};
......@@ -12,6 +12,6 @@
"path": "/uni-pay-co",
"timeout": 60,
"triggers": [],
"runtime": "Nodejs8"
"runtime": "Nodejs18"
}
}
\ No newline at end of file
}
......@@ -32,7 +32,7 @@ class service {
getConfig() {
return config;
}
/**
* 支付成功 - 异步通知
*/
......@@ -157,7 +157,7 @@ class service {
return libs.common.returnNotifySUCCESS({ provider, provider_pay_type });
}
/**
* 微信虚拟支付异步通知
*/
......@@ -187,6 +187,7 @@ class service {
clientInfo, // 客户端信息
cloudInfo, // 云端信息
wxpay_virtual, // 仅用于微信虚拟支付
apple_virtual, // 仅用于苹果虚拟支付
} = data;
let subject = description;
let body = description;
......@@ -204,6 +205,13 @@ class service {
if (typeof wxpay_virtual.buy_quantity !== "number" || wxpay_virtual.buy_quantity <= 0) {
throw { errCode: ERROR[51012] };
}
} else if (provider === "appleiap") {
if (typeof apple_virtual !== "object") {
throw { errCode: ERROR[51013] };
}
if (typeof apple_virtual.buy_quantity !== "number" || apple_virtual.buy_quantity <= 0) {
throw { errCode: ERROR[51012] };
}
} else {
if (typeof total_fee !== "number" || total_fee <= 0 || total_fee % 1 !== 0) {
throw { errCode: ERROR[51005] };
......@@ -319,7 +327,7 @@ class service {
// 微信虚拟支付扩展数据
expand_data = {
mode: wxpay_virtual.mode, // short_series_coin 代币充值; short_series_goods 道具直购
buy_quantity: wxpay_virtual.buy_quantity,
buy_quantity: wxpay_virtual.buy_quantity,
rate: uniPayConifg.rate || 100,
sandbox: uniPayConifg.sandbox,
};
......@@ -344,7 +352,7 @@ class service {
} else if (getOrderInfoParam.mode === "short_series_goods") {
// 计算支付金额
total_fee = expand_data.buy_quantity * expand_data.goods_price;
}
}
}
orderInfo = await uniPayInstance.getOrderInfo(getOrderInfoParam);
if (qr_code && orderInfo.codeUrl) {
......@@ -397,6 +405,11 @@ class service {
let userInfo = await dao.uniIdUsers.findById(user_id);
if (userInfo) nickname = userInfo.nickname;
}
let appleiap_account_token;
if (provider === "appleiap") {
appleiap_account_token = libs.crypto.generateUUID();
res.appleiap_account_token = appleiap_account_token;
}
await dao.uniPayOrders.add({
provider,
provider_pay_type,
......@@ -419,6 +432,7 @@ class service {
custom,
create_date,
expand_data,
appleiap_account_token, // 苹果虚拟支付专用字段
stat_data: {
platform: stat_platform,
app_version: clientInfo.appVersion,
......@@ -466,7 +480,7 @@ class service {
payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
}
}
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
......@@ -487,7 +501,7 @@ class service {
console.log('queryRes: ', queryRes)
} else {
// 无uniPayInstance.orderQuery函数时的兼容处理
if ([1,2].indexOf(payOrderInfo.status) > -1) {
if ([1, 2].indexOf(payOrderInfo.status) > -1) {
queryRes = {
tradeState: "SUCCESS",
tradeStateDesc: "订单已支付"
......@@ -610,7 +624,7 @@ class service {
if (errMsg) {
if (errMsg.indexOf("verify failure") > -1) {
throw { errCode: ERROR[53005] };
}
}
if (errMsg.indexOf("header too long") > -1) {
throw { errCode: ERROR[53005] };
}
......@@ -754,7 +768,7 @@ class service {
});
let wxpayResult = (provider === "wxpay" && closeOrderRes.resultCode === "SUCCESS");
let alipayResult = (provider === "alipay" && closeOrderRes.code === "10000");
if (wxpayResult || alipayResult) {
// 修改订单状态为已取消
await dao.uniPayOrders.update({
......@@ -806,8 +820,8 @@ class service {
uniPayConifg = wxpayVirtualPayConifg;
needCacheSessionKey = true;
}
} catch(err){}
} catch (err) {}
let res = await libs.wxpay.getOpenid({
config: uniPayConifg,
code,
......@@ -860,62 +874,207 @@ class service {
async verifyReceiptFromAppleiap(data) {
let {
out_trade_no,
appleiap_account_token,
transaction_receipt,
transaction_identifier,
clientInfo,
} = data;
if (!out_trade_no) {
throw { errCode: ERROR[51001] };
if (!appleiap_account_token) {
return {
errCode: 0,
errMsg: "Invalid out_trade_no"
}
}
appleiap_account_token = appleiap_account_token.toLowerCase(); // 转小写
let payOrderInfo = await dao.uniPayOrders.find({
provider: "appleiap",
appleiap_account_token
});
if (!payOrderInfo || !payOrderInfo.out_trade_no) {
return {
errCode: 0,
errMsg: "Invalid out_trade_no"
}
}
out_trade_no = payOrderInfo.out_trade_no;
}
// 初始化uniPayInstance
let uniPayInstance = await this.initUniPayInstance({ provider: "appleiap", provider_pay_type: "app" });
let verifyReceiptRes = await uniPayInstance.verifyReceipt({
receiptData: transaction_receipt
let payOrderInfo = await dao.uniPayOrders.find({
out_trade_no,
});
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
const verifyReceipt = async (uniPayConifg) => {
const jwt = libs.jsonwebtoken;
const fs = require('fs');
const privateKey = fs.readFileSync(uniPayConifg.appCertPath, 'utf8');
const header = {
alg: 'ES256',
kid: uniPayConifg.appId, // 替换为您的密钥ID
typ: "JWT"
};
const nowTime = Date.now();
const bundleId = uniPayConifg.sandbox ? uniPayConifg.devBundleId || uniPayConifg.bundleId : uniPayConifg.bundleId;
const payload = {
iss: uniPayConifg.issuerId, // 替换为您的团队ID
iat: Math.floor(nowTime / 1000), // 当前时间戳
exp: Math.floor(nowTime / 1000) + 3600, // 当前时间戳加1小时
aud: 'appstoreconnect-v1',
bid: bundleId
};
const iapToken = jwt.sign(payload, privateKey, {
algorithm: 'ES256',
header: header
});
const serviceUrl = uniPayConifg.sandbox ? "https://api.storekit-sandbox.itunes.apple.com" : "https://api.appstoreconnect.apple.com";
const url = `${serviceUrl}/inApps/v1/transactions/${transaction_identifier}`;
let requestRes;
// 如果请求苹果服务器失败,则重试5次
for (let i = 0; i <= 5; i++) {
try {
requestRes = await uniCloud.request({
method: "GET",
header: {
'Authorization': `Bearer ${iapToken}`,
'Content-Type': 'application/json'
},
url
});
break;
} catch (err) {
// console.log('errCode: ', err.code || err.errCode, 'errMsg: ', err.message || err.errMsg)
}
}
if (requestRes.statusCode !== 200) {
return {};
}
const signedInfoTokenArr = requestRes.data.signedTransactionInfo.split('.');
const signedInfoString = Buffer.from(signedInfoTokenArr[1], 'base64').toString('utf8');
const verifyReceiptRes = JSON.parse(signedInfoString);
const appAccountToken = verifyReceiptRes.appAccountToken.toLowerCase();
verifyReceiptRes.tradeState = verifyReceiptRes.inAppOwnershipType === "PURCHASED" && payOrderInfo.appleiap_account_token === appAccountToken ? "SUCCESS" : "fail";
return verifyReceiptRes;
};
let uniPayConifg = await this.getUniPayConfig({ provider: "appleiap", provider_pay_type: "app" });
let verifyReceiptRes = await verifyReceipt(uniPayConifg);
let userOrderSuccess = false;
let pay_date;
if (verifyReceiptRes.tradeState !== "SUCCESS") {
throw { errCode: ERROR[54002] };
// 尝试使用相反的环境再次验证
console.log('尝试使用相反的环境再次验证: ');
verifyReceiptRes = await verifyReceipt({
...uniPayConifg,
sandbox: !uniPayConifg.sandbox
});
if (verifyReceiptRes.tradeState !== "SUCCESS") {
// 如果还是不成功,则校验不通过
throw { errCode: ERROR[54002] };
}
}
// 支付成功
pay_date = Number(verifyReceiptRes.receipt.receipt_creation_date_ms);
let inAppList = verifyReceiptRes.receipt.in_app;
let inApp = inAppList.find((item) => {
return item.transaction_id === transaction_identifier;
});
if (!inApp) {
// 校验不通过
throw { errCode: ERROR[54002] };
//console.log('verifyReceiptRes: ', verifyReceiptRes)
let isSubscribe = false;
if (["Auto-Renewable Subscription"].indexOf(verifyReceiptRes.type) > -1) {
isSubscribe = true; // 标记为自动订阅订单
}
let quantity = inApp.quantity; // 购买数量
let product_id = inApp.product_id; // 对应的内购产品id
let transaction_id = inApp.transaction_id; // 本次交易id
if ((Date.now() - 1000 * 3600 * 24) > pay_date) {
// 订单已超24小时,不做处理,通知前端直接关闭订单。
// 支付成功
pay_date = Number(verifyReceiptRes.purchaseDate);
let quantity = verifyReceiptRes.quantity; // 购买数量
let product_id = verifyReceiptRes.productId; // 对应的内购产品id
let transaction_id = verifyReceiptRes.transactionId; // 本次交易id
let original_transaction_id = verifyReceiptRes.originalTransactionId; // 原始交易id
if ((Date.now() - 1000 * 3600 * 72) > pay_date && !isSubscribe) {
// 非自动订阅订单,若超72小时,不做处理,通知前端直接关闭订单。
return {
errCode: 0,
errMsg: "ok"
};
}
if (isSubscribe && original_transaction_id !== transaction_id) {
let findOrderInfo = await dao.uniPayOrders.find({
appleiap_account_token: payOrderInfo.appleiap_account_token,
user_order_success: _.exists(true)
});
if (findOrderInfo) {
// 自动订阅产品自动续期时需要创建新的支付订单
let quantity = verifyReceiptRes.quantity;
let goods_price = parseFloat((verifyReceiptRes.price / 1000).toFixed(2));
let total_fee = parseFloat((goods_price * 100 * quantity).toFixed(2));
let description = "[自动续期]" + payOrderInfo.description.replace(/\[自动续期\]/g, '');
// 添加数据库(数据库的out_trade_no字段需设置为唯一索引)
let stat_platform = clientInfo.platform;
if (stat_platform === "app") {
stat_platform = clientInfo.os;
}
// 创建新的支付订单
let addId = await dao.uniPayOrders.add({
provider: payOrderInfo.provider,
provider_pay_type: payOrderInfo.provider_pay_type,
uni_platform: clientInfo.platform,
status: 0,
type: payOrderInfo.type,
order_no: payOrderInfo.order_no,
out_trade_no: transaction_id,
user_id: payOrderInfo.user_id,
nickname: payOrderInfo.nickname,
device_id: clientInfo.deviceId,
client_ip: clientInfo.client_ip,
openid: payOrderInfo.openid,
description,
total_fee,
refund_fee: 0,
refund_count: 0,
provider_appid: uniPayConifg.appId,
appid: clientInfo.appId,
custom: payOrderInfo.custom,
create_date: Date.now(),
expand_data: payOrderInfo.expand_data,
appleiap_account_token, // 苹果虚拟支付专用字段
stat_data: {
platform: stat_platform,
app_version: clientInfo.appVersion,
app_version_code: clientInfo.appVersionCode,
app_wgt_version: clientInfo.appWgtVersion,
os: clientInfo.os,
ua: clientInfo.ua,
channel: clientInfo.channel ? clientInfo.channel : String(clientInfo.scene),
scene: clientInfo.scene
}
});
payOrderInfo = await dao.uniPayOrders.find({
_id: addId,
});
out_trade_no = transaction_id;
}
}
// 查询该transaction_id是否使用过,如果已使用,则不做处理,通知前端直接关闭订单。
let findOrderInfo = await dao.uniPayOrders.find({
transaction_id,
});
if (findOrderInfo) {
const repeatReceipt = () => {
return {
errCode: 0,
errMsg: "ok"
errMsg: "ok",
repeat: true, // 代表重复通知了
};
};
if (findOrderInfo) {
// 不允许重复通知
return repeatReceipt();
}
// 否则,执行用户回调
// 用户自己的逻辑处理 开始-----------------------------------------------------------
let orderPaySuccess;
let payOrderInfo = await dao.uniPayOrders.find({
out_trade_no,
});
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
try {
// 加载自定义异步回调函数
orderPaySuccess = require(`../notify/${payOrderInfo.type}`);
......@@ -923,7 +1082,7 @@ class service {
console.log(err);
}
if (typeof orderPaySuccess === "function") {
payOrderInfo = await dao.uniPayOrders.updateAndReturn({
let newPayOrderInfo = await dao.uniPayOrders.updateAndReturn({
whereJson: {
status: 0, // status:0 为必须条件,防止重复推送时的错误
out_trade_no: out_trade_no, // 商户订单号
......@@ -936,6 +1095,11 @@ class service {
original_data: verifyReceiptRes
}
});
if (!newPayOrderInfo) {
// 不允许重复通知
return repeatReceipt();
}
payOrderInfo = newPayOrderInfo;
console.log('用户自己的回调逻辑 - 开始执行');
userOrderSuccess = await orderPaySuccess({
verifyResult: verifyReceiptRes,
......@@ -969,9 +1133,10 @@ class service {
status: payOrderInfo.status, // 标记当前支付订单状态 -1:已关闭 0:未支付 1:已支付 2:已部分退款 3:已全额退款
user_order_success: payOrderInfo.user_order_success, // 用户异步通知逻辑是否全部执行完成,且无异常(建议前端通过此参数是否为true来判断是否支付成功)
pay_order: payOrderInfo,
is_subscribe: isSubscribe
};
}
/**
* 获取对应支付配置
* let uniPayConifg = await this.getUniPayConfig({ provider, provider_pay_type });
......@@ -1007,7 +1172,7 @@ class service {
if (uniPayConifg.version === 3) {
try {
uniPayInstance = uniPay.initWeixinV3(uniPayConifg);
} catch(err){
} catch (err) {
console.error(err);
let errMsg = err.message;
if (errMsg && errMsg.indexOf("invalid base64 body") > -1) {
......@@ -1022,28 +1187,12 @@ class service {
// 支付宝
uniPayInstance = uniPay.initAlipay(uniPayConifg);
} else if (provider === "appleiap") {
// ios内购
// 苹果虚拟支付
uniPayInstance = uniPay.initAppleIapPayment(uniPayConifg);
} else if (provider === "wxpay-virtual") {
// 微信虚拟支付
// 还需要额外传accessToken
let cacheKey = {
appId: uniPayConifg.appId,
platform: "weixin-mp"
}
let cacheInfo = await dao.opendbOpenData.getAccessToken(cacheKey);
if (cacheInfo) {
// 缓存有值
uniPayConifg.accessToken = cacheInfo.access_token;
} else {
// 缓存无值
let getAccessTokenRes = await libs.wxpay.getAccessToken(uniPayConifg);
uniPayConifg.accessToken = getAccessTokenRes.accessToken;
// 缓存accessToken
await dao.opendbOpenData.setAccessToken(cacheKey, {
access_token: getAccessTokenRes.accessToken,
}, getAccessTokenRes.expiresIn);
}
uniPayConifg.accessToken = await this.getAccessToken(data);
uniPayInstance = uniPay.initWeixinVirtualPayment(uniPayConifg);
} else {
throw new Error(`${provider} : 不支持的支付方式`);
......@@ -1051,7 +1200,77 @@ class service {
return uniPayInstance;
}
/**
* 获取accessToken
* let uniPayInstance = await service.pay.getAccessToken({ provider, provider_pay_type });
*/
async getAccessToken(data = {}) {
let uniPayConifg = await this.getUniPayConfig(data);
let cacheKey = {
appId: uniPayConifg.appId,
platform: "weixin-mp"
}
let cacheInfo = await dao.opendbOpenData.getAccessToken(cacheKey);
if (cacheInfo) {
// 缓存有值
return cacheInfo.access_token;
} else {
// 缓存无值
let getAccessTokenRes = await libs.wxpay.getAccessToken(uniPayConifg);
let accessToken = getAccessTokenRes.accessToken;
// 缓存accessToken
await dao.opendbOpenData.setAccessToken(cacheKey, {
access_token: getAccessTokenRes.accessToken,
}, getAccessTokenRes.expiresIn);
return accessToken;
}
}
/**
* 获取sessionKey
* let sessionKey = await service.pay.getSessionKey({ provider, provider_pay_type, openid });
*/
async getSessionKey(data = {}) {
let {
openid,
} = data;
// 获取用户的sessionKey
let uniPayConifg = await this.getUniPayConfig(data);
let { session_key } = await dao.opendbOpenData.getSessionKey({
appId: uniPayConifg.appId,
platform: "weixin-mp",
openid
});
return session_key;
}
/**
* 请求微信小程序虚拟支付API
* let res = await service.pay.requestWxpayVirtualApi(data);
*/
async requestWxpayVirtualApi(options = {}) {
let {
method,
data = {}
} = options;
// 微信虚拟支付固定参数
let provider = "wxpay-virtual";
let provider_pay_type = "mp";
// 获得微信小程序虚拟支付实例
let uniPayInstance = await this.initUniPayInstance({ provider, provider_pay_type });
// 调用微信小程序虚拟支付云端API
if (["currencyPay"].indexOf(method) > -1) {
if (!data.sessionKey) {
data.sessionKey = await this.getSessionKey({ ...data, provider, provider_pay_type });
}
}
let res = await uniPayInstance[method](data);
return res;
}
}
module.exports = new service();
module.exports = new service();
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册