feat(ops): WeCom webhook alerts for pay/redeem anomalies

Add outbound group-bot alerts, rate/amount rules, stuck-order cron, HQ test button, and health db/redis checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-02 16:01:30 +08:00
parent cb6ecaaba6
commit 3328c52cb4
27 changed files with 1036 additions and 103 deletions
@@ -32,6 +32,8 @@ import { buildOrderStatusEvent, orderStatusLogWhere } from '../../common/event/e
import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat';
import { FulfillmentService } from '../fulfillment/fulfillment.service';
import { WechatOrderShippingService } from '../../integrations/wechat/wechat-order-shipping.service';
import { AlertService } from '../../common/alert/alert.service';
import { PayRedeemAnomalyService } from '../../common/alert/pay-redeem-anomaly.service';
import type { Request } from 'express';
@Injectable()
@@ -51,6 +53,8 @@ export class TradeService {
@Inject(forwardRef(() => FulfillmentService))
private readonly fulfillmentService: FulfillmentService,
private readonly wechatOrderShipping: WechatOrderShippingService,
private readonly payRedeemAnomaly: PayRedeemAnomalyService,
private readonly alert: AlertService,
) {}
async preview(
@@ -265,14 +269,33 @@ export class TradeService {
throw new BadRequestException('订单状态不可支付');
}
const payAmountYuan = Number(order.payAmount);
this.payRedeemAnomaly.onPayAttempt(payAmountYuan, {
orderNo: order.orderNo,
userId,
});
const user = await this.prisma.user.findUnique({ where: { id: userId } });
const openId = user?.wxOpenId ?? undefined;
const appConfig = loadAppConfig();
if (!appConfig.mockPay && !openId) {
this.payRedeemAnomaly.onPayFail(WECHAT_AUTH_REQUIRED, {
orderNo: order.orderNo,
userId,
});
throw new BadRequestException(WECHAT_AUTH_REQUIRED);
}
const payPlatform = clientApp === ClientApp.USER_MINI ? 'mini' : 'h5';
const payResult = await this.payProvider.payOrder(orderId, openId, payPlatform);
let payResult;
try {
payResult = await this.payProvider.payOrder(orderId, openId, payPlatform);
} catch (e) {
this.payRedeemAnomaly.onPayFail(e instanceof Error ? e.message : '拉起支付失败', {
orderNo: order.orderNo,
userId,
});
throw e;
}
if (payResult.mode === 'jsapi') {
return {
@@ -330,6 +353,11 @@ export class TradeService {
await this.afterOrderPaid(order.id);
this.payRedeemAnomaly.onPaySuccess(payAmountYuan, {
orderNo: order.orderNo,
userId,
});
this.analyticsService.trackOneSafe(userId, 'USER_H5', {
eventName: 'pay_success',
refType: 'ORDER',
@@ -380,8 +408,18 @@ export class TradeService {
return { orderId: order.id.toString(), alreadyPaid: true };
}
const expectedFen = Math.round(Number(order.payAmount) * 100);
const payAmountYuan = Number(order.payAmount);
const expectedFen = Math.round(payAmountYuan * 100);
if (params.amountFen > 0 && params.amountFen !== expectedFen) {
const reason = `支付金额与订单不符 expected=${expectedFen} got=${params.amountFen}`;
this.payRedeemAnomaly.onPayFail(reason, { orderNo: order.orderNo, userId: order.userId });
this.alert.notify({
level: 'P0',
category: 'pay',
title: '支付金额不一致',
detail: `订单 ${order.orderNo}\n期望 ${expectedFen} 分,回调 ${params.amountFen}`,
dedupeKey: `pay_amount_mismatch|${order.orderNo}`,
});
throw new BadRequestException('支付金额与订单不符');
}
@@ -449,6 +487,10 @@ export class TradeService {
const refreshed = await this.prisma.order.findUnique({ where: { id: order.id } });
if (refreshed?.payStatus === 'PAID') {
await this.afterOrderPaid(order.id);
this.payRedeemAnomaly.onPaySuccess(payAmountYuan, {
orderNo: order.orderNo,
userId: order.userId,
});
this.analyticsService.trackOneSafe(order.userId, 'USER_H5', {
eventName: 'pay_success',
refType: 'ORDER',
@@ -1653,16 +1695,34 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
this.payRedeemAnomaly.onPayAttempt(Number(order.payAmount), {
orderNo: order.orderNo,
userId: order.userId,
});
let openId: string | undefined;
if (payMethod === 'JSAPI') {
openId = primary.wxOpenId ?? undefined;
const appConfig = loadAppConfig();
if (!appConfig.mockPay && !openId) {
this.payRedeemAnomaly.onPayFail('代付未绑定微信', {
orderNo: order.orderNo,
userId: order.userId,
});
throw new BadRequestException('请先在微信内登录并绑定微信后再代付');
}
}
const payResult = await this.payProvider.payOrder(orderId, openId, 'h5', payMethod);
let payResult;
try {
payResult = await this.payProvider.payOrder(orderId, openId, 'h5', payMethod);
} catch (e) {
this.payRedeemAnomaly.onPayFail(e instanceof Error ? e.message : '代付拉起失败', {
orderNo: order.orderNo,
userId: order.userId,
});
throw e;
}
if (payResult.mode === 'native') {
return {
@@ -1709,8 +1769,26 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
const payResult = await this.payProvider.payOrder(orderId, undefined, 'h5', 'NATIVE');
this.payRedeemAnomaly.onPayAttempt(Number(order.payAmount), {
orderNo: order.orderNo,
userId: order.userId,
});
let payResult;
try {
payResult = await this.payProvider.payOrder(orderId, undefined, 'h5', 'NATIVE');
} catch (e) {
this.payRedeemAnomaly.onPayFail(e instanceof Error ? e.message : '总部代付拉起失败', {
orderNo: order.orderNo,
userId: order.userId,
});
throw e;
}
if (payResult.mode !== 'native') {
this.payRedeemAnomaly.onPayFail('无法生成收款码', {
orderNo: order.orderNo,
userId: order.userId,
});
throw new BadRequestException('无法生成收款码');
}
return {