diff --git a/src/lib/alipay/client.ts b/src/lib/alipay/client.ts index 96b62f8..74e1128 100644 --- a/src/lib/alipay/client.ts +++ b/src/lib/alipay/client.ts @@ -10,7 +10,7 @@ function getCommonParams(appId: string): Record { format: 'JSON', charset: 'utf-8', sign_type: 'RSA2', - timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19), + timestamp: new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }).replace('T', ' '), version: '1.0', }; } diff --git a/src/lib/alipay/provider.ts b/src/lib/alipay/provider.ts index f43d23e..00263fc 100644 --- a/src/lib/alipay/provider.ts +++ b/src/lib/alipay/provider.ts @@ -75,7 +75,7 @@ export class AlipayProvider implements PaymentProvider { return { tradeNo: result.trade_no || tradeNo, status, - amount: parseFloat(result.total_amount || '0'), + amount: Math.round(parseFloat(result.total_amount || '0') * 100) / 100, paidAt: result.send_pay_date ? new Date(result.send_pay_date) : undefined, }; } @@ -90,15 +90,25 @@ export class AlipayProvider implements PaymentProvider { params[key] = value; } + // sign_type 过滤:仅接受 RSA2 + if (params.sign_type && params.sign_type !== 'RSA2') { + throw new Error('Unsupported sign_type, only RSA2 is accepted'); + } + const sign = params.sign || ''; if (!env.ALIPAY_PUBLIC_KEY || !verifySign(params, env.ALIPAY_PUBLIC_KEY, sign)) { throw new Error('Alipay notification signature verification failed'); } + // app_id 校验 + if (params.app_id !== env.ALIPAY_APP_ID) { + throw new Error('Alipay notification app_id mismatch'); + } + return { tradeNo: params.trade_no || '', orderId: params.out_trade_no || '', - amount: parseFloat(params.total_amount || '0'), + amount: Math.round(parseFloat(params.total_amount || '0') * 100) / 100, status: params.trade_status === 'TRADE_SUCCESS' || params.trade_status === 'TRADE_FINISHED' ? 'success' : 'failed', rawData: params, @@ -110,6 +120,7 @@ export class AlipayProvider implements PaymentProvider { out_trade_no: request.orderId, refund_amount: request.amount.toFixed(2), refund_reason: request.reason || '', + out_request_no: request.orderId + '-refund', }); return { diff --git a/src/lib/payment/index.ts b/src/lib/payment/index.ts index 995950c..ab953b4 100644 --- a/src/lib/payment/index.ts +++ b/src/lib/payment/index.ts @@ -49,11 +49,12 @@ export function initPaymentProviders(): void { !env.WXPAY_PRIVATE_KEY || !env.WXPAY_API_V3_KEY || !env.WXPAY_PUBLIC_KEY || + !env.WXPAY_PUBLIC_KEY_ID || !env.WXPAY_CERT_SERIAL || !env.WXPAY_NOTIFY_URL ) { throw new Error( - 'PAYMENT_PROVIDERS includes wxpay but required env vars are missing: WXPAY_APP_ID, WXPAY_MCH_ID, WXPAY_PRIVATE_KEY, WXPAY_API_V3_KEY, WXPAY_PUBLIC_KEY, WXPAY_CERT_SERIAL, WXPAY_NOTIFY_URL', + 'PAYMENT_PROVIDERS includes wxpay but required env vars are missing: WXPAY_APP_ID, WXPAY_MCH_ID, WXPAY_PRIVATE_KEY, WXPAY_API_V3_KEY, WXPAY_PUBLIC_KEY, WXPAY_PUBLIC_KEY_ID, WXPAY_CERT_SERIAL, WXPAY_NOTIFY_URL', ); } paymentRegistry.register(new WxpayProvider()); diff --git a/src/lib/wxpay/client.ts b/src/lib/wxpay/client.ts index f3d96a6..7c4db5d 100644 --- a/src/lib/wxpay/client.ts +++ b/src/lib/wxpay/client.ts @@ -26,7 +26,10 @@ function getPayInstance(): WxPay { const env = assertWxpayEnv(getEnv()); const privateKey = Buffer.from(env.WXPAY_PRIVATE_KEY); - const publicKey = env.WXPAY_PUBLIC_KEY ? Buffer.from(env.WXPAY_PUBLIC_KEY) : Buffer.alloc(0); + if (!env.WXPAY_PUBLIC_KEY) { + throw new Error('WXPAY_PUBLIC_KEY is required'); + } + const publicKey = Buffer.from(env.WXPAY_PUBLIC_KEY); payInstance = new WxPay({ appid: env.WXPAY_APP_ID, @@ -45,7 +48,7 @@ function yuanToFen(yuan: number): number { async function request(method: string, url: string, body?: Record): Promise { const pay = getPayInstance(); - const nonce_str = Math.random().toString(36).substring(2, 15); + const nonce_str = crypto.randomBytes(16).toString('hex'); const timestamp = Math.floor(Date.now() / 1000).toString(); const signature = pay.getSignature(method, nonce_str, timestamp, url, body ? JSON.stringify(body) : ''); @@ -134,8 +137,17 @@ export async function createRefund(params: WxpayRefundParams): Promise(ciphertext: string, associatedData: string, nonce: string): T { - const pay = getPayInstance(); - return pay.decipher_gcm(ciphertext, associatedData, nonce); + const env = assertWxpayEnv(getEnv()); + const key = env.WXPAY_API_V3_KEY; + const ciphertextBuf = Buffer.from(ciphertext, 'base64'); + // AES-GCM 最后 16 字节是 AuthTag + const authTag = ciphertextBuf.subarray(ciphertextBuf.length - 16); + const data = ciphertextBuf.subarray(0, ciphertextBuf.length - 16); + const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce); + decipher.setAuthTag(authTag); + decipher.setAAD(Buffer.from(associatedData)); + const decoded = Buffer.concat([decipher.update(data), decipher.final()]); + return JSON.parse(decoded.toString('utf-8')) as T; } export async function verifyNotifySign(params: { diff --git a/src/lib/wxpay/provider.ts b/src/lib/wxpay/provider.ts index d67d88e..97d2e60 100644 --- a/src/lib/wxpay/provider.ts +++ b/src/lib/wxpay/provider.ts @@ -112,20 +112,20 @@ export class WxpayProvider implements PaymentProvider { } // 验证 serial 匹配我们配置的公钥 ID - if (env.WXPAY_PUBLIC_KEY_ID && serial !== env.WXPAY_PUBLIC_KEY_ID) { + if (serial !== env.WXPAY_PUBLIC_KEY_ID) { throw new Error(`Wxpay serial mismatch: expected ${env.WXPAY_PUBLIC_KEY_ID}, got ${serial}`); } - const valid = await verifyNotifySign({ timestamp, nonce, body, serial, signature }); - if (!valid) { - throw new Error('Wxpay notification signature verification failed'); - } - const now = Math.floor(Date.now() / 1000); if (Math.abs(now - Number(timestamp)) > 300) { throw new Error('Wechatpay notification timestamp expired'); } + const valid = await verifyNotifySign({ timestamp, nonce, body, serial, signature }); + if (!valid) { + throw new Error('Wxpay notification signature verification failed'); + } + const payload: WxpayNotifyPayload = JSON.parse(body); if (payload.event_type !== 'TRANSACTION.SUCCESS') {