refactor(server): 日期工具统一迁入 dayjs(utc 插件),移除手写 Intl/padStart 重复实现
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"dayjs": "^1.11.21",
|
||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import dayjs from '../common/dayjs';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import type { AgentToolContext } from '../agent-tools/agent-tool.types';
|
import type { AgentToolContext } from '../agent-tools/agent-tool.types';
|
||||||
import type { StudentAccessScope } from '../students/student-access-scope';
|
import type { StudentAccessScope } from '../students/student-access-scope';
|
||||||
@@ -55,20 +56,11 @@ function teacherScopedCountSql(sql: (scope: StudentAccessScope) => string): SqlB
|
|||||||
}
|
}
|
||||||
|
|
||||||
function today(): string {
|
function today(): string {
|
||||||
const now = new Date();
|
return dayjs().format('YYYY-MM-DD');
|
||||||
const year = now.getFullYear();
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(now.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function inDays(days: number): string {
|
function inDays(days: number): string {
|
||||||
const date = new Date();
|
return dayjs().add(days, 'day').format('YYYY-MM-DD');
|
||||||
date.setDate(date.getDate() + days);
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TASKS: readonly TaskDefinition[] = [
|
const TASKS: readonly TaskDefinition[] = [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ScheduleType,
|
ScheduleType,
|
||||||
} from '../entities';
|
} from '../entities';
|
||||||
import { toMinutes, isClassStudentActiveOnDate } from './attendance-time';
|
import { toMinutes, isClassStudentActiveOnDate } from './attendance-time';
|
||||||
|
import dayjs from '../common/dayjs';
|
||||||
import type { BatchCreateAttendanceDto, GenerateAttendanceFromSchedulesDto, GenerateFromSchedulesDto, SaveAttendancePeriodConfigsDto } from './dto/attendance.dto';
|
import type { BatchCreateAttendanceDto, GenerateAttendanceFromSchedulesDto, GenerateFromSchedulesDto, SaveAttendancePeriodConfigsDto } from './dto/attendance.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -162,23 +163,15 @@ export class AttendanceGenerationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCourseClock(date: Date): { date: string; minutes: number } {
|
private getCourseClock(date: Date): { date: string; minutes: number } {
|
||||||
const parts = Object.fromEntries(
|
const c = dayjs.utc(date).add(8, 'hour');
|
||||||
new Intl.DateTimeFormat('en-CA', {
|
|
||||||
timeZone: 'Asia/Shanghai',
|
|
||||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
||||||
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
||||||
}).formatToParts(date).filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
date: `${parts.year}-${parts.month}-${parts.day}`,
|
date: c.format('YYYY-MM-DD'),
|
||||||
minutes: Number(parts.hour) * 60 + Number(parts.minute),
|
minutes: c.hour() * 60 + c.minute(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private shiftDate(date: string, days: number): string {
|
private shiftDate(date: string, days: number): string {
|
||||||
const shifted = new Date(`${date}T00:00:00.000Z`);
|
return dayjs.utc(`${date}T00:00:00.000Z`).add(days, 'day').format('YYYY-MM-DD');
|
||||||
shifted.setUTCDate(shifted.getUTCDate() + days);
|
|
||||||
return shifted.toISOString().slice(0, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureAttendancePeriodConfigs() {
|
private async ensureAttendancePeriodConfigs() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import dayjs from '../common/dayjs';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, LessThan, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';
|
import { In, LessThan, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';
|
||||||
@@ -283,18 +284,13 @@ export class AttendanceSettlementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCourseClock(date: Date): { date: string; weekDay: number; minutes: number } {
|
private getCourseClock(date: Date): { date: string; weekDay: number; minutes: number } {
|
||||||
const parts = Object.fromEntries(
|
// Asia/Shanghai 无夏令时,固定 UTC+8 与 Intl 时区格式化等价
|
||||||
new Intl.DateTimeFormat('en-CA', {
|
const c = dayjs.utc(date).add(8, 'hour');
|
||||||
timeZone: this.courseTimeZone,
|
const weekDay = c.day() === 0 ? 7 : c.day();
|
||||||
year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'short',
|
|
||||||
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
||||||
}).formatToParts(date).filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
|
|
||||||
);
|
|
||||||
const weekDays: Record<string, number> = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
|
|
||||||
return {
|
return {
|
||||||
date: `${parts.year}-${parts.month}-${parts.day}`,
|
date: c.format('YYYY-MM-DD'),
|
||||||
weekDay: weekDays[parts.weekday],
|
weekDay,
|
||||||
minutes: Number(parts.hour) * 60 + Number(parts.minute),
|
minutes: c.hour() * 60 + c.minute(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
|
import dayjs from '../common/dayjs';
|
||||||
|
|
||||||
export function toMinutes(time: string): number {
|
export function toMinutes(time: string): number {
|
||||||
const [hour, minute] = time.split(':').map(Number);
|
const [hour, minute] = time.split(':').map(Number);
|
||||||
return hour * 60 + minute;
|
return hour * 60 + minute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCourseClock(date: Date): { date: string; minutes: number } {
|
export function getCourseClock(date: Date): { date: string; minutes: number } {
|
||||||
const year = date.getFullYear();
|
const d = dayjs(date);
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return {
|
return {
|
||||||
date: `${year}-${month}-${day}`,
|
date: d.format('YYYY-MM-DD'),
|
||||||
minutes: date.getHours() * 60 + date.getMinutes(),
|
minutes: d.hour() * 60 + d.minute(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shiftDate(date: string, days: number): string {
|
export function shiftDate(date: string, days: number): string {
|
||||||
const d = new Date(`${date}T00:00:00.000Z`);
|
return dayjs.utc(`${date}T00:00:00.000Z`).add(days, 'day').format('YYYY-MM-DD');
|
||||||
d.setUTCDate(d.getUTCDate() + days);
|
|
||||||
return d.toISOString().slice(0, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapLessonScheduleTimeToSession(startTime: string): string {
|
export function mapLessonScheduleTimeToSession(startTime: string): string {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import dayjs from '../common/dayjs';
|
||||||
import { AttendanceService } from './attendance.service';
|
import { AttendanceService } from './attendance.service';
|
||||||
import { AttendanceImportService } from './attendance-import.service';
|
import { AttendanceImportService } from './attendance-import.service';
|
||||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||||
@@ -32,11 +33,7 @@ export abstract class AttendanceControllerBase {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
protected getTodayDateOnly(): string {
|
protected getTodayDateOnly(): string {
|
||||||
const today = new Date();
|
return dayjs().format('YYYY-MM-DD');
|
||||||
const year = today.getFullYear();
|
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageAllAttendance(req: { user: RequestUser }): boolean {
|
protected canManageAllAttendance(req: { user: RequestUser }): boolean {
|
||||||
|
|||||||
7
apps/server/src/common/dayjs.ts
Normal file
7
apps/server/src/common/dayjs.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
|
||||||
|
// 服务端统一 dayjs 实例:扩展 utc 插件以支持固定时区(Asia/Shanghai 无夏令时,恒为 UTC+8)。
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
export default dayjs;
|
||||||
@@ -144,33 +144,13 @@ export const DEPRECATED_PERMISSION_CODES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const DEPRECATED_PERMISSION_CODE_SET = new Set<string>(DEPRECATED_PERMISSION_CODES);
|
export const DEPRECATED_PERMISSION_CODE_SET = new Set<string>(DEPRECATED_PERMISSION_CODES);
|
||||||
|
import dayjs from '../common/dayjs';
|
||||||
|
|
||||||
export function getChinaDateParts(date = new Date()): { date: string; weekDay: number } {
|
export function getChinaDateParts(date = new Date()): { date: string; weekDay: number } {
|
||||||
const parts = Object.fromEntries(
|
// Asia/Shanghai 无夏令时,固定 UTC+8 与 Intl en-CA 格式化等价
|
||||||
new Intl.DateTimeFormat('en-CA', {
|
const c = dayjs.utc(date).add(8, 'hour');
|
||||||
timeZone: 'Asia/Shanghai',
|
const weekDay = c.day() === 0 ? 7 : c.day();
|
||||||
year: 'numeric',
|
return { date: c.format('YYYY-MM-DD'), weekDay };
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
weekday: 'short',
|
|
||||||
})
|
|
||||||
.formatToParts(date)
|
|
||||||
.filter((part) => part.type !== 'literal')
|
|
||||||
.map((part) => [part.type, part.value]),
|
|
||||||
);
|
|
||||||
const weekDays: Record<string, number> = {
|
|
||||||
Mon: 1,
|
|
||||||
Tue: 2,
|
|
||||||
Wed: 3,
|
|
||||||
Thu: 4,
|
|
||||||
Fri: 5,
|
|
||||||
Sat: 6,
|
|
||||||
Sun: 7,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
date: `${parts.year}-${parts.month}-${parts.day}`,
|
|
||||||
weekDay: weekDays[parts.weekday],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRESET_ROLES: Array<{
|
export const PRESET_ROLES: Array<{
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -108,6 +108,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"dayjs": "^1.11.21",
|
||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user