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 与时间
- 后台主键使用 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

View File

@ -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()`

View File

@ -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()`

View File

@ -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);