/** * @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 * @throws BusinessError 业务逻辑错误 */ public async createDict(body: CreateDictRequest): Promise { // 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 * @throws BusinessError 业务逻辑错误 */ public async getDictById(id: string): Promise { 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 */ public async getDictTree(query: GetDictTreeQueryRequest): Promise { 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(); 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 */ public async getDictTreeByCode( { code }: { code: string }, query: GetDictTreeByCodeRequest, ): Promise { 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(); 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 */ public async updateDict(id: string, body: UpdateDictBodyRequest): Promise { Logger.info(`更新字典项: ${id}, body: ${JSON.stringify(body)}`); // 1. 检查字典项是否存在 const existingArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); if (existingArr.length === 0) { throw new BusinessError(`字典项不存在: ${id}`, 404); } const existing = existingArr[0]!; // 2. 唯一性校验 if (body.code) { const existCode = await db() .select({ id: sysDict.id }) .from(sysDict) .where(and(eq(sysDict.code, body.code), ne(sysDict.id, id))) .limit(1); if (existCode.length > 0) { throw new BusinessError(`字典代码已存在: ${body.code}`, 409); } } if (body.name) { const pid = body.pid ? String(body.pid) : existing.pid ? String(existing.pid) : '0'; const existName = await db() .select({ id: sysDict.id }) .from(sysDict) .where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid), ne(sysDict.id, id))) .limit(1); if (existName.length > 0) { throw new BusinessError(`同级下字典名称已存在: ${body.name}`, 409); } } // 3. 构建更新数据 const updateData: Partial = {}; for (const key in body) { if (Object.prototype.hasOwnProperty.call(body, key) && body[key as keyof typeof body] !== undefined) { if (key === 'isSystem') { (updateData as any)[key] = body[key] ? 1 : 0; } else { (updateData as any)[key] = body[key as keyof typeof body]; } } } if (Object.keys(updateData).length === 0) { return successResponse( { ...existing, id: String(existing.id), pid: String(existing.pid), isSystem: Boolean(existing.isSystem), }, '未提供任何更新内容', ); } // 4. 执行更新 await db().update(sysDict).set(updateData).where(eq(sysDict.id, id)); // 5. 查询并返回更新后的数据 const updatedArr = await db().select().from(sysDict).where(eq(sysDict.id, id)).limit(1); const updated = updatedArr[0]!; return successResponse( { ...updated, id: String(updated.id), pid: String(updated.pid), isSystem: Boolean(updated.isSystem), }, '更新字典项成功', ); } /** * 字典项排序 * @param body SortDictRequest 排序请求参数 * @returns Promise * @throws BusinessError 业务逻辑错误 * @description 处理字典项的排序和移动(改变父级),使用原生SQL保证性能和类型安全 */ public async sortDict(body: SortDictRequest): Promise { 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 * @throws BusinessError 业务逻辑错误 * @description 软删除字典项,可选择级联删除子项 */ public async deleteDict({ id, cascade, }: DeleteDictRequest): Promise { const dictId = BigInt(id); Logger.info(`请求删除字典项: ${dictId}, 是否级联: ${!!cascade}`); try { await db().transaction(async (tx) => { // 1. 查找字典项 const dictResult: any[] = await tx.execute( sql`SELECT * FROM sys_dict WHERE id = ${dictId} LIMIT 1`, ); const dictToDelete = dictResult[0] ? dictResult[0][0] : null; if (!dictToDelete) { Logger.warn(`删除失败,字典项不存在: ${dictId}`); throw new BusinessError('字典项不存在', 404); } // 2. 系统字典不允许删除 if (dictToDelete.is_system) { Logger.warn(`删除失败,系统字典不允许删除: ${dictId}`); throw new BusinessError('系统字典不允许删除', 409); } // 3. 查找所有子孙节点 const childrenIds: bigint[] = []; if (cascade) { const allDicts = await tx.query.sysDict.findMany({ columns: { id: true, pid: true }, }); const allDictsBigInt = allDicts.map((d) => ({ id: BigInt(d.id), pid: d.pid ? BigInt(d.pid) : null, })); this.findAllChildrenIds(dictId, allDictsBigInt, childrenIds); } else { // 如果不级联,检查是否有直接子节点 const childResult: any[] = await tx.execute( sql`SELECT id FROM sys_dict WHERE pid = ${dictId} LIMIT 1`, ); if (childResult[0] && childResult[0].length > 0) { Logger.warn(`删除失败,存在子字典项,且未启用级联删除: ${dictId}`); throw new BusinessError( '存在子字典项,无法删除,请先删除子项或使用级联删除', 400, ); } } const idsToDelete = [dictId, ...childrenIds]; Logger.info(`准备删除以下字典项ID: ${idsToDelete.join(', ')}`); // 4. 使用原生SQL执行软删除 if (idsToDelete.length > 0) { await tx.execute( sql`UPDATE sys_dict SET status = 'inactive' WHERE id IN ${idsToDelete}`, ); } }); } catch (error) { if (error instanceof BusinessError) { throw error; } Logger.error('字典项删除失败', { error }); throw new BusinessError('字典项删除失败,请稍后重试', 500); } Logger.info(`字典项 ${dictId} 及关联子项(如有)已成功软删除`); return successResponse(null, '字典项删除成功'); } /** * 递归查找所有子孙节点的ID * @param parentId 父级ID * @param allItems 所有字典项列表 * @param childrenIds 存储子孙节点ID的数组 */ private findAllChildrenIds( parentId: bigint, allItems: { id: bigint; pid: bigint | null }[], childrenIds: bigint[], ) { const children = allItems.filter((item) => item.pid === parentId); for (const child of children) { childrenIds.push(child.id); this.findAllChildrenIds(child.id, allItems, childrenIds); } } } // 导出单例实例 export const dictService = new DictService();