feat(dict): 添加获取指定字典树接口
- **新增接口** - 添加 `GET /api/dict/tree/:code` 接口,用于获取指定 code 的子树。 - 支持通过 `status` 和 `is_system` 查询参数对子树进行过滤。 - **Service层** - 在 `DictService` 中实现 `getDictTreeByCode` 方法。 - 解决了 Drizzle ORM 递归查询的类型问题,改用在内存中构建树的可靠方法。 - **Schema和Response** - 在 `dict.schema.ts` 中将 `GetDictTreeByCodeSchema` 拆分为 `Params` 和 `Query` 两部分,以适应 Elysia 的路由定义。 - 在 `dict.response.ts` 中添加了 `GetDictTreeByCodeResponsesSchema`,增加了 404 错误响应。 - **文档** - 在 `dict.test.md` 中为新接口添加了详细的测试用例。
This commit is contained in:
parent
b11dfa522b
commit
09a5dc30f1
@ -9,8 +9,19 @@
|
|||||||
|
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { dictService } from './dict.service';
|
import { dictService } from './dict.service';
|
||||||
import { CreateDictSchema, GetDictByIdSchema, GetDictTreeQuerySchema } from './dict.schema';
|
import {
|
||||||
import { CreateDictResponsesSchema, GetDictByIdResponsesSchema, GetDictTreeResponsesSchema } from './dict.response';
|
CreateDictSchema,
|
||||||
|
GetDictByIdSchema,
|
||||||
|
GetDictTreeByCodeParamsSchema,
|
||||||
|
GetDictTreeByCodeQuerySchema,
|
||||||
|
GetDictTreeQuerySchema,
|
||||||
|
} from './dict.schema';
|
||||||
|
import {
|
||||||
|
CreateDictResponsesSchema,
|
||||||
|
GetDictByIdResponsesSchema,
|
||||||
|
GetDictTreeByCodeResponsesSchema,
|
||||||
|
GetDictTreeResponsesSchema,
|
||||||
|
} from './dict.response';
|
||||||
import { tags } from '@/constants/swaggerTags';
|
import { tags } from '@/constants/swaggerTags';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,4 +73,20 @@ export const dictController = new Elysia()
|
|||||||
operationId: 'getDictTree',
|
operationId: 'getDictTree',
|
||||||
},
|
},
|
||||||
response: GetDictTreeResponsesSchema,
|
response: GetDictTreeResponsesSchema,
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 获取指定字典树接口
|
||||||
|
* @route GET /api/dict/tree/:code
|
||||||
|
* @description 根据字典代码获取其对应的子树结构
|
||||||
|
*/
|
||||||
|
.get('/tree/:code', ({ params, query }) => dictService.getDictTreeByCode(params, query), {
|
||||||
|
params: GetDictTreeByCodeParamsSchema,
|
||||||
|
query: GetDictTreeByCodeQuerySchema,
|
||||||
|
detail: {
|
||||||
|
summary: '获取指定字典树',
|
||||||
|
description: '根据字典代码获取其对应的子树结构,可根据状态和是否系统字典进行过滤',
|
||||||
|
tags: [tags.dict],
|
||||||
|
operationId: 'getDictTreeByCode',
|
||||||
|
},
|
||||||
|
response: GetDictTreeByCodeResponsesSchema,
|
||||||
});
|
});
|
||||||
|
@ -265,3 +265,22 @@ export const GetDictTreeResponsesSchema = {
|
|||||||
|
|
||||||
/** 获取完整字典树成功响应数据类型 */
|
/** 获取完整字典树成功响应数据类型 */
|
||||||
export type GetDictTreeSuccessType = Static<(typeof GetDictTreeResponsesSchema)[200]>;
|
export type GetDictTreeSuccessType = Static<(typeof GetDictTreeResponsesSchema)[200]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定字典树接口响应组合
|
||||||
|
* @description 用于Controller中定义所有可能的响应格式
|
||||||
|
*/
|
||||||
|
export const GetDictTreeByCodeResponsesSchema = {
|
||||||
|
...GetDictTreeResponsesSchema,
|
||||||
|
404: responseWrapperSchema(
|
||||||
|
t.Object({
|
||||||
|
error: t.String({
|
||||||
|
description: '资源不存在',
|
||||||
|
examples: ['字典代码不存在'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取指定字典树成功响应数据类型 */
|
||||||
|
export type GetDictTreeByCodeSuccessType = Static<(typeof GetDictTreeByCodeResponsesSchema)[200]>;
|
||||||
|
@ -164,7 +164,40 @@ export const GetDictTreeQuerySchema = t.Object({
|
|||||||
export type GetDictTreeQueryRequest = Static<typeof GetDictTreeQuerySchema>;
|
export type GetDictTreeQueryRequest = Static<typeof GetDictTreeQuerySchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定字典树请求参数Schema
|
* 获取指定字典树路径参数Schema
|
||||||
|
*/
|
||||||
|
export const GetDictTreeByCodeParamsSchema = t.Object({
|
||||||
|
/** 字典代码 */
|
||||||
|
code: t.String({
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 50,
|
||||||
|
description: '字典代码,用于查找指定的字典树',
|
||||||
|
examples: ['user_status', 'order_type', 'system_config'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定字典树查询参数Schema
|
||||||
|
*/
|
||||||
|
export const GetDictTreeByCodeQuerySchema = t.Object({
|
||||||
|
/** 状态过滤 */
|
||||||
|
status: t.Optional(
|
||||||
|
t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('all')], {
|
||||||
|
description: '状态过滤条件',
|
||||||
|
examples: ['active', 'inactive', 'all'],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
/** 是否系统字典过滤 */
|
||||||
|
isSystem: t.Optional(
|
||||||
|
t.Union([t.Literal('true'), t.Literal('false'), t.Literal('all')], {
|
||||||
|
description: '是否系统字典过滤条件',
|
||||||
|
examples: ['true', 'false', 'all'],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use GetDictTreeByCodeParamsSchema and GetDictTreeByCodeQuerySchema instead
|
||||||
* @description 根据code获取指定字典树的请求参数验证规则
|
* @description 根据code获取指定字典树的请求参数验证规则
|
||||||
*/
|
*/
|
||||||
export const GetDictTreeByCodeSchema = t.Object({
|
export const GetDictTreeByCodeSchema = t.Object({
|
||||||
@ -192,7 +225,7 @@ export const GetDictTreeByCodeSchema = t.Object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** 获取指定字典树请求参数类型 */
|
/** 获取指定字典树请求参数类型 */
|
||||||
export type GetDictTreeByCodeRequest = Static<typeof GetDictTreeByCodeSchema>;
|
export type GetDictTreeByCodeRequest = Static<typeof GetDictTreeByCodeQuerySchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新字典项请求参数Schema
|
* 更新字典项请求参数Schema
|
||||||
|
@ -10,11 +10,16 @@
|
|||||||
import { Logger } from '@/plugins/logger/logger.service';
|
import { Logger } from '@/plugins/logger/logger.service';
|
||||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||||
import { sysDict } from '@/eneities';
|
import { sysDict } from '@/eneities';
|
||||||
import { eq, and, max, asc } from 'drizzle-orm';
|
import { eq, and, max, asc, sql } from 'drizzle-orm';
|
||||||
import { successResponse, BusinessError } from '@/utils/responseFormate';
|
import { successResponse, BusinessError } from '@/utils/responseFormate';
|
||||||
import { nextId } from '@/utils/snowflake';
|
import { nextId } from '@/utils/snowflake';
|
||||||
import type { CreateDictRequest, GetDictTreeQueryRequest } from './dict.schema';
|
import type { CreateDictRequest, GetDictTreeByCodeRequest, GetDictTreeQueryRequest } from './dict.schema';
|
||||||
import type { CreateDictSuccessType, GetDictByIdSuccessType, GetDictTreeSuccessType } from './dict.response';
|
import type {
|
||||||
|
CreateDictSuccessType,
|
||||||
|
GetDictByIdSuccessType,
|
||||||
|
GetDictTreeByCodeSuccessType,
|
||||||
|
GetDictTreeSuccessType,
|
||||||
|
} from './dict.response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典服务类
|
* 字典服务类
|
||||||
@ -217,6 +222,79 @@ export class DictService {
|
|||||||
|
|
||||||
return successResponse(tree, '获取完整字典树成功');
|
return successResponse(tree, '获取完整字典树成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定字典树
|
||||||
|
* @param params 路径参数
|
||||||
|
* @param query 查询参数
|
||||||
|
* @returns Promise<GetDictTreeByCodeSuccessType>
|
||||||
|
*/
|
||||||
|
public async getDictTreeByCode({ code }: { code: string }, query: GetDictTreeByCodeRequest): Promise<GetDictTreeByCodeSuccessType> {
|
||||||
|
Logger.info(`获取指定字典树: ${code}, 查询参数: ${JSON.stringify(query)}`);
|
||||||
|
|
||||||
|
// 1. 查找根节点
|
||||||
|
const rootNodeArr = await db().select({ id: sysDict.id }).from(sysDict).where(eq(sysDict.code, code)).limit(1);
|
||||||
|
if (rootNodeArr.length === 0) {
|
||||||
|
throw new BusinessError(`字典代码不存在: ${code}`, 404);
|
||||||
|
}
|
||||||
|
const rootId = String(rootNodeArr[0]!.id);
|
||||||
|
|
||||||
|
// 2. 获取所有字典数据
|
||||||
|
const allDicts = await db().select().from(sysDict).orderBy(asc(sysDict.sortOrder));
|
||||||
|
|
||||||
|
// 3. 在内存中构建完整的树
|
||||||
|
type DictNode = Omit<typeof allDicts[0], 'id' | 'pid' | 'isSystem'> & {
|
||||||
|
id: string;
|
||||||
|
pid: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
children?: DictNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeMap = new Map<string, DictNode>();
|
||||||
|
allDicts.forEach((item) => {
|
||||||
|
nodeMap.set(String(item.id), {
|
||||||
|
...item,
|
||||||
|
id: String(item.id),
|
||||||
|
pid: String(item.pid),
|
||||||
|
isSystem: Boolean(item.isSystem),
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 定义递归过滤和构建子树的函数
|
||||||
|
const buildSubTree = (node: DictNode): DictNode | null => {
|
||||||
|
// 应用过滤条件
|
||||||
|
if (query.status && query.status !== 'all' && node.status !== query.status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (query.isSystem && query.isSystem !== 'all' && node.isSystem !== (query.isSystem === 'true')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = allDicts
|
||||||
|
.filter(child => String(child.pid) === node.id)
|
||||||
|
.map(child => buildSubTree(nodeMap.get(String(child.id))!))
|
||||||
|
.filter((child): child is DictNode => child !== null);
|
||||||
|
|
||||||
|
if(children.length > 0){
|
||||||
|
node.children = children;
|
||||||
|
} else {
|
||||||
|
delete node.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 找到根节点并构建其子树
|
||||||
|
const root = nodeMap.get(rootId);
|
||||||
|
if(!root) {
|
||||||
|
return successResponse([], '获取指定字典树成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalTree = buildSubTree(root);
|
||||||
|
|
||||||
|
return successResponse(finalTree ? [finalTree] : [], '获取指定字典树成功');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出单例实例
|
// 导出单例实例
|
||||||
|
@ -153,4 +153,34 @@
|
|||||||
- **请求方法**: `GET`
|
- **请求方法**: `GET`
|
||||||
- **请求路径**: `/api/dict/tree`
|
- **请求路径**: `/api/dict/tree`
|
||||||
- **预期响应 (200 OK)**:
|
- **预期响应 (200 OK)**:
|
||||||
- 响应体 `data` 是一个空数组 `[]`。
|
- 响应体 `data` 是一个空数组 `[]`。
|
||||||
|
|
||||||
|
## 4. 获取指定字典树接口 (GET /api/dict/tree/:code)
|
||||||
|
|
||||||
|
### 场景1: 成功获取存在的字典树
|
||||||
|
|
||||||
|
- **名称**: 根据存在的 code 成功获取指定的字典子树
|
||||||
|
- **前置条件**: 数据库中存在一个 code 为 `user_gender` 的字典项,且它有若干子项。
|
||||||
|
- **请求方法**: `GET`
|
||||||
|
- **请求路径**: `/api/dict/tree/user_gender`
|
||||||
|
- **预期响应 (200 OK)**:
|
||||||
|
- 响应体 `data` 是一个数组,仅包含一个根节点(即 `user_gender` 字典项)。
|
||||||
|
- 该根节点包含其所有的子孙节点,形成一棵完整的子树。
|
||||||
|
|
||||||
|
### 场景2: 失败 - code 不存在
|
||||||
|
|
||||||
|
- **名称**: 因 code 不存在导致获取失败
|
||||||
|
- **前置条件**: 数据库中不存在 code 为 `non_existent_code` 的字典项。
|
||||||
|
- **请求方法**: `GET`
|
||||||
|
- **请求路径**: `/api/dict/tree/non_existent_code`
|
||||||
|
- **预期响应 (404 Not Found)**:
|
||||||
|
- 响应体包含错误信息,提示 "字典代码不存在"。
|
||||||
|
|
||||||
|
### 场景3: 带查询参数过滤
|
||||||
|
|
||||||
|
- **名称**: 获取指定字典树并根据状态过滤
|
||||||
|
- **前置条件**: `user_gender` 字典树中,部分子项的状态为 `inactive`。
|
||||||
|
- **请求方法**: `GET`
|
||||||
|
- **请求路径**: `/api/dict/tree/user_gender?status=active`
|
||||||
|
- **预期响应 (200 OK)**:
|
||||||
|
- 返回的 `user_gender` 子树中,只包含状态为 `active` 的节点。
|
Loading…
Reference in New Issue
Block a user