From 3b669f7e383cab4344ebce45632d0099062f7b55 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Fri, 28 Aug 2026 14:12:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(settlement):=20=E9=97=A8=E5=BA=97=20T+1=20?= =?UTF-8?q?=E5=87=BA=E8=B4=A6=E6=97=A5=E6=94=B9=E4=B8=BA=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=BB=8A=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HQ 门店账单列名改为出账日;billDate 记今日(昨日 00:00–今日 00:00 右端),启动时把旧数据从核销自然日对齐过来。 Co-authored-by: Cursor --- apps/admin-web/src/pages/StoreBillsPage.tsx | 10 +-- docs/杜康好客-v3-PRD.md | 1 + packages/shared-types/src/settlement.ts | 1 + server/dukang-api/prisma/seed-finance-mock.ts | 6 +- .../src/jobs/settlement.scheduler.ts | 2 +- .../modules/settlement/settlement.service.ts | 63 ++++++++++++++++--- 6 files changed, 65 insertions(+), 18 deletions(-) diff --git a/apps/admin-web/src/pages/StoreBillsPage.tsx b/apps/admin-web/src/pages/StoreBillsPage.tsx index b5b680c..11360b1 100644 --- a/apps/admin-web/src/pages/StoreBillsPage.tsx +++ b/apps/admin-web/src/pages/StoreBillsPage.tsx @@ -297,8 +297,8 @@ export default function StoreBillsPage() { }, { title: ( - - 账单日 + + 出账日 ), dataIndex: 'date', @@ -395,7 +395,7 @@ export default function StoreBillsPage() { <> 含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。 「已打款」= T+1 出账终态,「已结算」= 手动提现终态。 - T+1「账单日」是出账对应的核销自然日(定时任务按昨日窗口生成),不是最后一笔核销时间。 + T+1「出账日」是任务跑的那天(昨日 00:00–今日 00:00 窗口中的今天),不是最后一笔核销时间。 {overdueSummary && overdueSummary.overdueCount > 0 ? (
@@ -476,7 +476,7 @@ export default function StoreBillsPage() { }))} /> - + @@ -550,7 +550,7 @@ export default function StoreBillsPage() { <> {String(detail.billNo)} - + {String(detail.billDate || '').slice(0, 10)} diff --git a/docs/杜康好客-v3-PRD.md b/docs/杜康好客-v3-PRD.md index 2c27406..66ce261 100644 --- a/docs/杜康好客-v3-PRD.md +++ b/docs/杜康好客-v3-PRD.md @@ -112,6 +112,7 @@ - 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行) - 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示 - 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型) +- 门店 T+1「出账日」= 出账当天(核销窗口「昨日 00:00–今日 00:00」中的今天) - 核销详情展示核销门店主账户收款信息 ### 3.4~3.8 Wave 能力 diff --git a/packages/shared-types/src/settlement.ts b/packages/shared-types/src/settlement.ts index cfa2b93..f83b12c 100644 --- a/packages/shared-types/src/settlement.ts +++ b/packages/shared-types/src/settlement.ts @@ -146,6 +146,7 @@ export interface StoreBillDto { id: string; billNo: string; storeId: string; + /** 出账日 YYYY-MM-DD(T+1 窗口「昨日 00:00–今日 00:00」中的今天) */ billDate: string; redeemCount: number; redeemAmount: number; diff --git a/server/dukang-api/prisma/seed-finance-mock.ts b/server/dukang-api/prisma/seed-finance-mock.ts index 83e0f70..24c205d 100644 --- a/server/dukang-api/prisma/seed-finance-mock.ts +++ b/server/dukang-api/prisma/seed-finance-mock.ts @@ -91,12 +91,12 @@ async function main() { await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } }); await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } }); - // ─── 1. 门店账单(昨日未打款 + 前日已打款)─── + // ─── 1. 门店账单(今日出账未打款 + 昨日出账已打款)─── const storeBillUnpaid = await prisma.storeBill.create({ data: { billNo: billNo('SB', `${yesterday.getTime()}U`), storeId: store.id, - billDate: yesterday, + billDate: today, redeemCount: 5, redeemAmount: 1500, settlementRate: 0.6, @@ -108,7 +108,7 @@ async function main() { data: { billNo: billNo('SB', `${dayBefore.getTime()}P`), storeId: store.id, - billDate: dayBefore, + billDate: yesterday, redeemCount: 3, redeemAmount: 800, settlementRate: 0.6, diff --git a/server/dukang-api/src/jobs/settlement.scheduler.ts b/server/dukang-api/src/jobs/settlement.scheduler.ts index cfc37cb..91b8915 100644 --- a/server/dukang-api/src/jobs/settlement.scheduler.ts +++ b/server/dukang-api/src/jobs/settlement.scheduler.ts @@ -5,7 +5,7 @@ import { AlertService } from '../common/alert/alert.service'; /** * 财务对账单定时任务(Asia/Shanghai) - * - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销) + * - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销,出账日=今天) * - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账 * - 工作日 18:05:门店提现 T+0 审完预警(FIN-003) */ diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts index 263b5b7..d209e31 100644 --- a/server/dukang-api/src/modules/settlement/settlement.service.ts +++ b/server/dukang-api/src/modules/settlement/settlement.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger, NotFoundException, OnModuleInit } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { DEFAULT_STORE_WITHDRAW_DAILY_LIMIT, @@ -55,11 +55,16 @@ function startOfDay(d: Date) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0); } +/** 核销窗口:昨日 00:00 ≤ t < 今日 00:00;出账日 = 今日 00:00(窗口右端) */ function dayWindow(anchor = new Date()) { const end = startOfDay(anchor); const start = new Date(end); start.setDate(start.getDate() - 1); - return { start, end, billDate: start }; + return { start, end, billDate: end }; +} + +function shanghaiYmd(d: Date): string { + return d.toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' }); } function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) { @@ -131,7 +136,9 @@ function isWithdrawOverdue(appliedAt: Date, now = new Date()): boolean { } @Injectable() -export class SettlementService { +export class SettlementService implements OnModuleInit { + private readonly logger = new Logger(SettlementService.name); + constructor( private readonly prisma: PrismaService, private readonly analyticsService: AnalyticsService, @@ -141,6 +148,37 @@ export class SettlementService { private readonly wecomPush: WecomMessagePushService, ) {} + async onModuleInit() { + try { + const shifted = await this.realignStoreBillIssueDates(); + if (shifted > 0) this.logger.log(`Store bill dates aligned to issue day: ${shifted}`); + } catch (e) { + this.logger.warn(`Store bill date align skipped: ${e instanceof Error ? e.message : e}`); + } + } + + /** + * 旧数据 billDate=核销自然日(窗口左端);现改为出账日(窗口右端=今天)。 + * 按 created_at 日历日对比,只把仍早一天的行 +1;从晚到早更新避免 (storeId,billDate) 冲突。 + */ + private async realignStoreBillIssueDates(): Promise { + const locked = await this.prisma.$queryRaw>` + SELECT GET_LOCK('store_bill_issue_date_align', 5) AS acquired + `; + if (!Number(locked[0]?.acquired)) return 0; + try { + const shifted = await this.prisma.$executeRaw` + UPDATE store_bill + SET bill_date = DATE_ADD(bill_date, INTERVAL 1 DAY) + WHERE DATE(bill_date) < DATE(created_at) + ORDER BY bill_date DESC + `; + return Number(shifted); + } finally { + await this.prisma.$queryRaw`SELECT RELEASE_LOCK('store_bill_issue_date_align')`; + } + } + private notifyPartnerBillDigest( period: string, rows: Array<{ @@ -975,7 +1013,7 @@ export class SettlementService { // ─── Store bills (daily header) ────────────────────── - /** 生成指定自然日窗口内的门店对账单(默认昨日) */ + /** 生成昨日核销窗口的门店对账单;billDate = 出账日(今日) */ async generateStoreBillsForDay(anchor = new Date()) { const { start, end, billDate } = dayWindow(anchor); const payouts = await this.prisma.storePayout.findMany({ @@ -1100,7 +1138,7 @@ export class SettlementService { }); } - const period = billDate.toISOString().slice(0, 10); + const period = shanghaiYmd(billDate); if (createdBills.length) { const banks = await loadStorePrimaryBanksMap( this.prisma, @@ -1265,6 +1303,7 @@ export class SettlementService { return serializeBigInt({ items: slice.map((r) => ({ ...r, + date: r.kind === 'T1_BILL' ? shanghaiYmd(r.date) : r.date, bankAccount: r.kind === 'T1_BILL' ? bankMap.get(String(r.storeId)) ?? null @@ -1319,7 +1358,13 @@ export class SettlementService { totalAmount: Number(aggregates._sum.payoutAmount ?? 0), }; - return serializeBigInt({ items, total, page, pageSize, summary }); + return serializeBigInt({ + items: items.map((b) => ({ ...b, billDate: shanghaiYmd(b.billDate) })), + total, + page, + pageSize, + summary, + }); } async getAdminStoreBill(id: bigint) { @@ -1335,7 +1380,7 @@ export class SettlementService { }); if (!bill) throw new NotFoundException('门店对账单不存在'); const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId); - return serializeBigInt({ ...bill, storeAccount }); + return serializeBigInt({ ...bill, billDate: shanghaiYmd(bill.billDate), storeAccount }); } async confirmStoreBill(id: bigint, dto: { paymentRef?: string } = {}) { @@ -1393,7 +1438,7 @@ export class SettlementService { ); const header = [ '账单号', - '账单日', + '出账日', '门店', '城市', '核销笔数', @@ -1411,7 +1456,7 @@ export class SettlementService { const bank = bankMap.get(String(b.storeId)); return [ csvEscape(b.billNo), - b.billDate.toISOString().slice(0, 10), + shanghaiYmd(b.billDate), csvEscape(b.store.name), csvEscape(b.store.cityName ?? ''), b.redeemCount,