v4.0.3版本
CI / verify (pull_request) Waiting to run

总部后端,开发计划-任务列表-来源工单支持链接
总部端-开发计划-任务列表-编辑任务-增加“开发中”状态
账单日期修正
This commit is contained in:
2026-08-31 14:22:49 +08:00
parent 3166467518
commit e26206b93c
23 changed files with 522 additions and 139 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
export type DevPlanTaskStatus = 'TODO' | 'DEVELOPED' | 'RELEASED';
export type DevPlanTaskStatus = 'TODO' | 'IN_PROGRESS' | 'DEVELOPED' | 'RELEASED';
export type DevPlanVersionStatus = 'PENDING' | 'IN_PROGRESS' | 'TESTING' | 'RELEASED';
export function computeDurationMinutes(
+1
View File
@@ -409,3 +409,4 @@ export * from './city-partner';
export * from './dev-plan';
export * from './support-ticket';
export * from './phone';
export * from './shanghai-date';
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest';
import {
addShanghaiDays,
previousShanghaiMonth,
shanghaiLaggedIssueWindow,
shanghaiMonthLastInstant,
shanghaiMonthRange,
shanghaiPeriodYmds,
shanghaiT1DayWindow,
shanghaiYmd,
startOfShanghaiDay,
} from './shanghai-date';
describe('shanghaiYmd / startOfShanghaiDay', () => {
it('UTC 16:00 已是北京次日 00:00,账单日取北京日历日', () => {
const utc = new Date('2026-08-30T16:00:00.000Z');
expect(shanghaiYmd(utc)).toBe('2026-08-31');
expect(startOfShanghaiDay(utc).toISOString()).toBe('2026-08-30T16:00:00.000Z');
});
it('UTC 15:59 仍是北京当日', () => {
const utc = new Date('2026-08-30T15:59:59.000Z');
expect(shanghaiYmd(utc)).toBe('2026-08-30');
expect(startOfShanghaiDay(utc).toISOString()).toBe('2026-08-29T16:00:00.000Z');
});
it('不依赖服务器本地时区:输入 ISO 即得到北京自然日 00:00', () => {
const noonUtc = new Date('2026-08-30T04:00:00.000Z'); // 北京 12:00
expect(shanghaiYmd(noonUtc)).toBe('2026-08-30');
expect(startOfShanghaiDay(noonUtc).toISOString()).toBe('2026-08-29T16:00:00.000Z');
});
});
describe('shanghaiT1DayWindow', () => {
it('北京 8 月 30 日 08:00 出账:出账日 30 日,窗口为 29 日全天', () => {
const { start, end, billDate } = shanghaiT1DayWindow(new Date('2026-08-30T00:00:00.000Z'));
expect(shanghaiYmd(billDate)).toBe('2026-08-30');
expect(shanghaiYmd(start)).toBe('2026-08-29');
expect(end.toISOString()).toBe(billDate.toISOString());
});
it('北京 8 月 31 日 00:05:出账日仍是 31 日,不是 UTC 的 30 日', () => {
const { billDate } = shanghaiT1DayWindow(new Date('2026-08-30T16:05:00.000Z'));
expect(shanghaiYmd(billDate)).toBe('2026-08-31');
});
});
describe('shanghaiLaggedIssueWindow', () => {
it('酒厂 T+3:出账日是今天,纳入 3 天前完成的订单', () => {
const { start, end, billDate } = shanghaiLaggedIssueWindow(
new Date('2026-08-31T00:00:00.000Z'),
3,
);
expect(shanghaiYmd(billDate)).toBe('2026-08-31');
expect(shanghaiYmd(start)).toBe('2026-08-28');
expect(shanghaiYmd(end)).toBe('2026-08-29');
});
});
describe('addShanghaiDays', () => {
it('跨月跨年按北京日历加减', () => {
expect(shanghaiYmd(addShanghaiDays(new Date('2026-08-31T16:00:00.000Z'), 1))).toBe(
'2026-09-02',
);
expect(shanghaiYmd(addShanghaiDays(new Date('2026-01-01T00:00:00.000Z'), -1))).toBe(
'2025-12-31',
);
});
});
describe('shanghaiMonthRange', () => {
it('8 月账期为北京 8/1 00:00 至 9/1 00:00(不含)', () => {
const { start, endExclusive } = shanghaiMonthRange(2026, 8);
expect(start.toISOString()).toBe('2026-07-31T16:00:00.000Z');
expect(endExclusive.toISOString()).toBe('2026-08-31T16:00:00.000Z');
expect(shanghaiYmd(shanghaiMonthLastInstant(2026, 8))).toBe('2026-08-31');
});
it('UTC 月末 23:59:59 不会把账期止日滚成下月', () => {
const periodStart = new Date('2026-08-01T00:00:00.000Z');
expect(shanghaiPeriodYmds(periodStart)).toEqual({
periodStart: '2026-08-01',
periodEnd: '2026-08-31',
});
});
});
describe('previousShanghaiMonth', () => {
it('北京 9 月 1 日 08:00UTC 9/1 00:00)取 8 月', () => {
expect(previousShanghaiMonth(new Date('2026-09-01T00:00:00.000Z'))).toEqual({
year: 2026,
month: 8,
});
});
it('北京 9 月 1 日 00:30UTC 8/31 16:30)取 8 月,不因 UTC 仍是 8 月而错到 7 月', () => {
expect(previousShanghaiMonth(new Date('2026-08-31T16:30:00.000Z'))).toEqual({
year: 2026,
month: 8,
});
});
});
+76
View File
@@ -0,0 +1,76 @@
/** 财务账单日 / 账期一律按中国北京时间(Asia/Shanghai,无夏令时) */
export const SHANGHAI_TIME_ZONE = 'Asia/Shanghai';
function pad2(n: number): string {
return String(n).padStart(2, '0');
}
export function shanghaiYmd(d: Date): string {
return d.toLocaleDateString('sv-SE', { timeZone: SHANGHAI_TIME_ZONE });
}
export function parseShanghaiYmd(ymd: string): Date {
return new Date(`${ymd}T00:00:00+08:00`);
}
export function startOfShanghaiDay(d: Date = new Date()): Date {
return parseShanghaiYmd(shanghaiYmd(d));
}
export function addShanghaiDays(d: Date, days: number): Date {
const [y, m, day] = shanghaiYmd(startOfShanghaiDay(d)).split('-').map(Number);
const utc = new Date(Date.UTC(y, m - 1, day + days));
const ymd = `${utc.getUTCFullYear()}-${pad2(utc.getUTCMonth() + 1)}-${pad2(utc.getUTCDate())}`;
return new Date(`${ymd}T00:00:00+08:00`);
}
/** 门店 T+1:核销窗口 [昨日 00:00, 今日 00:00);出账日 = 今天 */
export function shanghaiT1DayWindow(anchor = new Date()) {
const billDate = startOfShanghaiDay(anchor);
const start = addShanghaiDays(billDate, -1);
return { start, end: billDate, billDate };
}
/**
* 酒厂 T+N:出账日 = 今天;纳入窗口为「今天 − lagDays」当天完成的订单
*/
export function shanghaiLaggedIssueWindow(anchor = new Date(), lagDays: number) {
const billDate = startOfShanghaiDay(anchor);
const start = addShanghaiDays(billDate, -lagDays);
const end = addShanghaiDays(start, 1);
return { start, end, billDate };
}
export function shanghaiMonthRange(year: number, month: number): { start: Date; endExclusive: Date } {
const start = new Date(`${year}-${pad2(month)}-01T00:00:00+08:00`);
const endMonth = month === 12 ? 1 : month + 1;
const endYear = month === 12 ? year + 1 : year;
const endExclusive = new Date(`${endYear}-${pad2(endMonth)}-01T00:00:00+08:00`);
return { start, endExclusive };
}
export function shanghaiMonthLastInstant(year: number, month: number): Date {
return new Date(shanghaiMonthRange(year, month).endExclusive.getTime() - 1);
}
export function previousShanghaiMonth(anchor = new Date()): { year: number; month: number } {
const [y, m] = shanghaiYmd(anchor).split('-').map(Number);
if (m === 1) return { year: y - 1, month: 12 };
return { year: y, month: m - 1 };
}
export function shanghaiYearMonth(anchor = new Date()): { year: number; month: number } {
const [y, m] = shanghaiYmd(anchor).split('-').map(Number);
return { year: y, month: m };
}
/** 账期展示:以 periodStart 所在北京月的首末日,避免 UTC 23:59:59 滚到下月 */
export function shanghaiPeriodYmds(periodStart: Date): { periodStart: string; periodEnd: string } {
const [y, m] = shanghaiYmd(periodStart).split('-').map(Number);
const { start, endExclusive } = shanghaiMonthRange(y, m);
return {
periodStart: shanghaiYmd(start),
periodEnd: shanghaiYmd(new Date(endExclusive.getTime() - 1)),
};
}
+4 -2
View File
@@ -22,11 +22,11 @@ export const DEV_PLAN_TASK_TYPE_LABELS: Record<DevPlanTaskTypeDto, string> = {
/** 开发计划任务状态 */
export type DevPlanTaskStatusDto = 'TODO' | 'DEVELOPED' | 'RELEASED';
export type DevPlanTaskStatusDto = 'TODO' | 'IN_PROGRESS' | 'DEVELOPED' | 'RELEASED';
export const DEV_PLAN_TASK_STATUSES = ['TODO', 'DEVELOPED', 'RELEASED'] as const;
export const DEV_PLAN_TASK_STATUSES = ['TODO', 'IN_PROGRESS', 'DEVELOPED', 'RELEASED'] as const;
@@ -34,6 +34,8 @@ export const DEV_PLAN_TASK_STATUS_LABELS: Record<DevPlanTaskStatusDto, string> =
TODO: '待开发',
IN_PROGRESS: '开发中',
DEVELOPED: '已开发',
RELEASED: '已上线',
+2 -1
View File
@@ -187,6 +187,7 @@ export interface StoreBillDto {
export interface WineryBillDto {
id: string;
billNo: string;
/** 出账日 YYYY-MM-DD(北京时间;T+3 只决定纳入哪天完成的订单,不是账单日) */
billDate: string;
orderCount: number;
orderAmount: number;
@@ -212,7 +213,7 @@ export const STORE_SETTLEMENT_DEFAULT_RATE = 0.6;
/** 酒厂账单结算比例(酒单实付 × 比例),暂定 30% */
export const WINERY_SETTLEMENT_RATE = 0.3;
/** 酒厂账单出账滞后自然日(T+3:订单完成日 + 3 天后纳入账单 */
/** 酒厂账单出账滞后自然日(T+3:订单完成日 + 3 天后纳入当天出账的账单;账单日=出账当天北京日历日 */
export const WINERY_SETTLEMENT_LAG_DAYS = 3;
export type { LogisticsSettlementMethod } from './enums';