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 = ''; 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) { 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) { 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); } }