refactor(server): 用已装库替换手写工具
- uuid v7 替换手写 RFC9562 实现(uuid@11.1.1 转为直接依赖) - node:timers/promises setTimeout 替换两处手写 sleep(钉钉限流、AI 上游重试退避) - dayjs 替换 4 处零散 Date 格式化(imports/expenses/occupancies 导入模板、档案报告日期)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
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 服务请求过于频繁';
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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)}`;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 : '';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import dayjs from '../common/dayjs';
|
||||
import type { CellValue } from './imports.types';
|
||||
|
||||
export function parseJson<T>(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;
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})/);
|
||||
|
||||
@@ -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';
|
||||
|
||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user