# 小程序多页面联调改造任务书

> 创建日期：2026-03-27
> 来源：本轮对话排查 customer-service-records 页面加载问题后，用户提出的 4 页面改造需求
> 状态：第 1 步后端改造已完成，第 2-4 步待执行

---

## 一、背景与前提

### 1.1 已完成的工作（本轮对话）

1. **权限守卫修复**：`auth-guard.ts` 的 `PAGE_ROLES` 中 `customer-detail`、`customer-service-records`、`coach-detail` 三个页面加入了 `coach` 角色，解决了 coach 进入后被 `reLaunch` 踢回首页的问题。

2. **ETL 连接复用**：`fdw_queries.py` 的 `_fdw_context` 新增 `etl_conn` 可选参数，`customer_service.py` 的 `get_customer_records` 中只创建一个 ETL 连接传给所有子查询，连接开销从 ~10s 降到 ~2.6s。

3. **后端 assistant_id 过滤**：
   - `customer_service.py` 新增 `_get_assistant_id()` 从 `auth.user_assistant_binding` 获取 assistant_id
   - `get_customer_records` 签名新增 `user_id` 参数，内部获取 assistant_id 后传给所有 fdw_queries 调用
   - `fdw_queries.get_customer_service_records` 和 `get_total_service_count` 加入可选 `assistant_id` 过滤（`site_assistant_id = %s`）
   - `_get_month_aggregation` 同步加入 `assistant_id` 过滤
   - 路由层 `xcx_customers.py` 传入 `user.user_id`

4. **后端到手收入计算**：
   - `get_customer_records` 中查询 `v_dws_assistant_salary_calc` 获取当月费率
   - 基础课到手 = hours × (base_course_price - base_deduction)
   - 激励课到手 = hours × bonus_course_price × (1 - bonus_deduction_ratio)
   - 充值提成到手 = ledger_amount（直接取）
   - Schema 新增 `month_income: float` 字段

5. **BACKLOG 更新**：`docs/roadmap/BACKLOG.md` 新增 AI BudgetTracker 和 admin_db_health UnicodeDecodeError 两条待办。

6. **联调规范更新**：`docs/guides/FRONTEND-BACKEND-INTEGRATION.md` 和 `.kiro/steering/frontend-backend-integration.md` 新增 ETL 连接复用规则。

### 1.2 已识别但未修复的数据问题

通过开发者工具读取页面 data 发现以下问题：

| # | 问题 | 原因 | 影响 |
|---|------|------|------|
| 1 | `customerName` 为空 | `loadCustomerInfo` 调的 `get_customer_detail` 还没做 ETL 连接复用优化，可能超时 | 顶部不显示客户名 |
| 2 | `totalServiceCount: 0` | 后端 `get_total_service_count` 返回 0，需排查 assistant_id 是否正确传入 | 顶部"服务X次"显示 0 |
| 3 | `monthHours: "0.0h"` | 后端返回 `duration` 是小时数（如 1.1），前端 `loadMonthRecords` 中 `rawRecords.reduce` 累加的是 `r.duration`，但后端字段名是 `duration`（CamelModel 输出 camelCase），需确认字段映射 | 月度时长显示 0 |
| 4 | `income: 0` | 后端到手收入计算中 `salary_calc` 可能没查到数据（assistant_id 为 None 或费率为 0） | 列表收入全部显示 0 |
| 5 | `displayDate` 格式错误 | 前端 `generateTimeRange` 把 `r.duration`（小时数）当分钟数处理，导致 `19:1.116...` | 时间显示乱码 |
| 6 | `table` 显示 ID | 后端返回 `table_id`（数字 ID），前端直接显示，应该显示台桌名称 | 台桌号显示为长数字 |
| 7 | `durationHours: 0` | 前端 `(r.duration \|\| 0) / 60` 把小时数再除以 60 变成了接近 0 的值 | 列表时长显示 0 |

### 1.3 开发环境

- Windows + PowerShell
- 后端：`start-admin.bat` 启动（uvicorn --reload 端口 8000）
- 前端：微信开发者工具（自动化端口 9420）
- 数据库：ETL 库（PG_DSN）+ 业务库（APP_DB_DSN），ETL 连接耗时 ~2.6s
- 可用调试工具：weixin-devtools MCP power（连接 `ws://127.0.0.1:9420`）

### 1.4 关键文件位置

| 文件 | 说明 |
|------|------|
| `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` | 前端页面逻辑 |
| `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.wxml` | 前端页面模板 |
| `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.wxss` | 前端页面样式 |
| `apps/miniprogram/miniprogram/services/api.ts` | 前端 API 调用层 |
| `apps/miniprogram/miniprogram/utils/request.ts` | 前端请求封装（含 ResponseWrapper 解包） |
| `apps/miniprogram/miniprogram/utils/auth-guard.ts` | 权限守卫 |
| `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts` | 任务详情页（参考：60天服务记录列表格式） |
| `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml` | 任务详情页模板（参考：service-record-card） |
| `apps/miniprogram/miniprogram/pages/performance/performance.ts` | 绩效页 |
| `apps/miniprogram/miniprogram/pages/performance/performance.wxml` | 绩效页模板 |
| `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` | 任务列表页 |
| `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` | 任务列表页模板 |
| `apps/backend/app/services/customer_service.py` | 后端客户服务 |
| `apps/backend/app/services/fdw_queries.py` | 后端 FDW 查询 |
| `apps/backend/app/routers/xcx_customers.py` | 后端客户路由 |
| `apps/backend/app/schemas/xcx_customers.py` | 后端响应 Schema |
| `apps/backend/app/routers/xcx_auth.py` | 后端认证路由（/api/xcx/me） |
| `apps/backend/app/auth/dependencies.py` | JWT 解析（CurrentUser） |

### 1.5 关键数据口径

- **到手收入**：基础课 = hours × (base_course_price - base_deduction)；激励课 = hours × bonus_course_price × (1 - bonus_deduction_ratio)；充值提成 = ledger_amount
- **费率来源**：`v_dws_assistant_salary_calc`，按 `assistant_id + salary_month` 查询
- **DWD 层助教字段**：`site_assistant_id`（与 `auth.user_assistant_binding.assistant_id` 相同）
- **废单排除**：`is_delete = 0`
- **会员信息**：通过 `member_id` JOIN `v_dim_member (scd2_is_current=1)`
- **CamelModel**：后端 Pydantic schema 自动将 snake_case 转 camelCase 输出
- **ResponseWrapper**：后端中间件将 2xx JSON 包装为 `{ code: 0, data: <原始body> }`，前端 `request.ts` 自动解包

---

## 二、待执行任务

### 任务 A：customer-service-records 前端改造（优先级最高）

#### A1. 修复数据转换逻辑

前端 `loadMonthRecords` 中的字段映射需要对齐后端 CamelModel 输出：

```
后端返回（camelCase）    前端当前读取        问题
─────────────────────────────────────────────────
duration (float, 小时)   r.duration / 60     多除了一次 60
durationRaw (float, 小时) 未使用              应直接用
income (float, 到手元)   r.amount            字段名不匹配
timeRange (string|null)  未使用              后端已返回，前端用 generateTimeRange 自己算
courseType (string)       r.courseType        ✓ 正确
table (string|null)      r.table             后端返回 table_id 数字，需要改为台桌名
recordType (string)      前端自己判断         后端已返回，应直接用
isEstimate (bool)        r.isEstimate        ✓ 正确
drinks (string|null)     r.drinks            后端当前返回 null，后续需补齐
```

修改要点：
- `durationHours` 直接用 `r.duration`（已是小时数），不再 `/60`
- `durationRaw` 直接用 `r.durationRaw`
- `income` 用 `r.income`（后端已计算到手）
- `displayDate` 优先用后端返回的 `r.timeRange`，无值时才用 `generateTimeRange`
- `generateTimeRange` 的参数是小时数不是分钟数，需修正或移除
- `table` 字段：后端当前返回 `table_id`（数字），需要后端改为返回台桌名称，或前端做映射

#### A2. 修复月度统计

- `monthHours` 累加逻辑：`rawRecords.reduce((sum, r) => sum + (r.duration || 0), 0)` — 后端返回的 `duration` 已是小时数，不需要再 `/60`
- 新增 `monthIncome`：从后端返回的 `monthIncome` 字段读取并展示

#### A3. 修复顶部信息

- `customerName`：`loadCustomerInfo` 调的 `fetchCustomerDetail` 超时问题 — 方案 1：给 `get_customer_detail` 也做 ETL 连接复用；方案 2：从 `get_customer_records` 的返回值中直接取 `customerName`（后端已返回）
- `totalServiceCount`：从 `get_customer_records` 返回值中取（后端已返回 `totalServiceCount`）
- 手机号查看/复制交互：对齐 task-detail 页面的实现
- 首字母头像：使用通用颜色规则（`nameToAvatarColor`）

#### A4. 列表记录格式对齐 task-detail

- 对齐 task-detail 的"60天内服务记录"列表的 `service-record-card` 组件格式
- 确认 `service-record-card` 组件是否已存在，还是内联在 task-detail 的 WXML 中
- 到手收入显示：`¥XXX`
- 课程类型标签：基础课/激励课/充值
- 折前/折后时长：`durationRaw !== durationHours` 时才显示折前

#### A5. 加速加载

- `loadCustomerInfo` 可以去掉（改为从 `loadMonthRecords` 的返回值中取 `customerName` 和 `customerPhone`）
- 减少一个 API 请求 = 减少一次 ETL 连接开销

---

### 任务 B：task-detail 折前时长条件显示

文件：`apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`

- 找到服务记录列表中显示"折前X.Xh"的位置
- 加条件判断：`wx:if="{{item.durationRaw && item.durationRaw !== item.durationHours}}"`
- 折前和折后相等时隐藏折前标签

---

### 任务 C：performance 页面改造

文件：`apps/miniprogram/miniprogram/pages/performance/performance.ts` + `.wxml`

#### C1. 未知客户点击拦截

- 找到客户卡片点击跳转逻辑
- 判断 `memberId <= 0`（散客/未知客户）时，`wx.showToast({ title: '未知客户不提供查看客户任务详情', icon: 'none' })` 并 return
- 非散客正常跳转

#### C2. 角色标签对齐 task-list

- 查看 task-list 页面的角色标签展示方式（WXML + WXSS）
- 对齐 performance 页面顶部的角色标签样式

---

### 任务 D：头像修复（performance + task-list）

文件：`performance.ts`、`task-list.ts`、对应 `.wxml`

- 后端 `/api/xcx/me` 已返回 `avatarUrl` 字段（apply 时上传的头像）
- 前端 `fetchMe()` 返回的 `data.avatarUrl` 需要绑定到页面 data
- WXML 中头像 `<image>` 的 `src` 应优先用 `avatarUrl`，无值时 fallback 到默认占位图或首字母头像
- 确认 task-list 和 performance 两个页面的 banner 区域头像都使用此逻辑

---

### 任务 E：后端补充优化（可选）

#### E1. get_customer_detail ETL 连接复用

- 与 `get_customer_records` 相同的模式：创建一个 ETL 连接传给所有 fdw_queries 调用
- 涉及的子查询：`get_member_info`、`get_member_balance`、`get_consumption_60d`、`get_last_visit_days`、`_build_consumption_records`、`_build_coach_tasks`、`_build_favorite_coaches`

#### E2. 台桌名称返回

- 当前 `get_customer_service_records` 返回 `site_table_id`（数字 ID）
- 需要 JOIN `v_dim_table`（或类似维度表）获取台桌名称
- 或者前端做映射（不推荐，增加前端复杂度）

#### E3. 到手收入排查

- 确认 `_get_assistant_id` 返回值是否正确（可能 `user_assistant_binding` 中没有对应记录）
- 确认 `get_salary_calc` 是否能查到当月数据（可能该月还没有 salary_calc 记录）
- 费率为 0 时的兜底处理

---

## 三、执行顺序建议

1. **E3**（排查到手收入为 0 的原因）→ 确认后端数据正确性
2. **A1 + A2 + A3**（前端数据转换修复）→ 页面能正确展示数据
3. **A4**（列表格式对齐 task-detail）→ UI 一致性
4. **A5**（加速加载）→ 去掉多余 API 请求
5. **B**（task-detail 折前时长）→ 小改动
6. **C**（performance 页面）→ 独立改动
7. **D**（头像修复）→ 独立改动
8. **E1 + E2**（后端补充优化）→ 可选

---

## 四、验证方式

每步完成后：
1. 微信开发者工具编译（Ctrl+B）
2. 操作路径：任务列表 → 轩哥 → 任务详情 → 查看全部服务记录
3. 用 weixin-devtools MCP power 的 `evaluate_script` 读取页面 data 验证字段值
4. 用 `list_console_messages` 检查是否有错误
5. 后端改动通过 uvicorn `--reload` 自动加载，看后端窗口有无报错

---

## 五、参考文档

- `docs/guides/FRONTEND-BACKEND-INTEGRATION.md` — 联调规范（字段命名、到手收入口径、ETL 连接复用等）
- `.kiro/steering/frontend-backend-integration.md` — 联调规范摘要
- `.kiro/steering/feiqiu-data-rules.md` — 飞球数据规范索引
- `apps/backend/app/schemas/xcx_customers.py` — 后端响应 Schema（CamelModel 自动转 camelCase）
- `apps/miniprogram/miniprogram/utils/request.ts` — 前端请求封装（ResponseWrapper 解包逻辑）
