Compare commits

..

No commits in common. "e8e352b6b6f006544341786f24e87f01016c09ce" and "f9f75c9d2d76da02db5ce87fe61518b9240255b8" have entirely different histories.

8 changed files with 196 additions and 1569 deletions

View File

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

View File

@ -4,33 +4,17 @@
* @date 2024-12-19 * @date 2024-12-19
* @lastEditor AI Assistant * @lastEditor AI Assistant
* @lastEditTime 2025-01-07 * @lastEditTime 2025-01-07
* @description * @description
*/ */
import { Logger } from '@/plugins/logger/logger.service'; import { Logger } from '@/plugins/logger/logger.service';
import { db } from '@/plugins/drizzle/drizzle.service'; import { db } from '@/plugins/drizzle/drizzle.service';
import { sysDict } from '@/eneities'; import { sysDict } from '@/eneities';
import { eq, and, or, ne, desc, asc, sql, inArray, isNull, not, max } from 'drizzle-orm'; import { eq, and, max } from 'drizzle-orm';
import { successResponse, BusinessError } from '@/utils/responseFormate'; import { successResponse, BusinessError } from '@/utils/responseFormate';
import { nextId } from '@/utils/snowflake'; import { nextId } from '@/utils/snowflake';
import { DistributedLockService } from '@/utils/distributedLock'; import type { CreateDictRequest } from './dict.schema';
import type { import type { CreateDictSuccessType } from './dict.response';
CreateDictRequest,
GetDictTreeByCodeRequest,
GetDictTreeQueryRequest,
UpdateDictBodyRequest,
SortDictRequest,
DeleteDictRequest,
} from './dict.schema';
import type {
CreateDictSuccessType,
GetDictByIdSuccessType,
GetDictTreeByCodeSuccessType,
GetDictTreeSuccessType,
UpdateDictSuccessType,
SortDictSuccessType,
DeleteDictSuccessType,
} from './dict.response';
/** /**
* *
@ -44,573 +28,98 @@ export class DictService {
* @throws BusinessError * @throws BusinessError
*/ */
public async createDict(body: CreateDictRequest): Promise<CreateDictSuccessType> { public async createDict(body: CreateDictRequest): Promise<CreateDictSuccessType> {
// 1. code唯一性校验
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 pid = body.pid || '0';
const lockKey = `dict:create:code:${body.code}:name:${body.name}:pid:${pid}`; const existName = await db().select({id: sysDict.id}).from(sysDict).where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid))).limit(1);
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 }); if (existName.length > 0) {
throw new BusinessError(`字典名称已存在: ${body.name}`, 409);
}
try { // 3. 父级校验与层级处理
// 1. code唯一性校验 let level = 1;
const existCode = await db() if (pid !== '0') {
.select({ id: sysDict.id }) const parent = await db().select().from(sysDict).where(eq(sysDict.id, pid)).limit(1);
if (parent.length === 0) {
throw new BusinessError(`父级字典不存在: ${pid}`, 404);
}
if (parent[0]!.status !== 'active') {
throw new BusinessError(`父级字典状态非active: ${pid}`, 400);
}
level = parent[0]!.level + 1;
if (level > 10) {
throw new BusinessError(`字典层级超过限制: ${level}`, 400);
}
}
// 4. sortOrder处理同级最大+1
let sortOrder = 0;
if (body.sortOrder !== undefined) {
sortOrder = body.sortOrder;
} else {
const maxSort = await db()
.select({ maxSort: max(sysDict.sortOrder) })
.from(sysDict) .from(sysDict)
.where(eq(sysDict.code, body.code)) .where(eq(sysDict.pid, pid));
.limit(1); sortOrder = (maxSort[0]?.maxSort ?? 0) + 1;
if (existCode.length > 0) { }
throw new BusinessError(`字典代码已存在: ${body.code}`, 409);
}
// 2. name同级唯一性校验 // 5. 数据写入
const existName = await db() const dictId = nextId();
.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);
}
// 3. 父级校验与层级处理 await db()
let level = 1; .insert(sysDict)
if (pid !== '0') { .values([
const parent = await db().select().from(sysDict).where(eq(sysDict.id, pid)).limit(1);
if (parent.length === 0) {
throw new BusinessError(`父级字典不存在: ${pid}`, 404);
}
if (parent[0]!.status !== 'active') {
throw new BusinessError(`父级字典状态非active: ${pid}`, 400);
}
level = parent[0]!.level + 1;
if (level > 10) {
throw new BusinessError(`字典层级超过限制: ${level}`, 400);
}
}
// 4. sortOrder处理同级最大+1
let sortOrder = 0;
if (body.sortOrder !== undefined) {
sortOrder = body.sortOrder;
} else {
const maxSort = await db()
.select({ maxSort: max(sysDict.sortOrder) })
.from(sysDict)
.where(eq(sysDict.pid, pid));
sortOrder = (maxSort[0]?.maxSort ?? 0) + 1;
}
// 5. 数据写入
const dictId = nextId();
await db()
.insert(sysDict)
.values([
{
id: dictId.toString(),
code: body.code,
name: body.name,
value: body.value ?? null,
description: body.description ?? null,
icon: body.icon ?? null,
pid: BigInt(pid),
level,
sortOrder,
status: body.status,
isSystem: body.isSystem ? 1 : 0,
color: body.color ?? null,
extra: body.extra ?? {},
},
] as any);
// 6. 查询刚插入的数据
const insertedArr = await db().select().from(sysDict).where(eq(sysDict.id, dictId.toString())).limit(1);
if (!insertedArr || insertedArr.length === 0) {
throw new BusinessError('创建字典项失败', 500);
}
const inserted = insertedArr[0]!;
// 7. 返回统一响应
return successResponse(
{ {
id: String(inserted.id), id: dictId.toString(),
code: inserted.code, code: body.code,
name: inserted.name, name: body.name,
value: inserted.value, value: body.value ?? null,
description: inserted.description, description: body.description ?? null,
icon: inserted.icon, icon: body.icon ?? null,
pid: String(inserted.pid), pid: BigInt(pid),
level: inserted.level, level,
sortOrder: inserted.sortOrder, sortOrder,
status: inserted.status, status: body.status,
isSystem: Boolean(inserted.isSystem), isSystem: body.isSystem ? 1 : 0,
color: inserted.color, color: body.color ?? null,
extra: inserted.extra, extra: body.extra ?? {},
createdAt: inserted.createdAt,
updatedAt: inserted.updatedAt,
}, },
'创建字典项成功', ] as any);
);
} finally { // 6. 查询刚插入的数据
await lock.release(); const insertedArr = await db().select().from(sysDict).where(eq(sysDict.id, dictId.toString())).limit(1);
if (!insertedArr || insertedArr.length === 0) {
throw new BusinessError('创建字典项失败', 500);
} }
} const inserted = insertedArr[0]!;
/** // 7. 返回统一响应
* return successResponse(
* @param id ID {
* @returns Promise<GetDictByIdSuccessType> id: String(inserted.id),
* @throws BusinessError code: inserted.code,
*/ name: inserted.name,
public async getDictById(id: string): Promise<GetDictByIdSuccessType> { value: inserted.value,
Logger.info(`获取字典项内容:${id}`); description: inserted.description,
icon: inserted.icon,
const dictArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); pid: String(inserted.pid),
level: inserted.level,
if (!dictArr || dictArr.length === 0) { sortOrder: inserted.sortOrder,
Logger.warn(`字典项不存在:${id}`); status: inserted.status,
throw new BusinessError(`字典项不存在:${id}`, 404); isSystem: Boolean(inserted.isSystem),
} color: inserted.color,
extra: inserted.extra,
const dict = dictArr[0]!; createdAt: inserted.createdAt,
updatedAt: inserted.updatedAt,
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

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

View File

@ -1,169 +0,0 @@
- **预期响应 (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,14 +75,6 @@ export const errorHandlerPlugin = (app: Elysia) =>
errors: error.message || error.response.message || error.response, 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: { case 408: {
set.status = code; set.status = code;
return { return {

View File

@ -12,7 +12,7 @@ CREATE TABLE `sys_dict` (
`code` VARCHAR(50) NOT NULL COMMENT '字典代码,唯一标识', `code` VARCHAR(50) NOT NULL COMMENT '字典代码,唯一标识',
`name` VARCHAR(100) NOT NULL COMMENT '字典名称', `name` VARCHAR(100) NOT NULL COMMENT '字典名称',
`value` VARCHAR(200) NULL COMMENT '字典值(叶子节点才有值)', `value` VARCHAR(200) NULL COMMENT '字典值(叶子节点才有值)',
`description` VARCHAR(200) NULL COMMENT '字典描述', `description` VARCHAR(500) NULL COMMENT '字典描述',
`icon` VARCHAR(100) NULL COMMENT '图标CSS类名或图标路径', `icon` VARCHAR(100) NULL COMMENT '图标CSS类名或图标路径',
`pid` BIGINT NULL DEFAULT 0 COMMENT '父级ID0表示顶级', `pid` BIGINT NULL DEFAULT 0 COMMENT '父级ID0表示顶级',
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度1为顶级', `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度1为顶级',
@ -101,63 +101,63 @@ CREATE TABLE `sys_dict` (
### 阶段2字典模块核心接口开发 ### 阶段2字典模块核心接口开发
- [x] 2.0 创建字典项接口 (POST /api/dict) - [ ] 2.0 创建字典项接口 (POST /api/dict)
- [x] 2.1 生成接口业务逻辑文档,写入 `dict.docs.md` - [ ] 2.1 生成接口业务逻辑文档,写入 `dict.docs.md`
- [x] 2.2 创建 `dict.schema.ts` - 定义创建字典项Schema - [ ] 2.2 创建 `dict.schema.ts` - 定义创建字典项Schema
- [x] 2.3 创建 `dict.response.ts` - 定义创建字典项响应格式 - [ ] 2.3 创建 `dict.response.ts` - 定义创建字典项响应格式
- [x] 2.4 创建 `dict.service.ts` - 实现创建字典项业务逻辑 - [ ] 2.4 创建 `dict.service.ts` - 实现创建字典项业务逻辑
- [x] 2.5 创建 `dict.controller.ts` - 实现创建字典项路由 - [ ] 2.5 创建 `dict.controller.ts` - 实现创建字典项路由
- [x] 2.6 创建 `dict.test.md` - 编写创建字典项测试用例 - [ ] 2.6 创建 `dict.test.md` - 编写创建字典项测试用例
- [x] 3.0 获取字典项内容接口 (GET /api/dict/:id) - [ ] 3.0 获取字典项内容接口 (GET /api/dict/:id)
- [x] 3.1 更新 `dict.docs.md` - 添加获取字典项业务逻辑 - [ ] 3.1 更新 `dict.docs.md` - 添加获取字典项业务逻辑
- [x] 3.2 扩展 `dict.schema.ts` - 定义获取字典项Schema - [ ] 3.2 扩展 `dict.schema.ts` - 定义获取字典项Schema
- [x] 3.3 扩展 `dict.response.ts` - 定义获取字典项响应格式 - [ ] 3.3 扩展 `dict.response.ts` - 定义获取字典项响应格式
- [x] 3.4 扩展 `dict.service.ts` - 实现获取字典项业务逻辑 - [ ] 3.4 扩展 `dict.service.ts` - 实现获取字典项业务逻辑
- [x] 3.5 扩展 `dict.controller.ts` - 实现获取字典项路由 - [ ] 3.5 扩展 `dict.controller.ts` - 实现获取字典项路由
- [x] 3.6 更新 `dict.test.md` - 添加获取字典项测试用例 - [ ] 3.6 更新 `dict.test.md` - 添加获取字典项测试用例
- [x] 4.0 获取完整字典树接口 (GET /api/dict/tree) - [ ] 4.0 获取完整字典树接口 (GET /api/dict/tree)
- [x] 4.1 更新 `dict.docs.md` - 添加获取完整字典树业务逻辑 - [ ] 4.1 更新 `dict.docs.md` - 添加获取完整字典树业务逻辑
- [x] 4.2 扩展 `dict.schema.ts` - 定义获取字典树查询Schema - [ ] 4.2 扩展 `dict.schema.ts` - 定义获取字典树查询Schema
- [x] 4.3 扩展 `dict.response.ts` - 定义字典树响应格式 - [ ] 4.3 扩展 `dict.response.ts` - 定义字典树响应格式
- [x] 4.4 扩展 `dict.service.ts` - 实现获取完整字典树业务逻辑 - [ ] 4.4 扩展 `dict.service.ts` - 实现获取完整字典树业务逻辑
- [x] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由 - [ ] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由
- [x] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例 - [ ] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例
- [x] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code) - [ ] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code)
- [x] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑 - [ ] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑
- [x] 5.2 扩展 `dict.schema.ts` - 定义获取指定字典树Schema - [ ] 5.2 扩展 `dict.schema.ts` - 定义获取指定字典树Schema
- [x] 5.3 扩展 `dict.response.ts` - 定义指定字典树响应格式 - [ ] 5.3 扩展 `dict.response.ts` - 定义指定字典树响应格式
- [x] 5.4 扩展 `dict.service.ts` - 实现获取指定字典树业务逻辑 - [ ] 5.4 扩展 `dict.service.ts` - 实现获取指定字典树业务逻辑
- [x] 5.5 扩展 `dict.controller.ts` - 实现获取指定字典树路由 - [ ] 5.5 扩展 `dict.controller.ts` - 实现获取指定字典树路由
- [x] 5.6 更新 `dict.test.md` - 添加获取指定字典树测试用例 - [ ] 5.6 更新 `dict.test.md` - 添加获取指定字典树测试用例
### 阶段3字典管理接口开发 ### 阶段3字典管理接口开发
- [x] 6.0 更新字典项内容接口 (PUT /api/dict/:id) - [ ] 6.0 更新字典项内容接口 (PUT /api/dict/:id)
- [x] 6.1 更新 `dict.docs.md` - 添加更新字典项业务逻辑 - [ ] 6.1 更新 `dict.docs.md` - 添加更新字典项业务逻辑
- [x] 6.2 扩展 `dict.schema.ts` - 定义更新字典项Schema - [ ] 6.2 扩展 `dict.schema.ts` - 定义更新字典项Schema
- [x] 6.3 扩展 `dict.response.ts` - 定义更新字典项响应格式 - [ ] 6.3 扩展 `dict.response.ts` - 定义更新字典项响应格式
- [x] 6.4 扩展 `dict.service.ts` - 实现更新字典项业务逻辑 - [ ] 6.4 扩展 `dict.service.ts` - 实现更新字典项业务逻辑
- [x] 6.5 扩展 `dict.controller.ts` - 实现更新字典项路由 - [ ] 6.5 扩展 `dict.controller.ts` - 实现更新字典项路由
- [x] 6.6 更新 `dict.test.md` - 添加更新字典项测试用例 - [ ] 6.6 更新 `dict.test.md` - 添加更新字典项测试用例
- [x] 7.0 字典项排序接口 (PUT /api/dict/sort) - [ ] 7.0 字典项排序接口 (PUT /api/dict/sort)
- [x] 7.1 更新 `dict.docs.md` - 添加字典项排序业务逻辑 - [ ] 7.1 更新 `dict.docs.md` - 添加字典项排序业务逻辑
- [x] 7.2 扩展 `dict.schema.ts` - 定义字典项排序Schema - [ ] 7.2 扩展 `dict.schema.ts` - 定义字典项排序Schema
- [x] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式 - [ ] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式
- [x] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑 - [ ] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑
- [x] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由 - [ ] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由
- [x] 7.6 在 `dict.test.md` 中添加排序接口测试用例 - [ ] 7.6 更新 `dict.test.md` - 添加字典项排序测试用例
- [x] 8.0 删除字典项接口 (DELETE /api/dict/:id) - [ ] 8.0 删除字典项接口 (DELETE /api/dict/:id)
- [x] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除) - [ ] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除)
- [x] 8.2 扩展 `dict.schema.ts` - 定义删除字典项Schema - [ ] 8.2 扩展 `dict.schema.ts` - 定义删除字典项Schema
- [x] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式 - [ ] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式
- [x] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑 - [ ] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑
- [x] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由 - [ ] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由
- [x] 8.6 在 `dict.test.md` 中添加删除接口测试用例 - [ ] 8.6 更新 `dict.test.md` - 添加删除字典项测试用例
### 阶段4缓存机制和优化 ### 阶段4缓存机制和优化