# 实施计划：租户管理后台（tenant-admin-web）

## 概述

按照设计文档，将实施拆分为：DDL 迁移 → 后端认证模块 → 后端路由模块 → 前端项目骨架 → 前端页面实现 → 联调收尾。每个任务增量构建，确保无孤立代码。属性测试（Hypothesis / fast-check）和单元测试作为可选子任务紧跟实现步骤。

后端使用 Python（FastAPI + Pydantic），前端使用 TypeScript（React + Vite + Ant Design）。

## 任务

- [x] 1. DDL 迁移：新建表 + 表结构变更
  - [x] 1.1 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-xx__ns4_tenant_admin_tables.sql`
    - 在 `auth` Schema 创建 `tenant_admins` 表（id, username, password_hash, display_name, tenant_id, managed_site_ids, is_active, created_by, created_at, last_login_at）
    - 创建索引 `idx_tenant_admin_tenant ON auth.tenant_admins(tenant_id)`
    - 在 `biz` Schema 创建 `excel_upload_log` 表（id, site_id, upload_type, file_name, uploaded_by, row_count, conflict_count, resolved_count, status, error_detail, created_at, confirmed_at）
    - 创建索引 `idx_excel_log_site ON biz.excel_upload_log(site_id, created_at DESC)`
    - 在 `biz` Schema 创建 `salary_adjustments` 表（id, site_id, assistant_id, assistant_name, assistant_number, salary_month, adjustment_type, amount, reason, upload_batch_id FK, created_at, created_by）
    - 创建索引 `(site_id, salary_month)` 和 `(assistant_id, salary_month)`
    - 在 `biz` Schema 创建 3 张 staging 表：`stg_finance_expense`、`stg_platform_income`、`stg_recharge_commission`，各含 site_id、业务字段、upload_batch_id FK、synced_at、created_at
    - _需求: 14.1, 14.2, 14.3, 14.4_

  - [x] 1.2 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-xx__ns4_member_clue_is_hidden.sql`
    - ALTER TABLE `public.member_retention_clue` ADD COLUMN `is_hidden` BOOLEAN NOT NULL DEFAULT false
    - 添加 COMMENT ON COLUMN 注释："是否隐藏（true=管理后台保留但小程序不展示）"
    - _需求: 15.1, 15.2_

  - [x] 1.3 更新小程序端线索查询增加 `WHERE is_hidden = false` 条件
    - 搜索现有线索查询 SQL，在 WHERE 子句中追加 `AND is_hidden = false`
    - _需求: 15.3_

- [x] 2. 后端认证模块（auth.tenant_admins + JWT + 依赖注入）
  - [x] 2.1 创建 `apps/backend/app/auth/tenant_admins.py`
    - 定义 `CurrentTenantAdmin` dataclass（admin_id, tenant_id, managed_site_ids, display_name）
    - 实现 `require_tenant_admin()` 依赖注入：验证 JWT `aud=tenant-admin`，提取管理员信息
    - 实现 `site_filter_clause()` 工具函数：生成 `site_id IN (...)` SQL 片段
    - 实现 `verify_site_access()` 工具函数：校验 site_id 是否在 managed_site_ids 内，否则抛 403
    - _需求: 1.4, 1.6, 2.1, 2.3, 17.3_

  - [x] 2.2 编写属性测试：JWT 隔离与端点保护
    - **Property 2: JWT 隔离与端点保护**
    - 使用 Hypothesis 生成随机 JWT payload（不同 aud 值：tenant-admin / xcx / 无效值）
    - 验证：仅 `aud=tenant-admin` 时 `require_tenant_admin()` 返回成功，其余返回 401
    - **验证: 需求 1.4, 1.6, 17.3**

  - [x] 2.3 编写属性测试：数据隔离
    - **Property 3: 数据隔离**
    - 使用 Hypothesis 生成随机 managed_site_ids 集合和随机 site_id
    - 验证：`verify_site_access()` 仅当 site_id ∈ managed_site_ids 时通过，否则抛 403
    - **验证: 需求 2.1, 2.2, 2.3**

- [x] 3. 后端路由：tenant_auth.py（登录/刷新）
  - [x] 3.1 创建 `apps/backend/app/routers/tenant_auth.py`
    - 实现 `POST /api/tenant/auth/login`：接受 username + password，查询 `auth.tenant_admins`，bcrypt 验证，签发 JWT（aud=tenant-admin, sub=admin_id, tenant_id, managed_site_ids），返回 access_token + refresh_token
    - 实现 `POST /api/tenant/auth/refresh`：验证 refresh_token，签发新令牌对
    - 登录成功时更新 `last_login_at`
    - 账号禁用（is_active=false）返回 403
    - 凭据无效返回 401，错误消息不区分用户名/密码
    - _需求: 1.1, 1.2, 1.3, 1.5_

  - [x] 3.2 在 `apps/backend/app/main.py` 中注册 tenant_auth router，路径前缀 `/api/tenant/auth`
    - _需求: 17.2_

  - [x] 3.3 编写属性测试：认证正确性
    - **Property 1: 认证正确性**
    - 使用 Hypothesis 生成随机用户名+密码组合
    - 验证：有效凭据返回含 `aud=tenant-admin` 的 JWT；无效凭据返回 401 且错误消息统一
    - **验证: 需求 1.1, 1.2**

  - [x] 3.4 编写单元测试：登录边界条件
    - 测试文件 `apps/backend/tests/unit/test_tenant_auth.py`
    - 验证：禁用账号登录返回 403、用户名不存在返回 401、密码错误返回 401、JWT 过期处理、refresh_token 刷新
    - _需求: 1.1, 1.2, 1.3, 1.5_

- [x] 4. 检查点 — 认证模块验证
  - 确保认证模块所有测试通过，ask the user if questions arise.

- [x] 5. 后端路由：tenant_users.py（用户审核 + 用户管理）
  - [x] 5.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_users.py`
    - 继承 `TenantBaseModel`（alias_generator=to_camel），定义：
    - `ApplicationListItem`（昵称、手机号、球房编号、申请角色、员工编号、申请时间、状态）
    - `MatchSuggestion`（assistant_id/staff_id、姓名、编号、来源表）
    - `ApproveRequest`（role、assistant_id 可选、staff_id 可选）
    - `RejectRequest`（reason 必填）
    - `UserListItem`（姓名、角色、关联助教姓名、所属门店、账号状态）
    - `UserEditRequest`（role 可选、site_id 可选、status 可选）
    - `UserBindingRequest`（assistant_id 可选、staff_id 可选）
    - _需求: 3.2, 4.1_

  - [x] 5.2 创建 `apps/backend/app/routers/tenant_users.py`
    - `GET /api/tenant/applications`：申请列表，status 筛选 + 分页，附加 site_id IN 条件
    - `GET /api/tenant/applications/{id}/match-suggestions`：通过 site_code_mapping 查 site_id，并行匹配 v_dim_assistant（phone, scd2_is_current=1）和 v_dim_staff + v_dim_staff_ex（phone）
    - `POST /api/tenant/applications/{id}/approve`：事务内执行：更新 users.status='approved' → 写入 user_site_roles → 写入 user_assistant_binding → 更新 user_applications.status='approved' + 审核人 + 审核时间
    - `POST /api/tenant/applications/{id}/reject`：更新 user_applications.status='rejected' + review_note + reviewed_at
    - 非 pending 状态审核返回 409
    - `GET /api/tenant/users`：已通过审核用户列表，角色筛选 + 关键词搜索 + 分页
    - `PATCH /api/tenant/users/{id}`：编辑角色/门店/状态，site_id 超出管辖范围返回 403
    - `PUT /api/tenant/users/{id}/binding`：更新 user_assistant_binding
    - 禁用用户时设 users.status='disabled'
    - _需求: 3.1-3.6, 4.1-4.5_

  - [x] 5.3 在 `apps/backend/app/main.py` 中注册 tenant_users router，路径前缀 `/api/tenant`
    - _需求: 17.2_

  - [x] 5.4 编写属性测试：审核通过多表一致性
    - **Property 4: 审核通过多表一致性**
    - 使用 Hypothesis 生成随机 pending 申请 + 角色/绑定参数
    - 验证：审核通过后 users.status='approved'、user_site_roles 存在记录、user_assistant_binding 存在记录（如提供 assistant_id）、user_applications.status='approved' 且审核人和时间已填写
    - **验证: 需求 3.4**

  - [x] 5.5 编写属性测试：审核拒绝状态更新
    - **Property 5: 审核拒绝状态更新**
    - 使用 Hypothesis 生成随机 pending 申请 + 随机拒绝原因字符串
    - 验证：拒绝后 user_applications.status='rejected'、review_note 等于提交的原因、reviewed_at 非空
    - **验证: 需求 3.5**

  - [x] 5.6 编写属性测试：申请关联匹配
    - **Property 6: 申请关联匹配**
    - 使用 Hypothesis 生成随机手机号 + 随机助教/员工数据集
    - 验证：匹配结果仅包含 phone 匹配的记录，优先 v_dim_assistant 再 v_dim_staff
    - **验证: 需求 3.3**

  - [x] 5.7 编写属性测试：用户管理编辑
    - **Property 7: 用户管理编辑**
    - 使用 Hypothesis 生成随机编辑参数（角色、site_id 在管辖范围内、状态）
    - 验证：编辑后用户记录反映新值；site_id 超出管辖范围时返回 403
    - **验证: 需求 4.2, 4.3, 4.4**

  - [x] 5.8 编写单元测试：用户审核与管理边界条件
    - 测试文件 `apps/backend/tests/unit/test_tenant_users.py`
    - 验证：非 pending 状态审核返回 409、越权修改 site_id 返回 403、禁用用户后 status='disabled'
    - _需求: 3.6, 4.4, 4.5_

- [x] 6. 后端路由：tenant_excel.py（Excel 上传/校验/冲突/写入）
  - [x] 6.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_excel.py`
    - 定义 4 种模板的行数据模型：`ExpenseRow`、`PlatformIncomeRow`、`SalaryAdjRow`、`RechargeCommissionRow`
    - 定义校验结果模型：`ValidationResult`（errors[], warnings[], passed_rows[], upload_id）
    - 定义冲突模型：`ConflictDiff`（row_index, field_diffs[{field, old_value, new_value}]）
    - 定义确认请求：`ConfirmRequest`（upload_id, resolutions[{row_index, action: replace/keep}]）
    - 定义上传记录模型：`UploadLogItem`（模板类型、文件名、上传人、时间、行数、冲突数、状态）
    - _需求: 5.2, 7.2, 8.4_

  - [x] 6.2 创建 `apps/backend/app/routers/tenant_excel.py`
    - `POST /api/tenant/excel/upload`：multipart/form-data 接收文件 + upload_type 参数
      - 校验文件格式（.xlsx/.xls），非法返回 400
      - 按模板类型执行格式校验（月份格式、枚举值、金额精度、非空等）
      - salary_adj / recharge_commission 模板执行人员匹配校验（v_dim_assistant → v_dim_staff 降级匹配）
      - 格式校验通过后执行冲突检测（按模板主键规则匹配已有数据）
      - 创建 excel_upload_log 记录（status=pending），返回 upload_id + 校验结果 + 冲突 diff
    - `POST /api/tenant/excel/confirm`：接受 upload_id + resolutions[]
      - 单事务写入目标表（salary_adj → biz.salary_adjustments，其余 → staging 表）
      - 替换行执行 UPDATE，新增行执行 INSERT
      - 写入失败回滚整批，log status=failed，记录 error_detail
      - 写入成功更新 log status=confirmed + 实际行数 + 冲突解决数 + confirmed_at
    - `GET /api/tenant/excel/logs`：上传记录列表，分页，附加 site_id IN 条件
    - `GET /api/tenant/excel/template/{type}`：返回空白 Excel 模板文件（含表头和格式说明）
    - _需求: 5.1-5.5, 6.1-6.5, 7.1-7.5, 8.1-8.5_

  - [x] 6.3 在 `apps/backend/app/main.py` 中注册 tenant_excel router，路径前缀 `/api/tenant/excel`
    - _需求: 17.2_

  - [x] 6.4 编写属性测试：Excel 格式校验
    - **Property 8: Excel 格式校验**
    - 使用 Hypothesis 生成随机模板类型 + 随机数据行（含有效/无效混合）
    - 验证：符合模板定义的行标记通过，不符合的行返回行号+列名+错误描述；通过行数 + 警告行数 + 错误行数 = 总行数
    - **验证: 需求 5.1, 5.2, 5.3, 5.4, 6.5**

  - [x] 6.5 编写属性测试：人员匹配校验
    - **Property 9: 人员匹配校验**
    - 使用 Hypothesis 生成随机助教姓名+编号 + 随机助教/员工数据集
    - 验证：优先 v_dim_assistant 匹配，未匹配再查 v_dim_staff；匹配成功填充 assistant_id，失败标记 warning 不阻断
    - **验证: 需求 6.1, 6.2, 6.3, 6.4**

  - [x] 6.6 编写属性测试：冲突检测
    - **Property 10: 冲突检测**
    - 使用 Hypothesis 生成随机上传数据 + 随机已有数据
    - 验证：按模板主键规则识别冲突行，冲突行返回逐字段 diff，非冲突行标记待写入
    - **验证: 需求 7.1, 7.2**

  - [x] 6.7 编写属性测试：数据写入 round-trip
    - **Property 11: 数据写入 round-trip**
    - 使用 Hypothesis 生成随机数据 + 随机冲突解决方案（replace/keep）
    - 验证：写入后从目标表读取的数据与提交一致（replace 用新值，keep 保持旧值）；log status=confirmed，行数正确
    - **验证: 需求 7.4, 7.5, 8.1, 8.2**

  - [x] 6.8 编写单元测试：Excel 上传边界条件
    - 测试文件 `apps/backend/tests/unit/test_tenant_excel.py`
    - 验证：无效文件格式返回 400、写入失败回滚（log status=failed）、模板下载返回正确文件
    - _需求: 5.5, 8.3, 8.5_

- [x] 7. 后端路由：tenant_clues.py（维客线索管理）
  - [x] 7.1 创建 Pydantic Schema `apps/backend/app/schemas/tenant_clues.py`
    - 定义 `CustomerSearchItem`（member_id、姓名、手机号脱敏、所属门店）
    - 定义 `ClueListItem`（id、category、summary、detail、recorded_by_name、source、recorded_at、is_hidden）
    - 定义 `ClueEditRequest`（category 枚举 6 值、summary 非空 ≤200 字符、detail 可选）
    - 定义 `ClueVisibilityRequest`（is_hidden: bool）
    - _需求: 9.1, 10.1, 11.1_

  - [x] 7.2 创建 `apps/backend/app/routers/tenant_clues.py`
    - `GET /api/tenant/customers/search`：keyword + site_id 参数，在管辖门店范围内搜索 v_dim_member（nickname 模糊匹配 OR mobile 精确匹配，scd2_is_current=1），手机号脱敏返回
    - `GET /api/tenant/customers/{member_id}/clues`：返回该客户在管辖门店范围内的全部线索，支持 source 和 is_hidden 筛选
    - `PATCH /api/tenant/clues/{id}`：编辑线索 category/summary/detail，校验 category 枚举和 summary 长度
    - `DELETE /api/tenant/clues/{id}`：物理删除（DELETE FROM），线索不存在或不在管辖范围返回 404
    - `PATCH /api/tenant/clues/{id}/visibility`：切换 is_hidden 状态
    - 所有线索操作先校验线索 site_id 是否在管辖范围内，不在则返回 404
    - _需求: 9.1-9.4, 10.1, 11.1-11.3, 12.2-12.3, 13.1-13.4_

  - [x] 7.3 在 `apps/backend/app/main.py` 中注册 tenant_clues router，路径前缀 `/api/tenant`
    - _需求: 17.2_

  - [x] 7.4 编写属性测试：客户搜索
    - **Property 12: 客户搜索**
    - 使用 Hypothesis 生成随机关键词 + 随机客户数据集
    - 验证：结果中 nickname 包含关键词（模糊）或 mobile 等于关键词（精确），且 site_id 在管辖范围内
    - **验证: 需求 9.1, 9.3**

  - [x] 7.5 编写属性测试：线索编辑校验
    - **Property 13: 线索编辑校验**
    - 使用 Hypothesis 生成随机 category（含有效/无效值）+ 随机 summary（含空/超长）
    - 验证：有效参数编辑成功且记录反映新值；无效 category 或空/超长 summary 被拒绝
    - **验证: 需求 11.1, 11.2**

  - [x] 7.6 编写属性测试：线索删除
    - **Property 14: 线索删除**
    - 使用 Hypothesis 生成随机线索 ID
    - 验证：删除后该线索不可通过任何查询获取（物理删除）
    - **验证: 需求 12.2**

  - [x] 7.7 编写属性测试：线索隐藏/显示 round-trip
    - **Property 15: 线索隐藏/显示 round-trip**
    - 使用 Hypothesis 生成随机线索
    - 验证：设 is_hidden=true 后小程序端查询（WHERE is_hidden=false）不返回该线索，管理后台查询返回；设回 false 后小程序端恢复返回
    - **验证: 需求 13.1, 13.2, 13.3, 15.3**

  - [x] 7.8 编写单元测试：维客线索边界条件
    - 测试文件 `apps/backend/tests/unit/test_tenant_clues.py`
    - 验证：线索不存在返回 404、越权访问返回 404、无效 category 返回 422、空 summary 返回 422
    - _需求: 11.2, 11.3, 12.3, 13.4_

- [x] 8. 后端路由：admin_tenant_admins.py（管理端租户管理员 CRUD）
  - [x] 8.1 创建 Pydantic Schema `apps/backend/app/schemas/admin_tenant_admins.py`
    - 继承现有 BaseModel 模式，定义：
    - `TenantAdminListItem`（id、username、display_name、managed_site_ids、is_active、created_at、last_login_at）
    - `TenantAdminCreateRequest`（username、password、display_name、tenant_id、managed_site_ids）
    - `TenantAdminEditRequest`（display_name 可选、managed_site_ids 可选、is_active 可选）
    - `ResetPasswordRequest`（new_password 必填）
    - _需求: 14.1, 14.2, 14.4, 14.5_

  - [x] 8.2 创建 `apps/backend/app/routers/admin_tenant_admins.py`
    - 复用现有 `_require_admin()` 依赖（site_admin 或 tenant_admin 角色）
    - `GET /api/admin/tenant-admins`：管理员列表，分页 + 关键词搜索
    - `POST /api/admin/tenant-admins`：创建管理员，密码 bcrypt 哈希，username UNIQUE 冲突返回 409，记录 created_by
    - `PATCH /api/admin/tenant-admins/{id}`：编辑 display_name / managed_site_ids / is_active
    - `POST /api/admin/tenant-admins/{id}/reset-password`：新密码 bcrypt 哈希后更新 password_hash
    - 管理员 ID 不存在返回 404
    - _需求: 14.1-14.7_

  - [x] 8.3 在 `apps/backend/app/main.py` 中注册 admin_tenant_admins router，路径前缀 `/api/admin`
    - _需求: 18.2_

  - [x] 8.4 编写属性测试：租户管理员 CRUD 一致性
    - **Property 17: 租户管理员 CRUD 一致性**
    - 使用 Hypothesis 生成随机用户名+密码+门店列表
    - 验证：创建后可登录、编辑 managed_site_ids 后隔离范围更新、禁用后登录返回 403、重置密码后旧密码失效新密码生效
    - **验证: 需求 14.2, 14.4, 14.5, 14.6**

  - [x] 8.5 编写单元测试：租户管理员 CRUD 边界条件
    - 测试文件 `apps/backend/tests/unit/test_admin_tenant_admins.py`
    - 验证：用户名重复返回 409、管理员不存在返回 404、无管理员权限返回 403
    - _需求: 14.3, 14.7_

- [x] 9. 检查点 — 后端路由模块验证
  - 确保所有后端路由模块测试通过，5 个路由文件均已注册到 main.py，ask the user if questions arise.

- [x] 10. 前端项目骨架（apps/tenant-admin/）
  - [x] 10.1 创建 `apps/tenant-admin/` 项目结构
    - 初始化 `package.json`（React + Vite + Ant Design + axios 依赖）
    - 创建 `vite.config.ts`（proxy 配置 `/api/tenant` → 后端地址）
    - 创建 `tsconfig.json`
    - 创建 `index.html` 入口
    - _需求: 17.1_

  - [x] 10.2 创建 `apps/tenant-admin/src/services/api.ts` — API 调用封装
    - axios 实例，baseURL: `/api/tenant`
    - 请求拦截器：从 localStorage 读取 access_token 附加 Authorization header
    - 响应拦截器：401 时用 refresh_token 刷新，刷新失败重定向 `/login`
    - 并发刷新保护：多个 401 只触发一次 refresh，其余排队
    - 响应解包：`{ code: 0, data }` → 提取 `data`
    - _需求: 17.4, 17.5_

  - [x] 10.3 创建 `apps/tenant-admin/src/hooks/useAuth.ts` — 认证状态管理
    - 登录/登出方法、token 存储、用户信息（display_name, managed_site_ids）
    - _需求: 1.5, 17.3_

  - [x] 10.4 创建 `apps/tenant-admin/src/App.tsx` — 路由配置 + 侧边栏布局
    - react-router-dom 路由配置
    - Ant Design Layout + Sider 侧边栏导航（用户审核、用户管理、Excel 上传、维客线索管理）
    - 未认证时重定向到 `/login`
    - _需求: 17.2, 17.3_

  - [x] 10.5 创建 `apps/tenant-admin/src/components/SiteSelector/` — 门店筛选器组件
    - 页面顶部门店选择器，数据源为当前管理员的 managed_site_ids
    - 支持多选/全选
    - _需求: 2.4_

  - [x] 10.6 编写属性测试：响应格式一致性（前端 fast-check）
    - **Property 16: 响应格式一致性**
    - 使用 fast-check 生成随机 API 响应数据
    - 验证：成功响应符合 `{ code: 0, data }` 格式；错误响应符合 `{ code: number, message: string }` 格式；分页响应 data 包含 items/total/page/pageSize
    - **验证: 需求 18.4**

- [x] 11. 前端页面：登录页
  - [x] 11.1 创建 `apps/tenant-admin/src/pages/Login/index.tsx`
    - 用户名 + 密码表单（Ant Design Form + Input + Button）
    - 调用 `POST /api/tenant/auth/login`，成功后存储 token 并跳转首页
    - 错误提示：401 显示"用户名或密码错误"、403 显示"账号已被禁用"
    - _需求: 1.1, 1.2, 1.3, 17.3_

- [x] 12. 前端页面：用户审核
  - [x] 12.1 创建 `apps/tenant-admin/src/pages/UserApproval/index.tsx`
    - 申请列表表格（Ant Design Table），支持状态筛选（全部/待审核/已通过/已拒绝）+ 分页
    - 展示字段：昵称、手机号、球房编号、申请角色、员工编号、申请时间、状态
    - 待审核行提供"审核"操作按钮
    - _需求: 3.1, 3.2_

  - [x] 12.2 实现审核操作交互
    - 点击"审核"打开 Modal，展示关联建议列表（调用 match-suggestions API）
    - 通过操作：选择角色 + 关联助教/员工 → 调用 approve API
    - 拒绝操作：填写拒绝原因（必填）→ 调用 reject API
    - 操作成功后刷新列表
    - _需求: 3.3, 3.4, 3.5_

- [x] 13. 前端页面：用户管理
  - [x] 13.1 创建 `apps/tenant-admin/src/pages/UserManagement/index.tsx`
    - 用户列表表格，支持角色筛选 + 关键词搜索 + 分页
    - 展示字段：姓名、角色、关联助教姓名、所属门店、账号状态
    - 每行提供"编辑"和"绑定"操作按钮
    - _需求: 4.1_

  - [x] 13.2 实现编辑与绑定交互
    - 编辑 Modal：修改角色（Select）、所属门店（Select，限管辖范围）、账号状态（Switch）
    - 绑定 Modal：修改关联助教 ID / 员工 ID
    - 禁用用户时二次确认
    - _需求: 4.2, 4.3, 4.4_

- [x] 14. 前端页面：Excel 上传
  - [x] 14.1 创建 `apps/tenant-admin/src/pages/ExcelUpload/index.tsx`
    - 模板类型选择（Radio/Select：财务支出/团购收入/助教奖罚/充值业绩归属）
    - 模板下载按钮（调用 template/{type} API）
    - 文件上传组件（Ant Design Upload，限 .xlsx/.xls）
    - 上传后展示校验结果：错误行标红、警告行黄色高亮
    - 汇总展示：通过行数、警告行数、错误行数
    - _需求: 5.1, 5.3, 6.4, 6.5, 8.5_

  - [x] 14.2 创建 `apps/tenant-admin/src/components/DiffTable/index.tsx` — 冲突 diff 交互表格
    - 每行显示：字段名、旧值、新值、操作选项（替换/保留 Radio）
    - 支持"全部替换"和"全部保留"快捷操作按钮
    - 确认按钮提交 resolutions 数组
    - _需求: 7.3, 7.4_

  - [x] 14.3 实现确认写入与上传记录
    - 无冲突时直接展示"待写入"确认按钮
    - 有冲突时展示 DiffTable，用户选择后确认
    - 调用 confirm API 写入，成功/失败提示
    - 上传记录 Tab：展示历史上传记录列表（模板类型、文件名、上传人、时间、行数、冲突数、状态），分页
    - _需求: 7.5, 8.1, 8.2, 8.4_

- [x] 15. 前端页面：维客线索管理
  - [x] 15.1 创建 `apps/tenant-admin/src/pages/RetentionClues/index.tsx`
    - 客户搜索栏（Input.Search），支持姓名模糊/手机号精确搜索
    - 门店筛选器（复用 SiteSelector 组件）
    - 搜索结果列表：member_id、姓名、手机号脱敏、所属门店
    - 搜索结果为空时展示"未找到匹配客户"提示
    - _需求: 9.1, 9.3, 9.4_

  - [x] 15.2 实现线索列表与操作
    - 选择客户后展示该客户全部线索，按大类标签分组
    - 支持按来源和隐藏状态筛选
    - 已隐藏线索以灰色/删除线样式区分
    - 每条线索提供：编辑、删除、隐藏/显示操作按钮
    - _需求: 10.1, 10.2, 10.3_

  - [x] 15.3 创建 `apps/tenant-admin/src/components/ClueEditor/index.tsx` — 线索编辑表单
    - category Select（6 值枚举：客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈）
    - summary Input（必填，最长 200 字符）
    - detail TextArea（可选）
    - _需求: 11.1_

  - [x] 15.4 实现删除与隐藏/显示交互
    - 删除：二次确认对话框（Modal.confirm），明确提示"删除后不可恢复"
    - 隐藏/显示：Switch 切换，即时调用 visibility API
    - _需求: 12.1, 12.2, 13.1, 13.3_

- [x] 16. 管理后台前端：租户管理员管理页面（admin-web）
  - [x] 16.1 创建 `apps/admin-web/src/api/tenantAdmins.ts` — API 调用
    - 列表查询（分页+搜索）、创建、编辑、重置密码
    - 复用现有 `apiClient` axios 实例
    - _需求: 14.1_

  - [x] 16.2 创建 `apps/admin-web/src/pages/TenantAdmins/index.tsx` — 管理页面
    - Ant Design Table 展示管理员列表（用户名、显示名称、管辖门店、状态、创建时间、最后登录）
    - 创建 Modal：用户名 + 初始密码 + 显示名称 + tenant_id + managed_site_ids（门店多选）
    - 编辑 Modal：修改显示名称 / managed_site_ids / is_active
    - 重置密码 Modal：新密码 + 确认密码
    - _需求: 14.1, 14.2, 14.4, 14.5_

  - [x] 16.3 在 `apps/admin-web/src/App.tsx` 中注册路由和侧边栏导航
    - 新增 `/tenant-admins` 路由指向 TenantAdmins 页面
    - 侧边栏新增"租户管理员"导航入口
    - _需求: 14.1_

- [x] 17. 检查点 — 前端页面验证
  - 确保所有前端页面组件渲染正常，API 调用层工作正确，ask the user if questions arise.

- [x] 18. 前后端联调与集成
  - [x] 18.1 前端所有页面对接后端真实 API
    - 登录页 → tenant_auth 登录/刷新
    - 用户审核页 → tenant_users 申请列表/关联建议/审核
    - 用户管理页 → tenant_users 用户列表/编辑/绑定
    - Excel 上传页 → tenant_excel 上传/校验/冲突/确认/记录/模板下载
    - 维客线索页 → tenant_clues 客户搜索/线索列表/编辑/删除/隐藏
    - admin-web 租户管理员页 → admin_tenant_admins 列表/创建/编辑/重置密码
    - 验证所有页面 API 调用失败时显示友好错误提示，不出现白屏
    - _需求: 1.1-1.6, 3.1-3.6, 4.1-4.5, 5.1-5.5, 9.1-9.4, 10.1-10.3, 11.1-11.3, 12.1-12.3, 13.1-13.4, 14.1-14.7_

  - [x] 18.2 DDL 迁移合并到主 DDL 基线
    - 执行迁移脚本到 `test_zqyy_app`
    - 合并新表定义到 `docs/database/ddl/` 对应基线文件
    - _需求: 15.1-15.4, 16.1-16.2_

- [x] 19. 文档更新
  - [x] 19.1 创建/更新 BD 手册
    - 新增 `docs/database/BD_Manual_tenant_admin_tables.md`：tenant_admins、excel_upload_log、salary_adjustments、3 张 staging 表的字段明细、约束、索引、验证 SQL
    - 更新 `docs/database/BD_Manual_retention_clue.md`（如存在）：追加 is_hidden 字段说明
    - _规范: db-docs.md_

  - [x] 19.2 更新后端 API 参考文档
    - 在 `apps/backend/docs/API-REFERENCE.md` 新增 4 个 tenant 路由模块 + 1 个 admin_tenant_admins 路由模块文档
    - 更新 `apps/backend/README.md` 路由模块摘要
    - _需求: 18.1-18.4_

  - [x] 19.3 更新文档地图
    - 在 `docs/DOCUMENTATION-MAP.md` 新增 NS4 模块条目（tenant-admin-web spec、BD 手册、前端项目）
    - _规范: doc-map.md_

- [x] 20. 最终检查点 — 全量验证
  - 确保所有后端测试通过（单元测试 + 属性测试）
  - 确保前端所有页面连接真实后端运行正常（tenant-admin + admin-web 租户管理员页面）
  - 确保 DDL 迁移已合并到主基线，BD 手册已同步更新
  - 确保 API 文档、后端 README、文档地图均已更新
  - ask the user if questions arise.

## 备注

- 标记 `*` 的子任务为可选，可跳过以加速 MVP 交付
- 每个任务引用了具体的需求编号以确保可追溯性
- 属性测试覆盖 17 个正确性属性（Property 1-17），单元测试覆盖具体边界条件
- 检查点任务确保增量验证，避免问题累积
- 后端使用 Python（FastAPI + Pydantic + Hypothesis），前端使用 TypeScript（React + Vite + Ant Design + fast-check）
- 会员信息一律通过 `member_id` JOIN `v_dim_member`（`scd2_is_current=1`），不使用结算单冗余字段（DQ-6 规则）
- 多店铺 FDW 查询采用逐 site_id 查询后合并结果的策略
- admin-web 侧租户管理员 CRUD（任务 8 + 16）复用现有 `_require_admin()` 依赖和 admin-web 组件模式
