Compare commits

...

6 Commits

Author SHA1 Message Date
e8e352b6b6 feat(dict) 分布式锁 2025-07-07 22:34:45 +08:00
cd49a78678 feat(dict): 新增软删除字典项接口
- 实现了 DELETE /api/dict/:id 接口,用于软删除字典项。
- 支持 `cascade=true` 查询参数,用于级联软删除所有子孙节点。
- 添加了 DeleteDictSchema 用于请求验证。
- 添加了 DeleteDictResponsesSchema 用于 API 文档。
- 服务层实现包含对系统字典的保护、以及对有子节点的非级联删除的防护。
- 所有数据库查询和更新均使用原生 SQL 以规避 ORM 类型问题。
- 在控制器中添加了新路由。
- 在 dict.test.md 中为删除接口添加了全面的测试用例。
2025-07-07 21:59:52 +08:00
5580941ffb feat(dict): 新增字典项排序接口
- 实现了 PUT /api/dict/sort 接口,用于批量排序和移动字典项。
- 添加了 SortDictSchema 用于请求验证。
- 添加了 SortDictResponsesSchema 用于 API 文档。
- 使用原生 SQL 和 CASE 语句实现了 sortDict 服务层方法,进行高效的批量更新,解决了之前遇到的 ORM 类型问题。
- 在字典模块的控制器中添加了新路由。
- 在 dict.test.md 中为排序接口添加了全面的测试用例。
2025-07-07 21:56:18 +08:00
10ee246b7d feat(dict): 添加更新字典项内容接口
- **新增接口**
  - 添加 `PUT /api/dict/:id` 接口,用于更新指定ID的字典项。
  - 支持部分更新,只修改传入的字段。

- **Service层**
  - 在 `DictService` 中实现 `updateDict` 方法。
  - 包含了对字典项存在性的检查,以及对 `code` 和 `name` 唯一性冲突的校验。
  - 修复了若干 linter 错误。

- **Schema和Response**
  - 在 `dict.schema.ts` 中将 `UpdateDictSchema` 拆分为 `Params` 和 `Body` 两部分,以适应 Elysia 的路由定义。
  - 在 `dict.response.ts` 中添加了 `UpdateDictResponsesSchema`,覆盖了成功、未找到和冲突等场景。

- **文档**
  - 在 `dict.test.md` 中为新接口添加了详细的测试用例。
2025-07-07 21:36:51 +08:00
09a5dc30f1 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` 中为新接口添加了详细的测试用例。
2025-07-07 21:31:46 +08:00
b11dfa522b feat(dict): 添加获取完整字典树接口
- **新增接口**
  - 添加 `GET /api/dict/tree` 接口,用于获取完整的字典树结构。
  - 支持通过 `status` 和 `is_system` 查询参数进行过滤。

- **Service层**
  - 在 `DictService` 中实现 `getDictTree` 方法,包含从数据库查询数据并将列表转换为树形结构的逻辑。
  - 优化了日志记录,修复了linter错误。

- **Schema和Response**
  - 在 `dict.schema.ts` 中定义了 `GetDictTreeQuerySchema` 用于验证查询参数。
  - 在 `dict.response.ts` 中使用 `t.Recursive` 定义了递归的 `DictTreeNodeSchema` 来描述树形响应结构。

- **文档**
  - 更新了 `dict.docs.md`,添加了获取完整字典树的业务逻辑描述。
  - 在 `dict.test.md` 中为新接口添加了详细的测试用例。
2025-07-07 21:25:27 +08:00
8 changed files with 1569 additions and 196 deletions

View File

@ -7,10 +7,28 @@
* @description
*/
import { Elysia } from 'elysia';
import { Elysia, t } from 'elysia';
import { dictService } from './dict.service';
import { CreateDictSchema } from './dict.schema';
import { CreateDictResponsesSchema } from './dict.response';
import {
CreateDictSchema,
GetDictByIdSchema,
GetDictTreeByCodeParamsSchema,
GetDictTreeByCodeQuerySchema,
GetDictTreeQuerySchema,
UpdateDictBodySchema,
UpdateDictParamsSchema,
SortDictSchema,
DeleteDictSchema,
} from './dict.schema';
import {
CreateDictResponsesSchema,
GetDictByIdResponsesSchema,
GetDictTreeByCodeResponsesSchema,
GetDictTreeResponsesSchema,
UpdateDictResponsesSchema,
SortDictResponsesSchema,
DeleteDictResponsesSchema,
} from './dict.response';
import { tags } from '@/constants/swaggerTags';
/**
@ -32,4 +50,105 @@ 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,
})
/**
*
* @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,
})
/**
*
* @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: 'updateDictItem',
},
response: UpdateDictResponsesSchema,
})
/**
*
* @route PUT /api/dict/sort
* @description
*/
.put('/sort', ({ body }) => dictService.sortDict(body), {
body: SortDictSchema,
detail: {
summary: '字典项排序',
description: '对多个字典项进行批量排序或移动层级。',
tags: [tags.dict],
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,
},
);

View File

@ -128,3 +128,292 @@ 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: '字典项IDbigint类型以字符串返回防止精度丢失',
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: '父级IDbigint类型以字符串返回',
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]>;
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const GetDictTreeByCodeResponsesSchema = {
...GetDictTreeResponsesSchema,
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '资源不存在',
examples: ['字典代码不存在'],
}),
}),
),
};
/** 获取指定字典树成功响应数据类型 */
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]>;
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const SortDictResponsesSchema = {
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: ['部分字典项不存在', '父级字典不存在'],
}),
}),
),
500: responseWrapperSchema(
t.Object({
error: t.String({
description: '服务器错误',
examples: ['内部服务器错误'],
}),
}),
),
};
/** 字典项排序成功响应数据类型 */
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]>;

View File

@ -15,49 +15,52 @@ import { t, type Static } from 'elysia';
*/
export const CreateDictSchema = t.Object({
/** 字典代码,唯一标识 */
code: t.Transform(t.String({
code: t
.Transform(
t.String({
minLength: 1,
maxLength: 50,
description: '字典代码,全局唯一标识,自动转换为小写并去除两端空格',
examples: ['user_status', 'order_type', 'system_config'],
}))
}),
)
.Decode((value: string) => value.trim().toLowerCase())
.Encode((value: string) => value),
/** 字典名称 */
name: t.Transform(t.String({
name: t
.Transform(
t.String({
minLength: 1,
maxLength: 100,
description: '字典名称,同级下唯一,自动去除两端空格',
examples: ['用户状态', '订单类型', '系统配置'],
}))
}),
)
.Decode((value: string) => value.trim().toLowerCase())
.Encode((value: string) => value),
/** 字典值(叶子节点才有值) */
value: t.Optional(
t
.String({
t.String({
maxLength: 200,
description: '字典值,叶子节点才有值,自动去除两端空格',
examples: ['active', 'inactive', 'pending'],
})
}),
),
/** 字典描述 */
description: t.Optional(
t
.String({
maxLength: 500,
t.String({
maxLength: 200,
description: '字典描述,自动去除两端空格',
examples: ['用户状态字典,包含激活、禁用、待审核等状态'],
})
}),
),
/** 图标CSS类名或图标路径 */
icon: t.Optional(
t
.String({
t.String({
maxLength: 100,
description: '图标CSS类名或图标路径自动去除两端空格',
examples: ['icon-user', 'icon-order', '/icons/config.png'],
})
}),
),
/** 父级ID0表示顶级 */
pid: t.Optional(
@ -86,30 +89,27 @@ export const CreateDictSchema = t.Object({
),
/** 状态active-启用inactive-禁用 */
status: t.Optional(
t
.Union([t.Literal('active'), t.Literal('inactive')], {
t.Union([t.Literal('active'), t.Literal('inactive')], {
description: '字典状态默认active',
examples: ['active', 'inactive'],
default: 'active',
})
}),
),
/** 是否系统字典 */
isSystem: t.Optional(
t
.Boolean({
t.Boolean({
description: '是否系统字典系统字典只能由超级管理员创建默认false',
examples: [true, false],
default: false,
})
}),
),
/** 颜色标识 */
color: t.Optional(
t
.String({
t.String({
maxLength: 20,
description: '颜色标识,用于前端显示,自动去除两端空格',
examples: ['#1890ff', '#52c41a', '#faad14', '#f5222d'],
})
}),
),
/** 扩展字段 */
extra: t.Optional(
@ -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,13 +225,24 @@ export const GetDictTreeByCodeSchema = t.Object({
});
/** 获取指定字典树请求参数类型 */
export type GetDictTreeByCodeRequest = Static<typeof GetDictTreeByCodeSchema>;
export type GetDictTreeByCodeRequest = Static<typeof GetDictTreeByCodeQuerySchema>;
/**
* 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({
@ -228,7 +272,7 @@ export const UpdateDictSchema = t.Object({
/** 字典描述 */
description: t.Optional(
t.String({
maxLength: 500,
maxLength: 200,
description: '字典描述信息',
examples: ['用户状态字典,包含激活、禁用、待审核等状态'],
}),
@ -297,8 +341,8 @@ export const UpdateDictSchema = t.Object({
),
});
/** 更新字典项请求参数类型 */
export type UpdateDictRequest = Static<typeof UpdateDictSchema>;
/** 更新字典项Body参数类型 */
export type UpdateDictBodyRequest = Static<typeof UpdateDictBodySchema>;
/**
* Schema

View File

@ -4,17 +4,33 @@
* @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, or, ne, desc, asc, sql, inArray, isNull, not, max } 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 { DistributedLockService } from '@/utils/distributedLock';
import type {
CreateDictRequest,
GetDictTreeByCodeRequest,
GetDictTreeQueryRequest,
UpdateDictBodyRequest,
SortDictRequest,
DeleteDictRequest,
} from './dict.schema';
import type {
CreateDictSuccessType,
GetDictByIdSuccessType,
GetDictTreeByCodeSuccessType,
GetDictTreeSuccessType,
UpdateDictSuccessType,
SortDictSuccessType,
DeleteDictSuccessType,
} from './dict.response';
/**
*
@ -28,15 +44,27 @@ export class DictService {
* @throws BusinessError
*/
public async createDict(body: CreateDictRequest): Promise<CreateDictSuccessType> {
const pid = body.pid || '0';
const lockKey = `dict:create:code:${body.code}:name:${body.name}:pid:${pid}`;
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
try {
// 1. code唯一性校验
const existCode = await db().select({id: sysDict.id}).from(sysDict).where(eq(sysDict.code, body.code)).limit(1);
const existCode = await db()
.select({ id: sysDict.id })
.from(sysDict)
.where(eq(sysDict.code, body.code))
.limit(1);
if (existCode.length > 0) {
throw new BusinessError(`字典代码已存在: ${body.code}`, 409);
}
// 2. name同级唯一性校验
const pid = body.pid || '0';
const existName = await db().select({id: sysDict.id}).from(sysDict).where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid))).limit(1);
const existName = await db()
.select({ id: sysDict.id })
.from(sysDict)
.where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid)))
.limit(1);
if (existName.length > 0) {
throw new BusinessError(`字典名称已存在: ${body.name}`, 409);
}
@ -120,6 +148,469 @@ export class DictService {
},
'创建字典项成功',
);
} finally {
await lock.release();
}
}
/**
*
* @param id ID
* @returns Promise<GetDictByIdSuccessType>
* @throws BusinessError
*/
public async getDictById(id: string): Promise<GetDictByIdSuccessType> {
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<GetDictTreeSuccessType>
*/
public async getDictTree(query: GetDictTreeQueryRequest): Promise<GetDictTreeSuccessType> {
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<(typeof dictList)[0], 'id' | 'pid' | 'isSystem'> & {
id: string;
pid: string;
isSystem: boolean;
children?: DictNode[];
};
const nodeMap = new Map<string, DictNode>();
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, '获取完整字典树成功');
}
/**
*
* @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] : [], '获取指定字典树成功');
}
/**
*
* @param id ID
* @param body
* @returns Promise<UpdateDictSuccessType>
*/
public async updateDict(id: string, body: UpdateDictBodyRequest): Promise<UpdateDictSuccessType> {
const lock = await DistributedLockService.acquire({ key: `dict:update:${id}`, ttl: 10 });
try {
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<typeof existing> = {};
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),
},
'更新字典项成功',
);
} finally {
await lock.release();
}
}
/**
*
* @param body SortDictRequest
* @returns Promise<SortDictSuccessType>
* @throws BusinessError
* @description 使SQL保证性能和类型安全
*/
public async sortDict(body: SortDictRequest): Promise<SortDictSuccessType> {
const lock = await DistributedLockService.acquire({ key: `dict:sort`, ttl: 30 });
try {
const { items } = body;
if (!items || items.length === 0) {
throw new BusinessError('排序项列表不能为空', 400);
}
const itemIds = items.map((item) => BigInt(item.id));
const parentIds = [
...new Set(
items
.map((item) => item.pid)
.filter((pid) => pid !== 0 && pid !== '0')
.map((pid) => BigInt(pid as string)),
),
];
try {
await db().transaction(async (tx) => {
// 1. 使用原生SQL验证所有待排序的字典项是否存在
if (itemIds.length > 0) {
const result: any[] = await tx.execute(
sql`SELECT id FROM sys_dict WHERE id IN ${itemIds}`,
);
const existingIds = result[0].map((row: { id: bigint }) => row.id);
if (existingIds.length !== itemIds.length) {
const foundIds = new Set(existingIds);
const notFoundIds = itemIds.filter((id) => !foundIds.has(id));
Logger.warn(`部分字典项不存在:${notFoundIds.join(', ')}`);
throw new BusinessError(`部分字典项不存在: ${notFoundIds.join(', ')}`, 404);
}
}
// 2. 使用原生SQL验证所有父级字典是否存在
if (parentIds.length > 0) {
const result: any[] = await tx.execute(
sql`SELECT id FROM sys_dict WHERE id IN ${parentIds}`,
);
const existingParentIds = result[0].map((row: { id: bigint }) => row.id);
if (existingParentIds.length !== parentIds.length) {
const foundParentIds = new Set(existingParentIds);
const notFoundParentIds = parentIds.filter((id) => !foundParentIds.has(id));
Logger.warn(`部分父级字典不存在:${notFoundParentIds.join(', ')}`);
throw new BusinessError(
`部分父级字典不存在: ${notFoundParentIds.join(', ')}`,
404,
);
}
}
// 3. 使用原生SQL构建并执行批量更新
if (items.length > 0) {
const pidSqlChunks = items.map(
(item) =>
sql`WHEN ${BigInt(item.id)} THEN ${
item.pid === 0 || item.pid === '0' ? BigInt(0) : BigInt(item.pid)
}`,
);
const sortOrderSqlChunks = items.map(
(item) => sql`WHEN ${BigInt(item.id)} THEN ${item.sortOrder}`,
);
const finalUpdateQuery = sql`
UPDATE sys_dict
SET
pid = CASE id ${sql.join(pidSqlChunks, sql` `)} END,
sort_order = CASE id ${sql.join(sortOrderSqlChunks, sql` `)} END,
updated_at = CURRENT_TIMESTAMP
WHERE id IN ${itemIds}
`;
await tx.execute(finalUpdateQuery);
}
});
} catch (error) {
if (error instanceof BusinessError) {
throw error;
}
throw new BusinessError('字典项排序失败,请稍后重试', 500);
}
Logger.info('字典项排序成功');
return successResponse(null, '字典项排序成功');
} finally {
await lock.release();
}
}
/**
*
* @param params DeleteDictRequest { id, cascade }
* @returns Promise<DeleteDictSuccessType>
* @throws BusinessError
* @description
*/
public async deleteDict({
id,
cascade,
}: DeleteDictRequest): Promise<DeleteDictSuccessType> {
const lock = await DistributedLockService.acquire({ key: `dict:delete:${id}`, ttl: 10 });
try {
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;
}
throw new BusinessError('字典项删除失败,请稍后重试', 500);
}
Logger.info(`字典项 ${dictId} 及关联子项(如有)已成功软删除`);
return successResponse(null, '字典项删除成功');
} finally {
await lock.release();
}
}
/**
* 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);
}
}
}

View File

@ -0,0 +1,253 @@
# 字典模块测试用例文档
## 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` 是一个空数组 `[]`
## 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` 的节点。
## 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)**:
- 响应体包含错误信息,提示 "同级下字典名称已存在"。

169
src/modules/dict/test.md Normal file
View File

@ -0,0 +1,169 @@
- **预期响应 (409 Conflict)**:
- 响应体包含错误信息,提示 "同级下字典名称已存在"。
## 6. 字典项排序接口 (PUT /api/dict/sort)
### 场景1: 成功 - 同级排序
- **名称**: 成功对同一父级下的多个字典项进行重新排序
- **前置条件**:
- 字典项A (id: 201, pid: 200, sortOrder: 1)
- 字典项B (id: 202, pid: 200, sortOrder: 2)
- 字典项C (id: 203, pid: 200, sortOrder: 3)
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": [
{ "id": "201", "pid": "200", "sortOrder": 3 },
{ "id": "202", "pid": "200", "sortOrder": 1 },
{ "id": "203", "pid": "200", "sortOrder": 2 }
]
}
```
- **预期响应 (200 OK)**:
- 响应体 data 为 `null`message 为 "字典项排序成功"。
- 数据库中字典项A, B, C 的 `sortOrder` 分别更新为 3, 1, 2。
### 场景2: 成功 - 移动节点(跨父级)
- **名称**: 成功将一个字典项移动到另一个父级下
- **前置条件**:
- 字典项A (id: 201, pid: 200, sortOrder: 1)
- 父字典X (id: 200)
- 父字典Y (id: 300)
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": [
{ "id": "201", "pid": "300", "sortOrder": 5 }
]
}
```
- **预期响应 (200 OK)**:
- 数据库中字典项A 的 `pid` 更新为 300`sortOrder` 更新为 5。
### 场景3: 成功 - 移动节点到根级
- **名称**: 成功将一个字典项移动到根级
- **前置条件**:
- 字典项A (id: 201, pid: 200, sortOrder: 1)
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": [
{ "id": "201", "pid": 0, "sortOrder": 10 }
]
}
```
- **预期响应 (200 OK)**:
- 数据库中字典项A 的 `pid` 更新为 0`sortOrder` 更新为 10。
### 场景4: 失败 - 字典项ID不存在
- **名称**: 因请求中包含不存在的字典项ID而失败
- **前置条件**: 数据库中不存在 id 为 `999` 的字典项。
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": [
{ "id": "999", "pid": "200", "sortOrder": 1 }
]
}
```
- **预期响应 (404 Not Found)**:
- 响应体包含错误信息,提示 "部分字典项不存在"。
### 场景5: 失败 - 父级ID不存在
- **名称**: 因请求中包含不存在的父级ID而失败
- **前置条件**:
- 字典项A (id: 201) 存在。
- 数据库中不存在 id 为 `998` 的父级字典项。
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": [
{ "id": "201", "pid": "998", "sortOrder": 1 }
]
}
```
- **预期响应 (404 Not Found)**:
- 响应体包含错误信息,提示 "部分父级字典不存在"。
### 场景6: 失败 - 请求体为空
- **名称**: 因请求体中的 `items` 数组为空导致失败
- **前置条件**: -
- **请求方法**: `PUT`
- **请求路径**: `/api/dict/sort`
- **请求体**:
```json
{
"items": []
}
```
- **预期响应 (400 Bad Request)**:
- 响应体包含错误信息,提示 "排序项列表不能为空"。
## 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)**:
- 响应体包含错误信息,提示 "字典项不存在"。

View File

@ -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 {

View File

@ -12,7 +12,7 @@ CREATE TABLE `sys_dict` (
`code` VARCHAR(50) NOT NULL COMMENT '字典代码,唯一标识',
`name` VARCHAR(100) NOT NULL COMMENT '字典名称',
`value` VARCHAR(200) NULL COMMENT '字典值(叶子节点才有值)',
`description` VARCHAR(500) NULL COMMENT '字典描述',
`description` VARCHAR(200) NULL COMMENT '字典描述',
`icon` VARCHAR(100) NULL COMMENT '图标CSS类名或图标路径',
`pid` BIGINT NULL DEFAULT 0 COMMENT '父级ID0表示顶级',
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度1为顶级',
@ -101,63 +101,63 @@ 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` - 添加获取指定字典树业务逻辑
- [ ] 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` - 添加字典项排序业务逻辑
- [ ] 7.2 扩展 `dict.schema.ts` - 定义字典项排序Schema
- [ ] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式
- [ ] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑
- [ ] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由
- [ ] 7.6 更新 `dict.test.md` - 添加字典项排序测试用例
- [x] 7.0 字典项排序接口 (PUT /api/dict/sort)
- [x] 7.1 更新 `dict.docs.md` - 添加字典项排序业务逻辑
- [x] 7.2 扩展 `dict.schema.ts` - 定义字典项排序Schema
- [x] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式
- [x] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑
- [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` - 实现删除字典项路由
- [ ] 8.6 更新 `dict.test.md` - 添加删除字典项测试用例
- [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缓存机制和优化