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
|
# KT Template Online API
|
||||||
|
|
||||||
后端服务默认监听 `48085`,Swagger 地址为 `/api`,OpenAPI JSON 地址为 `/api-json`。接口除文件下载外,统一返回 `{ code, msg, data }`。
|
后端服务默认监听 `48085`,Swagger 地址为 `/api`,OpenAPI JSON 地址为 `/api-json`。
|
||||||
|
|
||||||
## 通用响应
|
除文件下载接口外,接口统一返回:
|
||||||
|
|
||||||
```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
|
### Component
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| ---------------- | ------- | ---------------------------- |
|
| ---------------- | ------- | ---------------------------------- |
|
||||||
| id | string | 组件 ID,新增时由后端生成 |
|
| id | string | 组件 ID,新增时由后端生成 |
|
||||||
| name | string | 组件名称 |
|
| name | string | 组件名称 |
|
||||||
| type | number | 一级类型,`1` 图表,`2` 组件 |
|
| type | number | 一级类型,实际含义由 `dict` 表维护 |
|
||||||
| componentType | number | 二级类型 |
|
| componentType | number | 二级类型,实际含义由 `dict` 表维护 |
|
||||||
| typeMsg | string | 一级类型文本,列表接口返回 |
|
| typeMsg | string | 一级类型文本,查询后自动映射 |
|
||||||
| componentTypeMsg | string | 二级类型文本,列表接口返回 |
|
| componentTypeMsg | string | 二级类型文本,查询后自动映射 |
|
||||||
| image | string | 封面图 |
|
| image | string | 封面图或封面图地址 |
|
||||||
| template | string | playground 序列化模板内容 |
|
| template | string | Playground 序列化模板内容 |
|
||||||
| createTime | string | 创建时间 |
|
| createTime | string | 创建时间 |
|
||||||
| updateTime | string | 更新时间 |
|
| updateTime | string | 更新时间 |
|
||||||
| is_deleted | boolean | 逻辑删除标记 |
|
| is_deleted | boolean | 逻辑删除标记 |
|
||||||
|
|
||||||
### 字典
|
### Dict
|
||||||
|
|
||||||
`COMPONENT_TYPE`:
|
接口返回的字典项结构:
|
||||||
|
|
||||||
| label | value |
|
| 字段 | 类型 | 说明 |
|
||||||
| ----- | ----- |
|
| ----- | ------------- | -------- |
|
||||||
| 图表 | 1 |
|
| label | string | 展示文本 |
|
||||||
| 组件 | 2 |
|
| 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`
|
### GET `/component/allList`
|
||||||
|
|
||||||
获取全部组件。
|
获取全部组件。
|
||||||
|
|
||||||
响应示例:
|
响应 `data`:`Component[]`。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -91,19 +133,26 @@
|
|||||||
|
|
||||||
### GET `/component/list`
|
### GET `/component/list`
|
||||||
|
|
||||||
分页获取组件列表。
|
分页获取组件列表。列表默认过滤 `is_deleted=false`,并支持按名称模糊搜索。
|
||||||
|
|
||||||
Query:
|
Query:
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
| ------------- | ------ | ---- | ------------ |
|
| ------------- | ------ | ---- | ---------------- |
|
||||||
| pageNo | number | 是 | 页码 |
|
| pageNo | number | 是 | 页码 |
|
||||||
| pageSize | number | 是 | 每页条数 |
|
| pageSize | number | 是 | 每页条数 |
|
||||||
| name | string | 否 | 名称模糊搜索 |
|
| name | string | 否 | 组件名称模糊搜索 |
|
||||||
| type | number | 否 | 一级类型 |
|
| type | number | 否 | 一级类型 |
|
||||||
| componentType | number | 否 | 二级类型 |
|
| componentType | number | 否 | 二级类型 |
|
||||||
|
|
||||||
响应 `data`:`{ list: Component[], total: number }`。
|
响应 `data`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
list: Component[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### GET `/component/detail`
|
### GET `/component/detail`
|
||||||
|
|
||||||
@ -119,7 +168,7 @@ Query:
|
|||||||
|
|
||||||
### POST `/component/save`
|
### POST `/component/save`
|
||||||
|
|
||||||
新增组件。`SaveMiddleware` 会删除 body 中的 `id`,新增时不需要传 `id`。
|
新增组件。全局 `SaveBodyInterceptor` 会删除 `body.id`,新增时不需要传 `id`。
|
||||||
|
|
||||||
Body:
|
Body:
|
||||||
|
|
||||||
@ -133,7 +182,7 @@ Body:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
响应示例:
|
响应 `data`:新增组件 ID。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -174,17 +223,17 @@ Query:
|
|||||||
|
|
||||||
响应 `data`:`true` 表示删除成功。
|
响应 `data`:`true` 表示删除成功。
|
||||||
|
|
||||||
## Dict
|
## Dict 接口
|
||||||
|
|
||||||
### GET `/dict/getDictByKey`
|
### GET `/dict/getDictByKey`
|
||||||
|
|
||||||
根据字典 key 获取字典。
|
根据字典分组获取字典项。
|
||||||
|
|
||||||
Query:
|
Query:
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 可选值 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
| ------- | ------ | ---- | -------------------------------------- |
|
| ------- | ------ | ---- | ---------------------- |
|
||||||
| dictKey | string | 是 | `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
| dictKey | string | 是 | 字典分组,例如 `CHART` |
|
||||||
|
|
||||||
响应示例:
|
响应示例:
|
||||||
|
|
||||||
@ -194,12 +243,8 @@ Query:
|
|||||||
"msg": "操作成功",
|
"msg": "操作成功",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"label": "图表",
|
"label": "折线图",
|
||||||
"value": 1
|
"value": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "组件",
|
|
||||||
"value": 2
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -207,23 +252,29 @@ Query:
|
|||||||
|
|
||||||
### GET `/dict/getComponentDictByType`
|
### GET `/dict/getComponentDictByType`
|
||||||
|
|
||||||
根据一级类型获取二级类型字典。
|
根据组件一级类型获取对应的二级类型字典。
|
||||||
|
|
||||||
|
查询逻辑:先查 `dictKey=COMPONENT_TYPE` 且 `value=type` 的字典项,再使用该项的 `childrenKey` 查询子字典。
|
||||||
|
|
||||||
Query:
|
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`
|
### GET `/minio/check`
|
||||||
|
|
||||||
检查 MinIO 连接和 bucket 状态。
|
检查 MinIO 连接和 bucket 状态。
|
||||||
|
|
||||||
Query:`bucketName?: string`
|
Query:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| ---------- | ------ | ---- | ----------- |
|
||||||
|
| bucketName | string | 否 | bucket 名称 |
|
||||||
|
|
||||||
响应 `data`:`{ bucketName: string; exists: boolean }`。
|
响应 `data`:`{ bucketName: string; exists: boolean }`。
|
||||||
|
|
||||||
@ -231,7 +282,11 @@ Query:`bucketName?: string`
|
|||||||
|
|
||||||
创建 bucket,已存在时跳过。
|
创建 bucket,已存在时跳过。
|
||||||
|
|
||||||
Query:`bucketName?: string`
|
Query:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| ---------- | ------ | ---- | ----------- |
|
||||||
|
| bucketName | string | 否 | bucket 名称 |
|
||||||
|
|
||||||
响应 `data`: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`。
|
响应 `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。
|
响应 `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`
|
### DELETE `/minio/remove`
|
||||||
|
|
||||||
删除文件。
|
删除文件。
|
||||||
|
|
||||||
Query:`objectName: string`、`bucketName?: string`
|
Query:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| ---------- | ------ | ---- | ----------- |
|
||||||
|
| objectName | string | 是 | 对象名 |
|
||||||
|
| bucketName | string | 否 | bucket 名称 |
|
||||||
|
|
||||||
响应 `data`:`true` 表示删除成功。
|
响应 `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`
|
||||||
|
|
||||||
# 生产环境
|
### Dict
|
||||||
$ pnpm start:prod
|
|
||||||
|
维护数据库字典:
|
||||||
|
|
||||||
|
- 字典项存储在 `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
|
```bash
|
||||||
# unit tests
|
pnpm install
|
||||||
$ pnpm test
|
```
|
||||||
|
|
||||||
# e2e tests
|
开发环境:
|
||||||
$ pnpm test:e2e
|
|
||||||
|
```bash
|
||||||
# test coverage
|
pnpm start:dev
|
||||||
$ pnpm test:cov
|
```
|
||||||
|
|
||||||
|
普通启动:
|
||||||
|
|
||||||
|
```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 { ConfigModule } from '@nestjs/config';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { MinioModule } from 'nestjs-minio-client';
|
import { MinioModule } from 'nestjs-minio-client';
|
||||||
import { ComponentModule } from './component/component.module';
|
import { ComponentModule } from './component/component.module';
|
||||||
import { DictModule } from './dict/dict.module';
|
import { DictModule } from './dict/dict.module';
|
||||||
import { MinioClientModule } from './minio/minio.module';
|
import { MinioClientModule } from './minio/minio.module';
|
||||||
import { SaveMiddleware } from './middleware/save.middleware';
|
import { SaveBodyInterceptor } from './common';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -51,10 +52,13 @@ import { SaveMiddleware } from './middleware/save.middleware';
|
|||||||
DictModule,
|
DictModule,
|
||||||
MinioClientModule,
|
MinioClientModule,
|
||||||
],
|
],
|
||||||
providers: [AppService, ConfigService],
|
providers: [
|
||||||
|
AppService,
|
||||||
|
ConfigService,
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: SaveBodyInterceptor,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule {}
|
||||||
configure(consumer: MiddlewareConsumer) {
|
|
||||||
consumer.apply(SaveMiddleware).forRoutes('*/save');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
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 {
|
export * from './decorators/decode-dict.decorator';
|
||||||
return (target, key: string | symbol) => {
|
export * from './interceptors/save-body.interceptor';
|
||||||
Reflect.defineProperty(target, `_${key.toString()}`, {
|
export * from './services/tool.service';
|
||||||
set(newVal) {
|
export * from './swagger/swagger-response';
|
||||||
this[key] = dict.find((i) => i.value == newVal).label;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
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,
|
ApiTags,
|
||||||
PartialType,
|
PartialType,
|
||||||
} from '@nestjs/swagger';
|
} 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 {
|
import {
|
||||||
PaginatedDto,
|
PaginatedDto,
|
||||||
ApiArrayResponse,
|
ApiArrayResponse,
|
||||||
ApiModelResponse,
|
ApiModelResponse,
|
||||||
ApiPageResponse,
|
ApiPageResponse,
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
} from '@/common/swagger-response';
|
ToolsService,
|
||||||
|
} from '@/common';
|
||||||
|
import { ComponentService } from './component.service';
|
||||||
|
import { Component } from './component.entity';
|
||||||
|
|
||||||
const componentExample = {
|
const componentExample = {
|
||||||
id: '1d8d3dd2-99f0-4d10-9a44-0cf9566b37c9',
|
id: '1d8d3dd2-99f0-4d10-9a44-0cf9566b37c9',
|
||||||
@ -65,11 +64,11 @@ export class ComponentController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly componentService: ComponentService,
|
private readonly componentService: ComponentService,
|
||||||
) {} //注入服务
|
) {}
|
||||||
|
|
||||||
@Get('allList')
|
@Get('allList')
|
||||||
@ApiOperation({ summary: '获取组件列表' })
|
@ApiOperation({ summary: '获取组件列表' })
|
||||||
@ApiArrayResponse(ComponentDto, [componentExample])
|
@ApiArrayResponse(Component, [componentExample])
|
||||||
async getAllList(@Res() res) {
|
async getAllList(@Res() res) {
|
||||||
const list = await this.componentService.all();
|
const list = await this.componentService.all();
|
||||||
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
||||||
@ -78,11 +77,11 @@ export class ComponentController {
|
|||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: '获取组件列表分页' })
|
@ApiOperation({ summary: '获取组件列表分页' })
|
||||||
@ApiQuery({ type: [CompPageDto] })
|
@ApiQuery({ type: [CompPageDto] })
|
||||||
@ApiPageResponse(ComponentDto, [componentExample], 1)
|
@ApiPageResponse(Component, [componentExample], 1)
|
||||||
async getList(
|
async getList(
|
||||||
@Res() res,
|
@Res() res,
|
||||||
@Query() { pageNo, pageSize, ...args }: PageParams<ComponentDto>,
|
@Query() { pageNo, pageSize, ...args }: PageParams<Component>,
|
||||||
): Promise<PaginatedDto<ComponentDto>> {
|
): Promise<PaginatedDto<Component>> {
|
||||||
const list = await this.componentService.page({
|
const list = await this.componentService.page({
|
||||||
pageNo,
|
pageNo,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -164,7 +163,7 @@ export class ComponentController {
|
|||||||
@Get('detail')
|
@Get('detail')
|
||||||
@ApiOperation({ summary: '组件详情' })
|
@ApiOperation({ summary: '组件详情' })
|
||||||
@ApiQuery({ name: 'id', type: String })
|
@ApiQuery({ name: 'id', type: String })
|
||||||
@ApiModelResponse(ComponentDto, componentExample)
|
@ApiModelResponse(Component, componentExample)
|
||||||
async detail(@Res() res, @Query('id') id) {
|
async detail(@Res() res, @Query('id') id) {
|
||||||
const detail = await this.componentService.find(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 {
|
import {
|
||||||
|
AfterLoad,
|
||||||
Entity,
|
Entity,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Column,
|
Column,
|
||||||
@ -6,15 +7,10 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { ComponentTypeEnum, ComponentEnum } from '@/utils/constant';
|
import { DecodeDictKey, decodeDictKeys } from '@/common';
|
||||||
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Component {
|
export class Component {
|
||||||
constructor(component?: Component) {
|
|
||||||
Object.assign(this, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
@ -25,27 +21,32 @@ export class Component {
|
|||||||
})
|
})
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty()
|
||||||
type: 'enum',
|
|
||||||
enum: ComponentTypeEnum,
|
|
||||||
})
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'enum',
|
type: 'int',
|
||||||
enum: ComponentTypeEnum,
|
})
|
||||||
|
@DecodeDictKey('COMPONENT_TYPE', {
|
||||||
|
targetKey: 'typeMsg',
|
||||||
})
|
})
|
||||||
type: number;
|
type: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty()
|
||||||
type: 'enum',
|
|
||||||
enum: ComponentEnum,
|
|
||||||
})
|
|
||||||
@Column({
|
@Column({
|
||||||
name: 'component_type',
|
name: 'component_type',
|
||||||
type: 'enum',
|
type: 'int',
|
||||||
enum: ComponentEnum,
|
})
|
||||||
|
// 二级类型值由数据库字典维护;未指定 dictKey 时会在全部字典缓存中匹配。
|
||||||
|
@DecodeDictKey(undefined, {
|
||||||
|
targetKey: 'componentTypeMsg',
|
||||||
})
|
})
|
||||||
componentType: number;
|
componentType: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
typeMsg: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
componentTypeMsg: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column({
|
@Column({
|
||||||
type: 'mediumtext',
|
type: 'mediumtext',
|
||||||
@ -74,4 +75,10 @@ export class Component {
|
|||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
is_deleted: boolean;
|
is_deleted: boolean;
|
||||||
|
|
||||||
|
@AfterLoad()
|
||||||
|
decodeDictKeys() {
|
||||||
|
// 查询结果初始化完成后再翻译,避免构造/赋值阶段覆盖派生字段。
|
||||||
|
decodeDictKeys(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { ComponentController } from './component.controller';
|
import { ComponentController } from './component.controller';
|
||||||
import { ComponentService } from './component.service';
|
import { ComponentService } from './component.service';
|
||||||
import { Component } from './component.entity';
|
import { Component } from './component.entity';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
import { ToolsService } from '@/common';
|
||||||
|
import { DictModule } from '@/dict/dict.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Component])],
|
imports: [TypeOrmModule.forFeature([Component]), DictModule],
|
||||||
controllers: [ComponentController],
|
controllers: [ComponentController],
|
||||||
providers: [ComponentService, ToolsService],
|
providers: [ComponentService, ToolsService],
|
||||||
exports: [ComponentService],
|
exports: [ComponentService],
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Component } from './component.entity';
|
import { Component } from './component.entity';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
import { ToolsService } from '@/common';
|
||||||
import { isNumber, omit, pick } from 'lodash';
|
import { isNumber, omit, pick } from 'lodash';
|
||||||
import { ComponentDto } from './component.dto';
|
import { DictService } from '@/dict/dict.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ComponentService {
|
export class ComponentService {
|
||||||
@ -12,9 +12,12 @@ export class ComponentService {
|
|||||||
@InjectRepository(Component)
|
@InjectRepository(Component)
|
||||||
private readonly userRepository: Repository<Component>,
|
private readonly userRepository: Repository<Component>,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
|
private readonly dictService: DictService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async all(): Promise<Component[]> {
|
async all(): Promise<Component[]> {
|
||||||
|
await this.dictService.refreshDecodeCache();
|
||||||
|
|
||||||
const components = await this.userRepository
|
const components = await this.userRepository
|
||||||
.createQueryBuilder('component')
|
.createQueryBuilder('component')
|
||||||
.getMany();
|
.getMany();
|
||||||
@ -26,6 +29,8 @@ export class ComponentService {
|
|||||||
pageSize,
|
pageSize,
|
||||||
...args
|
...args
|
||||||
}: PageParams<Component>): Promise<Page<Component>> {
|
}: PageParams<Component>): Promise<Page<Component>> {
|
||||||
|
await this.dictService.refreshDecodeCache();
|
||||||
|
|
||||||
const hasOwnEntity = new Component();
|
const hasOwnEntity = new Component();
|
||||||
|
|
||||||
const [wheres, likes] = [['is_deleted'], ['name']] as Array<
|
const [wheres, likes] = [['is_deleted'], ['name']] as Array<
|
||||||
@ -69,10 +74,7 @@ export class ComponentService {
|
|||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
return this.toolsService.page<Component>(
|
return this.toolsService.page<Component>(list, total);
|
||||||
list.map((component) => new ComponentDto(component)),
|
|
||||||
total,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(component: Component): Promise<Component> {
|
async save(component: Component): Promise<Component> {
|
||||||
@ -104,6 +106,8 @@ export class ComponentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async find(id: number): Promise<Component> {
|
async find(id: number): Promise<Component> {
|
||||||
|
await this.dictService.refreshDecodeCache();
|
||||||
|
|
||||||
const component = await this.userRepository
|
const component = await this.userRepository
|
||||||
.createQueryBuilder('component')
|
.createQueryBuilder('component')
|
||||||
.select([
|
.select([
|
||||||
|
|||||||
@ -6,11 +6,9 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Res,
|
Res,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
|
||||||
import { DictService } from './dict.service';
|
import { DictService } from './dict.service';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { ComponentTypeEnum, DictKeyEnum, DictKeyType } from '@/utils/constant';
|
import { ApiArrayResponse, ToolsService } from '@/common';
|
||||||
import { ApiArrayResponse } from '@/common/swagger-response';
|
|
||||||
import { DictDto } from './dict.dto';
|
import { DictDto } from './dict.dto';
|
||||||
|
|
||||||
const componentTypeDictExample = [
|
const componentTypeDictExample = [
|
||||||
@ -41,20 +39,20 @@ export class DictController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly dictService: DictService,
|
private readonly dictService: DictService,
|
||||||
) {} //注入服务
|
) {}
|
||||||
|
|
||||||
@ApiOperation({ summary: '根据key获取字典' })
|
@ApiOperation({ summary: '根据key获取字典' })
|
||||||
@ApiQuery({ name: 'dictKey', enum: DictKeyEnum })
|
@ApiQuery({ name: 'dictKey', type: String })
|
||||||
@ApiArrayResponse(DictDto, componentTypeDictExample)
|
@ApiArrayResponse(DictDto, componentTypeDictExample)
|
||||||
@Get('getDictByKey')
|
@Get('getDictByKey')
|
||||||
async getDictByKey(@Res() res, @Query('dictKey') dictKey: DictKeyType) {
|
async getDictByKey(@Res() res, @Query('dictKey') dictKey: string) {
|
||||||
const dict = this.toolsService.getDictByKey(dictKey);
|
const dict = await this.dictService.getDictByKey(dictKey);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', dict));
|
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', dict));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: '根据组件类型获取组件字典' })
|
@ApiOperation({ summary: '根据组件类型获取组件字典' })
|
||||||
@ApiQuery({ name: 'type', enum: ComponentTypeEnum })
|
@ApiQuery({ name: 'type', type: Number })
|
||||||
@ApiArrayResponse(DictDto, chartDictExample)
|
@ApiArrayResponse(DictDto, chartDictExample)
|
||||||
@Get('getComponentDictByType')
|
@Get('getComponentDictByType')
|
||||||
async getComponentDictByType(@Res() res, @Query('type', ParseIntPipe) type) {
|
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 { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { DictController } from './dict.controller';
|
import { DictController } from './dict.controller';
|
||||||
import { DictService } from './dict.service';
|
import { DictService } from './dict.service';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
import { ToolsService } from '@/common';
|
||||||
|
import { DictEntity } from './dict.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([DictEntity])],
|
||||||
controllers: [DictController],
|
controllers: [DictController],
|
||||||
providers: [DictService, ToolsService],
|
providers: [DictService, ToolsService],
|
||||||
exports: [DictService],
|
exports: [DictService],
|
||||||
|
|||||||
@ -1,21 +1,67 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { ComponentTypeEnum, DictKeyEnum } from '@/utils/constant';
|
import { Repository } from 'typeorm';
|
||||||
|
import { setDictDecodeCache } from '@/common';
|
||||||
|
import { DictEntity } from './dict.entity';
|
||||||
|
|
||||||
|
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DictService {
|
export class DictService implements OnApplicationBootstrap {
|
||||||
constructor(private readonly toolsService: ToolsService) {}
|
constructor(
|
||||||
|
@InjectRepository(DictEntity)
|
||||||
|
private readonly dictRepository: Repository<DictEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getComponentDictByType(type: ComponentTypeEnum): Promise<Dict[]> {
|
async onApplicationBootstrap() {
|
||||||
switch (type) {
|
await this.refreshDecodeCache();
|
||||||
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 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,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
|
||||||
import { MinioClientService } from './minio.service';
|
import { MinioClientService } from './minio.service';
|
||||||
import type { MinioUploadFile } from './minio.service';
|
import type { MinioUploadFile } from './minio.service';
|
||||||
import {
|
import {
|
||||||
@ -27,7 +26,8 @@ import {
|
|||||||
ApiArrayResponse,
|
ApiArrayResponse,
|
||||||
ApiModelResponse,
|
ApiModelResponse,
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
} from '@/common/swagger-response';
|
ToolsService,
|
||||||
|
} from '@/common';
|
||||||
import {
|
import {
|
||||||
MinioBucketStatusDto,
|
MinioBucketStatusDto,
|
||||||
MinioObjectDto,
|
MinioObjectDto,
|
||||||
@ -40,7 +40,7 @@ export class MinioClientController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly minioClientService: MinioClientService,
|
private readonly minioClientService: MinioClientService,
|
||||||
) {} //注入服务
|
) {}
|
||||||
|
|
||||||
@Get('check')
|
@Get('check')
|
||||||
@ApiOperation({ summary: '检查MinIO连接和Bucket状态' })
|
@ApiOperation({ summary: '检查MinIO连接和Bucket状态' })
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { MinioClientController } from './minio.controller';
|
import { MinioClientController } from './minio.controller';
|
||||||
import { MinioClientService } from './minio.service';
|
import { MinioClientService } from './minio.service';
|
||||||
import { ToolsService } from '@/utils/tool.service';
|
import { ToolsService } from '@/common';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
|||||||
@ -81,7 +81,8 @@ export class MinioClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetBucket = await this.ensureBucket(bucketName);
|
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(
|
const result = await this.client.putObject(
|
||||||
targetBucket,
|
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) {
|
if (!objectName) {
|
||||||
throw new BadRequestException('objectName不能为空');
|
throw new BadRequestException('objectName不能为空');
|
||||||
}
|
}
|
||||||
@ -171,6 +175,7 @@ export class MinioClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createObjectName(originalName: string): string {
|
private createObjectName(originalName: string): string {
|
||||||
|
// 前端未指定对象名时,生成带时间和随机段的路径,降低同名文件覆盖概率。
|
||||||
const safeName = originalName.replace(/[\\/]/g, '_');
|
const safeName = originalName.replace(/[\\/]/g, '_');
|
||||||
const random = Math.random().toString(36).slice(2, 8);
|
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