From 33e8f283544d74c5ca524103139138c1ad4a762a Mon Sep 17 00:00:00 2001 From: sunlei Date: Mon, 27 Jul 2026 16:25:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AF=B9=E9=BD=90=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=88=97=E9=BB=98=E8=AE=A4=E5=80=BC=E7=B2=BE?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 2 +- README.md | 2 +- .../decorators/kt-date-time.decorator.ts | 26 ++++++++- test/common/kt-date-time.decorator.spec.ts | 53 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index 561869e..7bf2fb7 100644 --- a/API.md +++ b/API.md @@ -47,7 +47,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管 ### ID 与时间 - 后台主键使用 Snowflake 数字 ID,接口按字符串返回,避免 JavaScript 长整型精度丢失。 -- 后端格式化时间字段统一使用 `KtDateTime extends Date`:Entity 通过 `@KtDateTimeColumn(format)`、`@KtCreateDateColumn(format)`、`@KtUpdateDateColumn(format)` 在 TypeORM hydrate 边界转换;DTO/外部数据源通过 `@KtDateTimeField(format)` + `transformKtDateTimeFields()` 转换。默认输出 `YYYY-MM-DD HH:mm:ss`,可在装饰器中传入格式字符串;响应包装不做递归遍历。 +- 后端格式化时间字段统一使用 `KtDateTime extends Date`:Entity 通过 `@KtDateTimeColumn(format)`、`@KtCreateDateColumn(format)`、`@KtUpdateDateColumn(format)` 在 TypeORM hydrate 边界转换;DTO/外部数据源通过 `@KtDateTimeField(format)` + `transformKtDateTimeFields()` 转换。默认输出 `YYYY-MM-DD HH:mm:ss`,可在装饰器中传入格式字符串;Create/Update 列显式配置 `precision` 时自动生成同精度 `CURRENT_TIMESTAMP(n)`,调用方显式 `default/onUpdate` 保持优先;响应包装不做递归遍历。 - `POST */save` 默认会删除请求体里的 `id`,防止新增接口误用前端主键。 ## Runtime Health diff --git a/README.md b/README.md index db9b57e..f06fd9b 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow ## 核心规则 - 后台主键使用 Snowflake 数字 ID,数据库字段为 `BIGINT`,接口按字符串返回。 -- 后端响应时间统一用 `KtDateTime extends Date` 承接序列化语义;Entity 使用 `@KtDateTimeColumn(format)`、`@KtCreateDateColumn(format)`、`@KtUpdateDateColumn(format)` 在 TypeORM hydrate 边界转换,DTO/外部数据源使用 `@KtDateTimeField(format)` + `transformKtDateTimeFields()` 转换,默认格式为 `YYYY-MM-DD HH:mm:ss`。`vbenSuccess` / `ToolsService.res` 不做全量递归格式化。 +- 后端响应时间统一用 `KtDateTime extends Date` 承接序列化语义;Entity 使用 `@KtDateTimeColumn(format)`、`@KtCreateDateColumn(format)`、`@KtUpdateDateColumn(format)` 在 TypeORM hydrate 边界转换,DTO/外部数据源使用 `@KtDateTimeField(format)` + `transformKtDateTimeFields()` 转换,默认格式为 `YYYY-MM-DD HH:mm:ss`。Create/Update 列显式配置 `precision` 时,公共装饰器会在调用方未覆盖的前提下生成同精度 `CURRENT_TIMESTAMP(n)` 默认值与更新表达式,避免 MySQL 列精度不匹配;`vbenSuccess` / `ToolsService.res` 不做全量递归格式化。 - 字典维护在 `admin_dict`,Admin 字典管理按 `dictCode` 分组展示;可运营映射优先走字典或静态配置,不硬编码到业务函数。 - 全局 `SaveBodyInterceptor` 会删除 `POST */save` 请求体里的 `id`;需要保留时使用 `@SkipSaveBodyNormalize()`。 - Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管理接口默认走 `JwtAuthGuard`;公开接口用 `@Public()`。 diff --git a/src/common/decorators/kt-date-time.decorator.ts b/src/common/decorators/kt-date-time.decorator.ts index 57ab3b7..f1e3ae0 100644 --- a/src/common/decorators/kt-date-time.decorator.ts +++ b/src/common/decorators/kt-date-time.decorator.ts @@ -201,8 +201,9 @@ export function KtCreateDateColumn( options?: ColumnOptions, ): PropertyDecorator { const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options); + const columnOptions = applyCurrentTimestampPrecision(normalized.options); return CreateDateColumn({ - ...normalized.options, + ...columnOptions, transformer: mergeDateTimeTransformer( normalized.options.transformer, normalized.format, @@ -248,8 +249,12 @@ export function KtUpdateDateColumn( options?: ColumnOptions, ): PropertyDecorator { const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options); + const columnOptions = applyCurrentTimestampPrecision( + normalized.options, + true, + ); return UpdateDateColumn({ - ...normalized.options, + ...columnOptions, transformer: mergeDateTimeTransformer( normalized.options.transformer, normalized.format, @@ -311,6 +316,23 @@ function normalizeDateTimeColumnOptions( }; } +function applyCurrentTimestampPrecision( + options: ColumnOptions, + includeOnUpdate = false, +): ColumnOptions { + if (options.precision === undefined) return options; + + const currentTimestamp = `CURRENT_TIMESTAMP(${options.precision})`; + return { + ...options, + default: + options.default === undefined ? () => currentTimestamp : options.default, + ...(includeOnUpdate && options.onUpdate === undefined + ? { onUpdate: currentTimestamp } + : {}), + }; +} + /** * 合并Date Time Transformer。 * @param existing - existing 输入;驱动 `Array.isArray()` 的 公共基础设施步骤。 diff --git a/test/common/kt-date-time.decorator.spec.ts b/test/common/kt-date-time.decorator.spec.ts index a497d42..24bd876 100644 --- a/test/common/kt-date-time.decorator.spec.ts +++ b/test/common/kt-date-time.decorator.spec.ts @@ -27,6 +27,28 @@ class DateTimeColumnFixture { @KtUpdateDateColumn('YYYY-MM-DD') updateTime!: KtDateTime; + + @KtCreateDateColumn({ precision: 3, type: 'datetime' }) + preciseCreateTime!: KtDateTime; + + @KtUpdateDateColumn({ precision: 3, type: 'datetime' }) + preciseUpdateTime!: KtDateTime; + + @KtCreateDateColumn({ + default: null, + nullable: true, + precision: 3, + type: 'datetime', + }) + customCreateTime!: KtDateTime | null; + + @KtUpdateDateColumn({ + default: () => 'CUSTOM_DEFAULT', + onUpdate: 'CUSTOM_ON_UPDATE', + precision: 3, + type: 'datetime', + }) + customUpdateTime!: KtDateTime; } describe('KtDateTime decorators', () => { @@ -77,6 +99,37 @@ describe('KtDateTime decorators', () => { expect(String(updateTime)).toBe('2026-05-13'); }); + it('matches generated current timestamp defaults to explicit column precision', () => { + const metadata = getMetadataArgsStorage().columns.filter( + (column) => column.target === DateTimeColumnFixture, + ); + const createOptions = metadata.find( + (column) => column.propertyName === 'preciseCreateTime', + )?.options; + const updateOptions = metadata.find( + (column) => column.propertyName === 'preciseUpdateTime', + )?.options; + const customCreateOptions = metadata.find( + (column) => column.propertyName === 'customCreateTime', + )?.options; + const customUpdateOptions = metadata.find( + (column) => column.propertyName === 'customUpdateTime', + )?.options; + + expect((createOptions?.default as () => string)()).toBe( + 'CURRENT_TIMESTAMP(3)', + ); + expect((updateOptions?.default as () => string)()).toBe( + 'CURRENT_TIMESTAMP(3)', + ); + expect(updateOptions?.onUpdate).toBe('CURRENT_TIMESTAMP(3)'); + expect(customCreateOptions?.default).toBeNull(); + expect((customUpdateOptions?.default as () => string)()).toBe( + 'CUSTOM_DEFAULT', + ); + expect(customUpdateOptions?.onUpdate).toBe('CUSTOM_ON_UPDATE'); + }); + it('keeps DTO transformer datetime fields usable as Date values', () => { const data = new DateTimeFormatFixture(); data.createTime = new Date(2026, 4, 13, 10, 30, 0);