179 lines
9.8 KiB
Markdown
179 lines
9.8 KiB
Markdown
# 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' })` |
|