Appearance
权限与记账 API 对齐:改造清单与表结构说明
目标:同一套 sys_permissions(及角色绑定)既控制「入口是否可见」,也控制「对应 HTTP API 是否可调用」;与「每个功能当一个应用、后台统一配置、按角色赋权」的设想一致。
下文基于当前仓库实现(MeRoutesService.resolveAllowedPermissionIds、resolveGatewayAuth + assertSiteScope、ZenStack 数据规则)。
一、现状(简要)
| 层级 | 行为 |
|---|---|
| PC 菜单 / 路由 | platform=pc 的 Permission 树 + 角色勾选 → me 路由接口拼侧栏。 |
| 小程序首页应用 | platform=mobile 且 route 以 /pages/ 开头 + 角色勾选 → POST /sys/me/mobile-shell-apps。 |
| 记账 acc 接口 | 已在入/出/生产、档案(商品/合作伙伴/工种/结算等)、库存流水等接口使用 assertAccReadPermission / assertAccWritePermission + assertSiteScope;详见 permission-matrix.md。 |
| PC Web 路由 | 已:动态 addRoute + beforeEach 对照 allowedRoutePaths(含参数路径);403 文案 getApiErrorMessage。 |
| Acc 路由 Guard(阶段 3 子集) | 已:acc-route-permission-registry + AccRoutePermissionMiddleware;permissionCodes + 未登记默认 service 兜底。 |
| Sys 路由 Guard(阶段 3 子集) | 已:sys-route-permission-registry + SysRoutePermissionMiddleware;/auth/* 跳过;管理端路由 admin。 |
| 冲红 / ADMIN 403 结构 | 已:assertCanCorrectWipProduction、assertAdminRole 使用 PERMISSION_DENIED。 |
| 角色分配 API | 已:RolesService.assignUserToRole / removeUserFromRole 调用 assertAdmin(与 Sys Guard admin 双保险)。 |
| 会话与 claims 缓存 | 已:管理端变更后 Redis 会话失效 + bumpGatewayClaimsCacheVersion;网关/JWT 用 loadUserGatewayClaimsCached(见 GATEWAY_CLAIMS_CACHE*)。 |
| 403 审计 | 已:Acc/Sys 路由中间件对 PERMISSION_DENIED 打 warn(permission-denied-audit.ts)。 |
| 待办(可选) | DB 驱动 apiPattern;@RequirePermissions 生成 registry。 |
| 子页面角色 | 小程序工作台用 component → miniRole 写本地会话,属 端上体验约束;API 已按 permissionCodes 拦截,子页宜逐步改 useAccPermissions(阶段 5)。 |
入口与 API:PC 菜单、Acc/Sys 写接口、小程序首页格子已 largely 对齐 Permission.code;未同源部分主要是小程序子页 miniRole 与阶段 0 产品约定(祖先 id 展开 vs API 叶节点)。
二、现有表结构(怎么用)
1. sys_permissions(模型名 Permission)
| 字段 | 用途(改造后仍成立) |
|---|---|
id | 主键;角色绑定、缓存集合都可用 id。 |
code | 全局唯一、稳定的能力标识(建议作为 API 鉴权主键之一,与前端/网关常量对齐)。 |
name | 展示名。 |
type | directory | menu | button(可扩展约定,见下文「可选扩展」)。 |
platform | pc | mobile:控制出现在哪棵菜单树;API 鉴权与 platform 解耦(见「能力节点」)。 |
route | PC 路由或小程序路径;不用于 acc REST 路径匹配(避免把 UI 路径和 API 绑死)。 |
component | PC 组件键;小程序上可作 miniRole 等 纯 UI 元数据。 |
parentId / sort | 树与排序。 |
重要:resolveAllowedPermissionIds 在角色勾选某节点后,会把 从该节点沿 parentId 直到根 的祖先 id 一并加入 allowedIds(用于菜单展示继承)。
API 鉴权建议:用 code 集合 或 「仅直接勾选的能力叶节点」 二选一,避免「勾了子菜单却隐含父级 API 全放行」的歧义;若采用「仅叶节点」,需在文档与后台 UI 上写清楚。
2. sys_role_permission_links(RolePermissionLink)
roleId+permissionId:角色拥有哪些权限节点。- 改造用法:登录或刷新会话时,据此解析出用户的
permissionId集合 或code集合,供网关 / Guard 使用。
3. sys_role_assignments(RoleAssignment)
- 用户 ↔ 角色;多角色权限取并集(与现有
resolveAllowedPermissionIds一致)。
4. sys_site_memberships(SiteMembership)
- 数据范围(站点);与 能力权限(能做什么) 正交:
- 站点:继续用现有
assertSiteScope+ JWTsiteIds。 - 能力:用
Permission+ 角色绑定。
- 站点:继续用现有
- 改造后仍是:先有站点范围,再校验是否具备该站点上的某 API 能力(顺序不要反)。
5. JWT / 网关上下文(JwtPayload / resolveGatewayAuth)
当前常见字段:userId、siteIds、roleCodes。
改造选项(择一或组合):
- 登录签发 JWT 时 写入
permissionCodes: string[]或permissionIds: string[](由RolePermissionLink聚合,注意 token 体积与更新时机)。 - 不在 JWT 放全量:每次请求在 网关或 acc 进程 用
userId+roleCodes查缓存(Redis)中的权限集合(需失效策略:改角色、改权限树时清缓存)。
三、目标架构(原则)
- 每个可独立售卖/赋权的能力 对应至少一个
Permission.code(可与现有menu_acc_inbound等对齐,或新增api:acc:inbound:page等专用码)。 - 每个 acc(及未来需细控的 sys)接口 声明所需 一个或多个
code;未满足则 403。 ADMIN/ 超管:保持与MeRoutesService一致,视为拥有全部权限节点(或单独SUPER_ADMIN旁路)。- ZenStack
@@allow:保留为 数据行级 约束;接口级 用权限 Guard,两层互补。 - 小程序:首页格子仍用
platform=mobile;同一code也可被 PC 复用,或 PC 用 menu 行、API 用同code的「逻辑节点」(见阶段 1)。
四、分阶段改造清单
阶段 0:约定与文档(1~2 天)
- [x] 定稿
Permission.code命名规范:沿用menu_acc_*/btn_acc_*/mobile_*,见 permission-code-conventions.md。 - [x] 鉴权主键:API / 网关 / 按钮用
code;PC 菜单可见用id(含祖先展开)。 - [x] 菜单与 API:读能力用
menu_*;写过账用子级btn_*_write;小程序入口用mobile_*+ 写按钮码。 - [x] 权限管理后台 文案:Web
PermissionsAdminView顶部说明 + 编辑弹窗提示。
阶段 1:能力节点入库(与现有菜单对齐)(2~5 天)
- [x] 盘点所有
/acc/*(及需管控的/sys/*)路由,列出 方法 + 路径 → 所需 capability(见acc-route-permission-registry.ts/sys-route-permission-registry.ts+ permission-matrix.md)。 - [x] 种子与
Permission.code对齐(seed-sys-admin-permissions.mjs);写操作须显式btn_*。 - [x] 默认角色种子:预置模板见 permission-role-templates.md,同步脚本
db:sync-roles-perms。
阶段 2:解析服务(1~3 天)
- [x]
resolveUserPermissionCodes(permission-resolver.ts):API 鉴权用 code 集合入口;菜单 id 仍走MeRoutesService.resolveAllowedPermissionIds。 - [x] 菜单与 API 叶节点策略 产品定稿 + 单测(多角色并集、仅
btn_*不隐含menu_*;见 permission-leaf-policy.md、permission-menu-leaf.spec.ts)。
阶段 3:请求侧鉴权(3~7 天)
方案 A(推荐先做):在 acc / sys 应用全局中间件 中(子集已落地):
- [x] 维护 registry(
acc-route-permission-registry.ts、sys-route-permission-registry.ts)+AccRoutePermissionMiddleware/SysRoutePermissionMiddleware。 - [x] Service 层
assertSiteScope/assertAcc*/assertAdmin与 Guard 双保险;403 统一PERMISSION_DENIED。 - [x] 登记 全部 Acc/Sys
@Post路由(pnpm --filter taskflow-backend run check:route-registry);未映射项在预发/生产设ACC_ROUTE_GUARD_STRICT=true、SYS_ROUTE_GUARD_STRICT=true(见backend/.env.example)。 - [x] 阶段 A:Acc
@Post仅维护@AccRoutePermission;codegen:acc-route-registry生成acc-route-permission-registry.ts,check:route-registry校验一致;Guard 仍读 registry。 - [x] 阶段 A(Sys):Sys
@Post仅维护@SysRoutePermission;codegen:sys-route-registry生成sys-route-permission-registry.ts(含全部/auth/*skip 登记);check:route-registry校验一致。
方案 B:在 网关 对 /api/acc/* 做白名单映射(集中配置,acc 进程无重复逻辑),适合多语言栈一致;缺点是网关配置与 Nest 路由 双处维护,需同步机制。
- [ ] 网关 JWT 透传 已具备时,Guard 在 sys/acc 均可访问同一
user负载。
阶段 4:登录与缓存(1~3 天)
- [x] JWT 签发仍携带
permissionCodes/roles/siteIds(登录快照);鉴权请求由loadUserGatewayClaims从库实时加载(JwtStrategy、网关转发 Acc/Sys)。 - [x] PC:
POST /sys/me/auth-claims+authStore.refreshAuthClaims(导航时合并按钮态)。 - [x] Redis 会话失效:
bindPermissions、权限save/remove、角色/用户管理变更后invalidateSessions*+bumpGatewayClaimsCacheVersion(须重登或等待 TTL)。 - [x] Claims 短缓存:
loadUserGatewayClaimsCached(GATEWAY_CLAIMS_CACHE、GATEWAY_CLAIMS_CACHE_TTL);网关GATEWAY_SESSION_CHECK可灰度关闭会话键校验。 - [x] 小程序:
fetchMeAuthClaims+useAccPermissions;业务页getMiniappApiErrorMessage/useMiniappApiToast。
阶段 5:小程序与 PC 对齐(2~4 天)
- [x] 子页门禁(子集):
useMiniappPageGate+acc-permissions.ts已接写操作/记工/结算/审核等页;首页格子permissionCodes过滤与点击校验见miniapp-shell-permission.ts(miniRole仅作 tab/路径体验)。 - [x] PC:无权限路由 →
/no-permission(beforeEach)。 - [ ] E2E 自动化(Playwright
e2e/已有 smoke;权限 1–13 仍建议预发手工表)。
阶段 6:观测与收尾(持续)
- [x] 403 响应体:
PERMISSION_DENIED+requiredPermissions(Acc/Sys/网关)。 - [x] 路由 Guard 拒绝审计:中间件
warn日志(可接日志平台检索[PERMISSION_DENIED])。 - [ ] 文档:运维/实施 新租户配角色(权限树截图 + API 矩阵);见 permission-maintenance.md。
五、可选 schema / 类型扩展(非必须)
若希望 不在代码里维护 path → permission 映射表:
- 在
Permission上增加可选字段,例如apiPattern(JSON:{ "method": "POST", "pathGlob": "/acc/inbound/page" }),由 数据驱动 Guard; - 或将
type扩展为api,与menu并列,platform可为all表示与终端无关。
此类改动涉及 ZenStack schema、Prisma migrate、后台权限表单,建议放在 阶段 1 之后 单独评审。
六、与「每个功能当一个应用」的对应关系
| 概念 | 建议落地 |
|---|---|
| 应用 | 权限树中的一个 目录 + 一组子节点(PC mobile 各一套或共用 code)。 |
| 功能入口 | type=menu,platform 区分端。 |
| 功能 API | 与 同一业务域 的 code 绑定;可一对多(一个菜单对应多个 API code)。 |
| 角色 | 现有 Role + RolePermissionLink,无需新表。 |
七、风险与注意点
- Token 体积:权限 code 很多时放 JWT 可能超长,优先考虑 Redis + userId 或 只放 hash/version。
- 性能:每请求查库不可接受,必须 缓存。
- 向后兼容:上线初期可用 特性开关:
ACC_PERMISSION_GUARD=false时仅打日志不拒绝。 - ZenStack:接口 Guard 与
@@allow同时存在时,避免规则互相矛盾(文档中写明优先级:先接口权限,再数据行级)。
八、参考代码位置(便于落地时跳转)
| 说明 | 路径 |
|---|---|
| 用户可见权限 id 解析 | backend/src/apps/sys/modules/me/me-routes.service.ts → resolveAllowedPermissionIds |
| 小程序应用列表 | 同上 → listMobileShellApps |
| JWT 用户上下文 | backend/src/libs/shared/auth/resolve-gateway-auth.ts |
| 站点范围 | backend/src/libs/shared/auth/assert-site-scope.ts |
| 权限模型 | backend/zenstack/schema.zmodel → Permission、RolePermissionLink、RoleAssignment |
| 种子菜单 | backend/scripts/seed-sys-admin-permissions.mjs |
| 三处维护说明 | permission-maintenance.md |
| Claims 缓存 | user-gateway-claims-cache.ts |
| 拒绝审计 | permission-denied-audit.ts |
九、已决议方案(当前实现)
- 方案 A:Acc/Sys registry + 全局中间件 + Service
assert*双保险;未登记默认放行,预发/生产开 严格模式。 - 鉴权数据:请求侧以
loadUserGatewayClaims/ Cached 为准,不信任 JWT 内权限快照;管理端改权限后会话失效 + claims 版本递增。 - 网关:校验 JWT +(可选)Redis 会话键,转发
x-user-*头;不在网关维护 path→permission 表(避免与 Nest 双处维护)。 - ZenStack:先接口权限、再行级
@@allow(见下文第七节)。