From 8936453e84b5c995915a8b152530e83b318eeb54 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sat, 8 Aug 2026 17:52:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server):=20=E6=97=A5=E6=9C=9F/?= =?UTF-8?q?=E6=9C=88=E4=BB=BD=E6=A0=BC=E5=BC=8F=E5=8C=96=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20dayjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换散落的 toISOString().slice(0,10) / split('T')[0] / replace('T',' ')(UTC 语义用 dayjs(...).utc() 保持完全一致) - Asia/Shanghai 固定时区日期(Intl.DateTimeFormat en-CA)改用 dayjs().utcOffset(8) - 月份边界 first/last、daysInMonth、nextMonth、shiftDate/addDays 等改 dayjs 简洁实现 - 涉及 attendance/classes/classrooms/room/dashboard/bills/expenses/occupancies/sync/ai-chat/rental 等 28 个文件 --- .../src/agent-tools/tools/tool-input.ts | 3 ++- .../src/ai-chat/ai-review.import-relations.ts | 3 ++- .../attendance/attendance-calendar.service.ts | 5 ++-- .../attendance-generation.service.ts | 6 ++--- .../attendance/attendance-import.service.ts | 3 ++- .../attendance-leave-sync.service.ts | 3 ++- .../attendance-records.controller.ts | 5 ++-- .../attendance/attendance-report.service.ts | 3 ++- .../attendance-settlement.service.ts | 4 +--- .../src/bills/bills-generation.service.ts | 5 ++-- .../src/classes/classes-queries.service.ts | 3 ++- apps/server/src/classes/classes.service.ts | 7 +++--- .../classroom-rentals.service.ts | 18 +++++---------- .../rental-schedule.service.ts | 23 ++++++++----------- .../src/classrooms/classrooms.service.ts | 19 ++++----------- .../dashboard/dashboard-queries.service.ts | 13 ++++------- .../server/src/dashboard/dashboard.service.ts | 16 +++---------- .../server/src/database/date-normalization.ts | 3 ++- .../expenses/expense-operations.service.ts | 3 ++- apps/server/src/expenses/expenses.service.ts | 3 ++- .../src/integration/dingtalk.attendance.ts | 3 ++- .../occupancies/occupancy-import.service.ts | 3 ++- .../occupancy-operations.service.ts | 3 ++- .../src/rooms/room-inspections.service.ts | 12 +++------- apps/server/src/rooms/room-query.service.ts | 8 ++----- apps/server/src/sync/schedule-sync.helpers.ts | 7 +++--- apps/server/src/sync/schedule-sync.service.ts | 3 ++- apps/server/src/sync/sync.service.ts | 7 +++--- 28 files changed, 82 insertions(+), 112 deletions(-) diff --git a/apps/server/src/agent-tools/tools/tool-input.ts b/apps/server/src/agent-tools/tools/tool-input.ts index d10e60a..8159b1c 100644 --- a/apps/server/src/agent-tools/tools/tool-input.ts +++ b/apps/server/src/agent-tools/tools/tool-input.ts @@ -1,4 +1,5 @@ import type { ToolInputResult } from '../agent-tool.types'; +import dayjs from '../../common/dayjs'; const FORBIDDEN_KEYS = new Set([ 'userId', 'isSuperAdmin', 'permissions', 'roles', 'ability', 'user', 'password', 'token', @@ -48,7 +49,7 @@ export function optionalDate(value: unknown, field: string): ToolInputResult { diff --git a/apps/server/src/attendance/attendance-records.controller.ts b/apps/server/src/attendance/attendance-records.controller.ts index 1549327..b02e09c 100644 --- a/apps/server/src/attendance/attendance-records.controller.ts +++ b/apps/server/src/attendance/attendance-records.controller.ts @@ -6,6 +6,7 @@ import { AttendanceImportService } from './attendance-import.service'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { AuthorizationService } from '../authorization'; import { logAudit } from '../common/with-audit-log'; +import dayjs from '../common/dayjs'; import { extractRequestInfo, type RequestInfoSource } from '../common/request-utils'; import { RequirePermission } from '../auth/decorators/permission.decorator'; import { @@ -195,11 +196,11 @@ export class AttendanceRecordsController extends AttendanceControllerBase { source: record.source || '', punchDevice: record.punchDeviceName || record.punchDeviceId || '', punchTime: record.punchTime - ? record.punchTime.toISOString().replace('T', ' ').substring(0, 19) + ? dayjs(record.punchTime).utc().format('YYYY-MM-DD HH:mm:ss') : '', remark: record.remark || '', createdAt: record.createdAt - ? record.createdAt.toISOString().replace('T', ' ').substring(0, 19) + ? dayjs(record.createdAt).utc().format('YYYY-MM-DD HH:mm:ss') : '', }); } diff --git a/apps/server/src/attendance/attendance-report.service.ts b/apps/server/src/attendance/attendance-report.service.ts index 79c877a..bed6638 100644 --- a/apps/server/src/attendance/attendance-report.service.ts +++ b/apps/server/src/attendance/attendance-report.service.ts @@ -4,6 +4,7 @@ import { Repository } from 'typeorm'; import { AttendanceRecord, AttendanceDevice } from '../entities'; import type { AttendanceReportQueryDto } from './dto/attendance.dto'; import { attachAttendanceDeviceMappings } from './attendance-device'; +import dayjs from '../common/dayjs'; @Injectable() export class AttendanceReportService { @@ -141,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 = cutoff.toISOString().slice(0, 10); + const cutoffStr = dayjs(cutoff).utc().format('YYYY-MM-DD'); const qb = this.attendanceRepo .createQueryBuilder('a') diff --git a/apps/server/src/attendance/attendance-settlement.service.ts b/apps/server/src/attendance/attendance-settlement.service.ts index 553bf1f..46f1daf 100644 --- a/apps/server/src/attendance/attendance-settlement.service.ts +++ b/apps/server/src/attendance/attendance-settlement.service.ts @@ -295,8 +295,6 @@ export class AttendanceSettlementService { } private shiftDate(date: string, days: number): string { - const shifted = new Date(`${date}T00:00:00.000Z`); - shifted.setUTCDate(shifted.getUTCDate() + days); - return shifted.toISOString().slice(0, 10); + return dayjs.utc(date).add(days, 'day').format('YYYY-MM-DD'); } } diff --git a/apps/server/src/bills/bills-generation.service.ts b/apps/server/src/bills/bills-generation.service.ts index cfcf2cc..a780489 100644 --- a/apps/server/src/bills/bills-generation.service.ts +++ b/apps/server/src/bills/bills-generation.service.ts @@ -4,6 +4,7 @@ import { DataSource, Repository } from 'typeorm'; import { Bill, BillItem, RoomExpense, PersonalExpense, Occupancy, Room } from '../entities'; import { WalletsService } from '../wallets/wallets.service'; import type { GenerateBillsDto } from './dto/bill.dto'; +import dayjs from '../common/dayjs'; @Injectable() export class BillsGenerationService { @@ -235,7 +236,7 @@ export class BillsGenerationService { let year = startYear, month = startMonth; year < endYear || (year === endYear && month <= endMonth); ) { - const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(); + const daysInMonth = dayjs.utc(`${year}-${month}`).daysInMonth(); const prefix = `${year}-${String(month).padStart(2, '0')}-`; const overlapStart = activeStart > `${prefix}01` ? activeStart : `${prefix}01`; const monthEnd = `${prefix}${String(daysInMonth).padStart(2, '0')}`; @@ -258,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()) && date.toISOString().slice(0, 10) === value; + return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value; } diff --git a/apps/server/src/classes/classes-queries.service.ts b/apps/server/src/classes/classes-queries.service.ts index 7e05d60..fcd93a6 100644 --- a/apps/server/src/classes/classes-queries.service.ts +++ b/apps/server/src/classes/classes-queries.service.ts @@ -5,6 +5,7 @@ import { Class, ClassStudent, ClassSchedule, AttendanceRecord } from '../entitie import { Classroom } from '../entities/classroom.entity'; import { syncDingTalkStudents } from '../integration/dingtalk-student-sync'; import type { QueryClassScheduleDto, QueryClassAttendanceSummaryDto } from './dto/class.dto'; +import dayjs from '../common/dayjs'; interface AgentClassRow { id: string | number; @@ -80,7 +81,7 @@ export class ClassesQueriesService { const existingByStudentId = new Map( existingClassStudents.map((classStudent) => [classStudent.studentId, classStudent]), ); - const today = new Date().toISOString().slice(0, 10); + const today = dayjs().utc().format('YYYY-MM-DD'); let skipped = 0; const memberships = studentIds.flatMap((studentId) => { const existing = existingByStudentId.get(studentId); diff --git a/apps/server/src/classes/classes.service.ts b/apps/server/src/classes/classes.service.ts index 42f3409..152e485 100644 --- a/apps/server/src/classes/classes.service.ts +++ b/apps/server/src/classes/classes.service.ts @@ -22,6 +22,7 @@ StudentDingMapping } from '../entities'; import { ClassesQueriesService } from './classes-queries.service'; import { normalizeDateOnly } from '../database/date-normalization'; +import dayjs from '../common/dayjs'; import { CreateClassDto, UpdateClassDto, @@ -184,7 +185,7 @@ export class ClassesService { this.classStudentRepo.create({ classId: saved.id, studentId: sid, - joinDate: new Date().toISOString().split('T')[0], + joinDate: dayjs().utc().format('YYYY-MM-DD'), }), ); await this.classStudentRepo.save(entries); @@ -305,7 +306,7 @@ export class ClassesService { const existingByStudentId = new Map( existing.map((classStudent) => [classStudent.studentId, classStudent]), ); - const today = new Date().toISOString().split('T')[0]; + const today = dayjs().utc().format('YYYY-MM-DD'); let skipped = 0; const memberships = uniqueStudentIds.flatMap((studentId) => { const current = existingByStudentId.get(studentId); @@ -341,7 +342,7 @@ export class ClassesService { if (membership.status !== 'active') throw new BadRequestException('学生已离班'); membership.status = 'left'; - membership.leaveDate = new Date().toISOString().split('T')[0]; + membership.leaveDate = dayjs().utc().format('YYYY-MM-DD'); await this.classStudentRepo.save(membership); return { success: true }; } diff --git a/apps/server/src/classroom-rentals/classroom-rentals.service.ts b/apps/server/src/classroom-rentals/classroom-rentals.service.ts index 8cd9f4c..242cf89 100644 --- a/apps/server/src/classroom-rentals/classroom-rentals.service.ts +++ b/apps/server/src/classroom-rentals/classroom-rentals.service.ts @@ -14,6 +14,7 @@ import { AttendanceRecord } from '../entities/attendance-record.entity'; import { AttendanceSession } from '../entities/attendance-session.entity'; import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto'; import { RentalScheduleService } from './rental-schedule.service'; +import dayjs from '../common/dayjs'; import * as path from 'path'; import * as fs from 'fs'; @@ -88,9 +89,8 @@ export class ClassroomRentalsService { qb.andWhere('r.lesseeOrganizationId = :oid', { oid: query.lesseeOrganizationId }); if (query?.month) { const [y, m] = query.month.split('-').map(Number); - const first = `${y}-${String(m).padStart(2, '0')}-01`; - const lastDay = new Date(y, m, 0).getDate(); - const last = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; + const first = dayjs(`${y}-${m}-01`).format('YYYY-MM-DD'); + const last = dayjs(`${y}-${m}`).endOf('month').format('YYYY-MM-DD'); qb.andWhere('r.startDate <= :last AND r.endDate >= :first', { first, last }); } if (!query?.includeEnded) { @@ -144,9 +144,8 @@ export class ClassroomRentalsService { } if (query?.month) { const [y, m] = query.month.split('-').map(Number); - const first = `${y}-${String(m).padStart(2, '0')}-01`; - const lastDay = new Date(y, m, 0).getDate(); - const last = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; + const first = dayjs(`${y}-${m}-01`).format('YYYY-MM-DD'); + const last = dayjs(`${y}-${m}`).endOf('month').format('YYYY-MM-DD'); qb.andWhere('r.startDate <= :last AND r.endDate >= :first', { first, last }); } if (!query?.includeEnded) { @@ -289,12 +288,7 @@ export class ClassroomRentalsService { if (rental.effectiveStatus !== ClassroomRentalStatus.ACTIVE) { throw new BadRequestException('仅有效租赁可以结束'); } - const today = new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(new Date()); + const today = dayjs().utcOffset(8).format('YYYY-MM-DD'); if (rental.startDate > today) throw new BadRequestException('租赁尚未开始,不能结束'); await this.repo.update(id, { status: ClassroomRentalStatus.ENDED, diff --git a/apps/server/src/classroom-rentals/rental-schedule.service.ts b/apps/server/src/classroom-rentals/rental-schedule.service.ts index c1d6a2d..321ab7b 100644 --- a/apps/server/src/classroom-rentals/rental-schedule.service.ts +++ b/apps/server/src/classroom-rentals/rental-schedule.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Not, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; import { ClassroomRental, Classroom, ClassSchedule, ClassroomStatus } from '../entities'; import { ClassroomRentalStatus } from '../entities/classroom-rental.entity'; +import dayjs from '../common/dayjs'; const COLOR_PALETTE = [ "#5B8FF9", @@ -26,9 +27,8 @@ export class RentalScheduleService { ) {} async getUnavailableDates(classroomId: number, year: number, month: number, excludeId?: number) { - const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); - const monthStart = `${year}-${String(month).padStart(2, '0')}-01`; - const monthEnd = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; + const monthStart = dayjs(`${year}-${month}-01`).format('YYYY-MM-DD'); + const monthEnd = dayjs(`${year}-${month}`).endOf('month').format('YYYY-MM-DD'); const [rentals, schedules] = await Promise.all([ this.repo.find({ @@ -135,7 +135,7 @@ export class RentalScheduleService { const current = this.toUtcDate(startDate); const end = this.toUtcDate(endDate); while (current <= end) { - dates.add(current.toISOString().slice(0, 10)); + dates.add(dayjs(current).utc().format('YYYY-MM-DD')); current.setUTCDate(current.getUTCDate() + 1); } } @@ -155,16 +155,16 @@ export class RentalScheduleService { const startWeekDay = current.getUTCDay() || 7; current.setUTCDate(current.getUTCDate() + ((schedule.weekDay - startWeekDay + 7) % 7)); while (current <= end) { - dates.add(current.toISOString().slice(0, 10)); + dates.add(dayjs(current).utc().format('YYYY-MM-DD')); current.setUTCDate(current.getUTCDate() + 7); } } async getSchedule(year: number, month: number) { - const lastDay = new Date(year, month, 0).getDate(); - const first = `${year}-${String(month).padStart(2, '0')}-01`; - const last = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; + const first = dayjs(`${year}-${month}-01`).format('YYYY-MM-DD'); + const last = dayjs(`${year}-${month}`).endOf('month').format('YYYY-MM-DD'); + const lastDay = dayjs(`${year}-${month}`).daysInMonth(); const classrooms = await this.classroomRepo.find({ where: { status: Not(ClassroomStatus.ARCHIVED) }, @@ -285,12 +285,7 @@ export class RentalScheduleService { }; } withEffectiveStatus(rental: ClassroomRental) { - const today = new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(new Date()); + const today = dayjs().utcOffset(8).format('YYYY-MM-DD'); const effectiveStatus = rental.status === ClassroomRentalStatus.ACTIVE && rental.endDate < today ? ClassroomRentalStatus.ENDED diff --git a/apps/server/src/classrooms/classrooms.service.ts b/apps/server/src/classrooms/classrooms.service.ts index ddbb607..a597402 100644 --- a/apps/server/src/classrooms/classrooms.service.ts +++ b/apps/server/src/classrooms/classrooms.service.ts @@ -6,6 +6,7 @@ import { ClassroomRental, ClassroomRentalStatus } from '../entities/classroom-re import { ClassSchedule } from '../entities/class-schedule.entity'; import { AttendanceDevice } from '../entities/attendance-device.entity'; import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto'; +import dayjs from '../common/dayjs'; /** getRawMany 原始行:驱动可能返回 string/number,date 列可能是 string 或 Date */ interface ScheduleUsageRawRow { @@ -173,12 +174,7 @@ export class ClassroomsService { } private async assertNoActiveAllocations(classroomId: number) { - const today = new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(new Date()); + const today = dayjs().utcOffset(8).format('YYYY-MM-DD'); const scheduleCount = await this.scheduleRepo .createQueryBuilder('schedule') .where('schedule.classroomId = :classroomId', { classroomId }) @@ -226,12 +222,7 @@ export class ClassroomsService { if (classroomIds.length === 0) return result; const now = new Date(); - const todayStr = new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(now); + const todayStr = dayjs(now).utcOffset(8).format('YYYY-MM-DD'); const currentTime = new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Shanghai', hour: '2-digit', @@ -401,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(d.toISOString().slice(0, 10)); + rentalDaysByRoom[r.classroomId].add(dayjs(d).utc().format('YYYY-MM-DD')); } } @@ -410,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(d.toISOString().slice(0, 10)); + scheduleDaysByRoom[s.classroomId].add(dayjs(d).utc().format('YYYY-MM-DD')); } } diff --git a/apps/server/src/dashboard/dashboard-queries.service.ts b/apps/server/src/dashboard/dashboard-queries.service.ts index 5b4e06f..6b8baad 100644 --- a/apps/server/src/dashboard/dashboard-queries.service.ts +++ b/apps/server/src/dashboard/dashboard-queries.service.ts @@ -5,11 +5,10 @@ import { Occupancy } from '../entities/occupancy.entity'; import { Bill } from '../entities/bill.entity'; import { RoomExpense } from '../entities/room-expense.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity'; +import dayjs from '../common/dayjs'; export function nextMonth(ym: string): string { - const d = new Date(`${ym}-01`); - d.setMonth(d.getMonth() + 1); - return d.toISOString().slice(0, 7) + '-01'; + return dayjs.utc(`${ym}-01`).add(1, 'month').format('YYYY-MM-DD'); } export function applyClassScope( @@ -40,9 +39,7 @@ async getAttendanceTrend( todayStr: string, accessibleClassIds?: number[], ) { - const thirtyDaysAgo = new Date(todayStr); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29); - const startStr = thirtyDaysAgo.toISOString().slice(0, 10); + const startStr = dayjs.utc(todayStr).subtract(29, 'day').format('YYYY-MM-DD'); const trendQb = attendanceRepo .createQueryBuilder('a') @@ -82,9 +79,7 @@ async getIncomeTrend( const results: { month: string; amount: number }[] = []; for (let i = 5; i >= 0; i--) { - const d = new Date(`${currentMonth}-01`); - d.setMonth(d.getMonth() - i); - const m = d.toISOString().slice(0, 7); + const m = dayjs.utc(`${currentMonth}-01`).subtract(i, 'month').format('YYYY-MM'); const row = await billRepo .createQueryBuilder('b') diff --git a/apps/server/src/dashboard/dashboard.service.ts b/apps/server/src/dashboard/dashboard.service.ts index d9a8234..974dd97 100644 --- a/apps/server/src/dashboard/dashboard.service.ts +++ b/apps/server/src/dashboard/dashboard.service.ts @@ -10,6 +10,7 @@ import { Classroom } from '../entities/classroom.entity'; import { ClassSchedule } from '../entities/class-schedule.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity'; import { Class } from '../entities/class.entity'; +import dayjs from '../common/dayjs'; import { Deposit } from '../entities/deposit.entity'; import { ClassroomRental } from '../entities/classroom-rental.entity'; import { ClassTeacher } from '../entities/class-teacher.entity'; @@ -277,9 +278,7 @@ export class DashboardService { } private nextMonth(ym: string): string { - const d = new Date(`${ym}-01`); - d.setMonth(d.getMonth() + 1); - return d.toISOString().slice(0, 7) + '-01'; + return dayjs.utc(`${ym}-01`).add(1, 'month').format('YYYY-MM-DD'); } async getClassroomOccupancy() { @@ -326,16 +325,7 @@ export class DashboardService { } private getChinaDate(date: Date): string { - const parts = new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).formatToParts(date); - const values = Object.fromEntries( - parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]), - ); - return `${values.year}-${values.month}-${values.day}`; + return dayjs(date).utcOffset(8).format('YYYY-MM-DD'); } async getClassroomUtilizationStats() { diff --git a/apps/server/src/database/date-normalization.ts b/apps/server/src/database/date-normalization.ts index 2184847..5717b61 100644 --- a/apps/server/src/database/date-normalization.ts +++ b/apps/server/src/database/date-normalization.ts @@ -1,3 +1,4 @@ +import dayjs from '../common/dayjs'; const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/; const ISO_DATE_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})T/; @@ -8,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 date.toISOString().slice(0, 10); + if (!Number.isNaN(date.getTime())) return dayjs(date).utc().format('YYYY-MM-DD'); } throw new Error(`无效日期格式: ${value}`); diff --git a/apps/server/src/expenses/expense-operations.service.ts b/apps/server/src/expenses/expense-operations.service.ts index 3dbcf1a..f51c177 100644 --- a/apps/server/src/expenses/expense-operations.service.ts +++ b/apps/server/src/expenses/expense-operations.service.ts @@ -5,6 +5,7 @@ import { DataSource, In, Repository } from 'typeorm'; import { RoomExpense, PersonalExpense, Room, Student } from '../entities'; import { BillsService } from '../bills/bills.service'; import { RoomsService } from '../rooms/rooms.service'; +import dayjs from '../common/dayjs'; import type { CreatePersonalExpenseDto } from './dto/expense.dto'; @Injectable() @@ -28,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()) && date.toISOString().slice(0, 10) === value; + return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value; } async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) { diff --git a/apps/server/src/expenses/expenses.service.ts b/apps/server/src/expenses/expenses.service.ts index 1b0d666..53fc9c0 100644 --- a/apps/server/src/expenses/expenses.service.ts +++ b/apps/server/src/expenses/expenses.service.ts @@ -12,6 +12,7 @@ import { CreateStudentUtilityBillDto, } from './dto/expense.dto'; import { BillsService } from '../bills/bills.service'; +import dayjs from '../common/dayjs'; import { ExpenseOperationsService } from './expense-operations.service'; /** getRawMany 返回的原始行:数据库标量值(string/number/Date)或 NULL */ @@ -341,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()) && date.toISOString().slice(0, 10) === value; + return !Number.isNaN(date.getTime()) && dayjs(date).utc().format('YYYY-MM-DD') === value; } async createStudentUtilityBill(dto: CreateStudentUtilityBillDto, userId?: number) { diff --git a/apps/server/src/integration/dingtalk.attendance.ts b/apps/server/src/integration/dingtalk.attendance.ts index edc75ee..afe9545 100644 --- a/apps/server/src/integration/dingtalk.attendance.ts +++ b/apps/server/src/integration/dingtalk.attendance.ts @@ -2,6 +2,7 @@ import type { DingTalkAttendanceResult, DingTalkServiceContext, } from './dingtalk.types'; +import dayjs from '../common/dayjs'; export class DingTalkAttendanceClient { constructor(private readonly context: DingTalkServiceContext) {} @@ -52,7 +53,7 @@ export class DingTalkAttendanceClient { return records.map((r) => ({ userId: r.userId, userName: '', - workDate: new Date(r.workDate + 8 * 60 * 60 * 1000).toISOString().slice(0, 10), + workDate: dayjs(r.workDate).utcOffset(8).format('YYYY-MM-DD'), timeResult: r.timeResult ?? r.sourceType ?? '', locationResult: r.locationResult ?? r.locationMethod ?? r.userAddress ?? '', planCheckTime: '', diff --git a/apps/server/src/occupancies/occupancy-import.service.ts b/apps/server/src/occupancies/occupancy-import.service.ts index 0fe34f2..b6eea7e 100644 --- a/apps/server/src/occupancies/occupancy-import.service.ts +++ b/apps/server/src/occupancies/occupancy-import.service.ts @@ -2,6 +2,7 @@ import { Injectable, BadRequestException } from '@nestjs/common'; import { DataSource, IsNull, DeepPartial } from 'typeorm'; import { Occupancy, Room, Student, Deposit, Bed, Locker, Organization } from '../entities'; import { RoomsService } from '../rooms/rooms.service'; +import dayjs from '../common/dayjs'; class ImportRowSkipped extends Error {} @@ -132,7 +133,7 @@ export class OccupancyImportService { ); } - const checkInDate = row.checkInDate?.trim() || new Date().toISOString().split('T')[0]; + const checkInDate = row.checkInDate?.trim() || dayjs().utc().format('YYYY-MM-DD'); const checkOutDate = row.checkOutDate?.trim(); const billingStartDate = row.billingStartDate?.trim() || checkInDate; const isHistoricalRecord = Boolean(checkOutDate); diff --git a/apps/server/src/occupancies/occupancy-operations.service.ts b/apps/server/src/occupancies/occupancy-operations.service.ts index 749809c..f0c503e 100644 --- a/apps/server/src/occupancies/occupancy-operations.service.ts +++ b/apps/server/src/occupancies/occupancy-operations.service.ts @@ -6,6 +6,7 @@ import { Room } from '../entities/room.entity'; import { Student } from '../entities/student.entity'; import { Bed } from '../entities/bed.entity'; import { Locker } from '../entities/locker.entity'; +import dayjs from '../common/dayjs'; import { Deposit } from '../entities/deposit.entity'; import { Organization } from '../entities/organization.entity'; import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity'; @@ -158,7 +159,7 @@ export class OccupancyOperationsService { const transferDate = new Date(dto.transferDate); const nextDay = new Date(transferDate); nextDay.setDate(nextDay.getDate() + 1); - const defaultBillingStart = nextDay.toISOString().split('T')[0]; + const defaultBillingStart = dayjs(nextDay).utc().format('YYYY-MM-DD'); this.assertDateOrder( dto.transferDate, dto.newBillingStartDate || defaultBillingStart, diff --git a/apps/server/src/rooms/room-inspections.service.ts b/apps/server/src/rooms/room-inspections.service.ts index 6b8d476..4fadab1 100644 --- a/apps/server/src/rooms/room-inspections.service.ts +++ b/apps/server/src/rooms/room-inspections.service.ts @@ -8,6 +8,7 @@ import { RoomInspection } from '../entities/room-inspection.entity'; import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { occupancyWhereOnDate } from './room-occupancy-date'; +import dayjs from '../common/dayjs'; interface InspectorIdentity { id?: number; @@ -239,17 +240,10 @@ export class RoomInspectionsService implements OnApplicationBootstrap { } private getChinaDate(now: Date): string { - return new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(now); + return dayjs(now).utcOffset(8).format('YYYY-MM-DD'); } private shiftDate(date: string, days: number): string { - const shifted = new Date(`${date}T12:00:00Z`); - shifted.setUTCDate(shifted.getUTCDate() + days); - return shifted.toISOString().slice(0, 10); + return dayjs.utc(date).add(days, 'day').format('YYYY-MM-DD'); } } diff --git a/apps/server/src/rooms/room-query.service.ts b/apps/server/src/rooms/room-query.service.ts index 92f3ba1..e5fddb2 100644 --- a/apps/server/src/rooms/room-query.service.ts +++ b/apps/server/src/rooms/room-query.service.ts @@ -7,6 +7,7 @@ import { Bed } from '../entities/bed.entity'; import { RoomInspectionsService } from './room-inspections.service'; import { occupancyWhereOnDate } from './room-occupancy-date'; import { parseRoomNumber } from './room-number'; +import dayjs from '../common/dayjs'; /** getRawMany 原始行:驱动可能返回 string 或 number,故标量字段用联合类型 */ interface RoomSearchRawRow { @@ -247,12 +248,7 @@ export class RoomQueryService { } private getChinaDate(now: Date): string { - return new Intl.DateTimeFormat('en-CA', { - timeZone: 'Asia/Shanghai', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }).format(now); + return dayjs(now).utcOffset(8).format('YYYY-MM-DD'); } diff --git a/apps/server/src/sync/schedule-sync.helpers.ts b/apps/server/src/sync/schedule-sync.helpers.ts index 5728151..559cbee 100644 --- a/apps/server/src/sync/schedule-sync.helpers.ts +++ b/apps/server/src/sync/schedule-sync.helpers.ts @@ -1,6 +1,7 @@ import { Repository, In } from 'typeorm'; import { ClassSchedule, ClassStudent, StudentDingMapping, Class } from '../entities'; import type { DingTalkScheduleItem } from '../integration/dingtalk.service'; +import dayjs from '../common/dayjs'; export interface DailySchedulePeriod { startTime: string; @@ -97,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 = date.toISOString().slice(0, 10); + const dateStr = dayjs(date).utc().format('YYYY-MM-DD'); const weekDay = date.getUTCDay() === 0 ? 7 : date.getUTCDay(); for (const schedule of schedules) { @@ -179,7 +180,5 @@ export function minutesBetween(startTime: string, endTime: string): number { } export function addDays(dateStr: string, days: number): string { - const d = new Date(`${dateStr}T00:00:00.000Z`); - d.setUTCDate(d.getUTCDate() + days); - return d.toISOString().slice(0, 10); + return dayjs.utc(dateStr).add(days, 'day').format('YYYY-MM-DD'); } diff --git a/apps/server/src/sync/schedule-sync.service.ts b/apps/server/src/sync/schedule-sync.service.ts index a3c4d59..8fa3f8c 100644 --- a/apps/server/src/sync/schedule-sync.service.ts +++ b/apps/server/src/sync/schedule-sync.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ClassSchedule, ClassStudent, StudentDingMapping, Class } from '../entities'; import { DingTalkService } from '../integration/dingtalk.service'; +import dayjs from '../common/dayjs'; import { buildClassDingUserMap, loadClassNames, @@ -45,7 +46,7 @@ export class ScheduleSyncService { opUserId = 'manager', attendanceMachineOnly = false, ): Promise { - const startDate = dateFrom || new Date().toISOString().slice(0, 10); + const startDate = dateFrom || dayjs().utc().format('YYYY-MM-DD'); const normalizedDays = Number.isFinite(days) ? Math.max(1, Math.floor(days)) : 30; const endDate = addDays(startDate, normalizedDays - 1); diff --git a/apps/server/src/sync/sync.service.ts b/apps/server/src/sync/sync.service.ts index b953de6..ed6ed2f 100644 --- a/apps/server/src/sync/sync.service.ts +++ b/apps/server/src/sync/sync.service.ts @@ -10,6 +10,7 @@ import { WeComService } from '../integration/wecom.service'; import { JinshujuService } from '../integration/jinshuju.service'; import { syncJinshujuStudents } from '../integration/jinshuju-student-sync'; import { ScheduleSyncService } from './schedule-sync.service'; +import dayjs from '../common/dayjs'; import { SyncRunner } from './sync-runner'; import { getMatchRule, validateMatchRule, extractField } from './jinshuju-rules'; @@ -70,8 +71,8 @@ export class SyncService { } const result = await this.attendanceImportService.importFromDingTalk({ - startDate: startDate.toISOString().slice(0, 10), - endDate: endDate.toISOString().slice(0, 10), + startDate: dayjs(startDate).utc().format('YYYY-MM-DD'), + endDate: dayjs(endDate).utc().format('YYYY-MM-DD'), userIds, autoMatch: true, }); @@ -304,7 +305,7 @@ export class SyncService { } async getScheduleSyncStatus(date?: string) { - return this.scheduleSyncService.getStatus(date || new Date().toISOString().slice(0, 10)); + return this.scheduleSyncService.getStatus(date || dayjs().utc().format('YYYY-MM-DD')); } /**