898fae4cb3
用户管理菜单权限此前只含后台 admin(单类型):前端授权树/回显仅拉 menuType=admin, 后端 replaceDirectPermissions 也固定用 ADMIN 作用域写。现改为同时覆盖 后台(admin)+前端客户端(app): - 前端 user-menu-auth 节点携带 type,api 并行拉取 admin/app 两类可授权树与已授权 id 合并去重,树内两类根并存(账号与权限/视频/前端工具/运营工具/后勤工具等) - 后端 PermissionMenuService 新增 resolveMenuTypes(按 id 解析类型);AdminUserService create/update 把勾选 id 拆 admin/app 分区整树替换(internal 不纳入) - 前端 1556 测试全绿 + build 通过;后端单测(AdminUserServiceTest/PermissionMenuServiceTest/Controller/PermissionMenuResolveTypeTest) EXIT 0 - 本地 E2E:创建用户写入 admin=7+app=3 → 按类型回读 admin[7]/app[3],删除正常
108 lines
4.2 KiB
TypeScript
108 lines
4.2 KiB
TypeScript
/** 用户菜单授权数据解析模型(任务 49):permission-menus 扁平项 → 嵌套树 + 用户已授权 id。 */
|
||
import { unwrap } from '../../api/envelope.ts'
|
||
|
||
/** 后台 Vue 控制台菜单类型(与 /api/admin/current-user/menus 一致)。 */
|
||
export const ADMIN_MENU_TYPE = 'admin'
|
||
/** 前端客户端菜单类型(桌面端功能模块,如视频/前端工具/运营工具/后勤工具)。 */
|
||
export const APP_MENU_TYPE = 'app'
|
||
|
||
/** 授权用可点选菜单节点(携带后端 numeric column id,用于提交授权)。 */
|
||
export interface MenuOptionNode {
|
||
id: number
|
||
/** column_key,稳定权限标识;缺省用 id 字符串兜底。 */
|
||
key: string
|
||
name: string
|
||
sort: number
|
||
parentId: number | null
|
||
/** 所属菜单类型(admin 后台 / app 前端客户端),提交时按类型分区落库。 */
|
||
type: string
|
||
children?: MenuOptionNode[]
|
||
}
|
||
|
||
function numberOrNull(value: unknown): number | null {
|
||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
|
||
}
|
||
|
||
/** 把单条 PermissionMenuItemVo(snake_case) 归一为授权节点;缺 id 视为无效。 */
|
||
export function parsePermissionMenuItem(raw: unknown, type = ''): MenuOptionNode | null {
|
||
if (!raw || typeof raw !== 'object') return null
|
||
const record = raw as Record<string, unknown>
|
||
const id = numberOrNull(record.id)
|
||
if (id === null) return null
|
||
const parentId = numberOrNull(record.parent_id)
|
||
const sortRaw = numberOrNull(record.sort_order)
|
||
const columnKey = typeof record.column_key === 'string' ? record.column_key.trim() : ''
|
||
return {
|
||
id,
|
||
key: columnKey || String(id),
|
||
name: typeof record.name === 'string' && record.name.trim() ? record.name.trim() : '未命名菜单',
|
||
sort: sortRaw === null ? 0 : sortRaw,
|
||
parentId: parentId === null ? null : parentId,
|
||
type,
|
||
}
|
||
}
|
||
|
||
/** 归一化 permission-menus 响应(信封或裸数组)为扁平授权节点列表。 */
|
||
export function parseMenuOptionList(payload: unknown, type = ''): MenuOptionNode[] {
|
||
const data = unwrap<unknown>(payload)
|
||
if (!Array.isArray(data)) return []
|
||
return data
|
||
.map((raw) => parsePermissionMenuItem(raw, type))
|
||
.filter((node): node is MenuOptionNode => node !== null)
|
||
}
|
||
|
||
/** 按 parent_id 组装嵌套树;同级按 (sort,key) 升序,父缺失的节点按根处理。 */
|
||
export function buildMenuOptionTree(items: MenuOptionNode[]): MenuOptionNode[] {
|
||
const nodes = new Map<number, MenuOptionNode>()
|
||
for (const item of items) {
|
||
nodes.set(item.id, { ...item, children: [] })
|
||
}
|
||
const roots: MenuOptionNode[] = []
|
||
for (const item of items) {
|
||
const node = nodes.get(item.id)
|
||
if (!node) continue
|
||
const parent = item.parentId === null ? undefined : nodes.get(item.parentId)
|
||
if (parent) {
|
||
parent.children!.push(node)
|
||
} else {
|
||
roots.push(node)
|
||
}
|
||
}
|
||
const order = (a: MenuOptionNode, b: MenuOptionNode) =>
|
||
a.sort - b.sort || a.key.localeCompare(b.key)
|
||
const sortRecursive = (nodes: MenuOptionNode[]) => {
|
||
nodes.sort(order)
|
||
for (const node of nodes) node.children?.length && sortRecursive(node.children)
|
||
}
|
||
sortRecursive(roots)
|
||
for (const item of items) {
|
||
const node = nodes.get(item.id)
|
||
if (node && !node.children?.length) delete node.children
|
||
}
|
||
return roots
|
||
}
|
||
|
||
/** 解包用户已授权 columnIds(data.columnIds)→ 去重升序正整数。 */
|
||
export function parseUserGrantedColumnIds(payload: unknown): number[] {
|
||
const data = unwrap<unknown>(payload)
|
||
const record = data && typeof data === 'object' ? (data as Record<string, unknown>) : null
|
||
if (!record || !Array.isArray(record.columnIds)) return []
|
||
return normalizeMenuIds(record.columnIds)
|
||
}
|
||
|
||
/** 任意输入 id 集合归一为去重升序正整数(镜像后端 normalizeColumnIds)。 */
|
||
export function normalizeMenuIds(raw: unknown): number[] {
|
||
if (!Array.isArray(raw)) return []
|
||
const ids = new Set<number>()
|
||
for (const value of raw) {
|
||
const id = numberOrNull(value)
|
||
if (id !== null && id >= 1) ids.add(id)
|
||
}
|
||
return [...ids].sort((a, b) => a - b)
|
||
}
|
||
|
||
/** 提交授权请求体(任务 50):{columnIds} 与 Java UserColumnPermissionUpdateRequest 对齐。 */
|
||
export function buildUserColumnPermissionPayload(raw: unknown): { columnIds: number[] } {
|
||
return { columnIds: normalizeMenuIds(raw) }
|
||
}
|