refactor(server): 日期/月份格式化统一改用 dayjs

- 替换散落的 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 个文件
This commit is contained in:
2026-08-08 17:52:32 +08:00
parent 92ba2e779f
commit 8936453e84
28 changed files with 82 additions and 112 deletions

View File

@@ -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<str
return { ok: false, error: `${field} 必须是 YYYY-MM-DD 日期` };
}
const date = new Date(`${value}T00:00:00Z`);
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
if (Number.isNaN(date.getTime()) || dayjs(date).utc().format('YYYY-MM-DD') !== value) {
return { ok: false, error: `${field} 不是有效日期` };
}
return { ok: true, value };

View File

@@ -10,6 +10,7 @@ import type { AiReviewSection } from './entities/ai-review.entity';
import { normalizePhone, toDateString } from './ai-review.shared';
import { importRooms, importStudents, nextDay } from './ai-review.import-basic';
import type { AiReviewSectionResult } from './ai-review.shared';
import dayjs from '../common/dayjs';
export async function importOneSection(
section: AiReviewSection,
@@ -249,7 +250,7 @@ async function importCheckins(
continue;
}
const checkInDate = toDateString(row.checkInDate) ?? new Date().toISOString().slice(0, 10);
const checkInDate = toDateString(row.checkInDate) ?? dayjs().utc().format('YYYY-MM-DD');
const billingStartDate = toDateString(row.billingStartDate) ?? checkInDate;
const checkOutDate = toDateString(row.checkOutDate);
const isHistoricalRecord = Boolean(checkOutDate);

View File

@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
import { AttendanceRecord, ClassSchedule } from '../entities';
import type { AttendanceCalendarQueryDto } from './dto/attendance.dto';
import dayjs from '../common/dayjs';
@Injectable()
export class AttendanceCalendarService {
@@ -23,7 +24,7 @@ export class AttendanceCalendarService {
const diff = day === 0 ? -6 : 1 - day; // Monday offset
const monday = new Date(now);
monday.setDate(now.getDate() + diff);
const mondayStr = monday.toISOString().slice(0, 10);
const mondayStr = dayjs(monday).utc().format('YYYY-MM-DD');
return this.buildCalendar(classId, mondayStr);
}
@@ -77,7 +78,7 @@ export class AttendanceCalendarService {
const start = new Date(weekStart);
const end = new Date(start);
end.setDate(start.getDate() + 6);
const endStr = end.toISOString().slice(0, 10);
const endStr = dayjs(end).utc().format('YYYY-MM-DD');
const records = await this.attendanceRepo.find({
where: {

View File

@@ -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 = d.toISOString().slice(0, 10);
const dateStr = dayjs(d).utc().format('YYYY-MM-DD');
const weekDay = d.getDay() === 0 ? 7 : d.getDay();
for (const sched of schedules) {
@@ -147,8 +147,8 @@ export class AttendanceGenerationService {
sunday.setDate(monday.getDate() + 6);
sunday.setHours(23, 59, 59, 999);
const dateFrom = startDate ?? monday.toISOString().slice(0, 10);
const dateTo = endDate ?? sunday.toISOString().slice(0, 10);
const dateFrom = startDate ?? dayjs(monday).utc().format('YYYY-MM-DD');
const dateTo = endDate ?? dayjs(sunday).utc().format('YYYY-MM-DD');
return this.generateAttendanceFromSchedules({
classId,

View File

@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Subject, Observable } from 'rxjs';
import dayjs from '../common/dayjs';
import {
DingAttendanceRaw,
Student,
@@ -238,7 +239,7 @@ export class AttendanceImportService {
}
private formatDate(value: Date): string {
return value.toISOString().slice(0, 10);
return dayjs.utc(value).format('YYYY-MM-DD');
}
/**

View File

@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DingLeaveRaw, Student, StudentDingMapping } from '../entities';
import { DingTalkService, DingTalkLeaveResult } from '../integration/dingtalk.service';
import dayjs from '../common/dayjs';
/**
* 钉钉请假数据同步服务。
@@ -152,7 +153,7 @@ export class AttendanceLeaveSyncService {
}
private formatDate(value: Date): string {
return value.toISOString().slice(0, 10);
return dayjs.utc(value).format('YYYY-MM-DD');
}
private async resolveStudentName(dingUserId: string): Promise<string> {

View File

@@ -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')
: '',
});
}

View File

@@ -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')

View File

@@ -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');
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 };
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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'));
}
}

View File

@@ -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')

View File

@@ -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() {

View File

@@ -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}`);

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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: '',

View File

@@ -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);

View File

@@ -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,

View File

@@ -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');
}
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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<ScheduleSyncResult> {
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);

View File

@@ -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'));
}
/**