diff --git a/src/modules/dict/dict.controller.ts b/src/modules/dict/dict.controller.ts index 9cc6161..1ec2c82 100644 --- a/src/modules/dict/dict.controller.ts +++ b/src/modules/dict/dict.controller.ts @@ -9,8 +9,8 @@ import { Elysia } from 'elysia'; import { dictService } from './dict.service'; -import { CreateDictSchema } from './dict.schema'; -import { CreateDictResponsesSchema } from './dict.response'; +import { CreateDictSchema, GetDictByIdSchema, GetDictTreeQuerySchema } from './dict.schema'; +import { CreateDictResponsesSchema, GetDictByIdResponsesSchema, GetDictTreeResponsesSchema } from './dict.response'; import { tags } from '@/constants/swaggerTags'; /** @@ -32,4 +32,34 @@ export const dictController = new Elysia() operationId: 'createDict', }, response: CreateDictResponsesSchema, + }) + /** + * 获取字典项内容接口 + * @route GET /api/dict/:id + * @description 根据ID获取单个字典项的详细内容 + */ + .get('/:id', ({ params }) => dictService.getDictById(params.id), { + params: GetDictByIdSchema, + detail: { + summary: '获取字典项内容', + description: '根据ID获取单个字典项的详细内容', + tags: [tags.dict], + operationId: 'getDictById', + }, + response: GetDictByIdResponsesSchema, + }) + /** + * 获取完整字典树接口 + * @route GET /api/dict/tree + * @description 获取完整的字典树结构,可根据状态和是否系统字典进行过滤 + */ + .get('/tree', ({ query }) => dictService.getDictTree(query), { + query: GetDictTreeQuerySchema, + detail: { + summary: '获取完整字典树', + description: '获取完整的字典树结构,可根据状态和是否系统字典进行过滤', + tags: [tags.dict], + operationId: 'getDictTree', + }, + response: GetDictTreeResponsesSchema, }); diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts index bf981d5..a7b2e81 100644 --- a/src/modules/dict/dict.response.ts +++ b/src/modules/dict/dict.response.ts @@ -128,3 +128,140 @@ export const CreateDictResponsesSchema = { /** 创建字典项成功响应数据类型 */ export type CreateDictSuccessType = Static<(typeof CreateDictResponsesSchema)[200]>; + +/** + * 获取字典项成功响应数据结构 + * @description 获取单个字典项的成功响应数据结构,与创建成功响应结构相同 + */ +export const GetDictByIdSuccessSchema = CreateDictSuccessSchema; + +/** + * 获取字典项接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const GetDictByIdResponsesSchema = { + 200: responseWrapperSchema(GetDictByIdSuccessSchema), + 404: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '资源不存在', + examples: ['字典项不存在'], + }), + }), + ), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), +}; + +/** 获取字典项成功响应数据类型 */ +export type GetDictByIdSuccessType = Static<(typeof GetDictByIdResponsesSchema)[200]>; + +/** + * 字典树节点数据结构 + * @description 描述字典树中单个节点的数据结构,包含一个可选的children数组用于表示子节点 + */ +export const DictTreeNodeSchema = t.Recursive( + (self) => + t.Object({ + id: t.String({ + description: '字典项ID(bigint类型以字符串返回防止精度丢失)', + examples: ['1', '2', '100'], + }), + code: t.String({ + description: '字典代码', + examples: ['user_status'], + }), + name: t.String({ + description: '字典名称', + examples: ['用户状态'], + }), + value: t.Optional( + t.String({ + description: '字典值', + examples: ['active'], + }), + ), + description: t.Optional( + t.String({ + description: '字典描述', + examples: ['用户状态字典'], + }), + ), + icon: t.Optional( + t.String({ + description: '图标', + examples: ['icon-user'], + }), + ), + pid: t.String({ + description: '父级ID(bigint类型以字符串返回)', + examples: ['0', '1'], + }), + level: t.Number({ + description: '层级深度', + examples: [1, 2], + }), + sortOrder: t.Number({ + description: '排序号', + examples: [0, 1, 10], + }), + status: t.String({ + description: '状态', + examples: ['active', 'inactive'], + }), + isSystem: t.Boolean({ + description: '是否系统字典', + examples: [true, false], + }), + color: t.Optional( + t.String({ + description: '颜色标识', + examples: ['#1890ff'], + }), + ), + extra: t.Optional( + t.Record(t.String(), t.Any(), { + description: '扩展字段', + examples: [{ key1: 'value1' }], + }), + ), + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'], + }), + updatedAt: t.String({ + description: '更新时间', + examples: ['2024-12-19T10:30:00Z'], + }), + children: t.Optional(t.Array(self)), + }), + { + $id: 'DictTreeNode', + description: '字典树节点', + }, +); + +/** + * 获取完整字典树接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const GetDictTreeResponsesSchema = { + 200: responseWrapperSchema(t.Array(DictTreeNodeSchema)), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), +}; + +/** 获取完整字典树成功响应数据类型 */ +export type GetDictTreeSuccessType = Static<(typeof GetDictTreeResponsesSchema)[200]>; diff --git a/src/modules/dict/dict.service.ts b/src/modules/dict/dict.service.ts index 2e8ee3e..b9685ab 100644 --- a/src/modules/dict/dict.service.ts +++ b/src/modules/dict/dict.service.ts @@ -4,17 +4,17 @@ * @date 2024-12-19 * @lastEditor AI Assistant * @lastEditTime 2025-01-07 - * @description 字典模块的业务逻辑实现,仅实现创建字典项 + * @description 字典模块的业务逻辑实现,包括创建、查询和树结构生成 */ import { Logger } from '@/plugins/logger/logger.service'; import { db } from '@/plugins/drizzle/drizzle.service'; import { sysDict } from '@/eneities'; -import { eq, and, max } from 'drizzle-orm'; +import { eq, and, max, asc } from 'drizzle-orm'; import { successResponse, BusinessError } from '@/utils/responseFormate'; import { nextId } from '@/utils/snowflake'; -import type { CreateDictRequest } from './dict.schema'; -import type { CreateDictSuccessType } from './dict.response'; +import type { CreateDictRequest, GetDictTreeQueryRequest } from './dict.schema'; +import type { CreateDictSuccessType, GetDictByIdSuccessType, GetDictTreeSuccessType } from './dict.response'; /** * 字典服务类 @@ -121,6 +121,102 @@ export class DictService { '创建字典项成功', ); } + + /** + * 获取字典项内容 + * @param id 字典项ID + * @returns Promise + * @throws BusinessError 业务逻辑错误 + */ + public async getDictById(id: string): Promise { + Logger.info(`获取字典项内容:${id}`); + + const dictArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); + + if (!dictArr || dictArr.length === 0) { + Logger.warn(`字典项不存在:${id}`); + throw new BusinessError(`字典项不存在:${id}`, 404); + } + + const dict = dictArr[0]!; + + Logger.info(`获取字典项内容成功:${id}`); + + return successResponse({ + id: String(dict.id), + code: dict.code, + name: dict.name, + value: dict.value, + description: dict.description, + icon: dict.icon, + pid: String(dict.pid), + level: dict.level, + sortOrder: dict.sortOrder, + status: dict.status, + isSystem: Boolean(dict.isSystem), + color: dict.color, + extra: dict.extra, + createdAt: dict.createdAt, + updatedAt: dict.updatedAt, + }); + } + + /** + * 获取完整字典树 + * @param query 查询参数 + * @returns Promise + */ + public async getDictTree(query: GetDictTreeQueryRequest): Promise { + Logger.info(`获取完整字典树, 查询参数: ${JSON.stringify(query)}`); + + const conditions = []; + if (query.status && query.status !== 'all') { + conditions.push(eq(sysDict.status, query.status)); + } + if (query.isSystem && query.isSystem !== 'all') { + conditions.push(eq(sysDict.isSystem, query.isSystem === 'true' ? 1 : 0)); + } + + const dictList = await db().select().from(sysDict).where(and(...conditions)).orderBy(asc(sysDict.sortOrder)); + + if (!dictList || dictList.length === 0) { + return successResponse([], '获取完整字典树成功'); + } + + type DictNode = Omit & { + id: string; + pid: string; + isSystem: boolean; + children?: DictNode[]; + }; + + const nodeMap = new Map(); + const tree: DictNode[] = []; + + for (const item of dictList) { + const node: DictNode = { + ...item, + id: String(item.id), + pid: String(item.pid), + isSystem: Boolean(item.isSystem), + children: [], + }; + nodeMap.set(node.id, node); + } + + for (const node of nodeMap.values()) { + if (node.pid && node.pid !== '0' && nodeMap.has(node.pid)) { + const parent = nodeMap.get(node.pid); + if (parent && parent.children) { + parent.children.push(node); + } + } else { + tree.push(node); + } + } + + return successResponse(tree, '获取完整字典树成功'); + } } // 导出单例实例 diff --git a/src/modules/dict/dict.test.md b/src/modules/dict/dict.test.md new file mode 100644 index 0000000..4e50901 --- /dev/null +++ b/src/modules/dict/dict.test.md @@ -0,0 +1,156 @@ +# 字典模块测试用例文档 + +## 1. 创建字典项接口 (POST /api/dict) + +### 场景1: 成功创建顶级字典项 + +- **名称**: 成功创建一个顶级的、无父级的字典项 +- **前置条件**: 数据库中不存在 code 为 `test_root` 的字典项 +- **请求方法**: `POST` +- **请求路径**: `/api/dict` +- **请求体**: + ```json + { + "code": "test_root", + "name": "测试顶级字典", + "description": "这是一个用于测试的顶级字典项", + "status": "active" + } + ``` +- **预期响应 (200 OK)**: + - 响应体包含创建成功的字典项信息,`pid` 为 '0',`level` 为 1。 + - 数据库中存在该条记录。 + +### 场景2: 成功创建子级字典项 + +- **名称**: 成功创建一个子级的字典项 +- **前置条件**: + 1. 数据库中存在一个 code 为 `test_root` 的字典项,其 id 为 `100`。 + 2. 数据库中不存在 code 为 `test_child` 的字典项。 +- **请求方法**: `POST` +- **请求路径**: `/api/dict` +- **请求体**: + ```json + { + "code": "test_child", + "name": "测试子级字典", + "pid": "100", + "description": "这是一个用于测试的子级字典项", + "status": "active" + } + ``` +- **预期响应 (200 OK)**: + - 响应体包含创建成功的字典项信息,`pid` 为 '100',`level` 为父级 level + 1。 + - 数据库中存在该条记录。 + +### 场景3: 失败 - code冲突 + +- **名称**: 因 code 重复导致创建失败 +- **前置条件**: 数据库中已存在 code 为 `test_root` 的字典项。 +- **请求方法**: `POST` +- **请求路径**: `/api/dict` +- **请求体**: + ```json + { + "code": "test_root", + "name": "重复Code测试" + } + ``` +- **预期响应 (409 Conflict)**: + - 响应体包含错误信息,提示 "字典代码已存在"。 + +### 场景4: 失败 - 父级不存在 + +- **名称**: 因父级 ID 不存在导致创建失败 +- **前置条件**: - +- **请求方法**: `POST` +- **请求路径**: `/api/dict` +- **请求体**: + ```json + { + "code": "test_no_parent", + "name": "无效父级测试", + "pid": "99999" + } + ``` +- **预期响应 (404 Not Found)**: + - 响应体包含错误信息,提示 "父级字典不存在"。 + + +## 2. 获取字典项内容接口 (GET /api/dict/:id) + +### 场景1: 成功获取存在的字典项 + +- **名称**: 根据存在的 ID 成功获取字典项 +- **前置条件**: 数据库中存在一个 id 为 `100` 的字典项。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/100` +- **预期响应 (200 OK)**: + - 响应体包含 id 为 `100` 的字典项的完整信息。 + +### 场景2: 失败 - 字典项不存在 + +- **名称**: 因 ID 不存在导致获取失败 +- **前置条件**: 数据库中不存在 id 为 `99999` 的字典项。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/99999` +- **预期响应 (404 Not Found)**: + - 响应体包含错误信息,提示 "字典项不存在"。 + +### 场景3: 失败 - ID格式错误 + +- **名称**: 因 ID 格式无效(非数字)导致获取失败 +- **前置条件**: - +- **请求方法**: `GET` +- **请求路径**: `/api/dict/abc` +- **预期响应 (400 Bad Request / 422 Unprocessable Entity)**: + - 响应体包含参数验证错误信息。 + +## 3. 获取完整字典树接口 (GET /api/dict/tree) + +### 场景1: 成功获取所有字典 + +- **名称**: 成功获取完整的字典树结构 +- **前置条件**: 数据库中存在多个字典项,可以构成树形结构。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/tree` +- **预期响应 (200 OK)**: + - 响应体 `data` 是一个数组,每个元素是一个顶层字典节点。 + - 每个节点包含 `children` 数组,递归地包含其子节点。 + - 节点按 `sortOrder` 排序。 + +### 场景2: 根据状态过滤 (active) + +- **名称**: 获取所有状态为 'active' 的字典树 +- **前置条件**: 数据库中同时存在 'active' 和 'inactive' 状态的字典项。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/tree?status=active` +- **预期响应 (200 OK)**: + - 响应的树结构中只包含 `status` 为 'active' 的节点。 + +### 场景3: 根据系统字典过滤 (true) + +- **名称**: 获取所有系统字典的字典树 +- **前置条件**: 数据库中同时存在系统字典 (`is_system`=true) 和非系统字典 (`is_system`=false)。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/tree?is_system=true` +- **预期响应 (200 OK)**: + - 响应的树结构中只包含 `is_system` 为 `true` 的节点。 + +### 场景4: 组合过滤 + +- **名称**: 获取所有状态为 'active' 且非系统字典的字典树 +- **前置条件**: 数据库中存在满足各种组合条件的字典项。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/tree?status=active&is_system=false` +- **预期响应 (200 OK)**: + - 响应的树结构中只包含 `status` 为 'active' 且 `is_system` 为 `false` 的节点。 + +### 场景5: 数据库为空 + +- **名称**: 在没有字典数据的情况下获取字典树 +- **前置条件**: 数据库 `sys_dict` 表为空。 +- **请求方法**: `GET` +- **请求路径**: `/api/dict/tree` +- **预期响应 (200 OK)**: + - 响应体 `data` 是一个空数组 `[]`。 \ No newline at end of file diff --git a/src/plugins/errorHandle/errorHandler.plugins.ts b/src/plugins/errorHandle/errorHandler.plugins.ts index a3317c0..5c3ed78 100644 --- a/src/plugins/errorHandle/errorHandler.plugins.ts +++ b/src/plugins/errorHandle/errorHandler.plugins.ts @@ -75,6 +75,14 @@ export const errorHandlerPlugin = (app: Elysia) => errors: error.message || error.response.message || error.response, }; } + case 404: { + set.status = code; + return { + code: error.code, + message: '资源未找到', + errors: error.message || error.response.message || error.response, + }; + } case 408: { set.status = code; return { diff --git a/tasks/字典模块开发计划.md b/tasks/字典模块开发计划.md index 1f0b2ac..539e53c 100644 --- a/tasks/字典模块开发计划.md +++ b/tasks/字典模块开发计划.md @@ -101,29 +101,29 @@ CREATE TABLE `sys_dict` ( ### 阶段2:字典模块核心接口开发 -- [ ] 2.0 创建字典项接口 (POST /api/dict) - - [ ] 2.1 生成接口业务逻辑文档,写入 `dict.docs.md` - - [ ] 2.2 创建 `dict.schema.ts` - 定义创建字典项Schema - - [ ] 2.3 创建 `dict.response.ts` - 定义创建字典项响应格式 - - [ ] 2.4 创建 `dict.service.ts` - 实现创建字典项业务逻辑 - - [ ] 2.5 创建 `dict.controller.ts` - 实现创建字典项路由 - - [ ] 2.6 创建 `dict.test.md` - 编写创建字典项测试用例 +- [x] 2.0 创建字典项接口 (POST /api/dict) + - [x] 2.1 生成接口业务逻辑文档,写入 `dict.docs.md` + - [x] 2.2 创建 `dict.schema.ts` - 定义创建字典项Schema + - [x] 2.3 创建 `dict.response.ts` - 定义创建字典项响应格式 + - [x] 2.4 创建 `dict.service.ts` - 实现创建字典项业务逻辑 + - [x] 2.5 创建 `dict.controller.ts` - 实现创建字典项路由 + - [x] 2.6 创建 `dict.test.md` - 编写创建字典项测试用例 -- [ ] 3.0 获取字典项内容接口 (GET /api/dict/:id) - - [ ] 3.1 更新 `dict.docs.md` - 添加获取字典项业务逻辑 - - [ ] 3.2 扩展 `dict.schema.ts` - 定义获取字典项Schema - - [ ] 3.3 扩展 `dict.response.ts` - 定义获取字典项响应格式 - - [ ] 3.4 扩展 `dict.service.ts` - 实现获取字典项业务逻辑 - - [ ] 3.5 扩展 `dict.controller.ts` - 实现获取字典项路由 - - [ ] 3.6 更新 `dict.test.md` - 添加获取字典项测试用例 +- [x] 3.0 获取字典项内容接口 (GET /api/dict/:id) + - [x] 3.1 更新 `dict.docs.md` - 添加获取字典项业务逻辑 + - [x] 3.2 扩展 `dict.schema.ts` - 定义获取字典项Schema + - [x] 3.3 扩展 `dict.response.ts` - 定义获取字典项响应格式 + - [x] 3.4 扩展 `dict.service.ts` - 实现获取字典项业务逻辑 + - [x] 3.5 扩展 `dict.controller.ts` - 实现获取字典项路由 + - [x] 3.6 更新 `dict.test.md` - 添加获取字典项测试用例 -- [ ] 4.0 获取完整字典树接口 (GET /api/dict/tree) - - [ ] 4.1 更新 `dict.docs.md` - 添加获取完整字典树业务逻辑 - - [ ] 4.2 扩展 `dict.schema.ts` - 定义获取字典树查询Schema - - [ ] 4.3 扩展 `dict.response.ts` - 定义字典树响应格式 - - [ ] 4.4 扩展 `dict.service.ts` - 实现获取完整字典树业务逻辑 - - [ ] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由 - - [ ] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例 +- [x] 4.0 获取完整字典树接口 (GET /api/dict/tree) + - [x] 4.1 更新 `dict.docs.md` - 添加获取完整字典树业务逻辑 + - [x] 4.2 扩展 `dict.schema.ts` - 定义获取字典树查询Schema + - [x] 4.3 扩展 `dict.response.ts` - 定义字典树响应格式 + - [x] 4.4 扩展 `dict.service.ts` - 实现获取完整字典树业务逻辑 + - [x] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由 + - [x] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例 - [ ] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code) - [ ] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑