Files
Neo-ZQYY/docs/prd/Neo_Specs/NS4.1-tenant-admin-redesign.md
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

513 lines
20 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.1:租户管理员页面重构 — 项目级注册体系 + 简写ID管理
> 优先级NS4 后续迭代,依赖 NS4 基础设施已就绪)
> 预估工作量:中
> 前置条件NS4租户管理后台基础设施、P3用户认证体系
> 关联页面:`http://localhost:5173/tenant-admins`admin-web 系统管理后台)
---
## 一、背景与目标
### 1.1 现状问题
当前 `admin-web` 的租户管理员页面NS4 需求 14仅支持基础 CRUD
- 创建时手动输入 `tenant_id``managed_site_ids`(无下拉选项,无名称参考)
- 无法删除管理员记录
- 无法管理简写IDsite_code简写ID 的创建和修改散落在数据库手动操作中
- 缺少项目级的「连接器 → 租户 → 店铺」注册体系,租户名称无处存储
### 1.2 目标
1. 建立项目级注册体系:`biz.connectors``biz.tenants``biz.sites`,统一管理连接器、租户、店铺三级关系
2.`auth.site_code_mapping` 合并迁移至 `biz.sites`简写ID 成为店铺属性
3. 简写ID 变更增量记录(`biz.site_code_history`),保护已提交但未审核的用户申请
4. 重构租户管理员页面支持删除软删除、2 步创建流程、简写ID 管理
5. 新增 ETL 增量同步任务:从 `dwd.dim_site` 同步店铺信息到业务库
---
## 二、数据模型设计
### 2.1 新建表
#### 表 1`biz.connectors` — 连接器注册表
记录本项目接入的上游 SaaS 系统。当前仅「飞球」一个连接器,预留多连接器扩展。
```sql
CREATE TABLE biz.connectors (
id SERIAL PRIMARY KEY,
connector_key VARCHAR(50) NOT NULL UNIQUE, -- 连接器标识(如 'feiqiu'
display_name VARCHAR(100) NOT NULL, -- 显示名称(如 '飞球'
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.connectors IS '连接器注册表:记录本项目接入的上游 SaaS 系统';
```
初始数据:
```sql
INSERT INTO biz.connectors (connector_key, display_name)
VALUES ('feiqiu', '飞球');
```
#### 表 2`biz.tenants` — 租户注册表
记录每个连接器下的租户信息。`tenant_id` 来自上游系统(飞球的 `tenant_id`)。
```sql
CREATE TABLE biz.tenants (
id SERIAL PRIMARY KEY,
connector_id INTEGER NOT NULL REFERENCES biz.connectors(id),
tenant_id BIGINT NOT NULL, -- 上游系统的租户 ID
tenant_name VARCHAR(200), -- 租户名称(可从上游同步或手动填写)
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (connector_id, tenant_id) -- 同一连接器下 tenant_id 唯一
);
COMMENT ON TABLE biz.tenants IS '租户注册表连接器下的租户tenant_id 来自上游系统';
```
初始数据(从 ETL 库 `dwd.dim_site` 提取当前唯一租户):
```sql
INSERT INTO biz.tenants (connector_id, tenant_id, tenant_name)
VALUES (1, 2790683160709957, '朗朗桌球');
-- tenant_name 暂用店铺名,后续可由管理员修改或从上游同步
```
#### 表 3`biz.sites` — 店铺注册表(合并 `auth.site_code_mapping`
`auth.site_code_mapping` 的功能合并到此表,增加 `tenant_id` 外键关联。
`site_code` 为当前生效的简写ID6 位3+3 格式)。
```sql
CREATE TABLE biz.sites (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES biz.tenants(id),
site_id BIGINT NOT NULL UNIQUE, -- 上游系统的店铺 ID
site_name VARCHAR(200), -- 店铺名称(从 dwd.dim_site 同步)
site_code VARCHAR(6) UNIQUE, -- 当前生效的简写ID如 'LLQ001'
site_label VARCHAR(50), -- 店铺标签(从 dwd.dim_site 同步)
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.sites IS '店铺注册表:合并原 auth.site_code_mapping增加租户关联和简写ID管理';
COMMENT ON COLUMN biz.sites.site_code IS '当前生效的简写ID6位字符3+3格式全局唯一';
```
初始数据(从 `auth.site_code_mapping` 迁移):
```sql
-- 仅迁移真实数据(排除测试数据 tenant_id IS NULL
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_code)
SELECT t.id, scm.site_id, scm.site_name, scm.site_code
FROM auth.site_code_mapping scm
JOIN biz.tenants t ON t.tenant_id = scm.tenant_id
WHERE scm.tenant_id IS NOT NULL;
```
#### 表 4`biz.site_code_history` — 简写ID 变更历史表
增量记录所有使用过的简写ID用于保护已提交但未审核的用户申请。
```sql
CREATE TABLE biz.site_code_history (
id SERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 关联 biz.sites.site_id
site_code VARCHAR(6) NOT NULL, -- 历史简写ID
is_current BOOLEAN NOT NULL DEFAULT false, -- 是否为当前生效
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 该 code 生效时间
retired_at TIMESTAMPTZ, -- 该 code 失效时间NULL=当前生效)
UNIQUE (site_code) -- 简写ID 全局唯一(含历史)
);
COMMENT ON TABLE biz.site_code_history IS '简写ID变更历史增量记录所有使用过的简写ID';
COMMENT ON COLUMN biz.site_code_history.is_current IS 'true=当前生效的简写ID每个 site_id 最多一条 is_current=true';
```
### 2.2 表关系
```
biz.connectors (1)
└── biz.tenants (N) -- 一个连接器下多个租户
└── biz.sites (N) -- 一个租户下多个店铺
└── biz.site_code_history (N) -- 一个店铺的简写ID变更历史
auth.tenant_admins.tenant_id → biz.tenants.tenant_id逻辑关联不加 FK
auth.user_applications.site_code → biz.site_code_history.site_code逻辑关联
```
### 2.3 废弃表处理
`auth.site_code_mapping` 在数据迁移完成并验证后标记为废弃:
1. 迁移期间保留原表,新旧并行
2. 后端代码切换到 `biz.sites` 读取
3. 验证无误后,原表重命名为 `auth._archived_site_code_mapping`
### 2.4 `dwd.dim_site` 提升为项目级
当前 `dwd.dim_site` 位于 ETL 连接器级别(`apps/etl/connectors/feiqiu/`)。
本次不做物理迁移(表仍在 `dwd` schema但在语义上将其视为项目级维度表。
后续如有多连接器场景,再考虑拆分为项目级 `dim` schema。
---
## 三、功能详细设计
### 3.1 租户管理员列表页
#### 现有功能保留
- 分页 + 关键词搜索
- 编辑(显示名称、管辖门店、账号状态)
- 重置密码
#### 新增功能
##### 3.1.1 删除管理员(软删除)
- 操作:点击「删除」按钮 → 二次确认弹窗 → 确认后设置 `is_active = false`
- 列表默认只显示 `is_active = true` 的记录
- 可选:增加「显示已禁用」开关,查看所有记录
- 已禁用的记录不可再次删除,但可以重新启用
##### 3.1.2 创建管理员2 步流程)
**第 1 步:创建账号**
- 选择租户:下拉选择 `biz.tenants` 中的租户(显示 `tenant_name`,值为 `tenant_id`
- 输入用户名
- 输入初始密码
- 输入显示名称
- 选择管辖门店:根据所选租户,加载该租户下所有店铺(`biz.sites`),多选
**第 2 步设置简写ID**
- 展示所选租户下所有店铺列表
- 每个店铺显示店铺名称、当前简写ID如有
- 可为每个店铺设置/修改简写ID
- 简写ID 格式6 位字符数字3+3 模式,如 `LLQ001`
- 校验:全局唯一(含历史记录中的 code
> 第 2 步可跳过简写ID 后续可在编辑时设置)
##### 3.1.3 编辑管理员
- 不可修改所属租户(`tenant_id` 只读)
- 可修改用户名、密码重置、显示名称、管辖门店、简写ID
- 简写ID 编辑入口在编辑弹窗中增加「管理简写ID」区域展示该租户下所有店铺及其当前 code
### 3.2 简写ID 管理逻辑
#### 3.2.1 设置/修改简写ID
```
用户在管理后台修改某店铺的简写IDold_code → new_code
→ 校验 new_code 格式6位3+3
→ 校验 new_code 全局唯一biz.sites.site_code + biz.site_code_history.site_code
→ 事务内执行:
1. 将 old_code 在 site_code_history 中标记 is_current=false, retired_at=NOW()
2. 插入 new_code 到 site_code_historyis_current=true
3. 更新 biz.sites.site_code = new_code
4. 清理无引用的历史记录(见 3.2.2
```
#### 3.2.2 历史记录清理
每次修改简写ID 时,检查被替换的旧 code 是否有关联的用户申请:
```sql
-- 检查旧 code 是否有未审核的申请引用
SELECT COUNT(*) FROM auth.user_applications
WHERE site_code = :old_code AND status = 'pending';
```
- 如果有未审核申请引用 → 保留历史记录(`is_current=false`,但不删除)
- 如果无任何申请引用 → 从 `biz.site_code_history` 中删除该条记录
> 目的:防止用户已用旧 code 提交申请但尚未审核时,映射关系丢失
#### 3.2.3 简写ID 格式规范
- 总长度6 位
- 格式3 位字母/数字 + 3 位数字(如 `LLQ001``ABC123`
- 大小写:统一存储为大写
- 全局唯一:同一时刻不允许两个店铺使用相同 code含历史未清理的 code
### 3.3 租户/店铺信息展示
#### 3.3.1 租户下拉选项
创建管理员时,租户下拉数据来源:
```
GET /api/admin/tenants → biz.tenants (is_active=true)
返回:[{ id, tenantId, tenantName, connectorName }]
```
#### 3.3.2 店铺列表
选择租户后,加载该租户下所有店铺:
```
GET /api/admin/tenants/{tenant_id}/sites → biz.sites (tenant_id=?, is_active=true)
返回:[{ id, siteId, siteName, siteCode, siteLabel }]
```
---
## 四、接口设计
### 4.1 新增接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 租户列表 | GET | `/api/admin/tenants` | 所有活跃租户(含连接器名称) |
| 租户下店铺列表 | GET | `/api/admin/tenants/{tenant_id}/sites` | 指定租户下所有店铺(含当前 site_code |
| 删除管理员 | DELETE | `/api/admin/tenant-admins/{id}` | 软删除is_active=false |
| 设置简写ID | PUT | `/api/admin/sites/{site_id}/site-code` | 设置/修改店铺简写ID |
| 简写ID历史 | GET | `/api/admin/sites/{site_id}/site-code-history` | 查看某店铺的简写ID变更历史 |
### 4.2 修改接口
| 接口 | 变更内容 |
|------|----------|
| `POST /api/admin/tenant-admins` | 创建时 `tenant_id` 改为从 `biz.tenants` 选择;`managed_site_ids``biz.sites` 选择 |
| `PATCH /api/admin/tenant-admins/{id}` | 增加 `username` 可修改(需校验唯一性) |
| `GET /api/admin/tenant-admins` | 默认只返回 `is_active=true`;增加 `include_inactive` 参数 |
---
## 五、ETL 同步任务
### 5.1 店铺信息增量同步
新增 ETL 任务:从 `dwd.dim_site`ETL 库)增量同步到 `biz.sites`(业务库)。
#### 同步逻辑
```
1. 读取 dwd.dim_site WHERE scd2_is_current = 1
2. 对比 biz.sites 中已有记录:
- 新增店铺site_id 不存在)→ INSERTsite_code 留空,待管理员设置)
- 店铺名称变更 → UPDATE site_name
- 店铺标签变更 → UPDATE site_label
3. 不删除已有记录(即使上游标记为关闭)
```
#### 触发方式
- 手动触发:管理后台按钮或 CLI 命令
- 定时触发:随 ETL 日常调度DWD 层完成后)
#### 租户信息
- `biz.tenants` 中的 `tenant_name` 暂不自动同步(上游无 tenant_name 字段)
- 首次由迁移脚本写入,后续由管理员在管理后台手动修改
---
## 六、数据迁移计划
### 6.1 迁移步骤
```
1. 创建新表biz.connectors → biz.tenants → biz.sites → biz.site_code_history
2. 写入种子数据connectors飞球、tenants从 dim_site 提取)
3. 迁移 auth.site_code_mapping → biz.sites仅真实数据排除 tenant_id IS NULL 的测试数据)
4. 为已有 site_code 创建 site_code_history 记录is_current=true
5. 运行 ETL 同步任务,补充 biz.sites 中缺失的店铺dim_site 中有但 site_code_mapping 中没有的)
6. 后端代码切换:所有读取 auth.site_code_mapping 的地方改为读取 biz.sites
7. 验证:对比新旧表数据一致性
8. 废弃原表:重命名为 auth._archived_site_code_mapping
```
### 6.2 回滚策略
- 迁移期间保留原表不动
- 如需回滚:删除 biz.sites/tenants/connectors/site_code_history恢复后端代码指向 auth.site_code_mapping
---
## 七、前端页面设计
### 7.1 页面布局
```
┌─────────────────────────────────────────────────────┐
│ 租户管理员 │
│ [搜索框] [显示已禁用 ☐] [+ 创建管理员] │
├─────────────────────────────────────────────────────┤
│ 用户名 │ 显示名称 │ 租户 │ 管辖门店 │ 状态 │ 操作 │
│ admin1 │ 张三 │ 朗朗 │ 朗朗桌球 │ 启用 │ 编辑 │
│ │ │ │ │ │ 重置 │
│ │ │ │ │ │ 简写ID │
│ │ │ │ │ │ 删除 │
└─────────────────────────────────────────────────────┘
```
### 7.2 创建弹窗2 步)
```
步骤 1/2创建账号
┌──────────────────────────────┐
│ 租户: [▼ 朗朗桌球 ] │
│ 用户名: [ ] │
│ 初始密码:[ ] │
│ 显示名称:[ ] │
│ 管辖门店:[☑ 朗朗桌球 ] │
│ │
│ [下一步] [取消] │
└──────────────────────────────┘
步骤 2/2设置简写ID
┌──────────────────────────────┐
│ 店铺 当前简写ID │
│ 朗朗桌球 [LLQ001 ] │
│ │
│ 格式6位3字母+3数字
│ │
│ [跳过] [完成创建] [上一步]│
└──────────────────────────────┘
```
### 7.3 简写ID 管理弹窗
```
管理简写ID — 朗朗桌球(租户)
┌──────────────────────────────────────┐
│ 店铺 当前ID 操作 │
│ 朗朗桌球 LLQ001 [修改] │
│ │
│ 修改简写ID
│ 新ID[ ] [保存] [取消] │
│ │
│ 变更历史: │
│ LLQ001 当前生效 2026-03-22 │
│ LL001 已失效 2026-02-25 (保留) │
│ │
│ [关闭] │
└──────────────────────────────────────┘
```
---
## 八、影响范围
### 8.1 后端
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `app/routers/admin_tenant_admins.py` | 修改 | 增加 DELETE 端点、修改 CREATE/EDIT 逻辑 |
| `app/schemas/admin_tenant_admins.py` | 修改 | 新增/修改请求响应 Schema |
| `app/routers/admin_registry.py` | 新建 | 租户/店铺/简写ID 管理接口 |
| `app/schemas/admin_registry.py` | 新建 | 注册体系 Schema |
### 8.2 前端admin-web
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `src/pages/TenantAdmins/index.tsx` | 重构 | 2 步创建、删除、简写ID 管理 |
| `src/api/tenantAdmins.ts` | 修改 | 新增 API 调用 |
| `src/api/registry.ts` | 新建 | 租户/店铺列表 API |
### 8.3 数据库
| 操作 | 对象 | 说明 |
|------|------|------|
| 新建 | `biz.connectors` | 连接器注册表 |
| 新建 | `biz.tenants` | 租户注册表 |
| 新建 | `biz.sites` | 店铺注册表(合并 site_code_mapping |
| 新建 | `biz.site_code_history` | 简写ID 变更历史 |
| 废弃 | `auth.site_code_mapping` | 迁移完成后废弃 |
### 8.4 ETL
| 变更 | 说明 |
|------|------|
| 新增同步任务 | `dwd.dim_site``biz.sites` 增量同步 |
### 8.5 小程序端
| 变更 | 说明 |
|------|------|
| 用户申请时的 site_code 查询 | 从 `auth.site_code_mapping` 切换到 `biz.sites` + `biz.site_code_history` |
---
## 九、约束与边界条件
1. 一个租户只有一个管理员(软限制,不加 DB 约束)
2. 简写ID 全局唯一(含历史记录),通过 UNIQUE 约束保证
3. 删除管理员为软删除(`is_active = false`),不物理删除
4. 编辑时不可修改所属租户
5. 简写ID 格式6 位3+3 模式,统一大写存储
6. 历史简写ID 仅在无未审核申请引用时才可清理
7. ETL 同步不删除已有店铺记录(即使上游关闭)
8. `biz.tenants.tenant_name` 暂不自动同步,由管理员手动维护
---
## 十、不做的事情(明确排除)
1. 不做多连接器支持的完整实现(仅预留 `biz.connectors` 表结构)
2. 不做 `dwd.dim_site` 的物理迁移(保留在 `dwd` schema
3. 不做租户管理员的自助注册功能
4. 不做店铺管理员的管理(本 PRD 仅涉及租户管理员)
5. 不做 `auth.site_code_mapping` 的立即删除(迁移后保留为 `_archived`
6. 不做简写ID 的自动生成(由管理员手动设置)
---
## 十一、数据库现状参考
### ETL 库 `dwd.dim_site`(当前数据)
| site_id | tenant_id | shop_name | site_label |
|---------|-----------|-----------|------------|
| 2790685415443269 | 2790683160709957 | 朗朗桌球 | A |
### 业务库 `auth.site_code_mapping`(当前数据)
| id | site_code | site_id | site_name | tenant_id |
|----|-----------|---------|-----------|-----------|
| 1 | LL001 | 2790685415443269 | 朗朗桌球 | 2790683160709957 |
| 1448 | PT952 | 857189 | 测试球房_PT952 | NULL |
| 1470 | PT118 | 819193 | 测试球房_PT118 | NULL |
| 1471 | PT607 | 899675 | 测试球房_PT607 | NULL |
> 仅 id=1 为真实数据,其余为属性测试生成的测试数据(`tenant_id IS NULL`
### 业务库 `auth.tenant_admins`(现有结构)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| username | VARCHAR(50) | 登录用户名UNIQUE |
| password_hash | VARCHAR(255) | bcrypt 哈希 |
| display_name | VARCHAR(100) | 显示名称 |
| tenant_id | BIGINT | 所属租户 |
| managed_site_ids | BIGINT[] | 管辖门店 ID 列表 |
| is_active | BOOLEAN | 账号状态 |
| created_by | BIGINT | 创建者 |
| created_at | TIMESTAMPTZ | 创建时间 |
| last_login_at | TIMESTAMPTZ | 最后登录时间 |
> `tenant_admins` 表结构不变,仅后端逻辑调整(创建时从 `biz.tenants` 选择租户)
---
## 十二、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| NS4 原始 PRD | `docs/prd/Neo_Specs/NS4-tenant-admin-web.md` | 租户管理后台完整需求 |
| P3 认证体系 | `docs/prd/specs/P3-miniapp-auth-system.md` | site_code / user_applications 设计 |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |