fix: 对齐日期时间列默认值精度

This commit is contained in:
sunlei 2026-07-27 16:25:09 +08:00
parent 1c35fc9dbd
commit 33e8f28354
4 changed files with 79 additions and 4 deletions

2
API.md
View File

@ -47,7 +47,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
### ID 与时间 ### ID 与时间
- 后台主键使用 Snowflake 数字 ID接口按字符串返回避免 JavaScript 长整型精度丢失。 - 后台主键使用 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`,防止新增接口误用前端主键。 - `POST */save` 默认会删除请求体里的 `id`,防止新增接口误用前端主键。
## Runtime Health ## Runtime Health

View File

@ -181,7 +181,7 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
## 核心规则 ## 核心规则
- 后台主键使用 Snowflake 数字 ID数据库字段为 `BIGINT`,接口按字符串返回。 - 后台主键使用 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` 分组展示;可运营映射优先走字典或静态配置,不硬编码到业务函数。 - 字典维护在 `admin_dict`Admin 字典管理按 `dictCode` 分组展示;可运营映射优先走字典或静态配置,不硬编码到业务函数。
- 全局 `SaveBodyInterceptor` 会删除 `POST */save` 请求体里的 `id`;需要保留时使用 `@SkipSaveBodyNormalize()` - 全局 `SaveBodyInterceptor` 会删除 `POST */save` 请求体里的 `id`;需要保留时使用 `@SkipSaveBodyNormalize()`
- Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管理接口默认走 `JwtAuthGuard`;公开接口用 `@Public()` - Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管理接口默认走 `JwtAuthGuard`;公开接口用 `@Public()`

View File

@ -201,8 +201,9 @@ export function KtCreateDateColumn(
options?: ColumnOptions, options?: ColumnOptions,
): PropertyDecorator { ): PropertyDecorator {
const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options); const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options);
const columnOptions = applyCurrentTimestampPrecision(normalized.options);
return CreateDateColumn({ return CreateDateColumn({
...normalized.options, ...columnOptions,
transformer: mergeDateTimeTransformer( transformer: mergeDateTimeTransformer(
normalized.options.transformer, normalized.options.transformer,
normalized.format, normalized.format,
@ -248,8 +249,12 @@ export function KtUpdateDateColumn(
options?: ColumnOptions, options?: ColumnOptions,
): PropertyDecorator { ): PropertyDecorator {
const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options); const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options);
const columnOptions = applyCurrentTimestampPrecision(
normalized.options,
true,
);
return UpdateDateColumn({ return UpdateDateColumn({
...normalized.options, ...columnOptions,
transformer: mergeDateTimeTransformer( transformer: mergeDateTimeTransformer(
normalized.options.transformer, normalized.options.transformer,
normalized.format, 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 * Date Time Transformer
* @param existing - existing `Array.isArray()` * @param existing - existing `Array.isArray()`

View File

@ -27,6 +27,28 @@ class DateTimeColumnFixture {
@KtUpdateDateColumn('YYYY-MM-DD') @KtUpdateDateColumn('YYYY-MM-DD')
updateTime!: KtDateTime; 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', () => { describe('KtDateTime decorators', () => {
@ -77,6 +99,37 @@ describe('KtDateTime decorators', () => {
expect(String(updateTime)).toBe('2026-05-13'); 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', () => { it('keeps DTO transformer datetime fields usable as Date values', () => {
const data = new DateTimeFormatFixture(); const data = new DateTimeFormatFixture();
data.createTime = new Date(2026, 4, 13, 10, 30, 0); data.createTime = new Date(2026, 4, 13, 10, 30, 0);