# BD 手册:biz 注册体系表(NS4.1 registry) ## 概述 NS4.1 注册体系新增 4 张表,全部位于 `biz` Schema,建立「连接器 → 租户 → 店铺」三级注册体系。合并原 `auth.site_code_mapping`,统一管理上游 SaaS 系统、租户和店铺的关系,并为简写ID 提供归属和变更历史。 所有表位于 `zqyy_app` / `test_zqyy_app` 数据库。 ## 变更原因 - NS4.1 注册体系设计,建立项目级「连接器 → 租户 → 店铺」三级结构 - 合并原 `auth.site_code_mapping` 到 `biz.sites`,增加租户关联和简写ID 变更历史管理 - 数据来源:种子数据从 `auth.site_code_mapping` 迁移,ETL 增量同步从 `dwd.dim_site` ## 变更说明 | 库 | Schema | 表 | 变更类型 | 说明 | |----|--------|---|---------|------| | zqyy_app | biz | connectors | 新建 | 连接器注册表 | | zqyy_app | biz | tenants | 新建 | 租户注册表 | | zqyy_app | biz | sites | 新建 | 店铺注册表(合并原 auth.site_code_mapping) | | zqyy_app | biz | site_code_history | 新建 | 简写ID 变更历史表 | | zqyy_app | auth | site_code_mapping | 废弃重命名 | → `auth._archived_site_code_mapping` | --- ## 1. biz.connectors — 连接器注册表 记录本项目接入的上游 SaaS 系统。当前仅有飞球(feiqiu)一个连接器,预留多连接器扩展能力。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | SERIAL | PRIMARY KEY | 自增主键 | | connector_key | VARCHAR(50) | UNIQUE NOT NULL | 连接器标识(如 `'feiqiu'`) | | display_name | VARCHAR(100) | NOT NULL | 显示名称 | | is_active | BOOLEAN | NOT NULL DEFAULT true | 是否启用 | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | connectors_pkey | PRIMARY KEY | id | 主键 | | connectors_connector_key_key | UNIQUE | connector_key | 连接器标识唯一 | ### 种子数据 ```sql INSERT INTO biz.connectors (connector_key, display_name) VALUES ('feiqiu', '飞球'); ``` --- ## 2. biz.tenants — 租户注册表 连接器下的租户,`tenant_id` 来自上游系统。同一连接器下 `tenant_id` 唯一。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | SERIAL | PRIMARY KEY | 自增主键 | | connector_id | INTEGER | NOT NULL FK → 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() | 更新时间 | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | tenants_pkey | PRIMARY KEY | id | 主键 | | tenants_connector_id_tenant_id_key | UNIQUE | (connector_id, tenant_id) | 同一连接器下租户唯一 | | tenants_connector_id_fkey | FOREIGN KEY | connector_id → biz.connectors(id) | 外键关联连接器 | ### 种子数据 ```sql INSERT INTO biz.tenants (connector_id, tenant_id, tenant_name) VALUES (1, 2790683160709957, '朗朗桌球'); ``` --- ## 3. biz.sites — 店铺注册表 合并原 `auth.site_code_mapping`,增加租户关联和简写ID 管理。`site_id` 来自上游系统,全局唯一。`site_code` 为当前生效的简写ID(6 位字符,3+3 格式),全局唯一。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | SERIAL | PRIMARY KEY | 自增主键 | | tenant_id | INTEGER | NOT NULL FK → biz.tenants(id) | 所属租户 | | site_id | BIGINT | NOT NULL UNIQUE | 上游系统门店 ID | | site_name | VARCHAR(200) | — | 门店名称 | | site_code | VARCHAR(6) | UNIQUE | 当前生效的简写ID(3+3 格式,如 `LLQ001`) | | site_label | VARCHAR(50) | — | 门店标签 | | is_active | BOOLEAN | NOT NULL DEFAULT true | 是否启用 | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 更新时间 | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | sites_pkey | PRIMARY KEY | id | 主键 | | sites_site_id_key | UNIQUE | site_id | 上游门店 ID 唯一 | | sites_site_code_key | UNIQUE | site_code | 简写ID 全局唯一 | | sites_tenant_id_fkey | FOREIGN KEY | tenant_id → biz.tenants(id) | 外键关联租户 | ### 数据迁移 ```sql -- 从 auth.site_code_mapping 迁移真实数据 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; -- ETL 增量同步补充:通过 FDW 读取 dwd.dim_site(scd2_is_current=1), -- 补充 auth.site_code_mapping 中没有但 dwd.dim_site 中有的店铺 ``` --- ## 4. biz.site_code_history — 简写ID 变更历史表 增量记录所有使用过的简写ID。`site_code` 全局唯一(含历史),确保已退役的 code 不会被重新分配。每个 `site_id` 最多一条 `is_current=true` 记录。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | SERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 关联 biz.sites.site_id | | site_code | VARCHAR(6) | NOT NULL UNIQUE | 简写ID(全局唯一,含历史) | | is_current | BOOLEAN | NOT NULL DEFAULT false | true=当前生效,每个 site_id 最多一条 | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 | | retired_at | TIMESTAMPTZ | — | 退役时间(is_current=false 时设置) | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | site_code_history_pkey | PRIMARY KEY | id | 主键 | | site_code_history_site_code_key | UNIQUE | site_code | 简写ID 全局唯一(含历史) | ### 初始数据 ```sql -- 为已有 site_code 的店铺创建历史记录 INSERT INTO biz.site_code_history (site_id, site_code, is_current) SELECT site_id, site_code, true FROM biz.sites WHERE site_code IS NOT NULL; ``` --- ## 兼容性 | 组件 | 影响 | |------|------| | 后端 API | 全部切换到 `biz.sites` + `biz.site_code_history`。新增 `admin_registry` 路由模块(租户列表、店铺列表、简写ID 管理)。`admin_tenant_admins` 路由中 `tenant_id` 从 `biz.tenants` 选择。`tenant_users` 路由中 site_code 查询从 `auth.site_code_mapping` 切换到 `biz.sites` | | ETL | 无直接影响。店铺同步通过 FDW 只读访问 ETL 库 `dwd.dim_site`,写入 `biz.sites`。ETL 流程本身不变 | | 小程序 | 无需改动。用户申请时的 site_code 验证由后端 API 透明切换到 `biz.sites` + `biz.site_code_history` | | 管理后台(admin-web) | 新增注册体系 API 调用(`src/api/registry.ts`),租户管理员创建流程从 `biz.tenants`/`biz.sites` 选择 | | 原 auth.site_code_mapping | 迁移完成后重命名为 `auth._archived_site_code_mapping`,保留供回滚 | ## 回滚策略 ### 完整回滚(逆序 DROP + 恢复原表) ```sql BEGIN; -- 1. 逆序删除注册体系表 DROP TABLE IF EXISTS biz.site_code_history CASCADE; DROP TABLE IF EXISTS biz.sites CASCADE; DROP TABLE IF EXISTS biz.tenants CASCADE; DROP TABLE IF EXISTS biz.connectors CASCADE; -- 2. 恢复原表(如已重命名) ALTER TABLE IF EXISTS auth._archived_site_code_mapping RENAME TO site_code_mapping; COMMENT ON TABLE auth.site_code_mapping IS '店铺简写ID 映射表(已恢复)'; COMMIT; ``` 注意: - 回滚前需确认后端代码已切换回 `auth.site_code_mapping` 查询 - `CASCADE` 会级联删除依赖对象 - 如果 `biz.sites` 中已有新增店铺(ETL 同步补充的),回滚后这些数据将丢失 ## 验证 SQL ```sql -- 1. 验证 biz.sites 中已有 site_code 的店铺数量 SELECT COUNT(*) FROM biz.sites WHERE site_code IS NOT NULL; -- 预期:与原 auth.site_code_mapping 中有 site_code 的行数一致 -- 2. 验证 sites 与 site_code_history 的一致性 SELECT s.site_id, s.site_code, h.is_current FROM biz.sites s LEFT JOIN biz.site_code_history h ON h.site_id = s.site_id AND h.site_code = s.site_code WHERE s.site_code IS NOT NULL; -- 预期:所有行的 h.is_current = true(每个有 code 的店铺在历史表中有对应的当前记录) -- 3. 验证三级注册体系关联完整性 SELECT c.connector_key, t.tenant_name, COUNT(s.id) AS site_count FROM biz.connectors c JOIN biz.tenants t ON t.connector_id = c.id LEFT JOIN biz.sites s ON s.tenant_id = t.id GROUP BY c.connector_key, t.tenant_name; -- 预期:至少 1 行(feiqiu / 朗朗桌球 / N),site_count > 0 -- 4. 验证 4 张注册体系表全部存在 SELECT table_name FROM information_schema.tables WHERE table_schema = 'biz' AND table_name IN ('connectors', 'tenants', 'sites', 'site_code_history') ORDER BY table_name; -- 预期:返回 4 行 -- 5. 验证 site_code 全局唯一性(sites + history 无冲突) SELECT site_code, COUNT(*) AS cnt FROM biz.site_code_history GROUP BY site_code HAVING COUNT(*) > 1; -- 预期:返回 0 行(每个 site_code 在历史表中最多出现一次) -- 6. 验证种子数据 SELECT connector_key, display_name FROM biz.connectors WHERE connector_key = 'feiqiu'; -- 预期:1 行(feiqiu / 飞球) SELECT tenant_id, tenant_name FROM biz.tenants WHERE tenant_id = 2790683160709957; -- 预期:1 行(2790683160709957 / 朗朗桌球) ``` ## 关联文件 - DDL 基线(biz):`docs/database/ddl/zqyy_app__biz.sql` - 迁移脚本:`db/zqyy_app/migrations/2026-03-22__ns41_registry_tables.sql` - 后端路由:`apps/backend/app/routers/admin_registry.py` - 后端 Schema:`apps/backend/app/schemas/admin_registry.py` - 管理员路由:`apps/backend/app/routers/admin_tenant_admins.py` - 前端 API:`apps/admin-web/src/api/registry.ts` - Spec:`.kiro/specs/admin-web-enhancement/` - PRD:`docs/prd/Neo_Specs/NS4.1-tenant-admin-redesign.md`