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'); } } }