feat(trade): WeChat trade-manage push + delay shipping upload
Receive trade_manage_order_settlement to sync confirm-receive/settle; wait ~65s and retry on 10060001 before upload_shipping_info. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
decryptWechatEncrypt,
|
||||
normalizeTradeManageEvent,
|
||||
parseWechatPushBody,
|
||||
verifyWechatMsgSignature,
|
||||
verifyWechatUrlSignature,
|
||||
} from '../integrations/wechat/wechat-msg-crypto.util';
|
||||
import { WechatTradeManageService } from '../integrations/wechat/wechat-trade-manage.service';
|
||||
|
||||
/**
|
||||
* 小程序消息推送(发货信息管理事件)。
|
||||
* 后台配置 URL:https://{host}/api/v1/callbacks/wechat/message
|
||||
* 需配置 WX_MINI_MSG_TOKEN;安全模式另需 WX_MINI_MSG_AES_KEY。
|
||||
*/
|
||||
@Controller('callbacks/wechat')
|
||||
export class WechatMessageCallbackController {
|
||||
private readonly logger = new Logger(WechatMessageCallbackController.name);
|
||||
|
||||
constructor(private readonly tradeManage: WechatTradeManageService) {}
|
||||
|
||||
@Get('message')
|
||||
verify(
|
||||
@Query('signature') signature: string,
|
||||
@Query('timestamp') timestamp: string,
|
||||
@Query('nonce') nonce: string,
|
||||
@Query('echostr') echostr: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const token = (process.env.WX_MINI_MSG_TOKEN || '').trim();
|
||||
if (!token) {
|
||||
this.logger.error('WX_MINI_MSG_TOKEN 未配置,无法完成消息推送 URL 验证');
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).send('msg token not configured');
|
||||
}
|
||||
if (!verifyWechatUrlSignature(token, timestamp, nonce, signature)) {
|
||||
this.logger.warn('wechat message URL verify failed');
|
||||
return res.status(HttpStatus.FORBIDDEN).send('invalid signature');
|
||||
}
|
||||
return res.status(HttpStatus.OK).send(echostr ?? '');
|
||||
}
|
||||
|
||||
@Post('message')
|
||||
async receive(
|
||||
@Req() req: Request & { rawBody?: Buffer },
|
||||
@Query('signature') signature: string,
|
||||
@Query('timestamp') timestamp: string,
|
||||
@Query('nonce') nonce: string,
|
||||
@Query('msg_signature') msgSignature: string,
|
||||
@Query('encrypt_type') encryptType: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const token = (process.env.WX_MINI_MSG_TOKEN || '').trim();
|
||||
if (!token) {
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).send('msg token not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const raw =
|
||||
req.rawBody?.toString('utf8') ??
|
||||
(typeof req.body === 'string'
|
||||
? req.body
|
||||
: Buffer.isBuffer(req.body)
|
||||
? req.body.toString('utf8')
|
||||
: JSON.stringify(req.body ?? {}));
|
||||
|
||||
let body = parseWechatPushBody(raw);
|
||||
const encrypt = typeof body.Encrypt === 'string' ? body.Encrypt : undefined;
|
||||
const secure = encryptType === 'aes' || !!encrypt;
|
||||
|
||||
if (secure) {
|
||||
if (!encrypt) {
|
||||
return res.status(HttpStatus.BAD_REQUEST).send('missing Encrypt');
|
||||
}
|
||||
const aesKey = (process.env.WX_MINI_MSG_AES_KEY || '').trim();
|
||||
if (!aesKey) {
|
||||
this.logger.error('安全模式推送但未配置 WX_MINI_MSG_AES_KEY');
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).send('aes key not configured');
|
||||
}
|
||||
if (
|
||||
!verifyWechatMsgSignature(token, timestamp, nonce, encrypt, msgSignature || String(body.MsgSignature || ''))
|
||||
) {
|
||||
return res.status(HttpStatus.FORBIDDEN).send('invalid msg_signature');
|
||||
}
|
||||
const appId = (process.env.WX_MINI_APP_ID || process.env.WX_APP_ID || '').trim();
|
||||
const plain = decryptWechatEncrypt(encrypt, aesKey, appId || undefined);
|
||||
body = parseWechatPushBody(plain);
|
||||
} else if (!verifyWechatUrlSignature(token, timestamp, nonce, signature)) {
|
||||
return res.status(HttpStatus.FORBIDDEN).send('invalid signature');
|
||||
}
|
||||
|
||||
// 云托管探活
|
||||
if (String(body.action || '') === 'CheckContainerPath') {
|
||||
return res.status(HttpStatus.OK).send('success');
|
||||
}
|
||||
|
||||
const evt = normalizeTradeManageEvent(body);
|
||||
if (evt.event) {
|
||||
await this.tradeManage.handleEvent(evt);
|
||||
}
|
||||
return res.status(HttpStatus.OK).send('success');
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this.logger.error(`wechat message push failed: ${message}`);
|
||||
// 仍回 success,避免微信疯狂重试;错误已记日志
|
||||
return res.status(HttpStatus.OK).send('success');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user