# 看板全面排查修复计划

> 排查日期：2026-04-07
> 范围：客户看板（BOARD-2）8 维度 + 助教看板（BOARD-1）4 维度
> 状态：待实施（新对话执行）

---

## 一、已完成的修复

| # | 问题 | 修复内容 | 文件 |
|---|------|----------|------|
| ✅ | SPI 消费口径用 `pay_amount` 而非 `items_sum` | 3 处 settlement_head 查询改为 `items_sum`（DWD 规则 #1） | `apps/etl/connectors/feiqiu/tasks/dws/index/spending_power_index_task.py` |
| ✅ | 潜力标签阈值 60 但数据范围 0-10 | 阈值 60→6，返回 `[{text, theme}]` 对象数组 | `apps/backend/app/services/fdw_queries.py` `_derive_potential_tags` |

---

## 二、待实施修复（共 10 项）

### P1 — 功能性 Bug

#### Fix-1：客户看板项目筛选崩溃（C-1）

- **问题**：`_project_filter_clause()` 硬编码 `vd.member_id`，但 6/8 维度主表别名不是 `vd`，选择项目筛选时后端 SQL 500
- **影响维度**：recall / balance / recharge / spend60 / freq60 / loyal
- **不受影响**：recent（别名 `vd`）、potential（自写子查询）
- **修复方案**：统一改为独立子查询模式 `member_id IN (SELECT member_id FROM app.v_dws_member_project_tag WHERE category_code = %s AND is_tagged = true)`，不依赖外层别名（参考 potential 维度已有写法）
- **文件**：`apps/backend/app/services/fdw_queries.py` — `_project_filter_clause()` 函数及 6 个维度查询函数

#### Fix-2：助教看板 task 维度 callback 映射错误（A-3）

- **问题**：callback 统计映射到 `relationship_building`，但业务上"回访"应对应 `follow_up_visit`
- **修复**：`board_service.py` `_query_coach_tasks()` 中 callback 的 task_type 从 `relationship_building` 改为 `follow_up_visit`
- **文件**：`apps/backend/app/services/board_service.py`

### P2 — 数据口径错误

#### Fix-3：recall 维度 visits_30d 实为 14d（C-2）

- **问题**：`dws_member_winback_index` 无 `visits_30d` 字段，后端用 `visits_14d` 近似，前端显示"30天到店"
- **修复**：
  1. DDL：`dws.dws_member_winback_index` 加列 `visits_30d INTEGER DEFAULT 0`
  2. ETL：WBI 计算任务增加 30 天到店次数统计
  3. RLS 视图：`dws.v_dws_member_winback_index` 和 `app.v_dws_member_winback_index` 加新列
  4. 后端：查询改用新字段
  5. DDL 文档同步更新
- **文件**：
  - `db/etl_feiqiu/schemas/dws.sql`（DDL）
  - `apps/etl/connectors/feiqiu/tasks/dws/index/winback_index_task.py`（ETL）
  - `db/etl_feiqiu/schemas/app.sql`（RLS 视图）
  - `apps/backend/app/services/fdw_queries.py`（查询）
  - `docs/database/ddl/etl_feiqiu__dws.sql` / `etl_feiqiu__app.sql`（DDL 文档）

#### Fix-4：balance 维度月均消耗和可用月数偏差 2 倍（C-3 + C-4）

- **问题**：
  - `monthlyConsume`：直接用 `consume_amount_60d`（60天总额），标签"月均消耗"应为 `consume_amount_60d / 2`
  - `availableMonths`：`balance / consume_amount_60d`，应为 `balance / (consume_amount_60d / 2)` 即 `2 * balance / consume_amount_60d`
- **示例**：余额 49780 / 60天消费 14521 → 当前显示 3.4 个月，实际应 6.9 个月
- **文件**：`apps/backend/app/services/fdw_queries.py` — `get_customer_board_balance()`

#### Fix-5：freq60 柱状图数据源不一致（C-7）

- **问题**：汇总数据来自 `v_dws_member_consumption_summary`（消费维度），但 8 周柱状图来自 `v_dwd_assistant_service_log`（助教服务维度），口径不一致
- **修复**：柱状图改为从消费汇总或结算维度获取周数据，与汇总口径一致
- **文件**：`apps/backend/app/services/fdw_queries.py` — freq60 相关查询

#### Fix-6：助教看板 sv 维度不响应时间筛选（A-4）

- **问题**：函数接收 `start_date/end_date` 但 SQL 未使用，"消耗"始终是固定 60 天
- **修复**：`sv_consume` 的查询加入 `start_date/end_date` 过滤，使其随时间筛选联动
- **文件**：`apps/backend/app/services/fdw_queries.py` — `get_coach_sv_data()`

### P3 — 标签/文案修正

#### Fix-7：loyal 维度标签"近60天"→"近90天"

- **问题**：ETL 关系指数实际使用 90 天窗口（`lookback_days: 90`），非 60 天
- **修复**：下拉选项文本 `最专一 近60天` → `最专一 近90天`
- **文件**：`apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts` — `DIMENSION_OPTIONS`

#### Fix-8：potential 维度"月均到店"→"近30天到店"

- **问题**：实际是 30 天到店天数（同一天多次只算一天），非"月均到店次数"
- **位置**：board-customer 页面 → 最大消费潜力 tab → 4 列网格第 2 格
- **文件**：`apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml`

#### Fix-9：recharge 维度"充值"→"累计充值"

- **问题**：实际是 `SUM(pay_amount)` 历史累计充值总额，标签仅"充值"易误解为单次
- **位置**：board-customer 页面 → 最近充值 tab → 4 列网格第 2 格
- **文件**：`apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml`

#### Fix-10：freq60 维度标签确认

- 此维度标签"最频繁 近60天"与实际口径一致（`visit_count_60d`），无需修改

---

## 三、DWD 合规检查结果（通过）

| 检查项 | 客户看板 | 助教看板 |
|--------|----------|----------|
| consume_money 禁止直接使用 | ✅ 全部走 DWS items_sum | ✅ |
| settle_type IN (1,3) | ✅ 不直查结算表 | ✅ |
| DQ-6 会员姓名通过 dim_member | ✅ | ✅ |
| DQ-7 会员卡通过 dim_member_card_account | ✅ | ✅ |
| 助教费用拆分 pd/cx | N/A | ✅ |

---

## 四、已知但不修的项

| 项 | 原因 |
|----|------|
| A-2：sv 维度客户重复计算 | 业务定义确认：每个助教看"我的客户总余额"，允许跨助教重叠，作为催促消耗的参考值 |
| A-5：后端 `dim_type` 冗余 | 功能无影响，两端映射一致 |

---

## 五、recall_detector 修复 + 任务统计需求（同期实施）

> 需求确认日期：2026-04-07

### 背景

`biz.coach_tasks` 当前 255 条记录，completed = 0。排查确认：
- 系统完成逻辑设计正确（`recall_detector.py` 匹配服务记录→标记 completed）
- 但两个技术问题导致完成检测不可靠
- 缺少"助教主动标记完成"能力
- 任务完成统计需要三个维度

### 当前架构（调研结果）

| 服务 | 文件 | 职责 | 调度方式 |
|------|------|------|----------|
| `recall_detector` | `app/services/recall_detector.py` | 检测客户到店→标记召回任务完成→生成回访任务 | event: `etl_data_updated`（当前无人触发） |
| `task_generator` | `app/services/task_generator.py` | 根据 WBI/NCI/RS 指数生成召回/关系维护任务 | cron: `0 4 * * *`（每日凌晨 4 点） |

生成规则（四级漏斗）：
- `max(WBI, NCI) > 7` → `high_priority_recall`
- `max(WBI, NCI) > 5` → `priority_recall`
- `1 < RS < 6` → `relationship_building`
- 不满足 → 不生成；`session_count > 0` 无任务时保底补充 `relationship_building`

回溯逻辑（已有）：`recall_detector` 检测到新服务记录时，如匹配到活跃的 `follow_up_visit`，关闭旧任务（`superseded_by_new_visit`）并创建新回访。

### Fix-11：recall_detector 增量时序 Bug + 编排顺序

**问题**：`recall_detector` 用 `create_time > last_run_at` 全局增量指针。任务创建与客户到店时序不一致时漏匹配。

**示例时间线**：
1. 4/5 08:00 — `recall_detector` 运行，`last_run_at = 08:00`
2. 4/5 14:00 — 客户张三到店，服务记录 `create_time = 14:00`
3. 4/6 02:00 — `task_generator` 为张三创建召回任务
4. 4/6 08:00 — `recall_detector` 再次运行，但 `last_run_at` 已更新，张三 4/5 的记录在上轮已扫描过（当时无任务）→ 永远不会重新匹配

**修复方案**：
1. 废弃 `last_run_at` 增量指针，改为：对所有活跃任务，检查 `dwd_settlement_head` 中是否有 `pay_time > task.created_at` 的结算记录（`settle_type IN (1,3)`），匹配 `(site_id, assistant_id, member_id)`
2. 合并运行顺序：ETL 完成后，统一编排器按顺序执行：
   ```
   ETL 完成 → HTTP callback → 后端编排器:
     Step 1: recall_detector.run()    # 先检查完成（含回溯）
     Step 2: task_generator.run()     # 再生成新任务
   ```
3. 保留 `task_generator` 的每日 cron（`0 4 * * *`）作为兜底
4. 两个服务保持独立文件，仅在调度层串联

**文件**：`apps/backend/app/services/recall_detector.py`、调度编排器

### Fix-12：ETL 完成后自动触发（HTTP callback）

**问题**：`etl_data_updated` 事件无调用方，recall_detector 仅靠手动触发。

**业务影响**：ETL 每小时同步完新的飞球数据后，系统不会自动检查召回完成情况。客户回店后，助教看不到任务完成，必须有人手动在管理后台触发。

**修复方案**：ETL `api_full` pipeline 完成后，通过 HTTP callback 通知后端。

实现：
1. 后端新增 API：`POST /api/internal/etl-completed`（内部接口，仅限本机调用）
2. 该接口触发统一编排：`recall_detector.run()` → `task_generator.run()`
3. ETL orchestrator 在 pipeline 完成后调用此接口
4. 安全：校验来源 IP 或 shared secret

**文件**：
- `apps/backend/app/routers/internal.py`（新增）
- `apps/etl/connectors/feiqiu/orchestration/` — pipeline 完成回调
- `apps/backend/app/services/trigger_scheduler.py` — 编排逻辑

### Fix-13：任务完成统计三维度

**需求确认**：任务完成分三个统计维度：

| 统计维度 | 触发条件 | 是否需已有任务 | 判定数据源 |
|----------|----------|---------------|-----------|
| 召回完成（广义） | 关联客户来店 + 有结算单 | 不需要，只要是 `dws_member_assistant_relation_index` 中的关联客户 | `dwd_settlement_head`（`settle_type IN (1,3)`） |
| 优先/高优先召回完成 | 已有 `priority_recall` / `high_priority_recall` 任务 + 客户到店 | 需要 | 同上 + `coach_tasks.status = 'completed'` |
| 回访完成 | 完成 `follow_up_visit` 任务（含回溯完成） | 需要 | `coach_tasks` + 回溯检测 |

关键定义：
- **"关联客户"**：`dws_member_assistant_relation_index` 中有关系记录的客户
- **"来店"判定**：`dwd_settlement_head` 中有结算单（`settle_type IN (1,3)`）
- **时间窗口**：不限间隔，只要来了就算一次
- **统计时间范围**：全量（任务系统上线至今）

完成类型标记（两种都要记录）：

| 完成类型 | 触发方式 | 当前状态 |
|----------|----------|----------|
| 自动完成 | 客户到店，系统匹配结算记录 | 逻辑存在但有 Bug（Fix-11 + Fix-12） |
| 手动完成 | 助教在小程序中主动标记 | 当前不存在，需新建 |

手动完成需要：
- 小程序端：任务卡片增加"标记完成"按钮
- 后端 API：`POST /api/xcx/tasks/{task_id}/complete`，body: `{type: "manual", note?: string}`
- 数据库：`coach_tasks` 增加 `completion_type` 字段（`auto` / `manual`）
- `coach_task_history` 增加 `action='manual_completed'`

### Fix-14：助教看板 task 维度展示

**展示位置**：微信小程序 board-coach 页面 task_desc 排序维度

**统计口径**（按助教/按月）：
- 召回完成数：`coach_task_history` 中 `action='completed'` 的任务数（优先+高优先）
- 回访完成数：`follow_up_visit` 类型的完成数（含自动+手动+回溯）
- 广义召回（关联客户来店）：从 `dwd_settlement_head` + 关系表统计

**数据来源**：`biz.coach_tasks` + `biz.coach_task_history` + ETL 关系/结算表
**时间范围**：全量
**后续**：更详细的趋势报表作为独立需求

### 实施依赖关系

```
Fix-11（时序Bug+编排顺序）──┐
                              ├→ Fix-14（看板展示）
Fix-12（ETL 自动触发）────────┤
                              │
Fix-13（手动完成+三维度统计）─┘
```

建议实施顺序：Fix-11 → Fix-12 → Fix-13 → Fix-14
