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:
expressgy 2025-07-07 21:31:46 +08:00
parent b11dfa522b
commit 09a5dc30f1
5 changed files with 195 additions and 8 deletions

View File

@ -9,8 +9,19 @@
import { Elysia } from 'elysia';
import { dictService } from './dict.service';
import { CreateDictSchema, GetDictByIdSchema, GetDictTreeQuerySchema } from './dict.schema';
import { CreateDictResponsesSchema, GetDictByIdResponsesSchema, GetDictTreeResponsesSchema } from './dict.response';
import {
CreateDictSchema,
GetDictByIdSchema,
GetDictTreeByCodeParamsSchema,
GetDictTreeByCodeQuerySchema,
GetDictTreeQuerySchema,
} from './dict.schema';
import {
CreateDictResponsesSchema,
GetDictByIdResponsesSchema,
GetDictTreeByCodeResponsesSchema,
GetDictTreeResponsesSchema,
} from './dict.response';
import { tags } from '@/constants/swaggerTags';
/**
@ -62,4 +73,20 @@ export const dictController = new Elysia()
operationId: 'getDictTree',
},
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,
});

View File

@ -265,3 +265,22 @@ export const GetDictTreeResponsesSchema = {
/** 获取完整字典树成功响应数据类型 */
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]>;

View File

@ -164,7 +164,40 @@ export const GetDictTreeQuerySchema = t.Object({
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获取指定字典树的请求参数验证规则
*/
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

View File

@ -10,11 +10,16 @@
import { Logger } from '@/plugins/logger/logger.service';
import { db } from '@/plugins/drizzle/drizzle.service';
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 { nextId } from '@/utils/snowflake';
import type { CreateDictRequest, GetDictTreeQueryRequest } from './dict.schema';
import type { CreateDictSuccessType, GetDictByIdSuccessType, GetDictTreeSuccessType } from './dict.response';
import type { CreateDictRequest, GetDictTreeByCodeRequest, GetDictTreeQueryRequest } from './dict.schema';
import type {
CreateDictSuccessType,
GetDictByIdSuccessType,
GetDictTreeByCodeSuccessType,
GetDictTreeSuccessType,
} from './dict.response';
/**
*
@ -217,6 +222,79 @@ export class DictService {
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] : [], '获取指定字典树成功');
}
}
// 导出单例实例

View File

@ -154,3 +154,33 @@
- **请求路径**: `/api/dict/tree`
- **预期响应 (200 OK)**:
- 响应体 `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` 的节点。