feat: multi-module iteration

This commit is contained in:
2026-08-04 21:38:49 +08:00
parent 9d96c73246
commit 71f508e02b
1366 changed files with 202004 additions and 0 deletions
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { IntegrationsModule } from '../integrations/integrations.module';
import { TradeModule } from '../modules/trade/trade.module';
import { PrismaModule } from '../common/prisma/prisma.module';
import { WechatPayCallbackController } from './wechat-pay.controller';
import { WechatRefundCallbackController } from './wechat-refund.controller';
import { WechatMessageCallbackController } from './wechat-message.controller';
import { DeliveryCallbackController } from './delivery-track.controller';
@Module({
imports: [IntegrationsModule, TradeModule, PrismaModule],
controllers: [
WechatPayCallbackController,
WechatRefundCallbackController,
WechatMessageCallbackController,
DeliveryCallbackController,
],
})
export class CallbacksModule {}
@@ -0,0 +1,71 @@
import { Body, Controller, Post } from '@nestjs/common';
import { TradeService } from '../modules/trade/trade.service';
import { CourierService } from '../integrations/courier/courier.service';
import { PrismaService } from '../common/prisma/prisma.module';
import { logCourierCall } from '../integrations/courier/courier-log.util';
@Controller('callbacks/delivery')
export class DeliveryCallbackController {
constructor(
private readonly tradeService: TradeService,
private readonly courier: CourierService,
private readonly prisma: PrismaService,
) {}
@Post('track')
async track(@Body() body: { orderNo?: string; orderId?: string; status?: string }) {
const baseLog = {
scene: 'TRACK_CALLBACK',
requestUrl: '/api/v1/callbacks/delivery/track',
requestBody: body as Record<string, unknown>,
};
if (!body.orderId && !body.orderNo) {
await logCourierCall(this.prisma, {
...baseLog,
status: 'FAILED',
errorMessage: '缺少 orderId / orderNo',
});
return this.courier.buildTrackCallbackResponse(false);
}
const order = body.orderId
? await this.prisma.order.findUnique({ where: { id: BigInt(body.orderId) } })
: await this.prisma.order.findUnique({ where: { orderNo: body.orderNo! } });
if (!order) {
await logCourierCall(this.prisma, {
...baseLog,
status: 'FAILED',
errorMessage: '订单不存在',
externalNo: body.orderNo,
});
return this.courier.buildTrackCallbackResponse(false);
}
const statusMap: Record<string, string> = {
SHIPPED: 'SHIPPING',
OUT_WAREHOUSE: 'OUT_WAREHOUSE',
DELIVERED: 'COMPLETED',
COMPLETED: 'COMPLETED',
};
const target = statusMap[body.status ?? ''] ?? body.status;
let applied = false;
if (target && target !== order.status) {
await this.tradeService.applyStatusTransition(order.id, order.status, target, 'DELIVERY_CALLBACK');
applied = true;
}
const response = this.courier.buildTrackCallbackResponse(true);
await logCourierCall(this.prisma, {
...baseLog,
responseBody: response,
status: 'SUCCESS',
externalNo: order.orderNo,
ref: { refType: 'ORDER', refId: order.id },
errorMessage: applied ? undefined : '状态未变更',
});
return response;
}
}
@@ -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';
/**
* 小程序消息推送(发货信息管理事件)。
* 后台配置 URLhttps://{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');
}
}
}
@@ -0,0 +1,53 @@
import { Controller, Headers, Inject, Post, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { TradeService } from '../modules/trade/trade.service';
import { WECHAT_PROVIDER } from '../integrations/integrations.constants';
import type { IWechatProvider } from '../integrations/wechat/wechat.interface';
import { PayRedeemAnomalyService } from '../common/alert/pay-redeem-anomaly.service';
import { AlertService } from '../common/alert/alert.service';
type RawBodyRequest = Request & { body: Buffer };
@Controller('callbacks/wechat')
export class WechatPayCallbackController {
constructor(
private readonly tradeService: TradeService,
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
private readonly payRedeemAnomaly: PayRedeemAnomalyService,
private readonly alert: AlertService,
) {}
@Post('pay')
async payNotify(
@Req() req: RawBodyRequest,
@Headers() headers: Record<string, string | string[] | undefined>,
@Res() res: Response,
) {
let orderNo: string | undefined;
try {
const rawBody =
(req as Request & { rawBody?: Buffer }).rawBody?.toString('utf8') ??
(typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {}));
const notify = await this.wechat.parsePayNotification(headers, rawBody);
orderNo = notify.outTradeNo;
await this.tradeService.handlePaySuccess({
orderNo: notify.outTradeNo,
transactionId: notify.transactionId,
amountFen: notify.amountFen,
});
return res.status(200).json({ code: 'SUCCESS', message: '成功' });
} catch (err) {
const message = err instanceof Error ? err.message : '处理失败';
this.payRedeemAnomaly.onPayFail(message, { orderNo });
this.alert.notify({
level: 'P0',
category: 'pay',
title: '支付回调处理失败',
detail: `${orderNo ? `订单 ${orderNo}\n` : ''}${message}`,
dedupeKey: `pay_callback_fail|${orderNo ?? message.slice(0, 40)}`,
dedupeTtlSec: 120,
});
return res.status(500).json({ code: 'FAIL', message });
}
}
}
@@ -0,0 +1,52 @@
import { Controller, Headers, Inject, Post, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { TradeService } from '../modules/trade/trade.service';
import { WECHAT_PROVIDER } from '../integrations/integrations.constants';
import type { IWechatProvider } from '../integrations/wechat/wechat.interface';
import { AlertService } from '../common/alert/alert.service';
type RawBodyRequest = Request & { body: Buffer };
@Controller('callbacks/wechat')
export class WechatRefundCallbackController {
constructor(
private readonly tradeService: TradeService,
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
private readonly alert: AlertService,
) {}
@Post('refund')
async refundNotify(
@Req() req: RawBodyRequest,
@Headers() headers: Record<string, string | string[] | undefined>,
@Res() res: Response,
) {
let outRefundNo: string | undefined;
try {
const rawBody =
(req as Request & { rawBody?: Buffer }).rawBody?.toString('utf8') ??
(typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {}));
const notify = await this.wechat.parseRefundNotification(headers, rawBody);
outRefundNo = notify.outRefundNo;
await this.tradeService.handleRefundSuccess({
outRefundNo: notify.outRefundNo,
refundId: notify.refundId,
amountFen: notify.amountFen,
remark: '微信退款回调确认',
actorType: 'WECHAT_REFUND',
});
return res.status(200).json({ code: 'SUCCESS', message: '成功' });
} catch (err) {
const message = err instanceof Error ? err.message : '处理失败';
this.alert.notify({
level: 'P0',
category: 'pay',
title: '退款回调处理失败',
detail: `${outRefundNo ? `退款单 ${outRefundNo}\n` : ''}${message}`,
dedupeKey: `refund_callback_fail|${outRefundNo ?? message.slice(0, 40)}`,
dedupeTtlSec: 120,
});
return res.status(500).json({ code: 'FAIL', message });
}
}
}