feat: 重构时间序列化装饰器
This commit is contained in:
parent
ed5b1f23bd
commit
da5e5f27e2
490
API.md
490
API.md
@ -47,24 +47,24 @@ 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` |
|
||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD` |
|
||||
| Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_QUERY_SELECTOR` |
|
||||
| QQBot | `QQBOT_ENABLED`、`QQBOT_REVERSE_WS_PATH`、`QQBOT_REVERSE_WS_TOKEN`、`QQBOT_EVENT_BUS`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*` |
|
||||
| NapCat | `NAPCAT_WEBUI_BASE_URL`、`NAPCAT_WEBUI_TOKEN`、`QQBOT_NAPCAT_*` |
|
||||
| MQTT | `MQTT_URL`、`MQTT_USERNAME`、`MQTT_PASSWORD`、`MQTT_CLIENT_ID` |
|
||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_DEFAULT_WORLD` |
|
||||
| FFLogs | `FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||
| 分组 | 关键变量 |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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` |
|
||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD` |
|
||||
| Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_QUERY_SELECTOR` |
|
||||
| QQBot | `QQBOT_ENABLED`、`QQBOT_REVERSE_WS_PATH`、`QQBOT_REVERSE_WS_TOKEN`、`QQBOT_EVENT_BUS`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*` |
|
||||
| NapCat | `NAPCAT_WEBUI_BASE_URL`、`NAPCAT_WEBUI_TOKEN`、`QQBOT_NAPCAT_*` |
|
||||
| MQTT | `MQTT_URL`、`MQTT_USERNAME`、`MQTT_PASSWORD`、`MQTT_CLIENT_ID` |
|
||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_DEFAULT_WORLD` |
|
||||
| FFLogs | `FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||
|
||||
真实密码、Token、OAuth secret 和生产 env 不提交到 Git。
|
||||
|
||||
@ -72,103 +72,103 @@ 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 |
|
||||
| `GET` | `/auth/codes` | 获取当前用户按钮权限码 |
|
||||
| `GET` | `/user/info` | 获取当前用户信息 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | --------------- | ------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/auth/login` | 后台登录,返回 accessToken、用户信息和 WordPress 自动登录状态,并写入 httpOnly cookie |
|
||||
| `POST` | `/auth/refresh` | 通过 refresh token cookie 刷新 accessToken |
|
||||
| `POST` | `/auth/logout` | 清理 Admin 与 WordPress 登录 cookie |
|
||||
| `GET` | `/auth/codes` | 获取当前用户按钮权限码 |
|
||||
| `GET` | `/user/info` | 获取当前用户信息 |
|
||||
|
||||
`/auth/login` 会尝试用 env 中的 WordPress 管理员账号建立 WordPress 登录态。WordPress 不可用时,Admin 主登录仍成功,返回 `wordpressAuth=null`、`wordpressAvailable=false`,菜单和权限码会过滤 Blog 管理入口。
|
||||
|
||||
### Menu / Role / Dept / User Manage
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/menu/all` | 当前用户菜单 |
|
||||
| `GET` | `/system/menu/list` | 系统菜单树 |
|
||||
| `GET` | `/system/menu/name-exists` | 菜单 name 重名校验 |
|
||||
| `GET` | `/system/menu/path-exists` | 菜单 path 重名校验 |
|
||||
| `POST` | `/system/menu` | 新增菜单 |
|
||||
| `PUT` | `/system/menu/:id` | 更新菜单 |
|
||||
| `DELETE` | `/system/menu/:id` | 删除菜单及子菜单 |
|
||||
| `GET` | `/system/role/list` | 角色分页 |
|
||||
| `POST` | `/system/role` | 新增角色 |
|
||||
| `PUT` | `/system/role/:id` | 更新角色 |
|
||||
| `DELETE` | `/system/role/:id` | 删除角色 |
|
||||
| `GET` | `/system/dept/list` | 部门树 |
|
||||
| `POST` | `/system/dept` | 新增部门 |
|
||||
| `PUT` | `/system/dept/:id` | 更新部门 |
|
||||
| `DELETE` | `/system/dept/:id` | 删除部门 |
|
||||
| `GET` | `/system/user/list` | 用户分页 |
|
||||
| `POST` | `/system/user` | 新增用户 |
|
||||
| `PUT` | `/system/user/:id` | 更新用户 |
|
||||
| `DELETE` | `/system/user/:id` | 删除用户 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| -------- | -------------------------- | ------------------ |
|
||||
| `GET` | `/menu/all` | 当前用户菜单 |
|
||||
| `GET` | `/system/menu/list` | 系统菜单树 |
|
||||
| `GET` | `/system/menu/name-exists` | 菜单 name 重名校验 |
|
||||
| `GET` | `/system/menu/path-exists` | 菜单 path 重名校验 |
|
||||
| `POST` | `/system/menu` | 新增菜单 |
|
||||
| `PUT` | `/system/menu/:id` | 更新菜单 |
|
||||
| `DELETE` | `/system/menu/:id` | 删除菜单及子菜单 |
|
||||
| `GET` | `/system/role/list` | 角色分页 |
|
||||
| `POST` | `/system/role` | 新增角色 |
|
||||
| `PUT` | `/system/role/:id` | 更新角色 |
|
||||
| `DELETE` | `/system/role/:id` | 删除角色 |
|
||||
| `GET` | `/system/dept/list` | 部门树 |
|
||||
| `POST` | `/system/dept` | 新增部门 |
|
||||
| `PUT` | `/system/dept/:id` | 更新部门 |
|
||||
| `DELETE` | `/system/dept/:id` | 删除部门 |
|
||||
| `GET` | `/system/user/list` | 用户分页 |
|
||||
| `POST` | `/system/user` | 新增用户 |
|
||||
| `PUT` | `/system/user/:id` | 更新用户 |
|
||||
| `DELETE` | `/system/user/:id` | 删除用户 |
|
||||
|
||||
### Dict
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/dict/list` | 字典项分页,支持 `dictCode`、`keyword`、`label`、`value`、`childrenCode`、`status` |
|
||||
| `GET` | `/dict/tree` | 兼容树形字典视图 |
|
||||
| `GET` | `/dict/groups` | 字典编码分组列表,适合左右表左侧分组 |
|
||||
| `GET` | `/dict/codes` | 字典编码选项 |
|
||||
| `GET` | `/dict/getDictByKey` | 按 `dictKey` 获取启用字典项 |
|
||||
| `GET` | `/dict/getComponentDictByType` | 按组件一级类型查二级类型 |
|
||||
| `POST` | `/dict/save` | 新增字典项 |
|
||||
| `POST` | `/dict/update` | 更新字典项 |
|
||||
| `DELETE` | `/dict/:id` | 物理删除字典项 |
|
||||
| `POST` | `/dict/toggle` | 启停字典项 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| -------- | ------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `GET` | `/dict/list` | 字典项分页,支持 `dictCode`、`keyword`、`label`、`value`、`childrenCode`、`status` |
|
||||
| `GET` | `/dict/tree` | 兼容树形字典视图 |
|
||||
| `GET` | `/dict/groups` | 字典编码分组列表,适合左右表左侧分组 |
|
||||
| `GET` | `/dict/codes` | 字典编码选项 |
|
||||
| `GET` | `/dict/getDictByKey` | 按 `dictKey` 获取启用字典项 |
|
||||
| `GET` | `/dict/getComponentDictByType` | 按组件一级类型查二级类型 |
|
||||
| `POST` | `/dict/save` | 新增字典项 |
|
||||
| `POST` | `/dict/update` | 更新字典项 |
|
||||
| `DELETE` | `/dict/:id` | 物理删除字典项 |
|
||||
| `POST` | `/dict/toggle` | 启停字典项 |
|
||||
|
||||
字典核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `dictCode` | 字典分组,例如 `COMPONENT_TYPE`、`BANGDREAM_SERVER_ALIAS` |
|
||||
| `label` | 展示文本 |
|
||||
| `value` | 字典值 |
|
||||
| `childrenCode` | 关联子分组编码 |
|
||||
| `sort` | 排序 |
|
||||
| `status` | `1` 启用 |
|
||||
| 字段 | 说明 |
|
||||
| -------------- | --------------------------------------------------------- |
|
||||
| `dictCode` | 字典分组,例如 `COMPONENT_TYPE`、`BANGDREAM_SERVER_ALIAS` |
|
||||
| `label` | 展示文本 |
|
||||
| `value` | 字典值 |
|
||||
| `childrenCode` | 关联子分组编码 |
|
||||
| `sort` | 排序 |
|
||||
| `status` | `1` 启用 |
|
||||
|
||||
### Component
|
||||
|
||||
组件接口保持 `/component/*` 路径兼容,但数据表为 `admin_component`。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/component/allList` | 全量组件 |
|
||||
| `GET` | `/component/list` | 组件分页,支持 `pageNo`、`pageSize`、`name`、`type`、`componentType` |
|
||||
| `GET` | `/component/detail?id=` | 组件详情 |
|
||||
| `POST` | `/component/save` | 新增组件 |
|
||||
| `POST` | `/component/update` | 更新组件 |
|
||||
| `POST` | `/component/remove?id=` | 逻辑删除组件 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ----------------------- | -------------------------------------------------------------------- |
|
||||
| `GET` | `/component/allList` | 全量组件 |
|
||||
| `GET` | `/component/list` | 组件分页,支持 `pageNo`、`pageSize`、`name`、`type`、`componentType` |
|
||||
| `GET` | `/component/detail?id=` | 组件详情 |
|
||||
| `POST` | `/component/save` | 新增组件 |
|
||||
| `POST` | `/component/update` | 更新组件 |
|
||||
| `POST` | `/component/remove?id=` | 逻辑删除组件 |
|
||||
|
||||
### Timezone / Upload / Demo
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/timezone/getTimezoneOptions` | 时区选项 |
|
||||
| `GET` | `/timezone/getTimezone` | 当前用户时区 |
|
||||
| `POST` | `/timezone/setTimezone` | 设置当前用户时区 |
|
||||
| `POST` | `/upload` | Vben 上传适配,实际写入 MinIO |
|
||||
| `GET` | `/table/list` | Vben 示例表格 |
|
||||
| `GET` | `/status` | 状态码测试 |
|
||||
| `GET` | `/demo/bigint` | BigInt JSON 测试 |
|
||||
| `GET` | `/test` | GET 测试 |
|
||||
| `POST` | `/test` | POST 测试 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ------------------------------ | ----------------------------- |
|
||||
| `GET` | `/timezone/getTimezoneOptions` | 时区选项 |
|
||||
| `GET` | `/timezone/getTimezone` | 当前用户时区 |
|
||||
| `POST` | `/timezone/setTimezone` | 设置当前用户时区 |
|
||||
| `POST` | `/upload` | Vben 上传适配,实际写入 MinIO |
|
||||
| `GET` | `/table/list` | Vben 示例表格 |
|
||||
| `GET` | `/status` | 状态码测试 |
|
||||
| `GET` | `/demo/bigint` | BigInt JSON 测试 |
|
||||
| `GET` | `/test` | GET 测试 |
|
||||
| `POST` | `/test` | POST 测试 |
|
||||
|
||||
## 系统日志
|
||||
|
||||
后端通过 `nestjs-pino` 输出结构化日志。配置 Loki 后,Admin 日志页面通过后端代理查询,不直连 Loki。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/system/logs` | 日志分页,支持 `level`、`keyword`、`context`、`path`、`requestId`、`startTime`、`endTime`、`rangeMinutes` |
|
||||
| `GET` | `/system/logs/summary` | 按级别统计 |
|
||||
| `GET` | `/system/logs/levels` | 日志级别选项 |
|
||||
| `GET` | `/system/logs/status` | Loki 查询配置状态 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ----- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/system/logs` | 日志分页,支持 `level`、`keyword`、`context`、`path`、`requestId`、`startTime`、`endTime`、`rangeMinutes` |
|
||||
| `GET` | `/system/logs/summary` | 按级别统计 |
|
||||
| `GET` | `/system/logs/levels` | 日志级别选项 |
|
||||
| `GET` | `/system/logs/status` | Loki 查询配置状态 |
|
||||
|
||||
日志行包含 `timestamp`、`level`、`message`、`method`、`path`、`statusCode`、`durationMs`、`requestId`、`raw` 等字段。
|
||||
|
||||
@ -176,13 +176,13 @@ 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` | 删除站内信(逻辑删除) |
|
||||
| `POST` | `/system/notice/toggle` | 标记处理或重新打开(`id`、`status`,`1` 未处理,`0` 已处理) |
|
||||
| `POST` | `/system/notice/top` | 切换置顶(`id`、`isTop`) |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/system/notice/list` | 日志级站内信分页列表,支持 `keyword`、`severity`、`source`、`eventType`、`status`、`isTop`、`notifyRoleCode`、`notifyUsers`、`pageNo`、`pageSize` |
|
||||
| `GET` | `/system/notice/detail/:id` | 查询站内信详情 |
|
||||
| `DELETE` | `/system/notice/:id` | 删除站内信(逻辑删除) |
|
||||
| `POST` | `/system/notice/toggle` | 标记处理或重新打开(`id`、`status`,`1` 未处理,`0` 已处理) |
|
||||
| `POST` | `/system/notice/top` | 切换置顶(`id`、`isTop`) |
|
||||
|
||||
返回字段包含 `severity`、`source`、`eventType`、`dedupeKey`、`occurrenceCount`、`notifyRoleCode`、`metadata`、`firstSeenAt`、`lastSeenAt`。后端不暴露人工 `save/update` 入口。
|
||||
|
||||
@ -192,18 +192,18 @@ 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` | 后台文章分页 |
|
||||
| `GET` | `/blog/article/detail` | 后台文章详情 |
|
||||
| `POST` | `/blog/article/save` | 新增文章 |
|
||||
| `POST` | `/blog/article/update` | 更新文章 |
|
||||
| `POST` | `/blog/article/remove` | 删除文章 |
|
||||
| `GET` | `/blog/article/category-options` | 文章分类选项 |
|
||||
| `GET` | `/blog/article/tag-options` | 文章标签选项 |
|
||||
| `POST` | `/blog/article/import-wordpress` | 从 WordPress 导入文章 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | -------------------------------- | -------------------------- |
|
||||
| `GET` | `/blog/article/public/list` | 公开文章分页 |
|
||||
| `GET` | `/blog/article/public/detail` | 公开文章详情,支持 id/slug |
|
||||
| `GET` | `/blog/article/list` | 后台文章分页 |
|
||||
| `GET` | `/blog/article/detail` | 后台文章详情 |
|
||||
| `POST` | `/blog/article/save` | 新增文章 |
|
||||
| `POST` | `/blog/article/update` | 更新文章 |
|
||||
| `POST` | `/blog/article/remove` | 删除文章 |
|
||||
| `GET` | `/blog/article/category-options` | 文章分类选项 |
|
||||
| `GET` | `/blog/article/tag-options` | 文章标签选项 |
|
||||
| `POST` | `/blog/article/import-wordpress` | 从 WordPress 导入文章 |
|
||||
|
||||
文章 body 常用字段:
|
||||
|
||||
@ -222,70 +222,70 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
||||
|
||||
### Blog Category / Tag / Theme
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/blog/category/list` | 本地分类分页 |
|
||||
| `GET` | `/blog/category/detail` | 本地分类详情 |
|
||||
| `POST` | `/blog/category/save` | 新增分类 |
|
||||
| `POST` | `/blog/category/update` | 更新分类 |
|
||||
| `POST` | `/blog/category/remove` | 删除分类 |
|
||||
| `GET` | `/blog/tag/list` | 本地标签分页 |
|
||||
| `GET` | `/blog/tag/detail` | 本地标签详情 |
|
||||
| `POST` | `/blog/tag/save` | 新增标签 |
|
||||
| `POST` | `/blog/tag/update` | 更新标签 |
|
||||
| `POST` | `/blog/tag/remove` | 删除标签 |
|
||||
| `GET` | `/blog/term/options` | 分类/标签选项 |
|
||||
| `GET` | `/blog/theme/config` | 获取 Argon 主题配置 |
|
||||
| `POST` | `/blog/theme/save` | 保存本地主题配置 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ------------------------------ | ------------------------- |
|
||||
| `GET` | `/blog/category/list` | 本地分类分页 |
|
||||
| `GET` | `/blog/category/detail` | 本地分类详情 |
|
||||
| `POST` | `/blog/category/save` | 新增分类 |
|
||||
| `POST` | `/blog/category/update` | 更新分类 |
|
||||
| `POST` | `/blog/category/remove` | 删除分类 |
|
||||
| `GET` | `/blog/tag/list` | 本地标签分页 |
|
||||
| `GET` | `/blog/tag/detail` | 本地标签详情 |
|
||||
| `POST` | `/blog/tag/save` | 新增标签 |
|
||||
| `POST` | `/blog/tag/update` | 更新标签 |
|
||||
| `POST` | `/blog/tag/remove` | 删除标签 |
|
||||
| `GET` | `/blog/term/options` | 分类/标签选项 |
|
||||
| `GET` | `/blog/theme/config` | 获取 Argon 主题配置 |
|
||||
| `POST` | `/blog/theme/save` | 保存本地主题配置 |
|
||||
| `POST` | `/blog/theme/import-wordpress` | 从 WordPress 导入主题配置 |
|
||||
|
||||
## WordPress 代理
|
||||
|
||||
`/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 登录态 |
|
||||
| `GET` | `/wordpress/theme/config` | 读取 WordPress Argon 主题配置 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ------------------------- | ----------------------------------------------- |
|
||||
| `POST` | `/wordpress/auth/login` | 使用 env 管理员账号登录 WordPress 并写入 cookie |
|
||||
| `POST` | `/wordpress/auth/logout` | 清理 WordPress cookie |
|
||||
| `GET` | `/wordpress/auth/check` | 校验 WordPress 登录态 |
|
||||
| `GET` | `/wordpress/theme/config` | 读取 WordPress Argon 主题配置 |
|
||||
|
||||
### WordPress Article / Tag / Category
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/wordpress/article/public/list` | 公开文章列表代理 |
|
||||
| `GET` | `/wordpress/article/public/detail` | 公开文章详情代理 |
|
||||
| `GET` | `/wordpress/article/list` | WordPress 文章分页 |
|
||||
| `GET` | `/wordpress/article/detail` | WordPress 文章详情 |
|
||||
| `POST` | `/wordpress/article/save` | 新增 WordPress 文章 |
|
||||
| `POST` | `/wordpress/article/update` | 更新 WordPress 文章 |
|
||||
| `POST` | `/wordpress/article/remove` | 删除 WordPress 文章 |
|
||||
| `GET` | `/wordpress/tag/list` | 标签分页 |
|
||||
| `GET` | `/wordpress/tag/detail` | 标签详情 |
|
||||
| `POST` | `/wordpress/tag/save` | 新增标签 |
|
||||
| `POST` | `/wordpress/tag/update` | 更新标签 |
|
||||
| `POST` | `/wordpress/tag/remove` | 删除标签 |
|
||||
| `GET` | `/wordpress/category/list` | 分类分页 |
|
||||
| `GET` | `/wordpress/category/detail` | 分类详情 |
|
||||
| `POST` | `/wordpress/category/save` | 新增分类 |
|
||||
| `POST` | `/wordpress/category/update` | 更新分类 |
|
||||
| `POST` | `/wordpress/category/remove` | 删除分类 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ---------------------------------- | ------------------- |
|
||||
| `GET` | `/wordpress/article/public/list` | 公开文章列表代理 |
|
||||
| `GET` | `/wordpress/article/public/detail` | 公开文章详情代理 |
|
||||
| `GET` | `/wordpress/article/list` | WordPress 文章分页 |
|
||||
| `GET` | `/wordpress/article/detail` | WordPress 文章详情 |
|
||||
| `POST` | `/wordpress/article/save` | 新增 WordPress 文章 |
|
||||
| `POST` | `/wordpress/article/update` | 更新 WordPress 文章 |
|
||||
| `POST` | `/wordpress/article/remove` | 删除 WordPress 文章 |
|
||||
| `GET` | `/wordpress/tag/list` | 标签分页 |
|
||||
| `GET` | `/wordpress/tag/detail` | 标签详情 |
|
||||
| `POST` | `/wordpress/tag/save` | 新增标签 |
|
||||
| `POST` | `/wordpress/tag/update` | 更新标签 |
|
||||
| `POST` | `/wordpress/tag/remove` | 删除标签 |
|
||||
| `GET` | `/wordpress/category/list` | 分类分页 |
|
||||
| `GET` | `/wordpress/category/detail` | 分类详情 |
|
||||
| `POST` | `/wordpress/category/save` | 新增分类 |
|
||||
| `POST` | `/wordpress/category/update` | 更新分类 |
|
||||
| `POST` | `/wordpress/category/remove` | 删除分类 |
|
||||
|
||||
WordPress rewrite 未开启导致 `/wp-json/*` 返回 404 时,后端会回退到 `?rest_route=/...`。
|
||||
|
||||
## MinIO
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/minio/check` | 检查连接和 bucket |
|
||||
| `POST` | `/minio/bucket` | 创建 bucket |
|
||||
| `POST` | `/minio/upload` | 上传文件,`multipart/form-data` |
|
||||
| `GET` | `/minio/list` | 文件列表 |
|
||||
| `GET` | `/minio/url` | 临时访问 URL |
|
||||
| `GET` | `/minio/resource-proxy` | 代理读取资源 |
|
||||
| `GET` | `/minio/download` | 下载文件流 |
|
||||
| `DELETE` | `/minio/remove` | 删除文件 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| -------- | ----------------------- | ------------------------------- |
|
||||
| `GET` | `/minio/check` | 检查连接和 bucket |
|
||||
| `POST` | `/minio/bucket` | 创建 bucket |
|
||||
| `POST` | `/minio/upload` | 上传文件,`multipart/form-data` |
|
||||
| `GET` | `/minio/list` | 文件列表 |
|
||||
| `GET` | `/minio/url` | 临时访问 URL |
|
||||
| `GET` | `/minio/resource-proxy` | 代理读取资源 |
|
||||
| `GET` | `/minio/download` | 下载文件流 |
|
||||
| `DELETE` | `/minio/remove` | 删除文件 |
|
||||
|
||||
`bucketName` 不传时使用 `MINIO_BUCKET`。
|
||||
|
||||
@ -295,24 +295,24 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
### Account / Scan Login
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/qqbot/account/list` | QQBot 账号分页 |
|
||||
| `GET` | `/qqbot/account/enabled` | 启用账号列表 |
|
||||
| `POST` | `/qqbot/account/save` | 手动新增账号 |
|
||||
| `POST` | `/qqbot/account/update` | 更新账号 |
|
||||
| `POST` | `/qqbot/account/scan/create` | 扫码新增账号,创建登录会话 |
|
||||
| `POST` | `/qqbot/account/scan/refresh?id=` | 对已有账号刷新登录态 |
|
||||
| `GET` | `/qqbot/account/scan/status?sessionId=` | 查询扫码会话状态 |
|
||||
| `GET` | `/qqbot/account/scan/events?sessionId=` | SSE 订阅扫码进度 |
|
||||
| `POST` | `/qqbot/account/scan/qrcode/refresh?sessionId=` | 刷新当前会话二维码 |
|
||||
| `POST` | `/qqbot/account/scan/cancel?sessionId=` | 取消扫码会话 |
|
||||
| `POST` | `/qqbot/account/delete?id=` | 删除账号并断开 WS |
|
||||
| `POST` | `/qqbot/account/kick?selfId=` | 断开反向 WS 会话 |
|
||||
| `POST` | `/qqbot/account/bind/command` | 绑定账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/unbind/command` | 解绑账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/bind/rule` | 绑定账号和自动回复规则 |
|
||||
| `POST` | `/qqbot/account/unbind/rule` | 解绑账号和自动回复规则 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ----------------------------------------------- | -------------------------- |
|
||||
| `GET` | `/qqbot/account/list` | QQBot 账号分页 |
|
||||
| `GET` | `/qqbot/account/enabled` | 启用账号列表 |
|
||||
| `POST` | `/qqbot/account/save` | 手动新增账号 |
|
||||
| `POST` | `/qqbot/account/update` | 更新账号 |
|
||||
| `POST` | `/qqbot/account/scan/create` | 扫码新增账号,创建登录会话 |
|
||||
| `POST` | `/qqbot/account/scan/refresh?id=` | 对已有账号刷新登录态 |
|
||||
| `GET` | `/qqbot/account/scan/status?sessionId=` | 查询扫码会话状态 |
|
||||
| `GET` | `/qqbot/account/scan/events?sessionId=` | SSE 订阅扫码进度 |
|
||||
| `POST` | `/qqbot/account/scan/qrcode/refresh?sessionId=` | 刷新当前会话二维码 |
|
||||
| `POST` | `/qqbot/account/scan/cancel?sessionId=` | 取消扫码会话 |
|
||||
| `POST` | `/qqbot/account/delete?id=` | 删除账号并断开 WS |
|
||||
| `POST` | `/qqbot/account/kick?selfId=` | 断开反向 WS 会话 |
|
||||
| `POST` | `/qqbot/account/bind/command` | 绑定账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/unbind/command` | 解绑账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/bind/rule` | 绑定账号和自动回复规则 |
|
||||
| `POST` | `/qqbot/account/unbind/rule` | 解绑账号和自动回复规则 |
|
||||
|
||||
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成。
|
||||
|
||||
@ -322,29 +322,29 @@ 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` | 更新在线命令 |
|
||||
| `POST` | `/qqbot/command/delete?id=` | 删除在线命令 |
|
||||
| `POST` | `/qqbot/command/toggle?id=&enabled=` | 启停在线命令 |
|
||||
| `POST` | `/qqbot/command/test` | 预览测试在线命令 |
|
||||
| `GET` | `/qqbot/rule/list` | 自动回复规则分页 |
|
||||
| `POST` | `/qqbot/rule/save` | 新增自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/update` | 更新自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/delete?id=` | 删除自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/toggle?id=&enabled=` | 启停自动回复规则 |
|
||||
| `GET` | `/qqbot/permission/config` | 权限名单配置 |
|
||||
| `POST` | `/qqbot/permission/config` | 保存权限名单配置 |
|
||||
| `GET` | `/qqbot/permission/allowlist` | 白名单分页 |
|
||||
| `POST` | `/qqbot/permission/allowlist/save` | 新增白名单 |
|
||||
| `POST` | `/qqbot/permission/allowlist/update` | 更新白名单 |
|
||||
| `POST` | `/qqbot/permission/allowlist/delete?id=` | 删除白名单 |
|
||||
| `GET` | `/qqbot/permission/blocklist` | 黑名单分页 |
|
||||
| `POST` | `/qqbot/permission/blocklist/save` | 新增黑名单 |
|
||||
| `POST` | `/qqbot/permission/blocklist/update` | 更新黑名单 |
|
||||
| `POST` | `/qqbot/permission/blocklist/delete?id=` | 删除黑名单 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ---------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `GET` | `/qqbot/command/list` | 在线命令分页,支持 `pluginKey`、`operationKey`、`selfId`、`enabled` |
|
||||
| `POST` | `/qqbot/command/save` | 新增在线命令 |
|
||||
| `POST` | `/qqbot/command/update` | 更新在线命令 |
|
||||
| `POST` | `/qqbot/command/delete?id=` | 删除在线命令 |
|
||||
| `POST` | `/qqbot/command/toggle?id=&enabled=` | 启停在线命令 |
|
||||
| `POST` | `/qqbot/command/test` | 预览测试在线命令 |
|
||||
| `GET` | `/qqbot/rule/list` | 自动回复规则分页 |
|
||||
| `POST` | `/qqbot/rule/save` | 新增自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/update` | 更新自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/delete?id=` | 删除自动回复规则 |
|
||||
| `POST` | `/qqbot/rule/toggle?id=&enabled=` | 启停自动回复规则 |
|
||||
| `GET` | `/qqbot/permission/config` | 权限名单配置 |
|
||||
| `POST` | `/qqbot/permission/config` | 保存权限名单配置 |
|
||||
| `GET` | `/qqbot/permission/allowlist` | 白名单分页 |
|
||||
| `POST` | `/qqbot/permission/allowlist/save` | 新增白名单 |
|
||||
| `POST` | `/qqbot/permission/allowlist/update` | 更新白名单 |
|
||||
| `POST` | `/qqbot/permission/allowlist/delete?id=` | 删除白名单 |
|
||||
| `GET` | `/qqbot/permission/blocklist` | 黑名单分页 |
|
||||
| `POST` | `/qqbot/permission/blocklist/save` | 新增黑名单 |
|
||||
| `POST` | `/qqbot/permission/blocklist/update` | 更新黑名单 |
|
||||
| `POST` | `/qqbot/permission/blocklist/delete?id=` | 删除黑名单 |
|
||||
|
||||
`/qqbot/command/test` 示例:
|
||||
|
||||
@ -363,20 +363,20 @@ 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` | 插件健康检查 |
|
||||
| `GET` | `/qqbot/plugin/event/list` | 事件触发插件绑定状态 |
|
||||
| `POST` | `/qqbot/plugin/event/bind` | 绑定事件触发插件 |
|
||||
| `POST` | `/qqbot/plugin/event/unbind` | 解绑事件触发插件 |
|
||||
| `GET` | `/qqbot/dashboard/summary` | QQBot 工作台汇总 |
|
||||
| `GET` | `/qqbot/send/log/list` | 发送日志分页 |
|
||||
| `POST` | `/qqbot/send/private` | 发送私聊消息 |
|
||||
| `POST` | `/qqbot/send/group` | 发送群聊消息 |
|
||||
| `GET` | `/qqbot/conversation/list` | 会话列表 |
|
||||
| `GET` | `/qqbot/message/list` | 消息列表 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ------ | ------------------------------ | ------------------------------------------ |
|
||||
| `GET` | `/qqbot/plugin/list` | 插件列表,支持 `triggerMode=command/event` |
|
||||
| `GET` | `/qqbot/plugin/operation/list` | 插件能力列表 |
|
||||
| `GET` | `/qqbot/plugin/health` | 插件健康检查 |
|
||||
| `GET` | `/qqbot/plugin/event/list` | 事件触发插件绑定状态 |
|
||||
| `POST` | `/qqbot/plugin/event/bind` | 绑定事件触发插件 |
|
||||
| `POST` | `/qqbot/plugin/event/unbind` | 解绑事件触发插件 |
|
||||
| `GET` | `/qqbot/dashboard/summary` | QQBot 工作台汇总 |
|
||||
| `GET` | `/qqbot/send/log/list` | 发送日志分页 |
|
||||
| `POST` | `/qqbot/send/private` | 发送私聊消息 |
|
||||
| `POST` | `/qqbot/send/group` | 发送群聊消息 |
|
||||
| `GET` | `/qqbot/conversation/list` | 会话列表 |
|
||||
| `GET` | `/qqbot/message/list` | 消息列表 |
|
||||
|
||||
### OneBot Reverse WebSocket
|
||||
|
||||
@ -388,23 +388,23 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
插件 key:`bangDream`。当前源码根目录为 `src/qqbot/plugins/bangDream`,不再使用旧 `tsugu` 子目录。
|
||||
|
||||
| operation key | 命令 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `bangdream.song.search` | `/查曲` | 查歌曲信息图片 |
|
||||
| `bangdream.song.chart` | `/查谱面` | 查谱面图片 |
|
||||
| `bangdream.song.random` | `/随机曲` | 随机歌曲 |
|
||||
| `bangdream.song.meta` | `/查询分数表` | 查歌曲分数榜 |
|
||||
| `bangdream.card.search` | `/查卡` | 查卡牌信息图片 |
|
||||
| `bangdream.card.illustration` | `/查卡面` | 查卡面插画 |
|
||||
| `bangdream.character.search` | `/查角色` | 查角色信息 |
|
||||
| `bangdream.event.search` | `/查活动` | 查活动信息 |
|
||||
| `bangdream.event.stage` | `/查试炼` | 查活动试炼,保持拆图输出 |
|
||||
| `bangdream.player.search` | `/查玩家` | 查玩家信息 |
|
||||
| `bangdream.gacha.search` | `/查卡池` | 查卡池 |
|
||||
| `bangdream.gacha.simulate` | `/抽卡模拟` | 模拟抽卡 |
|
||||
| `bangdream.cutoff.detail` | `/ycx` | 单档位预测线 |
|
||||
| `bangdream.cutoff.all` | `/ycxall` | 全档位预测线 |
|
||||
| `bangdream.cutoff.recent` | `/lsycx` | 历史/近期档线 |
|
||||
| operation key | 命令 | 说明 |
|
||||
| ----------------------------- | ------------- | ------------------------ |
|
||||
| `bangdream.song.search` | `/查曲` | 查歌曲信息图片 |
|
||||
| `bangdream.song.chart` | `/查谱面` | 查谱面图片 |
|
||||
| `bangdream.song.random` | `/随机曲` | 随机歌曲 |
|
||||
| `bangdream.song.meta` | `/查询分数表` | 查歌曲分数榜 |
|
||||
| `bangdream.card.search` | `/查卡` | 查卡牌信息图片 |
|
||||
| `bangdream.card.illustration` | `/查卡面` | 查卡面插画 |
|
||||
| `bangdream.character.search` | `/查角色` | 查角色信息 |
|
||||
| `bangdream.event.search` | `/查活动` | 查活动信息 |
|
||||
| `bangdream.event.stage` | `/查试炼` | 查活动试炼,保持拆图输出 |
|
||||
| `bangdream.player.search` | `/查玩家` | 查玩家信息 |
|
||||
| `bangdream.gacha.search` | `/查卡池` | 查卡池 |
|
||||
| `bangdream.gacha.simulate` | `/抽卡模拟` | 模拟抽卡 |
|
||||
| `bangdream.cutoff.detail` | `/ycx` | 单档位预测线 |
|
||||
| `bangdream.cutoff.all` | `/ycxall` | 全档位预测线 |
|
||||
| `bangdream.cutoff.recent` | `/lsycx` | 历史/近期档线 |
|
||||
|
||||
`registry/operation-registry.ts` 是 BangDream operation、handlerName、别名、冷却和说明的单一来源。新增或调整命令必须同步在线命令 SQL,并跑 registry/command-SQL 测试。
|
||||
|
||||
@ -412,9 +412,9 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
插件 key:`ff14Market`。
|
||||
|
||||
| operation key | 说明 |
|
||||
| --- | --- |
|
||||
| `ff14.item.resolve` | 按物品名称或 ID 解析 XIVAPI 物品 |
|
||||
| operation key | 说明 |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `ff14.item.resolve` | 按物品名称或 ID 解析 XIVAPI 物品 |
|
||||
| `ff14.market.price` | 查询指定服务器/大区的 Universalis 市场价格 |
|
||||
|
||||
市场查价支持 `item`、`itemId`、`world`、`dataCenter`、`region`、`hq`、`language`。
|
||||
@ -423,26 +423,26 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
插件 key:`fflogs`。
|
||||
|
||||
| operation key | 说明 |
|
||||
| --- | --- |
|
||||
| operation key | 说明 |
|
||||
| -------------------------- | --------------------------------------------------------------- |
|
||||
| `fflogs.character.summary` | 查询 FFLogs 角色公开排名;传 `encounter` 时查询指定高难最近记录 |
|
||||
|
||||
常用输入:`characterName`、`serverSlug`、`serverRegion`、`encounter`、`limit`、`metric`、`timeframe`、`zoneId`。
|
||||
|
||||
## 初始化 SQL
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `sql/vben-admin-init.sql` | 创建 Admin 基础表、用户、角色、菜单、部门、字典和空组件表 |
|
||||
| `sql/blog-init.sql` | 初始化本地 Blog 表 |
|
||||
| `sql/blog-menu.sql` | 初始化 Blog 管理菜单 |
|
||||
| `sql/qqbot-init.sql` | 初始化 QQBot 表、插件命令和字典 |
|
||||
| `sql/system-log-menu.sql` | 初始化系统日志菜单和权限 |
|
||||
| `sql/system-notice-menu.sql` | 初始化系统站内信表与菜单权限 |
|
||||
| `sql/migrate-dict-to-admin-dict.sql` | 旧 `dict` 迁移到 `admin_dict` |
|
||||
| `sql/migrate-component-to-admin-component.sql` | 旧 `component` 迁移到 `admin_component` |
|
||||
| `sql/fix-admin-menu-meta.sql` | 修复菜单 meta 被覆盖为空 |
|
||||
| `sql/fix-admin-user-zero-id.sql` | 修复旧版本 `admin_user.id=0` 脏数据 |
|
||||
| 文件 | 用途 |
|
||||
| ---------------------------------------------- | --------------------------------------------------------- |
|
||||
| `sql/vben-admin-init.sql` | 创建 Admin 基础表、用户、角色、菜单、部门、字典和空组件表 |
|
||||
| `sql/blog-init.sql` | 初始化本地 Blog 表 |
|
||||
| `sql/blog-menu.sql` | 初始化 Blog 管理菜单 |
|
||||
| `sql/qqbot-init.sql` | 初始化 QQBot 表、插件命令和字典 |
|
||||
| `sql/system-log-menu.sql` | 初始化系统日志菜单和权限 |
|
||||
| `sql/system-notice-menu.sql` | 初始化系统站内信表与菜单权限 |
|
||||
| `sql/migrate-dict-to-admin-dict.sql` | 旧 `dict` 迁移到 `admin_dict` |
|
||||
| `sql/migrate-component-to-admin-component.sql` | 旧 `component` 迁移到 `admin_component` |
|
||||
| `sql/fix-admin-menu-meta.sql` | 修复菜单 meta 被覆盖为空 |
|
||||
| `sql/fix-admin-user-zero-id.sql` | 修复旧版本 `admin_user.id=0` 脏数据 |
|
||||
|
||||
## 验证入口
|
||||
|
||||
|
||||
50
README.md
50
README.md
@ -16,17 +16,17 @@
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
| --- | --- |
|
||||
| `admin` | Vben Admin 认证、用户、菜单、角色、部门、时区、字典、组件模板、系统日志 |
|
||||
| `blog` | 本地博客文章、分类、标签、Argon 主题配置和 WordPress 导入 |
|
||||
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
||||
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志 |
|
||||
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
||||
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
||||
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
||||
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
||||
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
|
||||
| 模块 | 说明 |
|
||||
| -------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `admin` | Vben Admin 认证、用户、菜单、角色、部门、时区、字典、组件模板、系统日志 |
|
||||
| `blog` | 本地博客文章、分类、标签、Argon 主题配置和 WordPress 导入 |
|
||||
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
||||
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志 |
|
||||
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
||||
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
||||
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
||||
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
||||
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
|
||||
|
||||
## 目录结构
|
||||
|
||||
@ -53,17 +53,17 @@ 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` |
|
||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD`、`WORDPRESS_*_TIMEOUT_MS` |
|
||||
| Logging/Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_*` |
|
||||
| 分组 | 变量 |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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` |
|
||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD`、`WORDPRESS_*_TIMEOUT_MS` |
|
||||
| Logging/Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_*` |
|
||||
| QQBot/NapCat | `QQBOT_ENABLED`、`QQBOT_REVERSE_WS_*`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*`、`NAPCAT_*`、`QQBOT_NAPCAT_*`、`MQTT_*` |
|
||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_MARKET_CACHE_TTL_MS` |
|
||||
| FFLogs | `FFLOGS_BASE_URL`、`FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_MARKET_CACHE_TTL_MS` |
|
||||
| FFLogs | `FFLOGS_BASE_URL`、`FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||
|
||||
`DB_SYNC=true` 只适合本地开发或明确允许自动同步表结构的环境;生产应关闭并使用 SQL/迁移脚本。
|
||||
|
||||
@ -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()`。
|
||||
@ -173,6 +173,6 @@ BangDream 图片能力改动:
|
||||
|
||||
## 来源与许可证
|
||||
|
||||
| 一级来源 | 使用方式 | License |
|
||||
| --- | --- | --- |
|
||||
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
|
||||
| 一级来源 | 使用方式 | 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