# 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`
