Files
soul/lib/payment/alipay.ts

615 lines
17 KiB
TypeScript
Raw Normal View History

/**
* (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<string, unknown>) {
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<TradeResult> {
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<TradeResult> {
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<TradeResult> {
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<TradeResult> {
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<string, string>,
returnUrl?: string,
notifyUrl?: string
): Record<string, string> {
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
const params: Record<string, string> = {
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, string>): 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, string>): string {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
/**
*
*/
verifySign(data: Record<string, string>): 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<string, string>): 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<string, unknown> = {};
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<string, string> {
const result: Record<string, string> = {};
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<NotifyResult | null> {
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<boolean> {
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<boolean> {
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<string, string> = {
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, string>): 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<string, string>): boolean {
return this.gateway.verifySign(params);
}
async queryTrade(tradeSn: string) {
return this.gateway.queryTrade(tradeSn);
}
private buildPaymentUrl(params: Record<string, string>, sign: string): string {
const gateway = 'https://openapi.alipay.com/gateway.do';
const queryParams = new URLSearchParams({ ...params, sign });
return `${gateway}?${queryParams.toString()}`;
}
}