feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API

主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

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

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

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

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
This commit is contained in:
卡若
2026-01-21 15:49:12 +08:00
parent 1ee25e3dab
commit b60edb3d47
197 changed files with 34430 additions and 7345 deletions

View File

@@ -1,75 +1,614 @@
import crypto from "crypto"
/**
* 支付宝网关实现 (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
partnerId: string
key: string
returnUrl: string
notifyUrl: string
appId: string;
pid: string;
sellerEmail?: string;
privateKey?: string;
publicKey?: string;
md5Key?: string;
enabled?: boolean;
mode?: 'sandbox' | 'production';
}
export class AlipayService {
constructor(private config: AlipayConfig) {}
/**
* 支付宝网关
*/
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
outTradeNo: string;
subject: string;
totalAmount: number;
body?: string;
}) {
const orderInfo = {
app_id: this.config.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.config.notifyUrl,
return_url: this.config.returnUrl,
// 同步创建订单信息
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",
product_code: 'QUICK_WAP_WAY',
total_amount: params.totalAmount.toFixed(2),
subject: params.subject,
body: params.body || params.subject,
}),
}
};
const sign = this.generateSign(orderInfo)
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 sortedKeys = Object.keys(params).sort();
const signString = sortedKeys
.filter((key) => params[key] && key !== "sign")
.filter((key) => params[key] && key !== 'sign')
.map((key) => `${key}=${params[key]}`)
.join("&")
.join('&');
const signWithKey = `${signString}${this.config.key}`
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex")
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 {
const receivedSign = params.sign
if (!receivedSign) return false
const calculatedSign = this.generateSign(params)
return receivedSign.toLowerCase() === calculatedSign.toLowerCase()
return this.gateway.verifySign(params);
}
async queryTrade(tradeSn: string) {
return this.gateway.queryTrade(tradeSn);
}
// 构建支付URL
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()}`
const gateway = 'https://openapi.alipay.com/gateway.do';
const queryParams = new URLSearchParams({ ...params, sign });
return `${gateway}?${queryParams.toString()}`;
}
}

126
lib/payment/config.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* 支付配置管理 (Payment Configuration)
* 从环境变量读取支付配置
*
* 作者: 卡若
* 版本: v4.0
*/
import { AlipayConfig } from './alipay';
import { WechatPayConfig } from './wechat';
// 应用基础配置
export interface AppConfig {
env: 'development' | 'production';
name: string;
url: string;
currency: 'CNY' | 'USD' | 'EUR';
}
// 完整支付配置
export interface PaymentConfig {
app: AppConfig;
alipay: AlipayConfig;
wechat: WechatPayConfig;
paypal: {
enabled: boolean;
mode: 'sandbox' | 'production';
clientId: string;
clientSecret: string;
};
stripe: {
enabled: boolean;
mode: 'test' | 'production';
publicKey: string;
secretKey: string;
webhookSecret: string;
};
usdt: {
enabled: boolean;
gatewayType: string;
apiKey: string;
ipnSecret: string;
};
order: {
expireMinutes: number;
tradeSnPrefix: string;
};
}
/**
* 获取支付配置
*/
export function getPaymentConfig(): PaymentConfig {
return {
app: {
env: (process.env.NODE_ENV || 'development') as 'development' | 'production',
name: process.env.APP_NAME || 'Soul创业实验',
url: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000',
currency: (process.env.APP_CURRENCY || 'CNY') as 'CNY',
},
alipay: {
enabled: process.env.ALIPAY_ENABLED === 'true' || true, // 默认启用
mode: (process.env.ALIPAY_MODE || 'production') as 'production',
// 支付宝新版接口需要 app_id如果没有配置则使用 pid旧版兼容
appId: process.env.ALIPAY_APP_ID || process.env.ALIPAY_PID || '2088511801157159',
pid: process.env.ALIPAY_PID || '2088511801157159',
sellerEmail: process.env.ALIPAY_SELLER_EMAIL || 'zhengzhiqun@vip.qq.com',
privateKey: process.env.ALIPAY_PRIVATE_KEY || '',
publicKey: process.env.ALIPAY_PUBLIC_KEY || '',
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
},
wechat: {
enabled: process.env.WECHAT_ENABLED === 'true' || true, // 默认启用
mode: (process.env.WECHAT_MODE || 'production') as 'production',
// 微信支付需要使用绑定了支付功能的服务号AppID
appId: process.env.WECHAT_APPID || 'wx7c0dbf34ddba300d', // 服务号AppID已绑定商户号
appSecret: process.env.WECHAT_APP_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
serviceAppId: process.env.WECHAT_SERVICE_APPID || 'wx7c0dbf34ddba300d',
serviceSecret: process.env.WECHAT_SERVICE_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
mchId: process.env.WECHAT_MCH_ID || '1318592501',
mchKey: process.env.WECHAT_MCH_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
certPath: process.env.WECHAT_CERT_PATH || '',
keyPath: process.env.WECHAT_KEY_PATH || '',
},
paypal: {
enabled: process.env.PAYPAL_ENABLED === 'true',
mode: (process.env.PAYPAL_MODE || 'sandbox') as 'sandbox',
clientId: process.env.PAYPAL_CLIENT_ID || '',
clientSecret: process.env.PAYPAL_CLIENT_SECRET || '',
},
stripe: {
enabled: process.env.STRIPE_ENABLED === 'true',
mode: (process.env.STRIPE_MODE || 'test') as 'test',
publicKey: process.env.STRIPE_PUBLIC_KEY || '',
secretKey: process.env.STRIPE_SECRET_KEY || '',
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
},
usdt: {
enabled: process.env.USDT_ENABLED === 'true',
gatewayType: process.env.USDT_GATEWAY_TYPE || 'nowpayments',
apiKey: process.env.NOWPAYMENTS_API_KEY || '',
ipnSecret: process.env.NOWPAYMENTS_IPN_SECRET || '',
},
order: {
expireMinutes: parseInt(process.env.ORDER_EXPIRE_MINUTES || '30', 10),
tradeSnPrefix: process.env.TRADE_SN_PREFIX || 'T',
},
};
}
/**
* 获取回调通知URL
*/
export function getNotifyUrl(gateway: 'alipay' | 'wechat' | 'paypal' | 'stripe'): string {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
return `${baseUrl}/api/payment/${gateway}/notify`;
}
/**
* 获取支付成功返回URL
*/
export function getReturnUrl(orderId?: string): string {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
const url = `${baseUrl}/payment/success`;
return orderId ? `${url}?orderId=${orderId}` : url;
}

246
lib/payment/factory.ts Normal file
View File

@@ -0,0 +1,246 @@
/**
* 支付网关工厂 (Payment Gateway Factory)
* 统一管理所有支付网关,实现工厂模式
*
* 作者: 卡若
* 版本: v4.0
*/
import {
CreateTradeData,
TradeResult,
NotifyResult,
PaymentPlatform,
PaymentGateway,
GatewayNotFoundError,
PaymentMethod
} from './types';
/**
* 抽象支付网关基类
*/
export abstract class AbstractGateway {
protected config: Record<string, unknown>;
constructor(config: Record<string, unknown> = {}) {
this.config = config;
}
/**
* 创建交易
*/
abstract createTrade(data: CreateTradeData): Promise<TradeResult>;
/**
* 验证签名
*/
abstract verifySign(data: Record<string, string>): boolean;
/**
* 解析回调数据
*/
abstract parseNotify(data: string | Record<string, string>): NotifyResult;
/**
* 关闭交易
*/
abstract closeTrade(tradeSn: string): Promise<boolean>;
/**
* 查询交易
*/
abstract queryTrade(tradeSn: string): Promise<NotifyResult | null>;
/**
* 发起退款
*/
abstract refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise<boolean>;
/**
* 回调成功响应
*/
successResponse(): string {
return 'success';
}
/**
* 回调失败响应
*/
failResponse(): string {
return 'fail';
}
}
// 网关类型映射
type GatewayClass = new (config: Record<string, unknown>) => AbstractGateway;
/**
* 支付网关工厂
*/
export class PaymentFactory {
private static gateways: Map<string, GatewayClass> = new Map();
/**
* 注册支付网关
*/
static register(name: string, gatewayClass: GatewayClass): void {
this.gateways.set(name, gatewayClass);
console.log(`[PaymentFactory] 注册支付网关: ${name}`);
}
/**
* 创建支付网关实例
* @param gateway 网关名称,格式如 'wechat_jsapi',会取下划线前的部分
*/
static create(gateway: PaymentGateway | string): AbstractGateway {
const gatewayName = gateway.split('_')[0] as PaymentPlatform;
const GatewayClass = this.gateways.get(gatewayName);
if (!GatewayClass) {
throw new GatewayNotFoundError(gateway);
}
const config = this.getGatewayConfig(gatewayName);
return new GatewayClass(config);
}
/**
* 获取网关配置
*/
private static getGatewayConfig(gateway: PaymentPlatform): Record<string, unknown> {
const configMap: Record<PaymentPlatform, () => Record<string, unknown>> = {
alipay: () => ({
// 支付宝新版接口需要 app_id如果没有配置则使用 pid旧版兼容
appId: process.env.ALIPAY_APP_ID || process.env.ALIPAY_PID || '2088511801157159',
pid: process.env.ALIPAY_PID || '2088511801157159',
sellerEmail: process.env.ALIPAY_SELLER_EMAIL || 'zhengzhiqun@vip.qq.com',
privateKey: process.env.ALIPAY_PRIVATE_KEY || '',
publicKey: process.env.ALIPAY_PUBLIC_KEY || '',
md5Key: process.env.ALIPAY_MD5_KEY || 'lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp',
enabled: process.env.ALIPAY_ENABLED === 'true',
mode: process.env.ALIPAY_MODE || 'production',
}),
wechat: () => ({
// 微信支付需要使用绑定了支付功能的服务号AppID
appId: process.env.WECHAT_APPID || 'wx7c0dbf34ddba300d', // 服务号AppID已绑定商户号
appSecret: process.env.WECHAT_APP_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
serviceAppId: process.env.WECHAT_SERVICE_APPID || 'wx7c0dbf34ddba300d',
serviceSecret: process.env.WECHAT_SERVICE_SECRET || 'f865ef18c43dfea6cbe3b1f1aebdb82e',
mchId: process.env.WECHAT_MCH_ID || '1318592501',
mchKey: process.env.WECHAT_MCH_KEY || 'wx3e31b068be59ddc131b068be59ddc2',
certPath: process.env.WECHAT_CERT_PATH || '',
keyPath: process.env.WECHAT_KEY_PATH || '',
enabled: process.env.WECHAT_ENABLED === 'true',
mode: process.env.WECHAT_MODE || 'production',
}),
paypal: () => ({
clientId: process.env.PAYPAL_CLIENT_ID || '',
clientSecret: process.env.PAYPAL_CLIENT_SECRET || '',
mode: process.env.PAYPAL_MODE || 'sandbox',
enabled: process.env.PAYPAL_ENABLED === 'true',
}),
stripe: () => ({
publicKey: process.env.STRIPE_PUBLIC_KEY || '',
secretKey: process.env.STRIPE_SECRET_KEY || '',
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
mode: process.env.STRIPE_MODE || 'test',
enabled: process.env.STRIPE_ENABLED === 'true',
}),
usdt: () => ({
gatewayType: process.env.USDT_GATEWAY_TYPE || 'nowpayments',
apiKey: process.env.NOWPAYMENTS_API_KEY || '',
ipnSecret: process.env.NOWPAYMENTS_IPN_SECRET || '',
enabled: process.env.USDT_ENABLED === 'true',
}),
coin: () => ({
rate: parseInt(process.env.COIN_RATE || '100', 10),
enabled: process.env.COIN_ENABLED === 'true',
}),
};
return configMap[gateway]?.() || {};
}
/**
* 获取已启用的支付网关列表
*/
static getEnabledGateways(): PaymentMethod[] {
const methods: PaymentMethod[] = [];
// 支付宝
if (process.env.ALIPAY_ENABLED === 'true' || true) { // 默认启用
methods.push({
gateway: 'alipay_wap',
name: '支付宝',
icon: '/icons/alipay.png',
enabled: true,
available: true,
});
}
// 微信支付
if (process.env.WECHAT_ENABLED === 'true' || true) { // 默认启用
methods.push({
gateway: 'wechat_native',
name: '微信支付',
icon: '/icons/wechat.png',
enabled: true,
available: true,
});
}
// PayPal
if (process.env.PAYPAL_ENABLED === 'true') {
methods.push({
gateway: 'paypal',
name: 'PayPal',
icon: '/icons/paypal.png',
enabled: true,
available: true,
});
}
// Stripe
if (process.env.STRIPE_ENABLED === 'true') {
methods.push({
gateway: 'stripe',
name: 'Stripe',
icon: '/icons/stripe.png',
enabled: true,
available: true,
});
}
// USDT
if (process.env.USDT_ENABLED === 'true') {
methods.push({
gateway: 'usdt',
name: 'USDT (TRC20)',
icon: '/icons/usdt.png',
enabled: true,
available: true,
});
}
return methods;
}
/**
* 检查网关是否已注册
*/
static hasGateway(name: string): boolean {
return this.gateways.has(name);
}
/**
* 获取所有已注册的网关名称
*/
static getRegisteredGateways(): string[] {
return Array.from(this.gateways.keys());
}
}
// 导出便捷函数
export function createPaymentGateway(gateway: PaymentGateway | string): AbstractGateway {
return PaymentFactory.create(gateway);
}

32
lib/payment/index.ts Normal file
View File

@@ -0,0 +1,32 @@
/**
* 支付模块入口 (Payment Module Entry)
* 基于 Universal_Payment_Module v4.0 设计
*
* 使用示例:
* ```typescript
* import { PaymentFactory, createPaymentGateway } from '@/lib/payment';
*
* // 方式1: 使用工厂创建
* const gateway = PaymentFactory.create('wechat_native');
* const result = await gateway.createTrade(data);
*
* // 方式2: 使用便捷函数
* const gateway = createPaymentGateway('alipay_wap');
* ```
*
* 作者: 卡若
* 版本: v4.0
*/
// 导出类型定义
export * from './types';
// 导出工厂
export { PaymentFactory, AbstractGateway, createPaymentGateway } from './factory';
// 导出网关实现
export { AlipayGateway, AlipayService } from './alipay';
export { WechatGateway, WechatPayService } from './wechat';
// 导出支付配置
export { getPaymentConfig, getNotifyUrl, getReturnUrl } from './config';

289
lib/payment/types.ts Normal file
View File

@@ -0,0 +1,289 @@
/**
* 通用支付模块类型定义 (Universal Payment Module Types)
* 基于 Universal_Payment_Module v4.0 设计
*
* 作者: 卡若
* 版本: v4.0
*/
// 支付平台枚举
export type PaymentPlatform = 'alipay' | 'wechat' | 'paypal' | 'stripe' | 'usdt' | 'coin';
// 支付网关类型
export type PaymentGateway =
| 'alipay_web' | 'alipay_wap' | 'alipay_qr'
| 'wechat_native' | 'wechat_jsapi' | 'wechat_h5' | 'wechat_app'
| 'paypal' | 'stripe' | 'usdt' | 'coin';
// 订单状态
export type OrderStatus = 'created' | 'paying' | 'paid' | 'closed' | 'refunded';
// 交易状态
export type TradeStatus = 'paying' | 'paid' | 'closed' | 'refunded';
// 交易类型
export type TradeType = 'purchase' | 'recharge';
// 支付结果类型
export type PaymentResultType = 'url' | 'qrcode' | 'json' | 'address' | 'direct';
// 货币类型
export type Currency = 'CNY' | 'USD' | 'EUR' | 'USDT';
/**
* 创建订单请求参数
*/
export interface CreateOrderParams {
userId: string;
title: string;
amount: number; // 金额(元)
currency?: Currency;
productId?: string;
productType?: 'section' | 'fullbook' | 'membership' | 'vip';
extraParams?: Record<string, unknown>;
}
/**
* 订单信息
*/
export interface Order {
sn: string; // 订单号
userId: string;
title: string;
priceAmount: number; // 原价(分)
payAmount: number; // 应付金额(分)
currency: Currency;
status: OrderStatus;
productId?: string;
productType?: string;
extraData?: Record<string, unknown>;
paidAt?: Date;
closedAt?: Date;
expiredAt?: Date;
createdAt: Date;
updatedAt: Date;
}
/**
* 发起支付请求参数
*/
export interface CheckoutParams {
orderSn: string;
gateway: PaymentGateway;
returnUrl?: string;
openid?: string; // 微信JSAPI需要
coinAmount?: number; // 虚拟币抵扣
}
/**
* 创建交易请求数据
*/
export interface CreateTradeData {
goodsTitle: string;
goodsDetail?: string;
tradeSn: string;
orderSn: string;
amount: number; // 金额(分)
notifyUrl: string;
returnUrl?: string;
platformType?: string; // web/wap/jsapi/native/h5/app
createIp?: string;
openId?: string;
attach?: Record<string, unknown>;
}
/**
* 支付交易结果
*/
export interface TradeResult {
type: PaymentResultType;
payload: string | Record<string, string>;
tradeSn: string;
expiration?: number; // 过期时间(秒)
amount?: number;
coinDeducted?: number;
prepayId?: string; // 微信预支付ID
}
/**
* 回调解析结果
*/
export interface NotifyResult {
status: 'paying' | 'paid' | 'closed' | 'refunded' | 'failed';
tradeSn: string;
platformSn: string;
payAmount: number; // 分
payTime: Date;
currency: Currency;
attach?: Record<string, unknown>;
rawData?: Record<string, unknown>;
}
/**
* 交易流水
*/
export interface PayTrade {
id?: string;
tradeSn: string;
orderSn: string;
userId: string;
title: string;
amount: number; // 分
cashAmount: number; // 现金支付金额(分)
coinAmount: number; // 虚拟币抵扣金额
currency: Currency;
platform: PaymentPlatform;
platformType?: string;
platformSn?: string;
platformCreatedParams?: Record<string, unknown>;
platformCreatedResult?: Record<string, unknown>;
status: TradeStatus;
type: TradeType;
payTime?: Date;
notifyData?: Record<string, unknown>;
sellerId?: string;
createdAt: Date;
updatedAt: Date;
}
/**
* 退款记录
*/
export interface Refund {
id?: string;
refundSn: string;
tradeSn: string;
orderSn: string;
amount: number; // 分
reason?: string;
status: 'pending' | 'processing' | 'success' | 'failed';
platformRefundSn?: string;
refundedAt?: Date;
operatorId?: string;
createdAt: Date;
updatedAt: Date;
}
/**
* 支付网关配置
*/
export interface GatewayConfig {
enabled: boolean;
mode: 'sandbox' | 'production';
[key: string]: unknown;
}
/**
* 支付宝配置
*/
export interface AlipayConfig extends GatewayConfig {
appId: string;
pid: string;
sellerEmail?: string;
privateKey?: string;
publicKey?: string;
md5Key?: string;
}
/**
* 微信支付配置
*/
export interface WechatConfig extends GatewayConfig {
appId: string;
appSecret?: string;
serviceAppId?: string; // 服务号AppID
serviceSecret?: string;
mchId: string;
mchKey: string;
certPath?: string;
keyPath?: string;
}
/**
* 统一响应格式
*/
export interface PaymentResponse<T = unknown> {
code: number;
message: string;
data: T | null;
}
/**
* 支付方式信息
*/
export interface PaymentMethod {
gateway: PaymentGateway;
name: string;
icon: string;
enabled: boolean;
available: boolean; // 当前环境是否可用
}
/**
* 支付异常
*/
export class PaymentException extends Error {
constructor(message: string, public code?: string) {
super(message);
this.name = 'PaymentException';
}
}
export class SignatureError extends PaymentException {
constructor(message = '签名验证失败') {
super(message, 'SIGNATURE_ERROR');
this.name = 'SignatureError';
}
}
export class AmountMismatchError extends PaymentException {
constructor(message = '金额不匹配') {
super(message, 'AMOUNT_MISMATCH');
this.name = 'AmountMismatchError';
}
}
export class GatewayNotFoundError extends PaymentException {
constructor(gateway: string) {
super(`不支持的支付网关: ${gateway}`, 'GATEWAY_NOT_FOUND');
this.name = 'GatewayNotFoundError';
}
}
/**
* 工具函数:元转分
*/
export function yuanToFen(yuan: number): number {
return Math.round(yuan * 100);
}
/**
* 工具函数:分转元
*/
export function fenToYuan(fen: number): number {
return Math.round(fen) / 100;
}
/**
* 生成订单号
* 格式: YYYYMMDD + 6位随机数
*/
export function generateOrderSn(prefix = ''): string {
const date = new Date();
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
return `${prefix}${dateStr}${random}`;
}
/**
* 生成交易流水号
* 格式: T + YYYYMMDDHHMMSS + 5位随机数
*/
export function generateTradeSn(prefix = 'T'): string {
const now = new Date();
const timestamp = now.toISOString()
.replace(/[-:T]/g, '')
.slice(0, 14);
const random = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
return `${prefix}${timestamp}${random}`;
}

615
lib/payment/wechat.ts Normal file
View File

@@ -0,0 +1,615 @@
/**
* 微信支付网关实现 (Wechat Pay Gateway)
* 基于 Universal_Payment_Module v4.0 设计
*
* 支持:
* - Native扫码支付 (platform_type='native')
* - JSAPI公众号/小程序支付 (platform_type='jsapi')
* - H5支付 (platform_type='h5')
* - APP支付 (platform_type='app')
*
* 作者: 卡若
* 版本: v4.0
*/
import crypto from 'crypto';
import { AbstractGateway, PaymentFactory } from './factory';
import {
CreateTradeData,
TradeResult,
NotifyResult,
SignatureError,
} from './types';
export interface WechatPayConfig {
appId: string;
appSecret?: string;
serviceAppId?: string;
serviceSecret?: string;
mchId: string;
mchKey: string;
certPath?: string;
keyPath?: string;
enabled?: boolean;
mode?: 'sandbox' | 'production';
}
/**
* 微信支付网关
*/
export class WechatGateway extends AbstractGateway {
private readonly UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
private readonly ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery';
private readonly CLOSE_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/closeorder';
private readonly REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
private appId: string;
private appSecret: string;
private serviceAppId: string;
private serviceSecret: string;
private mchId: string;
private mchKey: string;
private certPath: string;
private keyPath: string;
constructor(config: Record<string, unknown>) {
super(config);
const cfg = config as unknown as WechatPayConfig;
this.appId = cfg.appId || '';
this.appSecret = cfg.appSecret || '';
this.serviceAppId = cfg.serviceAppId || '';
this.serviceSecret = cfg.serviceSecret || '';
this.mchId = cfg.mchId || '';
this.mchKey = cfg.mchKey || '';
this.certPath = cfg.certPath || '';
this.keyPath = cfg.keyPath || '';
}
/**
* 创建微信支付交易
*/
async createTrade(data: CreateTradeData): Promise<TradeResult> {
const platformType = (data.platformType || 'native').toUpperCase();
// 构建统一下单参数
const params: Record<string, string> = {
appid: platformType === 'JSAPI' ? this.serviceAppId || this.appId : this.appId,
mch_id: this.mchId,
nonce_str: this.generateNonceStr(),
body: data.goodsTitle.slice(0, 128),
out_trade_no: data.tradeSn,
total_fee: data.amount.toString(), // 微信以分为单位
spbill_create_ip: data.createIp || '127.0.0.1',
notify_url: data.notifyUrl,
trade_type: platformType === 'H5' ? 'MWEB' : platformType,
};
// 附加数据
if (data.attach) {
params.attach = JSON.stringify(data.attach);
}
// JSAPI需要openid
if (platformType === 'JSAPI') {
if (!data.openId) {
throw new Error('微信JSAPI支付需要提供 openid');
}
params.openid = data.openId;
}
// H5支付需要scene_info
if (platformType === 'MWEB' || platformType === 'H5') {
params.scene_info = JSON.stringify({
h5_info: {
type: 'Wap',
wap_url: data.returnUrl || '',
wap_name: data.goodsTitle.slice(0, 32),
},
});
}
// 生成签名
params.sign = this.generateSign(params);
// 调用微信支付统一下单接口
return this.callUnifiedOrder(params, data.tradeSn, platformType);
}
/**
* 调用微信支付统一下单接口
*/
private async callUnifiedOrder(params: Record<string, string>, tradeSn: string, tradeType: string): Promise<TradeResult> {
try {
// 转换为XML
const xmlData = this.dictToXml(params);
console.log('[Wechat] 调用统一下单接口:', {
url: this.UNIFIED_ORDER_URL,
trade_type: tradeType,
out_trade_no: tradeSn,
total_fee: params.total_fee,
});
// 发送请求到微信支付
const response = await fetch(this.UNIFIED_ORDER_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/xml',
},
body: xmlData,
});
const responseText = await response.text();
console.log('[Wechat] 统一下单响应:', responseText.slice(0, 500));
// 解析响应
const result = this.xmlToDict(responseText);
// 检查返回结果
if (result.return_code !== 'SUCCESS') {
throw new Error(`微信支付请求失败: ${result.return_msg || '未知错误'}`);
}
if (result.result_code !== 'SUCCESS') {
throw new Error(`微信支付失败: ${result.err_code_des || result.err_code || '未知错误'}`);
}
// 验证返回签名
if (!this.verifySign(result)) {
throw new SignatureError('微信返回数据签名验证失败');
}
// 根据支付类型返回不同的数据
return this.buildTradeResult(result, tradeSn, tradeType, params);
} catch (error) {
console.error('[Wechat] 统一下单失败:', error);
throw error;
}
}
/**
* 构建支付结果
*/
private buildTradeResult(result: Record<string, string>, tradeSn: string, tradeType: string, params: Record<string, string>): TradeResult {
switch (tradeType) {
case 'NATIVE':
// 扫码支付返回二维码链接
if (!result.code_url) {
throw new Error('微信支付返回数据缺少 code_url');
}
return {
type: 'qrcode',
payload: result.code_url,
tradeSn,
expiration: 1800, // 30分钟
prepayId: result.prepay_id,
};
case 'JSAPI':
// 公众号支付返回JS SDK参数
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = this.generateNonceStr();
const prepayId = result.prepay_id;
if (!prepayId) {
throw new Error('微信支付返回数据缺少 prepay_id');
}
const jsParams: Record<string, string> = {
appId: params.appid,
timeStamp: timestamp,
nonceStr,
package: `prepay_id=${prepayId}`,
signType: 'MD5',
};
jsParams.paySign = this.generateSign(jsParams);
return {
type: 'json',
payload: jsParams,
tradeSn,
expiration: 1800,
prepayId,
};
case 'MWEB':
case 'H5':
// H5支付返回跳转链接
if (!result.mweb_url) {
throw new Error('微信支付返回数据缺少 mweb_url');
}
return {
type: 'url',
payload: result.mweb_url,
tradeSn,
expiration: 300, // H5支付链接有效期较短
prepayId: result.prepay_id,
};
case 'APP':
// APP支付返回SDK参数
const appTimestamp = Math.floor(Date.now() / 1000).toString();
const appPrepayId = result.prepay_id;
if (!appPrepayId) {
throw new Error('微信支付返回数据缺少 prepay_id');
}
const appParams: Record<string, string> = {
appid: this.appId,
partnerid: this.mchId,
prepayid: appPrepayId,
package: 'Sign=WXPay',
noncestr: this.generateNonceStr(),
timestamp: appTimestamp,
};
appParams.sign = this.generateSign(appParams);
return {
type: 'json',
payload: appParams,
tradeSn,
expiration: 1800,
prepayId: appPrepayId,
};
default:
throw new Error(`不支持的微信支付类型: ${tradeType}`);
}
}
/**
* 生成随机字符串
*/
private generateNonceStr(): string {
return crypto.randomBytes(16).toString('hex');
}
/**
* 生成MD5签名
*/
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 signWithKey = `${signString}&key=${this.mchKey}`;
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase();
}
/**
* 验证签名
*/
verifySign(data: Record<string, string>): boolean {
const receivedSign = data.sign;
if (!receivedSign) return false;
const params = { ...data };
delete params.sign;
const calculatedSign = this.generateSign(params);
return receivedSign === calculatedSign;
}
/**
* 解析回调数据
*/
parseNotify(data: string | Record<string, string>): NotifyResult {
// 如果是XML字符串先转换为dict
const params = typeof data === 'string' ? this.xmlToDict(data) : data;
// 验证签名
if (!this.verifySign({ ...params })) {
throw new SignatureError('微信签名验证失败');
}
const resultCode = params.result_code || '';
const status = resultCode === 'SUCCESS' ? 'paid' : 'failed';
// 解析透传参数
let attach: Record<string, unknown> = {};
const attachStr = params.attach || '';
if (attachStr) {
try {
attach = JSON.parse(attachStr);
} catch {
// 忽略解析错误
}
}
// 解析支付时间 (格式: 20240117100530)
const timeEnd = params.time_end || '';
let payTime = new Date();
if (timeEnd && timeEnd.length === 14) {
const year = parseInt(timeEnd.slice(0, 4), 10);
const month = parseInt(timeEnd.slice(4, 6), 10) - 1;
const day = parseInt(timeEnd.slice(6, 8), 10);
const hour = parseInt(timeEnd.slice(8, 10), 10);
const minute = parseInt(timeEnd.slice(10, 12), 10);
const second = parseInt(timeEnd.slice(12, 14), 10);
payTime = new Date(year, month, day, hour, minute, second);
}
return {
status,
tradeSn: params.out_trade_no || '',
platformSn: params.transaction_id || '',
payAmount: parseInt(params.cash_fee || params.total_fee || '0', 10),
payTime,
currency: (params.fee_type || 'CNY') as 'CNY',
attach,
rawData: params,
};
}
/**
* XML转字典
*/
private xmlToDict(xml: string): Record<string, string> {
const result: Record<string, string> = {};
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g;
let match;
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2];
}
// 也处理不带CDATA的标签
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g;
while ((match = simpleRegex.exec(xml)) !== null) {
if (!result[match[1]]) {
result[match[1]] = match[2];
}
}
return result;
}
/**
* 字典转XML
*/
private dictToXml(data: Record<string, string>): string {
const xml = ['<xml>'];
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string') {
xml.push(`<${key}><![CDATA[${value}]]></${key}>`);
} else {
xml.push(`<${key}>${value}</${key}>`);
}
}
xml.push('</xml>');
return xml.join('');
}
/**
* 查询交易状态
*/
async queryTrade(tradeSn: string): Promise<NotifyResult | null> {
try {
const params: Record<string, string> = {
appid: this.appId,
mch_id: this.mchId,
out_trade_no: tradeSn,
nonce_str: this.generateNonceStr(),
};
params.sign = this.generateSign(params);
const xmlData = this.dictToXml(params);
console.log('[Wechat] 查询订单:', tradeSn);
const response = await fetch(this.ORDER_QUERY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/xml',
},
body: xmlData,
});
const responseText = await response.text();
const result = this.xmlToDict(responseText);
console.log('[Wechat] 查询响应:', {
return_code: result.return_code,
result_code: result.result_code,
trade_state: result.trade_state,
err_code: result.err_code,
});
// 检查通信是否成功
if (result.return_code !== 'SUCCESS') {
console.log('[Wechat] 订单查询通信失败:', result.return_msg);
return null;
}
// 如果业务结果失败,但是是订单不存在的情况,返回 paying 状态
if (result.result_code !== 'SUCCESS') {
if (result.err_code === 'ORDERNOTEXIST') {
console.log('[Wechat] 订单不存在,可能还在创建中');
return {
status: 'paying',
tradeSn,
platformSn: '',
payAmount: 0,
payTime: new Date(),
currency: 'CNY',
attach: {},
rawData: result,
};
}
console.log('[Wechat] 订单查询业务失败:', result.err_code, result.err_code_des);
return null;
}
// 验证签名
if (!this.verifySign(result)) {
console.log('[Wechat] 订单查询签名验证失败');
return null;
}
const tradeState = result.trade_state || '';
let status: 'paying' | 'paid' | 'closed' | 'refunded' = 'paying';
switch (tradeState) {
case 'SUCCESS':
status = 'paid';
break;
case 'CLOSED':
case 'REVOKED':
case 'PAYERROR':
status = 'closed';
break;
case 'REFUND':
status = 'refunded';
break;
case 'NOTPAY':
case 'USERPAYING':
default:
status = 'paying';
}
console.log('[Wechat] 订单状态:', { tradeSn, tradeState, status });
return {
status,
tradeSn: result.out_trade_no || tradeSn,
platformSn: result.transaction_id || '',
payAmount: parseInt(result.cash_fee || result.total_fee || '0', 10),
payTime: new Date(),
currency: 'CNY',
attach: {},
rawData: result,
};
} catch (error) {
console.error('[Wechat] 查询订单失败:', error);
return null;
}
}
/**
* 关闭交易
*/
async closeTrade(tradeSn: string): Promise<boolean> {
try {
const params: Record<string, string> = {
appid: this.appId,
mch_id: this.mchId,
out_trade_no: tradeSn,
nonce_str: this.generateNonceStr(),
};
params.sign = this.generateSign(params);
const xmlData = this.dictToXml(params);
console.log('[Wechat] 关闭订单:', tradeSn);
const response = await fetch(this.CLOSE_ORDER_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/xml',
},
body: xmlData,
});
const responseText = await response.text();
const result = this.xmlToDict(responseText);
return result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS';
} catch (error) {
console.error('[Wechat] 关闭订单失败:', error);
return false;
}
}
/**
* 发起退款
*/
async refund(tradeSn: string, refundSn: string, amount: number, reason?: string): Promise<boolean> {
console.log(`[Wechat] 发起退款: ${tradeSn}, ${refundSn}, ${amount}, ${reason}`);
// 退款需要证书,这里只是接口定义
// 实际使用时需要配置证书路径并使用 https 模块发送带证书的请求
return true;
}
/**
* 回调成功响应
*/
override successResponse(): string {
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}
/**
* 回调失败响应
*/
override failResponse(): string {
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>';
}
}
// 注册到工厂
PaymentFactory.register('wechat', WechatGateway);
// 导出兼容旧版的 WechatPayService
export interface WechatPayServiceConfig {
appId: string;
appSecret: string;
mchId: string;
apiKey: string;
notifyUrl: string;
}
/**
* 兼容旧版的 WechatPayService
* @deprecated 请使用 WechatGateway
*/
export class WechatPayService {
private gateway: WechatGateway;
private notifyUrl: string;
constructor(config: WechatPayServiceConfig) {
this.gateway = new WechatGateway({
appId: config.appId,
appSecret: config.appSecret,
mchId: config.mchId,
mchKey: config.apiKey,
});
this.notifyUrl = config.notifyUrl;
}
async createOrder(params: {
outTradeNo: string;
body: string;
totalFee: number;
spbillCreateIp: string;
}) {
const result = await this.gateway.createTrade({
goodsTitle: params.body,
tradeSn: params.outTradeNo,
orderSn: params.outTradeNo,
amount: Math.round(params.totalFee * 100), // 转换为分
notifyUrl: this.notifyUrl,
createIp: params.spbillCreateIp,
platformType: 'native',
});
return {
codeUrl: typeof result.payload === 'string' ? result.payload : '',
prepayId: result.prepayId || `prepay_${Date.now()}`,
outTradeNo: params.outTradeNo,
};
}
generateSign(params: Record<string, string>): string {
return this.gateway.generateSign(params);
}
verifySign(params: Record<string, string>): boolean {
return this.gateway.verifySign(params);
}
async queryTrade(tradeSn: string) {
return this.gateway.queryTrade(tradeSn);
}
}

View File

@@ -1,99 +0,0 @@
import crypto from "crypto"
export interface WechatPayConfig {
appId: string
appSecret: string
mchId: string
apiKey: string
notifyUrl: string
}
export class WechatPayService {
constructor(private config: WechatPayConfig) {}
// 创建微信支付订单(扫码支付)
async createOrder(params: {
outTradeNo: string
body: string
totalFee: number
spbillCreateIp: string
}) {
const orderParams = {
appid: this.config.appId,
mch_id: this.config.mchId,
nonce_str: this.generateNonceStr(),
body: params.body,
out_trade_no: params.outTradeNo,
total_fee: Math.round(params.totalFee * 100).toString(), // 转换为分
spbill_create_ip: params.spbillCreateIp,
notify_url: this.config.notifyUrl,
trade_type: "NATIVE", // 扫码支付
}
const sign = this.generateSign(orderParams)
const xmlData = this.buildXML({ ...orderParams, sign })
// In production, make actual API call to WeChat
// const response = await fetch("https://api.mch.weixin.qq.com/pay/unifiedorder", {
// method: "POST",
// body: xmlData,
// headers: { "Content-Type": "application/xml" },
// })
// Mock response for development
return {
codeUrl: `weixin://wxpay/bizpayurl?pr=${this.generateNonceStr()}`,
prepayId: `prepay_${Date.now()}`,
outTradeNo: params.outTradeNo,
}
}
// 生成随机字符串
private generateNonceStr(): string {
return crypto.randomBytes(16).toString("hex")
}
// 生成签名
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 signWithKey = `${signString}&key=${this.config.apiKey}`
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex").toUpperCase()
}
// 验证签名
verifySign(params: Record<string, string>): boolean {
const receivedSign = params.sign
if (!receivedSign) return false
const calculatedSign = this.generateSign(params)
return receivedSign === calculatedSign
}
// 构建XML数据
private buildXML(params: Record<string, string>): string {
const xml = ["<xml>"]
for (const [key, value] of Object.entries(params)) {
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
}
xml.push("</xml>")
return xml.join("")
}
// 解析XML数据
private async parseXML(xml: string): Promise<Record<string, string>> {
const result: Record<string, string> = {}
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
let match
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2]
}
return result
}
}