# customer-service-records 页面数据来源排查

> 排查日期：2026-03-18
> 页面路径：pages/customer-service-records/customer-service-records

## 概览

| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 5 | 来自 `mock-data.ts` 的 `mockCustomers` + `mockCustomerDetail` |
| B. 硬编码数据 | 8 | 页面 data 初始值、辅助函数内联常量 |
| C. 已对接 API | 0 | 全部数据均为 Mock，无真实 API 调用 |
| D. 前端计算/派生 | 12 | 月份筛选、统计汇总、格式转换等 |
| E. 路由参数 | 1 | `customerId` / `id` |
| F. WXML 硬编码文案 | 10 | 状态提示、标签文字、按钮文案等 |

---

## 一、Mock 数据（来自 mock-data.ts）

所有业务数据均通过 `loadData()` 中的 `setTimeout` 模拟异步获取，数据源为 `mock-data.ts`。

| # | 字段 / 数据 | Mock 来源 | 说明 |
|---|------------|-----------|------|
| A1 | `mockCustomers` 列表 | `mock-data.ts → mockCustomers` | 用于按 `id` 查找客户基本信息（`id`, `name`） |
| A2 | `mockCustomerDetail` | `mock-data.ts → mockCustomerDetail` | 客户详情，提供 `name` 和 `consumptionRecords` |
| A3 | `consumptionRecords` 数组 | `mockCustomerDetail.consumptionRecords` | 5 条消费记录，字段：`id`, `date`, `project`, `duration`, `amount`, `coachName` |
| A4 | `customerName` / `customerInitial` | 从 `mockCustomerDetail.name` 或 `mockCustomers[].name` 取值 | 客户姓名及首字 |
| A5 | `allRecords` | `detail.consumptionRecords` 经 `sortByTimestamp` 排序 | 全量消费记录（降序） |

### Mock 数据结构（ConsumptionRecord）

```typescript
interface ConsumptionRecord {
  id: string        // 如 'cr-001'
  date: string      // 如 '2026-03-05'
  project: string   // 如 '中式台球 1v1'、'会员充值'
  duration: number  // 分钟数，如 90
  amount: number    // 金额，如 380
  coachName: string // 助教名，如 '王芳'
}
```

---

## 二、硬编码数据

| # | 字段 / 数据 | 位置 | 硬编码值 | 说明 |
|---|------------|------|----------|------|
| B1 | `customerPhone` | `data` 初始值 | `'139****5678'` | 脱敏手机号，应从 API 获取 |
| B2 | `customerPhoneFull` | `data` 初始值 | `'13900005678'` | 完整手机号，应从 API 获取 |
| B3 | `relationIndex` | `data` 初始值 | `'0.85'` | 关系指数，应从 API 获取 |
| B4 | `monthRelation` | `data` 初始值 | `'0.85'` | 月度关系指数，应从 API 获取（WXML 中绑定展示） |
| B5 | `minYearMonth` | `data` 初始值 | `202601` | 数据起始年月，应从 API 返回的最早记录推算 |
| B6 | `maxYearMonth` | `data` 初始值 | `202602` | 数据截止年月，应取当前月份动态计算 |
| B7 | `currentYear` / `currentMonth` | `data` 初始值 | `2026` / `2` | 当前年月，应取系统时间动态计算 |
| B8 | `tables` 数组 | `getTableNo()` 方法 | `['A12号台', '3号台', 'VIP1号房', '5号台', 'VIP2号房', '8号台']` | 模拟台号，应从消费记录 API 返回 |

---

## 三、已对接 API

| # | 接口 | 状态 |
|---|------|------|
| — | — | **无。页面当前 0 个真实 API 调用。** |

`loadData()` 方法中有明确 TODO 注释：
```typescript
// TODO: 替换为真实 API 调用
```

---

## 四、前端计算/派生数据

| # | 字段 / 数据 | 计算逻辑 | 来源依赖 |
|---|------------|----------|----------|
| D1 | `customerInitial` | `name[0] \|\| '?'` | Mock → `detail.name` |
| D2 | `totalServiceCount` | `allRecords.length` | Mock → `consumptionRecords` |
| D3 | `monthLabel` | `` `${currentYear}年${currentMonth}月` `` | 硬编码 `currentYear` / `currentMonth` |
| D4 | `records`（当月记录） | `allRecords.filter(r => r.date.startsWith(monthPrefix))` 后 `.map()` 转换 | Mock → `allRecords` + 硬编码年月 |
| D5 | `monthCount` | `monthRecords.length + '次'` | D4 筛选结果 |
| D6 | `monthHours` | `(totalMinutes / 60).toFixed(1) + 'h'`，`totalMinutes = reduce(sum + r.duration)` | D4 筛选结果 |
| D7 | `canPrev` / `canNext` | `yearMonth > minYearMonth` / `yearMonth < maxYearMonth` | 硬编码边界 |
| D8 | `pageState` | 根据 `records.length === 0 && allRecords.length === 0` 判断 `'empty'` / `'normal'` | D4 + A5 |
| D9 | `ServiceRecord.table` | `getTableNo(r.id)` — 按 id 数字取模从硬编码数组选取 | 硬编码 B8 |
| D10 | `ServiceRecord.type` / `typeClass` | `getTypeLabel(r.project)` / `getTypeClass(r.project)` — 按 `project` 字符串 `includes` 匹配 | Mock → `project` 字段 |
| D11 | `ServiceRecord.duration` | `parseFloat((r.duration / 60).toFixed(1))`（充值记录为 0） | Mock → `duration`（分钟） |
| D12 | `ServiceRecord.date`（展示用） | 格式化为 `"X月X日 HH:MM - HH:MM"`，时间段由 `generateTimeRange()` 随机生成 | Mock → `date` + 随机数 |

### 辅助函数详情

| 函数 | 逻辑 | 数据来源 |
|------|------|----------|
| `generateTimeRange(durationMin)` | 随机起始小时 14-19，计算结束时间 | 入参 `duration`（Mock） + `Math.random()` |
| `getTypeLabel(project)` | `includes('小组')→'小组课'`、`includes('1v1')→'基础课'`、`includes('充值')→'充值'`、`includes('斯诺克')→'斯诺克'`、默认 `'基础课'` | 硬编码映射规则 |
| `getTypeClass(project)` | `includes('充值')→'recharge'`、`includes('小组'/'斯诺克')→'vip'`、默认 `'basic'` | 硬编码映射规则 |
| `getTableNo(id)` | `id` 提取数字 → 取模 → 从 6 元素数组选取 | 硬编码台号数组 |
| `sortByTimestamp(list, field)` | 按指定字段降序排序（`utils/sort.ts`） | 纯工具函数 |

### 固定为默认值的 ServiceRecord 字段

| 字段 | 固定值 | 说明 |
|------|--------|------|
| `durationRaw` | `0` | 折算前小时数，未实现 |
| `isEstimate` | `false` | 是否预估金额，未实现 |
| `drinks` | `''` | 商品/饮品描述，未实现 |
| `income` | `r.amount`（直接透传） | 到手金额 = 消费金额，未做提成计算 |
| `recordType` | `project.includes('充值') ? 'recharge' : 'course'` | 仅按项目名判断 |

---

## 五、路由参数

| # | 参数 | 取值逻辑 | 说明 |
|---|------|----------|------|
| E1 | `customerId` | `options?.customerId \|\| options?.id \|\| ''` | 从上级页面传入，支持两种参数名 |

---

## 六、WXML 硬编码文案

| # | 文案 | 位置 | 说明 |
|---|------|------|------|
| F1 | `"加载中..."` | 加载态 toast | 状态提示 |
| F2 | `"暂无服务记录"` | `<t-empty>` description | 空态提示 |
| F3 | `"加载失败，请点击重试"` | 错误态文案 | 错误提示 |
| F4 | `"重试"` | 错误态按钮 | 按钮文案 |
| F5 | `"服务 {{totalServiceCount}} 次"` | Banner 徽章 | 模板 + 动态数据 |
| F6 | `"查看"` / `"复制"` | 手机号操作按钮 | 根据 `phoneVisible` 切换 |
| F7 | `"本月服务"` / `"服务时长"` / `"关系指数"` | 月度统计标签 | 固定标签文案 |
| F8 | `"本月暂无服务记录"` | 当月无数据提示 | 空月提示 |
| F9 | `"— 已加载全部记录 —"` | 列表底部 | 底部提示 |
| F10 | `"手机号码已复制"` | `onCopyPhone()` Toast | JS 中的提示文案 |

---

## 七、引用组件

| 组件 | 路径 | 用途 |
|------|------|------|
| `service-record-card` | `/components/service-record-card/` | 服务记录卡片，接收 `ServiceRecord` 各字段 |
| `ai-float-button` | `/components/ai-float-button/` | AI 悬浮按钮，传入 `customerId` |
| `dev-fab` | `/components/dev-fab/` | 开发调试浮动按钮 |
| `t-loading` | TDesign | 加载动画 |
| `t-icon` | TDesign | 图标 |
| `t-empty` | TDesign | 空态组件 |

---

## 八、联调 TODO

### 需要对接的 API 清单

| 优先级 | API | 用途 | 替换目标 |
|--------|-----|------|----------|
| P0 | `GET /api/customers/{id}/service-records` | 获取客户服务记录列表（支持月份筛选、分页） | `loadData()` 中的 `mockCustomerDetail.consumptionRecords` |
| P0 | `GET /api/customers/{id}` | 获取客户基本信息（姓名、手机号、关系指数） | `mockCustomers.find()` + `mockCustomerDetail` + 硬编码手机号 |
| P1 | `GET /api/customers/{id}/monthly-stats` | 获取月度统计（服务次数、时长、关系指数） | 前端 `reduce` 计算的 `monthCount`、`monthHours` + 硬编码 `monthRelation` |

### 联调时需处理的改动点

| # | 改动点 | 当前状态 | 联调要求 |
|---|--------|----------|----------|
| T1 | 移除 `mock-data.ts` 导入 | `import { mockCustomerDetail, mockCustomers }` | 替换为 API 请求模块 |
| T2 | `loadData()` 改为真实请求 | `setTimeout` + Mock 查找 | `wx.request` 或封装的 HTTP 客户端 |
| T3 | 手机号从 API 获取 | 硬编码 `'139****5678'` / `'13900005678'` | API 返回脱敏 + 完整手机号（或按需请求完整号） |
| T4 | 关系指数从 API 获取 | 硬编码 `'0.85'` | API 返回 `relationIndex` / `monthRelation` |
| T5 | 台桌号从 API 获取 | `getTableNo()` 硬编码数组取模 | 消费记录 API 返回 `tableNo` 字段 |
| T6 | 时间段从 API 获取 | `generateTimeRange()` 随机生成 | 消费记录 API 返回 `startTime` / `endTime` |
| T7 | 年月边界动态计算 | 硬编码 `minYearMonth=202601` / `maxYearMonth=202602` | 从 API 返回的最早记录推算 `min`，`max` 取当前月 |
| T8 | `currentYear` / `currentMonth` 动态化 | 硬编码 `2026` / `2` | 取 `new Date()` 当前年月 |
| T9 | 分页加载 | `onReachBottom` 空实现 | 对接分页 API（`page` / `pageSize` 参数） |
| T10 | `durationRaw` 字段 | 固定 `0` | API 返回折算前时长 |
| T11 | `drinks` 字段 | 固定 `''` | API 返回商品/饮品信息 |
| T12 | `isEstimate` 字段 | 固定 `false` | API 返回是否预估标记 |
| T13 | `income` 字段 | 直接透传 `amount` | API 返回助教到手金额（需提成计算） |
| T14 | 错误处理 | 无 `pageState='error'` 触发路径 | API 失败时 `setData({ pageState: 'error' })` |
