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
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { SettlementService } from '../modules/settlement/settlement.service';
import { AlertService } from '../common/alert/alert.service';
/**
* 财务对账单定时任务(Asia/Shanghai
@@ -11,7 +12,10 @@ import { SettlementService } from '../modules/settlement/settlement.service';
export class SettlementScheduler {
private readonly logger = new Logger(SettlementScheduler.name);
constructor(private readonly settlementService: SettlementService) {}
constructor(
private readonly settlementService: SettlementService,
private readonly alert: AlertService,
) {}
@Cron('0 8 * * *', { timeZone: 'Asia/Shanghai' })
async handleDailyBills() {
@@ -21,12 +25,26 @@ export class SettlementScheduler {
this.logger.log(`Winery bill: ${JSON.stringify(winery)}`);
} catch (e) {
this.logger.error('Winery bill job failed', e instanceof Error ? e.stack : e);
this.alert.notify({
level: 'P0',
category: 'job',
title: '酒厂日账单任务失败',
detail: e instanceof Error ? e.message : String(e),
dedupeKey: `job_winery_bill|${new Date().toISOString().slice(0, 10)}`,
});
}
try {
const store = await this.settlementService.generateStoreBillsForDay();
this.logger.log(`Store bills: ${JSON.stringify(store)}`);
} catch (e) {
this.logger.error('Store bill job failed', e instanceof Error ? e.stack : e);
this.alert.notify({
level: 'P0',
category: 'job',
title: '门店日账单任务失败',
detail: e instanceof Error ? e.message : String(e),
dedupeKey: `job_store_bill|${new Date().toISOString().slice(0, 10)}`,
});
}
}
@@ -38,16 +56,48 @@ export class SettlementScheduler {
this.logger.log(
`Partner bills: total=${result.total} success=${result.success} failed=${result.failed}`,
);
if (result.failed > 0) {
this.alert.notify({
level: 'P0',
category: 'job',
title: '合伙人月账单部分失败',
detail: `total=${result.total} success=${result.success} failed=${result.failed}`,
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
});
}
} catch (e) {
this.logger.error('Partner bill job failed', e instanceof Error ? e.stack : e);
this.alert.notify({
level: 'P0',
category: 'job',
title: '合伙人月账单任务失败',
detail: e instanceof Error ? e.message : String(e),
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 7)}`,
});
}
try {
const logistics = await this.settlementService.generatePreviousMonthLogisticsBills();
this.logger.log(
`Logistics bills: total=${logistics.total} success=${logistics.success} failed=${logistics.failed}`,
);
if (logistics.failed > 0) {
this.alert.notify({
level: 'P0',
category: 'job',
title: '物流月对账部分失败',
detail: `total=${logistics.total} success=${logistics.success} failed=${logistics.failed}`,
dedupeKey: `job_logistics_bill|${new Date().toISOString().slice(0, 7)}`,
});
}
} catch (e) {
this.logger.error('Logistics bill job failed', e instanceof Error ? e.stack : e);
this.alert.notify({
level: 'P0',
category: 'job',
title: '物流月对账任务失败',
detail: e instanceof Error ? e.message : String(e),
dedupeKey: `job_logistics_bill|${new Date().toISOString().slice(0, 7)}`,
});
}
}
}