feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import { TradeService } from '../modules/trade/trade.service';
|
||||
import { DELIVERY_QUEUE } from './jobs.constants';
|
||||
|
||||
@Processor(DELIVERY_QUEUE)
|
||||
export class DeliveryProcessor extends WorkerHost {
|
||||
constructor(private readonly tradeService: TradeService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<{ orderId: string; targetStatus: string }>) {
|
||||
await this.tradeService.applyStatusTransition(
|
||||
BigInt(job.data.orderId),
|
||||
'',
|
||||
job.data.targetStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const DELIVERY_QUEUE = 'delivery-mock';
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { TradeModule } from '../modules/trade/trade.module';
|
||||
import { SettlementModule } from '../modules/settlement/settlement.module';
|
||||
import { DeliveryProcessor } from './delivery.processor';
|
||||
import { SettlementScheduler } from './settlement.scheduler';
|
||||
import { MonitorScheduler } from './monitor.scheduler';
|
||||
import { DELIVERY_QUEUE } from './jobs.constants';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
BullModule.registerQueue({ name: DELIVERY_QUEUE }),
|
||||
TradeModule,
|
||||
SettlementModule,
|
||||
],
|
||||
providers: [DeliveryProcessor, SettlementScheduler, MonitorScheduler],
|
||||
})
|
||||
export class JobsModule {}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../common/prisma/prisma.module';
|
||||
import { RedisService } from '../common/redis/redis.service';
|
||||
import { AlertService } from '../common/alert/alert.service';
|
||||
import { ALERT_STUCK_ORDER } from '../common/alert/alert.constants';
|
||||
|
||||
@Injectable()
|
||||
export class MonitorScheduler {
|
||||
private readonly logger = new Logger(MonitorScheduler.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly alert: AlertService,
|
||||
) {}
|
||||
|
||||
/** 每分钟:MySQL / Redis 探活 */
|
||||
@Cron('*/1 * * * *', { timeZone: 'Asia/Shanghai' })
|
||||
async checkInfra() {
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e) {
|
||||
this.logger.error('MySQL ping failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'infra',
|
||||
title: 'MySQL 不可用',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: 'infra_mysql_down',
|
||||
dedupeTtlSec: 120,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const pong = await this.redis.client.ping();
|
||||
if (pong !== 'PONG') {
|
||||
throw new Error(`unexpected ping reply: ${pong}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('Redis ping failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'infra',
|
||||
title: 'Redis 不可用',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: 'infra_redis_down',
|
||||
dedupeTtlSec: 120,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 每 5 分钟:卡住订单扫描 */
|
||||
@Cron('*/5 * * * *', { timeZone: 'Asia/Shanghai' })
|
||||
async scanStuckOrders() {
|
||||
const now = new Date();
|
||||
const hourBucket = Math.floor(now.getTime() / 3_600_000);
|
||||
const sample = ALERT_STUCK_ORDER.sampleLimit;
|
||||
|
||||
try {
|
||||
const expiredUnpaid = await this.prisma.order.findMany({
|
||||
where: {
|
||||
status: 'PENDING_PAY',
|
||||
payExpireAt: { lt: now },
|
||||
},
|
||||
select: { orderNo: true },
|
||||
take: sample,
|
||||
orderBy: { payExpireAt: 'asc' },
|
||||
});
|
||||
const expiredUnpaidTotal = await this.prisma.order.count({
|
||||
where: { status: 'PENDING_PAY', payExpireAt: { lt: now } },
|
||||
});
|
||||
if (expiredUnpaidTotal > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'order',
|
||||
title: '超时未关待付款订单',
|
||||
detail: `共 ${expiredUnpaidTotal} 单\n样例:${expiredUnpaid.map((o) => o.orderNo).join(', ')}`,
|
||||
dedupeKey: `stuck_pending_pay|${hourBucket}`,
|
||||
});
|
||||
}
|
||||
|
||||
const shipBefore = new Date(
|
||||
now.getTime() - ALERT_STUCK_ORDER.pendingShipHours * 3600_000,
|
||||
);
|
||||
const stuckShip = await this.prisma.order.findMany({
|
||||
where: {
|
||||
status: 'PENDING_SHIP',
|
||||
paidAt: { lt: shipBefore },
|
||||
},
|
||||
select: { orderNo: true },
|
||||
take: sample,
|
||||
orderBy: { paidAt: 'asc' },
|
||||
});
|
||||
const stuckShipTotal = await this.prisma.order.count({
|
||||
where: { status: 'PENDING_SHIP', paidAt: { lt: shipBefore } },
|
||||
});
|
||||
if (stuckShipTotal > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'order',
|
||||
title: `待发货超过 ${ALERT_STUCK_ORDER.pendingShipHours}h`,
|
||||
detail: `共 ${stuckShipTotal} 单\n样例:${stuckShip.map((o) => o.orderNo).join(', ')}`,
|
||||
dedupeKey: `stuck_pending_ship|${hourBucket}`,
|
||||
});
|
||||
}
|
||||
|
||||
const deliveryBefore = new Date(
|
||||
now.getTime() - ALERT_STUCK_ORDER.inDeliveryHours * 3600_000,
|
||||
);
|
||||
const stuckDelivery = await this.prisma.order.findMany({
|
||||
where: {
|
||||
status: { in: ['OUT_WAREHOUSE', 'PENDING_RECEIVE'] },
|
||||
updatedAt: { lt: deliveryBefore },
|
||||
},
|
||||
select: { orderNo: true, status: true },
|
||||
take: sample,
|
||||
orderBy: { updatedAt: 'asc' },
|
||||
});
|
||||
const stuckDeliveryTotal = await this.prisma.order.count({
|
||||
where: {
|
||||
status: { in: ['OUT_WAREHOUSE', 'PENDING_RECEIVE'] },
|
||||
updatedAt: { lt: deliveryBefore },
|
||||
},
|
||||
});
|
||||
if (stuckDeliveryTotal > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'order',
|
||||
title: `配送中超过 ${ALERT_STUCK_ORDER.inDeliveryHours}h`,
|
||||
detail: `共 ${stuckDeliveryTotal} 单\n样例:${stuckDelivery
|
||||
.map((o) => `${o.orderNo}(${o.status})`)
|
||||
.join(', ')}`,
|
||||
dedupeKey: `stuck_in_delivery|${hourBucket}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('stuck order scan failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'job',
|
||||
title: '卡住订单扫描失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: 'stuck_scan_fail',
|
||||
dedupeTtlSec: 300,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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)
|
||||
* - 每日 08:00:酒厂日账单 + 门店日账单(统计昨日 00:00~今日 00:00)
|
||||
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
||||
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
||||
*/
|
||||
@Injectable()
|
||||
export class SettlementScheduler {
|
||||
private readonly logger = new Logger(SettlementScheduler.name);
|
||||
|
||||
constructor(
|
||||
private readonly settlementService: SettlementService,
|
||||
private readonly alert: AlertService,
|
||||
) {}
|
||||
|
||||
@Cron('0 8 * * *', { timeZone: 'Asia/Shanghai' })
|
||||
async handleDailyBills() {
|
||||
this.logger.log('Daily settlement bills job start');
|
||||
try {
|
||||
const winery = await this.settlementService.generateWineryBillForDay();
|
||||
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)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** FIN-003:工作日 18:05 扫描超时未审门店提现(企微提醒在 SettlementService 内发送) */
|
||||
@Cron('5 18 * * 1-5', { timeZone: 'Asia/Shanghai' })
|
||||
async handleWithdrawOverdueAlert() {
|
||||
this.logger.log('Store withdraw overdue scan start');
|
||||
try {
|
||||
const summary = await this.settlementService.scanOverdueStoreWithdrawals();
|
||||
this.logger.log(`Store withdraw overdue: ${JSON.stringify(summary)}`);
|
||||
} catch (e) {
|
||||
this.logger.error('Store withdraw overdue scan failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'job',
|
||||
title: '门店提现超时扫描失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_store_withdraw_overdue_fail|${new Date().toISOString().slice(0, 10)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Cron('0 8 1 * *', { timeZone: 'Asia/Shanghai' })
|
||||
async handleMonthlyPartnerBills() {
|
||||
this.logger.log('Monthly partner bills job start');
|
||||
try {
|
||||
const result = await this.settlementService.generatePreviousMonthPartnerBills();
|
||||
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)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user