Files
Neo-ZQYY/docs/prd/specs/board-detail-gap-analysis.md
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

619 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 看板 & 详情页差距分析与实施指南
> 调研日期2026-03-28
> 范围:小程序看板(助教/客户列表)、助教详情页、客户详情页
> 目的:全面梳理前后端完成度差距,为后续联调实施提供完整依据
---
## 一、总体完成度
| 模块 | 后端接口 | 前端页面 | 真实数据对接 | 完成度 | 主要差距 |
|------|---------|---------|-------------|--------|---------|
| 助教看板 BOARD-1 | ✅ | ✅ | ❌ Mock | ~60% | 前端用 Mockskills 为空;环比字段为 None |
| 客户看板 BOARD-2 | ✅ | ✅ | ❌ Mock | ~60% | 前端用 Mockloyal 缺 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-26个维度 |
| `dws_member_assistant_relation_index` | 客户-助教关系指数 | BOARD-2loyal、CUST-1 |
| `dws_member_winback_index` | 客户召回指数 | BOARD-2recall |
### 5.2 DWD 表
| 表名 | 用途 | 使用页面 |
|------|------|---------|
| `dwd_assistant_service_log` | 服务记录明细 | BOARD-2freq60 周粒度、COACH-1 |
| `dim_member` | 会员维度表 | 所有页面JOIN 取姓名) |
| `dim_assistant` | 助教维度表 | 所有页面JOIN 取姓名) |
| `dim_member_card_account` | 会员卡账户 | BOARD-2balance、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 → 真实 APIAI 相关暂跳过)
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, '')`