diff --git a/src/modules/dict/dict.controller.ts b/src/modules/dict/dict.controller.ts index 7701a70..ffd3921 100644 --- a/src/modules/dict/dict.controller.ts +++ b/src/modules/dict/dict.controller.ts @@ -15,12 +15,15 @@ import { GetDictTreeByCodeParamsSchema, GetDictTreeByCodeQuerySchema, GetDictTreeQuerySchema, + UpdateDictBodySchema, + UpdateDictParamsSchema, } from './dict.schema'; import { CreateDictResponsesSchema, GetDictByIdResponsesSchema, GetDictTreeByCodeResponsesSchema, GetDictTreeResponsesSchema, + UpdateDictResponsesSchema, } from './dict.response'; import { tags } from '@/constants/swaggerTags'; @@ -89,4 +92,20 @@ export const dictController = new Elysia() operationId: 'getDictTreeByCode', }, response: GetDictTreeByCodeResponsesSchema, + }) + /** + * 更新字典项内容接口 + * @route PUT /api/dict/:id + * @description 根据ID更新单个字典项的内容,所有字段均为可选 + */ + .put('/:id', ({ params, body }) => dictService.updateDict(params.id, body), { + params: UpdateDictParamsSchema, + body: UpdateDictBodySchema, + detail: { + summary: '更新字典项内容', + description: '根据ID更新单个字典项的内容,所有字段均为可选。', + tags: [tags.dict], + operationId: 'updateDict', + }, + response: UpdateDictResponsesSchema, }); diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts index 055a13e..866bf2f 100644 --- a/src/modules/dict/dict.response.ts +++ b/src/modules/dict/dict.response.ts @@ -284,3 +284,44 @@ export const GetDictTreeByCodeResponsesSchema = { /** 获取指定字典树成功响应数据类型 */ export type GetDictTreeByCodeSuccessType = Static<(typeof GetDictTreeByCodeResponsesSchema)[200]>; + +/** + * 更新字典项成功响应数据结构 + * @description 更新字典项的成功响应数据结构,与获取成功响应结构相同 + */ +export const UpdateDictSuccessSchema = GetDictByIdSuccessSchema; + +/** + * 更新字典项接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const UpdateDictResponsesSchema = { + 200: responseWrapperSchema(UpdateDictSuccessSchema), + 404: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '资源不存在', + examples: ['字典项不存在'], + }), + }), + ), + 409: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '唯一性冲突', + examples: ['字典代码已存在', '字典名称在同级下已存在'], + }), + }), + ), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), +}; + +/** 更新字典项成功响应数据类型 */ +export type UpdateDictSuccessType = Static<(typeof UpdateDictResponsesSchema)[200]>; diff --git a/src/modules/dict/dict.schema.ts b/src/modules/dict/dict.schema.ts index c6e8c2f..a3fa7f8 100644 --- a/src/modules/dict/dict.schema.ts +++ b/src/modules/dict/dict.schema.ts @@ -228,10 +228,21 @@ export const GetDictTreeByCodeSchema = t.Object({ export type GetDictTreeByCodeRequest = Static; /** - * 更新字典项请求参数Schema + * 更新字典项路径参数Schema + */ +export const UpdateDictParamsSchema = t.Object({ + id: t.String({ + pattern: '^[1-9]\\d*$', + description: '字典项ID,必须是正整数', + examples: ['1', '2', '100'], + }), +}); + +/** + * 更新字典项Body参数Schema * @description 更新字典项的请求参数验证规则,所有字段都是可选的 */ -export const UpdateDictSchema = t.Object({ +export const UpdateDictBodySchema = t.Object({ /** 字典代码,唯一标识 */ code: t.Optional( t.String({ @@ -330,8 +341,8 @@ export const UpdateDictSchema = t.Object({ ), }); -/** 更新字典项请求参数类型 */ -export type UpdateDictRequest = Static; +/** 更新字典项Body参数类型 */ +export type UpdateDictBodyRequest = Static; /** * 字典项排序请求参数Schema diff --git a/src/modules/dict/dict.service.ts b/src/modules/dict/dict.service.ts index 7d1de49..5bc061c 100644 --- a/src/modules/dict/dict.service.ts +++ b/src/modules/dict/dict.service.ts @@ -10,15 +10,21 @@ import { Logger } from '@/plugins/logger/logger.service'; import { db } from '@/plugins/drizzle/drizzle.service'; import { sysDict } from '@/eneities'; -import { eq, and, max, asc, sql } from 'drizzle-orm'; +import { eq, and, max, asc, sql, ne } from 'drizzle-orm'; import { successResponse, BusinessError } from '@/utils/responseFormate'; import { nextId } from '@/utils/snowflake'; -import type { CreateDictRequest, GetDictTreeByCodeRequest, GetDictTreeQueryRequest } from './dict.schema'; +import type { + CreateDictRequest, + GetDictTreeByCodeRequest, + GetDictTreeQueryRequest, + UpdateDictBodyRequest, +} from './dict.schema'; import type { CreateDictSuccessType, GetDictByIdSuccessType, GetDictTreeByCodeSuccessType, GetDictTreeSuccessType, + UpdateDictSuccessType, } from './dict.response'; /** @@ -295,6 +301,73 @@ export class DictService { return successResponse(finalTree ? [finalTree] : [], '获取指定字典树成功'); } + + /** + * 更新字典项 + * @param id 字典项ID + * @param body 更新内容 + * @returns Promise + */ + public async updateDict(id: string, body: UpdateDictBodyRequest): Promise { + Logger.info(`更新字典项: ${id}, body: ${JSON.stringify(body)}`); + + // 1. 检查字典项是否存在 + const existingArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); + if (existingArr.length === 0) { + throw new BusinessError(`字典项不存在: ${id}`, 404); + } + const existing = existingArr[0]!; + + // 2. 唯一性校验 + if (body.code) { + const existCode = await db().select({id: sysDict.id}).from(sysDict).where(and(eq(sysDict.code, body.code), ne(sysDict.id, id))).limit(1); + if (existCode.length > 0) { + throw new BusinessError(`字典代码已存在: ${body.code}`, 409); + } + } + if (body.name) { + const pid = body.pid ? String(body.pid) : (existing.pid ? String(existing.pid) : '0'); + const existName = await db().select({id: sysDict.id}).from(sysDict).where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid), ne(sysDict.id, id))).limit(1); + if (existName.length > 0) { + throw new BusinessError(`同级下字典名称已存在: ${body.name}`, 409); + } + } + + // 3. 构建更新数据 + const updateData: Partial = {}; + for (const key in body) { + if (Object.prototype.hasOwnProperty.call(body, key) && body[key as keyof typeof body] !== undefined) { + if (key === 'isSystem') { + (updateData as any)[key] = body[key] ? 1 : 0; + } else { + (updateData as any)[key] = body[key as keyof typeof body]; + } + } + } + + if (Object.keys(updateData).length === 0) { + return successResponse({ + ...existing, + id: String(existing.id), + pid: String(existing.pid), + isSystem: Boolean(existing.isSystem), + }, '未提供任何更新内容'); + } + + // 4. 执行更新 + await db().update(sysDict).set(updateData).where(eq(sysDict.id, id)); + + // 5. 查询并返回更新后的数据 + const updatedArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); + const updated = updatedArr[0]!; + + return successResponse({ + ...updated, + id: String(updated.id), + pid: String(updated.pid), + isSystem: Boolean(updated.isSystem), + }, '更新字典项成功'); + } } // 导出单例实例 diff --git a/src/modules/dict/dict.test.md b/src/modules/dict/dict.test.md index 4f3c39a..bb893d9 100644 --- a/src/modules/dict/dict.test.md +++ b/src/modules/dict/dict.test.md @@ -183,4 +183,72 @@ - **请求方法**: `GET` - **请求路径**: `/api/dict/tree/user_gender?status=active` - **预期响应 (200 OK)**: - - 返回的 `user_gender` 子树中,只包含状态为 `active` 的节点。 \ No newline at end of file + - 返回的 `user_gender` 子树中,只包含状态为 `active` 的节点。 + +## 5. 更新字典项内容接口 (PUT /api/dict/:id) + +### 场景1: 成功更新部分字段 + +- **名称**: 成功更新一个字典项的名称和描述 +- **前置条件**: 数据库中存在一个 id 为 `101` 的字典项。 +- **请求方法**: `PUT` +- **请求路径**: `/api/dict/101` +- **请求体**: + ```json + { + "name": "更新后的字典名称", + "description": "这是更新后的描述信息。" + } + ``` +- **预期响应 (200 OK)**: + - 响应体包含更新后的完整字典项信息。 + - 数据库中对应记录的 `name` 和 `description` 字段已更新。 + +### 场景2: 失败 - ID 不存在 + +- **名称**: 因 ID 不存在导致更新失败 +- **前置条件**: 数据库中不存在 id 为 `99999` 的字典项。 +- **请求方法**: `PUT` +- **请求路径**: `/api/dict/99999` +- **请求体**: + ```json + { + "name": "任意名称" + } + ``` +- **预期响应 (404 Not Found)**: + - 响应体包含错误信息,提示 "字典项不存在"。 + +### 场景3: 失败 - code 冲突 + +- **名称**: 更新时提供的 code 与其他字典项冲突 +- **前置条件**: + 1. 数据库中存在 id 为 `101` 的字典项 (code: `dict_a`)。 + 2. 数据库中存在另一个 id 为 `102` 的字典项 (code: `dict_b`)。 +- **请求方法**: `PUT` +- **请求路径**: `/api/dict/101` +- **请求体**: + ```json + { + "code": "dict_b" + } + ``` +- **预期响应 (409 Conflict)**: + - 响应体包含错误信息,提示 "字典代码已存在"。 + +### 场景4: 失败 - name 同级冲突 + +- **名称**: 更新时提供的 name 与同级其他字典项冲突 +- **前置条件**: + 1. 数据库中存在一个父id为 `100` 的字典项 (id: `101`, name: `name_a`)。 + 2. 数据库中存在另一个父id也为 `100` 的字典项 (id: `102`, name: `name_b`)。 +- **请求方法**: `PUT` +- **请求路径**: `/api/dict/101` +- **请求体**: + ```json + { + "name": "name_b" + } + ``` +- **预期响应 (409 Conflict)**: + - 响应体包含错误信息,提示 "同级下字典名称已存在"。 \ No newline at end of file diff --git a/tasks/字典模块开发计划.md b/tasks/字典模块开发计划.md index 539e53c..d7ae5b6 100644 --- a/tasks/字典模块开发计划.md +++ b/tasks/字典模块开发计划.md @@ -125,23 +125,23 @@ CREATE TABLE `sys_dict` ( - [x] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由 - [x] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例 -- [ ] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code) - - [ ] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑 - - [ ] 5.2 扩展 `dict.schema.ts` - 定义获取指定字典树Schema - - [ ] 5.3 扩展 `dict.response.ts` - 定义指定字典树响应格式 - - [ ] 5.4 扩展 `dict.service.ts` - 实现获取指定字典树业务逻辑 - - [ ] 5.5 扩展 `dict.controller.ts` - 实现获取指定字典树路由 - - [ ] 5.6 更新 `dict.test.md` - 添加获取指定字典树测试用例 +- [x] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code) + - [x] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑 + - [x] 5.2 扩展 `dict.schema.ts` - 定义获取指定字典树Schema + - [x] 5.3 扩展 `dict.response.ts` - 定义指定字典树响应格式 + - [x] 5.4 扩展 `dict.service.ts` - 实现获取指定字典树业务逻辑 + - [x] 5.5 扩展 `dict.controller.ts` - 实现获取指定字典树路由 + - [x] 5.6 更新 `dict.test.md` - 添加获取指定字典树测试用例 ### 阶段3:字典管理接口开发 -- [ ] 6.0 更新字典项内容接口 (PUT /api/dict/:id) - - [ ] 6.1 更新 `dict.docs.md` - 添加更新字典项业务逻辑 - - [ ] 6.2 扩展 `dict.schema.ts` - 定义更新字典项Schema - - [ ] 6.3 扩展 `dict.response.ts` - 定义更新字典项响应格式 - - [ ] 6.4 扩展 `dict.service.ts` - 实现更新字典项业务逻辑 - - [ ] 6.5 扩展 `dict.controller.ts` - 实现更新字典项路由 - - [ ] 6.6 更新 `dict.test.md` - 添加更新字典项测试用例 +- [x] 6.0 更新字典项内容接口 (PUT /api/dict/:id) + - [x] 6.1 更新 `dict.docs.md` - 添加更新字典项业务逻辑 + - [x] 6.2 扩展 `dict.schema.ts` - 定义更新字典项Schema + - [x] 6.3 扩展 `dict.response.ts` - 定义更新字典项响应格式 + - [x] 6.4 扩展 `dict.service.ts` - 实现更新字典项业务逻辑 + - [x] 6.5 扩展 `dict.controller.ts` - 实现更新字典项路由 + - [x] 6.6 更新 `dict.test.md` - 添加更新字典项测试用例 - [ ] 7.0 字典项排序接口 (PUT /api/dict/sort) - [ ] 7.1 更新 `dict.docs.md` - 添加字典项排序业务逻辑 @@ -149,7 +149,6 @@ CREATE TABLE `sys_dict` ( - [ ] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式 - [ ] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑 - [ ] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由 - - [ ] 7.6 更新 `dict.test.md` - 添加字典项排序测试用例 - [ ] 8.0 删除字典项接口 (DELETE /api/dict/:id) - [ ] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除) @@ -157,7 +156,6 @@ CREATE TABLE `sys_dict` ( - [ ] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式 - [ ] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑 - [ ] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由 - - [ ] 8.6 更新 `dict.test.md` - 添加删除字典项测试用例 ### 阶段4:缓存机制和优化