feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
273
packages/shared-ts/src/permission-bitmap.ts
Normal file
273
packages/shared-ts/src/permission-bitmap.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 权限位图编解码(对齐 CICD 项目 permission-bitmap.ts)
|
||||
*
|
||||
* 核心价值:将 N 个权限点压缩为 base36 字符串,JWT cookie 体积减少 ~99%。
|
||||
* - 67 权限点数组(JSON ~1.1KB)→ base36 字符串(~14 字符)
|
||||
* - Edge Runtime 单点检查 hasPermissionInBitmap 无需完整解码
|
||||
*
|
||||
* 关键约束(不可破坏):
|
||||
* - PERMISSION_BITMAP_ORDER 顺序一经确定不可变,新增权限只能追加末尾
|
||||
* - 因 Number 仅支持 53 bit 精度,使用 BigInt 实现
|
||||
* - 未知权限静默忽略,无效字符返回空数组
|
||||
*
|
||||
* 编码原理:
|
||||
* - 每个权限点对应一个 bit 位(按 ORDER 数组下标)
|
||||
* - permissions 数组 → BigInt(每个有权限的 bit 置 1)→ base36 字符串
|
||||
* - 解码:base36 → BigInt → 遍历 ORDER,bit 为 1 的权限加入结果
|
||||
*
|
||||
* 关联:project_rules §3.1(前端禁止 role === "xxx" 硬编码)、
|
||||
* portal-shell README v2.0 §3.3 三层安全边界
|
||||
*/
|
||||
|
||||
/**
|
||||
* 权限点位图顺序(一经确定不可变,新增只能追加末尾)
|
||||
*
|
||||
* 命名规范:`<RESOURCE>_<ACTION>`(F7 裁决)
|
||||
* 数据范围后缀:`_OWN`/`_CHILD`(如 `GRADE_READ_CHILD`)
|
||||
*
|
||||
* 注意:顺序变更会破坏所有已签发的 JWT cookie,只能在版本升级时追加。
|
||||
*/
|
||||
export const PERMISSION_BITMAP_ORDER = [
|
||||
// ── 仪表盘(5)─────────────────────────────────────────
|
||||
"DASHBOARD_ADMIN_READ",
|
||||
"DASHBOARD_TEACHER_READ",
|
||||
"DASHBOARD_STUDENT_READ",
|
||||
"DASHBOARD_PARENT_READ",
|
||||
"DASHBOARD_READ",
|
||||
|
||||
// ── 用户管理(4)──────────────────────────────────────
|
||||
"USER_MANAGE",
|
||||
"USER_CREATE",
|
||||
"USER_UPDATE",
|
||||
"USER_DELETE",
|
||||
|
||||
// ── 角色权限(4)──────────────────────────────────────
|
||||
"ROLE_READ",
|
||||
"ROLE_MANAGE",
|
||||
"PERMISSION_READ",
|
||||
"PERMISSION_MANAGE",
|
||||
|
||||
// ── 审计与邀请(3)────────────────────────────────────
|
||||
"AUDIT_LOG_READ",
|
||||
"INVITATION_CODE_MANAGE",
|
||||
"INVITATION_CODE_CREATE",
|
||||
|
||||
// ── 学校设置(2)──────────────────────────────────────
|
||||
"SCHOOL_READ",
|
||||
"SCHOOL_MANAGE",
|
||||
|
||||
// ── 班级与年级(4)────────────────────────────────────
|
||||
"CLASS_READ",
|
||||
"CLASS_MANAGE",
|
||||
"GRADE_READ",
|
||||
"GRADE_MANAGE",
|
||||
|
||||
// ── 考试(5)──────────────────────────────────────────
|
||||
"EXAM_READ",
|
||||
"EXAM_CREATE",
|
||||
"EXAM_UPDATE",
|
||||
"EXAM_DELETE",
|
||||
"EXAM_GRADE",
|
||||
|
||||
// ── 作业(4)──────────────────────────────────────────
|
||||
"HOMEWORK_READ",
|
||||
"HOMEWORK_CREATE",
|
||||
"HOMEWORK_SUBMIT",
|
||||
"HOMEWORK_GRADE",
|
||||
|
||||
// ── 成绩(5)──────────────────────────────────────────
|
||||
"GRADE_READ",
|
||||
"GRADE_READ_OWN",
|
||||
"GRADE_READ_CHILD",
|
||||
"GRADE_RECORD_MANAGE",
|
||||
"GRADE_RECORD_READ",
|
||||
|
||||
// ── 考勤(3)──────────────────────────────────────────
|
||||
"ATTENDANCE_READ",
|
||||
"ATTENDANCE_MANAGE",
|
||||
"ATTENDANCE_RECORD",
|
||||
|
||||
// ── 课表与排课(4)────────────────────────────────────
|
||||
"SCHEDULE_READ",
|
||||
"SCHEDULE_AUTO",
|
||||
"SCHEDULE_ADJUST",
|
||||
"SCHEDULE_MANAGE",
|
||||
|
||||
// ── 备课与教材(5)────────────────────────────────────
|
||||
"LESSON_PLAN_READ",
|
||||
"LESSON_PLAN_CREATE",
|
||||
"LESSON_PLAN_UPDATE",
|
||||
"LESSON_PLAN_DELETE",
|
||||
"TEXTBOOK_READ",
|
||||
|
||||
// ── 题库(4)──────────────────────────────────────────
|
||||
"QUESTION_READ",
|
||||
"QUESTION_CREATE",
|
||||
"QUESTION_UPDATE",
|
||||
"QUESTION_DELETE",
|
||||
|
||||
// ── 学情诊断(2)──────────────────────────────────────
|
||||
"DIAGNOSTIC_READ",
|
||||
"DIAGNOSTIC_MANAGE",
|
||||
|
||||
// ── 选修课(3)────────────────────────────────────────
|
||||
"ELECTIVE_READ",
|
||||
"ELECTIVE_MANAGE",
|
||||
"ELECTIVE_SELECT",
|
||||
|
||||
// ── 错题本与学习路径(2)──────────────────────────────
|
||||
"ERROR_BOOK_READ",
|
||||
"LEARNING_PATH_READ",
|
||||
|
||||
// ── AI 辅导(2)───────────────────────────────────────
|
||||
"AI_CHAT",
|
||||
"AI_TUTOR_USE",
|
||||
|
||||
// ── 公告与消息(4)────────────────────────────────────
|
||||
"ANNOUNCEMENT_READ",
|
||||
"ANNOUNCEMENT_MANAGE",
|
||||
"MESSAGE_READ",
|
||||
"MESSAGE_SEND",
|
||||
|
||||
// ── 请假(2)──────────────────────────────────────────
|
||||
"LEAVE_REQUEST_CREATE",
|
||||
"LEAVE_APPROVAL_MANAGE",
|
||||
|
||||
// ── 插件与布局(4)────────────────────────────────────
|
||||
"PLUGIN_REGISTRY_READ",
|
||||
"PLUGIN_REGISTRY_MANAGE",
|
||||
"LAYOUT_TEMPLATE_MANAGE",
|
||||
"ROLE_LAYOUT_MANAGE",
|
||||
] as const;
|
||||
|
||||
/** 权限点类型(从 ORDER 数组推导) */
|
||||
export type Permission = (typeof PERMISSION_BITMAP_ORDER)[number];
|
||||
|
||||
/** 权限点 → bit 位映射表(启动时构建一次) */
|
||||
const PERMISSION_BIT_INDEX: ReadonlyMap<string, bigint> = new Map(
|
||||
PERMISSION_BITMAP_ORDER.map((perm, idx) => [perm, 1n << BigInt(idx)]),
|
||||
);
|
||||
|
||||
/**
|
||||
* 将权限点数组编码为 base36 字符串
|
||||
*
|
||||
* @example
|
||||
* encodePermissionsBitmap(["USER_MANAGE", "ROLE_READ"])
|
||||
* // => "j" (前 14 个权限点中 USER_MANAGE=bit5, ROLE_READ=bit9 → 0b10100100000 → base36="j")
|
||||
*/
|
||||
export function encodePermissionsBitmap(
|
||||
permissions: readonly string[],
|
||||
): string {
|
||||
let bits = 0n;
|
||||
for (const perm of permissions) {
|
||||
const bit = PERMISSION_BIT_INDEX.get(perm);
|
||||
if (bit !== undefined) {
|
||||
bits |= bit;
|
||||
}
|
||||
// 未知权限静默忽略
|
||||
}
|
||||
return bits.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 base36 字符串解码为权限点数组
|
||||
*
|
||||
* 容错:无效字符返回空数组,未知 bit 位静默忽略
|
||||
*/
|
||||
export function decodePermissionsBitmap(bitmap: string): Permission[] {
|
||||
if (!bitmap || !/^[0-9a-z]+$/.test(bitmap)) {
|
||||
return [];
|
||||
}
|
||||
const bits = parseBase36BigInt(bitmap);
|
||||
if (bits === null) {
|
||||
return [];
|
||||
}
|
||||
const result: Permission[] = [];
|
||||
for (let i = 0; i < PERMISSION_BITMAP_ORDER.length; i++) {
|
||||
const bit = 1n << BigInt(i);
|
||||
if ((bits & bit) !== 0n) {
|
||||
result.push(PERMISSION_BITMAP_ORDER[i]!);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单点权限检查(不解码整个位图,性能优)
|
||||
*
|
||||
* 适用场景:Edge Runtime / middleware / proxy 等高频检查
|
||||
*
|
||||
* @example
|
||||
* hasPermissionInBitmap("j", "USER_MANAGE") // true
|
||||
* hasPermissionInBitmap("j", "EXAM_CREATE") // false
|
||||
*/
|
||||
export function hasPermissionInBitmap(
|
||||
bitmap: string,
|
||||
permission: string,
|
||||
): boolean {
|
||||
const bit = PERMISSION_BIT_INDEX.get(permission);
|
||||
if (bit === undefined) {
|
||||
return false; // 未知权限点
|
||||
}
|
||||
const bits = parseBase36BigInt(bitmap);
|
||||
if (bits === null) {
|
||||
return false;
|
||||
}
|
||||
return (bits & bit) !== 0n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量权限检查(任一满足即 true)
|
||||
*/
|
||||
export function hasAnyPermissionInBitmap(
|
||||
bitmap: string,
|
||||
permissions: readonly string[],
|
||||
): boolean {
|
||||
return permissions.some((p) => hasPermissionInBitmap(bitmap, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量权限检查(全部满足才 true)
|
||||
*/
|
||||
export function hasAllPermissionsInBitmap(
|
||||
bitmap: string,
|
||||
permissions: readonly string[],
|
||||
): boolean {
|
||||
return permissions.every((p) => hasPermissionInBitmap(bitmap, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 base36 字符串为 BigInt
|
||||
*
|
||||
* base36 字符集:0-9, a-z(小写)
|
||||
* 实现原理:从高位到低位逐字符累加
|
||||
*/
|
||||
function parseBase36BigInt(str: string): bigint | null {
|
||||
if (!str) return 0n;
|
||||
let result = 0n;
|
||||
const base = 36n;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str[i]!;
|
||||
let digit: number;
|
||||
if (ch >= "0" && ch <= "9") {
|
||||
digit = ch.charCodeAt(0) - 48; // '0' = 48
|
||||
} else if (ch >= "a" && ch <= "z") {
|
||||
digit = ch.charCodeAt(0) - 87; // 'a' = 97, 97-10=87
|
||||
} else if (ch >= "A" && ch <= "Z") {
|
||||
digit = ch.charCodeAt(0) - 55; // 'A' = 65, 65-10=55
|
||||
} else {
|
||||
return null; // 非法字符
|
||||
}
|
||||
result = result * base + BigInt(digit);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验权限点是否在位图顺序表中
|
||||
*
|
||||
* 用于开发时校验 manifest 声明的权限点是否合法
|
||||
*/
|
||||
export function isValidPermission(permission: string): boolean {
|
||||
return PERMISSION_BIT_INDEX.has(permission);
|
||||
}
|
||||
Reference in New Issue
Block a user