From 7291fed6821209f1338d1044f5e19f8236f4c0b6 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 13 May 2026 15:33:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=AD=97=E5=85=B8=E6=98=A0=E5=B0=84=E5=B9=B6=E6=95=B4?= =?UTF-8?q?=E7=90=86=E5=85=AC=E5=85=B1=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 dict 实体表,字典查询改为从数据库读取 - 使用 childrenKey 实现组件一级类型到二级字典的关系映射 - Component 查询后通过 DecodeDictKey 和 AfterLoad 自动补充翻译字段 - 移除 ComponentDto、本地字典常量和本地 seed 文件 - 将 common 能力按 decorators/interceptors/services/swagger 重新分层 - 使用全局 SaveBodyInterceptor 替代 SaveMiddleware 统一处理 save 请求 - 更新 MinIO、组件、字典接口的 Swagger 响应和少量关键注释 - 重写 README.md 和 API.md,补充项目功能、目录结构和接口说明 BREAKING CHANGE: 字典数据依赖数据库 dict 表维护,不再使用本地字典常量。 --- API.md | 213 ++++++++++++------ README.md | 185 +++++++++++++-- src/app.module.ts | 20 +- .../decorators/decode-dict.decorator.ts | 92 ++++++++ src/common/index.ts | 13 +- .../interceptors/save-body.interceptor.ts | 46 ++++ src/common/services/tool.service.ts | 91 ++++++++ src/common/{ => swagger}/swagger-response.ts | 0 src/component/component.controller.ts | 21 +- src/component/component.dto.ts | 23 -- src/component/component.entity.ts | 43 ++-- src/component/component.module.ts | 5 +- src/component/component.service.ts | 16 +- src/dict/dict.controller.ts | 14 +- src/dict/dict.entity.ts | 67 ++++++ src/dict/dict.module.ts | 5 +- src/dict/dict.service.ts | 74 ++++-- src/middleware/save.middleware.ts | 10 - src/minio/minio.controller.ts | 6 +- src/minio/minio.module.ts | 2 +- src/minio/minio.service.ts | 9 +- src/utils/constant.ts | 172 -------------- src/utils/tool.service.ts | 105 --------- 23 files changed, 749 insertions(+), 483 deletions(-) create mode 100644 src/common/decorators/decode-dict.decorator.ts create mode 100644 src/common/interceptors/save-body.interceptor.ts create mode 100644 src/common/services/tool.service.ts rename src/common/{ => swagger}/swagger-response.ts (100%) delete mode 100644 src/component/component.dto.ts create mode 100644 src/dict/dict.entity.ts delete mode 100644 src/middleware/save.middleware.ts delete mode 100644 src/utils/constant.ts delete mode 100644 src/utils/tool.service.ts diff --git a/API.md b/API.md index ec9df83..3dae025 100644 --- a/API.md +++ b/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 @@ } ``` +## 功能模块 + +| 模块 | 说明 | +| --------- | --------------------------------------------------------------------- | +| Component | 组件/图表模板的列表、详情、新增、编辑、逻辑删除 | +| Dict | 数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 | +| MinIO | Bucket 检查/创建、文件上传、列表、临时访问地址、下载和删除 | +| Common | 统一响应 Swagger 注解、字典翻译注解、`POST */save` 请求体规范化拦截器 | + +## 通用规则 + +### 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 | 一级类型,实际含义由 `dict` 表维护 | +| componentType | number | 二级类型,实际含义由 `dict` 表维护 | +| typeMsg | string | 一级类型文本,查询后自动映射 | +| componentTypeMsg | string | 二级类型文本,查询后自动映射 | +| image | string | 封面图或封面图地址 | +| template | string | Playground 序列化模板内容 | +| createTime | string | 创建时间 | +| updateTime | string | 更新时间 | +| is_deleted | boolean | 逻辑删除标记 | + +### Dict + +接口返回的字典项结构: + +| 字段 | 类型 | 说明 | +| ----- | ------------- | -------- | +| label | string | 展示文本 | +| value | number/string | 字典值 | + +### MinIO + +`bucketName` 未传时默认读取环境变量 `MINIO_BUCKET`,缺省值为 `kt-template-online`。 + ## Root ### GET `/` 重定向到 Swagger 文档页 `/api#/`,HTTP 状态码为 `301`。 -## 数据结构 - -### Component - -| 字段 | 类型 | 说明 | -| ---------------- | ------- | ---------------------------- | -| id | string | 组件 ID,新增时由后端生成 | -| name | string | 组件名称 | -| type | number | 一级类型,`1` 图表,`2` 组件 | -| componentType | number | 二级类型 | -| typeMsg | string | 一级类型文本,列表接口返回 | -| componentTypeMsg | string | 二级类型文本,列表接口返回 | -| image | string | 封面图 | -| template | string | playground 序列化模板内容 | -| createTime | string | 创建时间 | -| updateTime | string | 更新时间 | -| is_deleted | boolean | 逻辑删除标记 | - -### 字典 - -`COMPONENT_TYPE`: - -| label | value | -| ----- | ----- | -| 图表 | 1 | -| 组件 | 2 | - -`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)`。 - -`COMPONENT`:`未分类(-1)`、`表格(23)`、`表单(24)`、`容器(25)`。 - -## Component +## Component 接口 ### GET `/component/allList` 获取全部组件。 -响应示例: +响应 `data`:`Component[]`。 + +示例: ```json { @@ -91,19 +133,26 @@ ### GET `/component/list` -分页获取组件列表。 +分页获取组件列表。列表默认过滤 `is_deleted=false`,并支持按名称模糊搜索。 Query: -| 参数 | 类型 | 必填 | 说明 | -| ------------- | ------ | ---- | ------------ | -| pageNo | number | 是 | 页码 | -| pageSize | number | 是 | 每页条数 | -| name | string | 否 | 名称模糊搜索 | -| type | number | 否 | 一级类型 | -| componentType | number | 否 | 二级类型 | +| 参数 | 类型 | 必填 | 说明 | +| ------------- | ------ | ---- | ---------------- | +| pageNo | number | 是 | 页码 | +| pageSize | number | 是 | 每页条数 | +| 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` 表示删除成功。 diff --git a/README.md b/README.md index 49ab364..e31aede 100644 --- a/README.md +++ b/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 ``` diff --git a/src/app.module.ts b/src/app.module.ts index 5d341e5..b5cc9dc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 {} diff --git a/src/common/decorators/decode-dict.decorator.ts b/src/common/decorators/decode-dict.decorator.ts new file mode 100644 index 0000000..e13fb7d --- /dev/null +++ b/src/common/decorators/decode-dict.decorator.ts @@ -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>(); + +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(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>, +): 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); + }); +} diff --git a/src/common/index.ts b/src/common/index.ts index c033382..0777303 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -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'; diff --git a/src/common/interceptors/save-body.interceptor.ts b/src/common/interceptors/save-body.interceptor.ts new file mode 100644 index 0000000..79dda0a --- /dev/null +++ b/src/common/interceptors/save-body.interceptor.ts @@ -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 { + if (this.shouldSkip(context)) { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + + 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(SKIP_SAVE_BODY_NORMALIZE, [ + context.getHandler(), + context.getClass(), + ]); + } + + private isSaveRequest(request: Request): boolean { + return request.method === 'POST' && request.path.endsWith('/save'); + } +} diff --git a/src/common/services/tool.service.ts b/src/common/services/tool.service.ts new file mode 100644 index 0000000..f568032 --- /dev/null +++ b/src/common/services/tool.service.ts @@ -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(list: T[], total: number): Page { + 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( + alias: string, + wheres: Array, + likes: Array, + values: Partial, + operator: 'AND' | 'OR' = 'AND', + ): [string, Record] { + const hasValue = (value: unknown) => + value !== undefined && value !== null && value !== ''; + + const conditions: string[] = []; + const params: Record = {}; + + 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( + label: string, + value: any, + other: Partial, + ): Dict { + const options = { + label, + value, + ...other, + }; + + return options; + } +} diff --git a/src/common/swagger-response.ts b/src/common/swagger/swagger-response.ts similarity index 100% rename from src/common/swagger-response.ts rename to src/common/swagger/swagger-response.ts diff --git a/src/component/component.controller.ts b/src/component/component.controller.ts index 52e6228..cc666a7 100644 --- a/src/component/component.controller.ts +++ b/src/component/component.controller.ts @@ -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, - ): Promise> { + @Query() { pageNo, pageSize, ...args }: PageParams, + ): Promise> { 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); diff --git a/src/component/component.dto.ts b/src/component/component.dto.ts deleted file mode 100644 index 0d2ba67..0000000 --- a/src/component/component.dto.ts +++ /dev/null @@ -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; - } -} diff --git a/src/component/component.entity.ts b/src/component/component.entity.ts index d07d11d..df1ce9e 100644 --- a/src/component/component.entity.ts +++ b/src/component/component.entity.ts @@ -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); + } } diff --git a/src/component/component.module.ts b/src/component/component.module.ts index 27b8c94..9a70586 100644 --- a/src/component/component.module.ts +++ b/src/component/component.module.ts @@ -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], diff --git a/src/component/component.service.ts b/src/component/component.service.ts index 1aa3b73..fed3fb8 100644 --- a/src/component/component.service.ts +++ b/src/component/component.service.ts @@ -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, private readonly toolsService: ToolsService, + private readonly dictService: DictService, ) {} async all(): Promise { + await this.dictService.refreshDecodeCache(); + const components = await this.userRepository .createQueryBuilder('component') .getMany(); @@ -26,6 +29,8 @@ export class ComponentService { pageSize, ...args }: PageParams): Promise> { + 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( - list.map((component) => new ComponentDto(component)), - total, - ); + return this.toolsService.page(list, total); } async save(component: Component): Promise { @@ -104,6 +106,8 @@ export class ComponentService { } async find(id: number): Promise { + await this.dictService.refreshDecodeCache(); + const component = await this.userRepository .createQueryBuilder('component') .select([ diff --git a/src/dict/dict.controller.ts b/src/dict/dict.controller.ts index 164afe3..4ab74cb 100644 --- a/src/dict/dict.controller.ts +++ b/src/dict/dict.controller.ts @@ -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) { diff --git a/src/dict/dict.entity.ts b/src/dict/dict.entity.ts new file mode 100644 index 0000000..8871ea1 --- /dev/null +++ b/src/dict/dict.entity.ts @@ -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; +} diff --git a/src/dict/dict.module.ts b/src/dict/dict.module.ts index b72e7ae..37f000e 100644 --- a/src/dict/dict.module.ts +++ b/src/dict/dict.module.ts @@ -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], diff --git a/src/dict/dict.service.ts b/src/dict/dict.service.ts index 6d54d04..34b1871 100644 --- a/src/dict/dict.service.ts +++ b/src/dict/dict.service.ts @@ -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, + ) {} - async getComponentDictByType(type: ComponentTypeEnum): Promise { - switch (type) { - case ComponentTypeEnum.CHART: - return this.toolsService.getDictByKey(DictKeyEnum.CHART); + async onApplicationBootstrap() { + await this.refreshDecodeCache(); + } - case ComponentTypeEnum.COMPONENT: - return this.toolsService.getDictByKey(DictKeyEnum.COMPONENT); + async getDictByKey(dictKey: string): Promise { + const list = await this.dictRepository.find({ + where: { + dictKey, + is_deleted: false, + }, + order: { + sort: 'ASC', + createTime: 'ASC', + }, + }); - default: - return this.toolsService.getDictByKey(DictKeyEnum.CHART); - } + return list.map(({ label, value }) => ({ + label, + value: Number.isNaN(Number(value)) ? value : Number(value), + })); + } + + async getComponentDictByType(type: number): Promise { + // 一级类型的 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); } } diff --git a/src/middleware/save.middleware.ts b/src/middleware/save.middleware.ts deleted file mode 100644 index 8bcb831..0000000 --- a/src/middleware/save.middleware.ts +++ /dev/null @@ -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(); - } -} diff --git a/src/minio/minio.controller.ts b/src/minio/minio.controller.ts index 604b8d5..82c2fff 100644 --- a/src/minio/minio.controller.ts +++ b/src/minio/minio.controller.ts @@ -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状态' }) diff --git a/src/minio/minio.module.ts b/src/minio/minio.module.ts index c1334d7..c5bdff5 100644 --- a/src/minio/minio.module.ts +++ b/src/minio/minio.module.ts @@ -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], diff --git a/src/minio/minio.service.ts b/src/minio/minio.service.ts index e04e71f..4e3e54e 100644 --- a/src/minio/minio.service.ts +++ b/src/minio/minio.service.ts @@ -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 { + async removeObject( + objectName: string, + bucketName?: string, + ): Promise { 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); diff --git a/src/utils/constant.ts b/src/utils/constant.ts deleted file mode 100644 index c9f7b3b..0000000 --- a/src/utils/constant.ts +++ /dev/null @@ -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 = 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); diff --git a/src/utils/tool.service.ts b/src/utils/tool.service.ts deleted file mode 100644 index 2b9c77b..0000000 --- a/src/utils/tool.service.ts +++ /dev/null @@ -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(list: T[], total: number): Page { - 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( - alias: string, - wheres: Array, - likes: Array, - values: Partial, - operator = 'AND', - ): [string, Record] { - 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), - ]; - } - - getDictByKey(key: DictKeyType): Dict[] { - if (!DictKeyEnum[key]) { - return []; - } - - return DictKeyMap.get(key); - } - - dictFormat( - label: string, - value: any, - other: Partial, - ): Dict { - const options = { - label, - value, - ...other, - }; - - return options; - } -}