feat: 重构时间序列化装饰器
This commit is contained in:
parent
ed5b1f23bd
commit
da5e5f27e2
44
API.md
44
API.md
@ -47,13 +47,13 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### ID 与时间
|
||||
|
||||
- 后台主键使用 Snowflake 数字 ID,接口按字符串返回,避免 JavaScript 长整型精度丢失。
|
||||
- DTO/Entity 需要后端格式化的时间字段使用 `@FormatDateTime()`,输出格式为 `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`,可在装饰器中传入格式字符串;响应包装不做递归遍历。
|
||||
- `POST */save` 默认会删除请求体里的 `id`,防止新增接口误用前端主键。
|
||||
|
||||
## 环境变量分组
|
||||
|
||||
| 分组 | 关键变量 |
|
||||
| --- | --- |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
||||
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
||||
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
||||
@ -73,7 +73,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Auth / User
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | --------------- | ------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/auth/login` | 后台登录,返回 accessToken、用户信息和 WordPress 自动登录状态,并写入 httpOnly cookie |
|
||||
| `POST` | `/auth/refresh` | 通过 refresh token cookie 刷新 accessToken |
|
||||
| `POST` | `/auth/logout` | 清理 Admin 与 WordPress 登录 cookie |
|
||||
@ -85,7 +85,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Menu / Role / Dept / User Manage
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| -------- | -------------------------- | ------------------ |
|
||||
| `GET` | `/menu/all` | 当前用户菜单 |
|
||||
| `GET` | `/system/menu/list` | 系统菜单树 |
|
||||
| `GET` | `/system/menu/name-exists` | 菜单 name 重名校验 |
|
||||
@ -109,7 +109,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Dict
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| -------- | ------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `GET` | `/dict/list` | 字典项分页,支持 `dictCode`、`keyword`、`label`、`value`、`childrenCode`、`status` |
|
||||
| `GET` | `/dict/tree` | 兼容树形字典视图 |
|
||||
| `GET` | `/dict/groups` | 字典编码分组列表,适合左右表左侧分组 |
|
||||
@ -124,7 +124,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
字典核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| -------------- | --------------------------------------------------------- |
|
||||
| `dictCode` | 字典分组,例如 `COMPONENT_TYPE`、`BANGDREAM_SERVER_ALIAS` |
|
||||
| `label` | 展示文本 |
|
||||
| `value` | 字典值 |
|
||||
@ -137,7 +137,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
组件接口保持 `/component/*` 路径兼容,但数据表为 `admin_component`。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ----------------------- | -------------------------------------------------------------------- |
|
||||
| `GET` | `/component/allList` | 全量组件 |
|
||||
| `GET` | `/component/list` | 组件分页,支持 `pageNo`、`pageSize`、`name`、`type`、`componentType` |
|
||||
| `GET` | `/component/detail?id=` | 组件详情 |
|
||||
@ -148,7 +148,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Timezone / Upload / Demo
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ------------------------------ | ----------------------------- |
|
||||
| `GET` | `/timezone/getTimezoneOptions` | 时区选项 |
|
||||
| `GET` | `/timezone/getTimezone` | 当前用户时区 |
|
||||
| `POST` | `/timezone/setTimezone` | 设置当前用户时区 |
|
||||
@ -164,7 +164,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
后端通过 `nestjs-pino` 输出结构化日志。配置 Loki 后,Admin 日志页面通过后端代理查询,不直连 Loki。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ----- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/system/logs` | 日志分页,支持 `level`、`keyword`、`context`、`path`、`requestId`、`startTime`、`endTime`、`rangeMinutes` |
|
||||
| `GET` | `/system/logs/summary` | 按级别统计 |
|
||||
| `GET` | `/system/logs/levels` | 日志级别选项 |
|
||||
@ -177,7 +177,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
站内信用于承接运行期事件,不再作为人工公告入口。后端在接口 5xx、QQBot OneBot 下线 notice、NapCat 容器日志检测到账户离线时自动生成或聚合一条通知,默认通知 `super` 角色;站内信接口在服务端也强制 `super` 角色访问。相同 `dedupeKey` 的事件通过 `active_dedupe_key` 唯一索引聚合,会累加 `occurrenceCount`,刷新 `lastSeenAt`,并把状态重新置为未处理。运行期通知会按表字段长度归一化 `title`、`dedupeKey`、`source`、`eventType` 和 `notifyRoleCode`,长 `dedupeKey` 会保留稳定 hash 后缀,避免长路径接口错误丢通知。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/system/notice/list` | 日志级站内信分页列表,支持 `keyword`、`severity`、`source`、`eventType`、`status`、`isTop`、`notifyRoleCode`、`notifyUsers`、`pageNo`、`pageSize` |
|
||||
| `GET` | `/system/notice/detail/:id` | 查询站内信详情 |
|
||||
| `DELETE` | `/system/notice/:id` | 删除站内信(逻辑删除) |
|
||||
@ -193,7 +193,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Blog Article
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | -------------------------------- | -------------------------- |
|
||||
| `GET` | `/blog/article/public/list` | 公开文章分页 |
|
||||
| `GET` | `/blog/article/public/detail` | 公开文章详情,支持 id/slug |
|
||||
| `GET` | `/blog/article/list` | 后台文章分页 |
|
||||
@ -223,7 +223,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### Blog Category / Tag / Theme
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ------------------------------ | ------------------------- |
|
||||
| `GET` | `/blog/category/list` | 本地分类分页 |
|
||||
| `GET` | `/blog/category/detail` | 本地分类详情 |
|
||||
| `POST` | `/blog/category/save` | 新增分类 |
|
||||
@ -244,7 +244,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
`/wordpress/*` 需要 Admin 登录态和 WordPress 登录态。后端优先使用 `kt_wordpress_auth` httpOnly cookie,也支持显式透传 WordPress 认证 header。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ------------------------- | ----------------------------------------------- |
|
||||
| `POST` | `/wordpress/auth/login` | 使用 env 管理员账号登录 WordPress 并写入 cookie |
|
||||
| `POST` | `/wordpress/auth/logout` | 清理 WordPress cookie |
|
||||
| `GET` | `/wordpress/auth/check` | 校验 WordPress 登录态 |
|
||||
@ -253,7 +253,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
### WordPress Article / Tag / Category
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ---------------------------------- | ------------------- |
|
||||
| `GET` | `/wordpress/article/public/list` | 公开文章列表代理 |
|
||||
| `GET` | `/wordpress/article/public/detail` | 公开文章详情代理 |
|
||||
| `GET` | `/wordpress/article/list` | WordPress 文章分页 |
|
||||
@ -277,7 +277,7 @@ WordPress rewrite 未开启导致 `/wp-json/*` 返回 404 时,后端会回退
|
||||
## MinIO
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| -------- | ----------------------- | ------------------------------- |
|
||||
| `GET` | `/minio/check` | 检查连接和 bucket |
|
||||
| `POST` | `/minio/bucket` | 创建 bucket |
|
||||
| `POST` | `/minio/upload` | 上传文件,`multipart/form-data` |
|
||||
@ -296,7 +296,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
### Account / Scan Login
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ----------------------------------------------- | -------------------------- |
|
||||
| `GET` | `/qqbot/account/list` | QQBot 账号分页 |
|
||||
| `GET` | `/qqbot/account/enabled` | 启用账号列表 |
|
||||
| `POST` | `/qqbot/account/save` | 手动新增账号 |
|
||||
@ -323,7 +323,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
### Command / Rule / Permission
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ---------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `GET` | `/qqbot/command/list` | 在线命令分页,支持 `pluginKey`、`operationKey`、`selfId`、`enabled` |
|
||||
| `POST` | `/qqbot/command/save` | 新增在线命令 |
|
||||
| `POST` | `/qqbot/command/update` | 更新在线命令 |
|
||||
@ -364,7 +364,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
### Plugin / Dashboard / Send / Message
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ------ | ------------------------------ | ------------------------------------------ |
|
||||
| `GET` | `/qqbot/plugin/list` | 插件列表,支持 `triggerMode=command/event` |
|
||||
| `GET` | `/qqbot/plugin/operation/list` | 插件能力列表 |
|
||||
| `GET` | `/qqbot/plugin/health` | 插件健康检查 |
|
||||
@ -389,7 +389,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
插件 key:`bangDream`。当前源码根目录为 `src/qqbot/plugins/bangDream`,不再使用旧 `tsugu` 子目录。
|
||||
|
||||
| operation key | 命令 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| ----------------------------- | ------------- | ------------------------ |
|
||||
| `bangdream.song.search` | `/查曲` | 查歌曲信息图片 |
|
||||
| `bangdream.song.chart` | `/查谱面` | 查谱面图片 |
|
||||
| `bangdream.song.random` | `/随机曲` | 随机歌曲 |
|
||||
@ -413,7 +413,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
插件 key:`ff14Market`。
|
||||
|
||||
| operation key | 说明 |
|
||||
| --- | --- |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `ff14.item.resolve` | 按物品名称或 ID 解析 XIVAPI 物品 |
|
||||
| `ff14.market.price` | 查询指定服务器/大区的 Universalis 市场价格 |
|
||||
|
||||
@ -424,7 +424,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
插件 key:`fflogs`。
|
||||
|
||||
| operation key | 说明 |
|
||||
| --- | --- |
|
||||
| -------------------------- | --------------------------------------------------------------- |
|
||||
| `fflogs.character.summary` | 查询 FFLogs 角色公开排名;传 `encounter` 时查询指定高难最近记录 |
|
||||
|
||||
常用输入:`characterName`、`serverSlug`、`serverRegion`、`encounter`、`limit`、`metric`、`timeframe`、`zoneId`。
|
||||
@ -432,7 +432,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
## 初始化 SQL
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| ---------------------------------------------- | --------------------------------------------------------- |
|
||||
| `sql/vben-admin-init.sql` | 创建 Admin 基础表、用户、角色、菜单、部门、字典和空组件表 |
|
||||
| `sql/blog-init.sql` | 初始化本地 Blog 表 |
|
||||
| `sql/blog-menu.sql` | 初始化 Blog 管理菜单 |
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
| --- | --- |
|
||||
| -------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `admin` | Vben Admin 认证、用户、菜单、角色、部门、时区、字典、组件模板、系统日志 |
|
||||
| `blog` | 本地博客文章、分类、标签、Argon 主题配置和 WordPress 导入 |
|
||||
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
||||
@ -54,7 +54,7 @@ ci/ Jenkins Agent/Docker 辅助文件
|
||||
主要配置分组:
|
||||
|
||||
| 分组 | 变量 |
|
||||
| --- | --- |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
||||
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
||||
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
||||
@ -124,7 +124,7 @@ pnpm exec jest --runInBand --runTestsByPath test/path/to/file.spec.ts
|
||||
## 核心规则
|
||||
|
||||
- 后台主键使用 Snowflake 数字 ID,数据库字段为 `BIGINT`,接口按字符串返回。
|
||||
- 后端响应时间统一用 `YYYY-MM-DD HH:mm:ss`,需要格式化的 DTO/Entity 字段使用 `@FormatDateTime()`。
|
||||
- 后端响应时间统一用 `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` 不做全量递归格式化。
|
||||
- 字典维护在 `admin_dict`,Admin 字典管理按 `dictCode` 分组展示;可运营映射优先走字典或静态配置,不硬编码到业务函数。
|
||||
- 全局 `SaveBodyInterceptor` 会删除 `POST */save` 请求体里的 `id`;需要保留时使用 `@SkipSaveBodyNormalize()`。
|
||||
- Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管理接口默认走 `JwtAuthGuard`;公开接口用 `@Public()`。
|
||||
@ -174,5 +174,5 @@ BangDream 图片能力改动:
|
||||
## 来源与许可证
|
||||
|
||||
| 一级来源 | 使用方式 | License |
|
||||
| --- | --- | --- |
|
||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
||||
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
import {
|
||||
AfterLoad,
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
DecodeDictKey,
|
||||
decodeDictKeys,
|
||||
ensureSnowflakeId,
|
||||
FormatDateTime,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
|
||||
@Entity('admin_component')
|
||||
export class Component {
|
||||
@ApiPropertyOptional()
|
||||
@ -69,17 +68,15 @@ export class Component {
|
||||
})
|
||||
template: string;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@Column({
|
||||
default: 0,
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
@Entity('admin_dept')
|
||||
export class AdminDept {
|
||||
@PrimaryColumn({
|
||||
@ -40,17 +37,15 @@ export class AdminDept {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
@Entity('admin_dict')
|
||||
export class AdminDict {
|
||||
@ApiPropertyOptional()
|
||||
@ -65,17 +62,15 @@ export class AdminDict {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { FormatDateTime } from '@/common';
|
||||
import { KtDateTime, KtDateTimeField } from '@/common';
|
||||
|
||||
export class DictDto {
|
||||
@ApiProperty({
|
||||
@ -59,14 +59,14 @@ export class AdminDictDto {
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime?: Date;
|
||||
@KtDateTimeField()
|
||||
createTime?: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime?: Date;
|
||||
@KtDateTimeField()
|
||||
updateTime?: KtDateTime;
|
||||
}
|
||||
|
||||
export class AdminDictTreeDto extends AdminDictDto {
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToMany,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import { AdminRole } from '../role/admin-role.entity';
|
||||
import type { AdminMenuMeta, AdminMenuType } from '../admin.types';
|
||||
|
||||
@ -83,17 +86,15 @@ export class AdminMenu {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@ManyToMany(() => AdminRole, (role) => role.menus)
|
||||
roles: AdminRole[];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { FormatDateTime } from '@/common';
|
||||
import { KtDateTime, KtDateTimeField } from '@/common';
|
||||
|
||||
export class AdminNoticeDto {
|
||||
@ApiProperty({
|
||||
@ -88,26 +88,26 @@ export class AdminNoticeDto {
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime?: Date;
|
||||
@KtDateTimeField()
|
||||
createTime?: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime?: Date;
|
||||
@KtDateTimeField()
|
||||
updateTime?: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
firstSeenAt?: Date;
|
||||
@KtDateTimeField()
|
||||
firstSeenAt?: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03 20:00:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastSeenAt?: Date;
|
||||
@KtDateTimeField()
|
||||
lastSeenAt?: KtDateTime;
|
||||
}
|
||||
|
||||
export class AdminNoticeQueryDto {
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
@Entity('admin_notice')
|
||||
export class AdminNotice {
|
||||
@ApiPropertyOptional()
|
||||
@ -159,33 +157,29 @@ export class AdminNotice {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
name: 'first_seen_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
firstSeenAt?: Date;
|
||||
firstSeenAt?: KtDateTime;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
name: 'last_seen_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastSeenAt?: Date;
|
||||
lastSeenAt?: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import { AdminMenu } from '../menu/admin-menu.entity';
|
||||
import { AdminUser } from '../user/admin-user.entity';
|
||||
|
||||
@ -44,17 +47,15 @@ export class AdminRole {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@ManyToMany(() => AdminMenu, (menu) => menu.roles)
|
||||
@JoinTable({
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { FormatDateTime } from '@/common';
|
||||
import { KtDateTime, KtDateTimeField } from '@/common';
|
||||
|
||||
export class SystemLogQueryDto {
|
||||
@ApiPropertyOptional({ description: '日志级别:debug/info/warning/error/critical' })
|
||||
@ApiPropertyOptional({
|
||||
description: '日志级别:debug/info/warning/error/critical',
|
||||
})
|
||||
level?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '全文关键字' })
|
||||
@ -44,8 +46,8 @@ export class SystemLogDto {
|
||||
id: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '日志时间' })
|
||||
@FormatDateTime()
|
||||
timestamp: string;
|
||||
@KtDateTimeField()
|
||||
timestamp: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Loki 纳秒时间戳' })
|
||||
timestampNs: string;
|
||||
|
||||
@ -6,7 +6,7 @@ import { URL } from 'node:url';
|
||||
import {
|
||||
getAppName,
|
||||
getLokiEnvironment,
|
||||
formatDateTimeFields,
|
||||
transformKtDateTimeFields,
|
||||
throwVbenError,
|
||||
ToolsService,
|
||||
} from '@/common';
|
||||
@ -97,11 +97,7 @@ export class SystemLogService {
|
||||
};
|
||||
}
|
||||
|
||||
const { pageNo, pageSize } = this.toolsService.getPageParams(
|
||||
query,
|
||||
1,
|
||||
20,
|
||||
);
|
||||
const { pageNo, pageSize } = this.toolsService.getPageParams(query, 1, 20);
|
||||
const skip = (pageNo - 1) * pageSize;
|
||||
const requestLimit = Math.min(
|
||||
this.toolsService.toPositiveNumber(query.limit, skip + pageSize),
|
||||
@ -119,9 +115,7 @@ export class SystemLogService {
|
||||
};
|
||||
}
|
||||
|
||||
async summary(
|
||||
query: SystemLogQueryDto = {},
|
||||
): Promise<SystemLogSummaryDto[]> {
|
||||
async summary(query: SystemLogQueryDto = {}): Promise<SystemLogSummaryDto[]> {
|
||||
if (!this.host) {
|
||||
return DEFAULT_LEVELS.map((level) => ({ count: 0, level }));
|
||||
}
|
||||
@ -239,7 +233,10 @@ export class SystemLogService {
|
||||
|
||||
private withLevelSelector(selector: string, level?: string) {
|
||||
if (!level || selector.includes('level=')) return selector;
|
||||
return selector.replace(/}\s*$/, `,level="${this.escapeLabelValue(level)}"}`);
|
||||
return selector.replace(
|
||||
/}\s*$/,
|
||||
`,level="${this.escapeLabelValue(level)}"}`,
|
||||
);
|
||||
}
|
||||
|
||||
private getBaseSelector() {
|
||||
@ -307,7 +304,7 @@ export class SystemLogService {
|
||||
const method =
|
||||
this.pickText(parsed.method, req.method)?.toUpperCase() || undefined;
|
||||
|
||||
return formatDateTimeFields(
|
||||
return transformKtDateTimeFields(
|
||||
Object.assign(new SystemLogDto(), {
|
||||
context:
|
||||
this.pickText(parsed.context, params.stream.context) || undefined,
|
||||
@ -454,7 +451,9 @@ export class SystemLogService {
|
||||
}
|
||||
|
||||
private asRecord(value: unknown): Record<string, any> {
|
||||
return value && typeof value === 'object' ? (value as Record<string, any>) : {};
|
||||
return value && typeof value === 'object'
|
||||
? (value as Record<string, any>)
|
||||
: {};
|
||||
}
|
||||
|
||||
private pickText(...values: unknown[]) {
|
||||
@ -472,7 +471,9 @@ export class SystemLogService {
|
||||
private toDate(value: unknown) {
|
||||
const text = this.toolsService.toTrimmedString(value);
|
||||
if (!text) return null;
|
||||
const timestamp = /^\d+$/.test(text) ? this.normalizeTimestamp(text) : Date.parse(text);
|
||||
const timestamp = /^\d+$/.test(text)
|
||||
? this.normalizeTimestamp(text)
|
||||
: Date.parse(text);
|
||||
const date = new Date(timestamp);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import { AdminDept } from '../dept/admin-dept.entity';
|
||||
import { AdminRole } from '../role/admin-role.entity';
|
||||
|
||||
@ -69,17 +72,15 @@ export class AdminUser {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@ManyToMany(() => AdminRole, (role) => role.users, {
|
||||
eager: true,
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
export type BlogArticleStatus = 'draft' | 'pending' | 'private' | 'publish';
|
||||
|
||||
export type BlogArticleTerm = {
|
||||
@ -117,25 +115,22 @@ export class BlogArticle {
|
||||
isDeleted: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
name: 'publish_time',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
publishTime: Date;
|
||||
publishTime: KtDateTime;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
categories?: string[];
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
export type BlogTermKind = 'category' | 'tag';
|
||||
|
||||
@Entity('blog_term')
|
||||
@ -56,17 +53,15 @@ export class BlogTerm {
|
||||
})
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
count?: number;
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { FormatDateTime } from '@/common';
|
||||
import { KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
|
||||
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
|
||||
|
||||
@Entity('blog_theme_config')
|
||||
@ -30,15 +24,13 @@ export class BlogThemeConfig {
|
||||
})
|
||||
source: string;
|
||||
|
||||
@CreateDateColumn({
|
||||
@KtCreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({
|
||||
@KtUpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
updateTime: KtDateTime;
|
||||
}
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
import { AfterLoad } from 'typeorm';
|
||||
|
||||
export const KT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
const DATE_TIME_FORMAT_RULES = Symbol('DATE_TIME_FORMAT_RULES');
|
||||
const DATE_TIME_FORMAT_HOOK_REGISTERED = Symbol(
|
||||
'DATE_TIME_FORMAT_HOOK_REGISTERED',
|
||||
);
|
||||
const DATE_TIME_FORMAT_HOOK = '__ktFormatDateTimeFields';
|
||||
const ISO_DATE_TIME_PATTERN =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})?$/;
|
||||
const DATE_TIME_TEXT_PATTERN =
|
||||
/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/;
|
||||
|
||||
type DateTimeFormatRule = {
|
||||
sourceKey: string;
|
||||
targetKey: string;
|
||||
};
|
||||
|
||||
type DateTimeFormatTarget = Record<PropertyKey, any> & {
|
||||
[DATE_TIME_FORMAT_HOOK_REGISTERED]?: boolean;
|
||||
[DATE_TIME_FORMAT_RULES]?: DateTimeFormatRule[];
|
||||
};
|
||||
|
||||
const padDateUnit = (value: number) => `${value}`.padStart(2, '0');
|
||||
|
||||
export const formatKtDateTime = (value: Date | number | string): string => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
return [
|
||||
`${date.getFullYear()}-${padDateUnit(date.getMonth() + 1)}-${padDateUnit(
|
||||
date.getDate(),
|
||||
)}`,
|
||||
`${padDateUnit(date.getHours())}:${padDateUnit(
|
||||
date.getMinutes(),
|
||||
)}:${padDateUnit(date.getSeconds())}`,
|
||||
].join(' ');
|
||||
};
|
||||
|
||||
const formatDateTimeFieldValue = (value: unknown) => {
|
||||
if (value instanceof Date) {
|
||||
return formatKtDateTime(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return formatKtDateTime(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && isDateTimeText(value)) {
|
||||
const formatted = formatKtDateTime(value);
|
||||
return formatted || value;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const isDateTimeText = (value: string) =>
|
||||
ISO_DATE_TIME_PATTERN.test(value) || DATE_TIME_TEXT_PATTERN.test(value);
|
||||
|
||||
const getDateTimeFormatRules = (target: object): DateTimeFormatRule[] => {
|
||||
const prototype = Object.getPrototypeOf(target);
|
||||
|
||||
return prototype?.[DATE_TIME_FORMAT_RULES] || [];
|
||||
};
|
||||
|
||||
const ensureDateTimeFormatHook = (target: DateTimeFormatTarget) => {
|
||||
if (target[DATE_TIME_FORMAT_HOOK_REGISTERED]) return;
|
||||
|
||||
Object.defineProperty(target, DATE_TIME_FORMAT_HOOK, {
|
||||
configurable: true,
|
||||
value: function (this: object) {
|
||||
formatDateTimeFields(this);
|
||||
},
|
||||
});
|
||||
|
||||
AfterLoad()(target, DATE_TIME_FORMAT_HOOK);
|
||||
target[DATE_TIME_FORMAT_HOOK_REGISTERED] = true;
|
||||
};
|
||||
|
||||
export const FormatDateTime = (): PropertyDecorator => {
|
||||
return (target, propertyKey) => {
|
||||
const fieldKey = propertyKey.toString();
|
||||
const rules = target[DATE_TIME_FORMAT_RULES] || [];
|
||||
|
||||
target[DATE_TIME_FORMAT_RULES] = [
|
||||
...rules.filter(({ targetKey }) => targetKey !== fieldKey),
|
||||
{
|
||||
sourceKey: fieldKey,
|
||||
targetKey: fieldKey,
|
||||
},
|
||||
];
|
||||
ensureDateTimeFormatHook(target as DateTimeFormatTarget);
|
||||
};
|
||||
};
|
||||
|
||||
export function formatDateTimeFields<T extends object>(target: T): T {
|
||||
getDateTimeFormatRules(target).forEach(({ sourceKey, targetKey }) => {
|
||||
target[targetKey] = formatDateTimeFieldValue(target[sourceKey]);
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
271
src/common/decorators/kt-date-time.decorator.ts
Normal file
271
src/common/decorators/kt-date-time.decorator.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
type ColumnOptions,
|
||||
type ValueTransformer,
|
||||
} from 'typeorm';
|
||||
|
||||
export const KT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
const KT_DATETIME_RULES = Symbol('KT_DATETIME_RULES');
|
||||
const KT_DATETIME_INSTANCE_FORMATS = new WeakMap<Date, string>();
|
||||
const ISO_DATE_TIME_PATTERN =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})?$/;
|
||||
const DATE_TIME_TEXT_PATTERN = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/;
|
||||
|
||||
type KtDateTimeRule = {
|
||||
format: string;
|
||||
sourceKey: string;
|
||||
targetKey: string;
|
||||
};
|
||||
|
||||
type KtDateTimeRuleTarget = Record<PropertyKey, any> & {
|
||||
[KT_DATETIME_RULES]?: KtDateTimeRule[];
|
||||
};
|
||||
|
||||
type KtDateTimeDecoratorOptions = string | ColumnOptions;
|
||||
|
||||
const padDateUnit = (value: number) => `${value}`.padStart(2, '0');
|
||||
|
||||
export class KtDateTime extends Date {
|
||||
constructor(value?: Date | number | string, format = KT_DATETIME_FORMAT) {
|
||||
super(value instanceof Date ? value.getTime() : (value ?? Date.now()));
|
||||
KT_DATETIME_INSTANCE_FORMATS.set(this, format);
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return formatKtDateTime(this, getKtDateTimeFormat(this));
|
||||
}
|
||||
|
||||
[Symbol.toPrimitive](hint: 'number'): number;
|
||||
[Symbol.toPrimitive](hint: 'default' | 'string'): string;
|
||||
[Symbol.toPrimitive](hint: string): string | number {
|
||||
return hint === 'number' ? this.getTime() : this.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export const formatKtDateTime = (
|
||||
value: Date | number | string,
|
||||
format = KT_DATETIME_FORMAT,
|
||||
): string => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const tokens: Record<string, string> = {
|
||||
DD: padDateUnit(date.getDate()),
|
||||
HH: padDateUnit(date.getHours()),
|
||||
MM: padDateUnit(date.getMonth() + 1),
|
||||
YYYY: `${date.getFullYear()}`,
|
||||
mm: padDateUnit(date.getMinutes()),
|
||||
ss: padDateUnit(date.getSeconds()),
|
||||
};
|
||||
|
||||
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (token) => tokens[token]);
|
||||
};
|
||||
|
||||
export const toKtDateTime = (
|
||||
value: Date | number | string,
|
||||
format = KT_DATETIME_FORMAT,
|
||||
) => {
|
||||
const date = new KtDateTime(value, format);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
export const createKtDateTimeTransformer = (
|
||||
format = KT_DATETIME_FORMAT,
|
||||
): ValueTransformer => ({
|
||||
from: (value: unknown) => transformKtDateTimeValue(value, format),
|
||||
to: (value: unknown) => {
|
||||
if (value instanceof Date) return new Date(value.getTime());
|
||||
if (
|
||||
(typeof value === 'number' && Number.isFinite(value)) ||
|
||||
(typeof value === 'string' && isDateTimeText(value))
|
||||
) {
|
||||
return toKtDateTime(value, format);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
export function KtDateTimeColumn(): PropertyDecorator;
|
||||
export function KtDateTimeColumn(format: string): PropertyDecorator;
|
||||
export function KtDateTimeColumn(options: ColumnOptions): PropertyDecorator;
|
||||
export function KtDateTimeColumn(
|
||||
format: string,
|
||||
options: ColumnOptions,
|
||||
): PropertyDecorator;
|
||||
export function KtDateTimeColumn(
|
||||
formatOrOptions?: KtDateTimeDecoratorOptions,
|
||||
options?: ColumnOptions,
|
||||
): PropertyDecorator {
|
||||
const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options);
|
||||
return Column({
|
||||
type: 'datetime',
|
||||
...normalized.options,
|
||||
transformer: mergeDateTimeTransformer(
|
||||
normalized.options.transformer,
|
||||
normalized.format,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function KtCreateDateColumn(): PropertyDecorator;
|
||||
export function KtCreateDateColumn(format: string): PropertyDecorator;
|
||||
export function KtCreateDateColumn(options: ColumnOptions): PropertyDecorator;
|
||||
export function KtCreateDateColumn(
|
||||
format: string,
|
||||
options: ColumnOptions,
|
||||
): PropertyDecorator;
|
||||
export function KtCreateDateColumn(
|
||||
formatOrOptions?: KtDateTimeDecoratorOptions,
|
||||
options?: ColumnOptions,
|
||||
): PropertyDecorator {
|
||||
const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options);
|
||||
return CreateDateColumn({
|
||||
...normalized.options,
|
||||
transformer: mergeDateTimeTransformer(
|
||||
normalized.options.transformer,
|
||||
normalized.format,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function KtUpdateDateColumn(): PropertyDecorator;
|
||||
export function KtUpdateDateColumn(format: string): PropertyDecorator;
|
||||
export function KtUpdateDateColumn(options: ColumnOptions): PropertyDecorator;
|
||||
export function KtUpdateDateColumn(
|
||||
format: string,
|
||||
options: ColumnOptions,
|
||||
): PropertyDecorator;
|
||||
export function KtUpdateDateColumn(
|
||||
formatOrOptions?: KtDateTimeDecoratorOptions,
|
||||
options?: ColumnOptions,
|
||||
): PropertyDecorator {
|
||||
const normalized = normalizeDateTimeColumnOptions(formatOrOptions, options);
|
||||
return UpdateDateColumn({
|
||||
...normalized.options,
|
||||
transformer: mergeDateTimeTransformer(
|
||||
normalized.options.transformer,
|
||||
normalized.format,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export const KtDateTimeField = (
|
||||
format = KT_DATETIME_FORMAT,
|
||||
): PropertyDecorator => {
|
||||
return (target, propertyKey) => {
|
||||
const fieldKey = propertyKey.toString();
|
||||
const formatTarget = target as KtDateTimeRuleTarget;
|
||||
const rules = formatTarget[KT_DATETIME_RULES] || [];
|
||||
|
||||
formatTarget[KT_DATETIME_RULES] = [
|
||||
...rules.filter(({ targetKey }) => targetKey !== fieldKey),
|
||||
{
|
||||
format,
|
||||
sourceKey: fieldKey,
|
||||
targetKey: fieldKey,
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
export function transformKtDateTimeFields<T extends object>(target: T): T {
|
||||
const result = copyEnumerableFields(target) as Record<string, unknown>;
|
||||
|
||||
applyKtDateTimeFields(target, result);
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
function normalizeDateTimeColumnOptions(
|
||||
formatOrOptions?: KtDateTimeDecoratorOptions,
|
||||
options: ColumnOptions = {},
|
||||
) {
|
||||
if (typeof formatOrOptions === 'string') {
|
||||
return {
|
||||
format: formatOrOptions || KT_DATETIME_FORMAT,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
format: KT_DATETIME_FORMAT,
|
||||
options: formatOrOptions || {},
|
||||
};
|
||||
}
|
||||
|
||||
function mergeDateTimeTransformer(
|
||||
existing: ColumnOptions['transformer'],
|
||||
format: string,
|
||||
) {
|
||||
const ktTransformer = createKtDateTimeTransformer(format);
|
||||
if (!existing) return ktTransformer;
|
||||
|
||||
return Array.isArray(existing)
|
||||
? [...existing, ktTransformer]
|
||||
: [existing, ktTransformer];
|
||||
}
|
||||
|
||||
function transformKtDateTimeValue(value: unknown, format = KT_DATETIME_FORMAT) {
|
||||
if (value == null) return value;
|
||||
if (value instanceof KtDateTime) return value;
|
||||
|
||||
if (value instanceof Date) {
|
||||
return toKtDateTime(value, format) || value;
|
||||
}
|
||||
|
||||
if (
|
||||
(typeof value === 'number' && Number.isFinite(value)) ||
|
||||
(typeof value === 'string' && isDateTimeText(value))
|
||||
) {
|
||||
return toKtDateTime(value, format) || value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getKtDateTimeFormat(value: KtDateTime) {
|
||||
return KT_DATETIME_INSTANCE_FORMATS.get(value) || KT_DATETIME_FORMAT;
|
||||
}
|
||||
|
||||
function isDateTimeText(value: string) {
|
||||
return (
|
||||
ISO_DATE_TIME_PATTERN.test(value) || DATE_TIME_TEXT_PATTERN.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function getKtDateTimeRules(target: object): KtDateTimeRule[] {
|
||||
const prototype = Object.getPrototypeOf(target);
|
||||
|
||||
return prototype?.[KT_DATETIME_RULES] || [];
|
||||
}
|
||||
|
||||
function applyKtDateTimeFields(
|
||||
source: object,
|
||||
target: Record<string, unknown> = source as Record<string, unknown>,
|
||||
) {
|
||||
const sourceRecord = source as Record<string, unknown>;
|
||||
|
||||
getKtDateTimeRules(source).forEach(({ format, sourceKey, targetKey }) => {
|
||||
target[targetKey] = transformKtDateTimeValue(
|
||||
sourceRecord[sourceKey],
|
||||
format,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function copyEnumerableFields(target: object) {
|
||||
return Object.entries(target).reduce<Record<string, unknown>>(
|
||||
(result, [key, value]) => {
|
||||
result[key] = value;
|
||||
return result;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export * from './common.module';
|
||||
export * from './decorators/current-admin-user.decorator';
|
||||
export * from './decorators/decode-dict.decorator';
|
||||
export * from './decorators/format-date-time.decorator';
|
||||
export * from './decorators/kt-date-time.decorator';
|
||||
export * from './decorators/public.decorator';
|
||||
export * from './filters/api-exception.filter';
|
||||
export * from './interceptors/api-request-log.interceptor';
|
||||
|
||||
@ -28,7 +28,7 @@ import {
|
||||
ApiArrayResponse,
|
||||
ApiModelResponse,
|
||||
ApiSuccessResponse,
|
||||
formatDateTimeFields,
|
||||
transformKtDateTimeFields,
|
||||
ToolsService,
|
||||
} from '@/common';
|
||||
import {
|
||||
@ -159,7 +159,7 @@ export class MinioClientController {
|
||||
recursive: recursive !== 'false',
|
||||
})) as Record<string, unknown>[]
|
||||
).map((item) =>
|
||||
formatDateTimeFields(Object.assign(new MinioObjectDto(), item)),
|
||||
transformKtDateTimeFields(Object.assign(new MinioObjectDto(), item)),
|
||||
);
|
||||
|
||||
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { FormatDateTime } from '@/common';
|
||||
import { KtDateTime, KtDateTimeField } from '@/common';
|
||||
|
||||
export class MinioBucketStatusDto {
|
||||
@ApiProperty({
|
||||
@ -64,6 +64,6 @@ export class MinioObjectDto {
|
||||
@ApiProperty({
|
||||
example: '2026-05-13 10:30:00',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastModified: string;
|
||||
@KtDateTimeField()
|
||||
lastModified: KtDateTime;
|
||||
}
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotAccountAbilityType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_account_ability')
|
||||
@ -34,13 +31,11 @@ export class QqbotAccountAbility {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type {
|
||||
QqbotConnectionMode,
|
||||
QqbotConnectionRole,
|
||||
@ -45,23 +44,21 @@ export class QqbotAccount {
|
||||
@Column({ default: null, length: 32, name: 'client_role', nullable: true })
|
||||
clientRole: null | QqbotConnectionRole;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_connected_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastConnectedAt: Date | null;
|
||||
lastConnectedAt: KtDateTime | null;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_heartbeat_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastHeartbeatAt: Date | null;
|
||||
lastHeartbeatAt: KtDateTime | null;
|
||||
|
||||
@Column({ default: null, length: 500, name: 'last_error', nullable: true })
|
||||
lastError: null | string;
|
||||
@ -72,13 +69,11 @@ export class QqbotAccount {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotMessageType } from '../qqbot.types';
|
||||
import type { QqbotCommandLogStatus } from '../qqbot.types';
|
||||
|
||||
@ -59,13 +57,11 @@ export class QqbotCommandLog {
|
||||
})
|
||||
errorMessage: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
import type { QqbotCommandParserType, QqbotRuleTargetType } from '../qqbot.types';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type {
|
||||
QqbotCommandParserType,
|
||||
QqbotRuleTargetType,
|
||||
} from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_command')
|
||||
export class QqbotCommand {
|
||||
@ -38,13 +40,28 @@ export class QqbotCommand {
|
||||
@Column({ default: 'all', length: 32, name: 'target_type' })
|
||||
targetType: QqbotRuleTargetType;
|
||||
|
||||
@Column({ default: null, name: 'default_params', nullable: true, type: 'text' })
|
||||
@Column({
|
||||
default: null,
|
||||
name: 'default_params',
|
||||
nullable: true,
|
||||
type: 'text',
|
||||
})
|
||||
defaultParams: string | null;
|
||||
|
||||
@Column({ default: null, name: 'reply_template', nullable: true, type: 'text' })
|
||||
@Column({
|
||||
default: null,
|
||||
name: 'reply_template',
|
||||
nullable: true,
|
||||
type: 'text',
|
||||
})
|
||||
replyTemplate: string | null;
|
||||
|
||||
@Column({ default: null, name: 'error_template', nullable: true, type: 'text' })
|
||||
@Column({
|
||||
default: null,
|
||||
name: 'error_template',
|
||||
nullable: true,
|
||||
type: 'text',
|
||||
})
|
||||
errorTemplate: string | null;
|
||||
|
||||
@Column({ default: true })
|
||||
@ -56,14 +73,13 @@ export class QqbotCommand {
|
||||
@Column({ default: 1500, name: 'cooldown_ms' })
|
||||
cooldownMs: number;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_hit_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastHitAt: Date | null;
|
||||
lastHitAt: KtDateTime | null;
|
||||
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
@ -71,13 +87,11 @@ export class QqbotCommand {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
@Entity('qqbot_config')
|
||||
@Index('uk_qqbot_config_key', ['configKey'], { unique: true })
|
||||
export class QqbotConfig {
|
||||
@ -24,13 +20,11 @@ export class QqbotConfig {
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
@Entity('qqbot_dedupe')
|
||||
export class QqbotDedupe {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
@ -16,22 +14,19 @@ export class QqbotDedupe {
|
||||
@Column({ length: 255, name: 'event_key', unique: true })
|
||||
eventKey: string;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'expire_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
expireAt: Date | null;
|
||||
expireAt: KtDateTime | null;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotMessageType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_conversation')
|
||||
@ -39,14 +37,13 @@ export class QqbotConversation {
|
||||
@Column({ name: 'last_message_text', type: 'text' })
|
||||
lastMessageText: string;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_message_time',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastMessageTime: Date | null;
|
||||
lastMessageTime: KtDateTime | null;
|
||||
|
||||
@Column({ default: 0, name: 'message_count' })
|
||||
messageCount: number;
|
||||
@ -54,13 +51,11 @@ export class QqbotConversation {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotMessageDirection, QqbotMessageType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_message')
|
||||
@ -57,17 +55,14 @@ export class QqbotMessage {
|
||||
@Column({ name: 'raw_event', nullable: true, type: 'simple-json' })
|
||||
rawEvent: Record<string, any>;
|
||||
|
||||
@Column({ name: 'event_time', type: 'datetime' })
|
||||
@FormatDateTime()
|
||||
eventTime: Date;
|
||||
@KtDateTimeColumn({ name: 'event_time', type: 'datetime' })
|
||||
eventTime: KtDateTime;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotAccountNapcatBindStatus } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_account_napcat')
|
||||
@ -29,14 +27,13 @@ export class QqbotAccountNapcat {
|
||||
@Column({ default: true, name: 'is_primary' })
|
||||
isPrimary: boolean;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_login_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastLoginAt: Date | null;
|
||||
lastLoginAt: KtDateTime | null;
|
||||
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
@ -44,13 +41,11 @@ export class QqbotAccountNapcat {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotNapcatContainerStatus } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_napcat_container')
|
||||
@ -47,23 +45,21 @@ export class QqbotNapcatContainer {
|
||||
@Column({ default: 'creating', length: 32 })
|
||||
status: QqbotNapcatContainerStatus;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_started_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastStartedAt: Date | null;
|
||||
lastStartedAt: KtDateTime | null;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_checked_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastCheckedAt: Date | null;
|
||||
lastCheckedAt: KtDateTime | null;
|
||||
|
||||
@Column({ default: null, length: 500, name: 'last_error', nullable: true })
|
||||
lastError: null | string;
|
||||
@ -74,13 +70,11 @@ export class QqbotNapcatContainer {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotPermissionTargetType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_allowlist')
|
||||
@ -38,13 +36,11 @@ export class QqbotAllowlist {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotPermissionTargetType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_blocklist')
|
||||
@ -38,13 +36,11 @@ export class QqbotBlocklist {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotRuleMatchType, QqbotRuleTargetType } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_rule')
|
||||
@ -38,14 +37,13 @@ export class QqbotRule {
|
||||
@Column({ default: 1500, name: 'cooldown_ms' })
|
||||
cooldownMs: number;
|
||||
|
||||
@Column({
|
||||
@KtDateTimeColumn({
|
||||
default: null,
|
||||
name: 'last_hit_at',
|
||||
nullable: true,
|
||||
type: 'datetime',
|
||||
})
|
||||
@FormatDateTime()
|
||||
lastHitAt: Date | null;
|
||||
lastHitAt: KtDateTime | null;
|
||||
|
||||
@Column({ default: '', length: 255 })
|
||||
remark: string;
|
||||
@ -53,13 +51,11 @@ export class QqbotRule {
|
||||
@Column({ default: false, name: 'is_deleted' })
|
||||
isDeleted: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ensureSnowflakeId, FormatDateTime } from '@/common';
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
import type { QqbotMessageType, QqbotSendStatus } from '../qqbot.types';
|
||||
|
||||
@Entity('qqbot_send_log')
|
||||
@ -49,13 +46,11 @@ export class QqbotSendLog {
|
||||
@Column({ nullable: true, type: 'simple-json' })
|
||||
response: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time' })
|
||||
@FormatDateTime()
|
||||
createTime: Date;
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time' })
|
||||
@FormatDateTime()
|
||||
updateTime: Date;
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import {
|
||||
FormatDateTime,
|
||||
formatDateTimeFields,
|
||||
formatKtDateTime,
|
||||
vbenSuccess,
|
||||
} from '../../src/common';
|
||||
|
||||
class DateTimeFormatFixture {
|
||||
@FormatDateTime()
|
||||
createTime!: Date;
|
||||
|
||||
@FormatDateTime()
|
||||
updateTime!: string;
|
||||
}
|
||||
|
||||
describe('FormatDateTime', () => {
|
||||
it('formats decorated fields as YYYY-MM-DD HH:mm:ss', () => {
|
||||
const data = new DateTimeFormatFixture();
|
||||
data.createTime = new Date(2026, 4, 13, 10, 30, 0);
|
||||
data.updateTime = '2026-05-13T10:31:00';
|
||||
|
||||
expect(formatDateTimeFields(data)).toEqual({
|
||||
createTime: '2026-05-13 10:30:00',
|
||||
updateTime: '2026-05-13 10:31:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not recursively format response payloads', () => {
|
||||
const createTime = new Date(2026, 4, 13, 10, 30, 0);
|
||||
const response = vbenSuccess({
|
||||
createTime,
|
||||
nested: {
|
||||
updateTime: '2026-05-13T10:31:00',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.data).toEqual({
|
||||
createTime,
|
||||
nested: {
|
||||
updateTime: '2026-05-13T10:31:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes the configured formatter', () => {
|
||||
expect(formatKtDateTime(new Date(2026, 4, 13, 10, 30, 0))).toBe(
|
||||
'2026-05-13 10:30:00',
|
||||
);
|
||||
});
|
||||
});
|
||||
115
test/common/kt-date-time.decorator.spec.ts
Normal file
115
test/common/kt-date-time.decorator.spec.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { getMetadataArgsStorage, type ValueTransformer } from 'typeorm';
|
||||
import {
|
||||
transformKtDateTimeFields,
|
||||
formatKtDateTime,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtDateTimeField,
|
||||
KtUpdateDateColumn,
|
||||
vbenSuccess,
|
||||
} from '../../src/common';
|
||||
|
||||
class DateTimeFormatFixture {
|
||||
@KtDateTimeField()
|
||||
createTime!: KtDateTime;
|
||||
|
||||
@KtDateTimeField('YYYY/HH/DD')
|
||||
updateTime!: KtDateTime;
|
||||
}
|
||||
|
||||
class DateTimeColumnFixture {
|
||||
@KtDateTimeColumn('YYYY/HH/DD')
|
||||
publishTime!: KtDateTime | null;
|
||||
|
||||
@KtCreateDateColumn()
|
||||
createTime!: KtDateTime;
|
||||
|
||||
@KtUpdateDateColumn('YYYY-MM-DD')
|
||||
updateTime!: KtDateTime;
|
||||
}
|
||||
|
||||
describe('KtDateTime decorators', () => {
|
||||
it('wraps DTO fields with field-level format rules', () => {
|
||||
const data = new DateTimeFormatFixture();
|
||||
data.createTime = new Date(2026, 4, 13, 10, 30, 0);
|
||||
(data as any).updateTime = '2026-05-13T10:31:00';
|
||||
|
||||
const formatted = transformKtDateTimeFields(data);
|
||||
|
||||
expect(formatted.createTime).toBeInstanceOf(Date);
|
||||
expect(formatted.createTime).toBeInstanceOf(KtDateTime);
|
||||
expect(formatted.createTime.getTime()).toBe(data.createTime.getTime());
|
||||
expect(String(formatted.createTime)).toBe('2026-05-13 10:30:00');
|
||||
expect(String(formatted.updateTime)).toBe('2026/10/13');
|
||||
expect(JSON.stringify(formatted)).toBe(
|
||||
'{"createTime":"2026-05-13 10:30:00","updateTime":"2026/10/13"}',
|
||||
);
|
||||
expect(data.createTime).toBeInstanceOf(Date);
|
||||
expect(data.createTime).not.toBeInstanceOf(KtDateTime);
|
||||
expect(data.updateTime).toBe('2026-05-13T10:31:00');
|
||||
});
|
||||
|
||||
it('hydrates TypeORM date columns as KtDateTime with column format rules', () => {
|
||||
const metadata = getMetadataArgsStorage().columns.filter(
|
||||
(column) => column.target === DateTimeColumnFixture,
|
||||
);
|
||||
const getTransformer = (propertyName: string) =>
|
||||
metadata.find((column) => column.propertyName === propertyName)?.options
|
||||
.transformer as ValueTransformer;
|
||||
|
||||
const publishTime = getTransformer('publishTime').from(
|
||||
new Date(2026, 4, 13, 10, 30, 0),
|
||||
);
|
||||
const createTime = getTransformer('createTime').from(
|
||||
new Date(2026, 4, 13, 10, 31, 0),
|
||||
);
|
||||
const updateTime = getTransformer('updateTime').from(
|
||||
new Date(2026, 4, 13, 10, 32, 0),
|
||||
);
|
||||
|
||||
expect(publishTime).toBeInstanceOf(KtDateTime);
|
||||
expect(publishTime?.getTime()).toBe(
|
||||
new Date(2026, 4, 13, 10, 30, 0).getTime(),
|
||||
);
|
||||
expect(String(publishTime)).toBe('2026/10/13');
|
||||
expect(String(createTime)).toBe('2026-05-13 10:31:00');
|
||||
expect(String(updateTime)).toBe('2026-05-13');
|
||||
});
|
||||
|
||||
it('keeps DTO transformer datetime fields usable as Date values', () => {
|
||||
const data = new DateTimeFormatFixture();
|
||||
data.createTime = new Date(2026, 4, 13, 10, 30, 0);
|
||||
(data as any).updateTime = '2026-05-13T10:31:00';
|
||||
|
||||
const formatted = transformKtDateTimeFields(data);
|
||||
|
||||
expect(formatted.createTime).toBeInstanceOf(KtDateTime);
|
||||
expect(formatted.createTime.getTime()).toBe(
|
||||
new Date(2026, 4, 13, 10, 30, 0).getTime(),
|
||||
);
|
||||
expect(JSON.stringify(formatted)).toBe(
|
||||
'{"createTime":"2026-05-13 10:30:00","updateTime":"2026/10/13"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not recursively serialize plain response datetime values', () => {
|
||||
const createTime = new Date(2026, 4, 13, 10, 30, 0);
|
||||
const updateTime = new Date(2026, 4, 13, 10, 31, 0);
|
||||
const response = vbenSuccess({
|
||||
createTime,
|
||||
nested: {
|
||||
updateTime,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.data.createTime).toBe(createTime);
|
||||
expect(response.data.nested.updateTime).toBe(updateTime);
|
||||
});
|
||||
|
||||
it('exposes the configured formatter', () => {
|
||||
expect(formatKtDateTime(new Date(2026, 4, 13, 10, 30, 0))).toBe(
|
||||
'2026-05-13 10:30:00',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,9 @@
|
||||
jest.mock('@/common', () => {
|
||||
const actualCommon = jest.requireActual('@/common');
|
||||
return {
|
||||
FormatDateTime: actualCommon.FormatDateTime,
|
||||
...actualCommon,
|
||||
ToolsService: actualCommon.ToolsService,
|
||||
ensureSnowflakeId: jest.fn(),
|
||||
formatKtDateTime: actualCommon.formatKtDateTime,
|
||||
setDictDecodeCache: jest.fn(),
|
||||
throwVbenError: (message: string) => {
|
||||
throw new Error(message);
|
||||
@ -147,7 +146,9 @@ describe('QqbotNapcatLoginService', () => {
|
||||
|
||||
expect(result.status).toBe('pending');
|
||||
expect(result.qrcode).toBeUndefined();
|
||||
expect(result.errorMessage).toBe('NapCat 正在重置登录态并生成二维码,请稍后');
|
||||
expect(result.errorMessage).toBe(
|
||||
'NapCat 正在重置登录态并生成二维码,请稍后',
|
||||
);
|
||||
expect(containerService.resetRuntimeLoginState).toHaveBeenCalledWith(
|
||||
container,
|
||||
expect.any(Function),
|
||||
|
||||
@ -2,7 +2,11 @@ jest.mock(
|
||||
'@/common',
|
||||
() => ({
|
||||
ensureSnowflakeId: jest.fn(),
|
||||
FormatDateTime: () => () => undefined,
|
||||
KtCreateDateColumn: () => () => undefined,
|
||||
KtDateTime: Date,
|
||||
KtDateTimeColumn: () => () => undefined,
|
||||
KtDateTimeField: () => () => undefined,
|
||||
KtUpdateDateColumn: () => () => undefined,
|
||||
formatKtDateTime: (value: Date | number | string) => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return [
|
||||
@ -17,6 +21,7 @@ jest.mock(
|
||||
].join(' ');
|
||||
},
|
||||
setDictDecodeCache: jest.fn(),
|
||||
transformKtDateTimeFields: (value: unknown) => value,
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
jest.mock('@/common', () => {
|
||||
const actualCommon = jest.requireActual('@/common');
|
||||
return {
|
||||
FormatDateTime: actualCommon.FormatDateTime,
|
||||
...actualCommon,
|
||||
ToolsService: actualCommon.ToolsService,
|
||||
ensureSnowflakeId: jest.fn(),
|
||||
formatKtDateTime: actualCommon.formatKtDateTime,
|
||||
setDictDecodeCache: jest.fn(),
|
||||
throwVbenError: (message: string) => {
|
||||
throw new Error(message);
|
||||
|
||||
@ -2,7 +2,11 @@ jest.mock(
|
||||
'@/common',
|
||||
() => ({
|
||||
ensureSnowflakeId: jest.fn(),
|
||||
FormatDateTime: () => () => undefined,
|
||||
KtCreateDateColumn: () => () => undefined,
|
||||
KtDateTime: Date,
|
||||
KtDateTimeColumn: () => () => undefined,
|
||||
KtDateTimeField: () => () => undefined,
|
||||
KtUpdateDateColumn: () => () => undefined,
|
||||
formatKtDateTime: (value: Date | number | string) => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return [
|
||||
@ -17,6 +21,7 @@ jest.mock(
|
||||
].join(' ');
|
||||
},
|
||||
setDictDecodeCache: jest.fn(),
|
||||
transformKtDateTimeFields: (value: unknown) => value,
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import { formatKtDateTime, KtDateTime, KtDateTimeColumn } from '@/common';
|
||||
import {
|
||||
getEffectiveCooldownMs,
|
||||
isWithinCooldown,
|
||||
} from '@/qqbot/qqbot-cooldown.policy';
|
||||
|
||||
class CooldownEntityFixture {
|
||||
@KtDateTimeColumn()
|
||||
lastHitAt!: KtDateTime;
|
||||
}
|
||||
|
||||
describe('QQBot cooldown policy', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
@ -26,4 +32,20 @@ describe('QQBot cooldown policy', () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps decorated entity datetime usable for cooldown checks', () => {
|
||||
const entity = new CooldownEntityFixture();
|
||||
entity.lastHitAt = new KtDateTime(new Date(Date.now() - 10000));
|
||||
|
||||
expect(entity.lastHitAt).toBeInstanceOf(KtDateTime);
|
||||
expect(`${entity.lastHitAt}`).toBe(formatKtDateTime(entity.lastHitAt));
|
||||
expect(entity.lastHitAt).toBeInstanceOf(Date);
|
||||
expect(
|
||||
isWithinCooldown({
|
||||
cooldownMs: 500,
|
||||
lastHitAt: entity.lastHitAt,
|
||||
minCooldownMs: 30000,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user