cursor-init/src/modules/dict/dict.service.ts
expressgy 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

598 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file 字典模块Service层实现
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description 字典模块的业务逻辑实现,包括创建、查询和树结构生成
*/
import { Logger } from '@/plugins/logger/logger.service';
import { db } from '@/plugins/drizzle/drizzle.service';
import { sysDict } from '@/eneities';
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,
GetDictTreeByCodeRequest,
GetDictTreeQueryRequest,
UpdateDictBodyRequest,
SortDictRequest,
DeleteDictRequest,
} from './dict.schema';
import type {
CreateDictSuccessType,
GetDictByIdSuccessType,
GetDictTreeByCodeSuccessType,
GetDictTreeSuccessType,
UpdateDictSuccessType,
SortDictSuccessType,
DeleteDictSuccessType,
} from './dict.response';
/**
* 字典服务类
* @description 处理字典相关的业务逻辑
*/
export class DictService {
/**
* 创建字典项
* @param body 创建字典项请求参数
* @returns Promise<CreateDictSuccessType>
* @throws BusinessError 业务逻辑错误
*/
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 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);
}
// 3. 父级校验与层级处理
let level = 1;
if (pid !== '0') {
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),
code: inserted.code,
name: inserted.name,
value: inserted.value,
description: inserted.description,
icon: inserted.icon,
pid: String(inserted.pid),
level: inserted.level,
sortOrder: inserted.sortOrder,
status: inserted.status,
isSystem: Boolean(inserted.isSystem),
color: inserted.color,
extra: inserted.extra,
createdAt: inserted.createdAt,
updatedAt: inserted.updatedAt,
},
'创建字典项成功',
);
}
/**
* 获取字典项内容
* @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> {
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),
},
'更新字典项成功',
);
}
/**
* 字典项排序
* @param body SortDictRequest 排序请求参数
* @returns Promise<SortDictSuccessType>
* @throws BusinessError 业务逻辑错误
* @description 处理字典项的排序和移动改变父级使用原生SQL保证性能和类型安全
*/
public async sortDict(body: SortDictRequest): Promise<SortDictSuccessType> {
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;
}
Logger.error('字典项排序失败', { error });
throw new BusinessError('字典项排序失败,请稍后重试', 500);
}
Logger.info('字典项排序成功');
return successResponse(null, '字典项排序成功');
}
/**
* 删除字典项(软删除)
* @param params DeleteDictRequest 删除请求参数 { id, cascade }
* @returns Promise<DeleteDictSuccessType>
* @throws BusinessError 业务逻辑错误
* @description 软删除字典项,可选择级联删除子项
*/
public async deleteDict({
id,
cascade,
}: DeleteDictRequest): Promise<DeleteDictSuccessType> {
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);
}
}
}
// 导出单例实例
export const dictService = new DictService();