Skip to content

权限与记账 API 对齐:改造清单与表结构说明

目标:同一套 sys_permissions(及角色绑定)既控制「入口是否可见」,也控制「对应 HTTP API 是否可调用」;与「每个功能当一个应用、后台统一配置、按角色赋权」的设想一致。

下文基于当前仓库实现(MeRoutesService.resolveAllowedPermissionIdsresolveGatewayAuth + assertSiteScope、ZenStack 数据规则)。


一、现状(简要)

层级行为
PC 菜单 / 路由platform=pcPermission 树 + 角色勾选 → me 路由接口拼侧栏。
小程序首页应用platform=mobileroute/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 + AccRoutePermissionMiddlewarepermissionCodes + 未登记默认 service 兜底。
Sys 路由 Guard(阶段 3 子集)sys-route-permission-registry + SysRoutePermissionMiddleware/auth/* 跳过;管理端路由 admin
冲红 / ADMIN 403 结构assertCanCorrectWipProductionassertAdminRole 使用 PERMISSION_DENIED
角色分配 APIRolesService.assignUserToRole / removeUserFromRole 调用 assertAdmin(与 Sys Guard admin 双保险)。
会话与 claims 缓存:管理端变更后 Redis 会话失效 + bumpGatewayClaimsCacheVersion;网关/JWT 用 loadUserGatewayClaimsCached(见 GATEWAY_CLAIMS_CACHE*)。
403 审计:Acc/Sys 路由中间件对 PERMISSION_DENIEDwarnpermission-denied-audit.ts)。
待办(可选)DB 驱动 apiPattern@RequirePermissions 生成 registry。
子页面角色小程序工作台用 componentminiRole 写本地会话,属 端上体验约束;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展示名。
typedirectory | menu | button(可扩展约定,见下文「可选扩展」)。
platformpc | mobile:控制出现在哪棵菜单树;API 鉴权与 platform 解耦(见「能力节点」)。
routePC 路由或小程序路径;不用于 acc REST 路径匹配(避免把 UI 路径和 API 绑死)。
componentPC 组件键;小程序上可作 miniRole纯 UI 元数据
parentId / sort树与排序。

重要resolveAllowedPermissionIds 在角色勾选某节点后,会把 从该节点沿 parentId 直到根 的祖先 id 一并加入 allowedIds(用于菜单展示继承)。
API 鉴权建议:用 code 集合「仅直接勾选的能力叶节点」 二选一,避免「勾了子菜单却隐含父级 API 全放行」的歧义;若采用「仅叶节点」,需在文档与后台 UI 上写清楚。

  • roleId + permissionId:角色拥有哪些权限节点。
  • 改造用法:登录或刷新会话时,据此解析出用户的 permissionId 集合code 集合,供网关 / Guard 使用。

3. sys_role_assignmentsRoleAssignment

  • 用户 ↔ 角色;多角色权限取并集(与现有 resolveAllowedPermissionIds 一致)。

4. sys_site_membershipsSiteMembership

  • 数据范围(站点);与 能力权限(能做什么) 正交:
    • 站点:继续用现有 assertSiteScope + JWT siteIds
    • 能力:用 Permission + 角色绑定。
  • 改造后仍是:先有站点范围,再校验是否具备该站点上的某 API 能力(顺序不要反)。

5. JWT / 网关上下文(JwtPayload / resolveGatewayAuth

当前常见字段:userIdsiteIdsroleCodes
改造选项(择一或组合)

  1. 登录签发 JWT 时 写入 permissionCodes: string[]permissionIds: string[](由 RolePermissionLink 聚合,注意 token 体积与更新时机)。
  2. 不在 JWT 放全量:每次请求在 网关或 acc 进程userId + roleCodes 查缓存(Redis)中的权限集合(需失效策略:改角色、改权限树时清缓存)。

三、目标架构(原则)

  1. 每个可独立售卖/赋权的能力 对应至少一个 Permission.code(可与现有 menu_acc_inbound 等对齐,或新增 api:acc:inbound:page 等专用码)。
  2. 每个 acc(及未来需细控的 sys)接口 声明所需 一个或多个 code;未满足则 403
  3. ADMIN / 超管:保持与 MeRoutesService 一致,视为拥有全部权限节点(或单独 SUPER_ADMIN 旁路)。
  4. ZenStack @@allow:保留为 数据行级 约束;接口级 用权限 Guard,两层互补。
  5. 小程序:首页格子仍用 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] resolveUserPermissionCodespermission-resolver.ts):API 鉴权用 code 集合入口;菜单 id 仍走 MeRoutesService.resolveAllowedPermissionIds
  • [x] 菜单与 API 叶节点策略 产品定稿 + 单测(多角色并集、仅 btn_* 不隐含 menu_*;见 permission-leaf-policy.mdpermission-menu-leaf.spec.ts)。

阶段 3:请求侧鉴权(3~7 天)

方案 A(推荐先做):在 acc / sys 应用全局中间件 中(子集已落地):

  • [x] 维护 registryacc-route-permission-registry.tssys-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=trueSYS_ROUTE_GUARD_STRICT=true(见 backend/.env.example)。
  • [x] 阶段 A:Acc @Post 仅维护 @AccRoutePermissioncodegen:acc-route-registry 生成 acc-route-permission-registry.tscheck:route-registry 校验一致;Guard 仍读 registry。
  • [x] 阶段 A(Sys):Sys @Post 仅维护 @SysRoutePermissioncodegen: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 短缓存loadUserGatewayClaimsCachedGATEWAY_CLAIMS_CACHEGATEWAY_CLAIMS_CACHE_TTL);网关 GATEWAY_SESSION_CHECK 可灰度关闭会话键校验。
  • [x] 小程序:fetchMeAuthClaims + useAccPermissions;业务页 getMiniappApiErrorMessage / useMiniappApiToast

阶段 5:小程序与 PC 对齐(2~4 天)

  • [x] 子页门禁(子集):useMiniappPageGate + acc-permissions.ts 已接写操作/记工/结算/审核等页;首页格子 permissionCodes 过滤与点击校验见 miniapp-shell-permission.tsminiRole 仅作 tab/路径体验)。
  • [x] PC:无权限路由 → /no-permissionbeforeEach)。
  • [ ] 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=menuplatform 区分端。
功能 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.tsresolveAllowedPermissionIds
小程序应用列表同上 → listMobileShellApps
JWT 用户上下文backend/src/libs/shared/auth/resolve-gateway-auth.ts
站点范围backend/src/libs/shared/auth/assert-site-scope.ts
权限模型backend/zenstack/schema.zmodelPermissionRolePermissionLinkRoleAssignment
种子菜单backend/scripts/seed-sys-admin-permissions.mjs
三处维护说明permission-maintenance.md
Claims 缓存user-gateway-claims-cache.ts
拒绝审计permission-denied-audit.ts

九、已决议方案(当前实现)

  1. 方案 A:Acc/Sys registry + 全局中间件 + Service assert* 双保险;未登记默认放行,预发/生产开 严格模式
  2. 鉴权数据:请求侧以 loadUserGatewayClaims / Cached 为准,不信任 JWT 内权限快照;管理端改权限后会话失效 + claims 版本递增。
  3. 网关:校验 JWT +(可选)Redis 会话键,转发 x-user-* 头;不在网关维护 path→permission 表(避免与 Nest 双处维护)。
  4. ZenStack先接口权限、再行级 @@allow(见下文第七节)。

TaskFlow 内部文档 Released under the MIT License.