第一次提交

This commit is contained in:
gxwebsoft
2023-08-04 13:14:48 +08:00
commit 1b923e5cff
1030 changed files with 128016 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
## 2.0.22022-12-12
- 【修复】`createOrder`接口传了`other`参数后可能会报`snake2camelJson is not function`的问题。
## 2.0.12022-12-12
- 【优化】补全package.json内的uni_modules依赖
## 2.0.02022-12-05
- 【重要】uni-pay 2.0从公共模块升级为包含前端页面、uni-pay-co云对象让支付更加简单省心 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-pay.html)
## 1.1.12022-09-22
- 修复 微信支付V3提示 appCert 不存在的bug
- 新增 微信支付V3可以通过证书字符串方式导入证书
## 1.1.02022-09-22
- 新增 微信支付V3接口 [详情](https://uniapp.dcloud.io/uniCloud/unipay?id=微信支付v3)
## 1.0.292022-06-14
- 修复app平台PLATFORM变更引起的支付报错的Bug
## 1.0.282022-01-10
- 支付宝下单接口返回详细错误信息
- 优化发行包体积
## 1.0.272021-11-02
- 新增 苹果应用内购买凭证校验接口 [详情](https://uniapp.dcloud.io/uniCloud/unipay?id=verifyreceipt)
## 1.0.262021-11-01
- 新增 苹果内购凭证校验接口
## 1.0.252021-10-18
- 修复微信子商户id参数错误的Bug
## 1.0.242021-09-23
- 新增 微信外部浏览器支付H5支付
## 1.0.232021-09-22
- 修复微信支付部分值被转化为NaN导致无法直接入库的错误
## 1.0.222021-08-26
- 修复 支付宝用户未支付状态下查询订单状态orderQuery报错的Bug
## 1.0.212021-08-19
- 修复1.0.18版本引出的微信退款通知验签失败的bug
## 1.0.202021-08-04
- 修复1.0.19版本引出的微信支付签名错误问题
## 1.0.192021-08-03
- 修复timeStamp大小写导致的微信公众号支付失败
## 1.0.182021-07-16
- 通知类型不匹配时返回校验未通过
## 1.0.172021-07-16
- 新增 支付宝退款通知回调 [详情](https://uniapp.dcloud.io/uniCloud/unipay?id=verify-refund-notify)
- 新增 判断通知类型接口 [详情](https://uniapp.dcloud.io/uniCloud/unipay?id=check-notify-type)
## 1.0.162021-07-14
- 修复APP微信支付报签名错误的Bug
## 1.0.152021-07-13
- 修复1.0.14版本引出的微信支付使用pfx时报错的Bug
## 1.0.142021-07-12
- 支持使用微信子商户号,[详情](https://uniapp.dcloud.net.cn/uniCloud/unipay?id=init),感谢[studytime](https://gitee.com/studytime)
- 修复支付宝支付传入encode后的passbackParams参数导致验签无法通过的Bug
## 1.0.132021-03-25
- 修复 微信退款通知解析报错的Bug
## 1.0.122021-02-03
- 调整为uni_modules目录规范

View File

@@ -0,0 +1,829 @@
<template>
<view class="uni-pay" >
<!-- PC版收银台弹窗开始 -->
<uni-popup v-if="modeCom === 'pc'" ref="payPopup" type="center" :safe-area="false">
<view class="pc-pay-popup">
<view class="pc-pay-popup-title">收银台</view>
<view class="pc-pay-popup-flex">
<view class="pc-pay-popup-qrcode-box">
<image class="pc-pay-popup-qrcode-image" :src="res.qr_code_image"></image>
<view class="pc-pay-popup-amount-box">
<view class="pc-pay-popup-amount-tips">扫一扫付款</view>
<view class="pc-pay-popup-amount">{{ (options.total_fee / 100).toFixed(2) }}</view>
</view>
<view class="pc-pay-popup-complete-button" v-if="res.qr_code_image">
<button type="primary" @click="_getOrder()">我已完成支付</button>
</view>
</view>
<view class="pc-pay-popup-provider-list">
<view class="pc-pay-popup-provider-item" v-if="currentProviders.indexOf('wxpay') > -1" :class="options.provider == 'wxpay' ? 'active' : ''" @click="_pcChooseProvider('wxpay')">
<image :src="images.wxpay" class="pc-pay-popup-provider-image"></image>
<text class="pc-pay-popup-provider-text">微信支付</text>
</view>
<view class="pc-pay-popup-provider-item" v-if="currentProviders.indexOf('alipay') > -1" :class="options.provider == 'alipay' ? 'active' : ''" @click="_pcChooseProvider('alipay')">
<image :src="images.alipay" class="pc-pay-popup-provider-image"></image>
<text class="pc-pay-popup-provider-text">支付宝支付</text>
</view>
<view class="pc-pay-popup-logo">
<image :src="logo" mode="widthFix"></image>
</view>
</view>
</view>
</view>
</uni-popup>
<!-- PC版收银台弹窗结束 -->
<!-- 手机版收银台弹窗开始 -->
<uni-popup v-else ref="payPopup" type="bottom" :safe-area="false">
<view class="mobile-pay-popup" :style="'min-height: '+height+';'">
<view class="mobile-pay-popup-title">收银台</view>
<view class="mobile-pay-popup-amount-box">
<view>待支付金额</view>
<view class="mobile-pay-popup-amount">{{ (options.total_fee / 100).toFixed(2) }}</view>
</view>
<view class="mobile-pay-popup-provider-list">
<uni-list>
<!-- #ifdef MP-WEIXIN || H5 || APP -->
<uni-list-item v-if="currentProviders.indexOf('wxpay') > -1" :thumb="images.wxpay" title="微信支付" @click="createOrder({ provider: 'wxpay' })" clickable link></uni-list-item>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY || H5 || APP -->
<uni-list-item v-if="currentProviders.indexOf('alipay') > -1" :thumb="images.alipay" title="支付宝" @click="createOrder({ provider: 'alipay' })" clickable link></uni-list-item>
<!-- #endif -->
</uni-list>
</view>
</view>
</uni-popup>
<!-- 手机版收银台弹窗结束 -->
<!-- 二维码支付弹窗开始 -->
<uni-popup ref="qrcodePopup" type="center" :safe-area="false" :animation="false" :mask-click="false" @close="clearQrcode">
<view class="qrcode-popup-content">
<image :src="res.qr_code_image" class="qrcode-image"></image>
<view class="qrcode-popup-info">
<view>
<text class="qrcode-popup-info-fee">{{ (options.total_fee / 100).toFixed(2) }}</text>
<text></text>
</view>
<view v-if="options.provider == 'wxpay'">请用微信扫码支付</view>
<view v-else-if="options.provider == 'alipay'">请用支付宝扫码支付</view>
</view>
<button type="primary" @click="_getOrder()">我已完成支付</button>
<view class="qrcode-popup-cancel" @click="clearQrcodePopup">暂不支付</view>
</view>
</uni-popup>
<!-- 二维码支付弹窗结束 -->
<!-- 外部浏览器确认支付弹窗开始 -->
<uni-popup ref="payConfirmPopup" type="center" :safe-area="false" :animation="false" :mask-click="false">
<view class="pay-confirm-popup-content">
<view class="pay-confirm-popup-title">请确认支付是否已完成</view>
<view><button type="primary" @click="_getOrder()">已完成支付</button></view>
<view class="pay-confirm-popup-refresh"><button type="default" @click="_afreshPayment()">支付遇到问题重新支付</button></view>
<view class="pay-confirm-popup-cancel" @click="clearPayConfirmPopup">暂不支付</view>
</view>
</uni-popup>
<!-- 外部浏览器确认支付弹窗结束 -->
</view>
</template>
<script>
// 引入支付云对象
const uniPayCo = uniCloud.importObject("uni-pay-co");
import jsSdk from "../../js_sdk/js_sdk.js"
var myOpenid; // 将openid临时缓存避免重复获取openid
// #ifdef APP
import appleiapSdk from "../../js_sdk/appleiap.js"
// #endif
export default {
name: "uni-pay",
emits: ["success", "cancel", "fail", "create", "mounted"],
props: {
/**
* Banner广告位id
*/
adpid: {
Type: String,
default: ""
},
/**
* 是否自动跳转到插件内置的支付成功页面具有看广告功能可以增加开发者收益默认true
*/
toSuccessPage:{
Type: Boolean,
default: true
},
/**
* 支付成功后,点击查看订单按钮时跳转的页面地址
*/
returnUrl:{
Type: String,
default: ""
},
/**
* 支付结果页主色调,默认支付宝小程序为#108ee9其他端均为#01be6e
* 建议:绿色系 #01be6e 蓝色系 #108ee9 咖啡色 #816a4e 粉红 #fe4070 橙黄 #ffac0c 橘黄 #ff7100
*/
mainColor:{
Type: String,
default: ""
},
/**
* 收银台模式
* mobile 手机版
* pc 电脑版
*/
mode:{
Type: String,
default: ""
},
/**
* PC收银台模式时展示的logo
*/
logo:{
Type: String,
default: "/static/logo.png"
},
/**
* 收银台高度默认70vh
*/
height: {
Type: [String],
default: "70vh"
},
/**
* 是否打印运行过程日志
*/
debug: {
Type: Boolean,
default: false
}
},
data() {
return {
// 支付参数
options: {},
// 支付云对象返回结果
res: {},
images: {
wxpay: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABC9JREFUeF7tWk1a20AMlUzv0bDr13AAYAOcpLCBcoqQU1DYEE6C2QAHIP26q3sPPOqniU2cZMYj+SeGxN5kEXlm9ObpjaQxwpY/uOX+Qw9Az4AtR6APgS0nQC+CfQi0FQLfrvcHXwAGPP4bQMK/fy5f7O9HehphwPfb/dOIogEhHQHBcamDCDESPoIxMQPTNSi1ABj+OrwDpNMaO5og4P2bMZOugFADwNTewWhU0/FVzAgnKZnxuoFQAbB3vX9MET7U2PHgq4R09vv8ZRI0bMhADMDw9uAhGN8NLQrWyAYRAGt1PgcRIU5TOms7JIIAdOL8nElJauikTRBKAdi7ObwioFFTzHaMw3mBzRV8DwKOXy+ertpagxcAq/YR/g2d6TlNrUDu4EiiE0Why4T1rgyINoXRC4DgjE+mF8+7RYAkp4RrRyVztRUKTgCkuz89fz4pAiB5z7WbklBrKxScAEgWxI6joZPXy5c4B0H0nkPdhzcHFIxxhHgZ8OA7AgMnAMObA479UnF6H5twQpF5RBMdibPDvB4AAAL6IZ0rNbTb9IngAyC8IwJ0K5okQBgzqFEKSV4wcXg17bxl8fIiJXFc0bHAgYLjYlHEFaZlVUQDoAIbcVZaN1VRrgAgUfImASiKW6Yh4pAohmHVQqpLABI0dMYiKhJPCeoV0ueuQsDmEJrkSeJ/bqNJnOqfApqVzWznzrdYWkvzhnUDYGnPKLTdV5gpfLiOqJUIaTefF8RKH6wxtAOX2IdA8NcCmmRItmBLfVF5jRBnR58kGQtWlGUJlBeAxpQ5A4eFKTu/ufLzPQv1f2mRRDiZ/nyyYwYrypI0OlQOc/9PsgshDsh2v+BUwTFnD3K5DglVlD4WlDZEsqywNgiK2F9gQBkLi7EtyV59WhBsiTURCjy5QZMgYRn9cxbZWgCQ+IKlnH2sFQYTURHmCYMgAJaKs9aYPkXNXGK6QhQdt9xeC4UhTC+eV/wVASASmrKj6IMA4NIBMQDsX1VN4IlbuU0K7vmiQS0G5EOpmiW6I1Dpjtp8pYc5yxYVj0RtXcMJcwDFSiqYLh2x+QgqAJwnAuEEydxbkZtdj+fKPVfwbPIq7KngqvMVX4WoAmDBAcH9HTMmXw23s0LJSlPOOsZx0l8VAu/0Fzjuc2Td3aY5zf1VoZgBvPgmvuhoIrFSMSXQThcDoJo0YLxGLfBSv5IINgVC1XxCOb/oZrkTBtRJqkQgKG6ROgPgPbGq/6HVIiYK51WngAj5ikbBhoZi3FALbHmozhlQXFChTc75g6wRM2ufzb9N/IwMcG0wg8HZJf9HBF/tFZnBBBH+cW/BpBDnd4XLDNJcon4oBiiY7jS194mEI0IaSz+12ygAclSYEcXvFsqA3UgANEzqAdCgtYm2PQM2cVc1PvUM0KC1ibY9AzZxVzU+bT0D/gPs/oxfcUEcJAAAAABJRU5ErkJggg==",
alipay: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAA2FJREFUeF7tmU122jAQx2cMB2hp9yULeK+naHISwhJyiIRDBJaQkwRO0fdgEWff0h4ANH0yFc+m+hhbckKNvJUsaX76z4ckhAv/8MLthwggKuDCCUQXuHABxCAYXSC6wIUTiC7wFgL4MPveTaj9optrO+696ya8yeQRQFRAdIEYAxoVBD/PNtdCwHWpDIJwr+1PMCk1DgAkCSx/jHrLsv/p+lfKAp3HzQOYDAqxKtcYBJPtXe/B1Y3TXgmATGst0WIrgAC7JmBINOQsNN8HE0zfVQFlFxzrgFgHNLQOkNJuQ7vrcgkS1CXEua5fgnDj+l+172CX/h59Tbn9Of0qBUE1cGe2ngPhLWeiEH0krFDBT63HC8Cnx/VtFuFDfgl90UOldDvuX4WcSo7lBSD0YuR4H6ebZwRNkRUw9xdSah1G+IzZmW5IW7ERDX/e9Rc+YwerBEMvQo1nrhfqkf/ZuYCxxK5J/t4AjkFQBi71CXxFoFSWq2XTlkn+AndXodNf5SwgT4J7gnttoNL6BqUEmCZET/tkvzQZYj5g1Sf/0goIkfcJYIkEK5HsFnkYnen6BXQptUb5lwJgMz4zCihXD/BqAwVDuoy+Uqx399kACrkZaYECVjY5qxJZXpoQwrcDHB6UghfVvPssAMo35W4R7oZVg5EMmAJxUCZ2CNzfVJ2Pm6qdleDBNwFClaHZdRrQAEhWe25VSPCuAMo1tnQhdIzMNUixfDYBqAOGVQEKQB15OFMCwXPV3QsFww7g73E39Pudr/Gn0EyplQPXCkBF/5AKsBtPKRA+AdKAEx/0BhYLL9nHFkhZLiBvbkOcxFzG5wPtoe7gBUrrTiMttqO+8ebZCkAtWErs17jHvrrSLcj+lkCpKeV5g/ABIA05lqgVM4Er2nPhZgev7DHGnToLG+ALIC9budgWwoRzyuMUPlzj8waVBuELIFOB5iksi7xIKQh8PS4wu8/j+a3vBScbRAgABVfg5BZbH6SFgP0kVIl7UCjNja4RCkAGwecaPLDhp4yNsSYkADlp/mncdNLLu8fpud9XQK7//wERGoBrAefSfgRBsLI9pTtPg+diUNV1yLuJypVg1Un/p/8arwDXZkQALkJNb48KaPoOu+yLCnARanp7VEDTd9hlX1SAi1DT2/8AaakVXysj5qkAAAAASUVORK5CYII="
},
originalRroviders: ["wxpay","alipay"],
currentProviders: ["wxpay","alipay"],
}
},
async mounted() {
let code;
let res;
if (!myOpenid) {
// #ifdef MP-WEIXIN
code = await this.getCode();
res = await this.getOpenid({
provider: "wxpay",
code
});
if (res) myOpenid = res.openid;
// #endif
// #ifdef MP-ALIPAY
code = await this.getCode();
res = await this.getOpenid({
provider: "alipay",
code
});
if (res) myOpenid = res.openid;
// #endif
}
// #ifndef MP
// 如果不是小程序,则请求云端获取支持的支付方式
let getPayProviderFromCloudRes = await this.getPayProviderFromCloud();
if (getPayProviderFromCloudRes.errCode === 0) {
this.originalRroviders = getPayProviderFromCloudRes.provider;
this.currentProviders = JSON.parse(JSON.stringify(this.originalRroviders));
}
// #endif
// #ifdef MP-WEIXIN
// 如果是微信小程序,则设置只支持微信支付
this.originalRroviders = ["wxpay"];
this.currentProviders = JSON.parse(JSON.stringify(this.originalRroviders));
// #endif
// #ifdef MP-ALIPAY
// 如果是支付宝小程序,则设置只支持支付宝支付
this.originalRroviders = ["alipay"];
this.currentProviders = JSON.parse(JSON.stringify(this.originalRroviders));
// #endif
this.$emit("mounted", {
images: this.images,
originalRroviders: this.originalRroviders,
currentProviders: this.currentProviders,
// #ifdef APP
appleiapSdk: appleiapSdk,
// #endif
});
},
methods: {
// 发起支付 - 打开支付选项弹窗
async open(options = {}) {
if (options.provider) {
let providers = [];
this.originalRroviders.map((item, index) => {
if (options.provider.indexOf(item) > -1) {
providers.push(item);
}
});
this.currentProviders = providers;
delete options.provider;
} else {
this.currentProviders = JSON.parse(JSON.stringify(this.originalRroviders));
}
this.options = options;
if (this.currentProviders.length === 1) {
this.createOrder({ provider: this.currentProviders[0] });
} else {
if (this.modeCom === "pc") {
await this._pcChooseProvider(this.currentProviders[0]);
}
this.$refs.payPopup.open();
}
},
// 创建支付
async createOrder(data = {}) {
let { options } = this;
Object.assign(options, data);
if (options.provider === "appleiap") {
// ios内购走特殊逻辑
return this._appleiapCreateOrder(options);
}
// #ifdef H5
// 判断如果是pc访问则强制扫码模式
if (jsSdk.checkPlatform() === "pc") {
options.qr_code = true;
}
// #endif
let createOrderData = {
provider: options.provider,
total_fee: options.total_fee,
openid: myOpenid,
order_no: options.order_no || this.res.order_no,
out_trade_no: options.out_trade_no || this.res.out_trade_no,
description: options.description,
type: options.type,
qr_code: options.qr_code,
custom: options.custom,
other: options.other,
};
if (myOpenid) {
createOrderData.openid = myOpenid;
}
// #ifdef H5
if (options.openid && options.provider === "wxpay") createOrderData.openid = options.openid;
// #endif
let res = await uniPayCo.createOrder(createOrderData);
if (res.errCode === 0) {
this.$emit("create", res);
this.res = res;
if (res.qr_code) {
if (!options.cancel_popup) {
// 展示组件自带的二维码弹窗
if (this.modeCom === "pc") {
this.$refs.payPopup.open();
this._pcChooseProvider(options.provider);
} else {
this.$refs.qrcodePopup.open();
}
}
} else if (res.order) {
// #ifdef H5
if (res.provider_pay_type === "jsapi") {
// 微信公众号支付
WeixinJSBridge.invoke("getBrandWCPayRequest", res.order, (res) => {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 用户支付成功回调
this._getOrder();
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
// 用户取消支付回调
this.$emit("cancel", res);
} else if (res.err_msg == "get_brand_wcpay_request:fail") {
// 用户支付失败回调
console.error('getBrandWCPayRequest-fail: ', res);
this.$emit("fail", res);
}
});
} else {
// 外部浏览器支付
let codeUrl = res.order.codeUrl;
let mwebUrl = res.order.mwebUrl || res.order.mweb_url;
setTimeout(() => {
this.$refs.payConfirmPopup.open();
window.location.href = codeUrl || mwebUrl;
}, 200);
}
// #endif
// #ifndef H5
uni.requestPayment({
// #ifdef APP-PLUS
provider: res.provider, // App端此参数必填可以通过uni.getProvider获取
// #endif
// #ifdef MP-WEIXIN
...res.order,
// #endif
// #ifdef APP-PLUS || MP-ALIPAY
orderInfo: res.order,
// #endif
...res.order,
success:(res)=>{
this._getOrder();
},
fail:(err)=>{
if (err.errMsg.indexOf("fail cancel") == -1) {
// 发起支付失败
console.error("uni.requestPayment:fail", err);
this.$emit("fail", err);
} else {
// 用户取消支付
this.$emit("cancel", err);
}
}
});
// #endif
}
}
},
// 查询订单(查询支付情况)
async getOrder(data = {}) {
try {
let res = await uniPayCo.getOrder(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 发起退款此接口需要admin角色才可以访问
async refund(data = {}) {
try {
let res = await uniPayCo.refund(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 查询退款(查询退款情况)
async getRefund(data = {}) {
try {
let res = await uniPayCo.getRefund(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 关闭订单
async closeOrder(data = {}) {
try {
let res = await uniPayCo.closeOrder(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 获取支持的支付供应商
async getPayProviderFromCloud(data = {}) {
try {
let res = await uniPayCo.getPayProviderFromCloud(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 获取支付配置内的appid主要用于获取获取微信公众号的appid用以获取code
async getProviderAppId(data = {}) {
try {
let res = await uniPayCo.getProviderAppId(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 根据code获取openid
async getOpenid(data = {}) {
try {
let res = await uniPayCo.getOpenid(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 验证iosIap苹果内购支付凭据
async verifyReceiptFromAppleiap(data = {}) {
try {
let res = await uniPayCo.verifyReceiptFromAppleiap(data);
if (typeof data.success === "function") data.success(res);
return res;
} catch (err) {
if (typeof data.fail === "function") data.fail(err);
}
},
// 获取code
async getCode() {
// #ifdef MP-WEIXIN
return jsSdk.getWeixinCode();
// #endif
// #ifdef MP-ALIPAY
return jsSdk.getAlipayCode();
// #endif
},
// 支付成功后的逻辑
paySuccess(res={}) {
this.$refs.payPopup.close();
this.$refs.payConfirmPopup.close();
this.clearQrcode();
if (this.toSuccessPage){
// 跳转到支付成功的内置页面
this.pageToSuccess(res);
}
this.$emit("success", res);
},
pageToSuccess(res){
if (this.modeCom !== "pc") {
uni.navigateTo({
url:`/uni_modules/uni-pay/pages/success/success?out_trade_no=${res.out_trade_no}&order_no=${res.pay_order.order_no}&pay_date=${res.pay_order.pay_date}&total_fee=${res.pay_order.total_fee}&adpid=${this.adpid}&return_url=${this.returnUrl}&main_color=${this.mainColor}`
});
} else {
if (this.returnUrl) {
let url = this.returnUrl + `?out_trade_no=${res.out_trade_no}&order_no=${res.pay_order.order_no}`;
if (url.indexOf("/") !== 0) url = `/${url}`;
uni.navigateTo({
url,
});
}
}
},
// 监听 - 关闭二维码弹窗
clearQrcode() {
this.res.codeUrl = "";
this.res.qr_code_image = "";
},
// 内部函数查询支付状态
async _getOrder() {
this.getOrder({
out_trade_no: this.res.out_trade_no,
await_notify: true,
success: (res) => {
if (res.has_paid) {
this.$refs.qrcodePopup.close();
this.paySuccess(res);
}
}
});
},
// 关闭二维码支付弹窗
clearQrcodePopup(){
this.$refs.qrcodePopup.close();
},
// 重新发起支付
_afreshPayment(){
this.createOrder();
},
// 关闭确认弹出
clearPayConfirmPopup(){
this.$refs.payConfirmPopup.close();
},
// pc版弹窗选择支付方式
_pcChooseProvider(provider){
if (provider === this.options.provider) {
return;
}
return this.createOrder({ provider: provider })
},
// ios内购支付逻辑
async _appleiapCreateOrder(options){
// 初始化ios内购商品
let appleiap = new appleiapSdk.Iap({
// products为苹果开发者后台的商品id数组
products: [options.productid]
});
uni.showLoading({
title: '加载中...'
});
// 初始化获取iap支付通道
await appleiap.init();
// 从苹果服务器获取产品列表
let productList = await appleiap.getProduct();
let productInfo = productList[0];
options.total_fee = productInfo.price * 100;
options.description = productInfo.description;
let createOrderData = {
provider: options.provider,
total_fee: options.total_fee,
order_no: options.order_no || this.res.order_no,
out_trade_no: options.out_trade_no || this.res.out_trade_no,
description: options.description,
type: options.type,
custom: options.custom,
};
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("正在请求苹果服务器", options.productid, res.out_trade_no);
let requestPaymentRes = await appleiap.requestPayment({
productid: options.productid,
username: res.out_trade_no
});
if (this.debug) console.log('用户支付成功', requestPaymentRes);
uni.showLoading({
title: '正在处理支付结果...'
});
// 云端请求苹果服务器验证票据
let verifyRes = await this.verifyReceiptFromAppleiap({
out_trade_no: requestPaymentRes.payment.username,
transaction_receipt: requestPaymentRes.transactionReceipt,
transaction_identifier: requestPaymentRes.transactionIdentifier
});
if (verifyRes.errCode === 0) {
// 完结订单
await appleiap.finishTransaction(requestPaymentRes);
uni.hideLoading();
this.paySuccess(verifyRes);
}
} 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();
}
}
},
// ios内购支付漏单重试
async appleiapRestore(){
uni.showLoading({
title: '检测支付环境...'
});
// 初始化
let appleiap = new appleiapSdk.Iap();
// 初始化获取iap支付通道
await appleiap.init();
try {
if (this.debug) console.log("正在查询是否有漏单信息");
const transactions = await appleiap.restoreCompletedTransactions({
username: ""
});
if (this.debug) console.log('漏单查询结果:' + (transactions.length === 0 ? '未漏单' : "有漏单"), transactions);
if (!transactions.length) {
return;
}
// 开发者业务逻辑,从服务器获取当前用户未完成的订单列表,和本地的比较
for (let i = 0; i < transactions.length; i++) {
let requestPaymentRes = transactions[i];
switch (transaction.transactionState) {
case appleiapSdk.IapTransactionState.purchased:
// 云端请求苹果服务器验证票据
let verifyRes = await this.verifyReceiptFromAppleiap({
out_trade_no: requestPaymentRes.payment.username,
transaction_receipt: requestPaymentRes.transactionReceipt,
transaction_identifier: requestPaymentRes.transactionIdentifier
});
if (verifyRes.errCode === 0) {
// 完结订单
await appleiap.finishTransaction(requestPaymentRes);
}
break;
case appleiapSdk.IapTransactionState.failed:
// 关闭未支付的订单
await appleiap.finishTransaction(requestPaymentRes);
break;
default:
break;
}
}
} catch (e) {
console.error(e)
} finally {
uni.hideLoading();
}
}
},
watch: {
},
computed: {
modeCom(){
if (this.mode) return this.mode;
let systemInfo = uni.getSystemInfoSync();
return systemInfo && systemInfo.deviceType === "pc" ? "pc" : "mobile";
}
},
}
</script>
<style lang="scss" scoped>
.uni-pay {
--bgcolor: #f3f3f3;
}
/* 手机版收银台弹窗开始 */
.mobile-pay-popup {
min-height: 70vh;
background-color: var(--bgcolor);
border-radius: 30rpx 30rpx 0 0;
overflow: hidden;
.mobile-pay-popup-title {
background-color: #ffffff;
text-align: center;
font-weight: bold;
font-size: 40rpx;
padding: 20rpx;
}
.mobile-pay-popup-amount-box {
background-color: #ffffff;
padding: 30rpx;
.mobile-pay-popup-amount {
color: #e43d33;
font-size: 60rpx;
margin-top: 20rpx;
}
}
.mobile-pay-popup-provider-list {
background-color: #ffffff;
margin-top: 20rpx;
}
}
/* 手机版收银台弹窗结束 */
/* PC版收银台弹窗开始 */
.pc-pay-popup {
width: 800px;
height: 600px;
background-color: var(--bgcolor);
border-radius: 10px;
overflow: hidden;
.pc-pay-popup-title{
background-color: #ffffff;
text-align: center;
font-weight: bold;
font-size: 20px;
height: 66px;
line-height: 66px;
}
.pc-pay-popup-flex{
width: 100%;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
.pc-pay-popup-qrcode-box{
height: calc(600px - 66px);
flex: 1;
background-color: #ffffff;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
.pc-pay-popup-qrcode-image{
width: 225px;
height: 225px;
}
.pc-pay-popup-amount-box{
text-align: center;
.pc-pay-popup-amount-tips{
color: #333;
font-size: 20px;
margin-top: 20px;
}
.pc-pay-popup-amount{
color: #dd524d;
font-weight: bold;
font-size: 32px;
margin-top: 20px;
}
}
.pc-pay-popup-complete-button{
margin-top: 20px;
}
}
.pc-pay-popup-provider-list{
width: 300px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
.pc-pay-popup-provider-item{
padding: 20px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
.pc-pay-popup-provider-image{
width: 60px;
height: 60px;
}
.pc-pay-popup-provider-text{
color: #333;
font-size: 20px;
margin-left: 10px;
}
}
.pc-pay-popup-provider-item.active{
background-color: #ffffff;
}
.pc-pay-popup-provider-item:hover{
background-color: #ffffff;
cursor: pointer;
}
.pc-pay-popup-logo{
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
image{
width: 120px;
}
}
}
}
}
/* PC版收银台弹窗结束 */
/* 二维码支付弹窗开始 */
.qrcode-popup-content {
width: 600rpx;
background-color: #ffffff;
border-radius: 10rpx;
padding: 40rpx;
box-sizing: border-box;
text-align: center;
.qrcode-image {
width: 450rpx;
height: 450rpx;
}
.qrcode-popup-info {
text-align: center;
padding: 20rpx;
.qrcode-popup-info-fee {
color: red;
font-size: 60rpx;
font-weight: bold;
}
}
.qrcode-popup-cancel{
margin-top: 20rpx;
text-align: center;
}
}
/* 二维码支付弹窗结束 */
/* 外部浏览器H5支付弹窗确认开始 */
.pay-confirm-popup-content {
width: 550rpx;
background-color: #ffffff;
border-radius: 10rpx;
padding: 40rpx;
.pay-confirm-popup-title {
text-align: center;
padding: 20rpx 0;
margin-bottom: 30rpx;
}
.pay-confirm-popup-refresh{
margin-top: 20rpx;
}
.pay-confirm-popup-cancel{
margin-top: 20rpx;
text-align: center;
}
}
/* 外部浏览器H5支付弹窗确认结束 */
</style>

View File

@@ -0,0 +1,123 @@
// uni iap
const IapTransactionState = {
purchasing: "0", // A transaction that is being processed by the App Store.
purchased: "1", // A successfully processed transaction.
failed: "2", // A failed transaction.
restored: "3", // A transaction that restores content previously purchased by the user.
deferred: "4" // A transaction that is in the queue, but its final status is pending external action such as Ask to Buy.
};
class Iap {
constructor(data={}) {
this._productIds = data.products || [];
this._channel = null;
this._channelError = null;
this.ready = false;
}
init() {
return new Promise((resolve, reject) => {
this.getChannels((channel) => {
this.ready = true;
resolve(channel);
}, (err) => {
reject(err);
})
})
}
getProduct(productIds) {
return new Promise((resolve, reject) => {
this._channel.requestProduct(productIds || this._productIds, (res) => {
resolve(res);
}, (err) => {
reject(err);
})
});
}
requestPayment(orderInfo) {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: "appleiap",
orderInfo: {
quantity: 1,
manualFinishTransaction: true,
...orderInfo
},
success: (res) => {
resolve(res);
},
fail: (err) => {
//console.log('requestPayment-err: ', err)
reject(err);
}
});
});
}
restoreCompletedTransactions(username) {
return new Promise((resolve, reject) => {
this._channel.restoreCompletedTransactions({
manualFinishTransaction: true,
username
}, (res) => {
resolve(res);
}, (err) => {
console.log('restoreCompletedTransactions-err: ', err)
reject(err);
})
});
}
finishTransaction(transaction) {
return new Promise((resolve, reject) => {
this._channel.finishTransaction(transaction, (res) => {
resolve(res);
}, (err) => {
reject(err);
});
});
}
getChannels(success, fail) {
if (this._channel !== null) {
success(this._channel)
return
}
if (this._channelError !== null) {
fail(this._channelError)
return
}
uni.getProvider({
service: 'payment',
success: (res) => {
this._channel = res.providers.find((channel) => {
return (channel.id === 'appleiap')
})
if (this._channel) {
success(this._channel)
} else {
this._channelError = {
errMsg: 'paymentContext:fail iap service not found'
}
fail(this._channelError)
}
}
});
}
get channel() {
return this._channel;
}
}
export default {
Iap,
IapTransactionState
};

View File

@@ -0,0 +1,158 @@
var util = {};
/*
* 此方法不支持微信公众号
util.getWeixinCode().then((code) => {
});
*/
util.getWeixinCode = function() {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success(res) {
resolve(res.code)
},
fail(err) {
reject(new Error('获取微信code失败'))
}
})
// #endif
// #ifdef APP-PLUS
plus.oauth.getServices((services) => {
let weixinAuthService = services.find((service) => {
return service.id === 'weixin';
});
if (weixinAuthService) {
weixinAuthService.authorize(function(res) {
resolve(res.code);
}, function(err) {
console.log(err);
reject(new Error('获取微信code失败'));
});
}
});
// #endif
})
};
util.getAlipayCode = function() {
// #ifdef APP-PLUS || MP-ALIPAY
return new Promise((resolve, reject) => {
uni.login({
provider: 'alipay',
success(res) {
resolve(res.code);
},
fail(err) {
reject(new Error('获取支付宝code失败可能是没有关联appid或你的支付宝开发者工具还没有登录'));
}
});
});
// #endif
};
util.checkPlatform = function() {
// #ifdef H5
let system = {
win: false,
mac: false,
xll: false
};
let p = navigator.platform;
system.win = p.indexOf("Win") == 0;
system.mac = p.indexOf("Mac") == 0;
system.x11 = p == "X11" || p.indexOf("Linux") == 0;
if (system.win || system.mac || system.xll) {
let ua = navigator.userAgent.toLowerCase();
if (ua.indexOf("micromessenger") > -1) {
// 微信开发者工具下访问(注意微信开发者工具下无法唤起微信公众号支付)
return "pc-weixin";
} else {
return "pc";
}
} else {
if (p.indexOf("iPhone") > -1 || p.indexOf("iPad") > -1) {
return "ios";
} else {
return "android";
}
}
// #endif
};
/**
* 获取当前H5所在的环境
*/
util.getH5Env = function() {
let ua = window.navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger' && (ua.match(/miniprogram/i) == 'miniprogram')) {
// 微信小程序
return "mp-weixin";
}
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
// 微信公众号
return "h5-weixin";
}
if (ua.match(/alipay/i) == 'alipay' && ua.match(/miniprogram/i) == 'miniprogram') {
return "mp-alipay";
}
if (ua.match(/alipay/i) == 'alipay') {
return "h5-alipay";
}
// 外部 H5
return "h5";
};
/**
* 日期格式化
* @params {Date || Number} date 需要格式化的时间
* timeFormat(new Date(),"yyyy-MM-dd hh:mm:ss");
*/
util.timeFormat = function(time, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone = 8) {
try {
if (!time) {
return "";
}
if (typeof time === "string" && !isNaN(time)) time = Number(time);
// 其他更多是格式化有如下:
// yyyy-MM-dd hh:mm:ss|yyyy年MM月dd日 hh时MM分等,可自定义组合
let date;
if (typeof time === "number") {
if (time.toString().length == 10) time *= 1000;
date = new Date(time);
} else {
date = time;
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let opt = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};
export default util;

View File

@@ -0,0 +1,81 @@
{
"id": "uni-pay",
"displayName": "uni-pay",
"version": "2.0.2",
"description": "更简单的支付接口调用方式、拉齐不同支付平台",
"keywords": [
"unipay",
"uni-pay",
"微信支付",
"支付宝",
"ios内购"
],
"repository": "https://gitee.com/dcloud/uniPay.git",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-page"
},
"uni_modules": {
"dependencies": ["uni-config-center","uni-id-common","uni-popup","uni-list"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
<template>
<web-view :src="url"></web-view>
</template>
<script>
export default {
data() {
return {
url: ''
}
},
onLoad(options) {
if (options && options.url) {
this.url = decodeURIComponent(options.url);
}
}
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<!-- 自定义收银台页面模式 -->
<view class="uni-pay">
<view class="mobile-pay-popup" v-if="insideData && insideData.currentProviders">
<view class="mobile-pay-popup-amount-box">
<view>待支付金额</view>
<view class="mobile-pay-popup-amount">{{ (options.total_fee / 100).toFixed(2) }}</view>
</view>
<view class="mobile-pay-popup-provider-list">
<uni-list>
<!-- #ifdef MP-WEIXIN || H5 || APP -->
<uni-list-item v-if="insideData.currentProviders.indexOf('wxpay') > -1" :thumb="insideData.images.wxpay" title="微信支付" @click="createOrder({ provider: 'wxpay' })" clickable link></uni-list-item>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY || H5 || APP -->
<uni-list-item v-if="insideData.currentProviders.indexOf('alipay') > -1" :thumb="insideData.images.alipay" title="支付宝" @click="createOrder({ provider: 'alipay' })" clickable link></uni-list-item>
<!-- #endif -->
</uni-list>
</view>
</view>
<!-- 挂载支付组件 -->
<uni-pay ref="uniPay" :to-success-page="false" @mounted="onMounted" @success="onSuccess"></uni-pay>
</view>
</template>
<script>
export default {
data() {
return {
options: {
total_fee: "",
},
insideData: {}, // uni-pay组件mounted事件获得的数据
adpid: "", // 广告id
return_url: "", // 支付成功后点击查看订单跳转的订单详情页面地址
main_color: "", // 支付成功页面的主色调
}
},
// 监听 - 页面每次【加载时】执行(如:前进)
onLoad(options = {}) {
options = JSON.parse(decodeURI(options.options));
//console.log('options: ', options)
this.options = options;
},
// 监听 - 页面【首次渲染完成时】执行。注意如果渲染速度快,会在页面进入动画完成前触发
onReady(){},
// 监听 - 页面每次【显示时】执行(如:前进和返回) (页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面)
onShow() {},
// 监听 - 页面每次【隐藏时】执行(如:返回)
onHide() {},
// 函数
methods: {
// 监听 - 支付组件加载完毕事件
onMounted(insideData){
this.insideData = insideData;
},
// 发起支付
createOrder(provider){
Object.assign(this.options, provider);
this.$refs.uniPay.createOrder(this.options);
},
// 监听事件 - 支付成功
onSuccess(res){
console.log('success: ', res);
if (res.user_order_success) {
// 代表用户已付款,且你自己写的回调成功并正确执行了
uni.redirectTo({
url:`/uni_modules/uni-pay/pages/success/success?out_trade_no=${res.out_trade_no}&order_no=${res.pay_order.order_no}&pay_date=${res.pay_order.pay_date}&total_fee=${res.pay_order.total_fee}&adpid=${this.adpid}&return_url=${this.return_url}&main_color=${this.main_color}`
});
} else {
// 代表用户已付款,但你自己写的回调执行成功(通常是因为你的回调代码有问题)
}
},
},
// 监听器
watch:{
},
// 计算属性
computed:{
}
}
</script>
<style lang="scss" scoped>
.mobile-pay-popup {
min-height: calc(100vh - var(--window-bottom) - var(--window-top));
background-color: #f3f3f3;
border-radius: 30rpx 30rpx 0 0;
overflow: hidden;
.mobile-pay-popup-title {
background-color: #ffffff;
text-align: center;
font-weight: bold;
font-size: 40rpx;
padding: 20rpx;
}
.mobile-pay-popup-amount-box {
background-color: #ffffff;
padding: 30rpx;
.mobile-pay-popup-amount {
color: #e43d33;
font-size: 60rpx;
margin-top: 20rpx;
}
}
.mobile-pay-popup-provider-list {
background-color: #ffffff;
margin-top: 20rpx;
}
}
</style>

View File

@@ -0,0 +1,233 @@
<template>
<view class="app" :style="styleCom">
<view class="header">
<image :src="images.success" class="success-image"></image>
<view class="success-title">支付成功</view>
<view class="hr"></view>
</view>
<view class="info-box">
<view class="info-amount">¥ {{ (options.total_fee / 100).toFixed(2) }}</view>
<view class="left-circle"></view>
<view class="right-circle"></view>
<view class="info-main">
<view class="info-cell">
<view class="left">订单编号</view>
<view class="right">{{ options.order_no }}</view>
</view>
<view class="info-cell">
<view class="left">付款时间</view>
<view class="right">{{ timeFormat(options.pay_date,'yyyy-MM-dd hh:mm:ss') }}</view>
</view>
</view>
</view>
<!-- 广告位开始 -->
<view class="uni-ad">
<!-- 红包广告-->
<ad-interactive v-if="options.adpid" :adpid="options.adpid" v-slot:default="{data, loading, error}" open-page-path="/uni_modules/uni-pay/pages/ad-interactive-webview/ad-interactive-webview" @error="onaderror">
<view v-if="data" class="ad-interactive">
<!-- 可以自定义此图片组件提供了默认素材通过 uni-ad 后台配置 -->
<image :src="data.imgUrl" mode="widthFix"></image>
</view>
</ad-interactive>
<!-- #ifndef MP-WEIXIN -->
<!-- 注意h5下的广告出来有延迟后续要优化 -->
<!-- <ad v-if="options.adpid" :adpid="options.adpid" type="banner" @error="onaderror"></ad> -->
<!-- #endif -->
</view>
<!-- 广告位结束 -->
<view v-if="options.return_url" class="button-query" @click="queryOrder">查看订单</view>
<view class="footer-hr"></view>
</view>
</template>
<script>
import jsSdk from "../../js_sdk/js_sdk.js"
export default {
data() {
return {
options:{
adpid:"",
main_color:"",
order_no:"",
out_trade_no:"",
total_fee:"",
pay_date:"",
return_url:""
},
images:{
success:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAEAYAAAD6+a2dAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAACAlJREFUeNrtnVlszF8Ux++d4B+JWhqERGJvlRJLJQ1RHsQeFEXFUmt5scWWiPVB2qKERIglSpEg9vJAYn8RQlRFI5ryYm+prbZ7/g/fOdPOtNPfdDrjzEx/n5dvf3fuTM895869v3t/997RKswhQ4ZM69a4SkpSWmmle/TAdbdu0JgYaIsW0ObNoU2auH/a16/QT5+gpaXQwkLos2fQggJFihTdvq0d2qEd795J+yHiQaD794dmZxMREeXnQ40hEfj/5ufDru3boQkJ0v4KW+DApk2hK1bAwU+fygS4rhQUuMphyJCJipL2b8gBR0VHQzdtgpaUSIcuOHC5Nm6EcpdUj0DBtcY3YuZMXL97Jx0aGT5+hC5ZAnU4pOMTvMAbMmS6dEFB796Vdn1ocucOtHNn6XgFLvBERJScDC0tlXZxeFBWBp06VTp+fgbc4YDu2CHtyrCHRxdEFNJdBAxt1AiGHj8u7bfI5Ngx+Llhw0DFTQcq8JiAOXsWqaNGyVXF+kBeHiaikpMxEfX7t7+f5HeTghqpNQK/fz9S7cD/G0aPht9zchAH/7uGOvYp2dnQmTOlXVI/SU1FS5CZ6e8n1LoLQJOfkoIaePKktAtsKjNtmtZaa33ihK/v8LkCoKnh8emDB9BmzaSLbFOZz5+hffuiIhQVWb3Dsgtw9fVKKaWOHIHagQ9NOC6HD7vHzTvW9wCkSNHcubgYMEC6iDa+MGgQ1PrezGsNQQ2KjkYFKCxEn9+ypXTRbGoDr1OIjUWXwOscKrBoAZYutQMfzvBCmcWLveWo0gLw83gEvrgYqfXwsWVEUVKClrxDB0wcffnCr3hpARYsgNqBjwyio/GFnj/f85WqFUArrfScOdIm1w/KyqA8hR5sZs/2THFVAF5zh6u4OGnXRDbfv0PHjoWmpEDPnQvu/42Px819nz6c4tECpKZKuyay+fEDOmYM7spv3oT+/Yv06dOhL18GzQRSpGjaNL6sqABaaaWHDZN2UWTy6xc0JQUBv369+ny8AKR9++DaM3Qo/6Vd6+q10kq/eeNMrvNjYhullOLHtJMmIfAXLnjmQJM8YgSuLl6ENmgQXLuI0BK0bu1sAZKSoHbgA8OfP9CpU2sOPPfF/FAt2IFn+DF+UpIDf8THS7ssMuC+PC0NgT9zxjMHWtx27XDFFUNqv0CPHs4WIDZWxoBIgZvURYsQ+GPHquQwZMg0a4Yv3OXLSOWKIEVsrLMCdO0qa0i4woFPT8cMG6+MqpTDkCHz338I/PnzSO3ZU9pyEBPj7HPsuf5aQ4oUrVrlNfBuj2MPHIAOHixttjstWzpbAM9dslIcOQLH8i7cUGXFCgR+27aa82VkQHl8H0KQIkVRUc6a+vOn7HLne/fcl5W3aIHr69dl7fJk7VpLvxoyZNLTpS31jfJy4QrAO4U6dqzekVwhcnNlHbV5s2+BHzMG+f/8kbXXV1wV4MMHGQNmzbJ0LBFVbCbdsuWfmWbIkMnK8s2+xETot28yfvS3fO/fOwtQXCxjxdOn7uNiH7ouIiKaMwf661dw7Nq1yzc7OnWCvn0r47+6UlTkLMj9+2I2GDJkXrzARadOtasII0dCv3wJjDF790K9z4jC3latkO/5czG/BYR795yODJW9fK9fw8G9evlcEQwZMj174v2vXvn3f3kVrfcdNni9cWNopGxvP3rUWWA+BEmaNm0wYXLjBgxMTLR6B4Zj+fm4SkzE8ObRI+uao0jRqVPQefMwg2dM9YF3OJAvNxepEbA6mhf7ooATJ0rXxerhffJDhvhcLueUK/Tateo/99QpvG798AX5d++W9kRwSE52HbOGBKnTtqwoL3cZXKuK0KAB3rdvH/TKFdfUrE+BX7VKuuTB4e9f+KHSDDBeePxY2rSa4bt+31cuIT8PI6331SP/lCkuR0UahgyZhw+5vBU3PaRI0bVrvjpWBg4gTwwtXGj1DvTtRFb76OEYXheRk+N0T+ieyFEnrl714oCEBOkK6h8bNvjrCrw/Lg4aqcfTedK7t4VD+ATOcCMjA2q9sgn52raFSk2E/WsKCjz9ULWJI0WKuAkMN1avhu7ZgwJXbcJdJ3aSIkV5eUgN9iLMUOHgQc8UL1vDoqIwHuflyeG6Q+jECQS60jMHrbTSvPhy+HBpC/8N3reGVRkHcwZ8g3hO3P8+VpbUVASc1zt8+ACtL4Fndu70DDxjsT2cv/k8U9iqlXRRbGrD27f45sfGogLwCSIVeB3mYPjE5+WvWSNdFJtaQooUrVzpLfCMj3fLfFd9+zZ04EDp8tnUxK1b0CFDeB7EW077kKiIgk8A6dcvYIdEMfjAFy/46Zl0UW2qY+5cXwPP1HqqE33K6dOoCHxQpI0sWVnediJZ4fdeQPd7g0OHoGlp0q6oXxw/Dp0xw9t6BisCdFh0w4YYb/MBB/aZwcHl0iW0wBMmiB0WzbgMIEWKxo2DcotgE1hycwMV+KDBXQNahq1bpR9/hDe8QCczExqG2/dh+Pjx0PryuLWufP4MnTxZOn4Brgi8jv7WLWkXhyY3b0Kr7pSKGFBAz5+NC9eNFXWFfzZuwQL2i3R8hCpE8+bQ9evdHRNp8Ba8detcB0bYuAPHNGkCRy1fDn3yRDp0/sErq5Yt43JJ+zdsgSP79HEfXTx6BJVavcvLqx8+5M2kSK9hzV2IEfZ9jmt9u/PUK4yTu3fHdVwcrvnn4/nM3Bp+Pp4UKeKHKiUlyF9Y6Dq4QiuttOfPx/NCk/Djf0hQD04eJaNOAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIyLTEwLTI3VDE0OjAzOjAyKzA4OjAwisT1owAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMi0xMC0yN1QxNDowMzowMiswODowMPuZTR8AAABQdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX3ZwM212emVpcjcvemhpZnVjaGVuZ2dvbmcuc3ZntdPldAAAAABJRU5ErkJggg=="
},
// 默认颜色
color:{
wxpay:"#01be6e",
alipay:"#108ee9"
}
}
},
// 监听 - 页面每次【加载时】执行(如:前进)
onLoad(options = {}) {
this.options = options;
},
// 监听 - 页面【首次渲染完成时】执行。注意如果渲染速度快,会在页面进入动画完成前触发
onReady(){},
// 监听 - 页面每次【显示时】执行(如:前进和返回) (页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面)
onShow() {},
// 监听 - 页面每次【隐藏时】执行(如:返回)
onHide() {},
// 函数
methods: {
timeFormat: jsSdk.timeFormat,
queryOrder(){
let url = this.options.return_url + `?out_trade_no=${this.options.out_trade_no}&order_no=${this.options.order_no}`;
if (url.indexOf("/") !== 0) url = `/${url}`;
uni.navigateTo({
url,
});
},
onaderror(e) {
console.log("ad-error", e);
},
},
// 监听器
watch:{
"mainColorCom":{
immediate:true,
handler(newVal, oldVal){
// 动态改变导航栏颜色
setTimeout(function(){
uni.setNavigationBarColor({
frontColor: "#ffffff",
backgroundColor: newVal
});
}, 0);
}
}
},
// 计算属性
computed:{
mainColorCom(){
let color = "";
// #ifdef MP-ALIPAY
color = this.options.main_color || this.color.alipay;
// #endif
// #ifndef MP-ALIPAY
color = this.options.main_color || this.color.wxpay
// #endif
return color;
},
styleCom(){
return `--main:${this.mainColorCom};`;
}
}
}
</script>
<style lang="scss" scoped>
.app{
--bgcolor: #f3f3f3;
background-color: var(--bgcolor);
min-height: calc(100vh - var(--window-bottom) - var(--window-top));
}
.header{
background-color: var(--main);
text-align: center;
color: #ffffff;
padding: 80rpx 30rpx 50rpx 30rpx;
.success-image{
width: 120rpx;
height: 120rpx;
}
.success-title{
font-size: 34rpx;
margin-top: 40rpx;
font-weight: bold;
}
.hr{
margin-top: 40rpx;
width: 100%;
height: 30rpx;
border-radius: 20rpx;
opacity: 0.1;
background-color: #000000;
}
}
.info-box{
width: calc(100% - 100rpx);
margin: 0 50rpx;
position: relative;
margin-top: -64rpx;
background-color: #ffffff;
.info-amount{
height: 150rpx;
line-height: 150rpx;
text-align: center;
color: var(--main);
font-weight: bold;
font-size: 60rpx;
border-bottom: 4rpx dashed #f3f3f3;
}
.left-circle{
background-color: var(--bgcolor);
position: absolute;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
top:calc(150rpx - 20rpx);
left:-20rpx;
}
.right-circle{
background-color: var(--bgcolor);
position: absolute;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
top:calc(150rpx - 20rpx);
right:-20rpx;
}
.info-main{
padding: 30rpx;
font-size: 26rpx;
color: #333333;
.info-cell{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
line-height: 50rpx;
.left{
width: 200rpx;
text-align: left;
}
.right{
flex: 1;
text-align: right;
}
}
}
}
.uni-ad{
margin-top: 50rpx;
min-height: 100rpx;
.ad-interactive{
text-align: center;
}
}
.button-query{
background-color: var(--main);
color: #ffffff;
width: calc(100% - 120rpx);
margin: 50rpx 60rpx 0 60rpx;
padding: 20rpx 30rpx;
border-radius: 50rpx;
text-align: center;
box-sizing: border-box;
}
.button-query:active{
opacity: 0.7;
}
.footer-hr{
height: 100rpx;
display: block;
}
</style>

View File

@@ -0,0 +1,31 @@
`uni-pay`已升级为`uni-pay 2.x`从公共模块升级为包含前端页面、uni-pay-co云对象让支付更加简单省心 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-pay.html)
## 简介
支付,重要的变现手段,但开发复杂。在不同端,对接微信支付、支付宝等渠道,前端后端都要写不少代码。
涉及金额可不是小事,生成业务订单、获取收银台、发起支付、支付状态查询、支付异步回调、失败处理、发起退款、退款状态查询、支付统计...众多环节,代码量多,出错率高。
为什么不能有一个开源的、高质量的项目即可以避免大家重复开发又可以安心使用不担心自己从头写产生Bug。
`uni-pay`应需而生。
之前`uni-pay 1.x`版本,仅是一个公共模块,它让开发者无需研究支付宝、微信等支付平台的后端开发、无需为它们编写不同代码,拿来即用,屏蔽差异。
但开发者还是需要自己编写前端页面和云函数,还是有一定的开发难度和工作量的,特别对于新手来说,门槛高、易出错。
`uni-pay 2.0` 起,补充了前端页面和云对象,让开发者开箱即用。
**注意:`uni-pay 2` 仍内置了uni-pay公共模块向下兼容`uni-pay 1.x`,即从`uni-pay 1.x`可以一键升级到`uni-pay 2.x`,且不会对你的老项目造成影响。**
开发者在项目中引入 `uni-pay` 后,微信支付、支付宝支付等功能无需自己再开发。由于源码的开放性和层次结构清晰,有二次开发需求也很方便调整。
> 下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-pay](https://ext.dcloud.net.cn/plugin?name=uni-pay)
> 开发文档:[https://uniapp.dcloud.io/uniCloud/uni-pay](https://uniapp.dcloud.io/uniCloud/uni-pay)
**线上体验地址**
注意:线上体验地址用的是阿里云免费版,免费版请求次数有限,如请求失败为正常现象,可直接导入示例项目绑定自己的空间体验。
![](https://f184e7c3-1912-41b2-b81f-435d1b37c7b4.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/a9489bd6-37d6-4664-9e0b-346f8859534c.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
{
"name": "uni-pay",
"version": "2.0.0",
"description": "unipay for uniCloud",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/unipay",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uniPay.git"
},
"scripts": {},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"devDependencies": {}
}

View File

@@ -0,0 +1,64 @@
/**
* 全局错误码
*/
const ERROR = {
50403: 50403,
// 参数错误
51001: 51001,
51002: 51002,
51003: 51003,
51004: 51004,
51005: 51005,
51006: 51006,
51007: 51007,
51008: 51008,
51009: 51009,
51010: 51010,
// 数据不存在
52001: 52001,
52002: 52002,
// 运行错误
53001: 53001,
53002: 53002,
53003: 53003,
53004: 53004,
53005: 53005,
}
const errSubject = "uni-pay";
function isUniPayError(errCode) {
return Object.values(ERROR).includes(errCode);
}
class UniCloudError extends Error {
constructor(options = {}) {
super(options.message);
this.errMsg = options.message || '';
this.code = this.errCode = options.code;
this.errSubject = options.subject || errSubject;
this.forceReturn = options.forceReturn || false;
this.cause = options.cause;
}
toJson(level = 0) {
if (level >= 10) {
return
}
level++
return {
errCode: this.errCode,
errMsg: this.errMsg,
code: this.errCode,
message: this.message,
errSubject: this.errSubject,
cause: this.cause && this.cause.toJson ? this.cause.toJson(level) : this.cause
}
}
}
module.exports = {
ERROR,
isUniPayError,
UniCloudError
}

View File

@@ -0,0 +1,9 @@
// 各接口权限配置,未配置接口表示允许任何用户访问(包括未登录用户)
module.exports = {
refund: {
// auth: true // 已登录用户方可操作,配置角色或权限时此项可不写
role: ['admin'] // 允许进行此操作的角色,包含任一角色均可操作。
// permission: [] // 允许进行此操作的权限,包含任一权限均可操作。
// 权限角色均配置时,用户拥有任一权限或任一角色均可操作
}
}

View File

@@ -0,0 +1,6 @@
const uniPayOrders = require("./uniPayOrders");
const uniIdUsers = require("./uniIdUsers");
module.exports = {
uniPayOrders,
uniIdUsers
};

View File

@@ -0,0 +1 @@
dao名词解释Data Access Object

View File

@@ -0,0 +1,25 @@
const dbName = {
user: "uni-id-users"
}
const db = uniCloud.database();
const _ = db.command;
var dao = {};
/**
* 获取 - 第三方支付订单数据
let userInfo = await dao.uniIdUsers.findById(id);
*/
dao.findById = async (id) => {
let res = await db.collection(dbName.user).doc(id).get();
if (res.data && res.data.length > 0) {
return res.data[0];
} else {
return null;
}
return res;
};
module.exports = dao;

View File

@@ -0,0 +1,108 @@
const dbName = {
payOrders: "uni-pay-orders" // 数据库表名 - 第三方支付订单表
}
const db = uniCloud.database();
const _ = db.command;
var dao = {};
/**
* 添加 - 第三方支付订单数据
await dao.uniPayOrders.add({
});
*/
dao.add = async (dataJson = {}) => {
// 数据库操作开始-----------------------------------------------------------
let res = await db.collection(dbName.payOrders).add(dataJson);
// 数据库操作结束-----------------------------------------------------------
return res.id ? res.id : null;
};
/**
* 获取 - 第三方支付订单数据
let payOrderInfo = await dao.uniPayOrders.find({
order_no,
out_trade_no
});
*/
dao.find = async (where) => {
let res = await db.collection(dbName.payOrders)
.where(where)
.limit(1)
.get();
if (res.data && res.data.length > 0) {
return res.data[0];
} else {
return null;
}
return res;
};
/**
* 修改 - 第三方支付订单数据
await dao.uniPayOrders.updateById(id, {
});
*/
dao.updateById = async (id = "___", dataJson) => {
// 数据库操作开始-----------------------------------------------------------
let res = await db.collection(dbName.payOrders).doc(id).update(dataJson);
// 数据库操作结束-----------------------------------------------------------
return res ? res.updated : 0;
};
/**
* 修改 - 第三方支付订单数据
await dao.uniPayOrders.update({
whereJson:{
},
dataJson:{
}
});
*/
dao.update = async (obj) => {
let { whereJson, dataJson } = obj;
// 数据库操作开始-----------------------------------------------------------
let res = await db.collection(dbName.payOrders).where(whereJson).update(dataJson);
// 数据库操作结束-----------------------------------------------------------
return res ? res.updated : 0;
};
/**
* 修改 - 第三方支付订单数据
await dao.uniPayOrders.updateAndReturn({
whereJson:{
},
dataJson:{
}
});
*/
dao.updateAndReturn = async (obj) => {
let { whereJson, dataJson } = obj;
// 数据库操作开始-----------------------------------------------------------
let res = await db.collection(dbName.payOrders).where(whereJson).updateAndReturn(dataJson);
// 数据库操作结束-----------------------------------------------------------
return res.doc ? res.doc : null;
};
/**
* 删除超过3天还未支付款的订单
await dao.uniPayOrders.deleteExpPayOrders();
*/
dao.deleteExpPayOrders = async () => {
// 数据库操作开始-----------------------------------------------------------
let time = Date.now() - 1000 * 3600 * 24 * 3;
let res = await db.collection(dbName.payOrders)
.where({
status: _.in([-1,0]),
create_date: _.lt(time)
})
.remove();
// 数据库操作结束-----------------------------------------------------------
return res ? res.updated : 0;
};
module.exports = dao;

View File

@@ -0,0 +1,268 @@
/**
* uni-pay-co 统一支付云对象
*/
// 加载服务
const service = require('./service');
// 加载全局错误码
const { UniCloudError, isUniPayError, ERROR } = require('./common/error');
// 加载全局中间件
const middleware = require('./middleware/index');
// 加载uniId公共模块
const uniIdCommon = require('uni-id-common');
module.exports = {
/**
* 中间件(请求前执行)
*/
async _before() {
const params = this.getParams();
let clientInfo;
if (params && params[0] && params[0].clientInfo) {
clientInfo = params[0].clientInfo;
} else {
clientInfo = this.getClientInfo();
}
// 挂载uni-id实例到this上方便后续调用
this.uniIdCommon = uniIdCommon.createInstance({
clientInfo
});
// 国际化开始
const i18n = uniCloud.initI18n({
locale: clientInfo.locale || 'zh-Hans',
fallbackLocale: 'zh-Hans',
messages: require('./lang/index')
})
this.t = i18n.t.bind(i18n);
// 国际化结束
// 挂载中间件
this.middleware = {}
for (const mwName in middleware) {
this.middleware[mwName] = middleware[mwName].bind(this);
}
// 尝试从token获取用户信息
await this.middleware.auth(false);
// 通用权限校验模块
await this.middleware.accessControl();
// 设置全局获取userId公共函数可在此云对象的任意其他函数内通过 this.getUserId() 获取当前登录用户的id
this.getUserId = () => {
return this.authInfo && this.authInfo.uid ? this.authInfo.uid : undefined;
}
},
/**
* 中间件(请求后执行)
*/
_after(error, result) {
if (error) {
if (error.errCode) {
let errCode = error.errCode
if (!isUniPayError(errCode)) {
// 如果不是插件预设的错误码,则原样返回错误信息
return error;
}
return new UniCloudError({
code: errCode,
message: error.errMsg || this.t(errCode, error.errMsgValue),
});
}
throw error
}
return result;
},
/**
* 创建支付订单
*/
async createOrder(data) {
let {
provider, // 支付供应商 如 wxpay alipay 参考 https://uniapp.dcloud.net.cn/api/plugins/provider.html#
total_fee, // 订单总金额单位为分100等于1元
openid, // 发起支付的用户openid
order_no, // 业务系统订单号 建议控制在20-28位(不可以是24位,24位在阿里云空间可能会有问题)(可重复,代表1个业务订单会有多次付款的情况)
out_trade_no, // 支付插件订单号(需控制唯一,不传则由插件自动生成)
description, // 支付描述uniCloud个人版包月套餐
type, // 订单类型 goods订单付款 recharge余额充值付款 vipvip充值付款 等等,可自定义
qr_code, // true 强制开启二维码支付模式
custom, // 自定义参数(不会发送给第三方支付服务器)
other, // 其他请求参数(会发送给第三方支付服务器),
clientInfo, // 兼容云对象调用云对象模式
cloudInfo, // 兼容云对象调用云对象模式
} = data;
if (!clientInfo) clientInfo = this.getClientInfo();
if (!cloudInfo) cloudInfo = this.getCloudInfo();
// 获取当前登录的user_id
let user_id = this.getUserId();
let res = await service.pay.createOrder({
provider,
total_fee,
user_id,
openid,
order_no,
out_trade_no,
description,
type,
qr_code,
custom,
other,
clientInfo,
cloudInfo,
});
return res;
},
/**
* 接收支付异步通知
*/
async payNotify(data) {
const httpInfo = this.getHttpInfo();
return service.pay.paymentNotify({
httpInfo,
});
},
/**
* 查询支付状态
*/
async getOrder(data) {
let {
out_trade_no, // 插件订单号
transaction_id, // 第三方支付交易单号
await_notify = false, // 是否需要等待异步通知执行完成若为了响应速度可以设置为false若需要等待异步回调执行完成则设置为true
} = data;
res = await service.pay.getOrder({
out_trade_no,
transaction_id,
await_notify
});
return res;
},
/**
* 发起退款
* 此api只有admin角色可以访问
*/
async refund(data) {
let {
out_trade_no, // 插件订单号
out_refund_no, // 插件退款订单号
refund_desc, // 退款描述
refund_fee, // 退款金额
} = data;
res = await service.pay.refund({
out_trade_no,
out_refund_no,
refund_desc,
refund_fee,
});
return res;
},
/**
* 查询退款(查询退款情况)
*/
async getRefund(data) {
let {
out_trade_no, // 插件订单号
} = data;
res = await service.pay.getRefund({
out_trade_no,
});
return res;
},
/**
* 关闭订单(只有订单未支付时,才可以关闭,关闭后,用户即使在付款页面也无法付款)
*/
async closeOrder(data) {
let {
out_trade_no, // 插件订单号
} = data;
res = await service.pay.closeOrder({
out_trade_no,
});
return res;
},
/**
* 根据code获取openid
*/
async getOpenid(data = {}) {
let {
provider,
code,
clientInfo, // 兼容云对象调用云对象模式
} = data;
if (!clientInfo) clientInfo = this.getClientInfo();
res = await service.pay.getOpenid({
provider,
code,
clientInfo
});
return res;
},
/**
* 获取当前支持的支付方式
*/
async getPayProviderFromCloud() {
return await service.pay.getPayProviderFromCloud();
},
/**
* 获取支付配置内的appid主要用于获取获取微信公众号的appid用以获取code
*/
async getProviderAppId(data) {
let {
provider,
provider_pay_type
} = data;
// 注意,前往不要直接把 conifg 内的所有内容返回给前端
let conifg = service.pay.getConfig();
try {
return {
errorCode: 0,
appid: conifg[provider][provider_pay_type].appId,
}
} catch (err) {
return {
errorCode: 0,
appid: null
};
}
},
/**
* 验证iosIap苹果内购支付凭据
*/
async verifyReceiptFromAppleiap(data) {
let {
out_trade_no,
transaction_receipt,
transaction_identifier,
} = data;
return await service.pay.verifyReceiptFromAppleiap({
out_trade_no,
transaction_receipt,
transaction_identifier
});
},
}

View File

@@ -0,0 +1,29 @@
const word = {
};
const sentence = {
50403: 'Permission denied',
51001: 'Invalid out_trade_no',
51002: 'Invalid code',
51003: 'Invalid order_no',
51004: 'Invalid type',
51005: 'Invalid total_fee',
51006: 'Invalid description',
51007: 'Invalid provider',
51008: 'Invalid clientInfo',
51009: 'Invalid cloudInfo',
51010: 'Invalid out_trade_no or transaction_id',
52001: 'NotExist payOrder',
52002: 'NotExist notifyUrl',
53001: 'Create payment error',
53002: 'Refund error',
53003: 'Query refund error',
53004: 'Close order error',
53005: 'Cert verify error',
};
module.exports = {
...word,
...sentence
}

View File

@@ -0,0 +1,22 @@
let lang = {
'zh-Hans': require('./zh-hans'),
en: require('./en')
}
function mergeLanguage(lang1, lang2) {
const localeList = Object.keys(lang1)
localeList.push(...Object.keys(lang2))
const result = {}
for (let i = 0; i < localeList.length; i++) {
const locale = localeList[i]
result[locale] = Object.assign({}, lang1[locale], lang2[locale])
}
return result
}
try {
const langPath = require.resolve('uni-config-center/uni-id/lang/index.js')
lang = mergeLanguage(lang, require(langPath))
} catch (error) {}
module.exports = lang

View File

@@ -0,0 +1,29 @@
const word = {
};
const sentence = {
50403: '权限错误',
51001: '支付单号out_trade_no不能为空',
51002: 'code不能为空',
51003: '订单号order_no不能为空',
51004: '回调类型type不能为空如设置为goods代表商品订单',
51005: '支付金额total_fee必须为正整数>0的整数注意100=1元',
51006: '支付描述description不能为空',
51007: '支付供应商provider不能为空',
51008: 'clientInfo不能为空',
51009: 'cloudInfo不能为空',
51010: '支付单号或第三方交易单号不能同时为空',
52001: '支付订单不存在',
52002: '请先配置正确的异步回调URL',
53001: '获取支付信息失败,请稍后再试',
53002: '退款失败',
53003: '查询退款信息失败,请稍后再试',
53004: '关闭订单失败,请稍后再试',
53005: '证书错误,请检查支付证书',
};
module.exports = {
...word,
...sentence
}

View File

@@ -0,0 +1,145 @@
/**
* 支付宝相关函数
*/
const common = require('./common');
const crypto = require('crypto');
const ALIPAY_ALGORITHM_MAPPING = {
RSA: 'RSA-SHA1',
RSA2: 'RSA-SHA256'
}
var alipay = {
/**
* 获取openid
*/
async getOpenid(data) {
let {
config={},
code,
} = data;
if (!config.appId) throw new Error("uni-pay配置alipay.mp节点下的appId不能为空");
if (!config.privateKey) throw new Error("uni-pay配置alipay.mp节点下的privateKey不能为空");
let timestamp = common.timeFormat(new Date(), "yyyy-MM-dd hh:mm:ss");
let method = "alipay.system.oauth.token";
let params = {
timestamp,
code,
grant_type: "authorization_code"
};
let signData = this._getSign(method, params, config);
// 格式化url和请求参数
const { url, execParams } = this._formatUrl(signData);
let res = await uniCloud.httpclient.request(url, {
method: 'POST',
data: execParams,
dataType: 'text',
});
const result = JSON.parse(res.data)
let response = result.alipay_system_oauth_token_response;
if (res.status === 200 && response.user_id) {
return {
errCode: 0,
errMsg: 'ok',
openid: response.user_id,
user_id: response.user_id,
}
} else {
return {
errCode: -1,
errMsg: result.error_response.sub_msg
}
}
},
/**
* 签名
* @param {String} method 方法名
* @param {Object} params 参数
* @param {Object} config 配置
*/
_getSign(method, params, config) {
let signParams = Object.assign({
method,
app_id: config.appId,
charset: config.charset || "utf-8",
version: config.version || "1.0",
sign_type: config.signType || "RSA2",
}, params);
if (config.appCertSn && config.alipayRootCertSn) {
signParams = Object.assign({
app_cert_sn: config.appCertSn,
alipay_root_cert_sn: config.alipayRootCertSn,
}, signParams);
}
const bizContent = params.biz_content;
if (bizContent) {
signParams.biz_content = JSON.stringify(bizContent);
}
// 排序
const decamelizeParams = this._sortObj(signParams);
// 拼接url参数
let signStr = this._objectToUrl(decamelizeParams);
let keyType = config.keyType || 'PKCS8';
const privateKeyType = keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY'
let privateKey = this._formatKey(config.privateKey, privateKeyType);
// 计算签名
const sign = crypto.createSign(ALIPAY_ALGORITHM_MAPPING[signParams.sign_type])
.update(signStr, 'utf8').sign(privateKey, 'base64');
return Object.assign(decamelizeParams, { sign });
},
_formatKey(key, type) {
return `-----BEGIN ${type}-----\n${key}\n-----END ${type}-----`
},
_sortObj(params) {
let keysArr = Object.keys(params).sort();
let sortObj = {};
for (let i in keysArr) {
sortObj[keysArr[i]] = (params[keysArr[i]]);
}
return sortObj;
},
_objectToUrl(obj) {
let str = "";
for (let key in obj) {
if (obj[key]) {
str += `&${key}=${obj[key]}`;
}
}
if (str) str = str.substring(1);
return str;
},
_formatUrl(params, url = "https://openapi.alipay.com/gateway.do") {
let requestUrl = url;
// 需要放在 url 中的参数列表
const urlArgs = [
'app_id',
'method',
'format',
'charset',
'sign_type',
'sign',
'timestamp',
'version',
'notify_url',
'return_url',
'auth_token',
'app_auth_token'
]
for (const key in params) {
if (urlArgs.indexOf(key) > -1) {
const val = encodeURIComponent(params[key])
requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'
}${key}=${val}`
// 删除 postData 中对应的数据
delete params[key]
}
}
return { execParams: params, url: requestUrl }
}
};
module.exports = alipay;

View File

@@ -0,0 +1,293 @@
/**
* 通用公共函数
*/
var common = {};
/**
* 日期格式化
*/
common.timeFormat = function(time, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone = 8) {
try {
if (!time) {
return "";
}
if (typeof time === "string" && !isNaN(time)) time = Number(time);
// 其他更多是格式化有如下:
// yyyy-MM-dd hh:mm:ss|yyyy年MM月dd日 hh时MM分等,可自定义组合
let date;
if (typeof time === "number") {
if (time.toString().length == 10) time *= 1000;
date = new Date(time);
} else {
date = time;
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let opt = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[
k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};
/**
* 产生订单号,不依赖数据库,高并发时性能高(理论上会重复,但概率非常非常低)
*/
common.createOrderNo = function(prefix = "", num = 25) {
// 获取当前时间字符串格式如20200803093000123
let timeStr = common.timeFormat(Date.now(), "yyyyMMddhhmmssS");
timeStr = timeStr.substring(2);
let randomNum = num - (prefix + timeStr).length;
return prefix + timeStr + common.random(randomNum, "123456789");
};
/**
* 产生随机数
*/
common.random = function(length, list = "123456789") {
let s = "";
for (let i = 0; i < length; i++) {
let code = list[Math.floor(Math.random() * list.length)];
s += code;
}
return s;
};
/**
* 休眠,等待(单位毫秒)
* @param {Number} ms 毫秒
* await common.sleep(1000);
*/
common.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
/**
* 获取platform
* let provider_pay_type = common.getPlatform(platform);
*/
common.getPlatform = function(platform) {
if (["h5", "web"].indexOf(platform) > -1) {
platform = "web";
} else if (["app", "app-plus"].indexOf(platform) > -1) {
platform = "app";
}
return platform;
};
/**
* 获取 provider_pay_type
let provider_pay_type = common.getProviderPayType({
platform,
provider,
ua,
qr_code
});
*/
common.getProviderPayType = function(data) {
let {
platform,
provider,
ua,
qr_code
} = data;
// 扫码支付
if (qr_code) return "native";
// 小程序支付
if (platform.indexOf("mp") > -1) return "mp";
// APP支付
if (platform.indexOf("app") > -1) return "app";
// 微信公众号支付
if (platform === "web" && provider === "wxpay" && ua.toLowerCase().indexOf("micromessenger") > -1) return "jsapi";
// 微信外部浏览器支付
if (platform === "web" && provider === "wxpay" && ua.toLowerCase().indexOf("micromessenger") === -1) return "mweb";
if (platform === "web" && provider === "alipay") return "native";
throw new Error(`不支持的支付方式${provider}-${platform}`);
};
/**
* 获取uniPay交易类型
let tradeType = common.getTradeType({ provider, provider_pay_type });
*/
common.getTradeType = function(data) {
let { provider, provider_pay_type } = data;
let pay_type = `${provider}_${provider_pay_type}`;
let obj = {
// 微信
"wxpay_app": "APP", // 微信app支付
"wxpay_mp": "JSAPI", // 微信小程序支付
"wxpay_native": "NATIVE", // 微信扫码支付
"wxpay_mweb": "MWEB", // 微信外部浏览器支付
"wxpay_jsapi": "JSAPI", // 微信公众号支付
// 支付宝
"alipay_app": "APP", // 支付宝app支付
"alipay_mp": "JSAPI", // 支付宝小程序支付
"alipay_native": "NATIVE", // 支付宝扫码支付
"alipay_mweb": "NATIVE", // 支付宝外部浏览器支付
};
return obj[pay_type];
};
/**
* 给第三方服务器返回成功通知
*/
common.returnNotifySUCCESS = function(data) {
let { provider, provider_pay_type } = data;
if (provider === "wxpay") {
// 微信支付需返回 xml 格式的字符串
return {
mpserverlessComposedResponse: true,
statusCode: 200,
headers: {
'content-type': 'text/xml;charset=utf-8'
},
body: "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"
};
} else if (provider === "alipay") {
// 支付宝支付直接返回 success 字符串
return {
mpserverlessComposedResponse: true,
statusCode: 200,
headers: {
'content-type': 'text/plain'
},
body: "success"
}
}
return "success";
};
// 获取异步通知的参数,并转成json对象
common.getNotifyData = function(data) {
let {
provider,
httpInfo
} = data;
let json = {};
let body = httpInfo.body;
if (httpInfo.isBase64Encoded) {
body = Buffer.from(body, 'base64').toString('utf-8');
}
if (provider === "wxpay") {
if (body.indexOf("<xml>") > -1) {
// 微信支付v2
json = common.parseXML(body);
} else {
// 微信支付v3
json = common.urlStringToJson(body);
}
} else if (provider === "alipay") {
// 支付宝支付
json = common.urlStringToJson(body);
}
return json;
};
// 简易版XML转Object只可在微信支付时使用不支持嵌套
common.parseXML = function(xml) {
const xmlReg = /<(?:xml|root).*?>([\s|\S]*)<\/(?:xml|root)>/
const str = xmlReg.exec(xml)[1]
const obj = {}
const nodeReg = /<(.*?)>(?:<!\[CDATA\[){0,1}(.*?)(?:\]\]>){0,1}<\/.*?>/g
let matches = null
// eslint-disable-next-line no-cond-assign
while ((matches = nodeReg.exec(str))) {
obj[matches[1]] = matches[2]
}
return obj
};
// url参数转json
common.urlStringToJson = function(str) {
let json = {};
if (str != "" && str != undefined && str != null) {
let arr = str.split("&");
for (let i = 0; i < arr.length; i++) {
let arrstr = arr[i].split("=");
let k = arrstr[0];
let v = arrstr[1];
json[k] = v;
}
}
return json;
};
const isSnakeCase = new RegExp('_(\\w)', 'g');
const isCamelCase = new RegExp('[A-Z]', 'g');
function parseObjectKeys(obj, type) {
let parserReg;
let parser;
switch (type) {
case 'snake2camel':
parser = common.snake2camel
parserReg = isSnakeCase
break
case 'camel2snake':
parser = common.camel2snake
parserReg = isCamelCase
break
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (parserReg.test(key)) {
const keyCopy = parser(key)
obj[keyCopy] = obj[key]
delete obj[key]
if (Object.prototype.toString.call((obj[keyCopy])) === '[object Object]') {
obj[keyCopy] = parseObjectKeys(obj[keyCopy], type)
} else if (Array.isArray(obj[keyCopy])) {
obj[keyCopy] = obj[keyCopy].map((item) => {
return parseObjectKeys(item, type)
})
}
}
}
}
return obj
}
common.snake2camel = function(value) {
return value.replace(isSnakeCase, (_, c) => (c ? c.toUpperCase() : ''))
}
common.camel2snake = function(value) {
return value.replace(isCamelCase, str => '_' + str.toLowerCase())
}
// 转驼峰
common.snake2camelJson = function(obj) {
return parseObjectKeys(obj, 'snake2camel');
};
// 转蛇形
common.camel2snakeJson = function(obj) {
return parseObjectKeys(obj, 'camel2snake');
};
module.exports = common;

View File

@@ -0,0 +1,82 @@
/**
* 加密模块
*/
/*
加密解密示例
const payCrypto = require('../libs/crypto.js'); // 获取加密服务(注意文件所在相对路径)
let ciphertext = { a:1,b:2 };
let encrypted = payCrypto.aes.encrypt({
data: ciphertext, // 待加密的原文
});
let decrypted = payCrypto.aes.decrypt({
data: encrypted, // 待解密的原文
});
// 最终解密得出 decrypted = { a:1,b:2 }
*/
const configCenter = require("uni-config-center");
const config = configCenter({ pluginId: 'uni-pay' }).requireFile('config.js');
const crypto = require("crypto");
var util = {};
util.aes = {};
/**
* aes加密
* @param {Object} data 待加密的原文
* @param {Object} key 密钥如不传自动取config
* 调用示例
let encrypted = crypto.aes.encrypt({
data: "", // 待加密的原文
});
*/
util.aes.encrypt = function(obj) {
let {
data, // 待加密的原文
key, // 密钥如不传自动取config
} = obj;
if (!key) key = config.notifyKey;
if (typeof data === "object") data = JSON.stringify(data);
const cipher = crypto.createCipher('aes192', key);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
// encrypted 为加密后的内容
return encrypted;
};
/**
* aes解密
* @param {Object} data 待解密的原文
* @param {Object} key 密钥如不传自动取config
* 调用示例
let decrypted = crypto.aes.decrypt({
data: "", // 待解密的原文
});
*/
util.aes.decrypt = function(obj) {
let {
data, // 待解密的原文
key, // 密钥如不传自动取config
} = obj;
if (typeof data === "undefined") {
throw "待解密原文不能为空";
}
if (!key) key = config.notifyKey;
// 解密
let decrypted;
try {
const decipher = crypto.createDecipher('aes192', key);
decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
try {
decrypted = JSON.parse(decrypted);
} catch (err) {}
} catch (err) {
throw "解密失败";
}
// decrypted 为解密后的内容即最开始需要加密的原始数据文本data
return decrypted;
};
module.exports = util;

View File

@@ -0,0 +1,13 @@
const wxpay = require('./wxpay');
const alipay = require('./alipay');
const common = require('./common');
const qrcode = require('./qrcode'); // 此源码为npm i qrcode的压缩版本
const crypto = require('./crypto');
module.exports = {
wxpay,
alipay,
common,
qrcode,
crypto
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,82 @@
/**
* 微信支付相关函数
*/
var wxpay = {
async getOpenid(data) {
let {
provider_pay_type
} = data;
if (provider_pay_type === "jsapi") {
return this._getJsOpenid(data);
} else {
return this._getMpOpenid(data);
}
},
async _getMpOpenid(data) {
let {
config = {},
code,
provider_pay_type
} = data;
if (!config.appId) throw new Error("uni-pay配置wxpay.mp节点下的appId不能为空");
if (!config.secret) throw new Error("uni-pay配置wxpay.mp节点下的secret不能为空");
let res = await uniCloud.httpclient.request("https://api.weixin.qq.com/sns/jscode2session", {
method: 'GET',
data: {
appid: config.appId,
secret: config.secret,
js_code: code,
grant_type: "authorization_code"
},
contentType: 'json', // 指定以application/json发送data内的数据
dataType: 'json' // 指定返回值为json格式自动进行parse
});
if (res.data && !res.data.errcode && res.data.openid) {
return {
errCode: 0,
errMsg: 'ok',
openid: res.data.openid,
unionid: res.data.unionid,
}
} else {
return {
errCode: -1,
errMsg: res.data.errmsg
}
}
},
async _getJsOpenid(data) {
let {
config = {},
code,
provider_pay_type
} = data;
if (!config.appId) throw new Error("uni-pay配置wxpay.jsapi节点下的appId不能为空");
if (!config.secret) throw new Error("uni-pay配置wxpay.jsapi节点下的secret不能为空");
let res = await uniCloud.httpclient.request("https://api.weixin.qq.com/sns/oauth2/access_token", {
method: 'GET',
data: {
appid: config.appId,
secret: config.secret,
code: code,
grant_type: "authorization_code"
},
contentType: 'json', // 指定以application/json发送data内的数据
dataType: 'json' // 指定返回值为json格式自动进行parse
});
if (res.data && !res.data.errcode && res.data.openid) {
return {
errCode: 0,
errMsg: 'ok',
openid: res.data.openid,
unionid: res.data.unionid,
}
} else {
return {
errCode: -1,
errMsg: res.data.errmsg
}
}
}
};
module.exports = wxpay;

View File

@@ -0,0 +1,50 @@
/**
* 权限验证中间件,一般情况下,无需修改此处的代码
*/
const methodPermission = require('../config/permission');
const { ERROR } = require('../common/error');
function isAccessAllowed(user = {}, setting) {
const {
role: userRole = [],
permission: userPermission = []
} = user
const {
role: settingRole = [],
permission: settingPermission = []
} = setting
if (userRole.includes('admin')) {
return;
}
if (settingRole.length > 0 && settingRole.every(item => !userRole.includes(item))) {
throw { errCode: ERROR[50403] };
}
if (settingPermission.length > 0 && settingPermission.every(item => !userPermission.includes(item))) {
throw { errCode: ERROR[50403] };
}
}
module.exports = async function() {
const methodName = this.getMethodName();
if (!(methodName in methodPermission)) {
return;
}
const {
auth,
role,
permission
} = methodPermission[methodName];
if (auth || role || permission) {
await this.middleware.auth();
}
if (role && role.length === 0) {
throw new Error('[AccessControl]Empty role array is not supported');
}
if (permission && permission.length === 0) {
throw new Error('[AccessControl]Empty permission array is not supported');
}
return isAccessAllowed(this.authInfo, {
role,
permission
})
}

View File

@@ -0,0 +1,21 @@
module.exports = async function(key = true) {
if (this.authInfo) { // 多次执行auth时如果第一次成功后续不再执行
return;
}
const token = this.getUniIdToken();
const payload = await this.uniIdCommon.checkToken(token);
if (payload.errCode) {
if (key) {
throw payload;
} else {
return;
}
}
this.authInfo = payload;
if (payload.token && typeof this.response === "object") {
this.response.newToken = {
token: payload.token,
tokenExpired: payload.tokenExpired
}
}
}

View File

@@ -0,0 +1,7 @@
const accessControl = require("./access-control");
const auth = require("./auth");
module.exports = {
accessControl,
auth
}

View File

@@ -0,0 +1,78 @@
'use strict';
/**
* 此处建议只改下订单状态,保证能及时返回给第三方支付服务器成功状态
* 限制4秒内必须执行完全部的异步回调逻辑建议将消息发送、返佣、业绩结算等业务逻辑异步处理如用定时任务去处理这些异步逻辑
*/
const payCrypto = require('../libs/crypto.js'); // 获取加密服务
module.exports = async (obj) => {
let user_order_success = true;
let { data = {} } = obj;
let {
order_no,
out_trade_no,
total_fee
} = data; // uni-pay-orders 表内的数据均可获取到
// 此处写你自己的支付成功逻辑开始-----------------------------------------------------------
// 有三种方式
// 方式一:直接写数据库操作
// 方式二:使用 await uniCloud.callFunction 调用其他云函数或云对象,云对象则使用 uniCloud.importObject('云对象名称')来请求
// 方式三:使用 await uniCloud.httpclient.request 调用http接口地址
/*
// 方式二安全模式一(加密)
let encrypted = payCrypto.aes.encrypt({
data: data, // 待加密的原文
});
await uniCloud.callFunction({
name: "你的云函数名称",
data: {
encrypted, // 传输加密数据
},
});
*/
/*
// 方式二安全模式二(只传一个订单号 out_trade_no你自己的回调里查数据库表 uni-pay-orders 判断 status是否等于1来判断是否真的支付了
await uniCloud.callFunction({
name: "你的云函数名称",
data: {
out_trade_no, // 支付插件订单号
},
});
*/
/*
// 方式三安全模式一(加密)
let encrypted = payCrypto.aes.encrypt({
data: data, // 待加密的原文
});
await uniCloud.httpclient.request("你的服务器接口请求地址", {
method: "POST",
data: {
encrypted, // 传输加密数据
},
});
*/
/*
// 方式三安全模式二(只传一个订单号 out_trade_no你自己的回调里执行url请求来请求 uni-pay-co 云对象的 getOrder 接口来判断订单是否真的支付了)
await uniCloud.httpclient.request("你的服务器接口请求地址", {
method: "POST",
data: {
out_trade_no, // 支付插件订单号
},
});
*/
// 此处写你自己的支付成功逻辑结束-----------------------------------------------------------
// user_order_success = true 代表你自己的逻辑处理成功 返回 false 代表你自己的处理逻辑失败。
return user_order_success;
};

View File

@@ -0,0 +1,78 @@
'use strict';
/**
* 此处建议只改下订单状态,保证能及时返回给第三方支付服务器成功状态
* 限制4秒内必须执行完全部的异步回调逻辑建议将消息发送、返佣、业绩结算等业务逻辑异步处理如用定时任务去处理这些异步逻辑
*/
const payCrypto = require('../libs/crypto.js'); // 获取加密服务
module.exports = async (obj) => {
let user_order_success = true;
let { data = {} } = obj;
let {
order_no,
out_trade_no,
total_fee
} = data; // uni-pay-orders 表内的数据均可获取到
// 此处写你自己的支付成功逻辑开始-----------------------------------------------------------
// 有三种方式
// 方式一:直接写数据库操作
// 方式二:使用 await uniCloud.callFunction 调用其他云函数或云对象,云对象则使用 uniCloud.importObject('云对象名称')来请求
// 方式三:使用 await uniCloud.httpclient.request 调用http接口地址
/*
// 方式二安全模式一(加密)
let encrypted = payCrypto.aes.encrypt({
data: data, // 待加密的原文
});
await uniCloud.callFunction({
name: "你的云函数名称",
data: {
encrypted, // 传输加密数据
},
});
*/
/*
// 方式二安全模式二(只传一个订单号 out_trade_no你自己的回调里查数据库表 uni-pay-orders 判断 status是否等于1来判断是否真的支付了
await uniCloud.callFunction({
name: "你的云函数名称",
data: {
out_trade_no, // 支付插件订单号
},
});
*/
/*
// 方式三安全模式一(加密)
let encrypted = payCrypto.aes.encrypt({
data: data, // 待加密的原文
});
await uniCloud.httpclient.request("你的服务器接口请求地址", {
method: "POST",
data: {
encrypted, // 传输加密数据
},
});
*/
/*
// 方式三安全模式二(只传一个订单号 out_trade_no你自己的回调里执行url请求来请求 uni-pay-co 云对象的 getOrder 接口来判断订单是否真的支付了)
await uniCloud.httpclient.request("你的服务器接口请求地址", {
method: "POST",
data: {
out_trade_no, // 支付插件订单号
},
});
*/
// 此处写你自己的支付成功逻辑结束-----------------------------------------------------------
// user_order_success = true 代表你自己的逻辑处理成功 返回 false 代表你自己的处理逻辑失败。
return user_order_success;
};

View File

@@ -0,0 +1,51 @@
# 异步通知回调执行逻辑目录
**提示:异步通知写在 `uni-pay-co/notify` 目录下在此目录新建2个js文件分别为 `recharge.js`、`goods.js` 文件同时复制以下代码要你新建的2个js文件里。**
**注意**
为什么要你自己创建.js文件而不是插件默认给你创建好这是因为后面当插件更新时你写的代码会被插件更新的代码覆盖一键合并功能因此只要插件这里没有文件而是你自己新建的文件那么插件更新时不会覆盖你自己新建的文件内的代码。
```js
'use strict';
/**
* 此处建议只改下订单状态,保证能及时返回给第三方支付服务器成功状态
* 且where条件可以增加判断服务器推送过来的金额和订单表中订单需支付金额是否一致
* 将消息发送、返佣、业绩结算等业务逻辑异步处理(写入异步任务队列表)
* 如开启定时器每隔5秒触发一次处理订单
*/
module.exports = async (obj) => {
let user_order_success = true;
let { data = {} } = obj;
let {
order_no,
out_trade_no,
total_fee
} = data; // uni-pay-orders 表内的数据均可获取到
// 此处写你自己的支付成功逻辑开始-----------------------------------------------------------
// 有三种方式
// 方式一:直接写数据库操作
// 方式二:使用 await uniCloud.callFunction 调用其他云函数
// 方式三:使用 await uniCloud.httpclient.request 调用http接口地址
// 此处写你自己的支付成功逻辑结束-----------------------------------------------------------
// user_order_success = true 代表你自己的逻辑处理成功 返回 false 代表你自己的处理逻辑失败。
return user_order_success;
};
```
其中
- `recharge.js` 内可以写余额充值相关的回调逻辑
- `goods.js` 内可以写商品订单付款成功后的回调逻辑
最终调用哪个回调逻辑是根据你创建支付订单时,`type` 参数填的什么,`type` 如果填 `recharge` 则支付成功后就会执行 `recharge.js` 内的代码逻辑。
即前端调用支付时的这个 `type` 参数
```js
// 打开支付收银台
this.$refs.uniPay.open({
type: "recharge", // 支付回调类型 recharge 代表余额充值(当然你可以自己自定义)
});
```

View File

@@ -0,0 +1,17 @@
{
"name": "uni-pay-co",
"dependencies": {
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"uni-id-common": "file:../../../../uni-id-common/uniCloud/cloudfunctions/common/uni-id-common",
"uni-pay": "file:../common/uni-pay"
},
"extensions": {},
"cloudfunction-config": {
"concurrency": 1,
"memorySize": 128,
"path": "/uni-pay-co",
"timeout": 60,
"triggers": [],
"runtime": "Nodejs8"
}
}

View File

@@ -0,0 +1,5 @@
const pay = require("./pay");
module.exports = {
pay
};

View File

@@ -0,0 +1,865 @@
/**
* uni-pay-co 统一支付服务实现
*/
const crypto = require("crypto");
const uniPay = require("uni-pay");
const configCenter = require("uni-config-center");
const config = configCenter({ pluginId: 'uni-pay' }).requireFile('config.js');
const dao = require('../dao');
const libs = require('../libs');
const { UniCloudError, isUniPayError, ERROR } = require('../common/error')
const db = uniCloud.database();
const _ = db.command;
const notifyPath = "/payNotify/";
class service {
constructor(obj) {
}
/**
* 获取支付插件的完整配置
*/
getConfig() {
return config;
}
/**
* 支付成功 - 异步通知
*/
async paymentNotify(data = {}) {
let {
httpInfo,
} = data;
let path = httpInfo.path;
let pay_type = path.substring(notifyPath.length);
let provider = pay_type.split("-")[0]; // 获取支付供应商
let provider_pay_type = pay_type.split("-")[1]; // 获取支付方式
let original_data = libs.common.getNotifyData({ httpInfo, provider }); // 获取原始回调数据
// 初始化uniPayInstance
let uniPayInstance = await this.initUniPayInstance({ provider, provider_pay_type });
let notifyType = await uniPayInstance.checkNotifyType(httpInfo);
if (notifyType !== "payment") {
// 由于支付宝部分退款会触发支付成功的回调,但同时签名验证是算未通过的,为了避免支付宝重复推送,这里可以直接返回成功告知支付宝服务器,不用再推送过来了。
return libs.common.returnNotifySUCCESS({ provider, provider_pay_type });
}
let verifyResult = await uniPayInstance.verifyPaymentNotify(httpInfo);
if (!verifyResult) {
console.log('---------!签名验证未通过!---------');
console.log('---------!签名验证未通过!---------');
console.log('---------!签名验证未通过!---------');
return {}
}
console.log('---------!签名验证通过!---------');
verifyResult = JSON.parse(JSON.stringify(verifyResult)); // 这一句代码有用,请勿删除。
let {
outTradeNo,
totalFee,
transactionId,
resultCode, // 微信支付v2和支付宝支付判断成功的字段
openid,
appId,
tradeState, // 微信支付v3判断支付成功的字段
} = verifyResult;
//console.log('verifyResult: ', verifyResult)
if (resultCode == "SUCCESS" || tradeState === "SUCCESS") {
let time = Date.now();
let payOrderInfo = await dao.uniPayOrders.updateAndReturn({
whereJson: {
status: 0, // status:0 为必须条件,防止重复推送时的错误
out_trade_no: outTradeNo, // 商户订单号
},
dataJson: {
status: 1, // 设置为已付款
transaction_id: transactionId, // 第三方支付单号
pay_date: time,
notify_date: time,
openid,
original_data,
}
});
//console.log('payOrderInfo: ', payOrderInfo)
if (payOrderInfo) {
// 只有首次推送才执行用户自己的逻辑处理。
// 用户自己的逻辑处理 开始-----------------------------------------------------------
let userOrderSuccess = false;
let orderPaySuccess;
try {
// 加载自定义异步回调函数
orderPaySuccess = require(`../notify/${payOrderInfo.type}`);
} catch (err) {
console.log(err);
}
if (typeof orderPaySuccess === "function") {
console.log('用户自己的回调逻辑 - 开始执行');
userOrderSuccess = await orderPaySuccess({
verifyResult,
data: payOrderInfo,
});
console.log('用户自己的回调逻辑 - 执行完成');
}
console.log('userOrderSuccess', userOrderSuccess);
// 用户自己的逻辑处理 结束-----------------------------------------------------------
await dao.uniPayOrders.updateAndReturn({
whereJson: {
status: 1,
out_trade_no: outTradeNo,
},
dataJson: {
user_order_success: userOrderSuccess,
}
});
} else {
console.log('---------!注意:本次回调非首次回调,已被插件拦截,插件不会执行你的回调函数!---------');
console.log('---------!注意:本次回调非首次回调,已被插件拦截,插件不会执行你的回调函数!---------');
console.log('---------!注意:本次回调非首次回调,已被插件拦截,插件不会执行你的回调函数!---------');
console.log('verifyResult:', verifyResult);
}
} else {
console.log('verifyResult:', verifyResult);
}
return libs.common.returnNotifySUCCESS({ provider, provider_pay_type });
}
/**
* 统一支付 - 创建支付订单
*/
async createOrder(data = {}) {
let {
provider, // 支付供应商
total_fee, // 支付金额
user_id, // 用户user_id统计需要
openid, // 用户openid
order_no, // 订单号
out_trade_no, // 支付插件订单号
description, // 订单描述
type, // 回调类型
qr_code, // 是否强制使用扫码支付
clientInfo, // 客户端信息
cloudInfo, // 云端信息
custom, // 自定义参数(不会发送给第三方支付服务器)
other, // 其他请求参数(会发送给第三方支付服务器)
} = data;
let subject = description;
let body = description;
if (!out_trade_no) out_trade_no = libs.common.createOrderNo();
if (!order_no || typeof order_no !== "string") {
throw { errCode: ERROR[51003] };
}
if (!type || typeof type !== "string") {
throw { errCode: ERROR[51004] };
}
if (typeof total_fee !== "number" || total_fee <= 0 || total_fee % 1 !== 0) {
throw { errCode: ERROR[51005] };
}
if (!description || typeof description !== "string") {
throw { errCode: ERROR[51006] };
}
if (!provider || typeof provider !== "string") {
throw { errCode: ERROR[51007] };
}
if (!clientInfo) {
throw { errCode: ERROR[51008] };
}
if (!cloudInfo) {
throw { errCode: ERROR[51009] };
}
let res = { errCode: 0, errMsg: 'ok', order_no, out_trade_no, provider };
let {
clientIP: client_ip,
userAgent: ua,
appId: appid,
deviceId: device_id,
platform
} = clientInfo;
let {
spaceId, // 服务空间ID
} = cloudInfo;
let {
notifyUrl = {}
} = config;
// 业务逻辑开始-----------------------------------------------------------
let currentNotifyUrl = notifyUrl[spaceId] || notifyUrl["default"]; // 异步回调地址
if (!currentNotifyUrl || currentNotifyUrl.indexOf("http") !== 0) {
throw { errCode: ERROR[52002] };
}
platform = libs.common.getPlatform(platform);
// 如果需要二维码支付模式则清空下openid
if (qr_code) {
openid = undefined;
res.qr_code = qr_code;
}
// 获取并自动匹配支付供应商的支付类型
let provider_pay_type = libs.common.getProviderPayType({
platform,
provider,
ua,
qr_code
});
res.provider_pay_type = provider_pay_type;
// 拼接实际异步回调地址
let finalNotifyUrl = `${currentNotifyUrl}${notifyPath}${provider}-${provider_pay_type}`;
// 获取uniPay交易类型
let tradeType = libs.common.getTradeType({ provider, provider_pay_type });
let uniPayConifg = await this.getUniPayConfig({ provider, provider_pay_type });
// 初始化uniPayInstance
let uniPayInstance = await this.initUniPayInstance({ provider, provider_pay_type });
// 获取支付信息
let getOrderInfoParam = {
openid: openid,
subject: subject,
body: body,
outTradeNo: out_trade_no,
totalFee: total_fee,
notifyUrl: finalNotifyUrl,
tradeType: tradeType
};
if (provider === "wxpay" && provider_pay_type === "mweb") {
getOrderInfoParam.spbillCreateIp = client_ip;
if (uniPayConifg.version !== 3) {
// v2版本
getOrderInfoParam.sceneInfo = uniPayConifg.sceneInfo;
} else {
// v3版本特殊处理
getOrderInfoParam.sceneInfo = JSON.parse(JSON.stringify(uniPayConifg.sceneInfo));
if (getOrderInfoParam.sceneInfo.h5_info.wap_url) {
getOrderInfoParam.sceneInfo.h5_info.app_url = getOrderInfoParam.sceneInfo.h5_info.wap_url;
delete getOrderInfoParam.sceneInfo.h5_info.wap_url;
}
if (getOrderInfoParam.sceneInfo.h5_info.wap_name) {
getOrderInfoParam.sceneInfo.h5_info.app_name = getOrderInfoParam.sceneInfo.h5_info.wap_name;
delete getOrderInfoParam.sceneInfo.h5_info.wap_name;
}
}
}
try {
// 如果是苹果内购不需要执行uniPayInstance.getOrderInfo等操作
if (provider !== "appleiap") {
// 第三方支付服务器返回的订单信息
let orderInfo;
if (other) {
// other 内的键名转驼峰
other = libs.common.snake2camelJson(other);
getOrderInfoParam = Object.assign(getOrderInfoParam, other);
}
getOrderInfoParam = JSON.parse(JSON.stringify(getOrderInfoParam)); // 此为去除undefined的参数
orderInfo = await uniPayInstance.getOrderInfo(getOrderInfoParam);
if (qr_code && orderInfo.codeUrl) {
res.qr_code_image = await libs.qrcode.toDataURL(orderInfo.codeUrl, {
type: "image/png",
width: 200,
margin: 1,
scale: 1,
color: {
dark: "#000000",
light: "#ffffff",
},
errorCorrectionLevel: "Q",
quality: 1
});
}
// 支付宝支付参数特殊处理
if (provider === "alipay") {
if (typeof orderInfo === "object" && orderInfo.code && orderInfo.code !== "10000") {
res.errCode = orderInfo.code;
res.errMsg = orderInfo.subMsg;
}
}
res.order = orderInfo;
}
} catch (err) {
let errMsg = err.errorMessage || err.message;
console.error("data: ", data);
console.error("getOrderInfoParam: ", getOrderInfoParam);
console.error("err: ", err);
console.error("errMsg: ", errMsg);
throw { errCode: ERROR[53001], errMsg };
}
// 尝试获取下订单信息
let payOrderInfo = await dao.uniPayOrders.find({
order_no,
out_trade_no
});
let create_date = Date.now();
// 如果订单不存在,则添加
if (!payOrderInfo) {
// 添加数据库(数据库的out_trade_no字段需设置为唯一索引)
let stat_platform = clientInfo.platform;
if (stat_platform === "app") {
stat_platform = clientInfo.os;
}
let nickname;
if (user_id) {
// 获取nickname冗余昵称
let userInfo = await dao.uniIdUsers.findById(user_id);
if (userInfo) nickname = userInfo.nickname;
}
await dao.uniPayOrders.add({
provider,
provider_pay_type,
uni_platform: platform,
status: 0,
type,
order_no,
out_trade_no,
user_id,
nickname,
device_id,
client_ip,
openid,
description,
total_fee,
refund_fee: 0,
refund_count: 0,
provider_appid: uniPayConifg.appId,
appid,
custom,
create_date,
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
}
});
} else {
// 如果订单已经存在,则修改下支付方式(用户可能先点微信支付,未付款,又点了支付宝支付)
await dao.uniPayOrders.updateById(payOrderInfo._id, {
provider,
provider_pay_type,
});
}
// 自动删除3天前的订单未付款订单
// await dao.uniPayOrders.deleteExpPayOrders();
// 业务逻辑结束-----------------------------------------------------------
return res;
}
/**
* 统一支付结果查询
* @description 根据商户订单号或者平台订单号查询订单信息,主要用于未接收到支付通知时可以使用此接口进行支付结果验证
*/
async getOrder(data = {}) {
let {
out_trade_no, // 支付插件订单号
transaction_id, // 支付平台的交易单号
await_notify = false, // 是否需要等待异步通知执行完成才返回前端支付结果
} = data;
let res = { errCode: 0, errMsg: 'ok' };
// 业务逻辑开始-----------------------------------------------------------
if (!out_trade_no && !transaction_id) {
throw { errCode: ERROR[51010] };
}
let payOrderInfo;
if (transaction_id) {
payOrderInfo = await dao.uniPayOrders.find({
transaction_id
});
} else if (out_trade_no) {
payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
}
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
// 初始化uniPayInstance
let uniPayInstance = await this.initUniPayInstance(payOrderInfo);
let orderQueryJson = {};
if (out_trade_no) {
orderQueryJson.outTradeNo = out_trade_no;
} else {
orderQueryJson.transactionId = transaction_id;
}
let queryRes = await uniPayInstance.orderQuery(orderQueryJson);
if (queryRes.tradeState === 'SUCCESS' || queryRes.tradeState === 'FINISHED') {
if (typeof payOrderInfo.user_order_success == "undefined" && await_notify) {
let whileTime = 0; // 当前循环已执行的时间(毫秒)
let whileInterval = 500; // 每次循环间隔时间(毫秒)
let maxTime = 5000; // 循环执行时间超过此值则退出循环(毫秒)
while (typeof payOrderInfo.user_order_success == "undefined" && whileTime <= maxTime) {
await libs.common.sleep(whileInterval);
whileTime += whileInterval;
payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
}
}
res = {
errCode: 0,
errMsg: "ok",
has_paid: true, // 标记用户是否已付款成功(此参数只能表示用户确实付款了,但系统的异步回调逻辑可能还未执行完成)
out_trade_no, // 支付插件订单号
transaction_id, // 支付平台订单号
status: payOrderInfo.status, // 标记当前支付订单状态 -1已关闭 0未支付 1已支付 2已部分退款 3已全额退款
user_order_success: payOrderInfo.user_order_success, // 用户异步通知逻辑是否全部执行完成且无异常建议前端通过此参数是否为true来判断是否支付成功
pay_order: payOrderInfo,
}
} else {
let errMsg = queryRes.tradeStateDesc || "未支付或已退款";
if (errMsg.indexOf("订单发生过退款") > -1) {
errMsg = "订单已退款";
}
res = {
errCode: -1,
errMsg: errMsg,
has_paid: false,
out_trade_no, // 支付插件订单号
transaction_id, // 支付平台订单号
}
}
// 业务逻辑结束-----------------------------------------------------------
return res;
}
/**
* 统一退款
* @description 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家。
*/
async refund(data = {}) {
let {
out_trade_no, // 插件支付单号
out_refund_no, // 退款单号(若不传,则自动生成)
refund_desc = "用户申请退款",
refund_fee: myRefundFee,
refund_fee_type = "CNY"
} = data;
let res = { errCode: 0, errMsg: 'ok' };
// 业务逻辑开始-----------------------------------------------------------
if (!out_trade_no) {
throw { errCode: ERROR[51001] };
}
let payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
let refund_count = payOrderInfo.refund_count || 0;
refund_count++;
// 生成退款订单号
let outRefundNo = out_refund_no ? out_refund_no : `${out_trade_no}-${refund_count}`;
// 订单总金额
let totalFee = payOrderInfo.total_fee;
// 退款总金额
let refundFee = myRefundFee || totalFee;
let provider = payOrderInfo.provider;
let uniPayConifg = await this.getUniPayConfig(payOrderInfo);
let uniPayInstance = await this.initUniPayInstance(payOrderInfo);
console.log(`---- ${out_trade_no} -- ${outRefundNo} -- ${totalFee/100} -- ${refundFee/100}`)
// 退款操作
try {
res.result = await uniPayInstance.refund({
outTradeNo: out_trade_no,
outRefundNo,
totalFee,
refundFee,
refundDesc: refund_desc,
refundFeeType: refund_fee_type
});
} catch (err) {
console.error(err);
let errMsg = err.message;
if (errMsg && errMsg.indexOf("verify failure") > -1) {
throw { errCode: ERROR[53005] };
}
return { errCode: -1, errMsg: errMsg, err }
}
if (res.result.refundFee) {
res.errCode = 0;
res.errMsg = "ok";
// 修改数据库
try {
// 修改订单状态
payOrderInfo = await dao.uniPayOrders.updateAndReturn({
whereJson: {
_id: payOrderInfo._id
},
dataJson: {
status: 2,
refund_fee: _.inc(refundFee),
refund_count: refund_count,
// 记录每次的退款详情
refund_list: _.unshift({
refund_date: Date.now(),
refund_fee: refundFee,
out_refund_no: outRefundNo,
refund_desc
})
}
});
if (payOrderInfo && payOrderInfo.refund_fee >= payOrderInfo.total_fee) {
// 修改订单状态为已全额退款
await dao.uniPayOrders.updateById(payOrderInfo._id, {
status: 3,
refund_fee: payOrderInfo.total_fee,
});
}
} catch (err) {
console.error(err);
}
} else {
throw { errCode: ERROR[53002] };
}
// 业务逻辑结束-----------------------------------------------------------
return res;
}
/**
* 查询退款(查询退款情况)
* @description 提交退款申请后,通过调用该接口查询退款状态。
*/
async getRefund(data = {}) {
let {
out_trade_no, // 插件支付单号
} = data;
if (!out_trade_no) {
throw { errCode: ERROR[51001] };
}
let payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
let provider = payOrderInfo.provider;
let uniPayInstance = await this.initUniPayInstance(payOrderInfo);
let queryRes;
try {
let refundQueryJson = {
outTradeNo: out_trade_no,
outRefundNo: payOrderInfo.refund_list[0].out_refund_no
};
queryRes = await uniPayInstance.refundQuery(refundQueryJson);
} catch (err) {
throw { errCode: ERROR[53003], errMsg: err.errMsg };
}
let orderInfo = {
total_fee: payOrderInfo.total_fee,
refund_fee: payOrderInfo.refund_fee,
refund_count: payOrderInfo.refund_count,
refund_list: payOrderInfo.refund_list,
provider: payOrderInfo.provider,
provider_pay_type: payOrderInfo.provider_pay_type,
status: payOrderInfo.status,
type: payOrderInfo.type,
out_trade_no: payOrderInfo.out_trade_no,
transaction_id: payOrderInfo.transaction_id,
};
if (queryRes.refundFee > 0) {
let msg = "ok";
if (payOrderInfo.refund_list && payOrderInfo.refund_list.length > 0) {
msg = `合计退款 ${payOrderInfo.refund_fee/100}\r\n`;
for (let i in payOrderInfo.refund_list) {
let item = payOrderInfo.refund_list[i];
let index = Number(i) + 1;
let timeStr = libs.common.timeFormat(item.refund_date, "yyyy-MM-dd hh:mm:ss");
msg += `${index}${timeStr} \r\n退款 ${item.refund_fee/100} \r\n`;
}
}
return {
errCode: 0,
errMsg: msg,
pay_order: orderInfo,
result: queryRes
}
} else {
throw { errCode: ERROR[53003] };
}
}
/**
* 关闭订单
* @description 用于交易创建后,用户在一定时间内未进行支付,可调用该接口直接将未付款的交易进行关闭,避免重复支付。
* 注意
* 微信支付:订单生成后不能马上调用关单接口,最短调用时间间隔为 5 分钟。
*/
async closeOrder(data = {}) {
let {
out_trade_no, // 插件支付单号
} = data;
if (!out_trade_no) {
throw { errCode: ERROR[51001] };
}
let payOrderInfo = await dao.uniPayOrders.find({
out_trade_no
});
if (!payOrderInfo) {
throw { errCode: ERROR[52001] };
}
let { provider } = payOrderInfo;
let uniPayInstance = await this.initUniPayInstance(payOrderInfo);
let closeOrderRes = await uniPayInstance.closeOrder({
outTradeNo: out_trade_no
});
if ((provider === "wxpay" && closeOrderRes.resultCode === "SUCCESS") || (provider === "alipay" && closeOrderRes.code ===
"10000")) {
// 修改订单状态为已取消
await dao.uniPayOrders.update({
whereJson: {
_id: payOrderInfo._id,
status: 0
},
dataJson: {
status: -1,
cancel_date: Date.now()
}
});
return {
errCode: 0,
errMsg: "订单关闭成功",
result: closeOrderRes
}
} else {
throw { errCode: ERROR[53004] };
}
}
/**
* 根据code获取openid
*/
async getOpenid(data = {}) {
let {
provider, // 支付供应商
code, // 用户登录获取的code
clientInfo, // 客户端环境
} = data;
if (!code) {
throw { errCode: ERROR[51002] };
}
let { platform, ua } = clientInfo;
// 获取并自动匹配支付供应商的支付类型
let provider_pay_type = libs.common.getProviderPayType({
provider,
platform,
ua
});
let uniPayConifg = await this.getUniPayConfig({ provider, provider_pay_type });
if (provider === "wxpay") {
return await libs.wxpay.getOpenid({
config: uniPayConifg,
code,
provider_pay_type,
});
} else if (provider === "alipay") {
return await libs.alipay.getOpenid({
config: uniPayConifg,
code,
});
}
}
/**
* 获取支持的支付方式
* let payTypes = await service.pay.getPayProviderFromCloud();
*/
async getPayProviderFromCloud() {
let wxpay = config.wxpay && config.wxpay.enable ? true : false;
let alipay = config.alipay && config.alipay.enable ? true : false;
let provider = [];
if (wxpay) provider.push("wxpay");
if (alipay) provider.push("alipay");
return {
errCode: 0,
errMsg: "ok",
wxpay,
alipay,
provider
};
}
/**
* 验证iosIap苹果内购支付凭据
* let payTypes = await service.pay.verifyReceiptFromAppleiap();
*/
async verifyReceiptFromAppleiap(data) {
let {
out_trade_no,
transaction_receipt,
transaction_identifier,
} = data;
if (!out_trade_no) {
throw { errCode: ERROR[51001] };
}
// 初始化uniPayInstance
let uniPayInstance = await this.initUniPayInstance({ provider: "appleiap", provider_pay_type: "app" });
let verifyReceiptRes = await uniPayInstance.verifyReceipt({
receiptData: transaction_receipt
});
let transaction_id;
let userOrderSuccess = false;
let pay_date;
if (verifyReceiptRes.tradeState !== "SUCCESS") {
return {
errCode: -1,
errMsg: "未支付"
};
}
// 支付成功
pay_date = verifyReceiptRes.receipt.receipt_creation_date_ms;
let inApp = verifyReceiptRes.receipt.in_app[0];
let quantity = inApp.quantity; // 购买数量
let product_id = inApp.product_id; // 对应的内购产品id
transaction_id = inApp.transaction_id; // 本次交易id
if (transaction_identifier !== transaction_id) {
// 校验不通过
return {
errCode: -1,
errMsg: "ios内购凭据校验不通过"
};
}
if ((Date.now() - 1000 * 3600 * 24) > pay_date) {
// 订单已超24小时不做处理通知前端直接关闭订单。
return {
errCode: 0,
errMsg: "ok"
};
}
// 查询该transaction_id是否使用过如果已使用则不做处理通知前端直接关闭订单。
let findOrderInfo = await dao.uniPayOrders.find({
transaction_id,
});
if (findOrderInfo) {
return {
errCode: 0,
errMsg: "ok"
};
}
// 否则,执行用户回调
// 用户自己的逻辑处理 开始-----------------------------------------------------------
let orderPaySuccess;
let payOrderInfo;
try {
// 加载自定义异步回调函数
orderPaySuccess = require(`../notify/appleiap`);
} catch (err) {
console.log(err);
}
if (typeof orderPaySuccess === "function") {
payOrderInfo = await dao.uniPayOrders.updateAndReturn({
whereJson: {
status: 0, // status:0 为必须条件,防止重复推送时的错误
out_trade_no: out_trade_no, // 商户订单号
},
dataJson: {
status: 1, // 设置为已付款
transaction_id: transaction_id, // 第三方支付单号
pay_date: pay_date,
notify_date: pay_date,
original_data: verifyReceiptRes
}
});
console.log('用户自己的回调逻辑 - 开始执行');
userOrderSuccess = await orderPaySuccess({
verifyResult: verifyReceiptRes,
data: payOrderInfo,
});
console.log('用户自己的回调逻辑 - 执行完成');
await dao.uniPayOrders.updateAndReturn({
whereJson: {
status: 1,
out_trade_no,
},
dataJson: {
user_order_success: userOrderSuccess,
}
});
} else {
payOrderInfo = await dao.uniPayOrders.find({
out_trade_no,
});
}
console.log('userOrderSuccess', userOrderSuccess);
// 用户自己的逻辑处理 结束-----------------------------------------------------------
//console.log('verifyReceiptRes: ', verifyReceiptRes);
return {
errCode: 0,
errMsg: "ok",
has_paid: true, // 标记用户是否已付款成功(此参数只能表示用户确实付款了,但系统的异步回调逻辑可能还未执行完成)
out_trade_no, // 支付插件订单号
transaction_id, // 支付平台订单号
status: payOrderInfo.status, // 标记当前支付订单状态 -1已关闭 0未支付 1已支付 2已部分退款 3已全额退款
user_order_success: payOrderInfo.user_order_success, // 用户异步通知逻辑是否全部执行完成且无异常建议前端通过此参数是否为true来判断是否支付成功
pay_order: payOrderInfo,
};
}
/**
* 获取对应支付配置
* let uniPayConifg = await this.getUniPayConfig({ provider, provider_pay_type });
*/
async getUniPayConfig(data = {}) {
let {
provider,
provider_pay_type,
} = data;
if (config && config[provider] && config[provider][provider_pay_type]) {
let uniPayConfig = config[provider][provider_pay_type];
if (!uniPayConfig.appId && provider !== "appleiap") {
throw new Error(`uni-pay配置${provider}.${provider_pay_type}节点下的appId不能为空`);
}
return uniPayConfig;
} else {
throw new Error(`${provider}_${provider_pay_type} : 商户支付配置错误`);
}
}
/**
* 初始化uniPayInstance
* let uniPayInstance = await service.pay.initUniPayInstance({ provider, provider_pay_type });
*/
async initUniPayInstance(data = {}) {
let {
provider,
} = data;
let uniPayConifg = await this.getUniPayConfig(data);
let uniPayInstance;
if (provider === "wxpay") {
// 微信
if (uniPayConifg.version === 3) {
uniPayInstance = uniPay.initWeixinV3(uniPayConifg);
} else {
uniPayInstance = uniPay.initWeixin(uniPayConifg);
}
} else if (provider === "alipay") {
// 支付宝
uniPayInstance = uniPay.initAlipay(uniPayConifg);
} else if (provider === "appleiap") {
// ios内购
uniPayInstance = uniPay.initAppleIapPayment(uniPayConifg);
} else {
throw new Error(`${pay_type} : 不支持的支付方式`);
}
return uniPayInstance;
}
}
module.exports = new service();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
{
"uni-id-users": {},
"uni-pay-orders": {
"data": [],
"index": [{
"IndexName": "order_no",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "order_no", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "out_trade_no",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "out_trade_no", "Direction": "1" }], "MgoIsUnique": true }
},
{
"IndexName": "transaction_id",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "transaction_id", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "create_date",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "create_date", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "pay_date",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "pay_date", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "total_fee",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "total_fee", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "user_id",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "user_id", "Direction": "1" }], "MgoIsUnique": false }
},
{
"IndexName": "appid",
"MgoKeySchema": { "MgoIndexKeys": [{ "Name": "appid", "Direction": "1" }], "MgoIsUnique": false }
}
]
}