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:
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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')
|
||||
: '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user