Files
Neo-ZQYY/docs/prd/Neo_Specs/NS4-tenant-admin-web.md

533 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NS4租户管理后台 — tenant-admin-web
> 优先级:中(可与 NS1/NS2 并行,依赖 P1+P3
> 预估工作量:大
> 前置条件P1数据库基础、P3用户认证体系、admin-web-console 需求 11租户管理员账号管理
> 参考基准:`docs/prd/specs/P10-tenant-admin-web.md`
---
## 一、背景与目标
当前系统缺少面向租户管理员的独立管理界面。用户审核、Excel 数据上传、维客线索管理等运营操作无法自助完成,依赖开发人员手动操作数据库。
本 SPEC 目标:构建独立的租户管理 Web 应用,提供:
1. 用户审核与管理(申请审核、身份编辑、店铺归属、助教/员工绑定)
2. Excel 数据上传4 种模板:财务支出/团购收入/助教奖罚/充值业绩归属)
3. 维客线索管理(查看、修改、删除、隐藏)
### 与现有系统的关系
| 系统 | 用途 | 用户 |
|------|------|------|
| `apps/admin-web/`(系统管理后台) | 平台级管理Operator 操作) | 系统管理员 |
| `apps/tenant-admin/`(本 SPEC | 租户级管理 | 租户管理员 |
| `apps/miniprogram/`(小程序) | C 端业务 | 助教/管理者 |
租户管理员账号由系统管理后台(`apps/admin-web/`)的 Operator 创建,租户管理员不可自行注册。
---
## 二、技术架构
### 2.1 前端
- 独立 Web 应用React + Vite + Ant Design`apps/admin-web/` 同技术栈)
- 部署路径:`apps/tenant-admin/`
- 独立登录入口,与系统管理后台完全隔离
```
apps/tenant-admin/
├── src/
│ ├── pages/
│ │ ├── Login/ # 登录页
│ │ ├── UserApproval/ # 用户审核
│ │ ├── UserManagement/ # 用户管理
│ │ ├── ExcelUpload/ # Excel 上传4 种模板)
│ │ └── RetentionClues/ # 维客线索管理
│ ├── components/
│ │ ├── DiffTable/ # 冲突 diff 交互组件
│ │ └── ClueEditor/ # 线索编辑组件
│ ├── services/
│ │ └── api.ts # API 调用封装
│ ├── hooks/
│ ├── utils/
│ └── App.tsx
├── package.json
├── vite.config.ts
└── tsconfig.json
```
### 2.2 后端
复用 `apps/backend/` 的 FastAPI新增租户管理路由模块
```
apps/backend/app/routers/
├── tenant_auth.py 🆕 租户管理员登录/鉴权
├── tenant_users.py 🆕 用户审核 + 用户管理
├── tenant_excel.py 🆕 Excel 上传/校验/冲突处理
└── tenant_clues.py 🆕 维客线索管理
```
### 2.3 认证体系
- 独立凭据:用户名 + 密码(非微信登录)
- JWT 签发:与小程序 JWT 独立(不同 issuer 或 audience
- 账号创建:由系统管理后台 Operator 创建,指定用户名、初始密码、所属租户、管辖 site_id 列表
- 权限级别:
- 租户级管理员:管辖该租户下所有店铺
- 店铺级管理员:只能管理 Operator 分配的 site_id 列表内的店铺
### 2.4 数据隔离
- 所有查询附加 `site_id IN (管辖列表)` 条件
- FDW 查询需 `SET LOCAL app.current_site_id`(单店铺场景)
- 多店铺场景下,逐 site_id 查询后合并结果
---
## 三、功能详细设计
### 3.1 用户审核页面
#### 页面功能
- 申请列表:展示所有待审核/已审核的用户申请
- 状态筛选:全部 / 待审核(pending) / 已通过(approved) / 已拒绝(rejected)
- 关联建议:根据申请中的球房 ID + 手机号,同时在助教表和员工信息表中匹配
- 审核操作:通过(分配身份+关联助教/员工)/ 拒绝(填写原因)
#### 关联匹配逻辑
```
用户申请球房ID + 手机号)
→ site_code_mapping 查 site_id
→ 并行匹配:
├── fdw_etl.v_dim_assistantphone 匹配scd2_is_current=1
└── fdw_etl.v_dim_staff + v_dim_staff_exphone 匹配)
→ 返回匹配建议列表(可能多条)
→ 管理员选择关联目标
```
#### 审核通过后操作
1. 更新 `auth.users.status = 'approved'`
2. 分配角色(助教/管理者)→ 写入 `auth.user_roles`
3. 关联助教 → 写入 `auth.user_assistant_binding`(含 staff_id
4. 分配 site_id → 更新 `auth.users.site_id`
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 申请列表 | GET | `/api/tenant/applications` | 支持 status 筛选、分页 |
| 关联建议 | GET | `/api/tenant/applications/{id}/match-suggestions` | 返回助教+员工匹配结果 |
| 审核通过 | POST | `/api/tenant/applications/{id}/approve` | body: role, assistant_id, staff_id |
| 审核拒绝 | POST | `/api/tenant/applications/{id}/reject` | body: reason |
### 3.2 用户管理页面
#### 页面功能
- 用户列表:展示已通过审核的用户(姓名、角色、关联助教、店铺、状态)
- 身份编辑:修改角色(助教↔管理者)
- 店铺归属:修改用户的 site_id
- 关联助教/员工:修改绑定关系
- 禁用/启用:冻结用户账号
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 用户列表 | GET | `/api/tenant/users` | 支持角色筛选、搜索、分页 |
| 编辑用户 | PATCH | `/api/tenant/users/{id}` | body: role, site_id, status |
| 修改绑定 | PUT | `/api/tenant/users/{id}/binding` | body: assistant_id, staff_id |
### 3.3 Excel 上传
#### 4 种模板
##### 模板 1财务支出按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验,不超过当前月 |
| 支出类别 | 文本 | 是 | 枚举:房租/水电/物业/食品饮料进货/耗材/报销/固定人员工资/其他费用 |
| 金额 | 数值(2) | 是 | > 0精度 2 位小数 |
| 备注 | 文本 | 否 | 最长 500 字符 |
主键:月份 + 支出类别
写入目标:`dws.dws_finance_expense_summary`(通过后端 API 写入 ETL 库,或写入业务库 staging 表后由 ETL 同步)
##### 模板 2团购平台收入按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验 |
| 平台名称 | 文本 | 是 | 非空 |
| 收入金额 | 数值(2) | 是 | > 0 |
| 备注 | 文本 | 否 | 最长 500 字符 |
主键:月份 + 平台名称
写入目标:`dws.dws_platform_settlement`(或业务库 staging 表)
##### 模板 3助教奖罚按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验 |
| 助教姓名 | 文本 | 是 | 非空 |
| 助教编号 | 文本 | 是 | 非空 |
| 类型 | 文本 | 是 | 枚举:扣款/奖金 |
| 金额 | 数值(2) | 是 | > 0 |
| 原因 | 文本 | 是 | 非空,最长 200 字符 |
主键:月份 + 助教姓名 + 助教编号 + 类型 + 原因(同一助教同月可多笔)
写入目标:`biz.salary_adjustments`
##### 模板 4充值业绩归属按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 充值日期 | YYYY-MM-DD | 是 | 格式校验 |
| 会员名称 | 文本 | 是 | 非空 |
| 充值金额 | 数值(2) | 是 | > 0 |
| 归属助教 | 文本 | 是 | 非空 |
| 奖励金额 | 数值(2) | 是 | ≥ 0 |
主键:充值日期 + 会员名称 + 归属助教
写入目标:`dws.dws_assistant_recharge_commission`(或业务库 staging 表)
#### 人员匹配校验(模板 3/4
上传助教奖罚和充值业绩归属时,需校验助教姓名+编号是否存在:
```
助教姓名 + 助教编号
→ fdw_etl.v_dim_assistantnickname + assistant_number 匹配scd2_is_current=1
→ 如不匹配,尝试 fdw_etl.v_dim_staff + v_dim_staff_exname + staff_number 匹配)
→ 匹配失败:标记为校验警告(不阻断上传,但提示管理员确认)
```
#### 冲突处理流程
```
上传 Excel
→ 后端解析 + 格式校验
→ 返回校验结果:
├── 格式错误行 → 前端标红,要求修正后重新上传
├── 无冲突行 → 标记为"待写入"
└── 冲突行(主键已存在)→ 返回 diff 数据(旧值 vs 新值)
→ 前端展示 diff 交互表格:
- 每行显示:字段名 | 旧值 | 新值 | 操作(替换/保留)
- 支持"全部替换"/"全部保留"快捷操作
→ 用户确认后提交
→ 后端按选择写入
```
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 上传解析 | POST | `/api/tenant/excel/upload` | multipart/form-data返回校验结果+冲突列表 |
| 确认写入 | POST | `/api/tenant/excel/confirm` | body: upload_id, resolutions[] |
| 上传记录 | GET | `/api/tenant/excel/logs` | 历史上传记录列表 |
| 模板下载 | GET | `/api/tenant/excel/template/{type}` | 下载空白模板 |
### 3.4 维客线索管理
#### 页面功能
- 客户搜索:按客户姓名/手机号搜索(姓名从 dim_member.nickname手机从 dim_member.mobile
- 门店筛选:按管辖 site_id 筛选
- 线索列表:展示该客户的全部维客线索
- 标签(大类枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈)
- 摘要(含 Emoji 前缀)
- 详情
- 提供人recorded_by_name
- 来源manual / ai_consumption / ai_note
- 记录时间
- 隐藏状态
- 操作:
- 修改:编辑标签、摘要、详情
- 删除:二次确认后物理删除
- 隐藏/显示:切换 `is_hidden` 状态
#### 数据源
- `zqyy_app.public.member_retention_clue`(线索数据)
- `fdw_etl.v_dim_member`客户信息nickname、mobile通过 member_id 关联)
> ⚠️ 会员字段断档DQ-6客户姓名/手机必须从 dim_member 获取,不可使用结算单上的冗余字段
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 客户搜索 | GET | `/api/tenant/customers/search` | query: keyword, site_id |
| 线索列表 | GET | `/api/tenant/customers/{member_id}/clues` | 该客户全部线索 |
| 修改线索 | PATCH | `/api/tenant/clues/{id}` | body: category, summary, detail |
| 删除线索 | DELETE | `/api/tenant/clues/{id}` | 二次确认后物理删除 |
| 隐藏/显示 | PATCH | `/api/tenant/clues/{id}/visibility` | body: is_hidden |
---
## 四、数据库审查与新增表
### 4.1 现有表满足度
| 功能 | 现有表 | 是否满足 | 缺口 |
|------|--------|---------|------|
| 用户审核 | auth.users, auth.user_applications | ✅ 满足 | 无 |
| 用户管理 | auth.users, auth.user_roles, auth.user_assistant_binding | ✅ 满足 | 无 |
| 维客线索 | public.member_retention_clue | ⚠️ 部分 | 缺 is_hidden 字段 |
| 助教奖罚 | — | ❌ 不满足 | 需新建 biz.salary_adjustments |
| 上传记录 | — | ❌ 不满足 | 需新建 biz.excel_upload_log |
| 财务支出/团购收入/充值归属 | DWS 表 | ⚠️ 待定 | 可能需要 staging 表 |
### 4.2 需新建的表
#### 表 1`biz.salary_adjustments`(助教奖罚明细)
```sql
CREATE TABLE biz.salary_adjustments (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
assistant_id BIGINT, -- 匹配到的助教 ID可空匹配失败时为 NULL
assistant_name VARCHAR(100) NOT NULL,
assistant_number VARCHAR(50) NOT NULL,
salary_month VARCHAR(7) NOT NULL, -- YYYY-MM
adjustment_type VARCHAR(20) NOT NULL CHECK (adjustment_type IN ('deduction', 'bonus')),
amount NUMERIC(12,2) NOT NULL CHECK (amount > 0),
reason VARCHAR(200) NOT NULL,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by BIGINT -- 上传人(租户管理员 ID
);
CREATE INDEX idx_salary_adj_site_month ON biz.salary_adjustments(site_id, salary_month);
CREATE INDEX idx_salary_adj_assistant ON biz.salary_adjustments(assistant_id, salary_month);
```
#### 表 2`biz.excel_upload_log`Excel 上传记录)
```sql
CREATE TABLE biz.excel_upload_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
upload_type VARCHAR(30) NOT NULL CHECK (upload_type IN (
'expense', 'platform_income', 'salary_adj', 'recharge_commission'
)),
file_name VARCHAR(255) NOT NULL,
uploaded_by BIGINT NOT NULL, -- 租户管理员 ID
row_count INTEGER NOT NULL DEFAULT 0,
conflict_count INTEGER NOT NULL DEFAULT 0,
resolved_count INTEGER NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN (
'pending', 'confirmed', 'failed'
)),
error_detail JSONB, -- 校验错误详情
created_at TIMESTAMPTZ DEFAULT NOW(),
confirmed_at TIMESTAMPTZ
);
CREATE INDEX idx_excel_log_site ON biz.excel_upload_log(site_id, created_at DESC);
```
### 4.3 需变更的表
#### `public.member_retention_clue` — 新增字段
```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=管理后台保留但小程序不展示)';
```
> 小程序端查询线索时需增加 `WHERE is_hidden = false` 条件
#### `public.member_retention_clue` — 确认 source 字段
P10 spec 中提到需新增 `source` 字段,需确认该字段是否已在 P4/P5 阶段建立。如未建立:
```sql
ALTER TABLE public.member_retention_clue
ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'manual'
CHECK (source IN ('manual', 'ai_consumption', 'ai_note'));
COMMENT ON COLUMN public.member_retention_clue.source
IS '线索来源manual=人工录入, ai_consumption=应用3消费分析, ai_note=应用6备注分析';
```
### 4.4 Excel 写入目标表的 staging 策略
财务支出、团购收入、充值业绩归属三种模板的数据最终需要进入 DWS 层。有两种策略:
**方案 A直接写入 DWS 表**(通过后端 API 直连 ETL 库写入)
- 优点:数据即时可用
- 缺点:绕过 ETL 流程,数据一致性风险
**方案 B写入业务库 staging 表ETL 定时同步**
- 优点:数据经过 ETL 标准流程,一致性有保障
- 缺点:数据有延迟(取决于 ETL 调度频率)
建议采用方案 B需新建 3 张 staging 表:
```sql
-- biz.stg_finance_expense财务支出 staging
CREATE TABLE biz.stg_finance_expense (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
expense_month VARCHAR(7) NOT NULL,
category VARCHAR(50) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
remark TEXT,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ, -- ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- biz.stg_platform_income团购收入 staging
CREATE TABLE biz.stg_platform_income (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
income_month VARCHAR(7) NOT NULL,
platform_name VARCHAR(100) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
remark TEXT,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- biz.stg_recharge_commission充值业绩归属 staging
CREATE TABLE biz.stg_recharge_commission (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
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 REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
---
## 五、租户管理员账号体系
### 5.1 账号存储
租户管理员账号存储在 `auth.tenant_admins` 表(需新建):
```sql
CREATE TABLE auth.tenant_admins (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
tenant_id BIGINT NOT NULL, -- 所属租户
managed_site_ids BIGINT[] NOT NULL, -- 管辖的 site_id 列表
is_active BOOLEAN NOT NULL DEFAULT true,
created_by BIGINT, -- 创建该账号的 Operator ID
created_at TIMESTAMPTZ DEFAULT NOW(),
last_login_at TIMESTAMPTZ
);
CREATE INDEX idx_tenant_admin_tenant ON auth.tenant_admins(tenant_id);
```
### 5.2 与小程序用户体系的隔离
- 租户管理员使用 `auth.tenant_admins` 表,小程序用户使用 `auth.users`
- JWT 签发时使用不同的 `aud`audience字段区分
- 后端路由通过不同的认证依赖注入区分(`require_tenant_admin()` vs `require_approved()`
---
## 六、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| P10 原始 spec | `docs/prd/specs/P10-tenant-admin-web.md` | 需求定义基准 |
| admin-web 现有代码 | `apps/admin-web/` | 技术栈参考React + Vite + Ant Design |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |
| 权限矩阵 | `docs/permission_matrix/` | 角色-权限映射参考 |
| DWD-DOC 标杆 | `docs/reports/DWD-DOC/` | 金额口径权威参考 |
| 数据依赖矩阵 | `docs/prd/specs/00-数据依赖矩阵.md` | 租户管理后台数据源映射 |
| member_retention_clue DDL | `db/zqyy_app/` | 维客线索表结构 |
---
## 七、预审查清单SPEC 启动前确认)
### 7.1 账号与认证
1. **租户管理员账号模型**:是否需要独立的 `auth.tenant_admins` 表?还是复用 `auth.users` 表增加 `user_type` 字段区分?
2. **密码策略**:初始密码是否需要强制修改?密码复杂度要求?是否需要密码过期机制?
3. **多租户隔离**:一个管理员是否可以管辖多个租户?还是严格一对一?
4. **会话管理**JWT 过期时间?是否需要 refresh token是否支持多设备同时登录
### 7.2 用户审核
5. **关联匹配优先级**:助教表和员工信息表同时匹配到时,优先展示哪个?是否需要合并展示?
6. **审核拒绝后**:用户是否可以重新申请?重新申请是新建记录还是更新原记录?
7. **批量审核**:是否需要支持批量通过/拒绝?
8. **审核通知**:审核结果是否需要通知用户(小程序消息/微信模板消息)?
### 7.3 Excel 上传
9. **写入策略**:财务支出/团购收入/充值归属是直接写入 DWS 表(方案 A还是写入 staging 表由 ETL 同步(方案 B
10. **文件大小限制**:单次上传的 Excel 文件大小上限?行数上限?
11. **历史数据**:是否允许上传历史月份的数据?是否有时间范围限制?
12. **模板版本**Excel 模板是否需要版本管理?表头变更时如何兼容旧模板?
13. **助教匹配失败处理**:模板 3/4 中助教姓名+编号匹配失败时,是阻断上传还是允许上传但标记警告?
### 7.4 维客线索
14. **隐藏 vs 删除**:隐藏的线索是否可以恢复显示?删除是否需要软删除(保留记录但标记删除)?
15. **AI 线索保护**AI 生成的线索source=ai_consumption/ai_note是否允许管理员修改/删除?修改后 source 是否变更为 manual
16. **线索审计**:线索的修改/删除操作是否需要记录操作日志(谁在什么时间做了什么操作)?
17. **批量操作**:是否需要支持批量隐藏/删除线索?
### 7.5 部署与运维
18. **部署方式**:租户管理后台是独立部署还是与系统管理后台共享服务器?域名/路径如何规划?
19. **前端构建**:是否与 admin-web 共享 pnpm workspace还是完全独立的 package.json
20. **监控**:是否需要独立的访问日志和错误监控?
---
## 八、任务清单草案SPEC 细化后调整)
### Batch A基础设施
- [ ] T1创建 `apps/tenant-admin/` 项目骨架React + Vite + Ant Design
- [ ] T2创建 `auth.tenant_admins` 表 + DDL 迁移脚本
- [ ] T3实现租户管理员登录 API`tenant_auth.py`:登录/JWT 签发/鉴权中间件)
- [ ] T4创建 `biz.salary_adjustments` + `biz.excel_upload_log` 表 + DDL 迁移脚本
### Batch B用户审核与管理
- [ ] T5实现用户审核后端 API申请列表/关联建议/审核通过/审核拒绝)
- [ ] T6实现用户审核前端页面申请列表 + 状态筛选 + 关联建议展示 + 审核操作)
- [ ] T7实现用户管理后端 API用户列表/编辑/绑定修改)
- [ ] T8实现用户管理前端页面用户列表 + 身份编辑 + 店铺归属)
### Batch CExcel 上传
- [ ] T9实现 Excel 解析+校验后端4 种模板的格式校验 + 人员匹配校验)
- [ ] T10实现冲突检测后端主键匹配 + diff 数据生成)
- [ ] T11实现 Excel 上传前端(模板下载 + 上传 + 校验结果展示 + diff 交互 + 确认)
- [ ] T12创建 staging 表(如采用方案 B+ 写入逻辑
### Batch D维客线索管理
- [ ] T13`member_retention_clue` 新增 `is_hidden` 字段 + DDL 迁移
- [ ] T14实现维客线索后端 API客户搜索/线索列表/修改/删除/隐藏)
- [ ] T15实现维客线索前端页面客户搜索 + 线索列表 + 编辑/删除/隐藏操作)
- [ ] T16小程序端线索查询增加 `WHERE is_hidden = false` 条件