merge(dev_jacy): store bill issue date to today
This commit is contained in:
@@ -297,8 +297,8 @@ export default function StoreBillsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
<Tooltip title="T+1 为账单日(对应昨日核销自然日窗口,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
<Tooltip title="T+1 为出账日(昨日 00:00–今日 00:00 窗口中的今天,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
||||||
账单日
|
出账日
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
dataIndex: 'date',
|
dataIndex: 'date',
|
||||||
@@ -395,7 +395,7 @@ export default function StoreBillsPage() {
|
|||||||
<>
|
<>
|
||||||
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。
|
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。
|
||||||
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
||||||
T+1「账单日」是出账对应的核销自然日(定时任务按昨日窗口生成),不是最后一笔核销时间。
|
T+1「出账日」是任务跑的那天(昨日 00:00–今日 00:00 窗口中的今天),不是最后一笔核销时间。
|
||||||
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text type="danger">
|
<Typography.Text type="danger">
|
||||||
@@ -476,7 +476,7 @@ export default function StoreBillsPage() {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="range" label="账单日">
|
<Form.Item name="range" label="出账日">
|
||||||
<DatePicker.RangePicker />
|
<DatePicker.RangePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
@@ -550,7 +550,7 @@ export default function StoreBillsPage() {
|
|||||||
<>
|
<>
|
||||||
<Descriptions column={1} size="small" bordered>
|
<Descriptions column={1} size="small" bordered>
|
||||||
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="账单日">
|
<Descriptions.Item label="出账日">
|
||||||
{String(detail.billDate || '').slice(0, 10)}
|
{String(detail.billDate || '').slice(0, 10)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="应付">
|
<Descriptions.Item label="应付">
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
||||||
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
||||||
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
||||||
|
- 门店 T+1「出账日」= 出账当天(核销窗口「昨日 00:00–今日 00:00」中的今天)
|
||||||
- 核销详情展示核销门店主账户收款信息
|
- 核销详情展示核销门店主账户收款信息
|
||||||
|
|
||||||
### 3.4~3.8 Wave 能力
|
### 3.4~3.8 Wave 能力
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export interface StoreBillDto {
|
|||||||
id: string;
|
id: string;
|
||||||
billNo: string;
|
billNo: string;
|
||||||
storeId: string;
|
storeId: string;
|
||||||
|
/** 出账日 YYYY-MM-DD(T+1 窗口「昨日 00:00–今日 00:00」中的今天) */
|
||||||
billDate: string;
|
billDate: string;
|
||||||
redeemCount: number;
|
redeemCount: number;
|
||||||
redeemAmount: number;
|
redeemAmount: number;
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ async function main() {
|
|||||||
await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } });
|
await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } });
|
||||||
await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } });
|
await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } });
|
||||||
|
|
||||||
// ─── 1. 门店账单(昨日未打款 + 前日已打款)───
|
// ─── 1. 门店账单(今日出账未打款 + 昨日出账已打款)───
|
||||||
const storeBillUnpaid = await prisma.storeBill.create({
|
const storeBillUnpaid = await prisma.storeBill.create({
|
||||||
data: {
|
data: {
|
||||||
billNo: billNo('SB', `${yesterday.getTime()}U`),
|
billNo: billNo('SB', `${yesterday.getTime()}U`),
|
||||||
storeId: store.id,
|
storeId: store.id,
|
||||||
billDate: yesterday,
|
billDate: today,
|
||||||
redeemCount: 5,
|
redeemCount: 5,
|
||||||
redeemAmount: 1500,
|
redeemAmount: 1500,
|
||||||
settlementRate: 0.6,
|
settlementRate: 0.6,
|
||||||
@@ -108,7 +108,7 @@ async function main() {
|
|||||||
data: {
|
data: {
|
||||||
billNo: billNo('SB', `${dayBefore.getTime()}P`),
|
billNo: billNo('SB', `${dayBefore.getTime()}P`),
|
||||||
storeId: store.id,
|
storeId: store.id,
|
||||||
billDate: dayBefore,
|
billDate: yesterday,
|
||||||
redeemCount: 3,
|
redeemCount: 3,
|
||||||
redeemAmount: 800,
|
redeemAmount: 800,
|
||||||
settlementRate: 0.6,
|
settlementRate: 0.6,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AlertService } from '../common/alert/alert.service';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 财务对账单定时任务(Asia/Shanghai)
|
* 财务对账单定时任务(Asia/Shanghai)
|
||||||
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销)
|
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销,出账日=今天)
|
||||||
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
||||||
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
* - 工作日 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 { Prisma } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
DEFAULT_STORE_WITHDRAW_DAILY_LIMIT,
|
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);
|
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()) {
|
function dayWindow(anchor = new Date()) {
|
||||||
const end = startOfDay(anchor);
|
const end = startOfDay(anchor);
|
||||||
const start = new Date(end);
|
const start = new Date(end);
|
||||||
start.setDate(start.getDate() - 1);
|
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) {
|
function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) {
|
||||||
@@ -131,7 +136,9 @@ function isWithdrawOverdue(appliedAt: Date, now = new Date()): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SettlementService {
|
export class SettlementService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SettlementService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
@@ -141,6 +148,37 @@ export class SettlementService {
|
|||||||
private readonly wecomPush: WecomMessagePushService,
|
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(
|
private notifyPartnerBillDigest(
|
||||||
period: string,
|
period: string,
|
||||||
rows: Array<{
|
rows: Array<{
|
||||||
@@ -975,7 +1013,7 @@ export class SettlementService {
|
|||||||
|
|
||||||
// ─── Store bills (daily header) ──────────────────────
|
// ─── Store bills (daily header) ──────────────────────
|
||||||
|
|
||||||
/** 生成指定自然日窗口内的门店对账单(默认昨日) */
|
/** 生成昨日核销窗口的门店对账单;billDate = 出账日(今日) */
|
||||||
async generateStoreBillsForDay(anchor = new Date()) {
|
async generateStoreBillsForDay(anchor = new Date()) {
|
||||||
const { start, end, billDate } = dayWindow(anchor);
|
const { start, end, billDate } = dayWindow(anchor);
|
||||||
const payouts = await this.prisma.storePayout.findMany({
|
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) {
|
if (createdBills.length) {
|
||||||
const banks = await loadStorePrimaryBanksMap(
|
const banks = await loadStorePrimaryBanksMap(
|
||||||
this.prisma,
|
this.prisma,
|
||||||
@@ -1265,6 +1303,7 @@ export class SettlementService {
|
|||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
items: slice.map((r) => ({
|
items: slice.map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
|
date: r.kind === 'T1_BILL' ? shanghaiYmd(r.date) : r.date,
|
||||||
bankAccount:
|
bankAccount:
|
||||||
r.kind === 'T1_BILL'
|
r.kind === 'T1_BILL'
|
||||||
? bankMap.get(String(r.storeId)) ?? null
|
? bankMap.get(String(r.storeId)) ?? null
|
||||||
@@ -1319,7 +1358,13 @@ export class SettlementService {
|
|||||||
totalAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
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) {
|
async getAdminStoreBill(id: bigint) {
|
||||||
@@ -1335,7 +1380,7 @@ export class SettlementService {
|
|||||||
});
|
});
|
||||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||||
const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId);
|
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 } = {}) {
|
async confirmStoreBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
@@ -1393,7 +1438,7 @@ export class SettlementService {
|
|||||||
);
|
);
|
||||||
const header = [
|
const header = [
|
||||||
'账单号',
|
'账单号',
|
||||||
'账单日',
|
'出账日',
|
||||||
'门店',
|
'门店',
|
||||||
'城市',
|
'城市',
|
||||||
'核销笔数',
|
'核销笔数',
|
||||||
@@ -1411,7 +1456,7 @@ export class SettlementService {
|
|||||||
const bank = bankMap.get(String(b.storeId));
|
const bank = bankMap.get(String(b.storeId));
|
||||||
return [
|
return [
|
||||||
csvEscape(b.billNo),
|
csvEscape(b.billNo),
|
||||||
b.billDate.toISOString().slice(0, 10),
|
shanghaiYmd(b.billDate),
|
||||||
csvEscape(b.store.name),
|
csvEscape(b.store.name),
|
||||||
csvEscape(b.store.cityName ?? ''),
|
csvEscape(b.store.cityName ?? ''),
|
||||||
b.redeemCount,
|
b.redeemCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user