diff --git a/src/modules/dict/dict.controller.ts b/src/modules/dict/dict.controller.ts index 1ec2c82..7701a70 100644 --- a/src/modules/dict/dict.controller.ts +++ b/src/modules/dict/dict.controller.ts @@ -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, }); diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts index a7b2e81..055a13e 100644 --- a/src/modules/dict/dict.response.ts +++ b/src/modules/dict/dict.response.ts @@ -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]>; diff --git a/src/modules/dict/dict.schema.ts b/src/modules/dict/dict.schema.ts index c06c957..c6e8c2f 100644 --- a/src/modules/dict/dict.schema.ts +++ b/src/modules/dict/dict.schema.ts @@ -164,7 +164,40 @@ export const GetDictTreeQuerySchema = t.Object({ export type GetDictTreeQueryRequest = Static; /** - * 获取指定字典树请求参数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; +export type GetDictTreeByCodeRequest = Static; /** * 更新字典项请求参数Schema diff --git a/src/modules/dict/dict.service.ts b/src/modules/dict/dict.service.ts index b9685ab..7d1de49 100644 --- a/src/modules/dict/dict.service.ts +++ b/src/modules/dict/dict.service.ts @@ -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 + */ + public async getDictTreeByCode({ code }: { code: string }, query: GetDictTreeByCodeRequest): Promise { + 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 & { + id: string; + pid: string; + isSystem: boolean; + children?: DictNode[]; + }; + + const nodeMap = new Map(); + 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] : [], '获取指定字典树成功'); + } } // 导出单例实例 diff --git a/src/modules/dict/dict.test.md b/src/modules/dict/dict.test.md index 4e50901..4f3c39a 100644 --- a/src/modules/dict/dict.test.md +++ b/src/modules/dict/dict.test.md @@ -153,4 +153,34 @@ - **请求方法**: `GET` - **请求路径**: `/api/dict/tree` - **预期响应 (200 OK)**: - - 响应体 `data` 是一个空数组 `[]`。 \ No newline at end of file + - 响应体 `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` 的节点。 \ No newline at end of file