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>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -0,0 +1,512 @@
# 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 表结构 |