/** * 支付宝网关实现 (Alipay Gateway) * 基于 Universal_Payment_Module v4.0 设计 * * 支持: * - 电脑网站支付 (platform_type='web') * - 手机网站支付 (platform_type='wap') * - 扫码支付 (platform_type='qr') * * 作者: 卡若 * 版本: v4.0 */ import crypto from 'crypto'; import { AbstractGateway, PaymentFactory } from './factory'; import { CreateTradeData, TradeResult, NotifyResult, SignatureError, fenToYuan, yuanToFen, } from './types'; export interface AlipayConfig { appId: string; pid: string; sellerEmail?: string; privateKey?: string; publicKey?: string; md5Key?: string; enabled?: boolean; mode?: 'sandbox' | 'production'; } /** * 支付宝网关 */ export class AlipayGateway extends AbstractGateway { private readonly GATEWAY_URL = 'https://openapi.alipay.com/gateway.do'; private readonly SANDBOX_URL = 'https://openapi.alipaydev.com/gateway.do'; private appId: string; private pid: string; private sellerEmail: string; private privateKey: string; private publicKey: string; private md5Key: string; private mode: 'sandbox' | 'production'; constructor(config: Record) { super(config); const cfg = config as unknown as AlipayConfig; this.appId = cfg.appId || ''; this.pid = cfg.pid || ''; this.sellerEmail = cfg.sellerEmail || ''; this.privateKey = cfg.privateKey || ''; this.publicKey = cfg.publicKey || ''; this.md5Key = cfg.md5Key || ''; this.mode = cfg.mode || 'production'; } /** * 获取网关地址 */ private getGatewayUrl(): string { return this.mode === 'sandbox' ? this.SANDBOX_URL : this.GATEWAY_URL; } /** * 创建支付宝交易 */ async createTrade(data: CreateTradeData): Promise { const platformType = (data.platformType || 'wap').toLowerCase(); switch (platformType) { case 'web': return this.createWebTrade(data); case 'wap': return this.createWapTrade(data); case 'qr': return this.createQrTrade(data); default: // 默认使用 WAP 支付 return this.createWapTrade(data); } } /** * 电脑网站支付 */ private async createWebTrade(data: CreateTradeData): Promise { const bizContent = { subject: data.goodsTitle.slice(0, 256), out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), product_code: 'FAST_INSTANT_TRADE_PAY', body: data.goodsDetail?.slice(0, 128) || '', passback_params: data.attach ? encodeURIComponent(JSON.stringify(data.attach)) : '', }; const params = this.buildParams('alipay.trade.page.pay', bizContent, data.returnUrl, data.notifyUrl); const sign = this.generateMD5Sign(params); params.sign = sign; const payUrl = `${this.getGatewayUrl()}?${this.buildQueryString(params)}`; console.log('[Alipay] 创建电脑网站支付:', { out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), }); return { type: 'url', payload: payUrl, tradeSn: data.tradeSn, expiration: 1800, }; } /** * 手机网站支付 */ private async createWapTrade(data: CreateTradeData): Promise { const bizContent = { subject: data.goodsTitle.slice(0, 256), out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), product_code: 'QUICK_WAP_WAY', body: data.goodsDetail?.slice(0, 128) || '', passback_params: data.attach ? encodeURIComponent(JSON.stringify(data.attach)) : '', }; const params = this.buildParams('alipay.trade.wap.pay', bizContent, data.returnUrl, data.notifyUrl); const sign = this.generateMD5Sign(params); params.sign = sign; const payUrl = `${this.getGatewayUrl()}?${this.buildQueryString(params)}`; console.log('[Alipay] 创建手机网站支付:', { out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), }); return { type: 'url', payload: payUrl, tradeSn: data.tradeSn, expiration: 1800, }; } /** * 扫码支付(当面付) */ private async createQrTrade(data: CreateTradeData): Promise { const bizContent = { subject: data.goodsTitle.slice(0, 256), out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), body: data.goodsDetail?.slice(0, 128) || '', }; const params = this.buildParams('alipay.trade.precreate', bizContent, '', data.notifyUrl); const sign = this.generateMD5Sign(params); params.sign = sign; console.log('[Alipay] 创建扫码支付:', { out_trade_no: data.tradeSn, total_amount: fenToYuan(data.amount).toFixed(2), }); try { // 调用支付宝预下单接口 const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, { method: 'GET', }); const responseText = await response.text(); console.log('[Alipay] 预下单响应:', responseText.slice(0, 500)); // 解析JSON响应 const result = JSON.parse(responseText); const precreateResponse = result.alipay_trade_precreate_response; if (precreateResponse && precreateResponse.code === '10000' && precreateResponse.qr_code) { return { type: 'qrcode', payload: precreateResponse.qr_code, tradeSn: data.tradeSn, expiration: 1800, }; } // 如果API调用失败,回退到WAP支付方式 console.log('[Alipay] 预下单失败,使用WAP支付:', precreateResponse?.sub_msg || precreateResponse?.msg); return this.createWapTrade(data); } catch (error) { console.error('[Alipay] 预下单异常,使用WAP支付:', error); return this.createWapTrade(data); } } /** * 构建公共参数 */ private buildParams( method: string, bizContent: Record, returnUrl?: string, notifyUrl?: string ): Record { const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' '); const params: Record = { app_id: this.appId, method, charset: 'utf-8', sign_type: 'MD5', timestamp, version: '1.0', biz_content: JSON.stringify(bizContent), }; if (returnUrl) { params.return_url = returnUrl; } if (notifyUrl) { params.notify_url = notifyUrl; } return params; } /** * 生成MD5签名 */ private generateMD5Sign(params: Record): string { const sortedKeys = Object.keys(params).sort(); const signString = sortedKeys .filter((key) => params[key] && key !== 'sign') .map((key) => `${key}=${params[key]}`) .join('&'); const signWithKey = `${signString}${this.md5Key}`; return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex'); } /** * 构建查询字符串 */ private buildQueryString(params: Record): string { return Object.entries(params) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } /** * 验证签名 */ verifySign(data: Record): boolean { const receivedSign = data.sign; if (!receivedSign) return false; // 复制数据,移除 sign 和 sign_type const params = { ...data }; delete params.sign; delete params.sign_type; const calculatedSign = this.generateMD5Sign(params); return receivedSign.toLowerCase() === calculatedSign.toLowerCase(); } /** * 解析回调数据 */ parseNotify(data: string | Record): NotifyResult { const params = typeof data === 'string' ? this.parseFormData(data) : data; // 验证签名 if (!this.verifySign({ ...params })) { throw new SignatureError('支付宝签名验证失败'); } const tradeStatus = params.trade_status || ''; const status = ['TRADE_SUCCESS', 'TRADE_FINISHED'].includes(tradeStatus) ? 'paid' : 'failed'; // 解析透传参数 let attach: Record = {}; const passback = params.passback_params || ''; if (passback) { try { attach = JSON.parse(decodeURIComponent(passback)); } catch { // 忽略解析错误 } } // 解析支付时间 const gmtPayment = params.gmt_payment || ''; const payTime = gmtPayment ? new Date(gmtPayment) : new Date(); return { status, tradeSn: params.out_trade_no || '', platformSn: params.trade_no || '', payAmount: yuanToFen(parseFloat(params.total_amount || '0')), payTime, currency: 'CNY', attach, rawData: params, }; } /** * 解析表单数据 */ private parseFormData(formString: string): Record { const result: Record = {}; const pairs = formString.split('&'); for (const pair of pairs) { const [key, value] = pair.split('='); if (key && value !== undefined) { result[decodeURIComponent(key)] = decodeURIComponent(value); } } return result; } /** * 查询交易状态 */ async queryTrade(tradeSn: string): Promise { try { // 检查 appId 是否配置 if (!this.appId) { console.log('[Alipay] 查询跳过: 未配置 appId'); return { status: 'paying', tradeSn, platformSn: '', payAmount: 0, payTime: new Date(), currency: 'CNY', attach: {}, rawData: {}, }; } const bizContent = { out_trade_no: tradeSn, }; const params = this.buildParams('alipay.trade.query', bizContent); params.sign = this.generateMD5Sign(params); console.log('[Alipay] 查询订单:', { tradeSn, appId: this.appId }); const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, { method: 'GET', }); const responseText = await response.text(); console.log('[Alipay] 查询响应:', responseText.slice(0, 300)); const result = JSON.parse(responseText); const queryResponse = result.alipay_trade_query_response; // 如果订单不存在,返回 paying 状态(可能还没同步到支付宝) if (queryResponse?.code === '40004' && queryResponse?.sub_code === 'ACQ.TRADE_NOT_EXIST') { console.log('[Alipay] 订单不存在,可能还在等待支付'); return { status: 'paying', tradeSn, platformSn: '', payAmount: 0, payTime: new Date(), currency: 'CNY', attach: {}, rawData: queryResponse, }; } if (!queryResponse || queryResponse.code !== '10000') { console.log('[Alipay] 订单查询失败:', { code: queryResponse?.code, msg: queryResponse?.msg, sub_code: queryResponse?.sub_code, sub_msg: queryResponse?.sub_msg, }); // 返回 paying 状态而不是 null,让前端继续轮询 return { status: 'paying', tradeSn, platformSn: '', payAmount: 0, payTime: new Date(), currency: 'CNY', attach: {}, rawData: queryResponse || {}, }; } const tradeStatus = queryResponse.trade_status || ''; let status: 'paying' | 'paid' | 'closed' | 'refunded' = 'paying'; switch (tradeStatus) { case 'TRADE_SUCCESS': case 'TRADE_FINISHED': status = 'paid'; break; case 'TRADE_CLOSED': status = 'closed'; break; case 'WAIT_BUYER_PAY': default: status = 'paying'; } console.log('[Alipay] 订单状态:', { tradeSn, tradeStatus, status }); return { status, tradeSn: queryResponse.out_trade_no || tradeSn, platformSn: queryResponse.trade_no || '', payAmount: yuanToFen(parseFloat(queryResponse.total_amount || '0')), payTime: new Date(queryResponse.send_pay_date || Date.now()), currency: 'CNY', attach: {}, rawData: queryResponse, }; } catch (error) { console.error('[Alipay] 查询订单失败:', error); // 返回 paying 状态而不是 null return { status: 'paying', tradeSn, platformSn: '', payAmount: 0, payTime: new Date(), currency: 'CNY', attach: {}, rawData: {}, }; } } /** * 关闭交易 */ async closeTrade(tradeSn: string): Promise { try { const bizContent = { out_trade_no: tradeSn, }; const params = this.buildParams('alipay.trade.close', bizContent); params.sign = this.generateMD5Sign(params); console.log('[Alipay] 关闭订单:', tradeSn); const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, { method: 'GET', }); const responseText = await response.text(); const result = JSON.parse(responseText); const closeResponse = result.alipay_trade_close_response; return closeResponse && closeResponse.code === '10000'; } catch (error) { console.error('[Alipay] 关闭订单失败:', error); return false; } } /** * 发起退款 */ async refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise { try { const bizContent = { out_trade_no: tradeSn, out_request_no: refundSn, refund_amount: fenToYuan(amount).toFixed(2), refund_reason: reason || '用户退款', }; const params = this.buildParams('alipay.trade.refund', bizContent); params.sign = this.generateMD5Sign(params); console.log('[Alipay] 发起退款:', { tradeSn, refundSn, amount }); const response = await fetch(`${this.getGatewayUrl()}?${this.buildQueryString(params)}`, { method: 'GET', }); const responseText = await response.text(); const result = JSON.parse(responseText); const refundResponse = result.alipay_trade_refund_response; return refundResponse && refundResponse.code === '10000'; } catch (error) { console.error('[Alipay] 退款失败:', error); return false; } } /** * 回调成功响应 */ override successResponse(): string { return 'success'; } /** * 回调失败响应 */ override failResponse(): string { return 'fail'; } } // 注册到工厂 PaymentFactory.register('alipay', AlipayGateway); // 导出兼容旧版的 AlipayService export interface AlipayServiceConfig { appId: string; partnerId: string; key: string; returnUrl: string; notifyUrl: string; } /** * 兼容旧版的 AlipayService * @deprecated 请使用 AlipayGateway */ export class AlipayService { private gateway: AlipayGateway; private notifyUrl: string; private returnUrl: string; constructor(config: AlipayServiceConfig) { this.gateway = new AlipayGateway({ appId: config.appId, pid: config.partnerId, md5Key: config.key, }); this.notifyUrl = config.notifyUrl; this.returnUrl = config.returnUrl; } createOrder(params: { outTradeNo: string; subject: string; totalAmount: number; body?: string; }) { // 同步创建订单信息 const orderInfo: Record = { app_id: (this.gateway as AlipayGateway)['appId'], method: 'alipay.trade.wap.pay', format: 'JSON', charset: 'utf-8', sign_type: 'MD5', timestamp: new Date().toISOString().slice(0, 19).replace('T', ' '), version: '1.0', notify_url: this.notifyUrl, return_url: this.returnUrl, biz_content: JSON.stringify({ out_trade_no: params.outTradeNo, product_code: 'QUICK_WAP_WAY', total_amount: params.totalAmount.toFixed(2), subject: params.subject, body: params.body || params.subject, }), }; const sign = this.generateSign(orderInfo); return { ...orderInfo, sign, paymentUrl: this.buildPaymentUrl(orderInfo, sign), }; } generateSign(params: Record): string { const sortedKeys = Object.keys(params).sort(); const signString = sortedKeys .filter((key) => params[key] && key !== 'sign') .map((key) => `${key}=${params[key]}`) .join('&'); const md5Key = (this.gateway as AlipayGateway)['md5Key']; const signWithKey = `${signString}${md5Key}`; return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex'); } verifySign(params: Record): boolean { return this.gateway.verifySign(params); } async queryTrade(tradeSn: string) { return this.gateway.queryTrade(tradeSn); } private buildPaymentUrl(params: Record, sign: string): string { const gateway = 'https://openapi.alipay.com/gateway.do'; const queryParams = new URLSearchParams({ ...params, sign }); return `${gateway}?${queryParams.toString()}`; } }