470 lines
18 KiB
PHP
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;
|
|
}
|
|
}
|