# 变更审计记录:根治 tenant_admin 的 managed_site_ids 限制(跨模块权限验证改造) | 字段 | 值 | |------|-----| | 日期 | 2026-03-23 21:00:00 | | Prompt-ID | P20260323-210000 | | Session-ID | f0a03585 | | Session 路径 | docs/audit/session_logs/2026-03/23/21_28b7ab84_173223 | ## 操作摘要 根治 tenant_admin 的 JWT `managed_site_ids` 静态签发问题。tenant_admin 新建店铺后 JWT 中不含新店铺 ID,导致所有使用 `verify_site_access` 和 `site_filter_clause` 的端点无法访问新店铺。方案:tenant_admin 按 `tenant_id` 实时查 `biz.sites` 获取有效 site_ids,site_admin 仍用 JWT 中的 `managed_site_ids`。改造涉及 5 个文件、跨 4 个路由模块。 ## 核心方案 在 `tenant_admins.py` 新增两个函数: - `get_tenant_site_ids(tenant_id)` — 通过 `biz.tenants.tenant_id` → `biz.tenants.id` → `biz.sites.tenant_id` 关联查询 - `get_effective_site_ids(admin)` — 统一入口,自动区分 admin_type(tenant_admin 查库 / site_admin 用 JWT) 改造 `site_filter_clause` 和 `verify_site_access` 支持 `admin=` keyword-only 参数(向后兼容旧的 positional `managed_site_ids` 签名)。 ## 受影响文件 | 文件 | 改动内容 | |------|----------| | `apps/backend/app/auth/tenant_admins.py` | 新增 `get_tenant_site_ids`、`get_effective_site_ids`;改造 `site_filter_clause`、`verify_site_access` | | `apps/backend/app/routers/tenant_users.py` | 所有调用点改用 `admin=admin`;`list_my_sites` 简化为 `get_effective_site_ids`;`list_applications` 回退头痛医头代码 | | `apps/backend/app/routers/tenant_clues.py` | `_get_clue_with_site_check` 签名改为 `admin: CurrentTenantAdmin`;`search_customers` 用 `get_effective_site_ids`;`list_customer_clues` 用 `site_filter_clause(admin=admin)` | | `apps/backend/app/routers/tenant_excel.py` | 两个 `verify_site_access` 改用 `admin=admin`;`list_upload_logs` 的 `site_filter_clause` 改用 `admin=admin` | | `apps/backend/app/routers/tenant_site_admins.py` | `create_site_admin` 和 `edit_site_admin` 的权限子集校验改用 `get_effective_site_ids(admin)` | ## 影响范围 - 所有 `/api/tenant/*` 端点的门店权限验证 - tenant_admin 新建店铺后无需重新登录即可访问 - site_admin 行为不变(仍用 JWT managed_site_ids) ## 改动注解 ### `apps/backend/app/auth/tenant_admins.py` - 变更类型:修改 - 原始原因:JWT `managed_site_ids` 是登录时静态签发的,新建店铺后不在列表中,导致 tenant_admin 无法访问新店铺的任何端点。需要一个统一的动态获取机制。 - 思路分析:新增 `get_tenant_site_ids()` 通过 `biz.tenants.tenant_id`(外部标识)→ `biz.tenants.id`(内部 PK)→ `biz.sites.tenant_id` 三表关联查询活跃店铺。`get_effective_site_ids()` 作为统一入口,按 `admin_type` 分流:tenant_admin 走查库路径,site_admin 仍用 JWT。`site_filter_clause` 和 `verify_site_access` 新增 `admin=` keyword-only 参数,优先使用 admin 参数,也兼容旧的 positional `managed_site_ids` 直传方式,实现向后兼容。 - 修改结果:所有下游路由只需传 `admin=admin` 即可自动获得正确的 site_ids,无需关心 admin_type 差异。biz.sites 数据量极小(几条),无需缓存。 ### `apps/backend/app/routers/tenant_users.py` - 变更类型:修改 - 原始原因:该路由的所有端点(my-sites、applications、users、approve、reject、edit、binding)都直接使用 `admin.managed_site_ids`,新建店铺后全部受限。此前 `list_my_sites` 和 `list_applications` 已有针对 tenant_admin 的特殊处理(头痛医头),需要统一回退。 - 思路分析:所有 `verify_site_access(site_id, admin.managed_site_ids)` 改为 `verify_site_access(site_id, admin=admin)`;所有 `site_filter_clause(admin.managed_site_ids)` 改为 `site_filter_clause(admin=admin)`。`list_my_sites` 简化为直接调用 `get_effective_site_ids(admin)` 查库,删除之前针对 tenant_admin 的特殊 SQL 分支。`list_applications` 回退之前的 tenant_admin 特殊处理代码,统一走 `site_filter_clause(admin=admin)`。 - 修改结果:10 个端点的权限验证统一收口,代码更简洁。tenant_admin 新建店铺后所有用户管理功能立即可用。 ### `apps/backend/app/routers/tenant_clues.py` - 变更类型:修改 - 原始原因:维客线索管理的 `_get_clue_with_site_check`、`search_customers`、`list_customer_clues` 都直接使用 `admin.managed_site_ids`,新建店铺的线索无法查看和管理。 - 思路分析:`_get_clue_with_site_check` 签名从接受 `managed_site_ids: list[int]` 改为接受 `admin: CurrentTenantAdmin`,内部调用 `verify_site_access(site_id, admin=admin)`。`search_customers` 用 `get_effective_site_ids(admin)` 构建 IN 子句。`list_customer_clues` 用 `site_filter_clause(admin=admin)`。三个调用点(edit_clue、delete_clue、toggle_visibility)改传 admin 对象。 - 修改结果:维客线索管理覆盖新建店铺,签名更语义化。 ### `apps/backend/app/routers/tenant_excel.py` - 变更类型:修改 - 原始原因:Excel 上传/确认/日志三个端点的权限验证使用静态 `managed_site_ids`,新建店铺的 Excel 数据无法上传和查看。 - 思路分析:`upload_excel` 和 `confirm_excel` 中的 `verify_site_access(site_id, admin.managed_site_ids)` 改为 `verify_site_access(site_id, admin=admin)`。`list_upload_logs` 的 `site_filter_clause(admin.managed_site_ids)` 改为 `site_filter_clause(admin=admin)`。 - 修改结果:Excel 上传/确认/日志覆盖新建店铺。 ### `apps/backend/app/routers/tenant_site_admins.py` - 变更类型:修改 - 原始原因:创建和编辑店铺管理员时,权限子集校验(site_admin 的 managed_site_ids 必须是 tenant_admin 管辖范围的子集)使用静态 `admin.managed_site_ids`,导致无法为新建店铺分配管理员。 - 思路分析:`create_site_admin` 和 `edit_site_admin` 中的子集校验从 `set(body.managed_site_ids) <= set(admin.managed_site_ids)` 改为 `set(body.managed_site_ids) <= set(get_effective_site_ids(admin))`。 - 修改结果:tenant_admin 可以为新建店铺创建和编辑店铺管理员。 ## 合规检查 | 检查项 | 状态 | |--------|------| | 新增迁移 SQL | 无 | | DDL 基线 | 不涉及 | | OpenAPI spec | ✅ 已重新导出(137 paths, 194 schemas)。附带修复:`tenant_admins.py` 的 `AI_CHANGELOG` 文档字符串移至 `from __future__` 之后(消除 SyntaxError) | | BD 手册 | 不涉及 | ## 回滚方案 将 5 个文件的 `admin=admin` 参数改回 `admin.managed_site_ids`(positional),删除 `tenant_admins.py` 中新增的三个函数。旧签名向后兼容,无需修改函数定义。 ## 验证 SQL ```sql -- 验证 tenant_admin 的 tenant_id 能查到所有店铺(含新建) SELECT s.site_id, s.site_name, s.site_code FROM biz.sites s JOIN biz.tenants t ON t.id = s.tenant_id WHERE t.tenant_id = 'LLZQ' AND t.is_active = true AND s.is_active = true; ```