# 变更审计记录：根治 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;
```
