# 需求文档：租户管理后台（tenant-admin-web）

## 简介

构建独立的租户管理 Web 应用（`apps/tenant-admin/`），面向租户管理员，提供用户审核与管理、Excel 数据上传（4 种模板）、维客线索管理三大功能模块。前端采用 React + Vite + Ant Design（与 `apps/admin-web/` 同技术栈），后端复用 `apps/backend/` FastAPI 新增 4 个路由模块。认证体系与小程序完全隔离，使用独立的 `auth.tenant_admins` 表、用户名+密码登录、不同 JWT audience。所有数据查询附加 `site_id IN (管辖列表)` 条件实现多门店数据隔离。

## 术语表

- **Tenant_Admin_Web**：租户管理后台前端应用，部署在 `apps/tenant-admin/`
- **Backend_API**：FastAPI 后端服务，部署在 `apps/backend/`，为 Tenant_Admin_Web 提供 RESTful API
- **Tenant_Admin**：租户管理员，由系统管理后台 Operator 创建的账号，使用用户名+密码登录租户管理后台
- **Operator**：系统管理后台操作员，在 `apps/admin-web/` 中管理租户管理员账号
- **Site**：门店，通过 `site_id` 标识，是多门店数据隔离的基本单位
- **Site_Code**：球房编号，用户申请时输入的门店标识码，通过 `auth.site_code_mapping` 映射到 `site_id`
- **User_Application**：用户入驻申请，小程序用户提交的申请记录，存储在 `auth.user_applications`
- **Retention_Clue**：维客线索，助教为会员记录的销售/维护线索，存储在 `public.member_retention_clue`
- **Staging_Table**：暂存表，Excel 上传数据先写入业务库暂存表，再由 ETL 同步到 DWS 层
- **Upload_Batch**：上传批次，一次 Excel 上传操作的记录，存储在 `biz.excel_upload_log`
- **Salary_Adjustment**：助教奖罚明细，通过 Excel 上传写入 `biz.salary_adjustments`
- **Managed_Site_Ids**：管辖门店列表，Tenant_Admin 被授权管理的 `site_id` 数组

## 需求

### 需求 1：租户管理员认证

**用户故事：** 作为 Tenant_Admin，我希望通过用户名和密码登录租户管理后台，以便安全地执行用户审核、数据上传等管理操作。

#### 验收标准

1. WHEN Tenant_Admin 提交有效的用户名和密码, THE Backend_API SHALL 验证凭据（bcrypt 哈希比对），签发 JWT 访问令牌（audience 为 `tenant-admin`，与小程序 `xcx` 隔离），并返回访问令牌和刷新令牌
2. WHEN Tenant_Admin 提交无效的用户名或密码, THE Backend_API SHALL 返回 401 状态码和错误描述，不泄露具体是用户名还是密码错误
3. IF Tenant_Admin 账号状态为禁用（`is_active = false`）, THEN THE Backend_API SHALL 返回 403 状态码并提示账号已被禁用
4. WHILE Tenant_Admin 持有有效的 JWT 令牌, THE Backend_API SHALL 允许访问 `/api/tenant/*` 路径下的受保护端点
5. WHEN JWT 访问令牌过期, THE Tenant_Admin_Web SHALL 使用刷新令牌自动获取新的访问令牌；WHEN 刷新令牌也过期, THE Tenant_Admin_Web SHALL 将 Tenant_Admin 重定向到登录页面
6. THE Backend_API SHALL 通过 JWT 中的 `aud` 字段区分租户管理员和小程序用户，使用独立的认证依赖注入（`require_tenant_admin()`），拒绝小程序 JWT 访问租户管理端点

### 需求 2：数据隔离与权限控制

**用户故事：** 作为 Tenant_Admin，我希望只能查看和操作自己管辖门店的数据，以确保多门店之间的数据安全隔离。

#### 验收标准

1. THE Backend_API SHALL 在所有租户管理端点的数据查询中附加 `site_id IN (managed_site_ids)` 条件，其中 `managed_site_ids` 从当前 Tenant_Admin 的 JWT 令牌或 `auth.tenant_admins` 记录中获取
2. WHEN Tenant_Admin 的 `managed_site_ids` 包含多个 site_id, THE Backend_API SHALL 支持跨门店聚合查询（合并多个 site_id 的结果）
3. IF Tenant_Admin 尝试访问不在其 `managed_site_ids` 范围内的数据, THEN THE Backend_API SHALL 返回 403 状态码
4. THE Tenant_Admin_Web SHALL 在页面顶部提供门店筛选器，允许 Tenant_Admin 在管辖范围内切换或选择门店

### 需求 3：用户申请审核

**用户故事：** 作为 Tenant_Admin，我希望查看和审核小程序用户的入驻申请，以便控制哪些用户可以使用系统。

#### 验收标准

1. WHEN Tenant_Admin 打开用户审核页面, THE Tenant_Admin_Web SHALL 从 Backend_API 获取管辖门店范围内的用户申请列表，支持按状态筛选（全部 / 待审核 pending / 已通过 approved / 已拒绝 rejected），支持分页
2. THE Backend_API SHALL 返回每条申请的以下信息：申请人昵称、手机号、球房编号（site_code）、申请角色文本、员工编号、申请时间、当前状态
3. WHEN Tenant_Admin 查看某条待审核申请的关联建议, THE Backend_API SHALL 根据申请中的球房编号通过 `auth.site_code_mapping` 查得 `site_id`，再并行匹配 `fdw_etl.v_dim_assistant`（phone 匹配，`scd2_is_current=1`）和 `fdw_etl.v_dim_staff` + `v_dim_staff_ex`（phone 匹配），返回匹配建议列表
4. WHEN Tenant_Admin 审核通过一条申请, THE Backend_API SHALL 接受角色（助教/管理者/员工）、关联助教 ID（可选）、关联员工 ID（可选），执行以下操作：更新 `auth.users.status = 'approved'`、写入 `auth.user_site_roles`（分配角色）、写入 `auth.user_assistant_binding`（关联助教/员工，含 staff_id）、更新 `auth.user_applications.status = 'approved'` 及审核人和审核时间
5. WHEN Tenant_Admin 审核拒绝一条申请, THE Backend_API SHALL 接受拒绝原因（必填），更新 `auth.user_applications.status = 'rejected'`、`review_note` 和审核时间
6. IF 申请状态不是 pending, THEN THE Backend_API SHALL 拒绝审核操作并返回 409 状态码

### 需求 4：用户管理

**用户故事：** 作为 Tenant_Admin，我希望管理已通过审核的用户信息，包括修改角色、店铺归属和助教绑定关系，以便维护用户数据的准确性。

#### 验收标准

1. WHEN Tenant_Admin 打开用户管理页面, THE Tenant_Admin_Web SHALL 展示管辖门店范围内已通过审核的用户列表，包含姓名、角色、关联助教姓名、所属门店、账号状态，支持按角色筛选和关键词搜索，支持分页
2. WHEN Tenant_Admin 编辑用户信息, THE Backend_API SHALL 允许修改以下字段：角色（助教/管理者/员工）、所属门店（`site_id`，限管辖范围内）、账号状态（启用/禁用）
3. WHEN Tenant_Admin 修改用户的助教/员工绑定关系, THE Backend_API SHALL 更新 `auth.user_assistant_binding` 记录，接受新的 `assistant_id` 和/或 `staff_id`
4. WHEN Tenant_Admin 禁用某用户账号, THE Backend_API SHALL 将 `auth.users.status` 设为 `disabled`，该用户后续登录小程序时 SHALL 被拒绝
5. IF Tenant_Admin 尝试将用户的 site_id 修改为不在其管辖范围内的值, THEN THE Backend_API SHALL 返回 403 状态码

### 需求 5：Excel 上传 — 文件解析与格式校验

**用户故事：** 作为 Tenant_Admin，我希望上传 Excel 文件并获得即时的格式校验反馈，以便在数据写入前发现和修正错误。

#### 验收标准

1. WHEN Tenant_Admin 选择模板类型并上传 Excel 文件, THE Backend_API SHALL 解析文件内容，按对应模板的列定义进行格式校验，并返回校验结果
2. THE Backend_API SHALL 支持 4 种模板类型的格式校验：
   - 财务支出（expense）：月份（YYYY-MM，不超过当前月）、支出类别（枚举 8 值：房租/水电/物业/食品饮料进货/耗材/报销/固定人员工资/其他费用）、金额（> 0，精度 2 位小数）、备注（可选，最长 500 字符）
   - 团购收入（platform_income）：月份（YYYY-MM）、平台名称（非空）、收入金额（> 0）、备注（可选，最长 500 字符）
   - 助教奖罚（salary_adj）：月份（YYYY-MM）、助教姓名（非空）、助教编号（非空）、类型（枚举：扣款/奖金）、金额（> 0）、原因（非空，最长 200 字符）
   - 充值业绩归属（recharge_commission）：充值日期（YYYY-MM-DD）、会员名称（非空）、充值金额（> 0）、归属助教（非空）、奖励金额（≥ 0）
3. WHEN 校验发现格式错误行, THE Backend_API SHALL 返回错误行号、列名和具体错误描述，Tenant_Admin_Web SHALL 标红展示错误行
4. WHEN 校验全部通过, THE Backend_API SHALL 创建 `biz.excel_upload_log` 记录（状态为 pending），返回 upload_id 和解析后的数据预览
5. IF 上传文件不是有效的 Excel 格式（.xlsx/.xls）, THEN THE Backend_API SHALL 返回 400 状态码并提示文件格式错误

### 需求 6：Excel 上传 — 人员匹配校验

**用户故事：** 作为 Tenant_Admin，我希望上传助教奖罚和充值业绩归属数据时，系统能自动校验助教信息的准确性，以减少数据录入错误。

#### 验收标准

1. WHEN 上传助教奖罚（salary_adj）或充值业绩归属（recharge_commission）模板时, THE Backend_API SHALL 对每行数据中的助教姓名+助教编号执行匹配校验
2. THE Backend_API SHALL 按以下顺序匹配：先查 `fdw_etl.v_dim_assistant`（nickname + assistant_number，`scd2_is_current=1`），如不匹配再查 `fdw_etl.v_dim_staff` + `v_dim_staff_ex`（name + staff_number）
3. WHEN 匹配成功, THE Backend_API SHALL 在返回数据中填充 `assistant_id`
4. WHEN 匹配失败, THE Backend_API SHALL 标记该行为校验警告（warning），不阻断上传流程，但在前端以黄色高亮提示 Tenant_Admin 确认
5. THE Tenant_Admin_Web SHALL 在校验结果页面汇总展示：通过行数、警告行数、错误行数

### 需求 7：Excel 上传 — 冲突检测与解决

**用户故事：** 作为 Tenant_Admin，我希望在数据写入前看到与已有数据的冲突情况，并能逐行选择处理方式，以避免误覆盖历史数据。

#### 验收标准

1. WHEN 格式校验通过后, THE Backend_API SHALL 按各模板的主键规则检测冲突：
   - 财务支出：月份 + 支出类别
   - 团购收入：月份 + 平台名称
   - 助教奖罚：月份 + 助教姓名 + 助教编号 + 类型 + 原因
   - 充值业绩归属：充值日期 + 会员名称 + 归属助教
2. WHEN 检测到冲突行（主键已存在）, THE Backend_API SHALL 返回 diff 数据（旧值 vs 新值，按字段逐一对比）
3. THE Tenant_Admin_Web SHALL 展示 diff 交互表格，每行显示字段名、旧值、新值和操作选项（替换/保留），支持"全部替换"和"全部保留"快捷操作
4. WHEN Tenant_Admin 确认冲突解决方案并提交, THE Backend_API SHALL 接受 upload_id 和每行的解决方案（resolutions 数组），按选择执行写入
5. WHEN 无冲突行, THE Backend_API SHALL 直接标记为"待写入"，Tenant_Admin 确认后写入

### 需求 8：Excel 上传 — 数据写入与记录

**用户故事：** 作为 Tenant_Admin，我希望确认后的数据能正确写入目标表，并保留上传历史记录以便追溯。

#### 验收标准

1. WHEN Tenant_Admin 确认写入, THE Backend_API SHALL 将数据写入对应目标表：
   - 助教奖罚 → `biz.salary_adjustments`（直接写入业务库）
   - 财务支出 → `biz.stg_finance_expense`（staging 表，由 ETL 同步到 DWS）
   - 团购收入 → `biz.stg_platform_income`（staging 表）
   - 充值业绩归属 → `biz.stg_recharge_commission`（staging 表）
2. THE Backend_API SHALL 在写入完成后更新 `biz.excel_upload_log` 记录：状态设为 `confirmed`、记录实际写入行数、冲突解决数、确认时间
3. IF 写入过程中发生数据库错误, THEN THE Backend_API SHALL 回滚整个批次的写入，将 `biz.excel_upload_log` 状态设为 `failed`，记录错误详情到 `error_detail` 字段
4. WHEN Tenant_Admin 查看上传记录页面, THE Backend_API SHALL 返回管辖门店范围内的历史上传记录列表，包含模板类型、文件名、上传人、上传时间、行数、冲突数、状态，支持分页
5. WHEN Tenant_Admin 下载空白模板, THE Backend_API SHALL 返回对应模板类型的 Excel 文件（含表头和格式说明）

### 需求 9：维客线索 — 客户搜索

**用户故事：** 作为 Tenant_Admin，我希望通过客户姓名或手机号搜索客户，以便查看和管理其维客线索。

#### 验收标准

1. WHEN Tenant_Admin 输入搜索关键词, THE Backend_API SHALL 在管辖门店范围内搜索客户，匹配 `fdw_etl.v_dim_member` 的 `nickname`（模糊匹配）或 `mobile`（精确匹配），返回客户列表（member_id、姓名、手机号脱敏、所属门店）
2. THE Backend_API SHALL 通过 `member_id` JOIN `fdw_etl.v_dim_member`（`scd2_is_current=1`）获取客户姓名和手机号，不使用结算单上的冗余字段（DQ-6 规则）
3. WHEN Tenant_Admin 选择门店筛选条件, THE Backend_API SHALL 在搜索结果中仅返回该门店的客户
4. IF 搜索结果为空, THEN THE Tenant_Admin_Web SHALL 展示"未找到匹配客户"的提示

### 需求 10：维客线索 — 线索列表与展示

**用户故事：** 作为 Tenant_Admin，我希望查看某客户的全部维客线索，以便了解客户画像和历史记录。

#### 验收标准

1. WHEN Tenant_Admin 选择某客户, THE Backend_API SHALL 返回该客户在管辖门店范围内的全部维客线索列表，包含：线索 ID、大类标签（category）、摘要（summary）、详情（detail）、提供人（recorded_by_name）、来源（source：manual / ai_consumption / ai_note）、记录时间（recorded_at）、隐藏状态（is_hidden）
2. THE Tenant_Admin_Web SHALL 按大类标签分组展示线索，支持按来源和隐藏状态筛选
3. THE Tenant_Admin_Web SHALL 对已隐藏的线索以灰色或删除线样式区分展示

### 需求 11：维客线索 — 编辑

**用户故事：** 作为 Tenant_Admin，我希望修改维客线索的标签、摘要和详情，以便纠正错误或补充信息。

#### 验收标准

1. WHEN Tenant_Admin 编辑某条线索, THE Backend_API SHALL 接受修改后的 category（枚举 6 值：客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈）、summary（非空，最长 200 字符）、detail（可选）
2. THE Backend_API SHALL 验证 category 值在枚举范围内，summary 非空且不超过长度限制
3. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码

### 需求 12：维客线索 — 删除

**用户故事：** 作为 Tenant_Admin，我希望删除错误或无效的维客线索，以保持数据整洁。

#### 验收标准

1. WHEN Tenant_Admin 请求删除某条线索, THE Tenant_Admin_Web SHALL 弹出二次确认对话框，明确提示"删除后不可恢复"
2. WHEN Tenant_Admin 确认删除, THE Backend_API SHALL 对该线索执行物理删除（`DELETE FROM public.member_retention_clue WHERE id = ?`）
3. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码

### 需求 13：维客线索 — 隐藏与显示

**用户故事：** 作为 Tenant_Admin，我希望隐藏某些线索使其不在小程序端展示，同时保留在管理后台可见，以便灵活控制线索的可见性。

#### 验收标准

1. WHEN Tenant_Admin 切换某条线索的隐藏状态, THE Backend_API SHALL 更新 `public.member_retention_clue.is_hidden` 字段（true=隐藏，false=显示）
2. WHILE 线索的 `is_hidden = true`, THE Backend_API SHALL 确保小程序端查询线索时通过 `WHERE is_hidden = false` 条件过滤该线索
3. THE Tenant_Admin_Web SHALL 允许 Tenant_Admin 将已隐藏的线索恢复为显示状态（`is_hidden = false`）
4. IF 线索 ID 不存在或不在管辖门店范围内, THEN THE Backend_API SHALL 返回 404 状态码

### 需求 14：管理后台 — 租户管理员账号管理

**用户故事：** 作为 Operator，我希望在系统管理后台（admin-web）中创建、编辑和管理租户管理员账号，以便控制谁可以登录租户管理后台。

#### 验收标准

1. WHEN Operator 打开租户管理员管理页面, THE admin-web SHALL 展示所有租户管理员列表，包含用户名、显示名称、管辖门店、账号状态（启用/禁用）、创建时间、最后登录时间，支持分页和关键词搜索
2. WHEN Operator 创建新的租户管理员, THE Backend_API SHALL 接受用户名（唯一）、初始密码、显示名称、tenant_id、managed_site_ids（门店列表），将密码 bcrypt 哈希后写入 `auth.tenant_admins`，记录 `created_by` 为当前 Operator 的 user_id
3. IF 用户名已存在, THEN THE Backend_API SHALL 返回 409 状态码并提示"用户名已存在"
4. WHEN Operator 编辑租户管理员信息, THE Backend_API SHALL 允许修改以下字段：显示名称、managed_site_ids、is_active（启用/禁用）
5. WHEN Operator 重置租户管理员密码, THE Backend_API SHALL 接受新密码，bcrypt 哈希后更新 `auth.tenant_admins.password_hash`
6. IF Operator 禁用某租户管理员（is_active=false）, THEN 该管理员后续登录租户管理后台时 SHALL 被拒绝（返回 403）
7. THE Backend_API SHALL 要求 Operator 具有 site_admin 或 tenant_admin 角色才能访问租户管理员管理端点

### 需求 15：数据库变更 — 新建表

**用户故事：** 作为开发者，我需要创建 NS4 所需的新数据库表，以支撑租户管理后台的全部功能。

#### 验收标准

1. THE 迁移脚本 SHALL 在 `auth` Schema 中创建 `tenant_admins` 表，包含字段：id（BIGSERIAL PK）、username（VARCHAR(50) UNIQUE NOT NULL）、password_hash（VARCHAR(255) NOT NULL）、display_name（VARCHAR(100)）、tenant_id（BIGINT NOT NULL）、managed_site_ids（BIGINT[] NOT NULL）、is_active（BOOLEAN DEFAULT true）、created_by（BIGINT）、created_at（TIMESTAMPTZ DEFAULT NOW()）、last_login_at（TIMESTAMPTZ），并创建 tenant_id 索引
2. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `salary_adjustments` 表，包含字段：id（BIGSERIAL PK）、site_id（BIGINT NOT NULL）、assistant_id（BIGINT 可空）、assistant_name（VARCHAR(100) NOT NULL）、assistant_number（VARCHAR(50) NOT NULL）、salary_month（VARCHAR(7) NOT NULL）、adjustment_type（CHECK IN deduction/bonus）、amount（NUMERIC(12,2) > 0）、reason（VARCHAR(200) NOT NULL）、upload_batch_id（FK → excel_upload_log）、created_at、created_by，并创建 (site_id, salary_month) 和 (assistant_id, salary_month) 索引
3. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `excel_upload_log` 表，包含字段：id（BIGSERIAL PK）、site_id（BIGINT NOT NULL）、upload_type（CHECK IN expense/platform_income/salary_adj/recharge_commission）、file_name（VARCHAR(255) NOT NULL）、uploaded_by（BIGINT NOT NULL）、row_count（INTEGER DEFAULT 0）、conflict_count（INTEGER DEFAULT 0）、resolved_count（INTEGER DEFAULT 0）、status（CHECK IN pending/confirmed/failed）、error_detail（JSONB）、created_at、confirmed_at，并创建 (site_id, created_at DESC) 索引
4. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 3 张 staging 表：`stg_finance_expense`、`stg_platform_income`、`stg_recharge_commission`，各表包含 site_id、业务字段、upload_batch_id（FK）、synced_at（TIMESTAMPTZ 可空，NULL 表示未同步）、created_at

### 需求 16：数据库变更 — 表结构修改

**用户故事：** 作为开发者，我需要为现有表添加 NS4 所需的字段，以支持维客线索隐藏功能。

#### 验收标准

1. THE 迁移脚本 SHALL 为 `public.member_retention_clue` 表添加 `is_hidden` 列（BOOLEAN NOT NULL DEFAULT false），并添加列注释说明"是否隐藏（true=管理后台保留但小程序不展示）"
2. THE 迁移脚本 SHALL 确保已有数据的 `is_hidden` 值为 false（通过 DEFAULT 约束保证）
3. THE 小程序端现有的线索查询 API SHALL 在查询条件中增加 `WHERE is_hidden = false`，确保隐藏线索不在小程序端展示

### 需求 17：前端应用骨架

**用户故事：** 作为开发者，我需要搭建租户管理后台的前端项目骨架，以便后续功能页面的开发。

#### 验收标准

1. THE 前端项目 SHALL 在 `apps/tenant-admin/` 目录下创建，使用 React + Vite + Ant Design 技术栈，包含 `package.json`、`vite.config.ts`、`tsconfig.json` 等配置文件
2. THE Tenant_Admin_Web SHALL 提供侧边栏导航，包含四个功能模块入口：用户审核、用户管理、Excel 上传、维客线索管理
3. THE Tenant_Admin_Web SHALL 实现登录页面，未认证时自动重定向到登录页
4. THE Tenant_Admin_Web SHALL 封装统一的 API 调用层（`services/api.ts`），处理 JWT 令牌的自动附加、刷新和过期重定向
5. THE Tenant_Admin_Web SHALL 使用与 Backend_API 一致的响应格式（`{ code: 0, data: ... }`），Pydantic 使用 `alias_generator=to_camel` 实现驼峰命名转换

### 需求 18：后端路由模块

**用户故事：** 作为开发者，我需要在 FastAPI 后端中创建租户管理专用的路由模块，以提供 NS4 所需的全部 API 端点。

#### 验收标准

1. THE Backend_API SHALL 在 `apps/backend/app/routers/` 下创建 4 个路由文件：`tenant_auth.py`（登录/JWT 签发/鉴权）、`tenant_users.py`（用户审核+用户管理）、`tenant_excel.py`（Excel 上传/校验/冲突处理）、`tenant_clues.py`（维客线索管理），以及 1 个管理端路由文件 `admin_tenant_admins.py`（租户管理员 CRUD）
2. THE Backend_API SHALL 将所有租户管理端点注册在 `/api/tenant/` 路径前缀下，管理端租户管理员 CRUD 端点注册在 `/api/admin/tenant-admins/` 路径前缀下，与小程序端点（`/api/xcx/`）隔离
3. THE Backend_API SHALL 为所有 `/api/tenant/*` 端点（登录接口除外）添加 `require_tenant_admin()` 认证依赖，验证 JWT 的 `aud` 字段为 `tenant-admin`；为 `/api/admin/tenant-admins/*` 端点添加 `_require_admin()` 认证依赖，验证 site_admin 或 tenant_admin 角色
4. THE Backend_API SHALL 遵循现有的 API 响应格式约定：成功返回 `{ code: 0, data: ... }`，失败返回 `{ code: number, message: string }`，分页返回 `{ items: T[], total: number, page: number, pageSize: number }`
