mirror of
https://github.com/KwiTsukasa/kt-template-online-api.git
synced 2026-05-27 15:44:54 +08:00
feat: 支持数据库字典映射并整理公共能力
- 新增 dict 实体表,字典查询改为从数据库读取 - 使用 childrenKey 实现组件一级类型到二级字典的关系映射 - Component 查询后通过 DecodeDictKey 和 AfterLoad 自动补充翻译字段 - 移除 ComponentDto、本地字典常量和本地 seed 文件 - 将 common 能力按 decorators/interceptors/services/swagger 重新分层 - 使用全局 SaveBodyInterceptor 替代 SaveMiddleware 统一处理 save 请求 - 更新 MinIO、组件、字典接口的 Swagger 响应和少量关键注释 - 重写 README.md 和 API.md,补充项目功能、目录结构和接口说明 BREAKING CHANGE: 字典数据依赖数据库 dict 表维护,不再使用本地字典常量。
This commit is contained in:
parent
f236d86ff3
commit
7291fed682
175
API.md
175
API.md
@ -1,8 +1,8 @@
|
||||
# KT Template Online API
|
||||
|
||||
后端服务默认监听 `48085`,Swagger 地址为 `/api`,OpenAPI JSON 地址为 `/api-json`。接口除文件下载外,统一返回 `{ code, msg, data }`。
|
||||
后端服务默认监听 `48085`,Swagger 地址为 `/api`,OpenAPI JSON 地址为 `/api-json`。
|
||||
|
||||
## 通用响应
|
||||
除文件下载接口外,接口统一返回:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -22,50 +22,92 @@
|
||||
}
|
||||
```
|
||||
|
||||
## Root
|
||||
## 功能模块
|
||||
|
||||
### GET `/`
|
||||
| 模块 | 说明 |
|
||||
| --------- | --------------------------------------------------------------------- |
|
||||
| Component | 组件/图表模板的列表、详情、新增、编辑、逻辑删除 |
|
||||
| Dict | 数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 |
|
||||
| MinIO | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 |
|
||||
| Common | 统一响应 Swagger 注解、字典翻译注解、`POST */save` 请求体规范化拦截器 |
|
||||
|
||||
重定向到 Swagger 文档页 `/api#/`,HTTP 状态码为 `301`。
|
||||
## 通用规则
|
||||
|
||||
### Save 请求体规范化
|
||||
|
||||
系统全局注册 `SaveBodyInterceptor`,默认会对 `POST */save` 请求删除 `body.id`,避免新增接口因为前端误传 `id` 而走指定主键保存。
|
||||
|
||||
如果个别接口需要保留 `id`,可在对应 Controller 方法上使用 `@SkipSaveBodyNormalize()`。
|
||||
|
||||
### 数据库字典翻译
|
||||
|
||||
字典数据维护在数据库 `dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射。
|
||||
|
||||
`dict` 表核心字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ------- | ------------------------------------------------------ |
|
||||
| id | string | 字典 ID |
|
||||
| dictKey | string | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
||||
| label | string | 展示文本 |
|
||||
| value | string | 字典值 |
|
||||
| childrenKey | string | 子字典分组,例如 `COMPONENT_TYPE.value=1` 指向 `CHART` |
|
||||
| sort | number | 排序 |
|
||||
| is_deleted | boolean | 逻辑删除标记 |
|
||||
|
||||
当前数据库示例关系:
|
||||
|
||||
| dictKey | value | label | childrenKey |
|
||||
| -------------- | ----- | ----- | ----------- |
|
||||
| COMPONENT_TYPE | 1 | 图表 | CHART |
|
||||
| COMPONENT_TYPE | 2 | 组件 | COMPONENT |
|
||||
|
||||
## 数据结构
|
||||
|
||||
### Component
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------------- | ------- | ---------------------------- |
|
||||
| ---------------- | ------- | ---------------------------------- |
|
||||
| id | string | 组件 ID,新增时由后端生成 |
|
||||
| name | string | 组件名称 |
|
||||
| type | number | 一级类型,`1` 图表,`2` 组件 |
|
||||
| componentType | number | 二级类型 |
|
||||
| typeMsg | string | 一级类型文本,列表接口返回 |
|
||||
| componentTypeMsg | string | 二级类型文本,列表接口返回 |
|
||||
| image | string | 封面图 |
|
||||
| template | string | playground 序列化模板内容 |
|
||||
| type | number | 一级类型,实际含义由 `dict` 表维护 |
|
||||
| componentType | number | 二级类型,实际含义由 `dict` 表维护 |
|
||||
| typeMsg | string | 一级类型文本,查询后自动映射 |
|
||||
| componentTypeMsg | string | 二级类型文本,查询后自动映射 |
|
||||
| image | string | 封面图或封面图地址 |
|
||||
| template | string | Playground 序列化模板内容 |
|
||||
| createTime | string | 创建时间 |
|
||||
| updateTime | string | 更新时间 |
|
||||
| is_deleted | boolean | 逻辑删除标记 |
|
||||
|
||||
### 字典
|
||||
### Dict
|
||||
|
||||
`COMPONENT_TYPE`:
|
||||
接口返回的字典项结构:
|
||||
|
||||
| label | value |
|
||||
| ----- | ----- |
|
||||
| 图表 | 1 |
|
||||
| 组件 | 2 |
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----- | ------------- | -------- |
|
||||
| label | string | 展示文本 |
|
||||
| value | number/string | 字典值 |
|
||||
|
||||
`CHART`:`未分类(-1)`、`折线图(1)`、`柱状图(2)`、`饼图(3)`、`散点图(4)`、`地图(5)`、`K线图(6)`、`雷达图(7)`、`盒须图(8)`、`热力图(9)`、`关系图(10)`、`路径图(11)`、`树图(12)`、`矩树图(13)`、`旭日图(14)`、`平行坐标系(15)`、`桑基图(16)`、`漏斗图(17)`、`仪表盘(18)`、`象形图(19)`、`河流图(20)`、`水球(21)`、`词云(22)`。
|
||||
### MinIO
|
||||
|
||||
`COMPONENT`:`未分类(-1)`、`表格(23)`、`表单(24)`、`容器(25)`。
|
||||
`bucketName` 未传时默认读取环境变量 `MINIO_BUCKET`,缺省值为 `kt-template-online`。
|
||||
|
||||
## Component
|
||||
## Root
|
||||
|
||||
### GET `/`
|
||||
|
||||
重定向到 Swagger 文档页 `/api#/`,HTTP 状态码为 `301`。
|
||||
|
||||
## Component 接口
|
||||
|
||||
### GET `/component/allList`
|
||||
|
||||
获取全部组件。
|
||||
|
||||
响应示例:
|
||||
响应 `data`:`Component[]`。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -91,19 +133,26 @@
|
||||
|
||||
### GET `/component/list`
|
||||
|
||||
分页获取组件列表。
|
||||
分页获取组件列表。列表默认过滤 `is_deleted=false`,并支持按名称模糊搜索。
|
||||
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ------------ |
|
||||
| ------------- | ------ | ---- | ---------------- |
|
||||
| pageNo | number | 是 | 页码 |
|
||||
| pageSize | number | 是 | 每页条数 |
|
||||
| name | string | 否 | 名称模糊搜索 |
|
||||
| name | string | 否 | 组件名称模糊搜索 |
|
||||
| type | number | 否 | 一级类型 |
|
||||
| componentType | number | 否 | 二级类型 |
|
||||
|
||||
响应 `data`:`{ list: Component[], total: number }`。
|
||||
响应 `data`:
|
||||
|
||||
```ts
|
||||
{
|
||||
list: Component[]
|
||||
total: number
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/component/detail`
|
||||
|
||||
@ -119,7 +168,7 @@ Query:
|
||||
|
||||
### POST `/component/save`
|
||||
|
||||
新增组件。`SaveMiddleware` 会删除 body 中的 `id`,新增时不需要传 `id`。
|
||||
新增组件。全局 `SaveBodyInterceptor` 会删除 `body.id`,新增时不需要传 `id`。
|
||||
|
||||
Body:
|
||||
|
||||
@ -133,7 +182,7 @@ Body:
|
||||
}
|
||||
```
|
||||
|
||||
响应示例:
|
||||
响应 `data`:新增组件 ID。
|
||||
|
||||
```json
|
||||
{
|
||||
@ -174,17 +223,17 @@ Query:
|
||||
|
||||
响应 `data`:`true` 表示删除成功。
|
||||
|
||||
## Dict
|
||||
## Dict 接口
|
||||
|
||||
### GET `/dict/getDictByKey`
|
||||
|
||||
根据字典 key 获取字典。
|
||||
根据字典分组获取字典项。
|
||||
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 可选值 |
|
||||
| ------- | ------ | ---- | -------------------------------------- |
|
||||
| dictKey | string | 是 | `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------- | ------ | ---- | ---------------------- |
|
||||
| dictKey | string | 是 | 字典分组,例如 `CHART` |
|
||||
|
||||
响应示例:
|
||||
|
||||
@ -194,12 +243,8 @@ Query:
|
||||
"msg": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"label": "图表",
|
||||
"label": "折线图",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": "组件",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -207,23 +252,29 @@ Query:
|
||||
|
||||
### GET `/dict/getComponentDictByType`
|
||||
|
||||
根据一级类型获取二级类型字典。
|
||||
根据组件一级类型获取对应的二级类型字典。
|
||||
|
||||
查询逻辑:先查 `dictKey=COMPONENT_TYPE` 且 `value=type` 的字典项,再使用该项的 `childrenKey` 查询子字典。
|
||||
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------------ |
|
||||
| type | number | 是 | `1` 图表,`2` 组件 |
|
||||
| ---- | ------ | ---- | -------- |
|
||||
| type | number | 是 | 一级类型 |
|
||||
|
||||
响应 `data`:`Array<{ label: string; value: number }>`。
|
||||
响应 `data`:`Array<{ label: string; value: number | string }>`。
|
||||
|
||||
## MinIO
|
||||
## MinIO 接口
|
||||
|
||||
### GET `/minio/check`
|
||||
|
||||
检查 MinIO 连接和 bucket 状态。
|
||||
|
||||
Query:`bucketName?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ----------- |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
|
||||
响应 `data`:`{ bucketName: string; exists: boolean }`。
|
||||
|
||||
@ -231,7 +282,11 @@ Query:`bucketName?: string`
|
||||
|
||||
创建 bucket,已存在时跳过。
|
||||
|
||||
Query:`bucketName?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ----------- |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
|
||||
响应 `data`:bucket 名称。
|
||||
|
||||
@ -268,7 +323,13 @@ Body:
|
||||
|
||||
获取文件列表。
|
||||
|
||||
Query:`bucketName?: string`、`prefix?: string`、`recursive?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ----------------------------- |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
| prefix | string | 否 | 对象名前缀 |
|
||||
| recursive | string | 否 | 是否递归,传 `false` 时不递归 |
|
||||
|
||||
响应 `data`:MinIO 对象数组,常见字段为 `name`、`size`、`etag`、`lastModified`。
|
||||
|
||||
@ -276,7 +337,13 @@ Query:`bucketName?: string`、`prefix?: string`、`recursive?: string`
|
||||
|
||||
获取文件临时访问地址。
|
||||
|
||||
Query:`objectName: string`、`bucketName?: string`、`expiry?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | --------------------------- |
|
||||
| objectName | string | 是 | 对象名 |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
| expiry | string | 否 | 有效期秒数,默认 `86400` 秒 |
|
||||
|
||||
响应 `data`:临时访问 URL。
|
||||
|
||||
@ -284,12 +351,22 @@ Query:`objectName: string`、`bucketName?: string`、`expiry?: string`
|
||||
|
||||
下载文件,直接返回文件流。
|
||||
|
||||
Query:`objectName: string`、`bucketName?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ----------- |
|
||||
| objectName | string | 是 | 对象名 |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
|
||||
### DELETE `/minio/remove`
|
||||
|
||||
删除文件。
|
||||
|
||||
Query:`objectName: string`、`bucketName?: string`
|
||||
Query:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ----------- |
|
||||
| objectName | string | 是 | 对象名 |
|
||||
| bucketName | string | 否 | bucket 名称 |
|
||||
|
||||
响应 `data`:`true` 表示删除成功。
|
||||
|
||||
185
README.md
185
README.md
@ -1,35 +1,176 @@
|
||||
## 技术总览
|
||||
# KT Template Online API
|
||||
|
||||
`Node`、`Ts`、`Nest.js`、`TypeORM`、`MySQL`、`Express`
|
||||
`kt-template-online-api` 是 KT Template Online 的 Nest.js 后端服务,负责组件模板管理、数据库字典映射和 MinIO 文件能力。
|
||||
|
||||
## 项目简介
|
||||
## 技术栈
|
||||
|
||||
此项目为`kt-template-online`服务端,基于`Node`、`Ts`开发
|
||||
- Node.js + TypeScript
|
||||
- Nest.js
|
||||
- TypeORM + MySQL
|
||||
- MinIO
|
||||
- Swagger / Knife4j
|
||||
|
||||
使用服务端框架`Nest.js`构建项目以及`ORM`框架`TypeORM`快速生成`SQL`语句以及映射`SQL`库表字段关系
|
||||
## 功能概览
|
||||
|
||||
## 运行项目
|
||||
### Component
|
||||
|
||||
```bash
|
||||
# 运行
|
||||
$ pnpm start
|
||||
管理图表/组件模板:
|
||||
|
||||
# 开发环境
|
||||
$ pnpm start:dev
|
||||
- 组件列表、分页列表、详情
|
||||
- 新增、编辑、逻辑删除
|
||||
- 查询阶段自动补充 `typeMsg`、`componentTypeMsg`
|
||||
|
||||
# 生产环境
|
||||
$ pnpm start:prod
|
||||
### Dict
|
||||
|
||||
维护数据库字典:
|
||||
|
||||
- 字典项存储在 `dict` 表
|
||||
- `COMPONENT_TYPE.children_key` 关联二级字典,例如 `CHART`、`COMPONENT`
|
||||
- 服务会将数据库字典刷新到进程缓存,供实体 `AfterLoad` 阶段同步翻译字段
|
||||
|
||||
### MinIO
|
||||
|
||||
提供文件服务:
|
||||
|
||||
- 检查/创建 bucket
|
||||
- 上传文件
|
||||
- 查询对象列表
|
||||
- 获取临时访问地址
|
||||
- 下载和删除对象
|
||||
|
||||
### Common
|
||||
|
||||
项目通用能力:
|
||||
|
||||
- 统一响应 Swagger 注解
|
||||
- 字典翻译注解
|
||||
- `POST */save` 请求体规范化拦截器
|
||||
- 通用响应、分页和查询条件工具
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
src
|
||||
common
|
||||
decorators/ # 通用装饰器
|
||||
interceptors/ # 全局/通用拦截器
|
||||
services/ # 通用服务
|
||||
swagger/ # Swagger 响应注解封装
|
||||
index.ts # common 统一出口
|
||||
component/ # 组件模板模块
|
||||
dict/ # 数据库字典模块
|
||||
minio/ # MinIO 文件模块
|
||||
types/ # 全局类型声明
|
||||
app.module.ts
|
||||
main.ts
|
||||
```
|
||||
|
||||
## 测试
|
||||
## 环境变量
|
||||
|
||||
项目默认读取 `.env`,生产环境会读取 `.env.prod`。
|
||||
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_DATABASE=shy_template
|
||||
DB_SYNC=true
|
||||
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=kt-template-online
|
||||
```
|
||||
|
||||
`DB_SYNC=true` 时 TypeORM 会按实体同步表结构。生产环境建议关闭同步,改用迁移脚本维护表结构。
|
||||
|
||||
## 数据库字典
|
||||
|
||||
`dict` 表是字典翻译的唯一数据源,代码中不再维护本地字典列表。
|
||||
|
||||
核心字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| `dict_key` | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
||||
| `label` | 展示文本 |
|
||||
| `value` | 字典值 |
|
||||
| `children_key` | 子字典分组,例如一级类型 `1` 指向 `CHART` |
|
||||
| `sort` | 排序 |
|
||||
| `is_deleted` | 逻辑删除标记 |
|
||||
|
||||
组件类型示例:
|
||||
|
||||
| dict_key | value | label | children_key |
|
||||
| -------------- | ----- | ----- | ------------ |
|
||||
| COMPONENT_TYPE | 1 | 图表 | CHART |
|
||||
| COMPONENT_TYPE | 2 | 组件 | COMPONENT |
|
||||
|
||||
## 全局 Save 规则
|
||||
|
||||
项目注册了 `SaveBodyInterceptor`,会对 `POST */save` 请求统一删除 `body.id`,避免新增接口误用前端传入的主键。
|
||||
|
||||
如果某个接口需要保留 `id`,可以使用:
|
||||
|
||||
```ts
|
||||
@SkipSaveBodyNormalize()
|
||||
```
|
||||
|
||||
## 启动项目
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm test:cov
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发环境:
|
||||
|
||||
```bash
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
普通启动:
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
生产启动:
|
||||
|
||||
```bash
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
## 文档地址
|
||||
|
||||
服务默认监听 `48085`:
|
||||
|
||||
- Swagger UI:`http://localhost:48085/api`
|
||||
- OpenAPI JSON:`http://localhost:48085/api-json`
|
||||
- Knife4j:由 `nestjs-knife4j-plus` 根据 `/api-json` 提供增强文档
|
||||
|
||||
接口细节见 [API.md](./API.md)。
|
||||
|
||||
## 常用校验
|
||||
|
||||
类型检查:
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
```
|
||||
|
||||
格式化:
|
||||
|
||||
```bash
|
||||
pnpm exec prettier --write "src/**/*.ts" "test/**/*.ts"
|
||||
```
|
||||
|
||||
测试:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MinioModule } from 'nestjs-minio-client';
|
||||
import { ComponentModule } from './component/component.module';
|
||||
import { DictModule } from './dict/dict.module';
|
||||
import { MinioClientModule } from './minio/minio.module';
|
||||
import { SaveMiddleware } from './middleware/save.middleware';
|
||||
import { SaveBodyInterceptor } from './common';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -51,10 +52,13 @@ import { SaveMiddleware } from './middleware/save.middleware';
|
||||
DictModule,
|
||||
MinioClientModule,
|
||||
],
|
||||
providers: [AppService, ConfigService],
|
||||
providers: [
|
||||
AppService,
|
||||
ConfigService,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: SaveBodyInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(SaveMiddleware).forRoutes('*/save');
|
||||
}
|
||||
}
|
||||
export class AppModule {}
|
||||
|
||||
92
src/common/decorators/decode-dict.decorator.ts
Normal file
92
src/common/decorators/decode-dict.decorator.ts
Normal file
@ -0,0 +1,92 @@
|
||||
type DecodeDictKeyOptions = {
|
||||
fallback?: string;
|
||||
sourceKey?: string;
|
||||
targetKey?: string;
|
||||
};
|
||||
|
||||
type DictDecodeRule = DecodeDictKeyOptions & {
|
||||
targetKey: string;
|
||||
dictKeys: string[];
|
||||
};
|
||||
|
||||
const DICT_DECODE_RULES = Symbol('DICT_DECODE_RULES');
|
||||
const DICT_DECODE_CACHE = new Map<string, Map<string, string>>();
|
||||
|
||||
const getDictValueKey = (value: unknown) => String(value);
|
||||
|
||||
// 字典翻译规则挂在类原型上,实例加载完成后再统一读取并执行。
|
||||
const getDecodeRules = (target: object): DictDecodeRule[] => {
|
||||
const prototype = Object.getPrototypeOf(target);
|
||||
|
||||
return prototype?.[DICT_DECODE_RULES] || [];
|
||||
};
|
||||
|
||||
// 未指定 dictKey 时会在所有字典分组里查找,适合全局唯一的业务枚举值。
|
||||
const getTargetDictMaps = (dictKeys: string[]) => {
|
||||
if (dictKeys.length) {
|
||||
return dictKeys
|
||||
.map((dictKey) => DICT_DECODE_CACHE.get(dictKey))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return [...DICT_DECODE_CACHE.values()];
|
||||
};
|
||||
|
||||
// 只登记翻译关系,不在 setter 中翻译,避免实体继承字段和 TypeORM 赋值顺序带来的覆盖问题。
|
||||
export function DecodeDictKey(
|
||||
dictKeys?: string | string[],
|
||||
options: DecodeDictKeyOptions = {},
|
||||
): PropertyDecorator {
|
||||
return (target, key: string | symbol) => {
|
||||
const currentKey = key.toString();
|
||||
const sourceKey = options.sourceKey || currentKey;
|
||||
const targetKey = options.targetKey || currentKey;
|
||||
const sourceDictKeys = Array.isArray(dictKeys)
|
||||
? dictKeys
|
||||
: dictKeys
|
||||
? [dictKeys]
|
||||
: [];
|
||||
const rules = target[DICT_DECODE_RULES] || [];
|
||||
|
||||
target[DICT_DECODE_RULES] = [
|
||||
...rules,
|
||||
{
|
||||
...options,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
dictKeys: sourceDictKeys,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
// 在 TypeORM AfterLoad 等实体初始化完成后调用,将源字段值翻译到 targetKey。
|
||||
export function decodeDictKeys<T extends object>(target: T): T {
|
||||
getDecodeRules(target).forEach(
|
||||
({ sourceKey, targetKey, dictKeys, fallback }) => {
|
||||
const valueKey = getDictValueKey(target[sourceKey]);
|
||||
const label = getTargetDictMaps(dictKeys)
|
||||
.map((dictMap) => dictMap.get(valueKey))
|
||||
.find(Boolean);
|
||||
|
||||
target[targetKey] = label || fallback || '';
|
||||
},
|
||||
);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// DictService 从数据库刷新缓存后,实体 AfterLoad 可以同步完成字典映射。
|
||||
export function setDictDecodeCache(
|
||||
dicts: Array<Dict<{ dictKey: string }>>,
|
||||
): void {
|
||||
DICT_DECODE_CACHE.clear();
|
||||
|
||||
dicts.forEach(({ dictKey, value, label }) => {
|
||||
if (!DICT_DECODE_CACHE.has(dictKey)) {
|
||||
DICT_DECODE_CACHE.set(dictKey, new Map());
|
||||
}
|
||||
|
||||
DICT_DECODE_CACHE.get(dictKey).set(getDictValueKey(value), label);
|
||||
});
|
||||
}
|
||||
@ -1,9 +1,4 @@
|
||||
export function DecodeDictKey(dict: Dict[]): PropertyDecorator {
|
||||
return (target, key: string | symbol) => {
|
||||
Reflect.defineProperty(target, `_${key.toString()}`, {
|
||||
set(newVal) {
|
||||
this[key] = dict.find((i) => i.value == newVal).label;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
export * from './decorators/decode-dict.decorator';
|
||||
export * from './interceptors/save-body.interceptor';
|
||||
export * from './services/tool.service';
|
||||
export * from './swagger/swagger-response';
|
||||
|
||||
46
src/common/interceptors/save-body.interceptor.ts
Normal file
46
src/common/interceptors/save-body.interceptor.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { Request } from 'express';
|
||||
|
||||
const SKIP_SAVE_BODY_NORMALIZE = 'SKIP_SAVE_BODY_NORMALIZE';
|
||||
|
||||
export const SkipSaveBodyNormalize = () =>
|
||||
SetMetadata(SKIP_SAVE_BODY_NORMALIZE, true);
|
||||
|
||||
@Injectable()
|
||||
export class SaveBodyInterceptor implements NestInterceptor {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
if (this.shouldSkip(context)) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
if (this.isSaveRequest(request) && request.body) {
|
||||
// 新增接口统一忽略前端传入的 id,避免 TypeORM save 走指定主键写入。
|
||||
delete request.body.id;
|
||||
}
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
private shouldSkip(context: ExecutionContext): boolean {
|
||||
return this.reflector.getAllAndOverride<boolean>(SKIP_SAVE_BODY_NORMALIZE, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
}
|
||||
|
||||
private isSaveRequest(request: Request): boolean {
|
||||
return request.method === 'POST' && request.path.endsWith('/save');
|
||||
}
|
||||
}
|
||||
91
src/common/services/tool.service.ts
Normal file
91
src/common/services/tool.service.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as svgCaptcha from 'svg-captcha';
|
||||
|
||||
@Injectable()
|
||||
export class ToolsService {
|
||||
async captche(size = 4) {
|
||||
const captcha = svgCaptcha.create({
|
||||
size,
|
||||
fontSize: 50,
|
||||
width: 100,
|
||||
height: 34,
|
||||
background: '#ffffff',
|
||||
});
|
||||
return captcha;
|
||||
}
|
||||
|
||||
res(code: number, msg: string, data: any): Res {
|
||||
const retn: Res = {
|
||||
code,
|
||||
msg,
|
||||
data,
|
||||
};
|
||||
return retn;
|
||||
}
|
||||
|
||||
page<T = any>(list: T[], total: number): Page<T> {
|
||||
const retn = {
|
||||
list,
|
||||
total,
|
||||
};
|
||||
return retn;
|
||||
}
|
||||
|
||||
getWhereStr(alias: string, key) {
|
||||
return `${alias}.${key.toString()} = :${key.toString()}`;
|
||||
}
|
||||
|
||||
getLikeStr(alias: string, key) {
|
||||
return `${alias}.${key.toString()} like :${key.toString()}`;
|
||||
}
|
||||
|
||||
getLikeWhere<T = object>(
|
||||
alias: string,
|
||||
wheres: Array<keyof T>,
|
||||
likes: Array<keyof T>,
|
||||
values: Partial<T>,
|
||||
operator: 'AND' | 'OR' = 'AND',
|
||||
): [string, Record<string, unknown>] {
|
||||
const hasValue = (value: unknown) =>
|
||||
value !== undefined && value !== null && value !== '';
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
wheres.forEach((key) => {
|
||||
const value = values[key];
|
||||
|
||||
if (!hasValue(value)) return;
|
||||
|
||||
const paramKey = key.toString();
|
||||
conditions.push(this.getWhereStr(alias, key));
|
||||
params[paramKey] = value;
|
||||
});
|
||||
|
||||
likes.forEach((key) => {
|
||||
const value = values[key];
|
||||
|
||||
if (!hasValue(value)) return;
|
||||
|
||||
const paramKey = key.toString();
|
||||
conditions.push(this.getLikeStr(alias, key));
|
||||
params[paramKey] = `%${value}%`;
|
||||
});
|
||||
|
||||
return [conditions.join(` ${operator} `), params];
|
||||
}
|
||||
|
||||
dictFormat<T = object>(
|
||||
label: string,
|
||||
value: any,
|
||||
other: Partial<T>,
|
||||
): Dict<T> {
|
||||
const options = {
|
||||
label,
|
||||
value,
|
||||
...other,
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@ -16,17 +16,16 @@ import {
|
||||
ApiTags,
|
||||
PartialType,
|
||||
} from '@nestjs/swagger';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ComponentService } from './component.service';
|
||||
import { Component } from './component.entity';
|
||||
import { ComponentDto } from './component.dto';
|
||||
import {
|
||||
PaginatedDto,
|
||||
ApiArrayResponse,
|
||||
ApiModelResponse,
|
||||
ApiPageResponse,
|
||||
ApiSuccessResponse,
|
||||
} from '@/common/swagger-response';
|
||||
ToolsService,
|
||||
} from '@/common';
|
||||
import { ComponentService } from './component.service';
|
||||
import { Component } from './component.entity';
|
||||
|
||||
const componentExample = {
|
||||
id: '1d8d3dd2-99f0-4d10-9a44-0cf9566b37c9',
|
||||
@ -65,11 +64,11 @@ export class ComponentController {
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly componentService: ComponentService,
|
||||
) {} //注入服务
|
||||
) {}
|
||||
|
||||
@Get('allList')
|
||||
@ApiOperation({ summary: '获取组件列表' })
|
||||
@ApiArrayResponse(ComponentDto, [componentExample])
|
||||
@ApiArrayResponse(Component, [componentExample])
|
||||
async getAllList(@Res() res) {
|
||||
const list = await this.componentService.all();
|
||||
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
||||
@ -78,11 +77,11 @@ export class ComponentController {
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: '获取组件列表分页' })
|
||||
@ApiQuery({ type: [CompPageDto] })
|
||||
@ApiPageResponse(ComponentDto, [componentExample], 1)
|
||||
@ApiPageResponse(Component, [componentExample], 1)
|
||||
async getList(
|
||||
@Res() res,
|
||||
@Query() { pageNo, pageSize, ...args }: PageParams<ComponentDto>,
|
||||
): Promise<PaginatedDto<ComponentDto>> {
|
||||
@Query() { pageNo, pageSize, ...args }: PageParams<Component>,
|
||||
): Promise<PaginatedDto<Component>> {
|
||||
const list = await this.componentService.page({
|
||||
pageNo,
|
||||
pageSize,
|
||||
@ -164,7 +163,7 @@ export class ComponentController {
|
||||
@Get('detail')
|
||||
@ApiOperation({ summary: '组件详情' })
|
||||
@ApiQuery({ name: 'id', type: String })
|
||||
@ApiModelResponse(ComponentDto, componentExample)
|
||||
@ApiModelResponse(Component, componentExample)
|
||||
async detail(@Res() res, @Query('id') id) {
|
||||
const detail = await this.componentService.find(id);
|
||||
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import { DecodeDictKey } from '@/common';
|
||||
import { Component } from './component.entity';
|
||||
import { DictKeyEnum, DictKeyMap } from '@/utils/constant';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ComponentDto extends Component {
|
||||
[x: string]: any;
|
||||
@ApiProperty()
|
||||
@DecodeDictKey(DictKeyMap.get(DictKeyEnum.COMPONENT_TYPE))
|
||||
typeMsg: string;
|
||||
|
||||
@ApiProperty()
|
||||
@DecodeDictKey([
|
||||
...DictKeyMap.get(DictKeyEnum.CHART),
|
||||
...DictKeyMap.get(DictKeyEnum.COMPONENT),
|
||||
])
|
||||
componentTypeMsg: string;
|
||||
constructor(component: Component) {
|
||||
super(component);
|
||||
this._typeMsg = component.type;
|
||||
this._componentTypeMsg = component.componentType;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
AfterLoad,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
@ -6,15 +7,10 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ComponentTypeEnum, ComponentEnum } from '@/utils/constant';
|
||||
|
||||
import { DecodeDictKey, decodeDictKeys } from '@/common';
|
||||
|
||||
@Entity()
|
||||
export class Component {
|
||||
constructor(component?: Component) {
|
||||
Object.assign(this, component);
|
||||
}
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -25,27 +21,32 @@ export class Component {
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'enum',
|
||||
enum: ComponentTypeEnum,
|
||||
})
|
||||
@ApiProperty()
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ComponentTypeEnum,
|
||||
type: 'int',
|
||||
})
|
||||
@DecodeDictKey('COMPONENT_TYPE', {
|
||||
targetKey: 'typeMsg',
|
||||
})
|
||||
type: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'enum',
|
||||
enum: ComponentEnum,
|
||||
})
|
||||
@ApiProperty()
|
||||
@Column({
|
||||
name: 'component_type',
|
||||
type: 'enum',
|
||||
enum: ComponentEnum,
|
||||
type: 'int',
|
||||
})
|
||||
// 二级类型值由数据库字典维护;未指定 dictKey 时会在全部字典缓存中匹配。
|
||||
@DecodeDictKey(undefined, {
|
||||
targetKey: 'componentTypeMsg',
|
||||
})
|
||||
componentType: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
typeMsg: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
componentTypeMsg: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({
|
||||
type: 'mediumtext',
|
||||
@ -74,4 +75,10 @@ export class Component {
|
||||
default: 0,
|
||||
})
|
||||
is_deleted: boolean;
|
||||
|
||||
@AfterLoad()
|
||||
decodeDictKeys() {
|
||||
// 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。
|
||||
decodeDictKeys(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ComponentController } from './component.controller';
|
||||
import { ComponentService } from './component.service';
|
||||
import { Component } from './component.entity';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ToolsService } from '@/common';
|
||||
import { DictModule } from '@/dict/dict.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Component])],
|
||||
imports: [TypeOrmModule.forFeature([Component]), DictModule],
|
||||
controllers: [ComponentController],
|
||||
providers: [ComponentService, ToolsService],
|
||||
exports: [ComponentService],
|
||||
|
||||
@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Component } from './component.entity';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ToolsService } from '@/common';
|
||||
import { isNumber, omit, pick } from 'lodash';
|
||||
import { ComponentDto } from './component.dto';
|
||||
import { DictService } from '@/dict/dict.service';
|
||||
|
||||
@Injectable()
|
||||
export class ComponentService {
|
||||
@ -12,9 +12,12 @@ export class ComponentService {
|
||||
@InjectRepository(Component)
|
||||
private readonly userRepository: Repository<Component>,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly dictService: DictService,
|
||||
) {}
|
||||
|
||||
async all(): Promise<Component[]> {
|
||||
await this.dictService.refreshDecodeCache();
|
||||
|
||||
const components = await this.userRepository
|
||||
.createQueryBuilder('component')
|
||||
.getMany();
|
||||
@ -26,6 +29,8 @@ export class ComponentService {
|
||||
pageSize,
|
||||
...args
|
||||
}: PageParams<Component>): Promise<Page<Component>> {
|
||||
await this.dictService.refreshDecodeCache();
|
||||
|
||||
const hasOwnEntity = new Component();
|
||||
|
||||
const [wheres, likes] = [['is_deleted'], ['name']] as Array<
|
||||
@ -69,10 +74,7 @@ export class ComponentService {
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
|
||||
return this.toolsService.page<Component>(
|
||||
list.map((component) => new ComponentDto(component)),
|
||||
total,
|
||||
);
|
||||
return this.toolsService.page<Component>(list, total);
|
||||
}
|
||||
|
||||
async save(component: Component): Promise<Component> {
|
||||
@ -104,6 +106,8 @@ export class ComponentService {
|
||||
}
|
||||
|
||||
async find(id: number): Promise<Component> {
|
||||
await this.dictService.refreshDecodeCache();
|
||||
|
||||
const component = await this.userRepository
|
||||
.createQueryBuilder('component')
|
||||
.select([
|
||||
|
||||
@ -6,11 +6,9 @@ import {
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { DictService } from './dict.service';
|
||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { ComponentTypeEnum, DictKeyEnum, DictKeyType } from '@/utils/constant';
|
||||
import { ApiArrayResponse } from '@/common/swagger-response';
|
||||
import { ApiArrayResponse, ToolsService } from '@/common';
|
||||
import { DictDto } from './dict.dto';
|
||||
|
||||
const componentTypeDictExample = [
|
||||
@ -41,20 +39,20 @@ export class DictController {
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly dictService: DictService,
|
||||
) {} //注入服务
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: '根据key获取字典' })
|
||||
@ApiQuery({ name: 'dictKey', enum: DictKeyEnum })
|
||||
@ApiQuery({ name: 'dictKey', type: String })
|
||||
@ApiArrayResponse(DictDto, componentTypeDictExample)
|
||||
@Get('getDictByKey')
|
||||
async getDictByKey(@Res() res, @Query('dictKey') dictKey: DictKeyType) {
|
||||
const dict = this.toolsService.getDictByKey(dictKey);
|
||||
async getDictByKey(@Res() res, @Query('dictKey') dictKey: string) {
|
||||
const dict = await this.dictService.getDictByKey(dictKey);
|
||||
|
||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', dict));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '根据组件类型获取组件字典' })
|
||||
@ApiQuery({ name: 'type', enum: ComponentTypeEnum })
|
||||
@ApiQuery({ name: 'type', type: Number })
|
||||
@ApiArrayResponse(DictDto, chartDictExample)
|
||||
@Get('getComponentDictByType')
|
||||
async getComponentDictByType(@Res() res, @Query('type', ParseIntPipe) type) {
|
||||
|
||||
67
src/dict/dict.entity.ts
Normal file
67
src/dict/dict.entity.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
@Entity('dict')
|
||||
export class DictEntity {
|
||||
@ApiPropertyOptional()
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'CHART',
|
||||
})
|
||||
@Column({
|
||||
name: 'dict_key',
|
||||
})
|
||||
dictKey: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '折线图',
|
||||
})
|
||||
@Column()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
})
|
||||
@Column()
|
||||
value: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 'CHART',
|
||||
})
|
||||
@Column({
|
||||
name: 'children_key',
|
||||
nullable: true,
|
||||
})
|
||||
childrenKey: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1,
|
||||
})
|
||||
@Column({
|
||||
default: 0,
|
||||
})
|
||||
sort: number;
|
||||
|
||||
@Column({
|
||||
default: 0,
|
||||
})
|
||||
is_deleted: boolean;
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'create_time',
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'update_time',
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DictController } from './dict.controller';
|
||||
import { DictService } from './dict.service';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ToolsService } from '@/common';
|
||||
import { DictEntity } from './dict.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([DictEntity])],
|
||||
controllers: [DictController],
|
||||
providers: [DictService, ToolsService],
|
||||
exports: [DictService],
|
||||
|
||||
@ -1,21 +1,67 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ComponentTypeEnum, DictKeyEnum } from '@/utils/constant';
|
||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { setDictDecodeCache } from '@/common';
|
||||
import { DictEntity } from './dict.entity';
|
||||
|
||||
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
||||
|
||||
@Injectable()
|
||||
export class DictService {
|
||||
constructor(private readonly toolsService: ToolsService) {}
|
||||
export class DictService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
@InjectRepository(DictEntity)
|
||||
private readonly dictRepository: Repository<DictEntity>,
|
||||
) {}
|
||||
|
||||
async getComponentDictByType(type: ComponentTypeEnum): Promise<Dict[]> {
|
||||
switch (type) {
|
||||
case ComponentTypeEnum.CHART:
|
||||
return this.toolsService.getDictByKey(DictKeyEnum.CHART);
|
||||
|
||||
case ComponentTypeEnum.COMPONENT:
|
||||
return this.toolsService.getDictByKey(DictKeyEnum.COMPONENT);
|
||||
|
||||
default:
|
||||
return this.toolsService.getDictByKey(DictKeyEnum.CHART);
|
||||
async onApplicationBootstrap() {
|
||||
await this.refreshDecodeCache();
|
||||
}
|
||||
|
||||
async getDictByKey(dictKey: string): Promise<Dict[]> {
|
||||
const list = await this.dictRepository.find({
|
||||
where: {
|
||||
dictKey,
|
||||
is_deleted: false,
|
||||
},
|
||||
order: {
|
||||
sort: 'ASC',
|
||||
createTime: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
return list.map(({ label, value }) => ({
|
||||
label,
|
||||
value: Number.isNaN(Number(value)) ? value : Number(value),
|
||||
}));
|
||||
}
|
||||
|
||||
async getComponentDictByType(type: number): Promise<Dict[]> {
|
||||
// 一级类型的 childrenKey 决定二级字典来源,避免在代码里维护 1 -> CHART 这类关系。
|
||||
const componentType = await this.dictRepository.findOne({
|
||||
where: {
|
||||
dictKey: COMPONENT_TYPE_DICT_KEY,
|
||||
value: String(type),
|
||||
is_deleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!componentType?.childrenKey) return [];
|
||||
|
||||
return this.getDictByKey(componentType.childrenKey);
|
||||
}
|
||||
|
||||
async refreshDecodeCache() {
|
||||
// AfterLoad 字典翻译必须同步完成,所以这里先把数据库字典刷新到进程缓存。
|
||||
const list = await this.dictRepository.find({
|
||||
where: {
|
||||
is_deleted: false,
|
||||
},
|
||||
order: {
|
||||
sort: 'ASC',
|
||||
createTime: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
setDictDecodeCache(list);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class SaveMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: VoidFunction) {
|
||||
Reflect.deleteProperty(req.body, 'id');
|
||||
next();
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,6 @@ import {
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { MinioClientService } from './minio.service';
|
||||
import type { MinioUploadFile } from './minio.service';
|
||||
import {
|
||||
@ -27,7 +26,8 @@ import {
|
||||
ApiArrayResponse,
|
||||
ApiModelResponse,
|
||||
ApiSuccessResponse,
|
||||
} from '@/common/swagger-response';
|
||||
ToolsService,
|
||||
} from '@/common';
|
||||
import {
|
||||
MinioBucketStatusDto,
|
||||
MinioObjectDto,
|
||||
@ -40,7 +40,7 @@ export class MinioClientController {
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly minioClientService: MinioClientService,
|
||||
) {} //注入服务
|
||||
) {}
|
||||
|
||||
@Get('check')
|
||||
@ApiOperation({ summary: '检查MinIO连接和Bucket状态' })
|
||||
|
||||
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { MinioClientController } from './minio.controller';
|
||||
import { MinioClientService } from './minio.service';
|
||||
import { ToolsService } from '@/utils/tool.service';
|
||||
import { ToolsService } from '@/common';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
|
||||
@ -81,7 +81,8 @@ export class MinioClientService {
|
||||
}
|
||||
|
||||
const targetBucket = await this.ensureBucket(bucketName);
|
||||
const targetObjectName = objectName || this.createObjectName(file.originalname);
|
||||
const targetObjectName =
|
||||
objectName || this.createObjectName(file.originalname);
|
||||
|
||||
const result = await this.client.putObject(
|
||||
targetBucket,
|
||||
@ -161,7 +162,10 @@ export class MinioClientService {
|
||||
);
|
||||
}
|
||||
|
||||
async removeObject(objectName: string, bucketName?: string): Promise<boolean> {
|
||||
async removeObject(
|
||||
objectName: string,
|
||||
bucketName?: string,
|
||||
): Promise<boolean> {
|
||||
if (!objectName) {
|
||||
throw new BadRequestException('objectName不能为空');
|
||||
}
|
||||
@ -171,6 +175,7 @@ export class MinioClientService {
|
||||
}
|
||||
|
||||
private createObjectName(originalName: string): string {
|
||||
// 前端未指定对象名时,生成带时间和随机段的路径,降低同名文件覆盖概率。
|
||||
const safeName = originalName.replace(/[\\/]/g, '_');
|
||||
const random = Math.random().toString(36).slice(2, 8);
|
||||
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
export enum ComponentTypeEnum {
|
||||
CHART = 1,
|
||||
COMPONENT = 2,
|
||||
}
|
||||
|
||||
export enum ComponentEnum {
|
||||
NOT_CATEGORY = -1,
|
||||
LINE = 1,
|
||||
BAR = 2,
|
||||
PIE = 3,
|
||||
SCATTER = 4,
|
||||
MAP = 5,
|
||||
CANDLESTICK = 6,
|
||||
RADAR = 7,
|
||||
BOX_PLOT = 8,
|
||||
HEATMAP = 9,
|
||||
GRAPH = 10,
|
||||
LINES = 11,
|
||||
TREE = 12,
|
||||
TREE_MAP = 13,
|
||||
SUNBURST = 14,
|
||||
PARALLEL = 15,
|
||||
SAN_KEY = 16,
|
||||
FUNNEL = 17,
|
||||
GAUGE = 18,
|
||||
PICTORIAL_BAR = 19,
|
||||
THEME_RIVER = 20,
|
||||
LIQUID_FILL = 21,
|
||||
WORD_CLOUD = 22,
|
||||
TABLE = 23,
|
||||
FORM = 24,
|
||||
CONTAINER = 25,
|
||||
}
|
||||
|
||||
export enum DictKeyEnum {
|
||||
COMPONENT_TYPE = 'COMPONENT_TYPE',
|
||||
CHART = 'CHART',
|
||||
COMPONENT = 'COMPONENT',
|
||||
}
|
||||
|
||||
export type DictKeyType = keyof typeof DictKeyEnum;
|
||||
|
||||
export const DictKeyMap: Map<DictKeyType, Dict[]> = new Map();
|
||||
|
||||
const ComponentTypeDict = [
|
||||
{
|
||||
label: '图表',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '组件',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const ChartDict = [
|
||||
{
|
||||
label: '未分类',
|
||||
value: -1,
|
||||
},
|
||||
{
|
||||
label: '折线图',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '柱状图',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '饼图',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '散点图',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '地图',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: 'K线图',
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
label: '雷达图',
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
label: '盒须图',
|
||||
value: 8,
|
||||
},
|
||||
{
|
||||
label: '热力图',
|
||||
value: 9,
|
||||
},
|
||||
{
|
||||
label: '关系图',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
label: '路径图',
|
||||
value: 11,
|
||||
},
|
||||
{
|
||||
label: '树图',
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
label: '矩树图',
|
||||
value: 13,
|
||||
},
|
||||
{
|
||||
label: '旭日图',
|
||||
value: 14,
|
||||
},
|
||||
{
|
||||
label: '平行坐标系',
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
label: '桑基图',
|
||||
value: 16,
|
||||
},
|
||||
{
|
||||
label: '漏斗图',
|
||||
value: 17,
|
||||
},
|
||||
{
|
||||
label: '仪表盘',
|
||||
value: 18,
|
||||
},
|
||||
{
|
||||
label: '象形图',
|
||||
value: 19,
|
||||
},
|
||||
{
|
||||
label: '河流图',
|
||||
value: 20,
|
||||
},
|
||||
{
|
||||
label: '水球',
|
||||
value: 21,
|
||||
},
|
||||
{
|
||||
label: '词云',
|
||||
value: 22,
|
||||
},
|
||||
];
|
||||
|
||||
const ComponentDict = [
|
||||
{
|
||||
label: '未分类',
|
||||
value: -1,
|
||||
},
|
||||
{
|
||||
label: '表格',
|
||||
value: 23,
|
||||
},
|
||||
{
|
||||
label: '表单',
|
||||
value: 24,
|
||||
},
|
||||
{
|
||||
label: '容器',
|
||||
value: 25,
|
||||
},
|
||||
];
|
||||
|
||||
DictKeyMap.set(DictKeyEnum.COMPONENT_TYPE, ComponentTypeDict);
|
||||
DictKeyMap.set(DictKeyEnum.CHART, ChartDict);
|
||||
DictKeyMap.set(DictKeyEnum.COMPONENT, ComponentDict);
|
||||
@ -1,105 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as svgCaptcha from 'svg-captcha';
|
||||
import { DictKeyMap, DictKeyEnum } from './constant';
|
||||
import type { DictKeyType } from './constant';
|
||||
import { isBoolean } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class ToolsService {
|
||||
async captche(size = 4) {
|
||||
const captcha = svgCaptcha.create({
|
||||
//可配置返回的图片信息
|
||||
size, //生成几个验证码
|
||||
fontSize: 50, //文字大小
|
||||
width: 100, //宽度
|
||||
height: 34, //高度
|
||||
background: '#ffffff', //背景颜色
|
||||
});
|
||||
return captcha;
|
||||
}
|
||||
|
||||
res(code: number, msg: string, data: any): Res {
|
||||
const retn: Res = {
|
||||
code,
|
||||
msg,
|
||||
data,
|
||||
};
|
||||
return retn;
|
||||
}
|
||||
|
||||
page<T = any>(list: T[], total: number): Page<T> {
|
||||
const retn = {
|
||||
list,
|
||||
total,
|
||||
};
|
||||
return retn;
|
||||
}
|
||||
|
||||
getWhereStr(alias: string, key) {
|
||||
return `${alias}.${key.toString()} = :${key.toString()}`;
|
||||
}
|
||||
|
||||
getLikeStr(alias: string, key) {
|
||||
return `${alias}.${key.toString()} like :${key.toString()}`;
|
||||
}
|
||||
|
||||
getLikeWhere<T = object>(
|
||||
alias: string,
|
||||
wheres: Array<keyof T>,
|
||||
likes: Array<keyof T>,
|
||||
values: Partial<T>,
|
||||
operator = 'AND',
|
||||
): [string, Record<keyof T, string>] {
|
||||
const linkOperator = ` ${operator} `;
|
||||
const wheresEndIndex = wheres.length;
|
||||
|
||||
return [
|
||||
[...wheres, ...likes].reduce((pre, cur, index, source) => {
|
||||
const isLink = !!source
|
||||
.slice(0, index)
|
||||
.some((key) => isBoolean(values[key]) || !!values[key]);
|
||||
|
||||
const { getLikeStr, getWhereStr } = this;
|
||||
|
||||
if (!isBoolean(values[cur]) && !values[cur]) return pre;
|
||||
|
||||
if (!index) getWhereStr(alias, cur);
|
||||
|
||||
const matchSqlFn = index >= wheresEndIndex ? getLikeStr : getWhereStr;
|
||||
const beforeSql = `${pre}${isLink ? linkOperator : ' '}`;
|
||||
|
||||
return `${beforeSql}${matchSqlFn(alias, cur)}`;
|
||||
}, ''),
|
||||
Object.entries(values).reduce((pre, [key, value]) => {
|
||||
if (!isBoolean(value) && !value) return pre;
|
||||
|
||||
if (likes.includes(key as keyof T))
|
||||
return { ...pre, ...{ [key]: `%${value}%` } };
|
||||
|
||||
return { ...pre, ...{ [key]: value } };
|
||||
}, {} as Record<keyof T, string>),
|
||||
];
|
||||
}
|
||||
|
||||
getDictByKey(key: DictKeyType): Dict[] {
|
||||
if (!DictKeyEnum[key]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return DictKeyMap.get(key);
|
||||
}
|
||||
|
||||
dictFormat<T = object>(
|
||||
label: string,
|
||||
value: any,
|
||||
other: Partial<T>,
|
||||
): Dict<T> {
|
||||
const options = {
|
||||
label,
|
||||
value,
|
||||
...other,
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user