Files
chat/backend/app/service/PermissionCatalog.php
T
2026-08-05 15:56:08 +08:00

470 lines
18 KiB
PHP

<?php
namespace app\service;
use app\model\SysPermission;
use think\facade\Db;
/**
* 后台权限目录:目录(dir) / 菜单(menu) / 按钮(btn)
* 优先从 sys_permissions 表读取,表不存在时回退内置定义。
*/
class PermissionCatalog
{
public static function builtinTree(): array
{
return [
[
'code' => 'dir:overview',
'name' => '概览',
'type' => 'dir',
'children' => [
[
'code' => 'menu:dashboard',
'name' => '数据概览',
'type' => 'menu',
'path' => '/dashboard',
'icon' => '📊',
'children' => [],
],
],
],
[
'code' => 'dir:org',
'name' => '组织架构',
'type' => 'dir',
'children' => [
[
'code' => 'menu:users',
'name' => '用户管理',
'type' => 'menu',
'path' => '/users',
'icon' => '👥',
'children' => [
['code' => 'btn:user:create', 'name' => '新增用户', 'type' => 'btn'],
['code' => 'btn:user:edit', 'name' => '编辑用户', 'type' => 'btn'],
['code' => 'btn:user:reset_password', 'name' => '重置密码', 'type' => 'btn'],
['code' => 'btn:user:delete', 'name' => '删除用户', 'type' => 'btn'],
],
],
[
'code' => 'menu:guests',
'name' => '访客管理',
'type' => 'menu',
'path' => '/guests',
'icon' => 'visitors',
'children' => [
['code' => 'btn:guest:status', 'name' => '启用/禁用访客', 'type' => 'btn'],
['code' => 'btn:guest:delete', 'name' => '删除访客', 'type' => 'btn'],
],
],
[
'code' => 'menu:departments',
'name' => '部门管理',
'type' => 'menu',
'path' => '/departments',
'icon' => '🏢',
'children' => [
['code' => 'btn:dept:create', 'name' => '新增部门', 'type' => 'btn'],
['code' => 'btn:dept:edit', 'name' => '编辑部门', 'type' => 'btn'],
['code' => 'btn:dept:delete', 'name' => '删除部门', 'type' => 'btn'],
],
],
[
'code' => 'menu:invitations',
'name' => '邀请码管理',
'type' => 'menu',
'path' => '/invitations',
'icon' => 'ticket',
'children' => [
['code' => 'btn:invitation:create', 'name' => '生成邀请码', 'type' => 'btn'],
['code' => 'btn:invitation:revoke', 'name' => '作废邀请码', 'type' => 'btn'],
],
],
[
'code' => 'menu:roles',
'name' => '角色管理',
'type' => 'menu',
'path' => '/roles',
'icon' => '🛡️',
'children' => [
['code' => 'btn:role:create', 'name' => '新增角色', 'type' => 'btn'],
['code' => 'btn:role:edit', 'name' => '编辑角色', 'type' => 'btn'],
['code' => 'btn:role:delete', 'name' => '删除角色', 'type' => 'btn'],
],
],
],
],
[
'code' => 'dir:business',
'name' => '业务数据',
'type' => 'dir',
'children' => [
[
'code' => 'menu:conversations',
'name' => '会话管理',
'type' => 'menu',
'path' => '/conversations',
'icon' => '💬',
'children' => [
['code' => 'btn:conv:view_all', 'name' => '查看全部会话', 'type' => 'btn'],
['code' => 'btn:conv:view_subordinate', 'name' => '查看下级部门会话', 'type' => 'btn'],
],
],
[
'code' => 'menu:memberships',
'name' => '会员等级',
'type' => 'menu',
'path' => '/memberships',
'icon' => '⭐',
'children' => [
['code' => 'btn:membership:create', 'name' => '新增会员等级', 'type' => 'btn'],
['code' => 'btn:membership:edit', 'name' => '编辑会员等级', 'type' => 'btn'],
['code' => 'btn:membership:delete', 'name' => '删除会员等级', 'type' => 'btn'],
],
],
],
],
[
'code' => 'dir:system',
'name' => '系统管理',
'type' => 'dir',
'children' => [
[
'code' => 'menu:models',
'name' => 'AI 模型',
'type' => 'menu',
'path' => '/models',
'icon' => '🤖',
'children' => [
['code' => 'btn:model:create', 'name' => '新增模型', 'type' => 'btn'],
['code' => 'btn:model:edit', 'name' => '编辑模型', 'type' => 'btn'],
['code' => 'btn:model:delete', 'name' => '删除模型', 'type' => 'btn'],
['code' => 'btn:model:test', 'name' => '测试连接', 'type' => 'btn'],
],
],
[
'code' => 'menu:permissions',
'name' => '权限管理',
'type' => 'menu',
'path' => '/permissions',
'icon' => '🔑',
'children' => [
['code' => 'btn:perm:create', 'name' => '新增权限', 'type' => 'btn'],
['code' => 'btn:perm:edit', 'name' => '编辑权限', 'type' => 'btn'],
['code' => 'btn:perm:delete', 'name' => '删除权限', 'type' => 'btn'],
],
],
[
'code' => 'menu:settings',
'name' => '系统设置',
'type' => 'menu',
'path' => '/settings',
'icon' => '🔧',
'children' => [
['code' => 'btn:settings:save', 'name' => '保存设置', 'type' => 'btn'],
],
],
],
],
];
}
public static function tree(): array
{
try {
if (!self::tableReady()) {
return self::builtinTree();
}
$rows = SysPermission::order('sort_order')->order('id')->select()->toArray();
if (!$rows) {
return self::builtinTree();
}
return self::buildTreeFromRows($rows);
} catch (\Throwable $e) {
return self::builtinTree();
}
}
public static function flatList(): array
{
try {
if (!self::tableReady()) {
return self::flattenBuiltin();
}
return SysPermission::order('sort_order')->order('id')->select()->toArray();
} catch (\Throwable $e) {
return self::flattenBuiltin();
}
}
private static function tableReady(): bool
{
static $ready = null;
if ($ready !== null) {
return $ready;
}
try {
$db = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
$rows = Db::query(
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
[$db, 'sys_permissions']
);
$ready = !empty($rows);
} catch (\Throwable $e) {
$ready = false;
}
return $ready;
}
private static function buildTreeFromRows(array $rows): array
{
$byParent = [];
foreach ($rows as $row) {
$pid = $row['parent_id'] ? (int) $row['parent_id'] : 0;
$byParent[$pid][] = $row;
}
$mapNode = function (array $row) use (&$mapNode, $byParent) {
$children = [];
foreach ($byParent[(int) $row['id']] ?? [] as $child) {
$children[] = $mapNode($child);
}
return [
'id' => (int) $row['id'],
'code' => $row['code'],
'name' => $row['name'],
'type' => $row['type'],
'path' => $row['path'] ?? '',
'icon' => $row['icon'] ?? '',
'parent_id' => $row['parent_id'] ? (int) $row['parent_id'] : null,
'sort_order' => (int) ($row['sort_order'] ?? 0),
'is_system' => (int) ($row['is_system'] ?? 0),
'children' => $children,
];
};
$tree = [];
foreach ($byParent[0] ?? [] as $root) {
$tree[] = $mapNode($root);
}
return $tree;
}
private static function flattenBuiltin(): array
{
$list = [];
$walk = function (array $nodes, $parentId = null) use (&$walk, &$list) {
foreach ($nodes as $i => $node) {
$id = count($list) + 1;
$list[] = [
'id' => $id,
'type' => $node['type'],
'code' => $node['code'],
'name' => $node['name'],
'parent_id' => $parentId,
'path' => $node['path'] ?? null,
'icon' => $node['icon'] ?? null,
'sort_order' => $i,
'is_system' => 1,
];
if (!empty($node['children'])) {
$walk($node['children'], $id);
}
}
};
$walk(self::builtinTree());
return $list;
}
public static function emptyPermissions(): array
{
$perms = [
'can_access_admin' => false,
'dirs' => [],
'menus' => [],
'buttons' => [],
];
foreach (self::legacyKeys() as $key) {
$perms[$key] = false;
}
return $perms;
}
public static function fullPermissions(): array
{
$dirs = [];
$menus = [];
$buttons = [];
foreach (self::tree() as $dir) {
$dirs[] = $dir['code'];
foreach ($dir['children'] ?? [] as $menu) {
$menus[] = $menu['code'];
foreach ($menu['children'] ?? [] as $btn) {
$buttons[] = $btn['code'];
}
}
}
$perms = [
'can_access_admin' => true,
'dirs' => $dirs,
'menus' => $menus,
'buttons' => $buttons,
];
foreach (self::legacyKeys() as $key) {
$perms[$key] = true;
}
return $perms;
}
public static function legacyKeys(): array
{
return [
'can_manage_users',
'can_manage_roles',
'can_manage_departments',
'can_view_all_conversations',
'can_view_subordinate_conversations',
'can_manage_models',
'can_manage_settings',
'can_manage_memberships',
'can_manage_permissions',
];
}
public static function normalize(array $input): array
{
$base = self::emptyPermissions();
$dirs = array_values(array_unique(array_filter((array) ($input['dirs'] ?? []))));
$menus = array_values(array_unique(array_filter((array) ($input['menus'] ?? []))));
$buttons = array_values(array_unique(array_filter((array) ($input['buttons'] ?? []))));
if (empty($menus) && empty($buttons) && empty($dirs)) {
[$dirs, $menus, $buttons] = self::fromLegacy($input);
}
$perms = array_merge($base, [
'can_access_admin' => !empty($input['can_access_admin']),
'dirs' => $dirs,
'menus' => $menus,
'buttons' => $buttons,
]);
return self::syncLegacyFlags($perms);
}
public static function syncLegacyFlags(array $perms): array
{
$menus = $perms['menus'] ?? [];
$buttons = $perms['buttons'] ?? [];
$perms['can_manage_users'] = in_array('menu:users', $menus, true);
$perms['can_manage_roles'] = in_array('menu:roles', $menus, true);
$perms['can_manage_departments'] = in_array('menu:departments', $menus, true);
$perms['can_manage_models'] = in_array('menu:models', $menus, true);
$perms['can_manage_settings'] = in_array('menu:settings', $menus, true);
$perms['can_manage_memberships'] = in_array('menu:memberships', $menus, true);
$perms['can_manage_permissions'] = in_array('menu:permissions', $menus, true);
$perms['can_view_all_conversations'] = in_array('btn:conv:view_all', $buttons, true);
$perms['can_view_subordinate_conversations'] = in_array('btn:conv:view_subordinate', $buttons, true);
if (in_array('menu:conversations', $menus, true)
&& empty($perms['can_view_all_conversations'])
&& empty($perms['can_view_subordinate_conversations'])) {
$perms['can_view_subordinate_conversations'] = true;
if (!in_array('btn:conv:view_subordinate', $buttons, true)) {
$perms['buttons'][] = 'btn:conv:view_subordinate';
}
}
return $perms;
}
private static function fromLegacy(array $input): array
{
$dirs = [];
$menus = [];
$buttons = [];
$map = [
'can_manage_users' => ['dir:org', 'menu:users', ['btn:user:create', 'btn:user:edit', 'btn:user:reset_password', 'btn:user:delete']],
'can_manage_departments' => ['dir:org', 'menu:departments', ['btn:dept:create', 'btn:dept:edit', 'btn:dept:delete']],
'can_manage_roles' => ['dir:org', 'menu:roles', ['btn:role:create', 'btn:role:edit', 'btn:role:delete']],
'can_manage_memberships' => ['dir:business', 'menu:memberships', ['btn:membership:create', 'btn:membership:edit', 'btn:membership:delete']],
'can_manage_models' => ['dir:system', 'menu:models', ['btn:model:create', 'btn:model:edit', 'btn:model:delete', 'btn:model:test']],
'can_manage_settings' => ['dir:system', 'menu:settings', ['btn:settings:save']],
'can_manage_permissions' => ['dir:system', 'menu:permissions', ['btn:perm:create', 'btn:perm:edit', 'btn:perm:delete']],
];
foreach ($map as $key => [$dir, $menu, $btns]) {
if (!empty($input[$key])) {
$dirs[] = $dir;
$menus[] = $menu;
$buttons = array_merge($buttons, $btns);
}
}
if (!empty($input['can_view_all_conversations']) || !empty($input['can_view_subordinate_conversations'])) {
$dirs[] = 'dir:business';
$menus[] = 'menu:conversations';
if (!empty($input['can_view_all_conversations'])) {
$buttons[] = 'btn:conv:view_all';
}
if (!empty($input['can_view_subordinate_conversations'])) {
$buttons[] = 'btn:conv:view_subordinate';
}
}
if (!empty($input['can_access_admin'])) {
$dirs[] = 'dir:overview';
$menus[] = 'menu:dashboard';
}
return [
array_values(array_unique($dirs)),
array_values(array_unique($menus)),
array_values(array_unique($buttons)),
];
}
public static function hasCode(array $perms, string $code): bool
{
if ($code === 'can_access_admin') {
return !empty($perms['can_access_admin']);
}
if (str_starts_with($code, 'dir:')) {
return in_array($code, $perms['dirs'] ?? [], true);
}
if (str_starts_with($code, 'menu:')) {
return in_array($code, $perms['menus'] ?? [], true);
}
if (str_starts_with($code, 'btn:')) {
return in_array($code, $perms['buttons'] ?? [], true);
}
return !empty($perms[$code]);
}
public static function menuMeta(): array
{
$items = [];
foreach (self::tree() as $dir) {
foreach ($dir['children'] ?? [] as $menu) {
$items[] = [
'id' => $menu['id'] ?? null,
'code' => $menu['code'],
'name' => $menu['name'],
'path' => $menu['path'] ?? '',
'icon' => $menu['icon'] ?? '',
'dir' => $dir['code'],
'dirName' => $dir['name'],
];
}
}
return $items;
}
}