fix(server): 时间戳→业务日期/时间全部统一为中国时区(UTC+8)
此前仅'今天'默认值统一为中国日期,Date→字符串的转换仍是 UTC: - sync 考勤同步窗口 lastSyncAt 时间戳按 UTC 取日期,凌晨会偏一天 - 考勤导出 punchTime/createdAt 显示 UTC 时间 - normalizeDateOnly 把 ISO datetime 按 UTC 归一化,业务日期少一天 - getCourseClock/getChinaDateParts 用 dayjs.utc(date).add(8h) 隐式转换 统一为 dayjs(date).utcOffset(8),纯日期字符串运算(shiftDate/addDays/ daysInMonth/nextMonth)保留 dayjs.utc;date-normalization 测试断言 同步更新为北京时间语义。
This commit is contained in:
@@ -49,7 +49,7 @@ export function optionalDate(value: unknown, field: string): ToolInputResult<str
|
||||
return { ok: false, error: `${field} 必须是 YYYY-MM-DD 日期` };
|
||||
}
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
if (Number.isNaN(date.getTime()) || dayjs(date).utc().format('YYYY-MM-DD') !== value) {
|
||||
if (Number.isNaN(date.getTime()) || dayjs(date).utcOffset(8).format('YYYY-MM-DD') !== value) {
|
||||
return { ok: false, error: `${field} 不是有效日期` };
|
||||
}
|
||||
return { ok: true, value };
|
||||
|
||||
@@ -76,7 +76,7 @@ export class AttendanceCalendarService {
|
||||
const start = new Date(weekStart);
|
||||
const end = new Date(start);
|
||||
end.setDate(start.getDate() + 6);
|
||||
const endStr = dayjs(end).utc().format('YYYY-MM-DD');
|
||||
const endStr = dayjs(end).utcOffset(8).format('YYYY-MM-DD');
|
||||
|
||||
const records = await this.attendanceRepo.find({
|
||||
where: {
|
||||
|
||||
@@ -97,7 +97,7 @@ export class AttendanceGenerationService {
|
||||
const entities: AttendanceRecord[] = [];
|
||||
const end = new Date(dateTo);
|
||||
for (let d = new Date(dateFrom); d <= end; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = dayjs(d).utc().format('YYYY-MM-DD');
|
||||
const dateStr = dayjs(d).utcOffset(8).format('YYYY-MM-DD');
|
||||
const weekDay = d.getDay() === 0 ? 7 : d.getDay();
|
||||
|
||||
for (const sched of schedules) {
|
||||
@@ -158,7 +158,7 @@ export class AttendanceGenerationService {
|
||||
}
|
||||
|
||||
private getCourseClock(date: Date): { date: string; minutes: number } {
|
||||
const c = dayjs.utc(date).add(8, 'hour');
|
||||
const c = dayjs(date).utcOffset(8);
|
||||
return {
|
||||
date: c.format('YYYY-MM-DD'),
|
||||
minutes: c.hour() * 60 + c.minute(),
|
||||
|
||||
@@ -239,7 +239,7 @@ export class AttendanceImportService {
|
||||
}
|
||||
|
||||
private formatDate(value: Date): string {
|
||||
return dayjs.utc(value).format('YYYY-MM-DD');
|
||||
return dayjs(value).utcOffset(8).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -153,7 +153,7 @@ export class AttendanceLeaveSyncService {
|
||||
}
|
||||
|
||||
private formatDate(value: Date): string {
|
||||
return dayjs.utc(value).format('YYYY-MM-DD');
|
||||
return dayjs(value).utcOffset(8).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
private async resolveStudentName(dingUserId: string): Promise<string> {
|
||||
|
||||
@@ -196,11 +196,11 @@ export class AttendanceRecordsController extends AttendanceControllerBase {
|
||||
source: record.source || '',
|
||||
punchDevice: record.punchDeviceName || record.punchDeviceId || '',
|
||||
punchTime: record.punchTime
|
||||
? dayjs(record.punchTime).utc().format('YYYY-MM-DD HH:mm:ss')
|
||||
? dayjs(record.punchTime).utcOffset(8).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '',
|
||||
remark: record.remark || '',
|
||||
createdAt: record.createdAt
|
||||
? dayjs(record.createdAt).utc().format('YYYY-MM-DD HH:mm:ss')
|
||||
? dayjs(record.createdAt).utcOffset(8).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export class AttendanceReportService {
|
||||
async getAlerts(days: number = 14, threshold: number = 3, accessibleClassIds?: number[]) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
const cutoffStr = dayjs(cutoff).utc().format('YYYY-MM-DD');
|
||||
const cutoffStr = dayjs(cutoff).utcOffset(8).format('YYYY-MM-DD');
|
||||
|
||||
const qb = this.attendanceRepo
|
||||
.createQueryBuilder('a')
|
||||
|
||||
@@ -285,7 +285,7 @@ export class AttendanceSettlementService {
|
||||
|
||||
private getCourseClock(date: Date): { date: string; weekDay: number; minutes: number } {
|
||||
// Asia/Shanghai 无夏令时,固定 UTC+8 与 Intl 时区格式化等价
|
||||
const c = dayjs.utc(date).add(8, 'hour');
|
||||
const c = dayjs(date).utcOffset(8);
|
||||
const weekDay = c.day() === 0 ? 7 : c.day();
|
||||
return {
|
||||
date: c.format('YYYY-MM-DD'),
|
||||
|
||||
@@ -259,7 +259,7 @@ export class BillsGenerationService {
|
||||
private isValidDate(value: string) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value;
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utcOffset(8).format('YYYY-MM-DD') === value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export class RentalScheduleService {
|
||||
const current = this.toUtcDate(startDate);
|
||||
const end = this.toUtcDate(endDate);
|
||||
while (current <= end) {
|
||||
dates.add(dayjs(current).utc().format('YYYY-MM-DD'));
|
||||
dates.add(dayjs(current).utcOffset(8).format('YYYY-MM-DD'));
|
||||
current.setUTCDate(current.getUTCDate() + 1);
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export class RentalScheduleService {
|
||||
const startWeekDay = current.getUTCDay() || 7;
|
||||
current.setUTCDate(current.getUTCDate() + ((schedule.weekDay - startWeekDay + 7) % 7));
|
||||
while (current <= end) {
|
||||
dates.add(dayjs(current).utc().format('YYYY-MM-DD'));
|
||||
dates.add(dayjs(current).utcOffset(8).format('YYYY-MM-DD'));
|
||||
current.setUTCDate(current.getUTCDate() + 7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ export class ClassroomsService {
|
||||
const effStart = new Date(Math.max(new Date(r.startDate).getTime(), start.getTime()));
|
||||
const effEnd = new Date(Math.min(new Date(r.endDate).getTime(), end.getTime()));
|
||||
for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
|
||||
rentalDaysByRoom[r.classroomId].add(dayjs(d).utc().format('YYYY-MM-DD'));
|
||||
rentalDaysByRoom[r.classroomId].add(dayjs(d).utcOffset(8).format('YYYY-MM-DD'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ export class ClassroomsService {
|
||||
const effStart = new Date(Math.max(new Date(s.startDate).getTime(), start.getTime()));
|
||||
const effEnd = new Date(Math.min(new Date(s.endDate).getTime(), end.getTime()));
|
||||
for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
|
||||
scheduleDaysByRoom[s.classroomId].add(dayjs(d).utc().format('YYYY-MM-DD'));
|
||||
scheduleDaysByRoom[s.classroomId].add(dayjs(d).utcOffset(8).format('YYYY-MM-DD'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ describe('normalizeDateOnly', () => {
|
||||
expect(normalizeDateOnly('2026-07-02')).toBe('2026-07-02');
|
||||
});
|
||||
|
||||
it('converts legacy ISO timestamps to their UTC calendar date', () => {
|
||||
expect(normalizeDateOnly('2026-07-01T16:00:00.000Z')).toBe('2026-07-01');
|
||||
it('converts legacy ISO timestamps to their China calendar date (UTC+8)', () => {
|
||||
// 2026-07-01T16:00:00Z = 北京时间 2026-07-02 00:00
|
||||
expect(normalizeDateOnly('2026-07-01T16:00:00.000Z')).toBe('2026-07-02');
|
||||
});
|
||||
|
||||
it('rejects unsupported date formats', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export function normalizeDateOnly(value?: string | null): string | null | undefi
|
||||
const isoPrefix = ISO_DATE_PREFIX_PATTERN.exec(value)?.[1];
|
||||
if (isoPrefix) {
|
||||
const date = new Date(value);
|
||||
if (!Number.isNaN(date.getTime())) return dayjs(date).utc().format('YYYY-MM-DD');
|
||||
if (!Number.isNaN(date.getTime())) return dayjs(date).utcOffset(8).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
throw new Error(`无效日期格式: ${value}`);
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ExpenseOperationsService {
|
||||
private isValidDate(value: string) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value;
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utcOffset(8).format('YYYY-MM-DD') === value;
|
||||
}
|
||||
|
||||
async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) {
|
||||
|
||||
@@ -342,7 +342,7 @@ export class ExpensesService {
|
||||
private isValidDate(value: string) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value;
|
||||
return !Number.isNaN(date.getTime()) && dayjs(date).utcOffset(8).format('YYYY-MM-DD') === value;
|
||||
}
|
||||
|
||||
async createStudentUtilityBill(dto: CreateStudentUtilityBillDto, userId?: number) {
|
||||
|
||||
@@ -159,7 +159,7 @@ export class OccupancyOperationsService {
|
||||
const transferDate = new Date(dto.transferDate);
|
||||
const nextDay = new Date(transferDate);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
const defaultBillingStart = dayjs(nextDay).utc().format('YYYY-MM-DD');
|
||||
const defaultBillingStart = dayjs(nextDay).utcOffset(8).format('YYYY-MM-DD');
|
||||
this.assertDateOrder(
|
||||
dto.transferDate,
|
||||
dto.newBillingStartDate || defaultBillingStart,
|
||||
|
||||
@@ -148,7 +148,7 @@ import dayjs from '../common/dayjs';
|
||||
|
||||
export function getChinaDateParts(date = new Date()): { date: string; weekDay: number } {
|
||||
// Asia/Shanghai 无夏令时,固定 UTC+8 与 Intl en-CA 格式化等价
|
||||
const c = dayjs.utc(date).add(8, 'hour');
|
||||
const c = dayjs(date).utcOffset(8);
|
||||
const weekDay = c.day() === 0 ? 7 : c.day();
|
||||
return { date: c.format('YYYY-MM-DD'), weekDay };
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function buildDailySchedulePlans(
|
||||
const toDate = new Date(`${syncTo}T00:00:00.000Z`);
|
||||
|
||||
for (let date = new Date(fromDate); date <= toDate; date.setUTCDate(date.getUTCDate() + 1)) {
|
||||
const dateStr = dayjs(date).utc().format('YYYY-MM-DD');
|
||||
const dateStr = dayjs(date).utcOffset(8).format('YYYY-MM-DD');
|
||||
const weekDay = date.getUTCDay() === 0 ? 7 : date.getUTCDay();
|
||||
|
||||
for (const schedule of schedules) {
|
||||
|
||||
@@ -71,8 +71,8 @@ export class SyncService {
|
||||
}
|
||||
|
||||
const result = await this.attendanceImportService.importFromDingTalk({
|
||||
startDate: dayjs(startDate).utc().format('YYYY-MM-DD'),
|
||||
endDate: dayjs(endDate).utc().format('YYYY-MM-DD'),
|
||||
startDate: dayjs(startDate).utcOffset(8).format('YYYY-MM-DD'),
|
||||
endDate: dayjs(endDate).utcOffset(8).format('YYYY-MM-DD'),
|
||||
userIds,
|
||||
autoMatch: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user