From 8d68f8e76a5ea260c72900dd55a804d76a1ff73a Mon Sep 17 00:00:00 2001 From: snowgitea Date: Wed, 1 Jul 2026 17:56:16 +0800 Subject: [PATCH] chore: src/modules/base/service/sys/menu.ts --- src/modules/base/service/sys/menu.ts | 463 +++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 src/modules/base/service/sys/menu.ts diff --git a/src/modules/base/service/sys/menu.ts b/src/modules/base/service/sys/menu.ts new file mode 100644 index 0000000..261d5a7 --- /dev/null +++ b/src/modules/base/service/sys/menu.ts @@ -0,0 +1,463 @@ +import { App, IMidwayApplication, Scope, ScopeEnum } from '@midwayjs/core'; +import { ALL, Config, Inject, Provide } from '@midwayjs/core'; +import { BaseService, CoolCommException } from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { BaseSysMenuEntity } from '../../entity/sys/menu'; +import * as _ from 'lodash'; +import { BaseSysPermsService } from './perms'; +import { Context } from '@midwayjs/koa'; +import { TempDataSource } from './data'; +// eslint-disable-next-line node/no-unpublished-import +import * as ts from 'typescript'; +import * as fs from 'fs'; +import * as pathUtil from 'path'; +import { BaseSysRoleMenuEntity } from '../../entity/sys/role_menu'; +import { BaseSysUserRoleEntity } from '../../entity/sys/user_role'; + +/** + * 菜单 + */ +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +@Provide() +export class BaseSysMenuService extends BaseService { + @Inject() + ctx: Context; + + @InjectEntityModel(BaseSysMenuEntity) + baseSysMenuEntity: Repository; + + @InjectEntityModel(BaseSysRoleMenuEntity) + baseSysRoleMenuEntity: Repository; + + @Inject() + baseSysPermsService: BaseSysPermsService; + + @Config(ALL) + config; + + @App() + app: IMidwayApplication; + + /** + * 获得所有菜单 + */ + async list() { + const menus = await this.getMenus( + this.ctx.admin.roleIds, + this.ctx.admin.username === 'admin' + ); + if (!_.isEmpty(menus)) { + menus.forEach((e: any) => { + const parentMenu = menus.filter(m => { + e.parentId = parseInt(e.parentId); + if (e.parentId == m.id) { + return m.name; + } + }); + if (!_.isEmpty(parentMenu)) { + e.parentName = parentMenu[0].name; + } + }); + } + return menus; + } + + /** + * 修改之后 + * @param param + */ + async modifyAfter(param) { + if (param.id) { + await this.refreshPerms(param.id); + } + } + + /** + * 根据角色获得权限信息 + * @param {[]} roleIds 数组 + */ + async getPerms(roleIds) { + let perms = []; + if (!_.isEmpty(roleIds)) { + const find = await this.baseSysMenuEntity.createQueryBuilder('a'); + if (!roleIds.includes(1)) { + find.innerJoinAndSelect( + BaseSysRoleMenuEntity, + 'b', + 'a.id = b.menuId AND b.roleId in (:...roleIds)', + { roleIds } + ); + } + find.where('a.perms is not NULL'); + const result = await find.getMany(); + if (result) { + result.forEach(d => { + if (d.perms) { + perms = perms.concat(d.perms.split(',')); + } + }); + } + perms = _.uniq(perms); + perms = _.remove(perms, n => { + return !_.isEmpty(n); + }); + } + return _.uniq(perms); + } + + /** + * 获得用户菜单信息 + * @param roleIds + * @param isAdmin 是否是超管 + */ + async getMenus(roleIds, isAdmin) { + const find = this.baseSysMenuEntity.createQueryBuilder('a'); + if (!isAdmin) { + find.innerJoinAndSelect( + BaseSysRoleMenuEntity, + 'b', + 'a.id = b.menuId AND b.roleId in (:...roleIds)', + { roleIds } + ); + } + find.orderBy('a.orderNum', 'ASC'); + const list = await find.getMany(); + return _.uniqBy(list, 'id'); + } + + /** + * 删除 + * @param ids + */ + async delete(ids) { + let idArr; + if (ids instanceof Array) { + idArr = ids; + } else { + idArr = ids.split(','); + } + for (const id of idArr) { + await this.baseSysMenuEntity.delete({ id }); + await this.delChildMenu(id); + } + } + + /** + * 删除子菜单 + * @param id + */ + private async delChildMenu(id) { + await this.refreshPerms(id); + const delMenu = await this.baseSysMenuEntity.findBy({ parentId: id }); + if (_.isEmpty(delMenu)) { + return; + } + const delMenuIds = delMenu.map(e => { + return e.id; + }); + await this.baseSysMenuEntity.delete(delMenuIds); + for (const menuId of delMenuIds) { + await this.delChildMenu(menuId); + } + } + + /** + * 更新权限 + * @param menuId + */ + async refreshPerms(menuId) { + const find = this.baseSysRoleMenuEntity.createQueryBuilder('a'); + find.leftJoinAndSelect(BaseSysUserRoleEntity, 'b', 'a.roleId = b.roleId'); + find.where('a.menuId = :menuId', { menuId: menuId }); + find.select('b.userId', 'userId'); + const users = await find.getRawMany(); + // 刷新admin权限 + await this.baseSysPermsService.refreshPerms(1); + if (!_.isEmpty(users)) { + // 刷新其他权限 + for (const user of _.uniqBy(users, 'userId')) { + await this.baseSysPermsService.refreshPerms(user.userId); + } + } + } + + /** + * 解析实体和Controller + * @param entityString + * @param controller + * @param module + */ + async parse(entityString: string, controller: string, module: string) { + const tempDataSource = new TempDataSource({ + ...this.config.typeorm.dataSource.default, + entities: [], + }); + // 连接数据库 + await tempDataSource.initialize(); + const { newCode, className, oldTableName } = this.parseCode(entityString); + const code = ts.transpile( + `${newCode} + tempDataSource.options.entities.push(${className}) + `, + { + emitDecoratorMetadata: true, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2018, + removeComments: true, + experimentalDecorators: true, + noImplicitThis: true, + noUnusedLocals: true, + stripInternal: true, + skipLibCheck: true, + pretty: true, + declaration: true, + noImplicitAny: false, + } + ); + eval(code); + await tempDataSource.buildMetadatas(); + const meta = tempDataSource.getMetadata(className); + const columnArr = meta.columns; + await tempDataSource.destroy(); + + const commColums = []; + const columns = _.filter( + columnArr.map(e => { + return { + propertyName: e.propertyName, + type: typeof e.type == 'string' ? e.type : e.type.name.toLowerCase(), + length: e.length, + comment: e.comment, + nullable: e.isNullable, + }; + }), + o => { + if (['createTime', 'updateTime'].includes(o.propertyName)) { + commColums.push(o); + } + return o && !['createTime', 'updateTime'].includes(o.propertyName); + } + ).concat(commColums); + if (!controller) { + const tableNames = oldTableName.split('_'); + const fileName = tableNames[tableNames.length - 1]; + return { + columns, + className: className.replace('TEMP', ''), + tableName: oldTableName, + fileName, + path: `/admin/${module}/${fileName}`, + }; + } + const fileName = await this.fileName(controller); + return { + columns, + path: `/admin/${module}/${fileName}`, + }; + } + + /** + * 解析Entity类名 + * @param code + * @returns + */ + parseCode(code: string) { + try { + const oldClassName = code + .match('class(.*)extends')[1] + .replace(/\s*/g, ''); + const oldTableStart = code.indexOf('@Entity('); + const oldTableEnd = code.indexOf(')'); + + const oldTableName = code + .substring(oldTableStart + 9, oldTableEnd - 1) + .replace(/\s*/g, '') + // eslint-disable-next-line no-useless-escape + .replace(/\"/g, '') + // eslint-disable-next-line no-useless-escape + .replace(/\'/g, ''); + const className = `${oldClassName}TEMP`; + return { + newCode: code + .replace(oldClassName, className) + .replace(oldTableName, `func_${oldTableName}`), + className, + tableName: `func_${oldTableName}`, + oldTableName, + }; + } catch (err) { + throw new CoolCommException('代码结构不正确,请检查'); + } + } + + /** + * 创建代码 + * @param body body + */ + async create(body) { + const { module, entity, controller, service, fileName } = body; + const basePath = this.app.getBaseDir(); + const modulePath = pathUtil.join(basePath, '..', 'src', 'modules', module); + // 生成Entity + const entityPath = pathUtil.join(modulePath, 'entity', `${fileName}.ts`); + // 生成Controller + const controllerPath = pathUtil.join( + modulePath, + 'controller', + 'admin', + `${fileName}.ts` + ); + // 生成Service + const servicePath = pathUtil.join(modulePath, 'service', `${fileName}.ts`); + this.createConfigFile(module); + this.createFile(entityPath, entity); + this.createFile(controllerPath, controller); + this.createFile(servicePath, service); + } + + /** + * 创建配置文件 + * @param module + */ + async createConfigFile(module: string) { + const basePath = this.app.getBaseDir(); + const configFilePath = pathUtil.join( + basePath, + '..', + 'src', + 'modules', + module, + 'config.ts' + ); + if (!fs.existsSync(configFilePath)) { + const data = `import { ModuleConfig } from '@cool-midway/core'; + +/** + * 模块配置 + */ +export default () => { + return { + // 模块名称 + name: 'xxx', + // 模块描述 + description: 'xxx', + // 中间件,只对本模块有效 + middlewares: [], + // 中间件,全局有效 + globalMiddlewares: [], + // 模块加载顺序,默认为0,值越大越优先加载 + order: 0, + } as ModuleConfig; +}; +`; + await this.createFile(configFilePath, data); + } + } + + /** + * 找到文件名 + * @param controller + * @returns + */ + async fileName(controller: string) { + const regex = /import\s*{\s*\w+\s*}\s*from\s*'[^']*\/([\w-]+)';/; + const match = regex.exec(controller); + + if (match && match.length > 1) { + return match[1]; + } + + return null; + } + + /** + * 创建文件 + * @param filePath + * @param content + */ + async createFile(filePath: string, content: string) { + const folderPath = pathUtil.dirname(filePath); + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + fs.writeFileSync(filePath, content); + } + + /** + * 导出菜单 + * @param ids + * @returns + */ + async export(ids: number[]) { + const result: any[] = []; + const menus = await this.baseSysMenuEntity.findBy({ id: In(ids) }); + + // 递归取出子菜单 + const getChildMenus = (parentId: number): any[] => { + const children = _.remove(menus, e => e.parentId == parentId); + children.forEach(child => { + child.childMenus = getChildMenus(child.id); + // 删除不需要的字段 + delete child.id; + delete child.createTime; + delete child.updateTime; + delete child.parentId; + }); + return children; + }; + + // lodash取出父级菜单(parentId为 null), 并从menus 删除 + const parentMenus = _.remove(menus, e => { + return e.parentId == null; + }); + + // 对于每个父级菜单,获取它的子菜单 + parentMenus.forEach(parent => { + parent.childMenus = getChildMenus(parent.id); + // 删除不需要的字段 + delete parent.id; + delete parent.createTime; + delete parent.updateTime; + delete parent.parentId; + + result.push(parent); + }); + + return result; + } + + /** + * 导入 + * @param menus + */ + async import(menus: any[]) { + // 递归保存子菜单 + const saveChildMenus = async (parentMenu: any, parentId: number | null) => { + const children = parentMenu.childMenus || []; + for (let child of children) { + const childData = { ...child, parentId: parentId }; // 保持与数据库的parentId字段的一致性 + delete childData.childMenus; // 删除childMenus属性,因为我们不想将它保存到数据库中 + + // 保存子菜单并获取其ID,以便为其子菜单设置parentId + const savedChild = await this.baseSysMenuEntity.save(childData); + + if (!_.isEmpty(child.childMenus)) { + await saveChildMenus(child, savedChild.id); + } + } + }; + + for (let menu of menus) { + const menuData = { ...menu }; + delete menuData.childMenus; // 删除childMenus属性,因为我们不想将它保存到数据库中 + + // 保存主菜单并获取其ID + const savedMenu = await this.baseSysMenuEntity.save(menuData); + + if (menu.childMenus && menu.childMenus.length > 0) { + await saveChildMenus(menu, savedMenu.id); + } + } + } +}