# 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 | `"暂无服务记录"` | `` 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' })` |