订单配置

This commit is contained in:
tianmo committed 2026-09-30 11:02:58 +08:00
1 parent e81481623c
commit c7f43e30a9
11 files changed
+492 -19

No files matched your search

@@ -1,6 +1,8 @@
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Controller, Get, Logger, Post, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { Public } from '../../common/auth';
import { decryptWechatMessage, sameHex, wechatSignature, xmlField } from '../../common/wechat-msg';
import { InjectConfig, type AppConfig } from '../../config/config';
import { PaymentsService } from './payments.service';
/** 取原始请求体。虚拟支付回调是 XML,必须拿到未经解析的字节。 */
@@ -16,19 +18,74 @@ function rawBodyOf(req: Request): string {
*/
@Controller('v1/payments')
export class PaymentsController {
constructor(private readonly payments: PaymentsService) {}
private readonly logger = new Logger(PaymentsController.name);
constructor(
private readonly payments: PaymentsService,
@InjectConfig() private readonly cfg: AppConfig,
) {}
/**
* 微信配置「消息推送 URL」时的验签请求。
* 后台点保存会发 GET ?signature&timestamp&nonce&echostr(安全模式换成 msg_signature + 加密 echostr),
* 验签通过必须原样返回 echostr,否则一直提示「服务器地址验证失败」。
*/
@Public()
@Get('notify')
verify(@Query() query: Record<string, string>, @Res() res: Response): void {
const token = this.cfg.WX_MSG_TOKEN;
const aesKey = this.cfg.WX_MSG_AES_KEY;
const timestamp = query.timestamp ?? '';
const nonce = query.nonce ?? '';
const echostr = query.echostr ?? '';
if (token && query.msg_signature && aesKey) {
if (sameHex(wechatSignature(token, timestamp, nonce, echostr), query.msg_signature)) {
res.type('text/plain').send(decryptWechatMessage(aesKey, echostr));
return;
}
} else if (token && sameHex(wechatSignature(token, timestamp, nonce), query.signature ?? '')) {
res.type('text/plain').send(echostr);
return;
}
this.logger.warn('消息推送地址校验失败:签名不匹配,或 .env 未配置 WX_MSG_TOKEN');
res.status(401).send('');
}
@Public()
@Post('notify')
async notify(@Req() req: Request, @Res() res: Response): Promise<void> {
const xml = await this.payments.handleDeliverNotify(rawBodyOf(req));
res.type('application/xml').send(xml);
const body = this.decode(req);
if (body.includes('xpay_goods_deliver_notify')) {
res.type('application/xml').send(await this.payments.handleDeliverNotify(body));
return;
}
// 退款推送走的是同一个消息推送地址,漏了这段退款就永远不会被记上
if (body.includes('xpay_refund_notify')) {
res.type('application/xml').send(await this.payments.handleRefundNotify(body));
return;
}
// 地址会收到全部事件(投诉、风控、代币等),其余统一回 success 表示已收到
res.type('text/plain').send('success');
}
@Public()
@Post('refund-notify')
async refundNotify(@Req() req: Request, @Res() res: Response): Promise<void> {
const xml = await this.payments.handleRefundNotify(rawBodyOf(req));
res.type('application/xml').send(xml);
res.type('application/xml').send(await this.payments.handleRefundNotify(this.decode(req)));
}
/** 取原始报文;安全模式下包体是 <Encrypt> 密文,先解出来。 */
private decode(req: Request): string {
const raw = rawBodyOf(req);
const aesKey = this.cfg.WX_MSG_AES_KEY;
if (!aesKey || !raw.includes('<Encrypt>')) return raw;
try {
return decryptWechatMessage(aesKey, xmlField(raw, 'Encrypt'));
} catch (err) {
this.logger.error(`消息推送解密失败,请核对 WX_MSG_AES_KEY:${err instanceof Error ? err.message : err}`);
return raw;
}
}
}