# 变更审计记录：保底 relationship_building 任务生成

| 字段 | 值 |
|------|-----|
| 日期 | 2026-03-25 |
| Prompt | 扩大 relationship_building 任务生成范围（保底任务） |

## 操作摘要

扩大 relationship_building 任务生成范围：对每个助教，所有确切发生过服务关系（`session_count > 0`）的客户都生成一条 relationship_building 保底任务。只有满足 RS 指数范围（`1 < RS < 6`，来自 `cfg_task_generator_params`）的才展示在前端任务列表中，范围外的任务通过服务详情列表入口访问详情页。

## 风险标签

`dir:backend`、`dir:db`、`db-schema-change`

## 文件变更

### 新增文件
- `db/zqyy_app/migrations/2026-03-25__relationship_building_baseline.sql` — partial unique index

### 修改文件
- `apps/backend/app/services/fdw_queries.py` — 新增 `get_all_service_pairs()`
- `apps/backend/app/services/task_generator.py` — 新增 Step 3b + `_generate_baseline_relationship_tasks()`
- `apps/backend/app/services/task_manager.py` — `get_task_list_v2()` SQL 层面排除 RS 范围外的保底任务

## 改动注解

### 1. 迁移脚本（新建）
- 新增 partial unique index `idx_coach_tasks_rb_unique_active` on `biz.coach_tasks (site_id, assistant_id, member_id) WHERE task_type = 'relationship_building' AND status = 'active'`
- 保证每个 (site_id, assistant_id, member_id) 最多 1 条 active 的 relationship_building 任务
- 支持 upsert 的 ON CONFLICT 子句

### 2. fdw_queries.py — get_all_service_pairs()
- 查询 `app.v_dws_member_assistant_relation_index WHERE session_count > 0`
- 不限 os_label，只要确切发生过服务关系
- 返回 `[{"assistant_id", "member_id", "rs"}]`

### 3. task_generator.py — _generate_baseline_relationship_tasks()
- 在 `_run_for_site()` 的 Step 3 和 Step 4 之间插入 Step 3b
- 查询全量服务关系对 → 排除已有任何类型 active 任务的对 → 批量 upsert
- 每条失败独立 rollback + 重新 BEGIN，不影响其他记录
- 写入 coach_task_history（action="created", detail={"reason": "baseline_relationship_building"}）

### 4. task_manager.py — get_task_list_v2() 分页修复
- 原方案：内存过滤（循环中 continue 跳过 + total - filtered_count），跨页 total 不准确
- 新方案：Step 0 预查 ETL RS 排除列表 → SQL COUNT/分页查询加 NOT (task_type='relationship_building' AND member_id=ANY(exclude)) 条件
- RS 范围参数从 cfg_task_generator_params 读取（复用任务生成器配置）

## 风险评估

- 低风险：纯函数未修改，属性测试 23 passed, 3 skipped
- 中风险：_generate_baseline_relationship_tasks 循环内事务处理（部分失败时其他记录仍可提交）
- 低风险：get_task_list_v2 新增一次 ETL 查询（单助教级别，数据量小）

## 回滚策略

```sql
-- 1. 删除索引
DROP INDEX IF EXISTS biz.idx_coach_tasks_rb_unique_active;

-- 2. 清理保底任务（按时间范围）
DELETE FROM biz.coach_tasks
WHERE task_type = 'relationship_building'
  AND created_at >= '2026-03-25'::date
  AND id IN (
    SELECT h.task_id FROM biz.coach_task_history h
    WHERE h.detail->>'reason' = 'baseline_relationship_building'
  );
```

代码回滚：revert 三个 Python 文件的改动。

## 验证 SQL

```sql
-- 1. 确认索引存在
SELECT indexname, indexdef FROM pg_indexes WHERE indexname = 'idx_coach_tasks_rb_unique_active';

-- 2. 确认无重复（每对最多 1 条 active relationship_building）
SELECT site_id, assistant_id, member_id, COUNT(*)
FROM biz.coach_tasks
WHERE task_type = 'relationship_building' AND status = 'active'
GROUP BY site_id, assistant_id, member_id
HAVING COUNT(*) > 1;
-- 应返回 0 行

-- 3. 确认保底任务已生成
SELECT COUNT(*) FROM biz.coach_tasks
WHERE task_type = 'relationship_building' AND status = 'active';
```
