diff --git a/src/modules/dict/dict.controller.ts b/src/modules/dict/dict.controller.ts index b25cac7..db8deb8 100644 --- a/src/modules/dict/dict.controller.ts +++ b/src/modules/dict/dict.controller.ts @@ -7,7 +7,7 @@ * @description 字典模块的路由控制器,仅实现创建字典项接口 */ -import { Elysia } from 'elysia'; +import { Elysia, t } from 'elysia'; import { dictService } from './dict.service'; import { CreateDictSchema, @@ -18,6 +18,7 @@ import { UpdateDictBodySchema, UpdateDictParamsSchema, SortDictSchema, + DeleteDictSchema, } from './dict.schema'; import { CreateDictResponsesSchema, @@ -26,6 +27,7 @@ import { GetDictTreeResponsesSchema, UpdateDictResponsesSchema, SortDictResponsesSchema, + DeleteDictResponsesSchema, } from './dict.response'; import { tags } from '@/constants/swaggerTags'; @@ -125,4 +127,28 @@ export const dictController = new Elysia() operationId: 'sortDictItems', }, response: SortDictResponsesSchema, - }); + }) + /** + * 删除字典项接口 + * @route DELETE /api/dict/:id + * @description 软删除指定ID的字典项,可通过cascade参数级联删除子项 + */ + .delete( + '/:id', + ({ params, query }) => + dictService.deleteDict({ + id: params.id, + cascade: query.cascade, + }), + { + params: t.Object({ id: t.String() }), + query: t.Object({ cascade: t.Optional(t.Boolean()) }), + detail: { + summary: '删除字典项', + description: '软删除指定ID的字典项。如果字典项有子项,必须使用 `?cascade=true` 进行级联删除。', + tags: [tags.dict], + operationId: 'deleteDictItem', + }, + response: DeleteDictResponsesSchema, + }, + ); diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts index b9d7ed1..bb530b7 100644 --- a/src/modules/dict/dict.response.ts +++ b/src/modules/dict/dict.response.ts @@ -367,3 +367,53 @@ export const SortDictResponsesSchema = { /** 字典项排序成功响应数据类型 */ export type SortDictSuccessType = Static<(typeof SortDictResponsesSchema)[200]>; + +/** + * 删除字典项接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const DeleteDictResponsesSchema = { + 200: responseWrapperSchema(t.Null(), { + description: '删除成功', + examples: { + code: 0, + message: '字典项删除成功', + data: null, + }, + }), + 400: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '参数错误或业务规则冲突', + examples: ['存在子字典项,无法删除,请先删除子项或使用级联删除'], + }), + }), + ), + 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 DeleteDictSuccessType = Static<(typeof DeleteDictResponsesSchema)[200]>; diff --git a/src/modules/dict/dict.service.ts b/src/modules/dict/dict.service.ts index e2abb17..b7f38d0 100644 --- a/src/modules/dict/dict.service.ts +++ b/src/modules/dict/dict.service.ts @@ -19,6 +19,7 @@ import type { GetDictTreeQueryRequest, UpdateDictBodyRequest, SortDictRequest, + DeleteDictRequest, } from './dict.schema'; import type { CreateDictSuccessType, @@ -27,6 +28,7 @@ import type { GetDictTreeSuccessType, UpdateDictSuccessType, SortDictSuccessType, + DeleteDictSuccessType, } from './dict.response'; /** @@ -490,6 +492,105 @@ export class DictService { Logger.info('字典项排序成功'); return successResponse(null, '字典项排序成功'); } + + /** + * 删除字典项(软删除) + * @param params DeleteDictRequest 删除请求参数 { id, cascade } + * @returns Promise + * @throws BusinessError 业务逻辑错误 + * @description 软删除字典项,可选择级联删除子项 + */ + public async deleteDict({ + id, + cascade, + }: DeleteDictRequest): Promise { + const dictId = BigInt(id); + Logger.info(`请求删除字典项: ${dictId}, 是否级联: ${!!cascade}`); + + try { + await db().transaction(async (tx) => { + // 1. 查找字典项 + const dictResult: any[] = await tx.execute( + sql`SELECT * FROM sys_dict WHERE id = ${dictId} LIMIT 1`, + ); + const dictToDelete = dictResult[0] ? dictResult[0][0] : null; + + if (!dictToDelete) { + Logger.warn(`删除失败,字典项不存在: ${dictId}`); + throw new BusinessError('字典项不存在', 404); + } + + // 2. 系统字典不允许删除 + if (dictToDelete.is_system) { + Logger.warn(`删除失败,系统字典不允许删除: ${dictId}`); + throw new BusinessError('系统字典不允许删除', 409); + } + + // 3. 查找所有子孙节点 + const childrenIds: bigint[] = []; + if (cascade) { + const allDicts = await tx.query.sysDict.findMany({ + columns: { id: true, pid: true }, + }); + const allDictsBigInt = allDicts.map((d) => ({ + id: BigInt(d.id), + pid: d.pid ? BigInt(d.pid) : null, + })); + this.findAllChildrenIds(dictId, allDictsBigInt, childrenIds); + } else { + // 如果不级联,检查是否有直接子节点 + const childResult: any[] = await tx.execute( + sql`SELECT id FROM sys_dict WHERE pid = ${dictId} LIMIT 1`, + ); + if (childResult[0] && childResult[0].length > 0) { + Logger.warn(`删除失败,存在子字典项,且未启用级联删除: ${dictId}`); + throw new BusinessError( + '存在子字典项,无法删除,请先删除子项或使用级联删除', + 400, + ); + } + } + + const idsToDelete = [dictId, ...childrenIds]; + + Logger.info(`准备删除以下字典项ID: ${idsToDelete.join(', ')}`); + + // 4. 使用原生SQL执行软删除 + if (idsToDelete.length > 0) { + await tx.execute( + sql`UPDATE sys_dict SET status = 'inactive' WHERE id IN ${idsToDelete}`, + ); + } + }); + } catch (error) { + if (error instanceof BusinessError) { + throw error; + } + Logger.error('字典项删除失败', { error }); + throw new BusinessError('字典项删除失败,请稍后重试', 500); + } + + Logger.info(`字典项 ${dictId} 及关联子项(如有)已成功软删除`); + return successResponse(null, '字典项删除成功'); + } + + /** + * 递归查找所有子孙节点的ID + * @param parentId 父级ID + * @param allItems 所有字典项列表 + * @param childrenIds 存储子孙节点ID的数组 + */ + private findAllChildrenIds( + parentId: bigint, + allItems: { id: bigint; pid: bigint | null }[], + childrenIds: bigint[], + ) { + const children = allItems.filter((item) => item.pid === parentId); + for (const child of children) { + childrenIds.push(child.id); + this.findAllChildrenIds(child.id, allItems, childrenIds); + } + } } // 导出单例实例 diff --git a/src/modules/dict/test.md b/src/modules/dict/test.md index 88df529..1b6b29f 100644 --- a/src/modules/dict/test.md +++ b/src/modules/dict/test.md @@ -112,4 +112,58 @@ } ``` - **预期响应 (400 Bad Request)**: - - 响应体包含错误信息,提示 "排序项列表不能为空"。 \ No newline at end of file + - 响应体包含错误信息,提示 "排序项列表不能为空"。 +## 7. 删除字典项接口 (DELETE /api/dict/:id) + +### 场景1: 成功 - 删除无子项的节点 + +- **名称**: 成功软删除一个没有子节点的字典项 +- **前置条件**: + - 字典项A (id: 401, pid: 400) 存在,且没有子节点。 + - 字典项A的`is_system`为`false`。 +- **请求方法**: `DELETE` +- **请求路径**: `/api/dict/401` +- **预期响应 (200 OK)**: + - 响应体 data 为 `null`,message 为 "字典项删除成功"。 + - 数据库中,字典项A 的 `status` 更新为 `inactive`。 + +### 场景2: 成功 - 级联删除有子项的节点 + +- **名称**: 成功使用级联方式软删除一个有子节点的字典项 +- **前置条件**: + - 字典项A (id: 401, pid: 400) 存在。 + - 字典项B (id: 402, pid: 401) 是A的子节点。 + - 字典项C (id: 403, pid: 402) 是B的子节点。 +- **请求方法**: `DELETE` +- **请求路径**: `/api/dict/401?cascade=true` +- **预期响应 (200 OK)**: + - 数据库中,字典项A, B, C 的 `status` 都更新为 `inactive`。 + +### 场景3: 失败 - 删除有子项的节点(不使用级联) + +- **名称**: 在不提供`cascade`参数的情况下,尝试删除一个有子节点的字典项而失败 +- **前置条件**: + - 字典项A (id: 401, pid: 400) 存在。 + - 字典项B (id: 402, pid: 401) 是A的子节点。 +- **请求方法**: `DELETE` +- **请求路径**: `/api/dict/401` +- **预期响应 (400 Bad Request)**: + - 响应体包含错误信息,提示 "存在子字典项,无法删除..."。 + +### 场景4: 失败 - 删除系统字典 + +- **名称**: 尝试删除一个被标记为系统字典的项而失败 +- **前置条件**: 字典项A (id: 500) 存在,且其 `is_system` 字段为 `true`。 +- **请求方法**: `DELETE` +- **请求路径**: `/api/dict/500` +- **预期响应 (409 Conflict)**: + - 响应体包含错误信息,提示 "系统字典不允许删除"。 + +### 场景5: 失败 - 字典项ID不存在 + +- **名称**: 尝试删除一个不存在的字典项ID +- **前置条件**: 数据库中不存在 id 为 `9999` 的字典项。 +- **请求方法**: `DELETE` +- **请求路径**: `/api/dict/9999` +- **预期响应 (404 Not Found)**: + - 响应体包含错误信息,提示 "字典项不存在"。 \ No newline at end of file diff --git a/tasks/字典模块开发计划.md b/tasks/字典模块开发计划.md index 4648c20..f988bee 100644 --- a/tasks/字典模块开发计划.md +++ b/tasks/字典模块开发计划.md @@ -151,12 +151,13 @@ CREATE TABLE `sys_dict` ( - [x] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由 - [x] 7.6 在 `dict.test.md` 中添加排序接口测试用例 -- [ ] 8.0 删除字典项接口 (DELETE /api/dict/:id) - - [ ] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除) - - [ ] 8.2 扩展 `dict.schema.ts` - 定义删除字典项Schema - - [ ] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式 - - [ ] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑 - - [ ] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由 +- [x] 8.0 删除字典项接口 (DELETE /api/dict/:id) + - [x] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除) + - [x] 8.2 扩展 `dict.schema.ts` - 定义删除字典项Schema + - [x] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式 + - [x] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑 + - [x] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由 + - [x] 8.6 在 `dict.test.md` 中添加删除接口测试用例 ### 阶段4:缓存机制和优化