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

371 lines
16 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 { AlipaySdk } from 'alipay-sdk';
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 AliPayDebug from './AliPay.debug';
import { ToYuan } from "oak-domain/lib/utils/money";
// WAIT_BUYER_PAY交易创建等待买家付款、TRADE_CLOSED未付款交易超时关闭或支付完成后全额退款、TRADE_SUCCESS交易支付成功、TRADE_FINISHED交易结束不可退款
const TRADE_STATE_MATRIX = {
'TRADE_SUCCESS': 'paid',
'WAIT_BUYER_PAY': 'paying',
'TRADE_CLOSED': 'closed',
'TRADE_FINISHED': 'closed',
};
// REFUND_SUCCESS 退款处理成功; 未返回该字段表示退款请求未收到或者退款失败;
const REFUND_STATE_MATRIX = {
'REFUND_SUCCESS': "successful",
'REFUND_CLOSED': "failed",
"PROCESSING": 'refunding',
};
const PREPAY_TIMEOUT = 15 * 60 * 1000; // 默认订单15分钟过期
export default class AliPay extends AliPayDebug {
alipaySdk;
refundGapDays;
mchId;
appId;
publicKeyPath;
privateKeyPath;
payNotifyUrl;
refundNotifyUrl;
mode;
keyType;
gateway;
alipayRootCertPath;
alipayPublicCertPath;
appCertPath;
encryptKey;
needEncrypt;
static MAX_REFUND_DAYS_GAP = 365;
static MIN_REFUND_DAYS_GAP = 7;
static DEFAULT_REFUND_DAYS_GAP = 300;
constructor(apProduct) {
super(apProduct);
const { apAccount } = apProduct;
this.apProduct = apProduct;
// 这此不记也行,懒得改了
this.refundGapDays = typeof apProduct.refundGapDays === 'number' ? apProduct.refundGapDays : apAccount.refundGapDays;
if (typeof this.refundGapDays !== 'number') {
throw new Error('apProduct/apAccount的refundGapDays必须要设置');
}
this.mchId = apAccount.mchId;
this.appId = apAccount.appId;
this.publicKeyPath = apAccount.publicKeyPath;
this.privateKeyPath = apAccount.privateKeyPath;
this.payNotifyUrl = apAccount.aliPay.payNotifyUrl;
this.refundNotifyUrl = apAccount.aliPay.refundNotifyUrl;
this.mode = apAccount.mode;
this.keyType = apAccount.keyType;
this.gateway = apAccount.gateway;
this.alipayRootCertPath = apAccount.alipayRootCertPath;
this.alipayPublicCertPath = apAccount.alipayPublicCertPath;
this.appCertPath = apAccount.appCertPath;
this.encryptKey = apAccount.encryptKey;
this.needEncrypt = apAccount.needEncrypt || false;
const alipaySdkConfig = {
appId: this.appId,
// 密钥类型,请与生成的密钥格式保持一致,参考平台配置一节
keyType: this.keyType ? this.keyType : undefined,
gateway: apAccount.gateway ? apAccount.gateway : undefined,
// 设置应用私钥
privateKey: fs.readFileSync(this.privateKeyPath).toString(),
endpoint: apAccount.gateway ? apAccount.gateway.replace('/gateway.do', '') : undefined,
};
if (this.needEncrypt) {
alipaySdkConfig.encryptKey = this.encryptKey;
}
if (this.mode === 'publicKey') {
alipaySdkConfig.alipayPublicKey = fs.readFileSync(this.publicKeyPath).toString();
}
if (this.mode === 'certificate') {
alipaySdkConfig.alipayRootCertContent = fs.readFileSync(this.alipayRootCertPath);
alipaySdkConfig.alipayPublicCertContent = fs.readFileSync(this.alipayPublicCertPath);
alipaySdkConfig.appCertContent = fs.readFileSync(this.appCertPath);
}
this.alipaySdk = new AlipaySdk(alipaySdkConfig);
}
async refund(refund, context) {
const { id, price, pay, reason } = refund;
const serverUrl = context.composeAccessPath();
const endpoint = this.refundNotifyUrl;
const refundNotifyUrl = `${serverUrl}/endpoint/${endpoint}/${id}`;
const out_trade_no = compressTo32(pay.id);
const out_request_no = compressTo32(id);
const refundAmount = ToYuan(price);
const result = await this.alipaySdk.curl('POST', '/v3/alipay/trade/refund', {
body: {
out_trade_no: out_trade_no,
refund_amount: refundAmount,
out_request_no: out_request_no,
notify_url: refundNotifyUrl,
},
needEncrypt: this.needEncrypt,
});
// out_trade_no: '1749093519542',
// refund_fee: '0.01',
// gmt_refund_pay: '2025-06-05 11:38:47',
// trade_no: '2025060522001400570506333003',
// send_back_fee: '0.00',
// buyer_logon_id: 'enb***@sandbox.com',
// buyer_open_id: '057XBJUPmv-O8pJZYRH7AtgMTnmk4REw9lZKkcaTurFn542',
// fund_change: 'Y'
const { trade_no } = this.analyzeResult(result);
return {
externalId: trade_no,
};
}
async getRefundState(refund) {
const out_request_no = compressTo32(refund.id);
const out_trade_no = compressTo32(refund.payId);
const result = await this.alipaySdk.curl('POST', '/v3/alipay/trade/fastpay/refund/query', {
body: {
out_trade_no: out_trade_no,
out_request_no: out_request_no,
query_options: [
"refund_detail_item_list",
"gmt_refund_pay"
]
},
needEncrypt: this.needEncrypt,
});
// refund_detail_item_list: [ [Object] ],
// refund_status: 'REFUND_SUCCESS',
// out_trade_no: '1749093519542',
// total_amount: '0.01',
// refund_amount: '0.01',
// gmt_refund_pay: '2025-06-05 11:38:47',
// trade_no: '2025060522001400570506333003',
// send_back_fee: '0.01',
// out_request_no: '1749094724197'
const externalMeta = this.analyzeResult(result);
const { refund_status, gmt_refund_pay } = externalMeta;
/**
* 【退款状态。枚举值: REFUND_SUCCESS 退款处理成功; 未返回该字段表示退款请求未收到或者退款失败; 注如果退款查询发起时间早于退款时间或者间隔退款发起时间太短可能出现退款查询时还没处理成功后面又处理成功的情况建议商户在退款发起后间隔10秒以上再发起退款查询请求
*/
let status = refund_status;
if (refund_status !== 'REFUND_SUCCESS') {
status = 'REFUND_CLOSED';
}
const iState = REFUND_STATE_MATRIX[status];
return [iState, gmt_refund_pay ? {
successAt: gmt_refund_pay ? dayJs(gmt_refund_pay).valueOf() : undefined,
externalMeta,
} : undefined];
}
analyzeResult(result) {
const { data, responseHttpStatus } = result;
if (responseHttpStatus !== 200) {
console.error(JSON.stringify(result));
throw new ExternalPayUtilException(result);
}
return data;
}
caclRefundDeadline(successTime) {
let gapDays = this.refundGapDays || AliPay.DEFAULT_REFUND_DAYS_GAP;
if (gapDays > AliPay.MAX_REFUND_DAYS_GAP) {
gapDays = AliPay.MAX_REFUND_DAYS_GAP;
}
if (gapDays < AliPay.MIN_REFUND_DAYS_GAP) {
gapDays = AliPay.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.startsWith('/') ? endpoint : '/' + endpoint}/${pay.id}`;
const out_trade_no = compressTo32(pay.id);
// 支付宝金额单位为元,需要转换
const totalAmount = ToYuan(pay.price);
const timeoutAt = Date.now() + PREPAY_TIMEOUT;
switch (this.apProduct.type) {
case 'native': {
const method = this.apProduct.config?.prepayMethod || 'GET';
const returnUrl = this.apProduct.config?.returnUrl;
const qrPayMode = this.apProduct.config?.qrPayMode || '2';
const qrCodeWidth = this.apProduct.config?.qrCodeWidth || 100;
const params = {
bizContent: {
subject: pay.order?.desc || (pay.orderId ? '订单支付' : '帐户充值'), // 订单标题
out_trade_no: out_trade_no, // 商户订单号64个字符以内、可包含字母、数字、下划线需保证在商户端不重复
total_amount: totalAmount, // 订单总金额,单位为元
product_code: 'FAST_INSTANT_TRADE_PAY', // 销售产品码,商家和支付宝签约的产品码
notify_url: payNotifyUrl,
qr_pay_mode: qrPayMode,
qrcode_width: qrCodeWidth,
time_expire: dayJs(timeoutAt).format('YYYY-MM-DD HH:mm:ss'),
},
notify_url: payNotifyUrl,
needEncrypt: this.needEncrypt,
};
if (returnUrl) {
params.return_url = returnUrl;
}
const htmlUrl = this.alipaySdk.pageExecute('alipay.trade.page.pay', method, params);
// 电脑网站支付 下单不会返回trade_no, 等支付回调后再获取
data.externalId = 'alipay.trade.page.pay';
data.meta = {
...(pay.meta || {}),
htmlUrl: htmlUrl,
};
break;
}
default: {
throw new Error('本支付方法还没来的及实现呢');
}
}
// pay加一个过期时间到期自动close
data.timeoutAt = timeoutAt + 1000 * 60;
}
async getState(pay) {
const outTradeNo = compressTo32(pay.id);
let result;
try {
result = await this.alipaySdk.curl('POST', '/v3/alipay/trade/query', {
body: {
out_trade_no: outTradeNo,
query_options: ['trade_settle_info'],
},
needEncrypt: this.needEncrypt,
});
}
catch (e) {
// 在电脑网站下单时系统首先生成URL用户扫码后才会创建交易订单。若未完成扫码而查询订单提示'交易不存在'。
if (e.code === 'ACQ.TRADE_NOT_EXIST') {
return ['paying', {}];
}
throw e;
}
// WAIT_BUYER_PAY交易创建等待买家付款、TRADE_CLOSED未付款交易超时关闭或支付完成后全额退款、TRADE_SUCCESS交易支付成功、TRADE_FINISHED交易结束不可退款
// out_trade_no: '1749093519542',
// total_amount: '0.01',
// buyer_user_type: 'PRIVATE',
// trade_status: 'TRADE_SUCCESS',
// trade_no: '2025060522001400570506333003',
// receipt_amount: '0.00',
// buyer_logon_id: 'enb***@sandbox.com',
// send_pay_date: '2025-06-05 11:20:42',
// point_amount: '0.00',
// buyer_open_id: '057XBJUPmv-O8pJZYRH7AtgMTnmk4REw9lZKkcaTurFn542',
// buyer_pay_amount: '0.00',
// invoice_amount: '0.00'
const { trade_status: tradeState, send_pay_date, trade_no } = this.analyzeResult(result);
const iState = TRADE_STATE_MATRIX[tradeState];
assert(iState);
const updateData = {
meta: {
...(pay.meta || {}),
getState: omit(result, ['mch_id', 'app_id', 'out_trade_no']),
},
};
if (pay.externalId === 'alipay.trade.page.pay') {
updateData.externalId = trade_no;
}
if (iState === 'paid') {
updateData.successAt = dayJs(send_pay_date).valueOf();
}
return [iState, updateData];
}
async close(pay) {
const outTradeNo = compressTo32(pay.id);
try {
const result = await this.alipaySdk.curl('POST', '/v3/alipay/trade/close', {
body: {
out_trade_no: outTradeNo,
},
needEncrypt: this.needEncrypt,
});
this.analyzeResult(result);
}
catch (e) {
// 在电脑网站下单时系统首先生成URL用户扫码后才会创建交易订单。若未完成扫码而订单过期到了直接关闭提示'交易不存在'。
if (e.code === 'ACQ.TRADE_NOT_EXIST') {
return;
}
throw e;
}
}
async decodePayNotification(params, body) {
const { sign, sign_type } = body;
const result = body;
/**
* {
gmt_create: '2025-06-05 11:20:25',
charset: 'utf-8',
gmt_payment: '2025-06-05 11:20:42',
notify_time: '2025-06-05 11:20:44',
subject: '测试商品20250605111839',
sign: 'G5UXRw3+Kn8HMT+gNbNLFt/Weg+WNTIzCC+0MjtRjMxygRb9KXU71fXtnUUwVBUtAUbaQKGn4tz7mWum97GGWQtyPGfOZIfCeXZ59bAnnJWIczltwF/7rBlDFryZMSHzW8ghgJF/mmzqg77cucqbTi3kWAN6vmvSG+3hD6QDc3wPvFhHFjbFB+OKdg4Qfo1z68l4P+4DnrCmvtESDTyrAdfCk5HOu6zFz5wq/dO1huMCzsUx+cRK+dEGIY4IRfLkqq2QPMzy56iNIOsi64Ma1gbh3P4ngkDbt4xpwfWx+QLWM2fwbWyziJXiseOyxK9HXWknwPcdk1IcCejmpbpkog==',
buyer_id: '2088722068900570',
invoice_amount: '0.01',
version: '1.0',
notify_id: '2025060501222112043100570506479893',
fund_bill_list: '[{"amount":"0.01","fundChannel":"ALIPAYACCOUNT"}]',
notify_type: 'trade_status_sync',
out_trade_no: '1749093519542',
total_amount: '0.01',
trade_status: 'TRADE_SUCCESS',
trade_no: '2025060522001400570506333003',
auth_app_id: '2021000149620427',
receipt_amount: '0.01',
point_amount: '0.00',
buyer_pay_amount: '0.01',
app_id: '2021000149620427',
sign_type: 'RSA2',
seller_id: '2088721068920157'
}
*/
const { out_trade_no, app_id, trade_status, gmt_payment, trade_no, } = result;
const payId = decompressFrom32(out_trade_no);
assert(app_id === this.appId);
const iState = TRADE_STATE_MATRIX[trade_status];
assert(iState);
const extra = {
externalId: trade_no,
meta: {
decodePayNotification: omit(result, ['app_id', 'out_trade_no'])
},
};
if (iState === 'paid') {
extra.successAt = dayJs(gmt_payment).valueOf();
}
return {
payId,
iState,
extra,
};
}
async decodeRefundNotification(params, body) {
// 支付宝不支持退款回调通知,只能通过查询退款接口获取
const { sign, sign_type } = body;
const result = body;
const { out_refund_no, app_id, refund_status, success_time, } = result;
const refundId = decompressFrom32(out_refund_no);
assert(app_id === this.appId);
const iState = REFUND_STATE_MATRIX[refund_status];
assert(iState);
const extra = {
externalMeta: omit(result, ['app_id',]),
};
if (iState === 'successful') {
assert(success_time);
extra.successAt = dayJs(success_time).valueOf();
}
return {
refundId,
iState,
extra,
};
}
getRefundableAt(successAt) {
return this.caclRefundDeadline(successAt);
}
uploadShipInfo(pay) {
// assert(pay.entity === 'apProduct' && pay.apProduct?.shipping);
}
}