包含多个会话的累积代码变更: - 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>
20 KiB
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(无下拉选项,无名称参考) - 无法删除管理员记录
- 无法管理简写ID(site_code),简写ID 的创建和修改散落在数据库手动操作中
- 缺少项目级的「连接器 → 租户 → 店铺」注册体系,租户名称无处存储
1.2 目标
- 建立项目级注册体系:
biz.connectors→biz.tenants→biz.sites,统一管理连接器、租户、店铺三级关系 - 将
auth.site_code_mapping合并迁移至biz.sites,简写ID 成为店铺属性 - 简写ID 变更增量记录(
biz.site_code_history),保护已提交但未审核的用户申请 - 重构租户管理员页面:支持删除(软删除)、2 步创建流程、简写ID 管理
- 新增 ETL 增量同步任务:从
dwd.dim_site同步店铺信息到业务库
二、数据模型设计
2.1 新建表
表 1:biz.connectors — 连接器注册表
记录本项目接入的上游 SaaS 系统。当前仅「飞球」一个连接器,预留多连接器扩展。
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 系统';
初始数据:
INSERT INTO biz.connectors (connector_key, display_name)
VALUES ('feiqiu', '飞球');
表 2:biz.tenants — 租户注册表
记录每个连接器下的租户信息。tenant_id 来自上游系统(飞球的 tenant_id)。
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 提取当前唯一租户):
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 为当前生效的简写ID(6 位,3+3 格式)。
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 '当前生效的简写ID,6位字符(3+3格式),全局唯一';
初始数据(从 auth.site_code_mapping 迁移):
-- 仅迁移真实数据(排除测试数据 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,用于保护已提交但未审核的用户申请。
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 在数据迁移完成并验证后标记为废弃:
- 迁移期间保留原表,新旧并行
- 后端代码切换到
biz.sites读取 - 验证无误后,原表重命名为
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
用户在管理后台修改某店铺的简写ID(old_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_history(is_current=true)
3. 更新 biz.sites.site_code = new_code
4. 清理无引用的历史记录(见 3.2.2)
3.2.2 历史记录清理
每次修改简写ID 时,检查被替换的旧 code 是否有关联的用户申请:
-- 检查旧 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 不存在)→ INSERT(site_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 |
九、约束与边界条件
- 一个租户只有一个管理员(软限制,不加 DB 约束)
- 简写ID 全局唯一(含历史记录),通过 UNIQUE 约束保证
- 删除管理员为软删除(
is_active = false),不物理删除 - 编辑时不可修改所属租户
- 简写ID 格式:6 位,3+3 模式,统一大写存储
- 历史简写ID 仅在无未审核申请引用时才可清理
- ETL 同步不删除已有店铺记录(即使上游关闭)
biz.tenants.tenant_name暂不自动同步,由管理员手动维护
十、不做的事情(明确排除)
- 不做多连接器支持的完整实现(仅预留
biz.connectors表结构) - 不做
dwd.dim_site的物理迁移(保留在dwdschema) - 不做租户管理员的自助注册功能
- 不做店铺管理员的管理(本 PRD 仅涉及租户管理员)
- 不做
auth.site_code_mapping的立即删除(迁移后保留为_archived) - 不做简写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 表结构 |