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', }; 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}/${pay.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 { status, success_time } = this.analyzeResult(result); /** * 【退款状态】 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。 可选取值: SUCCESS: 退款成功 CLOSED: 退款关闭 PROCESSING: 退款处理中 ABNORMAL: 退款异常 */ const iState = REFUND_STATE_MATRIX[status]; return [iState, success_time ? { successAt: dayJs(success_time).millisecond() } : 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('本支付方法还没来的及实现呢'); } } } 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.forbidRefundAt = dayJs(success_time).millisecond(); } 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 = success_time; } 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 = { meta: omit(result, ['mchid',]), }; if (iState === 'successful') { extra.successAt = success_time; } return { refundId, iState, extra, }; } getRefundableAt(successAt) { return this.caclRefundDeadline(successAt); } }