oak-pay-business/es/utils/payClazz/WechatPay/WechatPay.js

384 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs';
import dayJs from 'dayjs';
import { WechatPay as WechatPaySDK } from 'wechat-pay-nodejs';
import { compressTo32, decompressFrom32 } from 'oak-domain/lib/utils/uuid';
import { ExternalPayUtilException } from "../../../types/Exception";
import assert from "assert";
import { omit } from "oak-domain/lib/utils/lodash";
import WechatPayDebug from './WechatPay.debug';
const TRADE_STATE_MATRIX = {
'SUCCESS': 'paid',
'USERPAYING': 'paying',
'NOTPAY': 'paying',
'CLOSED': 'closed',
'PAYERROR': 'closed',
'REVOKED': 'closed',
};
const REFUND_STATE_MATRIX = {
'SUCCESS': "successful",
'CLOSED': "failed",
"ABNORMAL": "refunding",
"PROCESSING": 'refunding',
};
const PREPAY_TIMEOUT = 15 * 60 * 1000; // 默认订单15分钟过期
export default class WechatPay extends WechatPayDebug {
wechatPay;
refundGapDays;
mchId;
publicKeyFilePath;
privateKeyFilePath;
apiV3Key;
payNotifyUrl;
refundNotifyUrl;
static MAX_REFUND_DAYS_GAP = 365;
static MIN_REFUND_DAYS_GAP = 7;
static DEFAULT_REFUND_DAYS_GAP = 300;
constructor(wpProduct, appId) {
super(wpProduct);
const { wpAccount } = wpProduct;
this.wpProduct = wpProduct;
// 这此不记也行,懒得改了
this.refundGapDays = wpAccount.refundGapDays;
this.mchId = wpAccount.mchId;
this.publicKeyFilePath = wpAccount.publicKeyFilePath;
this.privateKeyFilePath = wpAccount.privateKeyFilePath;
this.apiV3Key = wpAccount.apiV3Key;
this.payNotifyUrl = wpAccount.wechatPay.payNotifyUrl;
this.refundNotifyUrl = wpAccount.wechatPay.refundNotifyUrl;
this.wechatPay = new WechatPaySDK({
appid: appId,
mchid: this.mchId,
cert_private_content: fs.readFileSync(this.privateKeyFilePath),
cert_public_content: fs.readFileSync(this.publicKeyFilePath),
});
}
async refund(refund, context) {
const { id, price, pay } = refund;
const serverUrl = context.composeAccessPath();
const endpoint = this.refundNotifyUrl;
const refundNotifyUrl = `${serverUrl}/endpoint/${endpoint}/${id}`;
const result = await this.wechatPay.createRefund({
out_trade_no: compressTo32(pay.id),
out_refund_no: compressTo32(id),
notify_url: refundNotifyUrl,
amount: {
refund: price,
total: pay.price,
currency: 'CNY',
}
});
const { refund_id } = this.analyzeResult(result);
return {
externalId: refund_id,
};
}
async getRefundState(refund) {
const result = await this.wechatPay.queryRefundByOutRefundNo(compressTo32(refund.id));
const externalMeta = this.analyzeResult(result);
const { status, success_time } = externalMeta;
/**
* 【退款状态】 退款到银行发现用户的卡作废或者冻结了导致原路退款银行卡失败可前往商户平台pay.weixin.qq.com-交易中心,手动处理此笔退款。
可选取值:
SUCCESS: 退款成功
CLOSED: 退款关闭
PROCESSING: 退款处理中
ABNORMAL: 退款异常
*/
const iState = REFUND_STATE_MATRIX[status];
return [iState, success_time ? {
successAt: success_time ? dayJs(success_time).valueOf() : undefined,
externalMeta,
} : undefined];
}
analyzeResult(result) {
const { success, data, } = result;
if (!success) {
console.error(JSON.stringify(result));
throw new ExternalPayUtilException(result);
}
return data;
}
caclRefundDeadline(successTime) {
let gapDays = this.refundGapDays || WechatPay.DEFAULT_REFUND_DAYS_GAP;
if (gapDays > WechatPay.MAX_REFUND_DAYS_GAP) {
gapDays = WechatPay.MAX_REFUND_DAYS_GAP;
}
if (gapDays < WechatPay.MIN_REFUND_DAYS_GAP) {
gapDays = WechatPay.MIN_REFUND_DAYS_GAP;
}
return dayJs(successTime).add(gapDays, 'day').subtract(12, 'hour').valueOf();
}
async prepay(pay, data, context) {
const applicationId = context.getApplicationId();
const userId = context.getCurrentUserId();
const serverUrl = context.composeAccessPath();
const endpoint = this.payNotifyUrl;
const payNotifyUrl = `${serverUrl}/endpoint/${endpoint}/${pay.id}`;
switch (this.wpProduct.type) {
case 'native': {
const result = await this.wechatPay.prepayNative({
description: pay.order?.desc || pay.orderId ? '订单支付' : '帐户充值',
out_trade_no: compressTo32(pay.id),
notify_url: payNotifyUrl,
amount: {
total: pay.price,
},
});
const { code_url } = this.analyzeResult(result);
data.externalId = code_url;
data.meta = {
codeUrl: code_url,
};
break;
}
case 'jsapi':
case 'mp': {
const [wechatUser] = await context.select('wechatUser', {
data: {
id: 1,
openId: 1,
},
filter: {
applicationId,
userId,
}
}, { dontCollect: true });
assert(wechatUser, `user【${userId}】找不到相应的openId无法小程序支付`);
const result = await this.wechatPay.prepayJsapi({
description: pay.order?.desc || pay.orderId ? '订单支付' : '帐户充值',
out_trade_no: compressTo32(pay.id),
notify_url: payNotifyUrl,
amount: {
total: pay.price,
},
payer: {
openid: wechatUser.openId,
}
});
const prepayMeta = this.analyzeResult(result);
const prepayId = prepayMeta.package.slice(11); // `prepay_id=${prepay_id}`
data.externalId = prepayId;
data.meta = {
...pay.meta,
prepayMeta,
payId: userId,
payOpenId: wechatUser.openId,
};
break;
}
default: {
throw new Error('本支付方法还没来的及实现呢');
}
}
// pay加一个过期时间到期自动close
data.timeoutAt = Date.now() + PREPAY_TIMEOUT;
}
async getState(pay) {
const outTradeNo = compressTo32(pay.id);
const result = await this.wechatPay.queryOrderByOutTradeNo(outTradeNo);
// 返回结果中没有声明这个域
/**
* trade_state
必填
string(32)
【交易状态】 交易状态,枚举值:
* SUCCESS支付成功
* REFUND转入退款
* NOTPAY未支付
* CLOSED已关闭
* REVOKED已撤销仅付款码支付会返回
* USERPAYING用户支付中仅付款码支付会返回
* PAYERROR支付失败仅付款码支付会返回
*/
const { trade_state: tradeState, success_time } = this.analyzeResult(result);
const iState = TRADE_STATE_MATRIX[tradeState];
assert(iState);
const updateData = {
meta: {
getState: omit(result, ['mchid', 'appid', 'out_trade_no']),
},
};
if (iState === 'paid') {
updateData.successAt = dayJs(success_time).valueOf();
}
return [iState, updateData];
}
async close(pay) {
const outTradeNo = compressTo32(pay.id);
const result = await this.wechatPay.closeOrder(outTradeNo);
this.analyzeResult(result);
}
async decodePayNotification(params, body) {
const { resource } = body;
if (process.env.NODE_ENV !== 'production') {
console.log('decodePayNotification-resource', JSON.stringify(resource));
}
const { ciphertext, nonce, associated_data } = resource;
const result2 = this.wechatPay.decryptAesGcm({
ciphertext,
nonce,
apiV3Key: this.apiV3Key,
associatedData: associated_data,
});
const result = JSON.parse(result2);
if (process.env.NODE_ENV !== 'production') {
console.log('decodePayNotification-decrypt', JSON.stringify(result));
}
/**
* {
"transaction_id":"1217752501201407033233368018",
"amount":{
"payer_total":100,
"total":100,
"currency":"CNY",
"payer_currency":"CNY"
},
"mchid":"1230000109",
"trade_state":"SUCCESS",
"bank_type":"CMC",
"promotion_detail":[
{
"amount":100,
"wechatpay_contribute":0,
"coupon_id":"109519",
"scope":"GLOBAL",
"merchant_contribute":0,
"name":"单品惠-6",
"other_contribute":0,
"currency":"CNY",
"stock_id":"931386",
"goods_detail":[
{
"goods_remark":"商品备注信息",
"quantity":1,
"discount_amount":1,
"goods_id":"M1006",
"unit_price":100
},
{
"goods_remark":"商品备注信息",
"quantity":1,
"discount_amount":1,
"goods_id":"M1006",
"unit_price":100
}
]
},
{
"amount":100,
"wechatpay_contribute":0,
"coupon_id":"109519",
"scope":"GLOBAL",
"merchant_contribute":0,
"name":"单品惠-6",
"other_contribute":0,
"currency":"CNY",
"stock_id":"931386",
"goods_detail":[
{
"goods_remark":"商品备注信息",
"quantity":1,
"discount_amount":1,
"goods_id":"M1006",
"unit_price":100
},
{
"goods_remark":"商品备注信息",
"quantity":1,
"discount_amount":1,
"goods_id":"M1006",
"unit_price":100
}
]
}
],
"success_time":"2018-06-08T10:34:56+08:00",
"payer":{
"openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
},
"out_trade_no":"1217752501201407033233368018",
"AppID":"wxd678efh567hg6787",
"trade_state_desc":"支付成功",
"trade_type":"MICROPAY",
"attach":"自定义数据",
"scene_info":{
"device_id":"013467007045764"
}
}
*/
const { out_trade_no, mchid, trade_state, success_time, } = result;
const payId = decompressFrom32(out_trade_no);
assert(mchid === this.mchId);
const iState = TRADE_STATE_MATRIX[trade_state];
assert(iState);
const extra = {
meta: omit(result, ['mchid',]),
};
if (iState === 'paid') {
extra.successAt = dayJs(success_time).valueOf();
}
return {
payId,
iState,
extra,
};
}
async decodeRefundNotification(params, body) {
const { resource } = body;
if (process.env.NODE_ENV !== 'production') {
console.log('decodeRefundNotification-resource', JSON.stringify(resource));
}
const { ciphertext, nonce, associated_data } = resource;
const result2 = this.wechatPay.decryptAesGcm({
ciphertext,
nonce,
apiV3Key: this.apiV3Key,
associatedData: associated_data,
});
const result = JSON.parse(result2);
if (process.env.NODE_ENV !== 'production') {
console.log('decodeRefundNotification-decrypt', JSON.stringify(result));
}
/**
* 对resource对象进行解密后得到的资源对象示例
* https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html
* {
"mchid": "1900000100",
"transaction_id": "1008450740201411110005820873",
"out_trade_no": "20150806125346",
"refund_id": "50200207182018070300011301001",
"out_refund_no": "7752501201407033233368018",
"refund_status": "SUCCESS",
"success_time": "2018-06-08T10:34:56+08:00",
"user_received_account": "招商银行信用卡0403",
"amount" : {
"total": 999,
"refund": 999,
"payer_total": 999,
"payer_refund": 999
}
}
*/
const { out_refund_no, mchid, refund_status, success_time, } = result;
const refundId = decompressFrom32(out_refund_no);
assert(mchid === this.mchId);
const iState = REFUND_STATE_MATRIX[refund_status];
assert(iState);
const extra = {
externalMeta: omit(result, ['mchid',]),
};
if (iState === 'successful') {
assert(success_time);
extra.successAt = dayJs(success_time).valueOf();
}
return {
refundId,
iState,
extra,
};
}
getRefundableAt(successAt) {
return this.caclRefundDeadline(successAt);
}
}