This commit is contained in:
@@ -297,8 +297,8 @@ export default function StoreBillsPage() {
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="T+1 为账单日(对应昨日核销自然日窗口,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
||||
账单日
|
||||
<Tooltip title="T+1 为出账日(昨日 00:00–今日 00:00 窗口中的今天,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
||||
出账日
|
||||
</Tooltip>
|
||||
),
|
||||
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 ? (
|
||||
<div>
|
||||
<Typography.Text type="danger">
|
||||
@@ -476,7 +476,7 @@ export default function StoreBillsPage() {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="range" label="账单日">
|
||||
<Form.Item name="range" label="出账日">
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
@@ -550,7 +550,7 @@ export default function StoreBillsPage() {
|
||||
<>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
||||
<Descriptions.Item label="账单日">
|
||||
<Descriptions.Item label="出账日">
|
||||
{String(detail.billDate || '').slice(0, 10)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="应付">
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
||||
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
||||
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
||||
- 门店 T+1「出账日」= 出账当天(核销窗口「昨日 00:00–今日 00:00」中的今天)
|
||||
- 核销详情展示核销门店主账户收款信息
|
||||
|
||||
### 3.4~3.8 Wave 能力
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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<number> {
|
||||
const locked = await this.prisma.$queryRaw<Array<{ acquired: number | bigint | null }>>`
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user