From 92ba2e779f1c039113633ea219d9c48578d48429 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sat, 8 Aug 2026 17:46:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server):=20=E7=94=A8=E5=B7=B2=E8=A3=85?= =?UTF-8?q?=E5=BA=93=E6=9B=BF=E6=8D=A2=E6=89=8B=E5=86=99=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uuid v7 替换手写 RFC9562 实现(uuid@11.1.1 转为直接依赖) - node:timers/promises setTimeout 替换两处手写 sleep(钉钉限流、AI 上游重试退避) - dayjs 替换 4 处零散 Date 格式化(imports/expenses/occupancies 导入模板、档案报告日期) --- apps/server/package.json | 3 ++- apps/server/src/ai-chat/ai-chart.service.ts | 2 +- apps/server/src/ai-chat/ai-form.service.ts | 2 +- .../ai-chat/ai-model-stream.service.spec.ts | 7 ++++--- .../src/ai-chat/ai-model-stream.service.ts | 9 +++----- .../src/archive/archive-report.service.ts | 7 ++----- apps/server/src/common/uuid-v7.spec.ts | 11 ---------- apps/server/src/common/uuid-v7.ts | 21 ------------------- .../database/database-migrations.backfill.ts | 2 +- .../src/expenses/expenses.controller.ts | 6 ++---- apps/server/src/imports/imports.helpers.ts | 6 ++---- .../src/integration/dingtalk.service.ts | 6 ++---- .../occupancies/occupancy-import-template.ts | 6 ++---- .../organizations/organizations.service.ts | 2 +- package-lock.json | 3 ++- 15 files changed, 25 insertions(+), 68 deletions(-) delete mode 100644 apps/server/src/common/uuid-v7.spec.ts delete mode 100644 apps/server/src/common/uuid-v7.ts diff --git a/apps/server/package.json b/apps/server/package.json index 274eabe..896065c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -62,7 +62,8 @@ "pino": "^10.3.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.31" + "typeorm": "^0.3.31", + "uuid": "^11.1.1" }, "devDependencies": { "@eslint/js": "^9.18.0", diff --git a/apps/server/src/ai-chat/ai-chart.service.ts b/apps/server/src/ai-chat/ai-chart.service.ts index bfbb112..7868887 100644 --- a/apps/server/src/ai-chat/ai-chart.service.ts +++ b/apps/server/src/ai-chat/ai-chart.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { uuidV7 } from '../common/uuid-v7'; +import { v7 as uuidV7 } from 'uuid'; import type { AiReviewColumn, AiReviewRow } from './entities/ai-review.entity'; import { assertKeys, isPlainRecord, requireString } from './ai-validation'; diff --git a/apps/server/src/ai-chat/ai-form.service.ts b/apps/server/src/ai-chat/ai-form.service.ts index 74e3b2e..0983e8c 100644 --- a/apps/server/src/ai-chat/ai-form.service.ts +++ b/apps/server/src/ai-chat/ai-form.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { uuidV7 } from '../common/uuid-v7'; +import { v7 as uuidV7 } from 'uuid'; import { AiForm, type AiFormField } from './entities/ai-form.entity'; import { isPlainRecord, requireString } from './ai-validation'; diff --git a/apps/server/src/ai-chat/ai-model-stream.service.spec.ts b/apps/server/src/ai-chat/ai-model-stream.service.spec.ts index 9bf9155..b4a6414 100644 --- a/apps/server/src/ai-chat/ai-model-stream.service.spec.ts +++ b/apps/server/src/ai-chat/ai-model-stream.service.spec.ts @@ -1,6 +1,10 @@ import { AiModelStreamService } from './ai-model-stream.service'; import type { AiRuntimeConfig } from '../ai-config/dto/ai-config.dto'; +jest.mock('node:timers/promises', () => ({ + setTimeout: jest.fn().mockResolvedValue(undefined), +})); + const config: AiRuntimeConfig = { provider: 'DEEPSEEK' as AiRuntimeConfig['provider'], baseUrl: 'https://example.test/v1', @@ -57,7 +61,6 @@ describe('AiModelStreamService', () => { contentType: 'text/plain', body: body(), } as never); - jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never); const consume = async () => { for await (const _ of service.stream( config, @@ -90,7 +93,6 @@ describe('AiModelStreamService', () => { body: successBody(), } as never; }); - jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never); const events: Array<{ type: string; attempt?: number; maxRetries?: number; reason?: string }> = []; for await (const event of service.stream( config, @@ -116,7 +118,6 @@ describe('AiModelStreamService', () => { contentType: 'text/plain', body: body(), } as never); - jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never); const consume = async () => { for await (const _ of service.stream( config, diff --git a/apps/server/src/ai-chat/ai-model-stream.service.ts b/apps/server/src/ai-chat/ai-model-stream.service.ts index 018cd45..fa60d61 100644 --- a/apps/server/src/ai-chat/ai-model-stream.service.ts +++ b/apps/server/src/ai-chat/ai-model-stream.service.ts @@ -3,6 +3,7 @@ import { lookup } from 'node:dns'; import * as http from 'node:http'; import * as https from 'node:https'; import { isIP } from 'node:net'; +import { setTimeout as sleep } from 'node:timers/promises'; import type { AiRuntimeConfig } from '../ai-config/dto/ai-config.dto'; import { AiProvider } from '../ai-config/ai-config.entity'; import type { ModelMessage, ModelStreamEvent } from './ai-chat.types'; @@ -110,7 +111,7 @@ export class AiModelStreamService { delayMs, reason: error instanceof Error ? error.message : '网络连接失败', }; - await this.sleep(delayMs); + await sleep(delayMs); continue; } throw error; @@ -127,7 +128,7 @@ export class AiModelStreamService { delayMs, reason: `上游返回 ${response.status}`, }; - await this.sleep(delayMs); + await sleep(delayMs); continue; } const body = await this.readLimitedBody(response.body); @@ -220,10 +221,6 @@ export class AiModelStreamService { return /socket hang up|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(error.message); } - private sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } - private safeUpstreamMessage(status: number, body: string): string { if (status === 401 || status === 403) return 'AI 服务认证失败'; if (status === 429) return 'AI 服务请求过于频繁'; diff --git a/apps/server/src/archive/archive-report.service.ts b/apps/server/src/archive/archive-report.service.ts index 5542940..25ebfe1 100644 --- a/apps/server/src/archive/archive-report.service.ts +++ b/apps/server/src/archive/archive-report.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import dayjs from '../common/dayjs'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { StudentProfile } from '../entities/student-profile.entity'; @@ -65,11 +66,7 @@ export class ArchiveReportService { private buildHtml(data: ReportData): string { const { student, profile, enrollments, exams, learnings, result, attendances } = data; const name = student.name; - const now = new Date().toLocaleDateString('zh-CN', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); + const now = dayjs().format('YYYY年M月D日'); const sections = [ { diff --git a/apps/server/src/common/uuid-v7.spec.ts b/apps/server/src/common/uuid-v7.spec.ts deleted file mode 100644 index ae6a03a..0000000 --- a/apps/server/src/common/uuid-v7.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { uuidV7 } from './uuid-v7'; - -describe('uuidV7', () => { - it('creates an RFC 9562 version 7 UUID with time-sortable prefixes', () => { - const first = uuidV7(1_700_000_000_000); - const second = uuidV7(1_700_000_000_001); - - expect(first).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); - expect(first < second).toBe(true); - }); -}); diff --git a/apps/server/src/common/uuid-v7.ts b/apps/server/src/common/uuid-v7.ts deleted file mode 100644 index 966c73c..0000000 --- a/apps/server/src/common/uuid-v7.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { randomBytes } from 'node:crypto'; - -/** Generates an RFC 9562 UUIDv7 using the current Unix timestamp and cryptographic randomness. */ -export function uuidV7(now = Date.now()): string { - const bytes = Buffer.alloc(16); - const random = randomBytes(10); - let timestamp = BigInt(now); - - for (let index = 5; index >= 0; index -= 1) { - bytes[index] = Number(timestamp & 0xffn); - timestamp >>= 8n; - } - - bytes[6] = 0x70 | (random[0] & 0x0f); - bytes[7] = random[1]; - bytes[8] = 0x80 | (random[2] & 0x3f); - random.copy(bytes, 9, 3, 10); - - const hex = bytes.toString('hex'); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; -} diff --git a/apps/server/src/database/database-migrations.backfill.ts b/apps/server/src/database/database-migrations.backfill.ts index 078404d..e67eace 100644 --- a/apps/server/src/database/database-migrations.backfill.ts +++ b/apps/server/src/database/database-migrations.backfill.ts @@ -1,6 +1,6 @@ import { Logger } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { uuidV7 } from '../common/uuid-v7'; +import { v7 as uuidV7 } from 'uuid'; import { withQueryRunner } from './database-migrations.runner'; import { stringify } from '../common/stringify'; diff --git a/apps/server/src/expenses/expenses.controller.ts b/apps/server/src/expenses/expenses.controller.ts index 60ad909..7886b8c 100644 --- a/apps/server/src/expenses/expenses.controller.ts +++ b/apps/server/src/expenses/expenses.controller.ts @@ -18,6 +18,7 @@ import { ValidationPipe, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; +import dayjs from '../common/dayjs'; import type { Response } from 'express'; import { ExpensesService } from './expenses.service'; import { @@ -92,10 +93,7 @@ function readCell(cell: ExcelJS.Cell | undefined): CellScalar { else return ''; } if (v instanceof Date) { - const y = v.getFullYear(); - const m = String(v.getMonth() + 1).padStart(2, '0'); - const d = String(v.getDate()).padStart(2, '0'); - return `${y}-${m}-${d}`; + return dayjs(v).format('YYYY-MM-DD'); } if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return v; return v == null ? v : ''; diff --git a/apps/server/src/imports/imports.helpers.ts b/apps/server/src/imports/imports.helpers.ts index a4e9fe0..39a4b8a 100644 --- a/apps/server/src/imports/imports.helpers.ts +++ b/apps/server/src/imports/imports.helpers.ts @@ -1,4 +1,5 @@ import * as ExcelJS from 'exceljs'; +import dayjs from '../common/dayjs'; import type { CellValue } from './imports.types'; export function parseJson(raw: string | null | undefined): T | null { @@ -52,10 +53,7 @@ export function cellValue(cell: ExcelJS.Cell | undefined): CellValue { export function parseDateValue(value: CellValue): string | null { if (value instanceof Date && !Number.isNaN(value.getTime())) { - const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, '0'); - const day = String(value.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; + return dayjs(value).format('YYYY-MM-DD'); } const raw = textValue(value); if (!raw) return null; diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index e25c055..c91ee30 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -7,6 +7,7 @@ * - 用户同步(自动建 Student + StudentDingMapping) */ import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'; +import { setTimeout as sleep } from 'node:timers/promises'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Student } from '../entities/student.entity'; @@ -365,7 +366,7 @@ export class DingTalkService implements DingTalkServiceContext { // ═══════════════════════════════════════════ async rateLimit(): Promise { - await this.sleep(DingTalkService.MIN_INTERVAL); + await sleep(DingTalkService.MIN_INTERVAL); this.apiRequestCount++; } @@ -423,7 +424,4 @@ export class DingTalkService implements DingTalkServiceContext { return this.schedules.queryScheduleByUsers(...args); } - private sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } } diff --git a/apps/server/src/occupancies/occupancy-import-template.ts b/apps/server/src/occupancies/occupancy-import-template.ts index 12a00cc..27eb608 100644 --- a/apps/server/src/occupancies/occupancy-import-template.ts +++ b/apps/server/src/occupancies/occupancy-import-template.ts @@ -1,4 +1,5 @@ import * as ExcelJS from 'exceljs'; +import dayjs from '../common/dayjs'; export interface OccupancyImportRow { roomNumber: string; @@ -76,10 +77,7 @@ function parseDate(cell: ExcelJS.Cell | undefined): string { const value = cell?.value; if (!value) return ''; if (value instanceof Date) { - const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, '0'); - const day = String(value.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; + return dayjs(value).format('YYYY-MM-DD'); } const text = cellText(cell); const matched = text.match(/(\d{4})[/\-.](\d{1,2})[/\-.](\d{1,2})/); diff --git a/apps/server/src/organizations/organizations.service.ts b/apps/server/src/organizations/organizations.service.ts index ee38778..71cc479 100644 --- a/apps/server/src/organizations/organizations.service.ts +++ b/apps/server/src/organizations/organizations.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Not, Repository } from 'typeorm'; -import { uuidV7 } from '../common/uuid-v7'; +import { v7 as uuidV7 } from 'uuid'; import { Organization } from '../entities/organization.entity'; import { Student } from '../entities/student.entity'; import { Occupancy } from '../entities/occupancy.entity'; diff --git a/package-lock.json b/package-lock.json index 4d72a44..4316f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,8 @@ "pino": "^10.3.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.31" + "typeorm": "^0.3.31", + "uuid": "^11.1.1" }, "devDependencies": { "@eslint/js": "^9.18.0",