Files
soul/lib/payment/alipay.ts
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

615 lines
17 KiB
TypeScript
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.

/**
* 支付宝网关实现 (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()}`;
}
}