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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user