kt-template-online-api/src/modules/admin/identity/menu/admin-menu.service.ts

337 lines
9.6 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.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { toTree } from '@/common';
import { AdminUser } from '../user/admin-user.entity';
import { AdminMenu } from './admin-menu.entity';
import type { AdminMenuInput, AdminMenuMeta } from '../../contract/admin.types';
@Injectable()
export class AdminMenuService {
/**
* 初始化 AdminMenuService 实例。
* @param menuRepository - 菜单仓库依赖;影响 constructor 的返回值。
*/
constructor(
@InjectRepository(AdminMenu)
private readonly menuRepository: Repository<AdminMenu>,
) {}
/**
* 查询 Admin 身份权限数据。
* @param user - user 输入;驱动 `this.getAllowedMenus()` 的 Admin步骤。
*/
async getAccessCodes(user: AdminUser) {
const menus = await this.getAllowedMenus(user);
return menus.map((menu) => menu.authCode).filter((authCode) => !!authCode);
}
/**
* 查询 Admin 身份权限数据。
* @param user - user 输入;驱动 `this.getAllowedMenus()` 的 Admin步骤。
*/
async getRouteMenus(user: AdminUser) {
const menus = await this.getAllowedMenus(user);
return this.buildMenuTree(menus.filter((menu) => menu.type !== 'button'));
}
/**
* 查询 Admin 身份权限数据。
*/
async getMenuList() {
const menus = await this.menuRepository.find({
where: {
isDeleted: false,
},
});
return this.buildMenuTree(menus);
}
/**
* 判断 Admin 身份权限条件。
* @param name - 名称文本;计算 Admin判断结果。
* @param id - Admin记录 ID定位本次读取、更新、删除或关联的Admin记录。
*/
async isMenuNameExists(name: string, id?: string) {
const menu = await this.menuRepository.findOne({
where: {
isDeleted: false,
name,
},
});
return !!menu && (!id || menu.id !== id);
}
/**
* 判断 Admin 身份权限条件。
* @param path - 路由或文件路径;决定 Admin条件分支。
* @param id - Admin记录 ID定位本次读取、更新、删除或关联的Admin记录。
*/
async isMenuPathExists(path: string, id?: string) {
if (path === '/') return !id;
const menu = await this.menuRepository.findOne({
where: {
isDeleted: false,
path,
},
});
return !!menu && (!id || menu.id !== id);
}
/**
* 创建 Admin 身份权限对象或配置。
* @param data - 业务数据;承载 Admin新增、更新、导入或执行字段。
*/
async createMenu(data: AdminMenuInput) {
const entity = this.menuRepository.create({
...this.normalizeMenuInput(data, true),
});
await this.menuRepository.save(entity);
return null;
}
/**
* 更新Menu。
* @param id - Admin记录 ID定位本次读取、更新、删除或关联的Admin记录。
* @param data - 业务数据;承载 Admin新增、更新、导入或执行字段。
*/
async updateMenu(id: string, data: AdminMenuInput) {
await this.menuRepository.update(
{ id },
{
...this.normalizeMenuInput(data, false),
},
);
return null;
}
/**
* 删除Menu。
* @param id - Admin记录 ID定位本次读取、更新、删除或关联的Admin记录。
*/
async deleteMenu(id: string) {
const ids = await this.collectChildMenuIds(id);
await this.menuRepository.update(
{
id: In(ids),
},
{
isDeleted: true,
},
);
return null;
}
/**
* 查询 Admin 身份权限数据。
* @param user - user 输入;使用 `roles` 字段生成结果。
*/
private async getAllowedMenus(user: AdminUser) {
const activeRoles = (user.roles || []).filter(
(role) => !role.isDeleted && role.status === 1,
);
if (activeRoles.some((role) => role.roleCode === 'super')) {
return this.menuRepository.find({
where: {
isDeleted: false,
status: 1,
},
});
}
const menuMap = new Map<string, AdminMenu>();
activeRoles.forEach((role) => {
(role.menus || [])
.filter((menu) => !menu.isDeleted && menu.status === 1)
.forEach((menu) => menuMap.set(menu.id, menu));
});
return this.includeAncestorMenus([...menuMap.values()]);
}
/**
* 执行 Admin 身份权限流程。
* @param menus - 菜单列表;遍历并累积 Admin结果。
*/
private async includeAncestorMenus(menus: AdminMenu[]) {
const menuMap = new Map<string, AdminMenu>();
menus.forEach((menu) => menuMap.set(menu.id, menu));
const pendingParentIds = new Set<string>();
/**
* 收集 Admin 管理数据。
* @param pid - Admin ID定位本次读取、更新、删除或关联的Admin。
*/
const collectMissingParent = (pid?: null | string) => {
if (!pid || pid === '0' || menuMap.has(pid)) return;
pendingParentIds.add(pid);
};
menus.forEach((menu) => collectMissingParent(menu.pid));
while (pendingParentIds.size > 0) {
const ids = [...pendingParentIds];
pendingParentIds.clear();
const parents = await this.menuRepository.find({
where: {
id: In(ids),
isDeleted: false,
status: 1,
},
});
parents.forEach((parent) => {
if (menuMap.has(parent.id)) return;
menuMap.set(parent.id, parent);
collectMissingParent(parent.pid);
});
}
return [...menuMap.values()];
}
/**
* 转换 Admin 身份权限输入。
* @param data - 业务数据;承载 Admin新增、更新、导入或执行字段。
* @param includeEmptyMeta - includeEmptyMeta 输入;决定 Admin条件分支。
* @returns Admin 身份权限转换后的值。
*/
private normalizeMenuInput(
data: AdminMenuInput,
includeEmptyMeta: boolean,
): Partial<AdminMenu> {
const meta = this.normalizeMetaInput(data);
const menu: Partial<AdminMenu> = {
authCode: data.authCode || null,
component: data.component || null,
name: data.name,
path: data.path || null,
pid: data.pid || '0',
redirect: data.redirect || null,
status: data.status ?? 1,
sort: Number(data.sort ?? 0),
type: data.type || 'menu',
};
if (includeEmptyMeta || Object.keys(meta).length > 0) {
menu.meta = meta;
}
return menu;
}
/**
* 转换 Admin 身份权限输入。
* @param data - 业务数据;承载 Admin新增、更新、导入或执行字段。
* @returns Admin 身份权限转换后的值。
*/
private normalizeMetaInput(data: AdminMenuInput): AdminMenuMeta {
const meta = this.normalizeMetaValue(data.meta);
// 兼容表单库返回字面量 `meta.title` 的场景,避免更新菜单时把 meta 覆盖为空对象。
Object.entries(data).forEach(([key, value]) => {
if (!key.startsWith('meta.')) return;
const metaKey = key.slice('meta.'.length);
if (metaKey) meta[metaKey] = value;
});
if (data.activePath) meta.activePath = data.activePath;
if (data.linkSrc && data.type === 'embedded') meta.iframeSrc = data.linkSrc;
if (data.linkSrc && data.type === 'link') meta.link = data.linkSrc;
Object.keys(meta).forEach((key) => {
if (meta[key] === null || meta[key] === undefined || meta[key] === '') {
delete meta[key];
}
});
return meta;
}
/**
* 转换 Admin 身份权限输入。
* @param meta - meta 输入;转换 JSON 文本。
* @returns Admin 身份权限转换后的值。
*/
private normalizeMetaValue(
meta: AdminMenuMeta | null | string | undefined,
): AdminMenuMeta {
if (!meta) return {};
if (typeof meta !== 'string') return { ...meta };
try {
const parsed = JSON.parse(meta);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
/**
* 执行 Admin 身份权限流程。
* @param id - Admin记录 ID定位本次读取、更新、删除或关联的Admin记录。
*/
private async collectChildMenuIds(id: string) {
const menus = await this.menuRepository.find({
where: {
isDeleted: false,
},
});
const ids = new Set<string>([id]);
let changed = true;
while (changed) {
changed = false;
menus.forEach((menu) => {
if (ids.has(menu.pid) && !ids.has(menu.id)) {
ids.add(menu.id);
changed = true;
}
});
}
return [...ids];
}
/**
* 创建 Admin 身份权限对象或配置。
* @param menus - 菜单列表;生成 Admin对象。
*/
private buildMenuTree(menus: AdminMenu[]) {
const nodes = menus
.map((menu) => this.serializeMenu(menu))
.sort((prev, next) => {
const prevOrder = prev.meta?.order ?? prev.sort ?? 0;
const nextOrder = next.meta?.order ?? next.sort ?? 0;
return prevOrder - nextOrder;
});
return toTree(nodes);
}
/**
* 序列化Menu。
* @param menu - menu 输入;使用 `meta`、`name`、`authCode`、`component` 字段生成结果。
*/
private serializeMenu(menu: AdminMenu) {
const meta = this.normalizeMetaValue(menu.meta);
if (!meta.title) meta.title = menu.name;
const node = {
authCode: menu.authCode,
component: menu.component,
createTime: menu.createTime,
id: menu.id,
meta,
name: menu.name,
path: menu.path,
pid: menu.pid || '0',
redirect: menu.redirect,
status: menu.status,
sort: menu.sort,
type: menu.type,
} as any;
Object.keys(node).forEach((key) => {
if (node[key] === null || node[key] === undefined || node[key] === '') {
delete node[key];
}
});
return node;
}
}