# 看板 & 详情页差距分析与实施指南

> 调研日期：2026-03-28
> 范围：小程序看板（助教/客户列表）、助教详情页、客户详情页
> 目的：全面梳理前后端完成度差距，为后续联调实施提供完整依据

---

## 一、总体完成度

| 模块 | 后端接口 | 前端页面 | 真实数据对接 | 完成度 | 主要差距 |
|------|---------|---------|-------------|--------|---------|
| 助教看板 BOARD-1 | ✅ | ✅ | ❌ Mock | ~60% | 前端用 Mock；skills 为空；环比字段为 None |
| 客户看板 BOARD-2 | ✅ | ✅ | ❌ Mock | ~60% | 前端用 Mock；loyal 缺 coachDetails；排序规则需修正 |
| 助教详情 COACH-1 | ✅ | ✅ | ⚠️ 部分 | ~40% | API 已调用但 Mock 覆盖大部分字段 |
| 客户详情 CUST-1 | ✅ | ✅ | ⚠️ 部分 | ~35% | API 已调用但只映射了 id/name/phone |
| 客户服务记录 CUST-2 | ✅ | ✅ | ✅ 真实 | ~90% | 已完成联调 |
| 看板 Tab 切换 | N/A | ✅ | N/A | 需重构 | 当前是页面跳转，需改为同页切换 |

---

## 二、问题清单（按优先级排列）

### P1：看板 Tab 切换改为同页切换

**现状**：三个看板页面（board-finance / board-customer / board-coach）各自独立，通过 `wx.navigateTo()` 或 `wx.switchTab()` 跳转。

**问题**：
- `board-coach.ts` 第 245 行 `onTabChange()`：`wx.navigateTo` / `wx.switchTab`
- `board-customer.ts` 第 277 行 `onTabChange()`：同上
- `board-finance.ts` 第 473 行 `onTabChange()`：同上

**目标**：改为同一页面内的 tab 切换（无页面跳转感），类似 `wx:if` 或 `hidden` 控制显示/隐藏。

**实施方案选项**：
1. **方案 A：合并为单页**：将三个看板合并到一个页面（如 `board/board`），用 `wx:if="{{activeTab === 'finance'}}"` 切换内容区。优点：切换无延迟；缺点：单页代码量大，首次加载慢。
2. **方案 B：`wx.redirectTo` 替代**：用 `wx.redirectTo` 替代 `wx.navigateTo`，避免页面栈堆积。优点：改动最小；缺点：仍有页面切换闪烁。
3. **方案 C：组件化**：将三个看板内容抽为自定义组件，在一个容器页面中按 tab 切换。优点：代码隔离好、切换流畅；缺点：需要重构。

**建议**：方案 C（组件化），兼顾代码隔离和切换体验。

**影响范围**：
- 前端：`pages/board-finance/`、`pages/board-customer/`、`pages/board-coach/` → 抽为组件
- 路由：`app.json` 页面注册需调整
- 权限守卫：`checkPageAccess` 需适配新路由
- custom-tab-bar：如果 board-finance 是 tabBar 页面，需要调整

---

### P2：助教详情页 Mock → 真实 API 映射

**现状**（`coach-detail.ts` 第 297-370 行 `loadData()`）：
- 已调用 `fetchCoachDetail(id)` 获取后端数据
- 但只映射了 `id` 和 `name`，其余全部用 `mockCoachDetail` 覆盖
- 档位节点硬编码为 `[0, 100, 130, 160, 190, 220]`（实际应为 `[0, 120, 150, 180, 210]`）

**后端 Schema**（`CoachDetailResponse`）已返回的完整字段：
```
id, name, avatar, level, skills, work_years, customer_count, hire_date,
performance { monthly_hours, monthly_salary, customer_balance, tasks_completed,
              perf_current, perf_target },
income { this_month[], last_month[] },
tier_nodes[],
visible_tasks[], hidden_tasks[], abandoned_tasks[],
top_customers[], service_records[], history_months[], notes[]
```

**前端需要映射但当前被 Mock 覆盖的字段**：

| 字段 | Mock 变量 | 说明 |
|------|----------|------|
| `tier_nodes` | 硬编码 `[0, 100, 130, 160, 190, 220]` | 档位节点，后端从 `cfg_performance_tier` 读取 |
| `visible_tasks` | `mockVisibleTasks` | 可见任务列表 |
| `hidden_tasks` | `mockHiddenTasks` | 隐藏任务列表 |
| `abandoned_tasks` | `mockAbandonedTasks` | 已放弃任务列表 |
| `top_customers` | `mockTopCustomers` | TOP 客户列表 |
| `service_records` | `mockServiceRecords` | 近期服务记录 |
| `history_months` | `mockHistoryMonths` | 历史月份数据 |
| `notes` | Mock 内嵌 | 备注列表（已部分映射） |
| `income` | Mock 内嵌 | 收入明细（本月/上月） |
| `performance` 全部子字段 | `mockCoachDetail.performance` | 绩效数据 |

**实施要点**：
1. 移除 `mockCoachDetail` 及所有 `mock*` 变量
2. 直接使用 `fetchCoachDetail(id)` 返回的完整对象
3. 注意 camelCase 转换（后端 snake_case → 前端 camelCase 自动转换）
4. `tier_nodes` 使用后端返回值，fallback 用 `_FALLBACK_TIER_NODES = [0, 120, 150, 180, 210]`
5. 数值字段传原始数字给 `setData`，WXML 中用 WXS 格式化（TS 与 WXS 格式化互斥规则）
6. null 值清洗：`?? 0` / `?? ''`（组件 property 收到 null 不走默认值）

---

### P3：客户详情页 Mock → 真实 API 映射

**现状**（`customer-detail.ts` 第 97-120 行 `loadDetail()`）：
- 已调用 `fetchCustomerDetail(id)` 获取后端数据
- 只映射了 `id`、`name`、`phone` 三个字段
- 其余模块（Banner、AI 洞察、维客线索、助教任务、最亲密助教、消费记录、备注）全部使用 `data` 中的初始空值

**后端 Schema**（`CustomerDetailResponse`）已返回的完整字段：
```
id, name, phone, phone_full, avatar, member_level, relation_index, tags[],
balance, consumption_60d, ideal_interval, days_since_visit,
ai_insight { summary, strategies[] },
coach_tasks[], favorite_coaches[], retention_clues[],
consumption_records[], notes[]
```

**前端需要映射但当前未映射的字段**：

| 字段 | 前端 data key | 说明 |
|------|-------------|------|
| `phone_full` | `detail.phone` | 完整手机号（用于复制） |
| `avatar` | 未使用 | 头像 URL |
| `member_level` | 未使用 | 会员等级 |
| `relation_index` | 未使用 | 关系指数 |
| `tags` | 未使用 | 标签数组 |
| `balance` | `detail.balance` | 卡余额 |
| `consumption_60d` | `detail.consumption60d` | 60天消费 |
| `ideal_interval` | `detail.idealInterval` | 理想到店间隔 |
| `days_since_visit` | `detail.daysSinceVisit` | 距上次到店天数 |
| `ai_insight` | `aiInsight` | AI 洞察（暂不处理） |
| `coach_tasks` | `coachTasks` | 关联助教任务 |
| `favorite_coaches` | `favoriteCoaches` | 最亲密助教 |
| `retention_clues` | `clues` | 维客线索 |
| `consumption_records` | `consumptionRecords` | 消费记录 |
| `notes` | `sortedNotes` | 备注 |

**实施要点**：
1. `loadDetail` 中将 `fetchCustomerDetail(id)` 返回的完整对象映射到 `data`
2. AI 相关字段（`ai_insight`）暂不处理，保持空值
3. Banner 四项指标（balance / consumption60d / idealInterval / daysSinceVisit）直接映射
4. 子模块（coachTasks / favoriteCoaches / clues / consumptionRecords / notes）直接映射
5. null 值清洗规则同 P2

---

### P4：助教看板 Mock → 真实 API

**现状**（`board-coach.ts` 第 195-215 行 `loadData()`）：
- 使用 `setTimeout` + `MOCK_COACHES` 模拟加载
- 未调用任何 API

**后端接口**：`GET /api/xcx/board/coaches?sort=perf_desc&skill=ALL&time=month`

**后端返回结构**：
```json
{
  "items": [
    {
      "id": 123,
      "name": "张三",
      "initial": "张",
      "avatar_gradient": "",
      "level": "senior",
      "skills": [],
      "top_customers": ["李四", "王五"],
      "perf_hours": 156.5,
      "perf_hours_before": null,
      "perf_gap": null,
      "perf_reached": false,
      "salary": 18500.0,
      "salary_perf_hours": 156.5,
      "salary_perf_before": null,
      "sv_amount": 45000.0,
      "sv_customer_count": 12,
      "sv_consume": 8500.0,
      "task_recall": 5,
      "task_callback": 8
    }
  ],
  "dim_type": "perf"
}
```

**实施要点**：
1. 替换 `setTimeout` + `MOCK_COACHES` 为真实 API 调用
2. 筛选参数（sort / skill / time）传给 API
3. 筛选变更时重新请求（当前只在前端过滤 Mock 数据）
4. 注意 `skills` 字段当前为空数组（见 P6）
5. `perf_hours_before` / `salary_perf_before` 当前为 null（见 P7）

---

### P5：客户看板 Mock → 真实 API

**现状**（`board-customer.ts` 第 218-235 行 `loadData()`）：
- 使用 `setTimeout` + `MOCK_CUSTOMERS` 模拟加载
- 未调用任何 API

**后端接口**：`GET /api/xcx/board/customers?dimension=recall&project=ALL&page=1&page_size=20`

**实施要点**：
1. 替换 Mock 为真实 API 调用
2. 维度切换时重新请求（不同维度调用不同 FDW 查询函数）
3. 项目筛选传给 API
4. 分页支持（page / page_size）
5. 注意前端字段名与后端返回的映射（camelCase 自动转换）

**客户看板前端字段 → 后端字段映射**：

| 前端字段 | 后端字段 | 维度 |
|---------|---------|------|
| `idealDays` | `ideal_days` | recall |
| `elapsedDays` | `elapsed_days` | recall |
| `overdueDays` | `overdue_days` | recall |
| `visits30d` | `visits_30d` | recall |
| `balance` | `balance` | recall / balance |
| `recallIndex` | `recall_index` | recall |
| `spend30d` | `spend_30d` | potential |
| `avgVisits` | `avg_visits` | potential |
| `avgSpend` | `avg_spend` | potential |
| `lastVisit` | `last_visit` | balance / recent |
| `monthlyConsume` | `monthly_consume` | balance |
| `availableMonths` | `available_months` | balance |
| `lastRecharge` | `last_recharge` | recharge |
| `rechargeAmount` | `recharge_amount` | recharge |
| `recharges60d` | `recharges_60d` | recharge |
| `currentBalance` | `current_balance` | recharge |
| `spend60d` | `spend_60d` | spend60 |
| `visits60d` | `visits_60d` | spend60 / freq60 |
| `avgInterval` | `avg_interval_days` | freq60 |
| `weeklyVisits` | `weekly_visits` | freq60 |
| `intimacy` | `intimacy` | loyal |
| `topCoachName` | `top_coach_name` | loyal |
| `topCoachHeart` | `top_coach_heart` | loyal |
| `coachDetails` | ❌ 后端未返回 | loyal |
| `daysAgo` | `days_ago` | recent |
| `assistants` | `assistants` | 所有维度 |

---

### P6：助教 Skills 字段为空

**现状**：
- 后端 `get_coach_board()` 返回 `"skills": []`（第 316 行注释：`v_dim_assistant 无 skill 列，暂返回空`）
- 后端 `get_coach_detail()` 同样返回空 skills
- 前端 WXML 已有 skills 标签渲染逻辑

**调研发现——"技能"的两层含义**：

1. **课程类型（skill_id）**：指助教能教的课程类型
   - 基础课（陪打/PD）：`skill_id = 2791903611396869`
   - 附加课（超休/CX）：`skill_id = 2807440316432197`
   - 包厢课：归入基础课口径
   - 配置表：`cfg_skill_type`（`skill_id → course_type_code: BASE/BONUS/ROOM`）
   - 这不是看板需要展示的"技能"

2. **项目类型（area_category）**：指助教擅长的运动项目
   - BILLIARD（🎱 中式/追分）、SNOOKER（斯诺克）、MAHJONG（🀄 麻将/棋牌）、KTV（🎤 团建/K歌）
   - 配置表：`cfg_area_category`（台桌 → 区域 → 项目类型映射）
   - 视图：`app.v_cfg_area_category`（去重到 category 级别）
   - **这才是看板需要展示的"技能标签"**

**看板中 skills 的业务含义**：
- 前端 `SKILL_CLASS` 映射：`'🎱' → 'skill--chinese'`、`'斯' → 'skill--snooker'`、`'🀄' → 'skill--mahjong'`、`'🎤' → 'skill--karaoke'`
- 即：助教擅长哪些项目类型（台球/斯诺克/麻将/KTV）

**数据来源方案**：
- 从 `dwd_assistant_service_log` 按助教聚合历史服务的 `area_category_code`，取去重后的项目类型列表
- SQL 示例：
  ```sql
  SELECT DISTINCT asl.area_category_code
  FROM dwd.dwd_assistant_service_log asl
  WHERE asl.assistant_id = %s
    AND asl.is_delete = 0
    AND asl.area_category_code IS NOT NULL
  ```
- 或者：在 `v_dim_assistant` 视图中新增 `skills` 列（从服务记录聚合）

**实施建议**：
1. 在 `fdw_queries` 中新增 `get_assistant_skills_batch(conn, site_id, assistant_ids)` 函数
2. 从 `v_dwd_assistant_service_log` 按助教聚合 `area_category_code`
3. 映射为前端需要的格式：`["BILLIARD", "SNOOKER"]`
4. 在 `get_coach_board()` 和 `get_coach_detail()` 中调用并填充 `skills` 字段

---

### P7：助教看板"折前"字段（perf_hours_before / salary_perf_before）

**现状**：
- 后端 `get_coach_board()` 返回 `perf_hours_before: None`、`salary_perf_before: None`
- 前端 WXML 已有条件渲染：`wx:if="{{item.perfHoursBeforeLabel}}"` 显示"折前 XX.Xh"

**业务含义**：
- `perf_hours` = 折算后的定档业绩课时（effective_hours）
- `perf_hours_before` = 折算前的原始课时（base_hours + bonus_hours + room_hours）
- 折算规则：同一台桌同一时段 >2 名助教重叠挂台时，计算 `per_hour_contribution = base_ledger_amount / base_hours / overlap_count`，若 < 24 元/小时则按比例扣减 `penalty_minutes`
- 豁免条件：`is_exempt = true`（客人真实需求，前台核实并绑定客人信息）
- 只要 `effective_hours != raw_hours` 就显示折前课时，与新入职无关

**注意**：这不是"环比"，而是"折算前 vs 折算后"的对比。前端 WXML 中的 `perfHoursBeforeLabel` 显示的是"折前 XX.Xh"，不是"上期 XX.Xh"。

**数据来源**：
- `dws_assistant_salary_calc` 表中应有 `raw_hours`（折算前）和 `effective_hours`（折算后）
- 需确认 DWS 表是否已有 `raw_hours` 字段
- 如果没有，需要在 DWS 任务中补充计算

---

### P8：客户看板 loyal 维度缺少 coachDetails 子数组

**现状**：
- 前端 WXML 已有完整的助教服务明细表渲染（`board-customer.wxml` 第 254-277 行）
- 表头：助教 | 次均时长 | 服务次数 | 助教消费 | 关系指数
- 数据绑定：`wx:for="{{item.coachDetails}}"`
- 但后端 `get_customer_board_loyal()` 只返回了 `top_assistant_id` 和 `top_coach_name`，没有返回 `coach_details` 数组

**用户明确的排列规则**：
- 客户-助教对，RSI 从高到低排列
- 客户不重复，排重规则是放在最高对的位置
- 每个卡片展示客户信息 + 该客户对应所有助教 RSI 值从高到低排列
- 右上位置展示最高 RSI 值助教

**当前后端实现**（`fdw_queries.py` 第 2272-2340 行）：
```sql
WITH member_top AS (
    SELECT ri.member_id,
           MAX(ri.rs_display) AS max_rs,
           (ARRAY_AGG(ri.assistant_id ORDER BY ri.rs_display DESC))[1] AS top_assistant_id,
           (ARRAY_AGG(ri.rs_display ORDER BY ri.rs_display DESC))[1] AS top_rs
    FROM app.v_dws_member_assistant_relation_index ri
    GROUP BY ri.member_id
    ORDER BY MAX(ri.rs_display) DESC
    LIMIT %s OFFSET %s
)
```
- ✅ 排序正确：按 `max_rs DESC`（最高 RSI 降序）
- ✅ 客户不重复：`GROUP BY ri.member_id`
- ✅ 右上角最高 RSI 助教：`top_assistant_id` + `top_coach_name`
- ❌ 缺少：每个客户对应的所有助教明细

**需要补充的后端逻辑**：
在 `get_customer_board_loyal()` 返回 items 后，批量查询每个客户的所有助教 RSI 明细：

```sql
SELECT ri.member_id,
       ri.assistant_id,
       COALESCE(da.real_name, da.nickname, '') AS name,
       ri.rs_display,
       ri.service_count,
       ri.total_hours,
       ri.total_income
FROM app.v_dws_member_assistant_relation_index ri
LEFT JOIN app.v_dim_assistant da
    ON ri.assistant_id = da.assistant_id AND da.scd2_is_current = 1
WHERE ri.member_id = ANY(%s)
ORDER BY ri.member_id, ri.rs_display DESC
```

**前端 coachDetails 字段结构**：
```typescript
interface CoachDetail {
  name: string          // 助教姓名
  cls: string           // 样式类
  heartScore: number    // RSI 值（0-10）
  badge?: string        // "跟" / "弃"
  avgDuration: string   // 次均时长
  serviceCount: string  // 服务次数
  coachSpend: string    // 助教消费金额
  relationIdx: number   // 关系指数
}
```

**计算规则**：
- `avgDuration` = `total_hours / service_count`（次均时长）
- `serviceCount` = `service_count`（服务次数）
- `coachSpend` = `total_income`（助教消费金额，即该客户在该助教处的消费）
- `relationIdx` = `rs_display`（RSI 值）
- `heartScore` = `rs_display`（用于 heart-icon 组件）
- `badge`：跟/弃标记来源待确认（可能来自 `coach_tasks` 表的任务状态）

---

### P9：客户看板项目筛选枚举不一致

**现状**：
- `board-customer.ts` 的 `PROJECT_OPTIONS` 使用旧枚举值：
  ```typescript
  { value: 'all', text: '全部' },
  { value: 'chinese', text: '🎱 中式/追分' },
  { value: 'snooker', text: '斯诺克' },
  { value: 'mahjong', text: '🀄 麻将/棋牌' },
  { value: 'karaoke', text: '🎤 团建/K歌' },
  ```
- `board-coach.ts` 的 `SKILL_OPTIONS` 已修正为数据库枚举值（2026-03-20 修复）：
  ```typescript
  { value: 'ALL', text: '不限' },
  { value: 'BILLIARD', text: '🎱 中式/追分' },
  { value: 'SNOOKER', text: '斯诺克' },
  { value: 'MAHJONG', text: '🀄 麻将/棋牌' },
  { value: 'KTV', text: '🎤 团建/K歌' },
  ```

**问题**：客户看板的 `project` 参数值（`all/chinese/snooker/mahjong/karaoke`）与后端枚举（`ALL/BILLIARD/SNOOKER/MAHJONG/KTV`）不一致，API 调用会失败。

**修复**：将 `PROJECT_OPTIONS` 的 value 改为 `ALL/BILLIARD/SNOOKER/MAHJONG/KTV`。

---

## 三、环比字段分布

根据调研，环比字段的分布如下：

### 3.1 财务看板（BOARD-3）— 已实现环比

后端 `get_finance_board()` 接受 `compare` 参数（0/1），当 `compare=1` 时计算环比。

环比字段分布（`xcx_board.py` Schema）：
- `OverviewPanel`：8 项核心指标各有 `*_compare` / `*_down` / `*_flat` 三元组
- `RechargePanel`：储值卡各项 + 全类别余额合计
- `RevenuePanel`：总发生额 / 优惠总计 / 确认收入
- `CashflowPanel`：充值合计
- `ExpensePanel`：支出合计
- `CoachAnalysisPanel`：pay / share / hourly 各有环比

前端 WXML 使用 `fmt.compareText()` + `fmt.compareClass()` WXS 函数渲染。

### 3.2 助教看板（BOARD-1）— 非环比字段

`perf_hours_before` / `salary_perf_before` 不是环比，而是"折算前课时"：
- 前端 WXML 显示为"折前 XX.Xh"
- 只有新入职助教才有折算值
- 数据来源：DWS `dws_assistant_salary_calc` 的 `raw_hours` 字段

### 3.3 客户看板（BOARD-2）— 无环比

当前无环比字段设计。

### 3.4 助教详情页（COACH-1）— 无环比

当前无环比字段设计。

### 3.5 客户详情页（CUST-1）— 无环比

当前无环比字段设计。

---

## 四、统计规则详解

### 4.1 助教看板四维度

#### 定档业绩维度（perf）
- 排序字段：`effective_hours`（折算后工时）
- 升序/降序：`perf_desc` / `perf_asc`
- 卡片展示：定档课时、折前课时（新入职才有）、距升档差距、是否达标
- 数据来源：`dws_assistant_salary_calc.effective_hours`

#### 工资维度（salary）
- 排序字段：`gross_salary`
- 计算公式：`assistant_pd_money_total + assistant_cx_money_total + bonus_money + room_income`
- 卡片展示：工资金额、定档课时、折前课时
- 数据来源：`dws_assistant_salary_calc`

#### 客源储值维度（sv）
- 排序字段：`sv_amount`（客户余额合计）
- 卡片展示：储值金额、储值客户数、储值消耗
- 数据来源：`fdw_queries.get_coach_sv_data()`
- 互斥规则：`time=last_6m` 时不支持此维度（HTTP 400）

#### 任务维度（task）
- 排序字段：`task_total`（recall + callback）
- 卡片展示：回访完成数、召回完成数
- 数据来源：业务库 `biz.coach_tasks` 按 `task_type` 分类统计

### 4.2 客户看板八维度

#### 最应召回（recall）
- 排序：自定义排序（综合理想间隔、已过天数、余额等因素）
- 展示：理想间隔天数、已过天数、逾期天数、30天到店次数、余额、召回指数
- 数据来源：`v_dws_member_consumption_summary` + `v_dws_member_winback_index`

#### 最大消费潜力（potential）
- 排序：自定义评分
- 展示：潜力标签、30天消费、平均到店频率、平均客单价
- 数据来源：`v_dws_member_consumption_summary`

#### 最高余额（balance）
- 排序：`total_card_balance DESC`（卡余额快照值）
- 展示：最后到店日期、月消费、可用月数
- 数据来源：`v_dim_member_card_account`（快照值，取最后一天）
- ⚠️ 余额是快照值，禁止 SUM

#### 最近充值（recharge）
- 排序：`recharge_amount DESC`
- 展示：最后充值日期、充值金额、60天充值次数、当前余额
- 数据来源：`v_dws_member_consumption_summary`

#### 最近到店（recent）
- 排序：`last_visit_date DESC`
- 展示：距今天数、60天到店次数
- 数据来源：`v_dws_member_consumption_summary`

#### 最高消费 近60天（spend60）
- 排序：`consume_amount_60d DESC`
- 展示：60天消费金额、60天到店次数、高消费标签
- 数据来源：`v_dws_member_consumption_summary`

#### 最频繁 近60天（freq60）
- 排序：`visit_count_60d DESC`
- 展示：平均间隔天数、8周柱状图
- 数据来源：`v_dws_member_consumption_summary`（汇总）+ `v_dwd_assistant_service_log`（周粒度）

**8 周柱状图统计规则**（已实现，`_get_weekly_visits_batch()`）：
- 时间范围：最近 56 天（8 个自然周）
- 分组：按 ISO 周（`DATE_TRUNC('week', create_time::date)`）
- 每周统计到店次数（`COUNT(*)`）
- `val`：该周到店次数
- `pct`：相对于 8 周中最高值的百分比（`val / max_val * 100`）
- 固定返回 8 个元素，无数据的周 `val=0, pct=0`

#### 最专一 近60天（loyal）
- 排序：`max_rs DESC`（最高 RSI 降序）
- 展示：最高 RSI 助教（右上角）、助教服务明细表
- 数据来源：`v_dws_member_assistant_relation_index`

**排列规则**（用户明确）：
1. 客户-助教对，RSI 从高到低排列
2. 客户不重复，排重规则是放在最高对的位置
3. 每个卡片展示客户信息 + 该客户对应所有助教 RSI 值从高到低排列
4. 右上位置展示最高 RSI 值助教

**RSI（关系指数）**：
- 存储：`dws_member_assistant_relation_index` 表
- 展示值：`rs_display`（0-10 刻度）
- Emoji 四级映射：`>8.5→💖` / `>7→🧡` / `>5→💛` / `≤5→💙`
- 计算：由 DWS 层 `DWS_RELATION_INDEX` 任务产出（RS/OS/MS/ML 四个子指数）

### 4.3 薪酬计算规则（DWS 需求文档 3.2 节）

**现行方案（2026-03-01 起）**：

| 档位 | 总业绩小时数阈值 | 专业课抽成（元/h） | 打赏课抽成 | 次月休假 |
|------|-----------------|-------------------|-----------|---------|
| 0档 淘汰压力 | H < 120 | 28 | 50% | 3天 |
| 1档 及格档 | 120 ≤ H < 150 | 18 | 40% | 4天 |
| 2档 良好档 | 150 ≤ H < 180 | 13 | 35% | 5天 |
| 3档 优秀档 | 180 ≤ H < 210 | 10 | 30% | 6天 |
| 4档 销冠竞争 | H ≥ 210 | 8 | 25% | 休假自由 |

- 过档后所有时长按新档位计算
- 新入职助教：按日均 × 30 折算定档（25日后入职最高定档至 2档）
- 折算仅用于定档，不适用于 Top3 奖
- 档位节点：`[0, 120, 150, 180, 210]`（从 `cfg_performance_tier` 配置表读取）

---

## 五、数据层依赖

### 5.1 DWS 表

| 表名 | 用途 | 使用页面 |
|------|------|---------|
| `dws_assistant_salary_calc` | 助教绩效/工资 | BOARD-1、COACH-1 |
| `dws_member_consumption_summary` | 客户消费汇总 | BOARD-2（6个维度） |
| `dws_member_assistant_relation_index` | 客户-助教关系指数 | BOARD-2（loyal）、CUST-1 |
| `dws_member_winback_index` | 客户召回指数 | BOARD-2（recall） |

### 5.2 DWD 表

| 表名 | 用途 | 使用页面 |
|------|------|---------|
| `dwd_assistant_service_log` | 服务记录明细 | BOARD-2（freq60 周粒度）、COACH-1 |
| `dim_member` | 会员维度表 | 所有页面（JOIN 取姓名） |
| `dim_assistant` | 助教维度表 | 所有页面（JOIN 取姓名） |
| `dim_member_card_account` | 会员卡账户 | BOARD-2（balance）、CUST-1 |

### 5.3 配置表

| 表名 | 用途 |
|------|------|
| `cfg_performance_tier` | 绩效档位节点 |
| `cfg_bonus_rules` | 奖金规则 |
| `cfg_skill_type` | 课程类型映射（skill_id → BASE/BONUS/ROOM） |
| `cfg_area_category` | 区域-项目类型映射（BILLIARD/SNOOKER/MAHJONG/KTV） |

### 5.4 业务库表

| 表名 | 用途 | 使用页面 |
|------|------|---------|
| `biz.coach_tasks` | 助教任务 | BOARD-1（任务维度）、COACH-1、CUST-1 |
| `biz.notes` | 备注 | COACH-1、CUST-1 |
| `biz.ai_cache` | AI 洞察缓存 | CUST-1（暂不处理） |
| `public.member_retention_clue` | 维客线索 | CUST-1 |

---

## 六、实施顺序建议

```
Phase 1 — 基础联调（前端 Mock → 真实 API）
  ├── P9: 修复客户看板项目筛选枚举（5 分钟）
  ├── P4: 助教看板 Mock → 真实 API
  ├── P5: 客户看板 Mock → 真实 API
  ├── P2: 助教详情页 Mock → 真实 API
  └── P3: 客户详情页 Mock → 真实 API（AI 相关暂跳过）

Phase 2 — 数据补全
  ├── P6: 助教 skills 字段填充
  ├── P7: 助教看板折前课时字段
  └── P8: 客户看板 loyal 维度 coachDetails 子数组

Phase 3 — 交互优化
  └── P1: 看板 Tab 切换改为同页切换
```

---

## 七、风险点

1. **TS 与 WXS 格式化互斥**：替换 Mock 时，`setData` 必须传原始数字，禁止用 `formatMoney()` 预格式化
2. **null 值清洗**：后端返回 null 的字段，前端必须 `?? 0` / `?? ''` 清洗后再传给组件
3. **Pydantic 静默丢弃**：后端 service 新增返回字段时，必须同步更新 Schema，否则数据被静默丢弃
4. **余额快照值**：`balance` 是日末快照，禁止 SUM 聚合
5. **档位节点**：前端硬编码的 `[0, 100, 130, 160, 190, 220]` 与实际 `[0, 120, 150, 180, 210]` 不一致
6. **看板 Tab 重构**：如果 board-finance 是 tabBar 页面，合并后需要调整 `app.json` 和 custom-tab-bar
7. **助教姓名**：所有助教必须显示昵称/花名（`nickname`），禁止显示真实姓名（`real_name`）。SQL 统一用 `COALESCE(nickname, real_name, '')`
