137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
||
import { Cron } from '@nestjs/schedule';
|
||
import { addShanghaiDays, shanghaiYmd, startOfShanghaiDay } from '@dukang/domain';
|
||
import { SettlementService } from '../modules/settlement/settlement.service';
|
||
import { AlertService } from '../common/alert/alert.service';
|
||
|
||
/**
|
||
* 财务对账单定时任务(Asia/Shanghai)
|
||
* - 每日 08:00:酒厂 T+3 期账单(每 3 天出一期,纳入上期 3 天已完成已付订单)+ 近 45 日出账周期核对补漏 + 门店日账单
|
||
* - 每周一 08: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 today = startOfShanghaiDay(new Date());
|
||
const reconcile = await this.settlementService.reconcileWineryBills({
|
||
dateFrom: shanghaiYmd(addShanghaiDays(today, -45)),
|
||
dateTo: shanghaiYmd(today),
|
||
});
|
||
this.logger.log(
|
||
`Winery reconcile: days=${reconcile.days} generated=${reconcile.generated} orphans=${reconcile.orphanOrders} cleaned=${reconcile.cleanedNonIssueBills}`,
|
||
);
|
||
} catch (e) {
|
||
this.logger.error('Winery bill job failed', e instanceof Error ? e.stack : e);
|
||
this.alert.notify({
|
||
level: 'P0',
|
||
category: 'settlement',
|
||
title: '酒厂日账单任务失败',
|
||
detail: e instanceof Error ? e.message : String(e),
|
||
dedupeKey: `job_winery_bill|${shanghaiYmd(new Date())}`,
|
||
});
|
||
}
|
||
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: 'settlement',
|
||
title: '门店日账单任务失败',
|
||
detail: e instanceof Error ? e.message : String(e),
|
||
dedupeKey: `job_store_bill|${shanghaiYmd(new Date())}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 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: 'settlement',
|
||
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 handleWeeklyPartnerBills() {
|
||
this.logger.log('Weekly partner bills job start');
|
||
try {
|
||
const result = await this.settlementService.generatePreviousWeekPartnerBills();
|
||
this.logger.log(
|
||
`Partner bills: total=${result.total} success=${result.success} failed=${result.failed}`,
|
||
);
|
||
if (result.failed > 0) {
|
||
this.alert.notify({
|
||
level: 'P0',
|
||
category: 'settlement',
|
||
title: '合伙人周账单部分失败',
|
||
detail: `total=${result.total} success=${result.success} failed=${result.failed}`,
|
||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 10)}`,
|
||
});
|
||
}
|
||
} catch (e) {
|
||
this.logger.error('Partner bill job failed', e instanceof Error ? e.stack : e);
|
||
this.alert.notify({
|
||
level: 'P0',
|
||
category: 'settlement',
|
||
title: '合伙人周账单任务失败',
|
||
detail: e instanceof Error ? e.message : String(e),
|
||
dedupeKey: `job_partner_bill|${new Date().toISOString().slice(0, 10)}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
@Cron('0 8 1 * *', { timeZone: 'Asia/Shanghai' })
|
||
async handleMonthlyPartnerBills() {
|
||
this.logger.log('Monthly logistics bills job start');
|
||
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: 'settlement',
|
||
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: 'settlement',
|
||
title: '物流月对账任务失败',
|
||
detail: e instanceof Error ? e.message : String(e),
|
||
dedupeKey: `job_logistics_bill|${new Date().toISOString().slice(0, 7)}`,
|
||
});
|
||
}
|
||
}
|
||
}
|