# BD 手册:租户管理后台表(NS4 tenant-admin-web) ## 概述 NS4 租户管理后台新增 6 张表,分布在 `auth` 和 `biz` 两个 Schema 中。`auth.tenant_admins` 为租户管理员认证表,与小程序 `auth.users`(微信登录)完全隔离;`biz` Schema 下 5 张表支撑 Excel 数据上传功能(上传日志、助教奖罚、3 张 staging 暂存表)。 所有表位于 `zqyy_app` / `test_zqyy_app` 数据库。 ## 变更说明 | 库 | Schema | 表 | 变更类型 | 说明 | |----|--------|---|---------|------| | zqyy_app | auth | tenant_admins | 新建 | 租户管理员认证表 | | zqyy_app | biz | excel_upload_log | 新建 | Excel 上传记录表 | | zqyy_app | biz | salary_adjustments | 新建 | 助教奖罚明细表 | | zqyy_app | biz | stg_finance_expense | 新建 | 财务支出暂存表 | | zqyy_app | biz | stg_platform_income | 新建 | 团购收入暂存表 | | zqyy_app | biz | stg_recharge_commission | 新建 | 充值业绩归属暂存表 | --- ## 1. auth.tenant_admins — 租户管理员表 独立于小程序 `auth.users`,使用用户名+密码登录,JWT `aud=tenant-admin` 隔离。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | username | VARCHAR(50) | UNIQUE NOT NULL | 登录用户名 | | password_hash | VARCHAR(255) | NOT NULL | bcrypt 哈希密码 | | display_name | VARCHAR(100) | — | 显示名称 | | tenant_id | BIGINT | NOT NULL | 所属租户 ID | | managed_site_ids | BIGINT[] | NOT NULL | 管辖门店 ID 列表(数据隔离依据) | | admin_type | VARCHAR(20) | NOT NULL DEFAULT 'tenant_admin' | 管理员类型:tenant_admin(租户管理员)/ site_admin(店铺管理员),CHECK 约束 | | is_active | BOOLEAN | DEFAULT true | 账号状态(false=禁用,登录返回 403,仅控制启用/禁用,与软删除无关) | | deleted_at | TIMESTAMPTZ | DEFAULT NULL | 软删除时间戳:NULL=正常,非 NULL=已删除(与 is_active 分离) | | created_by | BIGINT | — | 创建者(管理员 ID) | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | | last_login_at | TIMESTAMPTZ | — | 最后登录时间 | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | tenant_admins_pkey | PRIMARY KEY | id | 主键 | | tenant_admins_username_key | UNIQUE | username | 用户名唯一 | | chk_admin_type | CHECK | admin_type | admin_type IN ('tenant_admin', 'site_admin') | | idx_tenant_admin_tenant | INDEX (btree) | tenant_id | 按租户查询 | | idx_tenant_admins_active_not_deleted | INDEX (btree, partial) | is_active WHERE deleted_at IS NULL | 加速列表和登录查询(仅索引未删除记录) | | idx_tenant_admins_username_lower | UNIQUE (partial) | LOWER(username) WHERE deleted_at IS NULL | 大小写不敏感唯一约束(2026-03-23) | --- ## 2. biz.excel_upload_log — Excel 上传记录表 记录每次 Excel 上传的批次信息,支撑上传→校验→冲突→确认的完整流程。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 门店 ID | | upload_type | VARCHAR(30) | NOT NULL, CHECK | 模板类型(见枚举) | | file_name | VARCHAR(255) | NOT NULL | 原始文件名 | | uploaded_by | BIGINT | NOT NULL | 上传人(管理员 ID) | | row_count | INTEGER | DEFAULT 0 | 数据行数 | | conflict_count | INTEGER | DEFAULT 0 | 冲突行数 | | resolved_count | INTEGER | DEFAULT 0 | 已解决冲突数 | | status | VARCHAR(20) | NOT NULL, CHECK | 批次状态(见枚举) | | error_detail | JSONB | — | 错误详情 / 临时缓存上传数据 | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 上传时间 | | confirmed_at | TIMESTAMPTZ | — | 确认写入时间 | ### upload_type 枚举值 | 值 | 说明 | |----|------| | expense | 财务支出 | | platform_income | 团购收入 | | salary_adj | 助教奖罚 | | recharge_commission | 充值业绩归属 | ### status 枚举值 | 值 | 说明 | |----|------| | pending | 待确认(已上传校验通过,等待用户确认写入) | | confirmed | 已确认(数据已写入目标表) | | failed | 失败(写入过程出错,已回滚) | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | excel_upload_log_pkey | PRIMARY KEY | id | 主键 | | CHECK (upload_type) | CHECK | upload_type | 限制模板类型枚举 | | CHECK (status) | CHECK | status | 限制状态枚举 | | idx_excel_log_site | INDEX (btree) | (site_id, created_at DESC) | 按门店+时间查询 | --- ## 3. biz.salary_adjustments — 助教奖罚明细表 直接写入 biz Schema(非 staging),记录助教扣款/奖金明细。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 门店 ID | | assistant_id | BIGINT | — | 匹配到的助教 ID(可空,人员匹配失败时为 NULL) | | assistant_name | VARCHAR(100) | NOT NULL | 助教姓名(Excel 原始值) | | assistant_number | VARCHAR(50) | NOT NULL | 助教编号(Excel 原始值) | | salary_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM) | | adjustment_type | VARCHAR(20) | NOT NULL, CHECK | 类型:deduction(扣款)/ bonus(奖金) | | amount | NUMERIC(12,2) | NOT NULL, CHECK (> 0) | 金额(正数) | | reason | VARCHAR(200) | NOT NULL | 原因说明 | | upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | | created_by | BIGINT | — | 上传人(管理员 ID) | ### adjustment_type 枚举值 | 值 | Excel 中文值 | 说明 | |----|-------------|------| | deduction | 扣款 | 扣款 | | bonus | 奖金 | 奖金 | ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | salary_adjustments_pkey | PRIMARY KEY | id | 主键 | | CHECK (adjustment_type) | CHECK | adjustment_type | 限制类型枚举 | | CHECK (amount) | CHECK | amount | 金额必须 > 0 | | salary_adjustments_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 | | idx_salary_adj_site_month | INDEX (btree) | (site_id, salary_month) | 按门店+月份查询 | | idx_salary_adj_assistant_month | INDEX (btree) | (assistant_id, salary_month) | 按助教+月份查询 | --- ## 4. biz.stg_finance_expense — 财务支出暂存表 通过 Excel 上传写入,等待 ETL 同步到正式表。 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 门店 ID | | expense_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM) | | category | VARCHAR(50) | NOT NULL | 支出类别(8 值枚举) | | amount | NUMERIC(12,2) | NOT NULL | 金额 | | remark | TEXT | — | 备注 | | upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 | | synced_at | TIMESTAMPTZ | — | ETL 同步时间(NULL=未同步) | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | ### 冲突检测主键 `(site_id, expense_month, category)` — 同门店同月份同类别视为冲突。 ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | stg_finance_expense_pkey | PRIMARY KEY | id | 主键 | | stg_finance_expense_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 | --- ## 5. biz.stg_platform_income — 团购收入暂存表 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 门店 ID | | income_month | VARCHAR(7) | NOT NULL | 月份(格式 YYYY-MM) | | platform_name | VARCHAR(100) | NOT NULL | 平台名称 | | amount | NUMERIC(12,2) | NOT NULL | 收入金额 | | remark | TEXT | — | 备注 | | upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 | | synced_at | TIMESTAMPTZ | — | ETL 同步时间(NULL=未同步) | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | ### 冲突检测主键 `(site_id, income_month, platform_name)` — 同门店同月份同平台视为冲突。 ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | stg_platform_income_pkey | PRIMARY KEY | id | 主键 | | stg_platform_income_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 | --- ## 6. biz.stg_recharge_commission — 充值业绩归属暂存表 ### 表结构 | 列名 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGSERIAL | PRIMARY KEY | 自增主键 | | site_id | BIGINT | NOT NULL | 门店 ID | | recharge_date | DATE | NOT NULL | 充值日期 | | member_name | VARCHAR(100) | NOT NULL | 会员名称 | | recharge_amount | NUMERIC(12,2) | NOT NULL | 充值金额 | | assigned_assistant | VARCHAR(100) | NOT NULL | 归属助教 | | reward_amount | NUMERIC(12,2) | NOT NULL | 奖励金额 | | upload_batch_id | BIGINT | FK → excel_upload_log(id) | 上传批次 | | synced_at | TIMESTAMPTZ | — | ETL 同步时间(NULL=未同步) | | created_at | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 | ### 冲突检测主键 `(site_id, recharge_date, member_name, assigned_assistant)` — 同门店同日期同会员同助教视为冲突。 ### 约束与索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | stg_recharge_commission_pkey | PRIMARY KEY | id | 主键 | | stg_recharge_commission_upload_batch_id_fkey | FOREIGN KEY | upload_batch_id → excel_upload_log(id) | 关联上传批次 | --- ## 兼容性 - **后端 API**:5 个新路由模块(tenant_auth / tenant_users / tenant_excel / tenant_clues / admin_tenant_admins)全部依赖上述表 - **ETL**:3 张 staging 表(stg_finance_expense / stg_platform_income / stg_recharge_commission)的 `synced_at` 字段供 ETL 同步标记,ETL 读取 `synced_at IS NULL` 的行进行同步 - **小程序**:无直接影响(租户管理后台独立认证体系) - **管理后台(admin-web)**:新增租户管理员 CRUD 页面,调用 admin_tenant_admins 路由 ## 回滚策略 ### 完整回滚(按依赖顺序) ```sql BEGIN; -- 先删除有外键依赖的表 DROP TABLE IF EXISTS biz.salary_adjustments CASCADE; DROP TABLE IF EXISTS biz.stg_finance_expense CASCADE; DROP TABLE IF EXISTS biz.stg_platform_income CASCADE; DROP TABLE IF EXISTS biz.stg_recharge_commission CASCADE; -- 再删除被引用的表 DROP TABLE IF EXISTS biz.excel_upload_log CASCADE; -- 最后删除认证表 DROP TABLE IF EXISTS auth.tenant_admins CASCADE; -- 清理序列(CASCADE 已处理,此处为显式确认) DROP SEQUENCE IF EXISTS biz.excel_upload_log_id_seq; DROP SEQUENCE IF EXISTS biz.salary_adjustments_id_seq; DROP SEQUENCE IF EXISTS biz.stg_finance_expense_id_seq; DROP SEQUENCE IF EXISTS biz.stg_platform_income_id_seq; DROP SEQUENCE IF EXISTS biz.stg_recharge_commission_id_seq; DROP SEQUENCE IF EXISTS auth.tenant_admins_id_seq; COMMIT; ``` ## 验证步骤 ```sql -- 1. 确认 auth.tenant_admins 表存在且列完整(12 列,含 admin_type + deleted_at) SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'auth' AND table_name = 'tenant_admins' ORDER BY ordinal_position; -- 预期:12 行(id, username, password_hash, display_name, tenant_id, -- managed_site_ids, admin_type, is_active, deleted_at, created_by, created_at, last_login_at) -- 2. 确认 tenant_admins 唯一约束 SELECT conname, contype FROM pg_constraint WHERE conrelid = 'auth.tenant_admins'::regclass; -- 预期:包含 tenant_admins_pkey (p) 和 tenant_admins_username_key (u) -- 3. 确认 biz.excel_upload_log 表存在且 CHECK 约束正确 SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'biz.excel_upload_log'::regclass AND contype = 'c'; -- 预期:2 行(upload_type CHECK 和 status CHECK) -- 4. 确认 biz.salary_adjustments 外键和 CHECK 约束 SELECT conname, contype, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'biz.salary_adjustments'::regclass AND contype IN ('c', 'f'); -- 预期:3 行(adjustment_type CHECK, amount CHECK, upload_batch_id FK) -- 5. 确认 3 张 staging 表的外键 SELECT c.conname, c.conrelid::regclass AS table_name FROM pg_constraint c WHERE c.confrelid = 'biz.excel_upload_log'::regclass AND c.contype = 'f'; -- 预期:4 行(salary_adjustments + 3 张 staging 表各 1 个 FK) -- 6. 确认索引 SELECT schemaname, tablename, indexname FROM pg_indexes WHERE tablename IN ('tenant_admins', 'excel_upload_log', 'salary_adjustments', 'stg_finance_expense', 'stg_platform_income', 'stg_recharge_commission') ORDER BY tablename, indexname; -- 预期:tenant_admins 3 个(pkey + idx_tenant_admin_tenant + idx_tenant_admins_active_not_deleted) -- excel_upload_log 2 个(pkey + idx_excel_log_site) -- salary_adjustments 3 个(pkey + idx_salary_adj_site_month + idx_salary_adj_assistant_month) -- stg_* 各 1 个(pkey) -- 7. 确认 6 张表均可查询 SELECT 'auth.tenant_admins' AS tbl, COUNT(*) FROM auth.tenant_admins UNION ALL SELECT 'biz.excel_upload_log', COUNT(*) FROM biz.excel_upload_log UNION ALL SELECT 'biz.salary_adjustments', COUNT(*) FROM biz.salary_adjustments UNION ALL SELECT 'biz.stg_finance_expense', COUNT(*) FROM biz.stg_finance_expense UNION ALL SELECT 'biz.stg_platform_income', COUNT(*) FROM biz.stg_platform_income UNION ALL SELECT 'biz.stg_recharge_commission', COUNT(*) FROM biz.stg_recharge_commission; -- 预期:6 行,各表行数 ≥ 0 ``` ## NS4.1 变更补充(2026-03-22) ### auth.tenant_admins 行为变更 NS4.1 对 `auth.tenant_admins` 表做了以下调整: #### deleted_at 软删除字段(已合并入主 DDL) `deleted_at` 字段已合并到主迁移脚本 `2026-03-20__ns4_tenant_admin_tables.sql` 中,不再需要独立的 ALTER TABLE 迁移。字段语义: - `NULL` = 正常记录 - 非 `NULL` = 已删除(时间戳记录删除时间) - 与 `is_active` 分离:`is_active` 控制启用/禁用,`deleted_at` 控制软删除 - 部分索引 `idx_tenant_admins_active_not_deleted` 仅索引 `deleted_at IS NULL` 的记录 #### 软删除逻辑 `DELETE /api/admin/tenant-admins/{id}` 端点实际执行软删除:将 `is_active` 设置为 `false`,不物理删除行。已禁用的管理员再次删除返回 409。 ```sql -- 软删除 UPDATE auth.tenant_admins SET is_active = false WHERE id = :id AND is_active = true; ``` #### tenant_id 来源变更 创建管理员时,`tenant_id` 不再是自由输入,而是从 `biz.tenants` 表中选择。后端校验 `tenant_id` 在 `biz.tenants` 中存在且 `is_active=true`。 ```sql -- 创建时校验 SELECT id FROM biz.tenants WHERE id = :tenant_id AND is_active = true; ``` #### username 可编辑 `PATCH /api/admin/tenant-admins/{id}` 端点支持修改 `username`。修改时校验全局唯一性(大小写不敏感),冲突返回 409 Conflict。 ```sql -- 唯一性校验(大小写不敏感) SELECT id FROM auth.tenant_admins WHERE LOWER(username) = LOWER(:new_username) AND id != :current_id AND deleted_at IS NULL; ``` #### 列表过滤 `GET /api/admin/tenant-admins` 端点默认只返回 `is_active=true` 的记录。新增 `include_inactive` 查询参数,设为 `true` 时返回所有记录(含已禁用)。 列表查询 JOIN `biz.tenants` 获取 `tenant_name` 字段。 --- ## 关联文件 - DDL 基线(auth):`docs/database/ddl/zqyy_app__auth.sql` - DDL 基线(biz):`docs/database/ddl/zqyy_app__biz.sql` - 迁移脚本:`db/zqyy_app/migrations/2026-03-20__ns4_tenant_admin_tables.sql` - 迁移脚本:`db/zqyy_app/migrations/2026-03-23__case_insensitive_username.sql` - 迁移脚本:`db/zqyy_app/migrations/2026-03-23__cleanup_roles_add_admin_type.sql` - 后端路由:`apps/backend/app/routers/tenant_auth.py`、`tenant_users.py`、`tenant_excel.py`、`tenant_clues.py`、`admin_tenant_admins.py`、`tenant_site_admins.py` - 后端 Schema:`apps/backend/app/schemas/tenant_excel.py`、`admin_tenant_admins.py` - 认证模块:`apps/backend/app/auth/tenant_admins.py` - Spec:`.kiro/specs/tenant-admin-web/` --- ## NS4.2 变更补充(2026-03-23) ### auth.tenant_admins 用户名大小写不敏感 #### 变更说明 登录、创建、编辑管理员时,用户名统一转小写存储,查询使用 `LOWER()` 比较。避免 `Admin` 和 `admin` 被视为不同账号。 #### 新增索引 | 名称 | 类型 | 列 | 说明 | |------|------|---|------| | idx_tenant_admins_username_lower | UNIQUE (partial) | LOWER(username) WHERE deleted_at IS NULL | 大小写不敏感唯一约束 | #### 代码变更 | 文件 | 变更 | |------|------| | `tenant_auth.py` | 登录查询 `WHERE LOWER(username) = LOWER(%s)` | | `admin_tenant_admins.py` | 创建 `INSERT ... VALUES (LOWER(%s), ...)`;编辑 `SET username = LOWER(%s)`;唯一性校验 `LOWER()` 比较 | #### 兼容性 - 后端 API:登录和 CRUD 接口透明兼容,无需前端改动 - ETL:无影响 - 小程序:无影响(独立认证体系) - 管理后台(admin-web):无需改动,后端统一处理 #### 回滚策略 ```sql -- 1. 删除函数索引 DROP INDEX IF EXISTS auth.idx_tenant_admins_username_lower; -- 2. 代码回滚:恢复 WHERE username = %s(不带 LOWER) -- 注意:已小写化的用户名不可逆,但不影响功能 ``` #### 验证 SQL ```sql -- 1. 确认索引存在 SELECT indexname FROM pg_indexes WHERE tablename = 'tenant_admins' AND indexname = 'idx_tenant_admins_username_lower'; -- 预期:1 行 -- 2. 确认无大写用户名残留 SELECT username FROM auth.tenant_admins WHERE username != LOWER(username); -- 预期:0 行 -- 3. 确认大小写不敏感登录可用 SELECT id FROM auth.tenant_admins WHERE LOWER(username) = LOWER('Admin') AND deleted_at IS NULL; -- 预期:与 SELECT ... WHERE LOWER(username) = 'admin' 结果一致 ``` --- ## NS4.3 变更补充(2026-03-23) ### 角色体系隔离 + 店铺管理员支持 #### 变更说明 | 对象 | 变更类型 | 说明 | |------|---------|------| | `auth.tenant_admins.admin_type` | 新增字段 | `VARCHAR(20) NOT NULL DEFAULT 'tenant_admin'`,区分租户管理员(`tenant_admin`)和店铺管理员(`site_admin`) | | `chk_admin_type` | 新增约束 | `CHECK (admin_type IN ('tenant_admin', 'site_admin'))` | | `auth.roles` — `site_admin` / `tenant_admin` | 删除 | 小程序 RBAC 体系不需要这两个角色,租户/店铺管理员的区分通过 `admin_type` 列实现 | | `auth.role_permissions` | 删除关联 | 删除 `site_admin` / `tenant_admin` 对应的 10 条权限映射 | | `auth.roles` — `head_coach` / `manager` | 新增 | 小程序端新增教练和管理员角色 | #### 业务规则 - 租户管理员(`admin_type='tenant_admin'`):可管理所有管辖门店,可创建/编辑/删除店铺管理员 - 店铺管理员(`admin_type='site_admin'`):仅可管理分配的门店,不可创建其他管理员 - 登录后界面一致,仅数据范围不同(由 `managed_site_ids` 控制) - 用户名格式:`{第一个管辖店铺的 site_code} + 最长 50 字符` - 只有 `admin_type='tenant_admin'` 的管理员可以访问 `/api/tenant/site-admins/*` 端点 #### 新增后端端点(店铺管理员 CRUD) | 方法 | 路径 | 说明 | 权限 | |------|------|------|------| | GET | `/api/tenant/site-admins` | 列出店铺管理员 | 仅 tenant_admin | | POST | `/api/tenant/site-admins` | 创建店铺管理员 | 仅 tenant_admin | | PATCH | `/api/tenant/site-admins/{id}` | 编辑店铺管理员 | 仅 tenant_admin | | DELETE | `/api/tenant/site-admins/{id}` | 删除店铺管理员(软删除) | 仅 tenant_admin | | POST | `/api/tenant/site-admins/{id}/reset-password` | 重置密码 | 仅 tenant_admin | #### 代码变更 | 文件 | 变更 | |------|------| | `tenant_auth.py` | 登录查询加入 `admin_type` 列,JWT 签发包含 `admin_type` | | `tenant_admins.py` | `CurrentTenantAdmin` dataclass 加入 `admin_type` 字段 | | `tenant_site_admins.py` | 新增路由模块,5 个 CRUD 端点 | | `main.py` | 注册 `tenant_site_admins` 路由 | | `admin_tenant_admins.py` | SQL 查询加入 `ta.admin_type` 列 | | `xcx_auth.py` | `dev-switch-role` 硬编码更新(删除 site_admin/tenant_admin,新增 head_coach/manager) | #### 兼容性 | 组件 | 影响 | |------|------| | 后端 API | 直接依赖。JWT 新增 `admin_type` 字段;新增 5 个店铺管理员端点;admin-web 列表接口返回 `admin_type` | | 租户管理后台(tenant-admin) | 直接依赖。菜单根据 `adminType` 动态显示;新增店铺管理员管理页面 | | 管理后台(admin-web) | 间接依赖。租户管理员列表新增"类型"列显示 | | 小程序 | 间接依赖。`auth.roles` 删除 site_admin/tenant_admin,新增 head_coach/manager | | ETL | 无影响 | #### 回滚策略 ```sql BEGIN; -- 1. 回滚 admin_type 列 ALTER TABLE auth.tenant_admins DROP CONSTRAINT IF EXISTS chk_admin_type; ALTER TABLE auth.tenant_admins DROP COLUMN IF EXISTS admin_type; -- 2. 回滚角色变更(恢复 site_admin/tenant_admin,删除 head_coach/manager) DELETE FROM auth.role_permissions WHERE role_id IN (SELECT id FROM auth.roles WHERE code IN ('head_coach', 'manager')); DELETE FROM auth.roles WHERE code IN ('head_coach', 'manager'); INSERT INTO auth.roles (code, name, description) VALUES ('site_admin', '店铺管理员', '单店管理员,可查看所有看板和审核用户'), ('tenant_admin', '租户管理员', '连锁管理员,可管理多店铺和所有功能'); -- 恢复 site_admin/tenant_admin 的权限映射(各 5 条) INSERT INTO auth.role_permissions (role_id, permission_id) SELECT r.id, p.id FROM auth.roles r, auth.permissions p WHERE r.code IN ('site_admin', 'tenant_admin'); -- 3. 代码回滚:移除 tenant_site_admins 路由;JWT 移除 admin_type;恢复 dev-switch-role 硬编码 COMMIT; ``` #### 验证 SQL ```sql -- 1. 确认 admin_type 列存在且默认值正确 SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = 'auth' AND table_name = 'tenant_admins' AND column_name = 'admin_type'; -- 预期:admin_type, character varying, 'tenant_admin'::character varying -- 2. 确认 CHECK 约束 SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'auth.tenant_admins'::regclass AND conname = 'chk_admin_type'; -- 预期:chk_admin_type, CHECK ((admin_type)::text = ANY (...)) -- 3. 确认角色体系(4 条,无 site_admin/tenant_admin) SELECT code, name FROM auth.roles ORDER BY id; -- 预期:coach, staff, head_coach, manager -- 4. 确认角色-权限映射(11 条) SELECT r.code, COUNT(rp.permission_id) AS perm_count FROM auth.roles r JOIN auth.role_permissions rp ON r.id = rp.role_id GROUP BY r.code ORDER BY r.code; -- 预期:coach=2, head_coach=2, manager=5, staff=2(共 11 条,head_coach 仅 view_tasks+view_board) ```