chore: 文档与 IDE 配置整理
- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro) - CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/ - 新增 /spec-close、/pre-change 两个工作流命令 - DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表) - BD_Manual → BD_manual 命名统一(48 个文件) - 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数) - 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表) - 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档) - docs/database/README.md 索引更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
544
docs/database/BD_manual_tenant_admin_tables.md
Normal file
544
docs/database/BD_manual_tenant_admin_tables.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# 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)
|
||||
```
|
||||
Reference in New Issue
Block a user