feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs

This commit is contained in:
Neo
2026-03-20 09:02:10 +08:00
parent 3d2e5f8165
commit beb88d5bea
388 changed files with 6436 additions and 25458 deletions

View File

@@ -0,0 +1 @@
{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,623 @@
# 技术设计文档租户管理后台tenant-admin-web
## 概述
本设计为 NS4 租户管理后台提供完整的技术方案。系统面向租户管理员Tenant_Admin提供用户审核与管理、Excel 数据上传4 种模板)、维客线索管理三大功能模块。
前端为独立 React 应用(`apps/tenant-admin/`),后端复用现有 FastAPI 服务(`apps/backend/`)新增 4 个路由模块,所有端点注册在 `/api/tenant/` 前缀下。认证体系与小程序完全隔离,使用独立的 `auth.tenant_admins` 表、用户名+密码登录、JWT `aud=tenant-admin`
### 关键设计决策
| 决策 | 选择 | 理由 |
|------|------|------|
| 认证表 | 独立 `auth.tenant_admins` | 与小程序 `auth.users`(微信登录)完全隔离,避免 user_type 判断复杂度 |
| JWT 隔离 | `aud` 字段区分 | 复用现有 `jose` + `bcrypt` 基础设施,仅扩展 payload |
| Excel 写入策略 | staging 表 + ETL 同步(方案 B | 数据经 ETL 标准流程,一致性有保障;助教奖罚直接写 biz |
| FDW 多店铺查询 | 逐 site_id 查询合并 | RLS 视图要求 `SET LOCAL app.current_site_id`,单次只能设一个 |
| 前端技术栈 | React + Vite + Ant Design | 与 `apps/admin-web/` 一致,降低维护成本 |
| 响应格式 | `{ code: 0, data }` | 复用现有 `ResponseWrapperMiddleware`,前端统一解包 |
---
## 架构
### 高层架构图
```mermaid
graph TB
subgraph "前端 apps/tenant-admin/"
FE[React + Vite + Ant Design]
API_LAYER[services/api.ts<br/>JWT 自动附加/刷新]
end
subgraph "后端 apps/backend/"
subgraph "路由层 /api/tenant/*"
R_AUTH[tenant_auth.py<br/>登录/刷新]
R_USERS[tenant_users.py<br/>审核+管理]
R_EXCEL[tenant_excel.py<br/>上传/校验/冲突]
R_CLUES[tenant_clues.py<br/>线索管理]
end
AUTH_DEP[require_tenant_admin<br/>认证依赖注入]
MIDDLEWARE[ResponseWrapperMiddleware<br/>全局响应包装]
end
subgraph "数据层"
APP_DB[(zqyy_app<br/>auth + biz + public)]
ETL_DB[(etl_feiqiu<br/>fdw_etl 视图)]
end
FE --> API_LAYER
API_LAYER -->|HTTP| MIDDLEWARE
MIDDLEWARE --> R_AUTH
MIDDLEWARE --> R_USERS
MIDDLEWARE --> R_EXCEL
MIDDLEWARE --> R_CLUES
R_USERS --> AUTH_DEP
R_EXCEL --> AUTH_DEP
R_CLUES --> AUTH_DEP
R_AUTH -->|登录无需认证| APP_DB
AUTH_DEP --> APP_DB
R_USERS -->|FDW 人员匹配| ETL_DB
R_EXCEL -->|FDW 助教匹配| ETL_DB
R_CLUES -->|FDW 客户搜索| ETL_DB
```
### 认证流程
```mermaid
sequenceDiagram
participant FE as Tenant Admin Web
participant BE as Backend API
participant DB as auth.tenant_admins
FE->>BE: POST /api/tenant/auth/login {username, password}
BE->>DB: SELECT password_hash WHERE username=? AND is_active=true
alt 凭据有效
BE->>BE: bcrypt.checkpw() 验证
BE->>BE: 签发 JWT (aud=tenant-admin, sub=admin_id, managed_site_ids)
BE-->>FE: {access_token, refresh_token}
else 凭据无效
BE-->>FE: 401 用户名或密码错误
else 账号禁用
BE-->>FE: 403 账号已被禁用
end
Note over FE,BE: 后续请求
FE->>BE: GET /api/tenant/* (Authorization: Bearer <token>)
BE->>BE: require_tenant_admin() 验证 aud=tenant-admin
BE->>BE: 提取 managed_site_ids附加数据隔离条件
```
### Excel 上传流程
```mermaid
flowchart TD
A[选择模板类型 + 上传 Excel] --> B[后端解析 + 格式校验]
B --> C{校验结果}
C -->|格式错误| D[返回错误行号/列名/描述<br/>前端标红展示]
D --> A
C -->|校验通过| E[人员匹配校验<br/>仅 salary_adj / recharge_commission]
E --> F[冲突检测<br/>按主键规则匹配]
F --> G{有冲突?}
G -->|是| H[返回 diff 数据<br/>前端展示 diff 表格]
H --> I[用户逐行选择<br/>替换/保留]
G -->|否| J[标记为待写入]
I --> K[确认写入]
J --> K
K --> L{写入目标}
L -->|助教奖罚| M[biz.salary_adjustments]
L -->|财务支出| N[biz.stg_finance_expense]
L -->|团购收入| O[biz.stg_platform_income]
L -->|充值归属| P[biz.stg_recharge_commission]
K --> Q[更新 excel_upload_log<br/>status=confirmed]
```
---
## 组件与接口
### 后端路由模块
#### 1. `tenant_auth.py` — 认证路由
| 端点 | 方法 | 路径 | 认证 | 说明 |
|------|------|------|------|------|
| 登录 | POST | `/api/tenant/auth/login` | 无 | 用户名+密码 → JWT 令牌对 |
| 刷新 | POST | `/api/tenant/auth/refresh` | 无 | refresh_token → 新令牌对 |
#### 2. `tenant_users.py` — 用户审核 + 管理路由
| 端点 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 申请列表 | GET | `/api/tenant/applications` | status 筛选 + 分页 |
| 关联建议 | GET | `/api/tenant/applications/{id}/match-suggestions` | FDW 助教/员工匹配 |
| 审核通过 | POST | `/api/tenant/applications/{id}/approve` | 分配角色 + 绑定 |
| 审核拒绝 | POST | `/api/tenant/applications/{id}/reject` | 填写拒绝原因 |
| 用户列表 | GET | `/api/tenant/users` | 角色筛选 + 搜索 + 分页 |
| 编辑用户 | PATCH | `/api/tenant/users/{id}` | 修改角色/门店/状态 |
| 修改绑定 | PUT | `/api/tenant/users/{id}/binding` | 更新助教/员工绑定 |
#### 3. `tenant_excel.py` — Excel 上传路由
| 端点 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 上传解析 | POST | `/api/tenant/excel/upload` | multipart/form-data校验+冲突检测 |
| 确认写入 | POST | `/api/tenant/excel/confirm` | upload_id + resolutions[] |
| 上传记录 | GET | `/api/tenant/excel/logs` | 历史记录列表 + 分页 |
| 模板下载 | GET | `/api/tenant/excel/template/{type}` | 下载空白 Excel 模板 |
#### 4. `tenant_clues.py` — 维客线索路由
| 端点 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 客户搜索 | GET | `/api/tenant/customers/search` | keyword + site_id 筛选 |
| 线索列表 | GET | `/api/tenant/customers/{member_id}/clues` | 该客户全部线索 |
| 修改线索 | PATCH | `/api/tenant/clues/{id}` | category + summary + detail |
| 删除线索 | DELETE | `/api/tenant/clues/{id}` | 物理删除 |
| 隐藏/显示 | PATCH | `/api/tenant/clues/{id}/visibility` | is_hidden 切换 |
### 认证依赖注入
```python
@dataclass(frozen=True)
class CurrentTenantAdmin:
"""从 JWT 解析出的租户管理员上下文。"""
admin_id: int
tenant_id: int
managed_site_ids: list[int]
display_name: str | None = None
async def require_tenant_admin(
credentials: HTTPAuthorizationCredentials = Depends(_bearer_scheme),
) -> CurrentTenantAdmin:
"""
验证 JWT aud=tenant-admin提取管理员信息。
拒绝小程序 JWTaud 不匹配)。
"""
payload = decode_access_token(token) # 复用现有解码
if payload.get("aud") != "tenant-admin":
raise HTTPException(401, "令牌类型不匹配")
return CurrentTenantAdmin(
admin_id=int(payload["sub"]),
tenant_id=payload["tenant_id"],
managed_site_ids=payload["managed_site_ids"],
)
```
数据隔离通过 `managed_site_ids` 实现:
```python
def site_filter_clause(admin: CurrentTenantAdmin) -> tuple[str, tuple]:
"""生成 site_id IN (...) SQL 片段。"""
placeholders = ",".join(["%s"] * len(admin.managed_site_ids))
return f"site_id IN ({placeholders})", tuple(admin.managed_site_ids)
```
### 前端组件结构
```
apps/tenant-admin/src/
├── pages/
│ ├── Login/ # 登录页
│ ├── UserApproval/ # 用户审核(申请列表 + 关联建议 + 审核操作)
│ ├── UserManagement/ # 用户管理(列表 + 编辑 + 绑定)
│ ├── ExcelUpload/ # Excel 上传(模板选择 + 上传 + 校验 + diff + 确认)
│ └── RetentionClues/ # 维客线索(客户搜索 + 线索列表 + 编辑/删除/隐藏)
├── components/
│ ├── SiteSelector/ # 门店筛选器(页面顶部)
│ ├── DiffTable/ # 冲突 diff 交互表格
│ └── ClueEditor/ # 线索编辑表单
├── services/
│ └── api.ts # API 调用封装JWT 自动附加/刷新)
├── hooks/
│ └── useAuth.ts # 认证状态管理
├── utils/
│ └── format.ts # 格式化工具(手机号脱敏等)
└── App.tsx # 路由配置 + 侧边栏布局
```
### API 调用层设计
复用 `apps/admin-web/src/api/client.ts` 的模式:
- axios 实例,`baseURL: "/api/tenant"`
- 请求拦截器:从 localStorage 读取 access_token 附加 Authorization header
- 响应拦截器401 时用 refresh_token 刷新,刷新失败重定向 `/login`
- 并发刷新保护:多个 401 只触发一次 refresh其余排队
- 响应解包:`{ code: 0, data }` → 提取 `data`
### 响应格式约定
成功响应(由 `ResponseWrapperMiddleware` 自动包装):
```json
{ "code": 0, "data": { ... } }
```
错误响应(由异常处理器格式化):
```json
{ "code": 401, "message": "用户名或密码错误" }
```
分页响应:
```json
{
"code": 0,
"data": {
"items": [...],
"total": 100,
"page": 1,
"pageSize": 20
}
}
```
Pydantic 模型使用 `alias_generator=to_camel` 实现 snake_case → camelCase 转换。
---
## 数据模型
### 新建表
#### 1. `auth.tenant_admins` — 租户管理员表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| username | VARCHAR(50) | UNIQUE NOT NULL | 登录用户名 |
| password_hash | VARCHAR(255) | NOT NULL | bcrypt 哈希 |
| display_name | VARCHAR(100) | — | 显示名称 |
| tenant_id | BIGINT | NOT NULL | 所属租户 |
| managed_site_ids | BIGINT[] | NOT NULL | 管辖门店 ID 列表 |
| is_active | BOOLEAN | DEFAULT true | 账号状态 |
| created_by | BIGINT | — | 创建者Operator ID |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| last_login_at | TIMESTAMPTZ | — | 最后登录时间 |
索引:`idx_tenant_admin_tenant ON (tenant_id)`
#### 2. `biz.excel_upload_log` — Excel 上传记录表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| upload_type | VARCHAR(30) | CHECK IN (expense, platform_income, salary_adj, recharge_commission) | 模板类型 |
| file_name | VARCHAR(255) | NOT NULL | 原始文件名 |
| uploaded_by | BIGINT | NOT NULL | 上传人(管理员 ID |
| row_count | INTEGER | DEFAULT 0 | 数据行数 |
| conflict_count | INTEGER | DEFAULT 0 | 冲突行数 |
| resolved_count | INTEGER | DEFAULT 0 | 已解决冲突数 |
| status | VARCHAR(20) | CHECK IN (pending, confirmed, failed) | 批次状态 |
| error_detail | JSONB | — | 错误详情 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 上传时间 |
| confirmed_at | TIMESTAMPTZ | — | 确认时间 |
索引:`idx_excel_log_site ON (site_id, created_at DESC)`
#### 3. `biz.salary_adjustments` — 助教奖罚明细表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| assistant_id | BIGINT | — | 匹配到的助教 ID可空 |
| assistant_name | VARCHAR(100) | NOT NULL | 助教姓名 |
| assistant_number | VARCHAR(50) | NOT NULL | 助教编号 |
| salary_month | VARCHAR(7) | NOT NULL | 月份 YYYY-MM |
| adjustment_type | VARCHAR(20) | CHECK IN (deduction, bonus) | 类型 |
| amount | NUMERIC(12,2) | CHECK > 0 | 金额 |
| reason | VARCHAR(200) | NOT NULL | 原因 |
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| created_by | BIGINT | — | 上传人 |
索引:`(site_id, salary_month)`, `(assistant_id, salary_month)`
#### 4. `biz.stg_finance_expense` — 财务支出暂存表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| expense_month | VARCHAR(7) | NOT NULL | 月份 |
| category | VARCHAR(50) | NOT NULL | 支出类别8 值枚举) |
| amount | NUMERIC(12,2) | NOT NULL | 金额 |
| remark | TEXT | — | 备注 |
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间NULL=未同步) |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
#### 5. `biz.stg_platform_income` — 团购收入暂存表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| income_month | VARCHAR(7) | NOT NULL | 月份 |
| platform_name | VARCHAR(100) | NOT NULL | 平台名称 |
| amount | NUMERIC(12,2) | NOT NULL | 收入金额 |
| remark | TEXT | — | 备注 |
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
#### 6. `biz.stg_recharge_commission` — 充值业绩归属暂存表
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PK | 自增主键 |
| site_id | BIGINT | NOT NULL | 门店 ID |
| recharge_date | DATE | NOT NULL | 充值日期 |
| member_name | VARCHAR(100) | NOT NULL | 会员名称 |
| recharge_amount | NUMERIC(12,2) | NOT NULL | 充值金额 |
| assigned_assistant | VARCHAR(100) | NOT NULL | 归属助教 |
| reward_amount | NUMERIC(12,2) | NOT NULL | 奖励金额 |
| upload_batch_id | BIGINT | FK → excel_upload_log | 上传批次 |
| synced_at | TIMESTAMPTZ | — | ETL 同步时间 |
| created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
### 表结构变更
#### `public.member_retention_clue` — 新增 `is_hidden` 列
```sql
ALTER TABLE public.member_retention_clue
ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN public.member_retention_clue.is_hidden
IS '是否隐藏true=管理后台保留但小程序不展示)';
```
已有数据通过 `DEFAULT false` 保证兼容。小程序端线索查询需增加 `WHERE is_hidden = false`
### 复用的现有表auth Schema
| 表 | 用途 |
|---|------|
| `auth.users` | 小程序用户主表(审核时更新 status |
| `auth.user_applications` | 用户入驻申请(审核列表数据源) |
| `auth.site_code_mapping` | 球房编号 → site_id 映射 |
| `auth.roles` | 角色定义coach/staff/site_admin/tenant_admin |
| `auth.user_site_roles` | 用户-门店-角色关联(审核通过时写入) |
| `auth.user_assistant_binding` | 用户-助教/员工绑定(审核通过时写入) |
### FDW 数据源ETL 库只读)
| 视图 | 用途 | 查询场景 |
|------|------|---------|
| `fdw_etl.v_dim_assistant` | 助教维度表 | 用户审核关联建议、Excel 人员匹配 |
| `fdw_etl.v_dim_staff` + `v_dim_staff_ex` | 员工维度表 | 用户审核关联建议、Excel 人员匹配 |
| `fdw_etl.v_dim_member` | 会员维度表 | 维客线索客户搜索DQ-6 规则) |
多店铺查询策略:逐 `site_id` 调用 `get_etl_readonly_connection(site_id)` 查询后合并结果。
### Pydantic Schema 设计
```python
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class TenantBaseModel(BaseModel):
"""租户管理后台基础模型,统一驼峰命名。"""
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class PaginatedResponse(TenantBaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
```
---
## 正确性属性Correctness Properties
*属性Property是在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: 认证正确性
*For any* 用户名和密码组合,如果该组合对应一个活跃的 `auth.tenant_admins` 记录且密码哈希匹配,则登录应返回包含 `aud=tenant-admin` 的有效 JWT 令牌对;如果用户名不存在或密码不匹配,则应返回 401 且错误消息不区分是用户名还是密码错误。
**Validates: Requirements 1.1, 1.2**
### Property 2: JWT 隔离与端点保护
*For any* JWT 令牌和任意 `/api/tenant/*` 受保护端点,当且仅当令牌有效且 `aud=tenant-admin` 时请求应被允许;`aud` 不匹配(如小程序 JWT `aud=xcx`)、令牌过期或令牌无效时应返回 401。
**Validates: Requirements 1.4, 1.6, 17.3**
### Property 3: 数据隔离
*For any* 租户管理员和任意数据查询端点,返回结果中每条记录的 `site_id` 必须属于该管理员的 `managed_site_ids` 集合;尝试访问不在 `managed_site_ids` 范围内的数据应返回 403。
**Validates: Requirements 2.1, 2.2, 2.3**
### Property 4: 审核通过多表一致性
*For any* 状态为 `pending` 的用户申请,执行审核通过操作后,以下条件应同时成立:`auth.users.status = 'approved'``auth.user_site_roles` 中存在对应角色记录、`auth.user_assistant_binding` 中存在绑定记录(如提供了 assistant_id/staff_id`auth.user_applications.status = 'approved'` 且审核人和审核时间已填写。
**Validates: Requirements 3.4**
### Property 5: 审核拒绝状态更新
*For any* 状态为 `pending` 的用户申请和任意非空拒绝原因字符串,执行审核拒绝操作后,`auth.user_applications.status` 应为 `rejected``review_note` 应等于提交的拒绝原因,`reviewed_at` 应非空。
**Validates: Requirements 3.5**
### Property 6: 申请关联匹配
*For any* 用户申请中的手机号和球房编号,关联匹配逻辑应:先通过 `site_code_mapping` 解析 `site_id`,然后在 `v_dim_assistant``scd2_is_current=1`)和 `v_dim_staff` + `v_dim_staff_ex` 中按 phone 匹配,返回的候选列表应仅包含 phone 匹配的记录。
**Validates: Requirements 3.3**
### Property 7: 用户管理编辑
*For any* 已通过审核的用户和任意有效的编辑参数角色、site_id 在管辖范围内、状态),编辑操作后用户记录应反映新值;修改绑定关系后 `user_assistant_binding` 应更新为新的 `assistant_id`/`staff_id`
**Validates: Requirements 4.2, 4.3, 4.4**
### Property 8: Excel 格式校验
*For any* 模板类型和任意数据行集合,校验器应:对符合模板列定义的行标记为通过,对不符合的行返回包含行号、列名和错误描述的错误信息;校验全部通过时应创建 `excel_upload_log` 记录status=pending通过行数 + 警告行数 + 错误行数 = 总行数。
**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 6.5**
### Property 9: 人员匹配校验
*For any* 助教姓名+编号组合和给定的助教/员工数据集,匹配逻辑应:优先在 `v_dim_assistant`nickname + assistant_number中匹配未匹配时再查 `v_dim_staff` + `v_dim_staff_ex`name + staff_number匹配成功时填充 `assistant_id`,匹配失败时标记为 warning 且不阻断流程。
**Validates: Requirements 6.1, 6.2, 6.3, 6.4**
### Property 10: 冲突检测
*For any* 模板类型、上传数据集和已有数据集,冲突检测应:按模板主键规则(如财务支出的"月份+支出类别")识别主键重复的行,对冲突行返回逐字段 diff旧值 vs 新值),非冲突行标记为待写入。
**Validates: Requirements 7.1, 7.2**
### Property 11: 数据写入 round-trip
*For any* 确认写入的数据批次和冲突解决方案,写入后从目标表读取的数据应与提交的数据一致(选择"替换"的行使用新值,选择"保留"的行保持旧值);`excel_upload_log` 状态应更新为 `confirmed`,实际写入行数应正确记录。
**Validates: Requirements 7.4, 7.5, 8.1, 8.2**
### Property 12: 客户搜索
*For any* 搜索关键词和管辖门店范围,搜索结果应满足:每条结果的 `nickname` 包含关键词(模糊匹配)或 `mobile` 等于关键词(精确匹配),且 `site_id` 在管辖范围内;指定门店筛选时结果应仅包含该门店的客户。
**Validates: Requirements 9.1, 9.3**
### Property 13: 线索编辑校验
*For any* 有效的编辑参数category 在 6 值枚举内、summary 非空且 ≤200 字符),编辑操作后线索记录应反映新值;对于无效的 category 或空/超长 summary应拒绝操作。
**Validates: Requirements 11.1, 11.2**
### Property 14: 线索删除
*For any* 存在且在管辖门店范围内的线索,执行删除操作后,该线索应不可通过任何查询接口获取(物理删除)。
**Validates: Requirements 12.2**
### Property 15: 线索隐藏/显示 round-trip
*For any* 线索,将 `is_hidden` 设为 `true` 后,小程序端查询(`WHERE is_hidden = false`)不应返回该线索,但管理后台查询应返回;再将 `is_hidden` 设回 `false` 后,小程序端查询应恢复返回该线索。
**Validates: Requirements 13.1, 13.2, 13.3, 15.3**
### Property 16: 响应格式一致性
*For any* `/api/tenant/*` 端点的成功响应,响应体应符合 `{ code: 0, data: ... }` 格式;错误响应应符合 `{ code: <status_code>, message: <string> }` 格式;分页响应的 `data` 应包含 `items``total``page``pageSize` 字段。
**Validates: Requirements 17.4**
---
## 错误处理
### 认证错误
| 场景 | HTTP 状态码 | 错误消息 | 处理方式 |
|------|------------|---------|---------|
| 用户名或密码错误 | 401 | "用户名或密码错误" | 统一消息,不区分 |
| 账号已禁用 | 403 | "账号已被禁用" | 检查 `is_active` |
| JWT 过期 | 401 | "令牌已过期" | 前端自动刷新 |
| JWT 无效/aud 不匹配 | 401 | "无效的令牌" | 重定向登录页 |
| 刷新令牌过期 | 401 | "刷新令牌已过期" | 清除令牌,重定向登录页 |
### 数据隔离错误
| 场景 | HTTP 状态码 | 错误消息 |
|------|------------|---------|
| 访问非管辖门店数据 | 403 | "无权访问该门店数据" |
| 修改 site_id 超出管辖范围 | 403 | "目标门店不在管辖范围内" |
### 业务逻辑错误
| 场景 | HTTP 状态码 | 错误消息 |
|------|------------|---------|
| 审核非 pending 状态的申请 | 409 | "该申请已被处理" |
| 线索 ID 不存在 | 404 | "线索不存在" |
| 线索不在管辖范围 | 404 | "线索不存在" |
| category 枚举值无效 | 422 | "无效的线索大类" |
| summary 为空或超长 | 422 | "摘要不能为空且不超过200字符" |
### Excel 上传错误
| 场景 | HTTP 状态码 | 错误消息 |
|------|------------|---------|
| 文件格式非 .xlsx/.xls | 400 | "请上传有效的 Excel 文件" |
| 格式校验失败 | 200 | 返回错误行详情(不阻断,前端标红) |
| 人员匹配失败 | 200 | 标记为 warning不阻断前端黄色高亮 |
| 写入数据库失败 | 500 | 回滚整批log status=failed记录 error_detail |
### 数据库事务策略
- Excel 写入:整批次在单个事务中执行,任何行写入失败则回滚全部
- 审核通过多表写入在单个事务中执行users + user_site_roles + user_assistant_binding + user_applications
- 线索操作:单条记录操作,无需跨表事务
---
## 测试策略
### 双轨测试方法
本项目采用单元测试 + 属性测试的双轨策略:
- **单元测试**:验证具体示例、边界条件和错误处理
- **属性测试**:验证跨所有输入的通用属性
两者互补:单元测试捕获具体 bug属性测试验证通用正确性。
### 属性测试配置
- **库**Python 后端使用 `hypothesis`(项目已有 `.hypothesis/` 目录);前端使用 `fast-check`
- **迭代次数**:每个属性测试最少 100 次迭代
- **标签格式**`Feature: tenant-admin-web, Property {number}: {property_text}`
- **每个正确性属性对应一个属性测试**
### 单元测试覆盖
| 模块 | 测试重点 |
|------|---------|
| `tenant_auth.py` | 登录成功/失败、禁用账号、JWT 签发/验证、刷新令牌 |
| `tenant_users.py` | 申请列表筛选、审核通过/拒绝、非 pending 状态拒绝409、用户编辑、绑定修改 |
| `tenant_excel.py` | 4 种模板格式校验、人员匹配、冲突检测、写入回滚、模板下载 |
| `tenant_clues.py` | 客户搜索、线索 CRUD、隐藏/显示、枚举校验、越权访问 |
| 前端 `services/api.ts` | JWT 自动附加、401 刷新、并发刷新保护 |
### 属性测试覆盖
| Property | 测试描述 | 生成器 |
|----------|---------|--------|
| P1 | 认证正确性 | 随机用户名+密码 |
| P2 | JWT 隔离 | 随机 JWT payload不同 aud |
| P3 | 数据隔离 | 随机 managed_site_ids + 随机数据 |
| P4 | 审核通过一致性 | 随机 pending 申请 + 角色/绑定参数 |
| P5 | 审核拒绝 | 随机 pending 申请 + 随机拒绝原因 |
| P6 | 关联匹配 | 随机手机号 + 随机助教/员工数据集 |
| P7 | 用户编辑 | 随机用户 + 随机编辑参数 |
| P8 | Excel 校验 | 随机模板类型 + 随机数据行(含有效/无效) |
| P9 | 人员匹配 | 随机姓名+编号 + 随机助教/员工数据集 |
| P10 | 冲突检测 | 随机上传数据 + 随机已有数据 |
| P11 | 写入 round-trip | 随机数据 + 随机冲突解决方案 |
| P12 | 客户搜索 | 随机关键词 + 随机客户数据集 |
| P13 | 线索编辑 | 随机 category + 随机 summary |
| P14 | 线索删除 | 随机线索 ID |
| P15 | 隐藏/显示 round-trip | 随机线索 + 隐藏→显示→验证 |
| P16 | 响应格式 | 随机端点调用 |
### 边界条件测试(单元测试覆盖)
- 禁用账号登录1.3)→ 403
- 非 pending 申请审核3.6)→ 409
- 越权修改 site_id4.5)→ 403
- 无效 Excel 文件格式5.5)→ 400
- 写入数据库失败回滚8.3)→ log status=failed
- 线索 ID 不存在/越权11.3, 12.3, 13.4)→ 404

View File

@@ -0,0 +1,241 @@
# 需求文档租户管理后台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 状态码
### 需求 5Excel 上传 — 文件解析与格式校验
**用户故事:** 作为 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 状态码并提示文件格式错误
### 需求 6Excel 上传 — 人员匹配校验
**用户故事:** 作为 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 在校验结果页面汇总展示:通过行数、警告行数、错误行数
### 需求 7Excel 上传 — 冲突检测与解决
**用户故事:** 作为 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 确认后写入
### 需求 8Excel 上传 — 数据写入与记录
**用户故事:** 作为 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、来源sourcemanual / 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`包含字段idBIGSERIAL PK、usernameVARCHAR(50) UNIQUE NOT NULL、password_hashVARCHAR(255) NOT NULL、display_nameVARCHAR(100)、tenant_idBIGINT NOT NULL、managed_site_idsBIGINT[] NOT NULL、is_activeBOOLEAN DEFAULT true、created_byBIGINT、created_atTIMESTAMPTZ DEFAULT NOW()、last_login_atTIMESTAMPTZ并创建 tenant_id 索引
2. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `salary_adjustments`包含字段idBIGSERIAL PK、site_idBIGINT NOT NULL、assistant_idBIGINT 可空、assistant_nameVARCHAR(100) NOT NULL、assistant_numberVARCHAR(50) NOT NULL、salary_monthVARCHAR(7) NOT NULL、adjustment_typeCHECK IN deduction/bonus、amountNUMERIC(12,2) > 0、reasonVARCHAR(200) NOT NULL、upload_batch_idFK → excel_upload_log、created_at、created_by并创建 (site_id, salary_month) 和 (assistant_id, salary_month) 索引
3. THE 迁移脚本 SHALL 在 `biz` Schema 中创建 `excel_upload_log`包含字段idBIGSERIAL PK、site_idBIGINT NOT NULL、upload_typeCHECK IN expense/platform_income/salary_adj/recharge_commission、file_nameVARCHAR(255) NOT NULL、uploaded_byBIGINT NOT NULL、row_countINTEGER DEFAULT 0、conflict_countINTEGER DEFAULT 0、resolved_countINTEGER DEFAULT 0、statusCHECK IN pending/confirmed/failed、error_detailJSONB、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_idFK、synced_atTIMESTAMPTZ 可空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`(维客线索管理)
2. THE Backend_API SHALL 将所有租户管理端点注册在 `/api/tenant/` 路径前缀下,与小程序端点(`/api/xcx/`)和管理后台端点(`/api/admin/`)隔离
3. THE Backend_API SHALL 为所有 `/api/tenant/*` 端点(登录接口除外)添加 `require_tenant_admin()` 认证依赖,验证 JWT 的 `aud` 字段为 `tenant-admin`
4. THE Backend_API SHALL 遵循现有的 API 响应格式约定:成功返回 `{ code: 0, data: ... }`,失败返回 `{ code: number, message: string }`,分页返回 `{ items: T[], total: number, page: number, pageSize: number }`

View File

@@ -0,0 +1,409 @@
# 实施计划租户管理后台tenant-admin-web
## 概述
按照设计文档将实施拆分为DDL 迁移 → 后端认证模块 → 后端路由模块 → 前端项目骨架 → 前端页面实现 → 联调收尾。每个任务增量构建确保无孤立代码。属性测试Hypothesis / fast-check和单元测试作为可选子任务紧跟实现步骤。
后端使用 PythonFastAPI + Pydantic前端使用 TypeScriptReact + Vite + Ant Design
## 任务
- [ ] 1. DDL 迁移:新建表 + 表结构变更
- [ ] 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_
- [ ] 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_
- [ ] 1.3 更新小程序端线索查询增加 `WHERE is_hidden = false` 条件
- 搜索现有线索查询 SQL在 WHERE 子句中追加 `AND is_hidden = false`
- _需求: 15.3_
- [ ] 2. 后端认证模块auth.tenant_admins + JWT + 依赖注入)
- [ ] 2.1 创建 `apps/backend/app/auth/tenant_admins.py`
- 定义 `CurrentTenantAdmin` dataclassadmin_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_
- [ ]* 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**
- [ ]* 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**
- [ ] 3. 后端路由tenant_auth.py登录/刷新)
- [ ] 3.1 创建 `apps/backend/app/routers/tenant_auth.py`
- 实现 `POST /api/tenant/auth/login`:接受 username + password查询 `auth.tenant_admins`bcrypt 验证,签发 JWTaud=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_
- [ ] 3.2 在 `apps/backend/app/main.py` 中注册 tenant_auth router路径前缀 `/api/tenant/auth`
- _需求: 17.2_
- [ ]* 3.3 编写属性测试:认证正确性
- **Property 1: 认证正确性**
- 使用 Hypothesis 生成随机用户名+密码组合
- 验证:有效凭据返回含 `aud=tenant-admin` 的 JWT无效凭据返回 401 且错误消息统一
- **验证: 需求 1.1, 1.2**
- [ ]* 3.4 编写单元测试:登录边界条件
- 测试文件 `apps/backend/tests/unit/test_tenant_auth.py`
- 验证:禁用账号登录返回 403、用户名不存在返回 401、密码错误返回 401、JWT 过期处理、refresh_token 刷新
- _需求: 1.1, 1.2, 1.3, 1.5_
- [ ] 4. 检查点 — 认证模块验证
- 确保认证模块所有测试通过ask the user if questions arise.
- [ ] 5. 后端路由tenant_users.py用户审核 + 用户管理)
- [ ] 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_
- [ ] 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_assistantphone, scd2_is_current=1和 v_dim_staff + v_dim_staff_exphone
- `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_
- [ ] 5.3 在 `apps/backend/app/main.py` 中注册 tenant_users router路径前缀 `/api/tenant`
- _需求: 17.2_
- [ ]* 5.4 编写属性测试:审核通过多表一致性
- **Property 4: 审核通过多表一致性**
- 使用 Hypothesis 生成随机 pending 申请 + 角色/绑定参数
- 验证:审核通过后 users.status='approved'、user_site_roles 存在记录、user_assistant_binding 存在记录(如提供 assistant_id、user_applications.status='approved' 且审核人和时间已填写
- **验证: 需求 3.4**
- [ ]* 5.5 编写属性测试:审核拒绝状态更新
- **Property 5: 审核拒绝状态更新**
- 使用 Hypothesis 生成随机 pending 申请 + 随机拒绝原因字符串
- 验证:拒绝后 user_applications.status='rejected'、review_note 等于提交的原因、reviewed_at 非空
- **验证: 需求 3.5**
- [ ]* 5.6 编写属性测试:申请关联匹配
- **Property 6: 申请关联匹配**
- 使用 Hypothesis 生成随机手机号 + 随机助教/员工数据集
- 验证:匹配结果仅包含 phone 匹配的记录,优先 v_dim_assistant 再 v_dim_staff
- **验证: 需求 3.3**
- [ ]* 5.7 编写属性测试:用户管理编辑
- **Property 7: 用户管理编辑**
- 使用 Hypothesis 生成随机编辑参数角色、site_id 在管辖范围内、状态)
- 验证编辑后用户记录反映新值site_id 超出管辖范围时返回 403
- **验证: 需求 4.2, 4.3, 4.4**
- [ ]* 5.8 编写单元测试:用户审核与管理边界条件
- 测试文件 `apps/backend/tests/unit/test_tenant_users.py`
- 验证:非 pending 状态审核返回 409、越权修改 site_id 返回 403、禁用用户后 status='disabled'
- _需求: 3.6, 4.4, 4.5_
- [ ] 6. 后端路由tenant_excel.pyExcel 上传/校验/冲突/写入)
- [ ] 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_
- [ ] 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_
- [ ] 6.3 在 `apps/backend/app/main.py` 中注册 tenant_excel router路径前缀 `/api/tenant/excel`
- _需求: 17.2_
- [ ]* 6.4 编写属性测试Excel 格式校验
- **Property 8: Excel 格式校验**
- 使用 Hypothesis 生成随机模板类型 + 随机数据行(含有效/无效混合)
- 验证:符合模板定义的行标记通过,不符合的行返回行号+列名+错误描述;通过行数 + 警告行数 + 错误行数 = 总行数
- **验证: 需求 5.1, 5.2, 5.3, 5.4, 6.5**
- [ ]* 6.5 编写属性测试:人员匹配校验
- **Property 9: 人员匹配校验**
- 使用 Hypothesis 生成随机助教姓名+编号 + 随机助教/员工数据集
- 验证:优先 v_dim_assistant 匹配,未匹配再查 v_dim_staff匹配成功填充 assistant_id失败标记 warning 不阻断
- **验证: 需求 6.1, 6.2, 6.3, 6.4**
- [ ]* 6.6 编写属性测试:冲突检测
- **Property 10: 冲突检测**
- 使用 Hypothesis 生成随机上传数据 + 随机已有数据
- 验证:按模板主键规则识别冲突行,冲突行返回逐字段 diff非冲突行标记待写入
- **验证: 需求 7.1, 7.2**
- [ ]* 6.7 编写属性测试:数据写入 round-trip
- **Property 11: 数据写入 round-trip**
- 使用 Hypothesis 生成随机数据 + 随机冲突解决方案replace/keep
- 验证写入后从目标表读取的数据与提交一致replace 用新值keep 保持旧值log status=confirmed行数正确
- **验证: 需求 7.4, 7.5, 8.1, 8.2**
- [ ]* 6.8 编写单元测试Excel 上传边界条件
- 测试文件 `apps/backend/tests/unit/test_tenant_excel.py`
- 验证:无效文件格式返回 400、写入失败回滚log status=failed、模板下载返回正确文件
- _需求: 5.5, 8.3, 8.5_
- [ ] 7. 后端路由tenant_clues.py维客线索管理
- [ ] 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_
- [ ] 7.2 创建 `apps/backend/app/routers/tenant_clues.py`
- `GET /api/tenant/customers/search`keyword + site_id 参数,在管辖门店范围内搜索 v_dim_membernickname 模糊匹配 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_
- [ ] 7.3 在 `apps/backend/app/main.py` 中注册 tenant_clues router路径前缀 `/api/tenant`
- _需求: 17.2_
- [ ]* 7.4 编写属性测试:客户搜索
- **Property 12: 客户搜索**
- 使用 Hypothesis 生成随机关键词 + 随机客户数据集
- 验证:结果中 nickname 包含关键词(模糊)或 mobile 等于关键词(精确),且 site_id 在管辖范围内
- **验证: 需求 9.1, 9.3**
- [ ]* 7.5 编写属性测试:线索编辑校验
- **Property 13: 线索编辑校验**
- 使用 Hypothesis 生成随机 category含有效/无效值)+ 随机 summary含空/超长)
- 验证:有效参数编辑成功且记录反映新值;无效 category 或空/超长 summary 被拒绝
- **验证: 需求 11.1, 11.2**
- [ ]* 7.6 编写属性测试:线索删除
- **Property 14: 线索删除**
- 使用 Hypothesis 生成随机线索 ID
- 验证:删除后该线索不可通过任何查询获取(物理删除)
- **验证: 需求 12.2**
- [ ]* 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**
- [ ]* 7.8 编写单元测试:维客线索边界条件
- 测试文件 `apps/backend/tests/unit/test_tenant_clues.py`
- 验证:线索不存在返回 404、越权访问返回 404、无效 category 返回 422、空 summary 返回 422
- _需求: 11.2, 11.3, 12.3, 13.4_
- [ ] 8. 检查点 — 后端路由模块验证
- 确保所有后端路由模块测试通过4 个路由文件均已注册到 main.pyask the user if questions arise.
- [ ] 9. 前端项目骨架apps/tenant-admin/
- [ ] 9.1 创建 `apps/tenant-admin/` 项目结构
- 初始化 `package.json`React + Vite + Ant Design + axios 依赖)
- 创建 `vite.config.ts`proxy 配置 `/api/tenant` → 后端地址)
- 创建 `tsconfig.json`
- 创建 `index.html` 入口
- _需求: 16.1_
- [ ] 9.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`
- _需求: 16.4, 16.5_
- [ ] 9.3 创建 `apps/tenant-admin/src/hooks/useAuth.ts` — 认证状态管理
- 登录/登出方法、token 存储、用户信息display_name, managed_site_ids
- _需求: 1.5, 16.3_
- [ ] 9.4 创建 `apps/tenant-admin/src/App.tsx` — 路由配置 + 侧边栏布局
- react-router-dom 路由配置
- Ant Design Layout + Sider 侧边栏导航用户审核、用户管理、Excel 上传、维客线索管理)
- 未认证时重定向到 `/login`
- _需求: 16.2, 16.3_
- [ ] 9.5 创建 `apps/tenant-admin/src/components/SiteSelector/` — 门店筛选器组件
- 页面顶部门店选择器,数据源为当前管理员的 managed_site_ids
- 支持多选/全选
- _需求: 2.4_
- [ ]* 9.6 编写属性测试:响应格式一致性(前端 fast-check
- **Property 16: 响应格式一致性**
- 使用 fast-check 生成随机 API 响应数据
- 验证:成功响应符合 `{ code: 0, data }` 格式;错误响应符合 `{ code: number, message: string }` 格式;分页响应 data 包含 items/total/page/pageSize
- **验证: 需求 17.4**
- [ ] 10. 前端页面:登录页
- [ ] 10.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, 16.3_
- [ ] 11. 前端页面:用户审核
- [ ] 11.1 创建 `apps/tenant-admin/src/pages/UserApproval/index.tsx`
- 申请列表表格Ant Design Table支持状态筛选全部/待审核/已通过/已拒绝)+ 分页
- 展示字段:昵称、手机号、球房编号、申请角色、员工编号、申请时间、状态
- 待审核行提供"审核"操作按钮
- _需求: 3.1, 3.2_
- [ ] 11.2 实现审核操作交互
- 点击"审核"打开 Modal展示关联建议列表调用 match-suggestions API
- 通过操作:选择角色 + 关联助教/员工 → 调用 approve API
- 拒绝操作:填写拒绝原因(必填)→ 调用 reject API
- 操作成功后刷新列表
- _需求: 3.3, 3.4, 3.5_
- [ ] 12. 前端页面:用户管理
- [ ] 12.1 创建 `apps/tenant-admin/src/pages/UserManagement/index.tsx`
- 用户列表表格,支持角色筛选 + 关键词搜索 + 分页
- 展示字段:姓名、角色、关联助教姓名、所属门店、账号状态
- 每行提供"编辑"和"绑定"操作按钮
- _需求: 4.1_
- [ ] 12.2 实现编辑与绑定交互
- 编辑 Modal修改角色Select、所属门店Select限管辖范围、账号状态Switch
- 绑定 Modal修改关联助教 ID / 员工 ID
- 禁用用户时二次确认
- _需求: 4.2, 4.3, 4.4_
- [ ] 13. 前端页面Excel 上传
- [ ] 13.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_
- [ ] 13.2 创建 `apps/tenant-admin/src/components/DiffTable/index.tsx` — 冲突 diff 交互表格
- 每行显示:字段名、旧值、新值、操作选项(替换/保留 Radio
- 支持"全部替换"和"全部保留"快捷操作按钮
- 确认按钮提交 resolutions 数组
- _需求: 7.3, 7.4_
- [ ] 13.3 实现确认写入与上传记录
- 无冲突时直接展示"待写入"确认按钮
- 有冲突时展示 DiffTable用户选择后确认
- 调用 confirm API 写入,成功/失败提示
- 上传记录 Tab展示历史上传记录列表模板类型、文件名、上传人、时间、行数、冲突数、状态分页
- _需求: 7.5, 8.1, 8.2, 8.4_
- [ ] 14. 前端页面:维客线索管理
- [ ] 14.1 创建 `apps/tenant-admin/src/pages/RetentionClues/index.tsx`
- 客户搜索栏Input.Search支持姓名模糊/手机号精确搜索
- 门店筛选器(复用 SiteSelector 组件)
- 搜索结果列表member_id、姓名、手机号脱敏、所属门店
- 搜索结果为空时展示"未找到匹配客户"提示
- _需求: 9.1, 9.3, 9.4_
- [ ] 14.2 实现线索列表与操作
- 选择客户后展示该客户全部线索,按大类标签分组
- 支持按来源和隐藏状态筛选
- 已隐藏线索以灰色/删除线样式区分
- 每条线索提供:编辑、删除、隐藏/显示操作按钮
- _需求: 10.1, 10.2, 10.3_
- [ ] 14.3 创建 `apps/tenant-admin/src/components/ClueEditor/index.tsx` — 线索编辑表单
- category Select6 值枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈)
- summary Input必填最长 200 字符)
- detail TextArea可选
- _需求: 11.1_
- [ ] 14.4 实现删除与隐藏/显示交互
- 删除二次确认对话框Modal.confirm明确提示"删除后不可恢复"
- 隐藏/显示Switch 切换,即时调用 visibility API
- _需求: 12.1, 12.2, 13.1, 13.3_
- [ ] 15. 检查点 — 前端页面验证
- 确保所有前端页面组件渲染正常API 调用层工作正确ask the user if questions arise.
- [ ] 16. 前后端联调与集成
- [ ] 16.1 前端所有页面对接后端真实 API
- 登录页 → tenant_auth 登录/刷新
- 用户审核页 → tenant_users 申请列表/关联建议/审核
- 用户管理页 → tenant_users 用户列表/编辑/绑定
- Excel 上传页 → tenant_excel 上传/校验/冲突/确认/记录/模板下载
- 维客线索页 → tenant_clues 客户搜索/线索列表/编辑/删除/隐藏
- 验证所有页面 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_
- [ ] 16.2 DDL 迁移合并到主 DDL 基线
- 执行迁移脚本到 `test_zqyy_app`
- 合并新表定义到 `docs/database/ddl/` 对应基线文件
- _需求: 14.1-14.4, 15.1-15.2_
- [ ] 17. 文档更新
- [ ] 17.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_
- [ ] 17.2 更新后端 API 参考文档
-`apps/backend/docs/API-REFERENCE.md` 新增 4 个 tenant 路由模块文档
- 更新 `apps/backend/README.md` 路由模块摘要
- _需求: 17.1-17.4_
- [ ] 17.3 更新文档地图
-`docs/DOCUMENTATION-MAP.md` 新增 NS4 模块条目tenant-admin-web spec、BD 手册、前端项目)
- _规范: doc-map.md_
- [ ] 18. 最终检查点 — 全量验证
- 确保所有后端测试通过(单元测试 + 属性测试)
- 确保前端所有页面连接真实后端运行正常
- 确保 DDL 迁移已合并到主基线BD 手册已同步更新
- 确保 API 文档、后端 README、文档地图均已更新
- ask the user if questions arise.
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
- 每个任务引用了具体的需求编号以确保可追溯性
- 属性测试覆盖 16 个正确性属性Property 1-16单元测试覆盖具体边界条件
- 检查点任务确保增量验证,避免问题累积
- 后端使用 PythonFastAPI + Pydantic + Hypothesis前端使用 TypeScriptReact + Vite + Ant Design + fast-check
- 会员信息一律通过 `member_id` JOIN `v_dim_member``scd2_is_current=1`不使用结算单冗余字段DQ-6 规则)
- 多店铺 FDW 查询采用逐 site_id 查询后合并结果的策略