139 lines
4.6 KiB
TypeScript
139 lines
4.6 KiB
TypeScript
import crypto from 'crypto';
|
||
import { SystemConfig } from '@/models';
|
||
import dbConnect from './dbConnect';
|
||
|
||
interface AlipayConfig {
|
||
appId: string;
|
||
privateKey: string;
|
||
alipayPublicKey: string;
|
||
gateway: string;
|
||
notifyUrl: string;
|
||
returnUrl?: string;
|
||
}
|
||
|
||
export class AlipayService {
|
||
private config: AlipayConfig | null = null;
|
||
|
||
async init() {
|
||
if (this.config) return;
|
||
|
||
await dbConnect();
|
||
const systemConfig = await SystemConfig.findOne({ 配置标识: 'default' }).select('+支付宝设置.AppID +支付宝设置.应用私钥 +支付宝设置.公钥 +支付宝设置.网关地址 +支付宝设置.回调URL');
|
||
|
||
if (!systemConfig || !systemConfig.支付宝设置) {
|
||
throw new Error('Alipay configuration not found');
|
||
}
|
||
|
||
const { AppID, 应用私钥, 公钥, 网关地址, 回调URL } = systemConfig.支付宝设置;
|
||
|
||
this.config = {
|
||
appId: AppID,
|
||
privateKey: this.convertBase64ToPem(应用私钥, 'PRIVATE KEY'),
|
||
alipayPublicKey: this.convertBase64ToPem(公钥, 'PUBLIC KEY'),
|
||
gateway: 网关地址 || 'https://openapi.alipay.com/gateway.do',
|
||
notifyUrl: 回调URL
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 将 Base64 编码的密钥转换为 PEM 格式
|
||
*/
|
||
private convertBase64ToPem(base64Key: string, keyType: 'PRIVATE KEY' | 'PUBLIC KEY'): string {
|
||
// 如果已经是 PEM 格式(包含 BEGIN),直接返回
|
||
if (base64Key.includes('-----BEGIN')) {
|
||
return base64Key;
|
||
}
|
||
|
||
// 移除所有空白字符
|
||
const cleanKey = base64Key.replace(/\s/g, '');
|
||
|
||
// 每 64 个字符插入一个换行符
|
||
const formattedKey = cleanKey.match(/.{1,64}/g)?.join('\n') || cleanKey;
|
||
|
||
return `-----BEGIN ${keyType}-----\n${formattedKey}\n-----END ${keyType}-----`;
|
||
}
|
||
|
||
/**
|
||
* Generate Alipay Page Pay URL
|
||
*/
|
||
async generatePagePayUrl(order: { outTradeNo: string; totalAmount: string; subject: string; body?: string; returnUrl?: string }) {
|
||
await this.init();
|
||
if (!this.config) throw new Error('Alipay not initialized');
|
||
|
||
const params: Record<string, string> = {
|
||
app_id: this.config.appId,
|
||
method: 'alipay.trade.page.pay',
|
||
format: 'JSON',
|
||
charset: 'utf-8',
|
||
sign_type: 'RSA2',
|
||
timestamp: new Date().toISOString().replace('T', ' ').split('.')[0],
|
||
version: '1.0',
|
||
notify_url: `${this.config.notifyUrl}/api/payment/notify`,
|
||
return_url: order.returnUrl || `${this.config.notifyUrl}/api/payment/return`, // 优先使用传入的跳转地址
|
||
biz_content: JSON.stringify({
|
||
out_trade_no: order.outTradeNo,
|
||
product_code: 'FAST_INSTANT_TRADE_PAY',
|
||
total_amount: order.totalAmount,
|
||
subject: order.subject,
|
||
body: order.body,
|
||
}),
|
||
};
|
||
|
||
params.sign = this.sign(params);
|
||
|
||
const query = Object.keys(params)
|
||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||
.join('&');
|
||
|
||
return `${this.config.gateway}?${query}`;
|
||
}
|
||
|
||
/**
|
||
* Verify Alipay Signature
|
||
*/
|
||
async verifySignature(params: Record<string, string>): Promise<boolean> {
|
||
await this.init();
|
||
if (!this.config) throw new Error('Alipay not initialized');
|
||
|
||
const { sign, sign_type, ...rest } = params;
|
||
if (!sign) return false;
|
||
|
||
// Sort and stringify params
|
||
const content = Object.keys(rest)
|
||
.sort()
|
||
.map((key) => {
|
||
const value = rest[key];
|
||
return value !== '' && value !== undefined && value !== null ? `${key}=${value}` : null;
|
||
})
|
||
.filter(Boolean)
|
||
.join('&');
|
||
|
||
const verify = crypto.createVerify('RSA-SHA256');
|
||
verify.update(content);
|
||
|
||
return verify.verify(this.config.alipayPublicKey, sign, 'base64');
|
||
}
|
||
|
||
/**
|
||
* Internal Sign Method
|
||
*/
|
||
private sign(params: Record<string, string>): string {
|
||
if (!this.config) throw new Error('Alipay not initialized');
|
||
|
||
const content = Object.keys(params)
|
||
.sort()
|
||
.map((key) => {
|
||
const value = params[key];
|
||
return value !== '' && value !== undefined && value !== null ? `${key}=${value}` : null;
|
||
})
|
||
.filter(Boolean)
|
||
.join('&');
|
||
|
||
const sign = crypto.createSign('RSA-SHA256');
|
||
sign.update(content);
|
||
return sign.sign(this.config.privateKey, 'base64');
|
||
}
|
||
}
|
||
|
||
export const alipayService = new AlipayService();
|