# BD 手册：user_site_roles / user_assistant_binding 软删除

## 变更日期
2026-03-24

## 变更说明

在 `auth.user_site_roles` 和 `auth.user_assistant_binding` 两张表上新增软删除字段，替代原有的物理删除（`DELETE`）。

### 新增字段

| 表 | 字段 | 类型 | 默认值 | 说明 |
|----|------|------|--------|------|
| `auth.user_site_roles` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_site_roles` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |
| `auth.user_assistant_binding` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_assistant_binding` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |

### 新增索引

| 索引名 | 表 | 类型 | 说明 |
|--------|-----|------|------|
| `ix_user_site_roles_active` | `user_site_roles` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |
| `ix_user_assistant_binding_active` | `user_assistant_binding` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |

## 兼容性影响

### 后端 API（已同步修改）
所有查询 `user_site_roles` 和 `user_assistant_binding` 的位置均已添加 `AND is_removed = false` 过滤：
- `xcx_auth.py`：登录、me 接口、切换门店、刷新令牌、获取门店列表、dev 调试接口
- `tenant_users.py`：用户列表、编辑用户、更新绑定、移除用户
- `role.py`：权限查询、门店列表、角色检查
- `task_manager.py`：获取助教 ID
- `task_generator.py`：门店助教规模检查、入驻时间保护
- `performance_service.py`：助教信息查询

### ETL
无直接影响。ETL 不写入这两张表。

### 小程序
无代码改动。被移除的用户在小程序端会因角色查询返回空而进入已有的无权限路由。

### 租户管理后台
`remove_user` 操作从 `DELETE` 改为 `UPDATE SET is_removed = true, removed_at = now()`。

## 查询规则（强制）

所有读取 `user_site_roles` 或 `user_assistant_binding` 的 SELECT 查询，必须包含 `AND is_removed = false`（或等效的 JOIN 条件 `AND xxx.is_removed = false`）。

例外：
- 管理后台需要查看已移除记录的场景（如审计日志）
- dev 调试接口中的物理删除操作（仅开发模式）

## 回滚策略

```sql
-- 1. 恢复所有被软删除的记录
UPDATE auth.user_site_roles SET is_removed = false, removed_at = NULL WHERE is_removed = true;
UPDATE auth.user_assistant_binding SET is_removed = false, removed_at = NULL WHERE is_removed = true;

-- 2. 删除索引
DROP INDEX IF EXISTS auth.ix_user_site_roles_active;
DROP INDEX IF EXISTS auth.ix_user_assistant_binding_active;

-- 3. 删除字段
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS is_removed;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS is_removed;
```

注意：回滚后需同步还原后端代码中所有 `AND is_removed = false` 过滤条件，并将 `remove_user` 恢复为 `DELETE`。

## 验证 SQL

```sql
-- 1. 确认字段存在且默认值正确
SELECT column_name, data_type, column_default
  FROM information_schema.columns
 WHERE table_schema = 'auth'
   AND table_name IN ('user_site_roles', 'user_assistant_binding')
   AND column_name IN ('is_removed', 'removed_at')
 ORDER BY table_name, column_name;

-- 2. 确认部分索引存在
SELECT indexname, indexdef
  FROM pg_indexes
 WHERE schemaname = 'auth'
   AND indexname IN ('ix_user_site_roles_active', 'ix_user_assistant_binding_active');

-- 3. 确认现有数据未被误标记（所有记录 is_removed 应为 false）
SELECT 'user_site_roles' AS tbl, COUNT(*) AS total,
       COUNT(*) FILTER (WHERE is_removed = true) AS removed
  FROM auth.user_site_roles
UNION ALL
SELECT 'user_assistant_binding', COUNT(*),
       COUNT(*) FILTER (WHERE is_removed = true)
  FROM auth.user_assistant_binding;

-- 4. 确认活跃记录查询走部分索引（EXPLAIN 检查）
EXPLAIN SELECT * FROM auth.user_site_roles WHERE user_id = 1 AND site_id = 1 AND is_removed = false;
```

## 迁移脚本
`db/zqyy_app/migrations/20260324_soft_delete_user_site_roles_and_binding.sql`
