feat: batch update - gift card breakdown spec, backend APIs, miniprogram pages, ETL finance recharge, docs & migrations

This commit is contained in:
Neo
2026-03-20 01:43:48 +08:00
parent 075caf067f
commit 79f9a0e1da
437 changed files with 118603 additions and 976 deletions

View File

@@ -2,6 +2,7 @@
> 本文档记录项目中所有文档资产的位置、类型和内容概要,方便快速定位。
> 归档规则见末尾「文档归档规则」章节;程序输出路径规范见 `docs/deployment/EXPORT-PATHS.md`。
> 最后更新2026-03-19RNS1.3 三看板接口文档补充)
---
@@ -53,8 +54,9 @@
| `audit_dashboard.md` | 审计仪表盘,汇总所有变更审计记录 |
| `README.md` | 审计目录说明 |
| `SESSION-LOG-GUIDE.md` | Session 日志使用指南:索引字段说明、查询方法、典型场景、与其他审计产物的关系 |
| `changes/` | 39 份变更审计文档(`YYYY-MM-DD__<slug>.md` 格式),每份包含:变更原因、影响范围、回滚策略、验证 SQL |
| `prompt_logs/` | ~500 份 Prompt 日志(`prompt_log_YYYYMMDD_HHMMSS.md`),记录每次 AI 交互的输入输出 |
| `AUDIT-HOOKS-GUIDE.md` | 审计 Hooks 使用指南Hook 触发机制、配置方式、与审计流程的集成 |
| `changes/` | 56 份变更审计文档(`YYYY-MM-DD__<slug>.md` 格式),每份包含:变更原因、影响范围、回滚策略、验证 SQL |
| `prompt_logs/` | Prompt 日志(`prompt_log_YYYYMMDD_HHMMSS.md`),记录每次 AI 交互的输入输出 |
| `session_logs/` | 全量会话记录(按 `YYYY-MM/DD/` 分层),含双索引(`_session_index.json` / `_session_index_full.json`)、每轮 execution 的完整 Markdown 记录、LLM 生成的操作摘要 |
### 2.4 数据契约 `docs/contracts/`
@@ -70,19 +72,26 @@
| 文件 | 内容 |
|------|------|
| `LAUNCH-CHECKLIST.md` | 上线检查清单:环境配置、数据库迁移、服务启动、验证步骤 |
| `launch-checklist-patch.md` | 上线检查清单补丁 |
| `EXPORT-PATHS.md` | 输出路径规范:环境变量映射表、目录结构、新增场景检查清单 |
| `PRIVACY-POLICY.md` | 隐私政策文档 |
| `wx-api-security-guide.md` | 微信 API 安全指南 |
| `wx-encrypt-guide.md` | 微信加密指南 |
### 2.6 产品需求 `docs/prd/`
| 路径 | 内容 |
|------|------|
| `小程序前后端.txt` | 小程序前后端原始需求描述 |
| `后端接口需求说明_数据需求PRD.md` | 后端接口需求说明与数据需求 PRD |
| `PRD审阅-Q&A.md` | PRD 审阅问答记录(第一轮) |
| `PRD审阅-Q&A-R2.md` | PRD 审阅问答记录(第二轮) |
| `SPI 消费力指数.md` | 消费力指数SPI算法需求说明 |
| `ai-app-prompts.md` | AI 应用 Prompt 设计 |
| `AI需求2.md` | AI 需求第二版 |
| `specs/00-数据依赖矩阵.md` | 各 SPEC 间的数据依赖关系矩阵 |
| `specs/01-SPEC任务拆分总览.md` | 11 个 SPEC 的任务拆分总览 |
| `specs/P1~P11` | 11 份 SPEC 拆分文档,覆盖:数据库基础(P1)、ETL DWS 扩展(P2)、认证系统(P3)、核心业务(P4)、AI 集成(P5)、前端任务/绩效/看板/详情(P6-P9)、租户管理后台(P10)、部署上线(P11) |
| `specs/P1~P11` | 15 份 SPEC 拆分文档,覆盖:数据库基础(P1)、ETL DWS 扩展(P2)、认证系统(P3)、核心业务(P4)、AI 集成(P5/P5.1/P5.2)、前端任务/绩效/看板/详情(P6-P9)、租户管理后台(P10)、部署上线(P11) |
### 2.7 小程序 UI 原型 `docs/h5_ui/`
@@ -91,40 +100,32 @@ H5 静态原型页面,用于小程序 UI 设计参考。
| 路径 | 内容 |
|------|------|
| `index.html` | 原型首页入口 |
| `pages/` | 23 个页面原型,包括:登录(`login`)、申请(`apply`)、审核中(`reviewing`)、无权限(`no-permission`)、任务列表/详情(`task-list`/`task-detail`)、绩效(`performance`/`performance-records`)、助教详情(`coach-detail`)、客户详情(`customer-detail`)、客户服务记录(`customer-service-records`)、看板(`board-coach`/`board-customer`/`board-finance`)、聊天(`chat`/`chat-history`)、个人中心(`my-profile`)、首页设置(`home-settings`)、笔记(`notes`)、AI 图标演示(`ai-icon-demo`) |
| `pages/` | 23+ 个页面原型,包括:登录(`login`)、申请(`apply`)、审核中(`reviewing`)、无权限(`no-permission`)、任务列表/详情(`task-list`/`task-detail`/`task-detail-callback`/`task-detail-priority`/`task-detail-relationship`)、绩效(`performance`/`performance-records`)、助教详情(`coach-detail`)、客户详情(`customer-detail`)、客户服务记录(`customer-service-records`)、看板(`board-coach`/`board-customer`/`board-finance`)、聊天(`chat`/`chat-history`)、个人中心(`my-profile`)、笔记(`notes`) |
| `pages/_baked/` | 已烘焙(固化)的页面快照 |
| `css/` | 6 个样式文件 |
| `js/` | 8 个交互脚本 |
| `img/` | 图片资源 |
| `tools/` | H5 转小程序检查工具(`h5-to-mp-checker/` |
### 2.8 小程序前端开发指南 `docs/miniprogram-dev/`
微信小程序前端页面开发与 H5 原型迁移的统一文档中心。覆盖批量自动迁移、用户指定半自动开发、新页面从零开发三种模式。
微信小程序前端页面开发与 H5 原型迁移的统一文档中心。
| 路径 | 内容 |
|------|------|
| `README.md` | 入口索引:快速导航、目录结构、Power 依赖、关联资源 |
| `README.md` | 入口索引:快速导航、目录结构、关联资源 |
| `QUICK-REFERENCE.md` | 快速参考手册 |
| `API-contract.md` | API 契约文档 |
| `API-OUTPUT-SPEC.md` | 后端接口输出规范DS-API-OUTPUT-001字段类型约定 / 枚举值 / 各页面接口字段对照表 |
| `API-requirement.md` | API 需求文档 |
| `design-system/VI-DESIGN-SYSTEM.md` | VI 设计系统:任务/客户/等级配色、AI 图标、CSS 变量速查 |
| `design-system/DATETIME-DISPLAY-STANDARD.md` | 日期时间展示规范:由近及远 6 级展示规则、边界处理、JS 实现参考 |
| `design-system/DISPLAY-STANDARDS.md` | 前端展示规范第1-6章金额 / 课时 / 计数 / 空值 / 百分比 / 等级文案 |
| `design-system/DISPLAY-STANDARDS-2.md` | 前端展示规范第7-9章截止日期 / 评分星级 / Mock 数据规范 + 工具函数总表 |
| `API-OUTPUT-SPEC.md` | 后端接口输出规范DS-API-OUTPUT-001字段类型约定 / 枚举值 / 各页面接口字段对照表 |
| `01-orchestration/batch-auto-playbook.md` | 批量自动模式89 单元编排 + 4 代理流水线 |
| `01-orchestration/user-guided-playbook.md` | 半自动模式:对话触发 + 按需执行 |
| `01-orchestration/new-page-playbook.md` | 新页面开发:有原型 / 无原型两条路径 |
| `02-action/screenshot-agent.md` | 截图代理H5/MP 双端截图执行手册 |
| `02-action/audit-agent.md` | 审计代理:结构化审计执行手册 |
| `02-action/fix-agent.md` | 修正代理P0-P7 分级修正执行手册 |
| `02-action/verify-agent.md` | 验证代理:回归校验执行手册 |
| `02-action/page-dev-agent.md` | 页面开发代理:新页面完整开发执行手册 |
| `03-reference/wxss-rules.md` | WXSS 规范 + rpx 换算 + 颜色/字号/圆角标准值 |
| `03-reference/page-structure-map.md` | 各页面特殊结构速查 |
| `03-reference/css-risk-features.md` | CSS 风险特性 + 替代方案 |
| `03-reference/power-integration.md` | Power 集成指南4 个 Power 的调用方式) |
| `03-reference/benchmark-history.md` | 基准测试历史记录 |
| `04-audit/PROGRESS.md` | 89 单元进度跟踪(实时更新) |
| `04-audit/CHANGELOG.md` | 文档体系版本变更记录 |
| `05-lessons/pitfalls.md` | 踩坑速查 + 页面迁移经验 |
| `05-lessons/convergence-patterns.md` | 收敛模式 + 不可消除差异白名单 |
| `design-system/vi-guide.html` | VI 设计系统可视化指南HTML |
| `api-audit/` | 19 个页面的 API 审计文档(每页一份 `.md`),含硬编码汇总(`_hardcode-summary.md` |
| `h5-migration/` | H5 转小程序迁移指南:桥接文档(`h5-to-mp-bridge.md`)、附录(`appendix/` |
| `API-requirement/` | API 需求子目录(预留) |
### 2.9 参考资料 `docs/reference/`
@@ -137,10 +138,15 @@ H5 静态原型页面,用于小程序 UI 设计参考。
一次性数据分析、调研产出的报告文档。与 `prd/specs/` 的区别specs 是需求规格reports 是基于数据的分析结论。
| 文件 | 内容 |
| 路径 | 内容 |
|------|------|
| `complex-orders-analysis.md` | 复杂订单结构分析ODS 全量扫描,关联键与复杂度定义) |
| `dwd-amount-duration-calibration.md` | DWD 层金额·绩效·时长字段口径全景(置信度与存疑项标注) |
| `DWD-DOC/` | **权威标杆文档**(业务模型与财务数据权威数据源):消费链路全景(01)、支付渠道与对账公式(02)、收入构成与储值卡资金流(03)、维度表全景(04)、F2 收支平衡公式(05)、校准清单(06)、GAP 闭环状态(07)、consume_money 口径时间线(`consume/`) |
| `business-analysis/` | 业务分析报告:每日营收报告(`daily-revenue-latest.md`)、高价值充值客户分析(`high-value-recharge-customer-analysis.md`) |
| `etl-calibration/` | ETL 校准报告DWS BD 手册校准(2026-03-07)、DWS 代码校准(2026-03-07)、财务看板 DWS 审计(2026-03-07) |
| `h5-mp-conversion/` | H5 转小程序报告:财务看板审计(`board-finance-h5-mp-audit.md`)、H5 UI 提取(`h5-ui-extraction.md`) |
| `p4-task/` | P4 任务报告Spec 与实现差距分析、任务生命周期全景 |
| `tech-solution/` | 技术方案:百炼技术方案(`bailian-technical-solution.md`) |
| `vi-color-audit/` | VI 配色审计:详细审计(Phase2)、完成报告、实施文档、合规审计 |
### 2.11 架构文档 `docs/architecture/`
@@ -148,20 +154,26 @@ H5 静态原型页面,用于小程序 UI 设计参考。
|------|------|
| `etl-feiqiu-architecture.md` | ETL Connector 整体架构说明数据流、DWS/INDEX 任务、调度编排、CLI |
### 2.12 其他项目级文档目录
### 2.12 MCP 文档 `docs/mcp/`
| 文件 | 内容 |
|------|------|
| `AI-DATABASE-QUERY-MANUAL.md` | AI 数据库查询手册 |
| `WEIXIN-DEVTOOLS-MCP.md` | 微信开发者工具 MCP 集成文档 |
### 2.13 其他项目级文档目录
| 路径 | 内容 |
|------|------|
| `docs/architecture/` | 架构文档(预留,待填充) |
| `docs/roadmap/BACKLOG.md` | 项目待办事项 |
| `docs/roadmap/2026-02-24__fdw-dwd-to-core-migration-plan.md` | FDW + DWD→Core 迁移计划 |
| `docs/migrate/monorepo-migration-summary.md` | Monorepo 迁移总结 |
| `docs/migrate/oldworkspace-kiro-agent-config-summary.md` | 旧工作区 Kiro 配置迁移记录 |
| `docs/ops/` | 运维文档(预留,待填充) |
| `docs/ops/init-test-user.md` | 测试用户初始化指南 |
| `docs/permission_matrix/` | 权限矩阵(预留,待填充) |
| `docs/vi-standards/` | VI 设计标准(预留,待填充) |
| `docs/spec-input/2026-02-22__etl-aggregation-fix-spec-input.md` | ETL 聚合修复的 Spec 输入文档 |
---
## 三、模块内部文档
@@ -173,6 +185,38 @@ H5 静态原型页面,用于小程序 UI 设计参考。
| `README.md` | 架构概览、双库连接、认证系统、17 个路由模块摘要、服务层、配置加载、触发器系统 |
| `docs/API-REFERENCE.md` | 完整 API 参考17 个路由模块的所有端点、请求/响应示例、认证要求、错误码 |
RNS1.2 新增模块(客户与助教接口):
| 路径 | 内容 |
|------|------|
| `app/routers/xcx_customers.py` | 客户端点CUST-1 客户详情、CUST-2 客户服务记录 |
| `app/routers/xcx_coaches.py` | 助教端点COACH-1 助教详情 |
| `app/services/customer_service.py` | 客户查询服务Banner 概览、AI 洞察、消费记录嵌套、coachTasks、favoriteCoaches |
| `app/services/coach_service.py` | 助教查询服务绩效、收入、任务分组、TOP 客户、历史月份 |
| `app/schemas/xcx_customers.py` | 客户相关 Pydantic SchemaCustomerDetailResponse、CustomerRecordsResponse 等) |
| `app/schemas/xcx_coaches.py` | 助教相关 Pydantic SchemaCoachDetailResponse 等) |
| `tests/unit/test_degradation_rns12.py` | RNS1.2 优雅降级单元测试 |
| `tests/unit/test_auth_rns12.py` | RNS1.2 权限校验单元测试 |
| `tests/integration/test_e2e_customer_coach.py` | CUST-1/CUST-2/COACH-1 端到端集成测试 |
RNS1.3 新增模块(三看板接口):
| 路径 | 内容 |
|------|------|
| `app/routers/xcx_board.py` | 看板端点BOARD-1 助教看板、BOARD-2 客户看板、BOARD-3 财务看板 |
| `app/routers/xcx_config.py` | 配置端点CONFIG-1 技能类型 |
| `app/services/board_service.py` | 看板编排服务:日期范围/环比/排序/分页/降级 |
| `app/schemas/xcx_board.py` | 看板相关 Pydantic Schema7 枚举 + ~40 响应 Schema |
| `app/schemas/xcx_config.py` | 配置相关 Pydantic SchemaSkillTypeItem |
Monorepo 级属性测试(`tests/`
| 路径 | 内容 |
|------|------|
| `tests/test_rns12_properties.py` | RNS1.2 属性测试14 个 PropertyHypothesis 框架) |
| `tests/test_board_properties.py` | RNS1.3 属性测试18 个测试函数12 个 PropertyHypothesis 框架) |
| `tests/test_board_service_unit.py` | RNS1.3 看板工具函数单元测试 |
### 3.2 ETL Connector `apps/etl/connectors/feiqiu/`
| 路径 | 内容 |
@@ -234,44 +278,111 @@ H5 静态原型页面,用于小程序 UI 设计参考。
### 5.1 Steering 文件(`.kiro/steering/`
13 个 Steering 文件,控制 AI 助手的行为规范:
17 个 Steering 文件,控制 AI 助手的行为规范:
| 文件 | 作用 |
|------|------|
| `language-zh.md` | 语言规范:输出简体中文,代码标识符保留英文 |
| `governance.md` | 治理规范:审计触发条件、执行方式、产物要求 |
| `product.md` / `product-full.md` | 产品概述(精简版 / 完整版) |
| `agent-behavior.md` | AI 执行行为约束:上下文保护、子代理委托场景 |
| `planning-interrogation.md` | 编码前需求审问:必问清单、追问规则 |
| `pre-change-research.md` | 逻辑改动前置调研子代理调研流程、Session 索引查询 |
| `project-overview.md` | 项目概览(精简版) |
| `product-full.md` | 产品概述完整版fileMatch 自动加载) |
| `tech.md` / `tech-full.md` | 技术栈与构建(精简版 / 完整版) |
| `structure-lite.md` / `structure.md` | 项目结构(精简版 / 完整版 |
| `structure.md` | 项目结构完整版fileMatch 自动加载 |
| `export-paths.md` / `export-paths-full.md` | 输出路径规范(精简版 / 完整版) |
| `testing-env.md` | 测试环境规范环境变量加载、cwd 要求、测试库使用 |
| `db-docs.md` | 数据库文档规范 |
| `doc-map.md` | 文档地图 SteeringfileMatch 触发) |
| `deprecated-objects.md` | 归档目录与废弃对象规则 |
| `dwd-doc-authority.md` | DWD-DOC 标杆文档权威性声明 |
| `steering-readme-maintainer.md` | README 维护者技能:变更影响审查与文档同步 |
### 5.2 Spec 文件`.kiro/specs/`
### 5.2 Hooks`.kiro/hooks/`
17Spec 目录,每个包含 `requirements.md``design.md``tasks.md` 三件套
11Agent Hook自动化审计与流程控制
| Hook | 触发事件 | 作用 |
|------|----------|------|
| `agent-on-stop` | agentStop | Agent 停止时触发审计流程 |
| `prompt-on-submit` | promptSubmit | Prompt 提交时触发合规预扫描 |
| `pre-change-guard` | preToolUse | 写操作前的变更守卫 |
| `cwd-guard-shell` | preToolUse | Shell 命令 cwd 守卫 |
| `run-audit-writer` | userTriggered | 手动触发审计写入 |
| `session-summary` | agentStop | Session 摘要生成 |
| `etl-fullstack-integration` | postTaskExecution | ETL 全栈集成任务后处理 |
| `etl-unified-analysis` | userTriggered | ETL 统一分析 |
| `field-disappearance-scan` | userTriggered | 字段消失扫描 |
| `h5-screenshot` | userTriggered | H5 截图 |
| `daily-revenue-report` | userTriggered | 每日营收报告 |
### 5.3 Skills`.kiro/skills/`
3 个技能模块:
| 技能 | 作用 |
|------|------|
| `bd-manual-db-docs` | PostgreSQL schema 变更时落盘 BD 手册到 `docs/database/` |
| `change-annotation-audit` | 每次修改生成审计记录、AI_CHANGELOG、CHANGE 标记注释 |
| `steering-readme-maintainer` | 变更影响审查并同步更新 README 与审计记录 |
### 5.4 Agents`.kiro/agents/`
| Agent | 作用 |
|-------|------|
| `audit-writer.md` | 变更后审计 + 文档同步,输出审计产物 |
### 5.5 Scripts`.kiro/scripts/`
11 个自动化脚本,支撑 Hooks 和审计流程:
| 脚本 | 作用 |
|------|------|
| `agent_on_stop.py` | Agent 停止时的审计处理 |
| `prompt_on_submit.py` | Prompt 提交时的合规预扫描 |
| `audit_flagger.py` | 审计标记器 |
| `audit_reminder.py` | 审计提醒器 |
| `build_audit_context.py` | 构建审计上下文 |
| `change_compliance_prescan.py` | 变更合规预扫描 |
| `file_baseline.py` | 文件基线管理 |
| `prompt_audit_log.py` | Prompt 审计日志 |
| `session_log.py` | Session 日志管理 |
| `_ensure_root.py` | 确保根目录工具 |
### 5.6 Spec 文件(`.kiro/specs/`
24 个 Spec 目录,每个包含 `requirements.md``design.md``tasks.md` 三件套:
| Spec | 内容 |
|------|------|
| `01-miniapp-db-foundation` | P1小程序数据库基础建设 |
| `02-etl-dws-miniapp-extensions` | P2ETL DWS 小程序扩展 |
| `03-miniapp-auth-system` | P3小程序认证系统 |
| `04-miniapp-core-business` | P4小程序核心业务 |
| `05-miniapp-ai-integration` | P5小程序 AI 集成 |
| `[ETL]-fullstack-integration` | ETL 全栈集成 |
| `miniapp-core-business` | 小程序核心业务 |
| `miniapp-db-foundation` | 小程序数据库基础(早期版本) |
| `admin-web-console` | 管理后台控制台 |
| `etl-aggregation-fix` | ETL 聚合修复 |
| `assistant-abolish-cleanup` | 助教废除清理 |
| `business-day-cutoff` | 营业日截止逻辑 |
| `dataflow-field-completion` | 数据流字段补全 |
| `dataflow-structure-audit` | 数据流结构审计 |
| `dwd-business-panorama` | DWD 业务全景 |
| `dwd-phase1-refactor` | DWD 第一阶段重构 |
| `etl-coupon-detail` | ETL 优惠券明细 |
| `etl-dws-flow-refactor` | ETL DWS 流程重构 |
| `etl-pipeline-debug` | ETL 管道调试 |
| `etl-staff-dimension` | ETL 员工维度 |
| `dwd-phase1-refactor` | DWD 第一阶段重构 |
| `etl-unified-pipeline` | ETL 统一管道 |
| `h5-miniprogram-migration` | H5 转小程序迁移 |
| `h5-miniprogram-migration-subsequent` | H5 转小程序迁移(后续) |
| `ods-dedup-standardize` | ODS 去重标准化 |
| `p4-prerequisite-fixes` | P4 前置修复 |
| `p52-miniapp-fe-all-pages` | P5.2 小程序前端全页面 |
| `rns1-infra-contract-rewrite` | RNS1.0 基础设施与契约重写响应包装、CamelModel、路由修正、API 契约) |
| `rns1-task-performance-api` | RNS1.1 任务与绩效接口TASK-1 扩展、TASK-2、PERF-1、PERF-2、前端适配 |
| `rns1-customer-coach-api` | RNS1.2 客户与助教接口CUST-1 客户详情、CUST-2 客户服务记录、COACH-1 助教详情) |
| `rns1-board-apis` | RNS1.3 三看板接口BOARD-1 助教看板、BOARD-2 客户看板、BOARD-3 财务看板、CONFIG-1 技能类型) |
| `spi-spending-power-index` | SPI 消费力指数 |
| `dataflow-field-completion` | 数据流字段补全 |
| `dataflow-structure-audit` | 数据流结构审计 |
| `assistant-abolish-cleanup` | 助教废除清理 |
---
@@ -287,16 +398,17 @@ H5 静态原型页面,用于小程序 UI 设计参考。
| 变更审计记录 | `audit/changes/` | `YYYY-MM-DD__<slug>.md` 格式 |
| 产品需求规格 | `prd/specs/` | P1-P11 等需求 spec不放分析报告 |
| 数据契约 | `contracts/` | OpenAPI spec、JSON Schema、数据字典 |
| 部署与运维配置 | `deployment/` | 启动清单、路径规范、安全指南 |
| 部署与运维配置 | `deployment/` | 启动清单、路径规范、安全指南、隐私政策 |
| 路线图与规划 | `roadmap/` | 迁移计划、BACKLOG |
| Spec 需求输入 | `spec-input/` | 问题汇总,供开启 Spec 流程 |
| 外部参考资料 | `reference/` | 第三方 API/SDK 指南 |
| 迁移记录 | `migrate/` | 仓库迁移总结、配置迁移记录 |
| MCP 相关 | `mcp/` | AI 工具集成文档 |
| UI 原型 | `h5_ui/` | H5 静态原型页面 |
| 小程序前端开发指南 | `miniprogram-dev/` | 页面开发流程、代理手册、规范参考、进度审计、经验教训 |
| 运维手册 | `ops/` | 故障排查、日常运维流程 |
| 小程序前端开发指南 | `miniprogram-dev/` | 页面开发流程、API 审计、设计系统、H5 迁移指南 |
| 运维手册 | `ops/` | 故障排查、日常运维流程、测试用户初始化 |
| 权限矩阵 | `permission_matrix/` | 角色-资源权限映射 |
| VI 设计标准 | `vi-standards/` | 视觉识别设计标准 |
### 判断流程

View File

@@ -71,17 +71,19 @@ NeoZQYY 是面向台球门店业务的全栈数据平台,包含 6 个子系统
| `audit/` | 统一审计目录(变更记录 + Prompt 日志) | `changes/` 变更审计、`prompt_logs/` 交互日志 |
| `contracts/` | 数据契约 | OpenAPI spec、JSON Schema、数据字典 |
| `database/` | 数据库设计与变更文档BD 手册) | `BD_Manual_*.md` 表结构变更审计、`ddl/` 基线 |
| `deployment/` | 部署与运维配置文档 | 启动清单、输出路径规范、隐私政策 |
| `h5_ui/` | 小程序 UI 原型H5 静态页面) | 页面原型、样式、交互脚本 |
| `mcp/` | MCP Server 相关文档 | AI 数据库查询手册 |
| `deployment/` | 部署与运维配置文档 | 启动清单、输出路径规范、隐私政策、微信安全/加密指南 |
| `h5_ui/` | 小程序 UI 原型H5 静态页面) | 页面原型、样式、交互脚本、H5 转 MP 检查工具 |
| `mcp/` | MCP Server 相关文档 | AI 数据库查询手册、微信开发者工具 MCP |
| `migrate/` | 迁移记录与指南 | Monorepo 迁移总结、旧配置迁移记录 |
| `ops/` | 运维手册 | 故障排查、日常运维流程(待填充) |
| `miniprogram-dev/` | 小程序前端开发指南 | API 审计、设计系统、H5 迁移指南、展示规范 |
| `ops/` | 运维手册 | 测试用户初始化、故障排查 |
| `permission_matrix/` | 权限矩阵 | 角色-资源权限映射(待填充) |
| `prd/` | 产品需求文档 | PRD 审阅 Q&A、`specs/` 下 P1-P11 需求规格 |
| `prd/` | 产品需求文档 | PRD 审阅 Q&A、`specs/` 下 P1-P11 需求规格(含 P5.1/P5.2 |
| `reference/` | 外部参考资料 | 百炼 Agent 指南、DashScope API 参考 |
| `reports/` | 数据分析报告与调研产出 | 复杂订单分析、DWD 字段口径全景等一次性分析报告 |
| `reports/` | 数据分析报告与调研产出 | DWD-DOC 标杆文档、业务分析、ETL 校准、VI 配色审计等(按主题分子目录) |
| `roadmap/` | 路线图与待办 | 迁移计划、BACKLOG |
| `spec-input/` | Spec 流程的需求输入文档 | 用户提交的问题汇总,供开启 Spec 使用 |
| `vi-standards/` | VI 设计标准 | 视觉识别设计标准(待填充) |
## 技术栈速览

View File

@@ -0,0 +1,234 @@
# 后端架构文档 — FastAPI 服务层
> 更新日期2026-03-19RNS1.3 三看板接口 + CONFIG-1 技能类型)
> 位置:`apps/backend/`
> 框架FastAPI + psycopg2同步连接池
> 数据库:`zqyy_app`(业务库)+ `etl_feiqiu`ETL 库,直连只读访问)
---
## 1. 模块交互总览
```mermaid
graph TB
subgraph "微信小程序"
MP[miniprogram pages]
end
subgraph "管理后台"
ADMIN[admin-web]
end
subgraph "FastAPI 后端"
subgraph "路由层 (routers/)"
R_AUTH[xcx_auth]
R_TASKS[xcx_tasks]
R_NOTES[xcx_notes]
R_PERF[xcx_performance]
R_CUST[xcx_customers<br/>CUST-1, CUST-2]
R_COACH[xcx_coaches<br/>COACH-1]
R_CHAT[xcx_ai_chat]
R_CACHE[xcx_ai_cache]
R_ADMIN[admin_*]
end
subgraph "服务层 (services/)"
S_TM[task_manager<br/>任务 CRUD + 列表扩展]
S_PS[performance_service<br/>绩效概览 + 明细]
S_NS[note_service<br/>备注 CRUD + 评分]
S_CS[customer_service<br/>客户详情 + 服务记录]
S_CO[coach_service<br/>助教详情]
S_FDW[fdw_queries<br/>FDW 查询集中封装]
S_TG[task_generator<br/>任务自动生成]
S_TE[task_expiry<br/>过期检测]
S_RD[recall_detector<br/>召回完成检测]
S_WX[wechat<br/>微信登录/Token]
end
subgraph "中间件 + 认证"
MW[ResponseWrapperMiddleware]
AUTH[JWT + require_approved]
end
end
subgraph "数据库"
BIZ[(zqyy_app<br/>biz / auth / public)]
ETL[(etl_feiqiu<br/>直连 app.v_*)]
end
MP --> R_AUTH & R_TASKS & R_NOTES & R_PERF & R_CUST & R_COACH & R_CHAT
ADMIN --> R_ADMIN
R_TASKS --> S_TM
R_PERF --> S_PS
R_NOTES --> S_NS
R_CUST --> S_CS
R_COACH --> S_CO
R_CHAT --> S_TM
S_TM --> S_FDW
S_PS --> S_FDW
S_CS --> S_FDW
S_CO --> S_FDW
S_TM --> BIZ
S_PS --> BIZ
S_NS --> BIZ
S_CS --> BIZ
S_CO --> BIZ
S_FDW --> ETL
style S_FDW fill:#9f9,stroke:#333
style S_PS fill:#9f9,stroke:#333
style R_PERF fill:#9f9,stroke:#333
style S_CS fill:#9f9,stroke:#333
style S_CO fill:#9f9,stroke:#333
style R_CUST fill:#9f9,stroke:#333
style R_COACH fill:#9f9,stroke:#333
```
## 2. 路由模块清单
| 路由文件 | 前缀 | 说明 |
|----------|------|------|
| `xcx_auth.py` | `/api/xcx/auth` | 微信登录、Token 刷新、入驻申请 |
| `xcx_tasks.py` | `/api/xcx/tasks` | 任务列表(TASK-1)、详情(TASK-2)、pin/unpin、abandon/restore |
| `xcx_notes.py` | `/api/xcx/notes` | 备注 CRUD含 score 评分) |
| `xcx_performance.py` | `/api/xcx/performance` | 绩效概览(PERF-1)、明细(PERF-2) |
| `xcx_customers.py` | `/api/xcx/customers` | 客户详情(CUST-1)、客户服务记录(CUST-2)RNS1.2 新增) |
| `xcx_coaches.py` | `/api/xcx/coaches` | 助教详情(COACH-1)RNS1.2 新增) |
| `xcx_board.py` | `/api/xcx/board` | 助教看板(BOARD-1)、客户看板(BOARD-2)、财务看板(BOARD-3)RNS1.3 新增) |
| `xcx_config.py` | `/api/xcx/config` | 技能类型配置(CONFIG-1)RNS1.3 新增) |
| `xcx_ai_chat.py` | `/api/xcx/ai` | AI 对话SSE 流式) |
| `xcx_ai_cache.py` | `/api/xcx/ai-cache` | AI 缓存查询 |
| `admin_applications.py` | `/api/admin/applications` | 入驻审核 |
| `auth.py` | `/api/auth` | 管理后台登录 |
| `member_retention_clue.py` | `/api/member-retention-clue` | 维客线索 |
| `ops_panel.py` | `/api/ops` | 运维面板 |
| `business_day.py` | `/api/business-day` | 营业日配置 |
## 3. 服务层模块清单
| 服务文件 | 职责 | 数据源 |
|----------|------|--------|
| `task_manager.py` | 任务 CRUD、`get_task_list_v2()``get_task_detail()` | biz.coach_tasks + FDW |
| `performance_service.py` | `get_overview()``get_records()`,绩效汇总与明细 | FDW |
| `note_service.py` | 备注创建(含 score、AI 占位、回访触发 | biz.notes |
| `customer_service.py` | 客户详情(CUST-1)、客户服务记录(CUST-2)RNS1.2 新增) | biz.coach_tasks + biz.ai_cache + biz.notes + public.member_retention_clue + FDW |
| `coach_service.py` | 助教详情(COACH-1):绩效/收入/任务分组/TOP客户/历史月份RNS1.2 新增) | biz.coach_tasks + biz.notes + FDW |
| `fdw_queries.py` | ETL RLS 视图查询集中封装,直连 ETL 库 + `SET LOCAL app.current_site_id` 门店隔离 | app.v_* (ETL 直连) |
| `task_generator.py` | 定时任务:基于 WBI/NCI/RS 指数自动生成助教任务 | biz + FDW |
| `task_expiry.py` | 定时任务:检测过期任务并标记 inactive | biz.coach_tasks |
| `recall_detector.py` | 事件驱动ETL 数据更新后检测召回完成 | biz + FDW |
| `note_reclassifier.py` | 事件驱动:召回完成后回溯重分类备注 | biz.notes |
| `wechat.py` | 微信 code2session、Token 管理 | 外部 API |
| `role.py` | 角色权限查询 | auth.* |
| `scheduler.py` | 触发器调度引擎 | biz.trigger_jobs |
| `board_service.py` | 三看板编排:`get_coach_board()``get_customer_board()``get_finance_board()`,含日期范围/环比/排序/分页/降级RNS1.3 新增) | FDW + biz.coach_tasks |
| `application.py` | 入驻申请处理 | auth.applications |
## 4. FDW 查询封装fdw_queries.py
所有跨库查询集中在此模块,确保 DWD-DOC 强制规则统一实施。
⚠️ 架构变更2026-03-18不再使用 zqyy_app 的 `fdw_etl.*` foreign table改为通过 `get_etl_readonly_connection(site_id)` 直连 ETL 库查询 `app.v_*` RLS 视图。原因:`postgres_fdw` 不传递自定义 GUC 参数到远端连接,导致 RLS 视图的 `current_setting('app.current_site_id')` 在远端未设置而报错。
所有函数通过 `_fdw_context()` 上下文管理器:创建 ETL 直连 → `SET LOCAL app.current_site_id` → 执行查询 → 关闭连接。
| 函数 | 用途 | DWD-DOC 规则 |
|------|------|-------------|
| `get_member_info()` | 批量查询会员姓名/手机号 | DQ-6JOIN v_dim_member |
| `get_member_balance()` | 批量查询储值卡余额 | DQ-7JOIN v_dim_member_card_account |
| `get_last_visit_days()` | 批量查询距上次到店天数 | 废单排除is_delete = 0 |
| `get_salary_calc()` | 助教绩效/档位/收入 | 收入用 gross_salary费用用 base_income + bonus_income |
| `get_service_records()` | 服务记录明细(分页) | 收入用 ledger_amount + DQ-6 + 废单排除 |
| `get_service_records_for_task()` | 特定客户服务记录 | 同上 |
| `get_consumption_60d()` | 客户近 60 天消费金额RNS1.2 新增) | 收入用 ledger_amountitems_sum+ 废单排除 |
| `get_relation_index()` | 客户与助教关系指数列表RNS1.2 新增) | 来源 v_dws_member_assistant_relation_index |
| `get_consumption_records()` | 客户消费记录嵌套查询RNS1.2 新增) | items_sum + DWD-DOC 规则 2 + 废单排除 |
| `get_total_service_count()` | 客户累计服务总次数RNS1.2 新增) | 废单排除is_delete = 0 |
| `get_coach_60d_stats()` | 助教对客户近 60 天统计RNS1.2 新增) | 废单排除is_delete = 0 |
| `get_assistant_info()` | 助教基本信息RNS1.2 新增) | 来源 v_dim_assistant |
| `get_salary_calc_multi_months()` | 批量多月绩效数据RNS1.2 新增) | 来源 v_dws_assistant_salary_calc |
| `get_monthly_customer_count()` | 各月不重复客户数RNS1.2 新增) | 废单排除is_delete = 0 |
| `get_coach_top_customers()` | 助教 TOP 客户RNS1.2 新增) | DQ-6 + DQ-7 + items_sum + 废单排除 |
| `get_customer_service_records()` | 客户按月服务记录RNS1.2 新增) | DQ-6 + items_sum + 废单排除 |
| `get_all_assistants()` | BOARD-1按技能筛选助教列表RNS1.3 新增) | v_dim_assistant |
| `get_salary_calc_batch()` | BOARD-1批量查询当期/上期绩效RNS1.3 新增) | items_sum + assistant_pd/cx_money |
| `get_top_customers_for_coaches()` | BOARD-1按亲密度 Top3 客户 + 四级 emojiRNS1.3 新增) | DQ-6 + v_dws_member_assistant_relation_index |
| `get_coach_sv_data()` | BOARD-1助教客源储值数据RNS1.3 新增) | v_dws_assistant_monthly_summary |
| `get_customer_board_recall()` | BOARD-2召回维度WBI 降序RNS1.3 新增) | v_dws_member_winback_index + DQ-6 |
| `get_customer_board_potential()` | BOARD-2潜力维度SPI 降序RNS1.3 新增) | v_dws_member_spending_power_index |
| `get_customer_board_balance()` | BOARD-2余额维度RNS1.3 新增) | v_dim_member_card_account + DQ-7 |
| `get_customer_board_recharge()` | BOARD-2充值维度RNS1.3 新增) | v_dwd_recharge_order |
| `get_customer_board_recent()` | BOARD-2最近到店维度RNS1.3 新增) | v_dws_member_visit_detail |
| `get_customer_board_spend60()` | BOARD-260 天消费维度RNS1.3 新增) | items_sum 口径 |
| `get_customer_board_freq60()` | BOARD-260 天频次维度 + weeklyVisitsRNS1.3 新增) | v_dws_member_consumption_summary |
| `get_customer_board_loyal()` | BOARD-2忠诚度维度RS 降序RNS1.3 新增) | v_dws_member_assistant_relation_index |
| `get_customer_assistants()` | BOARD-2批量查询客户关联助教RNS1.3 新增) | 含亲密度 + 当前跟进置顶 |
| `get_finance_overview()` | BOARD-3经营一览 8 项指标RNS1.3 新增) | v_dws_finance_daily_summary + items_sum |
| `get_finance_recharge()` | BOARD-3预收资产储值卡 + 赠送卡矩阵RNS1.3 新增) | v_dws_finance_recharge_summary |
| `get_finance_revenue()` | BOARD-3应计收入结构RNS1.3 新增) | v_dws_finance_income_structure + discount_detail |
| `get_finance_cashflow()` | BOARD-3现金流入RNS1.3 新增) | v_dws_finance_daily_summary + 互斥规则 7 |
| `get_finance_expense()` | BOARD-3现金流出 4 子分组RNS1.3 新增) | v_dws_finance_expense_summary + platform_settlement |
| `get_finance_coach_analysis()` | BOARD-3助教分析basic + incentiveRNS1.3 新增) | v_dws_assistant_salary_calc |
| `get_skill_types()` | CONFIG-1技能类型配置RNS1.3 新增) | ETL cfg 表 |
### 列名映射design.md 理想名 → 实际 FDW 视图列名)
RLS 视图直接暴露 DWD/DWS 原始列名,后端代码在 SQL 中使用 AS 别名转换。
**v_dwd_assistant_service_log**(基于 `dwd.dwd_assistant_service_log`,非 _ex 表):
| 代码语义 | 实际列名 | 说明 |
|----------|----------|------|
| id | `assistant_service_id` | 服务记录主键 |
| assistant_id (WHERE) | `site_assistant_id` | 与 salary_calc.assistant_id 同源 |
| member_id | `tenant_member_id` | 会员 ID |
| is_trash = false | `is_delete = 0` | 整数类型0=正常 |
| settle_time | `create_time` | 结算时间 |
| start_time | `start_use_time` | 开始使用时间 |
| end_time | `last_use_time` | 最后使用时间 |
| service_hours | `income_seconds / 3600.0` | 折算工时(计算字段) |
| service_hours_raw | `real_use_seconds / 3600.0` | 原始工时(计算字段) |
| income (items_sum) | `ledger_amount` | 收入金额 |
| course_type | `skill_name` | 课程类型 |
| table_name | `site_table_id` | 仅有台桌 ID无名称 |
**v_dws_assistant_salary_calc**
| 代码语义 | 实际列名 | 说明 |
|----------|----------|------|
| calc_month | `salary_month` | date 类型,存储为 YYYY-MM-01 |
| coach_level | `assistant_level_name` | 档位名称 |
| tier_index | `tier_id` | 档位 ID |
| basic_hours | `base_hours` | 基础课时 |
| total_hours | `effective_hours` | 有效总工时 |
| total_income | `gross_salary` | 总收入 |
| basic_rate | `base_course_price` | 基础课单价 |
| incentive_rate | `bonus_course_price` | 激励课单价 |
| bonus_money | `sprint_bonus` | 冲刺奖金 |
| assistant_pd_money_total | `base_income` | 基础课总收入 |
| assistant_cx_money_total | `bonus_income` | 激励课总收入 |
> 注意:`tier_nodes`、`total_customers`、`next_tier_*`、`tier_completed` 在视图中不存在,后端使用默认值或从其他数据源推算。
## 5. 数据流向
```
小程序请求
→ JWT 认证 (require_approved)
→ 路由层 (routers/)
→ 服务层 (services/)
→ 业务库 (zqyy_app) 直连
→ ETL 库 (etl_feiqiu) 直连只读app.v_* RLS 视图)
→ 响应包装 (ResponseWrapperMiddleware)
→ { code: 0, data: ..., message: "ok" }
```
## 6. 关键设计决策
- **ETL 直连**:所有 ETL 查询封装在 `fdw_queries.py`,通过 `_fdw_context()` 直连 ETL 库查询 `app.v_*` RLS 视图(不使用 FDW foreign table因 postgres_fdw 不传递自定义 GUC
- **优雅降级**扩展字段lastVisitDays/balance/aiSuggestion查询失败返回 null不影响核心响应
- **camelCase 转换**:所有小程序端响应通过 `CamelModel` 自动转换为 camelCase
- **门店隔离**:业务库通过 `site_id` 参数过滤ETL 查询通过 `SET LOCAL app.current_site_id` + RLS 视图隔离

View File

@@ -1,11 +1,17 @@
# 审计一览表
> 自动生成于 2026-03-15 10:14:07,请勿手动编辑。
> 自动生成于 2026-03-19 18:47:42,请勿手动编辑。
## 时间线视图
| 日期 | 项目 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|------|----------|----------|----------|------|------|
| 2026-03-20 | ETL-feiqiu, 后端, 项目级 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | ETL-feiqiu, 后端 | 变更审计记录card_type_id 年卡/月卡映射文档同步 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-19__card-type-id-doc-sync.md) |
| 2026-03-19 | 项目级 | 变更审计记录coach_service 绩效档位硬编码修复 | bugfix | 其他 | 低 | [链接](changes/2026-03-19__coach-tier-hardcode-fix.md) |
| 2026-03-19 | ETL-feiqiu, 后端 | 变更审计记录助教等级映射硬编码修复P2-9 | bugfix | 其他, 文档 | 低 | [链接](changes/2026-03-19__level-map-hardcode-fix.md) |
| 2026-03-19 | 项目级 | 数据库变更审计RNS1.2 客户与助教接口 | 文档 | 其他 | 低 | [链接](changes/2026-03-19__rns12-db-audit.md) |
| 2026-03-18 | 项目级 | RNS1.1 E2E 测试 — FDW 直连改造 + performance_service bug 修复 | bugfix | 其他 | 未知 | [链接](changes/2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md) |
| 2026-03-15 | 项目级 | 变更审计记录DDL 基线统一整理 + BD 手册重组 + 小程序副本清理 | bugfix | 其他 | 低 | [链接](changes/2026-03-15__ddl-baseline-consolidation-bd-manual-reorg.md) |
| 2026-03-13 | ETL-feiqiu, 小程序 | 审计记录board-finance line-height 全量补齐 | bugfix | 其他, 文档 | 低 | [链接](changes/2026-03-13__board-finance-line-height-audit.md) |
| 2026-03-13 | 项目级 | 审计记录board-finance.wxss rpx 换算公式修正 | 功能 | 其他 | 未知 | [链接](changes/2026-03-13__board-finance-rpx-formula-correction.md) |
@@ -69,6 +75,9 @@
| 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|----------|----------|----------|------|------|
| 2026-03-20 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | 变更审计记录card_type_id 年卡/月卡映射文档同步 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-19__card-type-id-doc-sync.md) |
| 2026-03-19 | 变更审计记录助教等级映射硬编码修复P2-9 | bugfix | 其他, 文档 | 低 | [链接](changes/2026-03-19__level-map-hardcode-fix.md) |
| 2026-03-13 | 审计记录board-finance line-height 全量补齐 | bugfix | 其他, 文档 | 低 | [链接](changes/2026-03-13__board-finance-line-height-audit.md) |
| 2026-03-08 | 变更审计记录P5 AI 集成需求审视 — 7 项歧义修补 + category 枚举对齐 | 文档 | 其他, 文档, 脚本工具 | 未知 | [链接](changes/2026-03-08__p5-ai-spec-review-category-enum-align.md) |
| 2026-03-02 | 变更审计:合并 ETL Hook 为统一分析入口 | 文档 | 其他, 脚本工具 | 未知 | [链接](changes/2026-03-02__etl-unified-analysis-hook-merge.md) |
@@ -104,6 +113,9 @@
| 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|----------|----------|----------|------|------|
| 2026-03-20 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | 变更审计记录card_type_id 年卡/月卡映射文档同步 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-19__card-type-id-doc-sync.md) |
| 2026-03-19 | 变更审计记录助教等级映射硬编码修复P2-9 | bugfix | 其他, 文档 | 低 | [链接](changes/2026-03-19__level-map-hardcode-fix.md) |
| 2026-03-08 | 变更审计记录P5 AI 集成需求审视 — 7 项歧义修补 + category 枚举对齐 | 文档 | 其他, 文档, 脚本工具 | 未知 | [链接](changes/2026-03-08__p5-ai-spec-review-category-enum-align.md) |
| 2026-02-28 | 变更审计记录:多模块累积变更(营业日/核心业务/认证/ETL DWS 重构/参考文档合并) | 重构 | 其他 | 未知 | [链接](changes/2026-02-28__multi-module-accumulated-changes.md) |
| 2026-02-15 | 审计记录:后端依赖补全使 FastAPI 可启动 | bugfix | 其他 | 未知 | [链接](changes/2026-02-15__backend-deps-bootstrap.md) |
@@ -142,6 +154,10 @@
| 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|----------|----------|----------|------|------|
| 2026-03-20 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 其他, 文档 | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | 变更审计记录coach_service 绩效档位硬编码修复 | bugfix | 其他 | 低 | [链接](changes/2026-03-19__coach-tier-hardcode-fix.md) |
| 2026-03-19 | 数据库变更审计RNS1.2 客户与助教接口 | 文档 | 其他 | 低 | [链接](changes/2026-03-19__rns12-db-audit.md) |
| 2026-03-18 | RNS1.1 E2E 测试 — FDW 直连改造 + performance_service bug 修复 | bugfix | 其他 | 未知 | [链接](changes/2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md) |
| 2026-03-15 | 变更审计记录DDL 基线统一整理 + BD 手册重组 + 小程序副本清理 | bugfix | 其他 | 低 | [链接](changes/2026-03-15__ddl-baseline-consolidation-bd-manual-reorg.md) |
| 2026-03-13 | 审计记录board-finance.wxss rpx 换算公式修正 | 功能 | 其他 | 未知 | [链接](changes/2026-03-13__board-finance-rpx-formula-correction.md) |
| 2026-03-13 | 变更审计记录task-list 页面 H5 原型 1:1 重写 | bugfix | 其他 | 未知 | [链接](changes/2026-03-13__task-list-h5-rewrite.md) |
@@ -215,6 +231,12 @@
| 日期 | 需求摘要 | 变更类型 | 风险 | 详情 |
|------|----------|----------|------|------|
| 2026-03-20 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | 变更审计记录card_type_id 年卡/月卡映射文档同步 | bugfix | 未知 | [链接](changes/2026-03-19__card-type-id-doc-sync.md) |
| 2026-03-19 | 变更审计记录coach_service 绩效档位硬编码修复 | bugfix | 低 | [链接](changes/2026-03-19__coach-tier-hardcode-fix.md) |
| 2026-03-19 | 变更审计记录助教等级映射硬编码修复P2-9 | bugfix | 低 | [链接](changes/2026-03-19__level-map-hardcode-fix.md) |
| 2026-03-19 | 数据库变更审计RNS1.2 客户与助教接口 | 文档 | 低 | [链接](changes/2026-03-19__rns12-db-audit.md) |
| 2026-03-18 | RNS1.1 E2E 测试 — FDW 直连改造 + performance_service bug 修复 | bugfix | 未知 | [链接](changes/2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md) |
| 2026-03-15 | 变更审计记录DDL 基线统一整理 + BD 手册重组 + 小程序副本清理 | bugfix | 低 | [链接](changes/2026-03-15__ddl-baseline-consolidation-bd-manual-reorg.md) |
| 2026-03-13 | 审计记录board-finance line-height 全量补齐 | bugfix | 低 | [链接](changes/2026-03-13__board-finance-line-height-audit.md) |
| 2026-03-13 | 审计记录board-finance.wxss rpx 换算公式修正 | 功能 | 未知 | [链接](changes/2026-03-13__board-finance-rpx-formula-correction.md) |
@@ -272,6 +294,9 @@
| 日期 | 需求摘要 | 变更类型 | 风险 | 详情 |
|------|----------|----------|------|------|
| 2026-03-20 | 变更审计记录RNS1.3 三看板 FDW 查询层数据口径修复 | bugfix | 未知 | [链接](changes/2026-03-20__rns13-board-apis-e2e-fix.md) |
| 2026-03-19 | 变更审计记录card_type_id 年卡/月卡映射文档同步 | bugfix | 未知 | [链接](changes/2026-03-19__card-type-id-doc-sync.md) |
| 2026-03-19 | 变更审计记录助教等级映射硬编码修复P2-9 | bugfix | 低 | [链接](changes/2026-03-19__level-map-hardcode-fix.md) |
| 2026-03-13 | 审计记录board-finance line-height 全量补齐 | bugfix | 低 | [链接](changes/2026-03-13__board-finance-line-height-audit.md) |
| 2026-03-08 | 变更审计记录P5 AI 集成需求审视 — 7 项歧义修补 + category 枚举对齐 | 文档 | 未知 | [链接](changes/2026-03-08__p5-ai-spec-review-category-enum-align.md) |
| 2026-02-15 | 变更审计记录Change Audit Record | 文档 | 极低 | [链接](changes/2026-02-15__audit-consolidation-doc-reorg.md) |

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
# BD_Manualapp Schema 与 RLS 视图层
> 目标库:`test_etl_feiqiu`(通过 `PG_DSN` 连接)
> 迁移脚本:`db/etl_feiqiu/migrations/2026-02-24__p1_create_app_schema_rls_views.sql`
> 迁移脚本:`db/etl_feiqiu/migrations/2026-02-24__p1_create_app_schema_rls_views.sql`、`db/etl_feiqiu/migrations/2026-03-19_add_board_rls_views.sql`
> DDL 位置:`docs/database/ddl/etl_feiqiu__app.sql`(执行后需重新生成)
> 关联 SPEC`miniapp-db-foundation`P1 基础设施层)
> 关联 SPEC`miniapp-db-foundation`P1 基础设施层)、`rns1-board-apis`BOARD 看板)
---
@@ -15,7 +15,7 @@
### 新增角色
- `app_reader`:只读角色(`LOGIN`),拥有 `app` Schema 的 `USAGE` + `SELECT` 权限
### 新增视图35 张)
### 新增视图38 张)
**DWD 层11 张,全部含 `site_id` 过滤):**
@@ -33,7 +33,7 @@
| `app.v_dim_staff` | `dwd.dim_staff` | 同上 |
| `app.v_dim_staff_ex` | `dwd.dim_staff_ex` | 同上 |
**DWS 层 — 含 `site_id` 过滤20 张):**
**DWS 层 — 含 `site_id` 过滤23 张):**
| 视图 | 源表 |
|------|------|
@@ -57,6 +57,9 @@
| `app.v_dws_platform_settlement` | `dws.dws_platform_settlement` |
| `app.v_dws_assistant_recharge_commission` | `dws.dws_assistant_recharge_commission` |
| `app.v_dws_order_summary` | `dws.dws_order_summary` |
| `app.v_dws_assistant_project_tag` | `dws.dws_assistant_project_tag` |
| `app.v_dws_member_project_tag` | `dws.dws_member_project_tag` |
| `app.v_dws_member_spending_power_index` | `dws.dws_member_spending_power_index` |
**DWS 层 — cfg_* 配置表4 张,无 `site_id`,直接 `SELECT *`**
@@ -74,9 +77,10 @@
| `app_reader` | `app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES` |
### P2 预留(注释形式,暂不创建)
- `dws.dws_member_spending_power_index``app.v_dws_member_spending_power_index`
- `dws.dws_assistant_order_contribution``app.v_dws_assistant_order_contribution`
> `v_dws_member_spending_power_index`、`v_dws_assistant_project_tag`、`v_dws_member_project_tag` 已于 2026-03-19 正式创建(迁移脚本 `2026-03-19_add_board_rls_views.sql`)。
---
## 2. 兼容性影响
@@ -113,7 +117,7 @@ DROP ROLE IF EXISTS app_reader;
-- 1. 验证 app Schema 存在
SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'app';
-- 2. 验证视图数量(应为 35 张
-- 2. 验证视图数量(应为 38 张:原 35 + 2026-03-19 新增 3
SELECT count(*) FROM information_schema.views WHERE table_schema = 'app';
-- 3. 验证 app_reader 角色存在且有 app Schema 权限

View File

@@ -16,7 +16,7 @@
|---|------|------|--------|
| 1 | `biz.coach_tasks` | 助教任务表:存储任务分配、状态、有效期、置顶、放弃原因等 | 15 |
| 2 | `biz.coach_task_history` | 任务变更历史表:记录任务关闭/新建/置顶/放弃的追溯链 | 9 |
| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 14 |
| 3 | `biz.notes` | 统一备注表:通过 `type` 字段区分普通备注/回访备注/放弃原因,含星星评分 | 15 |
| 4 | `biz.trigger_jobs` | 触发器配置表:存储 cron/interval/event 三种触发方式的配置与执行状态 | 9 |
### 表字段明细
@@ -55,7 +55,7 @@
| `detail` | JSONB | 可空 | 附加详情(如放弃原因等) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 记录时间 |
#### biz.notes14 字段)
#### biz.notes15 字段)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
@@ -73,6 +73,7 @@
| `ai_analysis` | TEXT | 可空 | AI 分析结果P5 实现) |
| `created_at` | TIMESTAMPTZ | DEFAULT NOW() | 创建时间 |
| `updated_at` | TIMESTAMPTZ | DEFAULT NOW() | 更新时间 |
| `score` | SMALLINT | CHECK (1-5),可空 | 备注星星评分,助教创建备注时可选填写,不参与 AI 分析RNS1.1 新增) |
#### biz.trigger_jobs9 字段)
@@ -99,6 +100,7 @@
| `notes` | `idx_notes_target` | INDEX | `(site_id, target_type, target_id)`,按目标查询备注加速 |
| `notes` | CHECK `rating_service_willingness` | CHECK | `BETWEEN 1 AND 5` |
| `notes` | CHECK `rating_revisit_likelihood` | CHECK | `BETWEEN 1 AND 5` |
| `notes` | CHECK `score` | CHECK | `score IS NULL OR (score >= 1 AND score <= 5)`RNS1.1 新增) |
| `notes` | FK `task_id` | FK | → `biz.coach_tasks(id)` |
| `trigger_jobs` | UNIQUE `job_name` | UNIQUE | 触发器名称唯一 |
@@ -128,6 +130,44 @@
---
## 2.1 RNS1.3 看板接口引用说明2026-03-20 补充)
RNS1.3三看板接口BOARD-1 助教看板的任务维度查询引用了 `biz.coach_tasks`(无 schema 变更,仅新增读取路径):
| 表 | 引用接口 | 用途 |
|----|---------|------|
| `biz.coach_tasks` | BOARD-1 `_query_coach_tasks()` | 按 `site_id` + `assistant_id` + 日期范围查询任务完成数,按 `task_type` 分类统计 `recall`(召回类:`high_priority_recall` / `priority_recall`)和 `callback`(回访类:`follow_up_visit` / `relationship_building`),筛选条件 `status = 'completed'` + `completed_at BETWEEN start_date AND end_date` |
查询模式:
```sql
SELECT assistant_id, task_type, COUNT(*) AS cnt
FROM biz.coach_tasks
WHERE site_id = :site_id
AND assistant_id = ANY(:assistant_ids)
AND status = 'completed'
AND completed_at BETWEEN :start_date AND :end_date
GROUP BY assistant_id, task_type
```
> 该查询走 `idx_coach_tasks_assistant_status` 索引(`site_id, assistant_id, status`),无需新增索引。
---
## 2.2 RNS1.2 接口引用说明2026-03-18 补充)
RNS1.2(客户与助教接口)新增 3 个端点,引用了以下 biz/public 表(无 schema 变更,仅新增读取路径):
| 表 | 引用接口 | 用途 |
|----|---------|------|
| `biz.coach_tasks` | CUST-1 `coachTasks` 模块、COACH-1 `visibleTasks`/`hiddenTasks`/`abandonedTasks`/`tasksCompleted` | 查询客户关联的助教任务(按 `member_id`);查询助教任务分组(按 `assistant_id``status` 分组);统计当月完成任务数 |
| `biz.notes` | CUST-1 `notes` 模块、COACH-1 `notes`/任务备注 | 查询客户备注(`target_type='member'`,最多 20 条);查询助教相关备注(最多 20 条);查询任务关联备注(`task_id` 关联) |
| `biz.ai_cache` | CUST-1 `aiInsight` 模块 | 查询 AI 分析缓存(`cache_type='app4_analysis'``target_id=customerId`),解析 `cache_value` JSON 生成洞察摘要和策略建议 |
| `public.member_retention_clue` | CUST-1 `retentionClues` 模块 | 查询维客线索(按 `created_at` 倒序),格式与 TASK-2 一致 |
> 以上均为只读查询,不涉及表结构变更。`biz.ai_cache` 表由 P5AI 集成)创建,`public.member_retention_clue` 表由独立迁移创建。
---
## 3. 回滚策略
按逆序 `DROP TABLE IF EXISTS CASCADE`(迁移脚本末尾已包含注释形式的回滚语句):

View File

@@ -34,9 +34,9 @@
### 导入的外部表
通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 35 张
通过 `IMPORT FOREIGN SCHEMA app` 批量导入,外部表与 ETL 库 `app` Schema 中的 RLS 视图一一对应(共 38 张2026-03-19 新增 3 张 BOARD 看板视图
- 11 张 DWD 视图:`v_dim_member``v_dim_assistant``v_dim_member_card_account``v_dim_table``v_dwd_settlement_head``v_dwd_table_fee_log``v_dwd_assistant_service_log``v_dwd_recharge_order``v_dwd_store_goods_sale``v_dim_staff``v_dim_staff_ex`
- 24 张 DWS 视图:`v_dws_member_consumption_summary``v_dws_member_visit_detail`
- 27 张 DWS 视图(含 4 张 cfg_* 配置表)`v_dws_member_consumption_summary``v_dws_member_visit_detail`
### 权限配置
@@ -46,6 +46,23 @@
---
## 1.1 RNS1.2 接口引用说明2026-03-19 补充)
RNS1.2(客户与助教接口)通过 `fdw_queries.py` **直连 ETL 库**查询以下 `app.v_*` RLS 视图(不使用 `fdw_etl.*` 外部表,原因:`postgres_fdw` 不传递自定义 GUC 参数到远端连接):
| 视图 | 引用接口 | 用途 |
|------|---------|------|
| `app.v_dim_member` | CUST-1, CUST-2, COACH-1 | 会员信息nickname, mobileDQ-6 合规 |
| `app.v_dim_member_card_account` | CUST-1, COACH-1 | 会员卡余额DQ-7 合规 |
| `app.v_dim_assistant` | CUST-1, COACH-1 | 助教基本信息 |
| `app.v_dwd_assistant_service_log` | CUST-1, CUST-2, COACH-1 | 服务记录明细(`is_delete=0` 废单排除,`ledger_amount` items_sum 口径) |
| `app.v_dws_assistant_salary_calc` | COACH-1 | 助教绩效/档位/收入 |
| `app.v_dws_member_assistant_relation_index` | CUST-1, COACH-1 | 会员-助教关系指数 |
> 本次无 FDW 配置变更。所有视图已存在于 ETL 库 `app` schema 中。审计详情见 `docs/audit/changes/2026-03-19__rns12-db-audit.md`。
---
## 2. 兼容性影响
| 组件 | 影响 |

View File

@@ -0,0 +1,261 @@
# BD 手册FDW 反向映射 — ETL 库读取业务库维客线索
> 创建日期2026-02-26原 `member_birthday_manual`2026-03-19 重写为当前版本
> 替代文档:`docs/database/_archived/BD_Manual_fdw_reverse_member_birthday.md`
> 关联 SQL`db/fdw/setup_fdw_reverse.sql`(生产)、`db/fdw/setup_fdw_reverse_test.sql`(测试)
> 关联表文档:`docs/database/BD_Manual_member_retention_clue.md`
---
## 1. 概述
`etl_feiqiu`(生产)/ `test_etl_feiqiu`(测试)数据库中,通过 `postgres_fdw` 创建指向 `zqyy_app` / `test_zqyy_app` 的外部表 `fdw_app.member_retention_clue`,使 ETL DWS 任务可只读访问助教为会员记录的维客线索数据。
方向:`etl_feiqiu → zqyy_app`(与正向 FDW `setup_fdw.sql``zqyy_app → etl_feiqiu` 方向相反)。
### 数据流向
```
zqyy_app.public.member_retention_clue (业务库源表,后端 API 写入)
│ postgres_fdw只读
etl_feiqiu.fdw_app.member_retention_clue ETL 库外部表DWS 任务读取)
```
### 历史沿革
| 日期 | 事件 |
|------|------|
| 2026-02-22 | 初版:`fdw_app.member_birthday_manual`(映射助教手动补录生日表) |
| 2026-02-26 | 重构:`member_birthday_manual``member_retention_clue`(维客线索替代单一生日方案) |
| 2026-02-27 | 业务库侧新增 `source`线索来源FDW 外部表定义未同步更新 |
| 2026-03-08 | 业务库侧 `category` 枚举对齐:`客户基础信息``客户基础` |
---
## 2. 变更说明
### 2.1 新增对象
| 所在库 | 对象类型 | 名称 | 说明 |
|--------|---------|------|------|
| etl_feiqiu / test_etl_feiqiu | 扩展 | `postgres_fdw` | PostgreSQL 外部数据包装器(如已安装则跳过) |
| etl_feiqiu | 外部服务器 | `zqyy_app_server` | 指向 `zqyy_app` 业务库host/port 按环境配置) |
| test_etl_feiqiu | 外部服务器 | `test_zqyy_app_server` | 指向 `test_zqyy_app` 测试业务库 |
| etl_feiqiu / test_etl_feiqiu | 用户映射 | `etl_user → app_reader` | ETL 连接角色映射到业务库只读角色 |
| etl_feiqiu / test_etl_feiqiu | Schema | `fdw_app` | 存放来自业务库的外部表 |
| etl_feiqiu / test_etl_feiqiu | 外部表 | `fdw_app.member_retention_clue` | 映射 `public.member_retention_clue` |
### 2.2 外部表列定义
| 列名 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 自增主键 |
| member_id | BIGINT | 会员 ID |
| category | VARCHAR(20) | 线索大类6 值枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈) |
| summary | VARCHAR(200) | 摘要 |
| detail | TEXT | 详情 |
| recorded_by_assistant_id | BIGINT | 记录助教 ID |
| recorded_by_name | VARCHAR(50) | 记录助教姓名 |
| recorded_at | TIMESTAMPTZ | 记录时间 |
| site_id | BIGINT | 门店 ID |
> **注意**:业务库侧 `member_retention_clue` 已于 2026-02-27 新增 `source VARCHAR(20)` 列(线索来源:`manual` / `ai_consumption` / `ai_note`),但当前 FDW 外部表定义(`db/fdw/setup_fdw_reverse*.sql`)尚未包含此列。如 ETL 任务需要读取 `source` 字段,需更新外部表定义并重新执行部署脚本。
### 2.3 角色与权限
| 角色 | 所在库 | 用途 |
|------|--------|------|
| `etl_user` | etl_feiqiu / test_etl_feiqiu | ETL 连接角色,通过 FDW 只读访问业务库数据 |
| `app_reader` | zqyy_app / test_zqyy_app | 业务库只读角色,供 FDW 用户映射使用 |
权限配置:
| 角色 | Schema | 权限 |
|------|--------|------|
| `etl_user` | `fdw_app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES`(自动授权未来新增外部表) |
---
## 3. 与正向 FDW 的对比
| 维度 | 正向 FDW`setup_fdw.sql` | 反向 FDW`setup_fdw_reverse.sql` |
|------|---------------------------|-------------------------------------|
| 执行位置 | `zqyy_app` 业务库 | `etl_feiqiu` ETL 库 |
| 数据方向 | 业务库读取 ETL 数据 | ETL 库读取业务库数据 |
| 目标 Schema | `fdw_etl`35 张外部表) | `fdw_app`1 张外部表) |
| 导入方式 | `IMPORT FOREIGN SCHEMA app`(批量) | `CREATE FOREIGN TABLE`(逐表定义) |
| 消费方 | 后端 API | ETL DWS 任务 |
| 文档 | `BD_Manual_fdw_etl_setup.md` | 本文档 |
---
## 4. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL DWS 任务 | 可通过 `fdw_app.member_retention_clue` 只读访问维客线索数据。当前无 DWS 任务直接消费此表(原 `member_birthday_manual` 的生日读取逻辑已移除,生日仅从 `dim_member.birthday` 读取) |
| 后端 API | 无影响。后端直接写入 `zqyy_app.public.member_retention_clue`,不经过 FDW |
| 小程序 | 无影响。小程序通过后端 API 间接访问 |
| 管理后台 | 无影响 |
| 正向 FDW`fdw_etl` | 无影响。两个方向的 FDW 配置完全独立 |
| 业务库 `member_retention_clue` 表 | 无影响。FDW 为只读映射,不修改源表 |
### 幂等性说明
`CREATE FOREIGN TABLE IF NOT EXISTS` 确保重复执行不会报错。如需更新列定义(如添加 `source` 列),需先 `DROP FOREIGN TABLE` 再重建,或使用 `ALTER FOREIGN TABLE ADD COLUMN`
---
## 5. 回滚策略
### 5.1 完整回滚(按逆序执行)
```sql
-- 在 etl_feiqiu生产中执行
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user;
REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user;
REVOKE USAGE ON SCHEMA fdw_app FROM etl_user;
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
DROP SCHEMA IF EXISTS fdw_app CASCADE;
DROP USER MAPPING IF EXISTS FOR etl_user SERVER zqyy_app_server;
DROP SERVER IF EXISTS zqyy_app_server CASCADE;
-- 注意:如果其他外部表也使用 postgres_fdw不要执行 DROP EXTENSION
```
```sql
-- 在 test_etl_feiqiu测试中执行
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user;
REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user;
REVOKE USAGE ON SCHEMA fdw_app FROM etl_user;
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
DROP SCHEMA IF EXISTS fdw_app CASCADE;
DROP USER MAPPING IF EXISTS FOR etl_user SERVER test_zqyy_app_server;
DROP SERVER IF EXISTS test_zqyy_app_server CASCADE;
```
### 5.2 仅删除外部表(保留 FDW 基础设施)
```sql
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
```
回滚无数据丢失风险:外部表不存储数据,仅为远程表的映射定义。
---
## 6. 验证 SQL
以下 SQL 在 ETL 库(`etl_feiqiu``test_etl_feiqiu`)中执行:
```sql
-- 1. 确认 postgres_fdw 扩展已安装
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'postgres_fdw';
-- 预期1 行
-- 2. 确认外部服务器存在
-- 生产:
SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'zqyy_app_server';
-- 测试:
SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'test_zqyy_app_server';
-- 预期1 行srvoptions 包含正确的 host/dbname/port
-- 3. 确认用户映射存在
SELECT um.umid, r.rolname AS local_role, s.srvname, um.umoptions
FROM pg_user_mappings um
JOIN pg_foreign_server s ON s.srvname = um.srvname
JOIN pg_roles r ON r.rolname = um.usename
WHERE s.srvname IN ('zqyy_app_server', 'test_zqyy_app_server')
AND r.rolname = 'etl_user';
-- 预期1 行
-- 4. 确认 fdw_app schema 存在
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = 'fdw_app';
-- 预期1 行
-- 5. 确认外部表列结构完整(当前 9 列)
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'fdw_app'
AND table_name = 'member_retention_clue'
ORDER BY ordinal_position;
-- 预期9 行id, member_id, category, summary, detail,
-- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id
-- 6. 确认外部表在 information_schema.foreign_tables 中注册
SELECT foreign_table_schema, foreign_table_name, foreign_server_name
FROM information_schema.foreign_tables
WHERE foreign_table_schema = 'fdw_app'
AND foreign_table_name = 'member_retention_clue';
-- 预期1 行
-- 7. 确认 etl_user 对 fdw_app 有 USAGE 权限
SELECT has_schema_privilege('etl_user', 'fdw_app', 'USAGE') AS has_usage;
-- 预期true
-- 8. 确认外部表可读取(需业务库侧表已存在且网络连通)
SELECT COUNT(*) FROM fdw_app.member_retention_clue;
-- 预期:返回行数(可能为 0
-- 9. 确认 ALTER DEFAULT PRIVILEGES 已设置
SELECT n.nspname AS schema_name, d.defaclacl AS default_acl
FROM pg_default_acl d
JOIN pg_namespace n ON n.oid = d.defaclnamespace
WHERE n.nspname = 'fdw_app';
-- 预期1 行default_acl 包含 etl_user 的 SELECT 权限
```
---
## 7. 已知差异与待办
| 项目 | 状态 | 说明 |
|------|------|------|
| `source` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `source VARCHAR(20) NOT NULL DEFAULT 'manual'`2026-02-27FDW 外部表定义未包含。当前无 ETL 任务需要此字段,但未来如需读取线索来源需先更新外部表 |
| DWS 任务消费 | 📋 待规划 | 原 `member_birthday_manual` 的 DWS 消费逻辑已移除。维客线索的 DWS 聚合任务尚未规划 |
### source 列同步方法(备用)
如需同步 `source` 列,在 ETL 库中执行:
```sql
-- 方法 1ALTER 追加列(推荐,无需重建)
ALTER FOREIGN TABLE fdw_app.member_retention_clue
ADD COLUMN source VARCHAR(20);
-- 方法 2DROP + 重建(完整重置)
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
CREATE FOREIGN TABLE fdw_app.member_retention_clue (
id BIGINT,
member_id BIGINT,
category VARCHAR(20),
summary VARCHAR(200),
detail TEXT,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ,
site_id BIGINT,
source VARCHAR(20)
) SERVER zqyy_app_server
OPTIONS (schema_name 'public', table_name 'member_retention_clue');
```
同步后需更新 `db/fdw/setup_fdw_reverse.sql``db/fdw/setup_fdw_reverse_test.sql` 中的外部表定义。
---
## 8. 关联文件
| 文件 | 说明 |
|------|------|
| `db/fdw/setup_fdw_reverse.sql` | 生产环境部署脚本(在 `etl_feiqiu` 中执行) |
| `db/fdw/setup_fdw_reverse_test.sql` | 测试环境部署脚本(在 `test_etl_feiqiu` 中执行) |
| `docs/database/BD_Manual_member_retention_clue.md` | 业务库侧 `member_retention_clue` 表结构文档 |
| `docs/database/BD_Manual_fdw_etl_setup.md` | 正向 FDW 配置文档(方向相反) |
| `docs/database/BD_Manual_app_schema_rls_views.md` | ETL 库 `app` Schema RLS 视图层文档 |
| `db/zqyy_app/migrations/2026-02-26__refactor_birthday_to_retention_clue.sql` | 业务库侧建表迁移脚本 |

View File

@@ -1187,3 +1187,34 @@ SELECT site_id,
FROM core.dim_site s;
;
-- 2026-03-19 新增BOARD 看板所需的 3 个 RLS 视图
-- 迁移脚本db/etl_feiqiu/migrations/2026-03-19_add_board_rls_views.sql
CREATE OR REPLACE VIEW app.v_dws_assistant_project_tag AS
SELECT id, site_id, tenant_id, assistant_id, time_window,
category_code, category_name, short_name,
duration_seconds, total_seconds, percentage, is_tagged,
computed_at, created_at, updated_at
FROM dws.dws_assistant_project_tag
WHERE site_id = current_setting('app.current_site_id')::bigint;
CREATE OR REPLACE VIEW app.v_dws_member_project_tag AS
SELECT id, site_id, tenant_id, member_id, time_window,
category_code, category_name, short_name,
duration_seconds, total_seconds, percentage, is_tagged,
computed_at, created_at, updated_at
FROM dws.dws_member_project_tag
WHERE site_id = current_setting('app.current_site_id')::bigint;
CREATE OR REPLACE VIEW app.v_dws_member_spending_power_index AS
SELECT spi_id, site_id, member_id,
spend_30, spend_90, recharge_90,
orders_30, orders_90, visit_days_30, visit_days_90,
avg_ticket_90, active_weeks_90, daily_spend_ewma_90,
score_level_raw, score_speed_raw, score_stability_raw,
score_level_display, score_speed_display, score_stability_display,
raw_score, display_score,
calc_time, created_at, updated_at
FROM dws.dws_member_spending_power_index
WHERE site_id = current_setting('app.current_site_id')::bigint;

View File

@@ -481,6 +481,12 @@ CREATE TABLE dws.dws_finance_recharge_summary (
total_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
cash_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_card_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_liquor_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_table_fee_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_voucher_balance numeric(14,2) DEFAULT 0 NOT NULL,
gift_liquor_recharge numeric(14,2) DEFAULT 0 NOT NULL,
gift_table_fee_recharge numeric(14,2) DEFAULT 0 NOT NULL,
gift_voucher_recharge numeric(14,2) DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);

View File

@@ -92,7 +92,8 @@ CREATE TABLE biz.notes (
ai_score smallint,
ai_analysis text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
updated_at timestamp with time zone DEFAULT now(),
score smallint
);
CREATE TABLE biz.trigger_jobs (

View File

@@ -1,545 +0,0 @@
# 小程序后端接口输出规范
> **文档编号**DS-API-OUTPUT-001
> **版本**v1.0.0
> **适用范围**NeoZQYY 小程序所有后端接口HTTP JSON 响应体)
> **最后更新**2026-03-18
> **关联文档**
> - [DISPLAY-STANDARDS.md](./design-system/DISPLAY-STANDARDS.md) — 前端展示规范第 16 章
> - [DISPLAY-STANDARDS-2.md](./design-system/DISPLAY-STANDARDS-2.md) — 第 79 章(截止日期 / 评分 / Mock
> - [DATETIME-DISPLAY-STANDARD.md](./design-system/DATETIME-DISPLAY-STANDARD.md) — 时间展示规范
---
## 目录
1. [总体约定](#1-总体约定)
2. [响应体结构](#2-响应体结构)
3. [字段类型规范(核心规则)](#3-字段类型规范核心规则)
4. [各业务字段细则](#4-各业务字段细则)
5. [枚举值规范](#5-枚举值规范)
6. [分页规范](#6-分页规范)
7. [接口端点命名规范](#7-接口端点命名规范)
8. [各页面接口字段对照表](#8-各页面接口字段对照表)
9. [禁止事项速查](#9-禁止事项速查)
---
## 1. 总体约定
| 项目 | 规范 |
|------|------|
| 协议 | HTTPS REST JSON |
| 编码 | UTF-8 |
| Content-Type | `application/json` |
| 时区 | **后端统一输出 UTC前端负责本地化展示** |
| 数值精度 | 金额为整数(元),课时为 0.5 步长浮点 |
| 空值 | 字段不存在时输出 `null`**不得省略字段** |
| 布尔值 | `true` / `false`,不用 `0/1` |
| 字符串编码格式 | 纯数据,**不含展示单位**¥、h、%、笔等) |
---
## 2. 响应体结构
### 2.1 成功响应
```json
{
"code": 0,
"message": "success",
"data": { ... }
}
```
### 2.2 失败响应
```json
{
"code": 40001,
"message": "参数错误:缺少 customerId",
"data": null
}
```
### 2.3 分页列表响应
```json
{
"code": 0,
"message": "success",
"data": {
"list": [ ... ],
"total": 32,
"page": 1,
"pageSize": 20,
"hasMore": true
}
}
```
---
## 3. 字段类型规范(核心规则)
> **核心原则:后端输出原始语义数值,前端负责格式化展示。**
> 后端不拼接 `¥`、`h`、`%`、`笔` 等展示字符,也不做千分位格式化。
### 3.1 类型对照表
| 字段类型 | 后端输出类型 | 示例 | 前端处理函数 | 禁止输出 |
|----------|-------------|------|-------------|----------|
| **金额** | `number`(元,整数) | `12680` | `formatMoney()` | `"¥12,680"` `"12680.00"` |
| **课时** | `number`小时0.5步长) | `2.5` | `formatHours()` | `"2.5h"` `"2h30min"` |
| **计数** | `number`(整数) | `32` | `formatCount()` | `"32笔"` `"32次"` |
| **百分比** | `number`0100整数或1位小数 | `85.5` | `formatPercent()` | `"85.5%"` `"0.855"` |
| **日期** | `string``YYYY-MM-DD` | `"2026-03-18"` | `formatDeadline()` | `"2026/03/18"` `1742256000000` |
| **时间戳** | `string`ISO 8601含时区 | `"2026-03-18T08:30:00+08:00"` | `formatRelativeTime()` | Unix 毫秒整数 |
| **等级 key** | `string`(英文 key | `"star"` | 查 `LEVEL_MAP` | `"星级"` `"gold"` |
| **状态 key** | `string`(英文 key | `"pending"` | 查枚举映射 | `"待处理"` |
| **评分** | `number`0100.5步长) | `8.5` | `scoreToHalfStar()` | `"8.5分"` `4` (5分制) |
| **布尔** | `boolean` | `true` | 直接使用 | `1` `"true"` |
| **空值** | `null` | `null` | 展示 `--` | `""` `0`(有语义时除外) |
---
## 4. 各业务字段细则
### 4.1 金额字段
```json
// 正确
{ "balance": 2000, "income": 510, "salary": 12680 }
// 错误
{ "balance": "¥2,000", "income": "¥510", "salary": "¥12,680" }
```
- 统一以「元」为单位,**不含分**
- 负数直接输出负整数:`-368`
- 零值输出 `0`,不输出 `null`
- 前端调用 `formatMoney(value)` 展示
### 4.2 课时字段
```json
// 正确
{ "hours": 2.5, "hoursRaw": 3.0 }
// 错误
{ "hours": "2.5h", "deduct": "(折0.5h)" }
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `hours` | `number` | 折算后实际计入课时(小时) |
| `hoursRaw` | `number \| null` | 折算前原始小时数;无折算时输出 `null` |
- 前端仅在 `hoursRaw !== null && hoursRaw !== hours` 时展示折算备注
- 步长为 0.5,如 `1.0``1.5``2.0``2.5`
### 4.3 计数字段
```json
// 正确
{ "totalCount": 32, "serviceCount": 18 }
// 错误
{ "totalCount": "32笔", "serviceCount": "18次" }
```
- 前端调用 `formatCount(value, '笔')` 展示,单位由页面层决定
### 4.4 百分比字段
```json
// 正确(进度/完成率)
{ "filledPct": 85, "completionRate": 72.5 }
// 错误
{ "filledPct": "85%", "completionRate": 0.725 }
```
- 统一 0100 整数或1位小数**不用小数形式**
- 允许超过 100如完成率 120%
### 4.5 日期与时间
| 场景 | 格式 | 示例 |
|------|------|------|
| 纯日期(截止日、生日等) | `YYYY-MM-DD` | `"2026-03-18"` |
| 精确时间(备注时间、服务记录) | ISO 8601 含时区 | `"2026-03-18T08:30:00+08:00"` |
| 时间区间service timeRange | 直接拼接字符串 | `"20:00-22:00"` |
- **禁止** 输出 Unix 毫秒时间戳整数
- **禁止** 输出 `YYYY/MM/DD` 格式
- 前端截止日期调用 `formatDeadline(date)` 语义化
- 前端相对时间调用 `formatRelativeTime(iso)` 展示
### 4.6 评分字段
```json
// 正确
{ "heartScore": 8.5, "noteScore": 7.0 }
// 错误
{ "heartScore": 4, "noteScore": "7.5分" }
```
- 统一使用 **010 分制**0.5 步长
- 0 表示「未评分」,前端展示 `--`
- 前端调用 `scoreToHalfStar(score)` 转换为 05 星展示
### 4.7 等级字段
```json
// 正确
{ "level": "star" }
// 错误
{ "level": "星级" }
```
| 后端 key | 前端展示 |
|----------|----------|
| `star` | 星级助教 |
| `senior` | 高级助教 |
| `middle` | 中级助教 |
| `junior` | 初级助教 |
### 4.8 天数字段
```json
// 正确
{ "lastVisitDays": 23, "daysAbsent": 23 }
// 错误
{ "lastVisitDays": "23天前", "daysAbsent": "23天" }
```
- 统一输出整数天数,不含「天」「天前」等汉字
- 前端拼接文案时自行添加单位
---
## 5. 枚举值规范
所有枚举字段统一使用英文下划线 key前端维护映射表。
### 5.1 任务类型 `taskType`
| key | 展示文案 |
|-----|----------|
| `callback` | 回访 |
| `priority_recall` | 优先召回 |
| `high_priority` | 高优先召回 |
| `relationship` | 关系构建 |
### 5.2 任务状态 `status`
| key | 展示文案 |
|-----|----------|
| `pending` | 待处理 |
| `completed` | 已完成 |
| `abandoned` | 已放弃 |
### 5.3 助教等级 `level`
| key | 展示文案 |
|-----|----------|
| `star` | 星级助教 |
| `senior` | 高级助教 |
| `middle` | 中级助教 |
| `junior` | 初级助教 |
### 5.4 课程类型 `courseType`
| key | 展示文案 | 样式 key |
|-----|----------|----------|
| `basic` | 基础课 | `tag-basic` |
| `vip` | 包厢课 | `tag-vip` |
| `tip` | 打赏课 | `tag-tip` |
| `recharge` | 充值 | `tag-recharge` |
| `incentive` | 激励 | `tag-incentive` |
### 5.5 线索来源 `source`
| key | 展示文案 |
|-----|----------|
| `manual` | 助教手动录入 |
| `ai_consumption` | 系统自动(消费分析) |
| `ai_note` | 系统自动(备注分析) |
### 5.6 线索分类 `category`
| key | 展示文案 |
|-----|----------|
| `customer_basic` | 客户基础 |
| `consumption` | 消费习惯 |
| `play_pref` | 玩法偏好 |
| `promo_pref` | 促销偏好 |
| `social` | 社交偏好 |
| `feedback` | 重要反馈 |
### 5.7 截止日期语义化(前端计算,非接口字段)
后端只输出 `deadline: "YYYY-MM-DD"`,前端调用 `formatDeadline()` 计算:
| diff | 展示文案 | 样式 |
|------|----------|------|
| < 0 | `逾期 N 天` | `danger`(红) |
| = 0 | `今天到期` | `warning`(橙) |
| 17 | `还剩 N 天` | `normal` |
| > 7 | `MM-DD` | `muted`(灰) |
| null | `--` | `muted` |
---
## 6. 分页规范
```json
{
"list": [],
"total": 100,
"page": 1,
"pageSize": 20,
"hasMore": true
}
```
- `page` 从 1 开始计数
- `hasMore` 由后端根据 `total` 和当前 `page * pageSize` 计算
- `total` 为全量数据总条数(不是当前页条数)
---
## 7. 接口端点命名规范
```
GET /api/xcx/tasks 任务列表
GET /api/xcx/tasks/:id 任务详情
GET /api/xcx/tasks/:id/notes 任务备注列表
POST /api/xcx/tasks/:id/notes 新增备注
GET /api/xcx/tasks/:id/service-records 近期服务记录
GET /api/xcx/coaches 助教看板列表
GET /api/xcx/coaches/:id 助教详情
GET /api/xcx/performance 绩效概览
GET /api/xcx/performance/records 绩效记录列表(支持月份分页)
GET /api/xcx/retention-clues/:customerId 维客线索
```
- 资源名用复数小写连字符
- 路径参数用 `:id`,不用查询参数代替
- 月份筛选用 `?year=2026&month=2`
---
## 8. 各页面接口字段对照表
### 8.1 任务列表页 `GET /api/xcx/tasks`
```json
{
"id": "task-001",
"customerName": "王先生",
"taskType": "high_priority",
"taskTypeLabel": "高优先召回",
"deadline": "2026-03-20",
"heartScore": 8.5,
"isPinned": true,
"hasNote": true,
"status": "pending",
"lastVisitDays": 23,
"balance": 2000,
"filledPct": 85,
"aiSuggestion": "建议本周内回访,余额即将到期"
}
```
**说明:**
- `lastVisitDays`integer距上次到店天数null 表示从未到店
- `balance`integer余额null 表示无储值
- `filledPct`integer0100关系进度百分比
- `deadline``YYYY-MM-DD`,前端调用 `formatDeadline()` 语义化
- `aiSuggestion`stringAI 摘要文案,后端生成,前端直接展示
### 8.2 任务详情页 `GET /api/xcx/tasks/:id`
```json
{
"id": "task-001",
"customerName": "王先生",
"taskType": "high_priority",
"taskTypeLabel": "高优先召回",
"status": "pending",
"deadline": "2026-03-20",
"heartScore": 8.5,
"phone": "138****5678",
"lastVisitDate": "2026-03-01",
"lastSpendAmount": 380,
"daysAbsent": 23,
"callbackReason": "上次体验课后未续费,需跟进意向",
"churnRisk": "high",
"spendTrend": "down",
"aiAnalysis": {
"summary": "最近 3 个月每周均有 1-2 次课程互动",
"suggestions": ["询问近期是否有空", "告知会员专属活动"]
},
"talkingPoints": ["王哥好久不见..."],
"notes": [
{
"id": "note-001",
"content": "已通过微信联系王先生...",
"tagType": "customer",
"tagLabel": "客户:王先生",
"createdAt": "2026-03-10T16:30:00+08:00",
"score": 8.5
}
],
"serviceRecords": [
{
"table": "A12号台",
"type": "基础课",
"typeClass": "basic",
"recordType": "course",
"hours": 2.5,
"hoursRaw": 3.0,
"income": 200,
"isEstimate": true,
"drinks": "百威x2 红牛x1",
"date": "2026-02-07T21:30:00+08:00"
}
],
"serviceSummary": {
"totalHours": 6.0,
"totalIncome": 510,
"avgIncome": 170
}
}
```
**说明:**
- `phone`:后端已做脱敏处理(`138****5678`),或根据权限返回明文
- `score`010 分制0 表示未评分
- `serviceRecords.hours``hoursRaw`number小时前端调用 `formatHours()`
- `serviceRecords.income``serviceSummary.*`integer前端调用 `formatMoney()`
- `isEstimate`booleantrue 时前端展示「预估」标注
### 8.3 助教看板页 `GET /api/xcx/coaches`
```json
{
"id": "coach-001",
"name": "小燕",
"level": "star",
"storeName": "朗朗桌球",
"customerCount": 28,
"monthHours": 86.0,
"monthIncome": 18600,
"svAmount": 5000,
"filledPct": 92,
"taskCount": 6,
"urgentTaskCount": 2,
"aiSuggestion": "本月表现优秀,建议重点跟进 2 位高风险流失客户"
}
```
**说明:**
- `level`:英文 key前端查 `LEVEL_MAP` 展示中文
- `monthHours`number小时前端调用 `formatHours()`
- `monthIncome``svAmount`integer前端调用 `formatMoney()`
- `filledPct`integer0100前端调用 `formatPercent()``toProgressWidth()`
- `customerCount``taskCount``urgentTaskCount`integer前端调用 `formatCount()`
### 8.4 绩效记录页 `GET /api/xcx/performance/records`
请求参数:`?year=2026&month=2&page=1&pageSize=20`
```json
{
"summary": {
"totalCount": 32,
"totalHours": 59.0,
"totalIncome": 4720
},
"dateGroups": [
{
"date": "2026-02-07",
"totalHours": 6.0,
"totalIncome": 510,
"records": [
{
"id": "r1",
"customerName": "王先生",
"timeRange": "20:00-22:00",
"hours": 2.0,
"hoursRaw": null,
"courseType": "basic",
"location": "3号台",
"income": 160
}
]
}
],
"hasMore": false,
"page": 1,
"pageSize": 20
}
```
**说明:**
- `date``YYYY-MM-DD`前端格式化为「2月7日」
- `summary.*``dateGroups.totalIncome``records.income`integer
- `summary.totalHours``dateGroups.totalHours``records.hours`number小时
- `hoursRaw`null 表示无折算,有值时前端展示折算备注
- `courseType`:英文 key`basic`/`vip`/`tip`/`recharge`/`incentive`
### 8.5 备注接口 `GET /api/xcx/tasks/:id/notes`
```json
[
{
"id": "note-001",
"content": "已通过微信联系王先生...",
"tagType": "customer",
"tagLabel": "客户:王先生",
"createdAt": "2026-03-10T16:30:00+08:00",
"score": 8.5
}
]
```
- `tagType``customer` / `coach` / `system`
- `score`0 表示未评分0.5 步长,前端调用 `scoreToHalfStar()` 转 5 星制
- `createdAt`ISO 8601前端调用 `formatRelativeTime()` 展示相对时间
---
## 9. 禁止事项速查
| 禁止行为 | 正确做法 |
|----------|----------|
| 输出 `"¥12,680"` | 输出 `12680` |
| 输出 `"2.5h"` | 输出 `2.5` |
| 输出 `"32笔"` | 输出 `32` |
| 输出 `"85%"` | 输出 `85` |
| 输出 `"星级"` 作为等级 | 输出 `"star"` |
| 输出 `"待处理"` 作为状态 | 输出 `"pending"` |
| 省略值为 null 的字段 | 显式输出 `null` |
| 用 `0` 代替 `null` 表示「无」 | 用 `null`0 保留给有意义的零值) |
| 输出 Unix 毫秒时间戳 | 输出 ISO 8601 字符串 |
| 输出 `YYYY/MM/DD` 日期格式 | 输出 `YYYY-MM-DD` |
| 后端做 `toLocaleString()` 格式化 | 后端输出原始数值,前端格式化 |
| `hoursRaw` 输出 `"(折0.5h)"` | 输出 `3.0`(原始小时数) |
---
## 关联文档
- [DISPLAY-STANDARDS.md](./design-system/DISPLAY-STANDARDS.md) — 前端格式化规范
- [DISPLAY-STANDARDS-2.md](./design-system/DISPLAY-STANDARDS-2.md) — 截止日期 / 评分 / Mock 规范
- [DATETIME-DISPLAY-STANDARD.md](./design-system/DATETIME-DISPLAY-STANDARD.md) — 时间展示规范
- `utils/money.ts``formatMoney` / `formatCount` / `formatPercent` / `toProgressWidth`
- `utils/time.ts``formatHours` / `formatDeadline` / `formatRelativeTime`
- `utils/rating.ts``scoreToHalfStar` / `isUnrated`
- `utils/mock-data.ts` — Mock 数据(字段结构与接口保持一致)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
# 小程序前端 → 后端接口需求记录
> 记录小程序前端在硬编码审计中发现的、需要后端配合的接口需求。
> 创建时间2026-03-18
---
## REQ-1技能类型配置接口
- 来源页面:`board-coach`
- 当前状态:`SKILL_OPTIONS` 硬编码 5 项(全部/中🎱/🎯斯诺克/小组课/打赏课)
- 需求:门店技能类型应可配置,需要后端提供配置接口
- 建议接口:`GET /api/xcx/config/skill-types`
- 返回格式:`{ skills: Array<{ value: string; text: string }> }`
- 优先级P2联调阶段处理
---
## REQ-2对话图标颜色固定化
- 来源页面:`chat-history`
- 当前状态:`iconGradient` 每次渲染随机分配颜色,刷新后颜色会变
- 需求:颜色应按对话 ID 哈希固定,无需后端配合
- 前端方案:已改为按 `id` 字符串哈希取模,从 6 色渐变数组中固定选取
- 状态:✅ 已完成(前端自行处理)
---
## REQ-3开发模式 openid 上线安全
- 来源页面:`login`
- 当前状态:开发模式使用固定 `dev_test_openid`,通过 `API_BASE.startsWith("http://127.0.0.1")` 判断
- 风险:如果 `isDevMode` 判断逻辑被绕过,可能存在安全隐患
- 需求:上线前确保后端 `/api/xcx/dev-login` 接口在生产环境不可用
- 建议:后端通过环境变量控制 dev-login 路由注册,生产环境不注册该路由
- 优先级P0上线前必须处理
---
## REQ-4`authUser` 字段扩展
- 来源页面:`performance-records``my-profile``coach-detail`
- 当前状态:`globalData.authUser` 仅有 `userId``status``nickname`
- 需求:多个页面需要展示当前用户的角色、门店名、助教等级等信息
- 建议:`GET /api/xcx/me` 返回扩展字段,前端存入 `authUser`
- 新增字段:
| 字段 | 类型 | 说明 | 来源 |
|------|------|------|------|
| `role` | `string` | 用户角色(如"助教" | 后端用户表 |
| `storeName` | `string` | 所属门店名称 | 后端门店表 |
| `coachLevel` | `string?` | 助教等级junior/middle/senior/star | 后端助教表(仅助教角色有) |
| `avatar` | `string?` | 用户头像 URL | 后端用户表 |
- 优先级P1联调阶段处理
---
## REQ-5筛选选项全平台共用
- 来源页面:`board-coach``board-customer`
- 当前状态:`SORT_OPTIONS`6 项排序维度)、`TIME_OPTIONS`6 项时间范围)、维度选项等硬编码在前端
- 决定:这些筛选选项全平台共用,暂保持前端硬编码
- 后续:如果需要动态调整,再改为后端配置接口
- 状态:🔒 暂不处理

View File

@@ -0,0 +1,159 @@
# 小程序全页面硬编码汇总与处置方案
> 生成时间2026-03-18 | 最后更新2026-03-18
> 覆盖页面18 个(全部业务页面)
---
## 一、分类说明
| 类别 | 说明 | 行动 |
|------|------|------|
| ✅ 已修复 | 已完成修改 | — |
| 📋 已决策-暂不动 | 用户已确认处置方式,当前不改 | 联调时处理 |
| 🔒 保留 | 合理的 UI 文案/默认值/样式常量 | 不动 |
---
## 二、✅ 已修复的硬编码
### 2.1 时间/日期硬编码 → 动态计算 ✅
| # | 页面 | 字段 | 原值 | 修改结果 |
|---|------|------|------|----------|
| 1 | customer-service-records | `currentYear` | `2026` | `new Date().getFullYear()` |
| 2 | customer-service-records | `currentMonth` | `2` | `new Date().getMonth() + 1` |
| 3 | customer-service-records | `maxYearMonth` | `202602` | 动态计算当前年月 |
| 4 | customer-service-records | `minYearMonth` | `202601` | 动态计算(当前年月 - 12 个月) |
| 5 | performance-records | `currentYear` | `2026` | `new Date().getFullYear()` |
| 6 | performance-records | `currentMonth` | `2` | `new Date().getMonth() + 1` |
| 7 | performance-records | `monthLabel` | `'2026年2月'` | 动态拼接 |
### 2.2 用户信息 → 从 globalData.authUser 读取 ✅
| # | 页面 | 字段 | 原值 | 修改结果 |
|---|------|------|------|----------|
| 8 | performance-records | `coachName` | `'小燕'` | `onLoad()``app.globalData.authUser.nickname` 读取 |
| 9 | performance-records | `coachLevel` | `'星级'` | `onLoad()``app.globalData.authUser.coachLevel` 读取 |
| 10 | performance-records | `storeName` | `'球会名称店'` | `onLoad()``app.globalData.authUser.storeName` 读取 |
| 11 | my-profile | `mockUserProfile` | `{name:'小燕',...}` | `onShow()``app.globalData.authUser` 读取,删除 mock import |
### 2.3 全局 UI 常量抽取 ✅
| # | 页面 | 原代码 | 修改结果 |
|---|------|--------|----------|
| 12 | my-profile | `confirmColor: '#e34d59'` | `CONFIRM_DANGER_COLOR`from `ui-constants.ts` |
| 13 | task-detail | `confirmColor: '#e34d59'` | `CONFIRM_DANGER_COLOR`from `ui-constants.ts` |
| 14 | notes | `confirmColor: '#e34d59'` | `CONFIRM_DANGER_COLOR`from `ui-constants.ts` |
> 新建 `utils/ui-constants.ts``CONFIRM_DANGER_COLOR` 引用 `vi-colors.ts` 的 `GLOBAL_COLORS.error`
### 2.4 死代码 / 无效路由清理 ✅
| # | 页面 | 问题 | 修改结果 |
|---|------|------|----------|
| 15 | customer-detail | 未使用的 `mockCustomerDetail` import | 已删除 |
| 16 | apply | 无效路由 `/pages/mvp/mvp` | → `/pages/task-list/task-list` |
| 17 | no-permission | 无效路由 `/pages/mvp/mvp` | → `/pages/task-list/task-list` |
| 18 | reviewing | 无效路由 `/pages/mvp/mvp` | → `/pages/task-list/task-list` |
| 19 | router | 死路由 `'settings': ''` | 已删除 |
### 2.5 交互优化 ✅
| # | 页面 | 问题 | 修改结果 |
|---|------|------|----------|
| 20 | chat-history | `iconGradient` 随机分配 | 改为按对话 ID 哈希固定(`hashIndex()` 函数) |
### 2.6 类型定义扩展 ✅
| # | 文件 | 修改 |
|---|------|------|
| 21 | `typings/index.d.ts` | `authUser` 接口新增 `role?``storeName?``coachLevel?``avatar?` 字段 |
---
## 三、📋 已决策-暂不动
### 3.1 Mock 数据(联调时替换)
| # | 页面 | 字段 | 用户决策 |
|---|------|------|----------|
| 1 | customer-service-records | `customerPhone`/`relationIndex` 等 | Mock 数据,联调时从接口取 |
| 2 | performance-records | mock 数据不一致totalCount/totalHours/totalIncome | 联调时 API 替换,暂不修 |
### 3.2 筛选选项(全平台共用,暂前端硬编码)
| # | 页面 | 常量 | 用户决策 |
|---|------|------|----------|
| 3 | board-coach | `SKILL_OPTIONS` | 后端需配置接口(已记录 REQ-1 |
| 4 | board-coach | `SORT_OPTIONS` | 全平台共用,暂保持 |
| 5 | board-customer | 维度/项目选项 | 全平台共用,暂保持 |
### 3.3 其他已决策项
| # | 页面 | 项目 | 用户决策 |
|---|------|------|----------|
| 6 | no-permission | 管理员姓名 `厉超` | 保持硬编码(只有一个管理员) |
| 7 | login | `dev_test_openid` | 上线前处理(已记录 REQ-3 |
> 后端接口需求已记录于 `docs/miniprogram-dev/API-requirement.md`REQ-1 ~ REQ-5
---
## 四、🔒 保留不动的硬编码
以下硬编码属于合理的 UI 文案、样式常量或默认值,无需修改:
### 4.1 UI 文案(各页面通用)
所有页面的以下文案保留不动:
- 加载态:`加载中...`
- 错误态:`加载失败,请重试` / `重新加载` / `重试`
- 空态:`暂无数据` / `暂无备注记录` / `暂无对话记录` / `暂无助教数据`
- 底部提示:`— 已加载全部记录 —`
- 导航栏标题(各页面 `.json` 中的 `navigationBarTitleText`
- 业务术语标签:`定档` / `折前` / `预估` / `召回` / `回访`
### 4.2 `statusBarHeight` 回退值
所有页面的 `statusBarHeight` 默认值(`20``44`)作为 `wx.getWindowInfo()` 的兜底值,保留不动。
### 4.3 `setTimeout` 模拟延迟
所有页面的 `setTimeout(400~600ms)` 模拟网络延迟,联调时统一替换为真实 API 调用。当前阶段保留不动(改了也没意义,联调时整体重构 `loadData()`)。
### 4.4 路由路径
各页面中的 `wx.navigateTo` / `wx.reLaunch` / `wx.switchTab` 路径硬编码,属于小程序标准做法,保留不动。
### 4.5 样式映射常量
- `board-coach`: `LEVEL_CLASS` / `SKILL_CLASS` / `SORT_TO_DIM` / `TIME_OPTIONS`
- `chat-history`: `ICON_GRADIENTS`6 组 CSS 渐变色)
- 各页面的 `courseTypeClass` / `avatarGradient` 等样式映射
### 4.6 表单校验规则
- `apply`: 手机号正则 `!/^\d{11}$/`、球房 ID `maxlength="5"`
- `login`: 错误提示文案(`账号已被禁用` / `登录凭证无效` 等)
### 4.7 Mock 数据本体
所有 `mockXxx` 数据(包括页面内联和 `mock-data.ts` 中的)在联调前保留不动。联调时统一替换为 API 调用。
---
## 五、变更统计
本轮共修复 21 项硬编码问题,涉及 14 个文件:
| 类别 | 数量 |
|------|------|
| 时间/日期动态化 | 7 |
| 用户信息 globalData 读取 | 4 |
| UI 常量抽取 | 3 |
| 死代码/无效路由清理 | 5 |
| 交互优化(哈希固定) | 1 |
| 类型定义扩展 | 1 |
涉及文件:`performance-records.ts``customer-service-records.ts``my-profile.ts``task-detail.ts``notes.ts``chat-history.ts``customer-detail.ts``apply.ts``no-permission.ts``reviewing.ts``router.ts``ui-constants.ts`(新建)、`vi-colors.ts`(已有)、`typings/index.d.ts`

View File

@@ -0,0 +1,134 @@
# apply 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/apply/apply
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 0 | 无 mock 数据 |
| B. 硬编码数据 | 5 | 表单校验规则、延时跳转、错误提示文案 |
| C. 已对接 API | 2 | `/api/xcx/me`GET`/api/xcx/apply`POST |
| D. 前端计算/派生数据 | 2 | statusBarHeight、submitting 状态 |
| E. 路由参数 | 0 | 无路由参数 |
| F. WXML 硬编码文案 | 16 | 标题、欢迎语、步骤条、表单标签、placeholder、提示语 |
---
## 一、Mock 数据
无。页面未使用任何 mock 数据或本地假数据。
---
## 二、硬编码数据
| # | 位置apply.ts 行号) | 内容 | 风险等级 | 说明 |
|---|----------------------|------|----------|------|
| B1 | L89 `!/^\d{11}$/.test(phone)` | 手机号正则:固定 11 位纯数字 | 🟡 低 | 仅适用于中国大陆手机号,若未来支持港澳台/海外号码需调整 |
| B2 | L85 `maxlength="5"`WXML | 球房ID最大长度 5 | 🟡 低 | 与后端 site_code 长度约束需保持一致 |
| B3 | L113 `setTimeout(() => {...}, 800)` | 提交成功后 800ms 延时跳转 | 🟢 无风险 | 纯 UX 延时,不影响业务逻辑 |
| B4 | L117-123 错误码映射 | `409 → "您已有待审核的申请"``422 → "表单信息有误,请检查"` | 🟡 低 | 错误文案硬编码在前端,后端 detail 字段优先使用 |
| B5 | L82-96 表单校验逻辑 | 4 条必填校验 + 手机号格式校验 | 🟢 无风险 | 标准前端校验,后端有独立校验 |
---
## 三、已对接 API
### API-1获取当前用户状态
| 项目 | 值 |
|------|-----|
| 端点 | `GET /api/xcx/me` |
| 调用位置 | `apply.ts``_checkAccess()` (L30) |
| 调用时机 | `onShow` 生命周期 |
| 请求参数 | 无(仅 Authorization header |
| 认证 | `needAuth: true`(自动附加 Bearer token |
| 响应字段 | `user_id``status`new/pending/approved/rejected/disabled`nickname` |
| 响应处理 | 写入 `app.globalData.authUser` + `wx.setStorageSync("userId"/"userStatus")`;根据 status 路由分发 |
| 错误处理 | catch 静默忽略(允许用户继续填表) |
**路由分发逻辑:**
| status | 行为 |
|--------|------|
| `new` | 留在当前页(允许填写申请表) |
| `rejected` | `reLaunch → /pages/no-permission/no-permission` |
| `approved` | `reLaunch → /pages/mvp/mvp` |
| `pending` | `reLaunch → /pages/reviewing/reviewing` |
| `disabled` | `reLaunch → /pages/no-permission/no-permission` |
### API-2提交入驻申请
| 项目 | 值 |
|------|-----|
| 端点 | `POST /api/xcx/apply` |
| 调用位置 | `apply.ts``onSubmit()` (L99) |
| 调用时机 | 用户点击"提交申请"按钮 |
| 认证 | `needAuth: true` |
| 请求参数 | 见下表 |
| 成功处理 | Toast "申请已提交" → 800ms 后 `reLaunch → /pages/reviewing/reviewing` |
| 错误处理 | 优先取 `err.data.detail`409 → "您已有待审核的申请"422 → "表单信息有误";其他 → "提交失败,请稍后重试" |
**请求参数:**
| 字段 | 来源 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `site_code` | 表单输入 `siteCode`trim | string | ✅ | 球房ID |
| `applied_role_text` | 表单输入 `role`trim | string | ✅ | 申请身份(自由文本) |
| `phone` | 表单输入 `phone` | string | ✅ | 11 位手机号 |
| `employee_number` | 表单输入 `employeeNumber`trim空则 undefined | string? | ❌ | 编号(选填) |
| `nickname` | 表单输入 `nickname`trim | string | ✅ | 昵称 |
---
## 四、前端计算/派生数据
| # | 数据 | 来源 | 位置 |
|---|------|------|------|
| D1 | `statusBarHeight` | `wx.getWindowInfo().statusBarHeight` | `onLoad()` L14 |
| D2 | `submitting` | 布尔状态锁,`onSubmit` 开始时设 truefinally 设 false | `onSubmit()` L98/L126 |
---
## 五、路由参数
无。`onLoad()` 未读取 `options` 参数。
---
## 六、WXML 硬编码文案
| # | 元素 | 文案内容 | 建议 |
|---|------|----------|------|
| F1 | navbar-title | `申请访问权限` | 可保留 |
| F2 | welcome-title | `欢迎加入球房运营助手` | 可保留 |
| F3 | welcome-desc | `请填写申请信息,等待管理员审核` | 可保留 |
| F4 | step-label 1 | `提交申请` | 可保留 |
| F5 | step-label 2 | `等待审核` | 可保留 |
| F6 | step-label 3 | `审核通过` | 可保留 |
| F7 | step-label 4 | `开始使用` | 可保留 |
| F8 | form-label | `球房ID` | 可保留 |
| F9 | form-label | `申请身份` | 可保留 |
| F10 | form-label | `手机号` | 可保留 |
| F11 | form-label | `编号` + `选填` | 可保留 |
| F12 | form-label | `昵称` | 可保留 |
| F13 | placeholder | `请输入球房ID` / `请输入申请身份(如:助教、店长等)` / `请输入手机号` / `请输入编号` / `请输入昵称` | 可保留 |
| F14 | form-tip | `请认真填写,信息不完整可能导致审核不通过` | 可保留 |
| F15 | submit-btn-text | `提交申请` | 可保留 |
| F16 | bottom-tip | `审核通常需要 1-3 个工作日` | 🟡 若审核时效变化需同步更新 |
---
## 七、联调 TODO
| # | 类型 | 描述 | 优先级 |
|---|------|------|--------|
| T1 | 验证 | 确认 `GET /api/xcx/me` 返回字段与前端解构一致(`user_id``status``nickname` | P0 |
| T2 | 验证 | 确认 `POST /api/xcx/apply` 请求体字段名与后端 schema 一致(`site_code``applied_role_text``phone``employee_number``nickname` | P0 |
| T3 | 验证 | 确认后端 409 响应场景(重复申请)和 422 响应场景(参数校验失败)的 `detail` 字段格式 | P1 |
| T4 | 验证 | 确认 `site_code` 最大长度(前端 maxlength=5与后端约束一致 | P1 |
| T5 | 优化 | `_checkAccess` 的 catch 静默忽略网络错误——考虑是否需要给用户提示网络异常 | P2 |
| T6 | 优化 | `employee_number` 空值传 `undefined`JSON 序列化时会被忽略)——确认后端是否接受缺省该字段 | P1 |
| T7 | 确认 | `status` 枚举值完整性——前端处理了 `new/rejected/approved/pending/disabled`,确认后端不会返回其他值 | P1 |

View File

@@ -0,0 +1,207 @@
# board-coach 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/board-coach/board-coach
> 排查范围board-coach.ts / board-coach.wxml / board-coach.wxss / board-coach.json + 引用工具函数
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 1 组6 条记录,含 ~20 个字段/条) | 页面内联 `MOCK_COACHES` 数组,未引用 `utils/mock-data.ts` |
| B. 硬编码数据 | 6 组常量 + 3 个默认值 | 排序/技能/时间选项、等级/技能样式映射、默认筛选值 |
| C. 已对接 API | 0 | 全部数据来自 Mock无任何 `wx.request` / service 调用 |
| D. 前端计算/派生数据 | 10 个派生字段 | `loadData()` 中通过 `formatMoney`/`formatHours`/`formatCount` 生成展示标签 |
| E. 路由参数 | 0 | `onLoad()` 未读取任何 query 参数 |
| F. WXML 硬编码文案 | 14 处 | Tab 标签、状态提示、维度标签等 |
---
## 一、Mock 数据
### 1.1 页面内联 Mock`MOCK_COACHES`board-coach.ts L82-L161
页面顶部定义了 `const MOCK_COACHES: CoachItem[]`,包含 6 位助教的完整数据。`loadData()` 通过 `setTimeout(400ms)` 模拟异步加载后直接使用该数组。
**未引用** `utils/mock-data.ts` 中的 `mockCoaches`(该文件有独立的 `CoachCard` 接口和 3 条记录,字段结构不同)。
| Mock 字段 | 类型 | 示例值 | 联调时应替换为 |
|-----------|------|--------|---------------|
| `id` | string | `'c1'` | API 返回的助教 ID |
| `name` | string | `'小燕'` | 助教姓名 |
| `initial` | string | `'小'` | 前端根据 name 首字计算,或 API 返回头像 URL |
| `avatarGradient` | string | `'blue'` | API 返回头像 URL 后可移除,改用真实头像 |
| `level` | string | `'star'` | 助教等级API`star/senior/middle/junior` |
| `skills` | Array | `[{text:'🎱', cls:'skill--chinese'}]` | API 返回技能列表,前端映射 cls |
| `topCustomers` | string[] | `['💖 王先生','💖 李女士']` | API 返回 top 客户列表(含亲密度标记) |
| `perfHours` | number | `86.2` | 定档业绩课时API 聚合) |
| `perfHoursBefore` | number? | `92.0` | 折前课时API 聚合) |
| `perfGap` | string? | `'距升档 13.8h'` | 可由前端计算或 API 直接返回 |
| `perfReached` | boolean | `false` | 是否已达标API 或前端判断) |
| `salary` | number | `12680` | 预估工资API 聚合) |
| `salaryPerfHours` | number | `86.2` | 工资维度-定档课时 |
| `salaryPerfBefore` | number? | `92.0` | 工资维度-折前课时 |
| `svAmount` | number | `45200` | 客源储值金额API 聚合) |
| `svCustomerCount` | number | `18` | 储值客户数 |
| `svConsume` | number | `8600` | 储值消耗金额 |
| `taskRecall` | number | `18` | 召回任务完成数 |
| `taskCallback` | number | `14` | 回访任务完成数 |
**联调替换目标 API**(待后端提供):
```
GET /api/v1/board/coach/list
Query: sort={selectedSort}&skill={selectedSkill}&time={selectedTime}
Response: { coaches: CoachItem[] }
```
---
## 二、硬编码数据
### 2.1 筛选选项常量board-coach.ts L14-L50
| 常量名 | 字段数 | 风险 | 说明 |
|--------|--------|------|------|
| `SORT_OPTIONS` | 6 项 | 🟡 中 | 排序维度选项value/text 写死。若后端新增排序维度需同步修改 |
| `SKILL_OPTIONS` | 5 项 | 🟡 中 | 技能筛选选项,含 emoji。若门店技能类型可配置则需改为 API 获取 |
| `TIME_OPTIONS` | 6 项 | 🟢 低 | 时间范围选项,纯前端逻辑,变动概率低 |
| `SORT_TO_DIM` | 6 项映射 | 🟢 低 | 排序值→维度类型映射,纯前端 UI 逻辑 |
### 2.2 样式映射常量board-coach.ts L53-L68
| 常量名 | 风险 | 说明 |
|--------|------|------|
| `LEVEL_CLASS` | 🟢 低 | 等级→CSS 类映射,含中英文 key 兼容。纯前端样式逻辑 |
| `SKILL_CLASS` | 🟢 低 | 技能 emoji→CSS 类映射。纯前端样式逻辑 |
### 2.3 data 对象默认值board-coach.ts L165-L180
| 字段 | 硬编码值 | 风险 | 说明 |
|------|----------|------|------|
| `selectedSort` | `'perf_desc'` | 🟢 低 | 默认排序,合理默认值 |
| `selectedSkill` | `'all'` | 🟢 低 | 默认不限技能 |
| `selectedTime` | `'month'` | 🟢 低 | 默认本月 |
| `dimType` | `'perf'` | 🟢 低 | 默认维度类型,与 selectedSort 联动 |
| `filterBarVisible` | `true` | 🟢 低 | 筛选栏初始可见 |
| `pageState` | `'loading'` | 🟢 低 | 页面初始状态 |
| `activeTab` | `'coach'` | 🟢 低 | 当前 Tab 标识 |
### 2.4 `loadData()` 中的模拟延迟board-coach.ts L195
```typescript
setTimeout(() => { ... }, 400) // 🔴 高风险:联调时必须替换为真实 API 调用
```
### 2.5 `onPullDownRefresh` 中的延迟board-coach.ts L192
```typescript
setTimeout(() => wx.stopPullDownRefresh(), 500) // 🟡 中风险:联调后应在 API 回调中停止
```
---
## 三、已对接 API
**无。** 当前页面 0 个 API 调用,全部数据来自页面内联 Mock。
页面中存在的导航跳转(非数据 API
- `wx.switchTab({ url: '/pages/board-finance/board-finance' })` — Tab 切换
- `wx.navigateTo({ url: '/pages/board-customer/board-customer' })` — Tab 切换
- `wx.navigateTo({ url: '/pages/coach-detail/coach-detail?id=' + id })` — 助教详情跳转
---
## 四、前端计算/派生数据
### 4.1 `loadData()` 中的格式化派生字段board-coach.ts L197-L208
| 派生字段 | 源字段 | 格式化函数 | 输出示例 |
|----------|--------|-----------|----------|
| `perfHoursLabel` | `perfHours` | `formatHours()` | `'86.2h'` |
| `perfHoursBeforeLabel` | `perfHoursBefore` | `formatHours()` | `'92h'` |
| `salaryLabel` | `salary` | `formatMoney()` | `'¥12,680'` |
| `salaryPerfHoursLabel` | `salaryPerfHours` | `formatHours()` | `'86.2h'` |
| `salaryPerfBeforeLabel` | `salaryPerfBefore` | `formatHours()` | `'92h'` |
| `svAmountLabel` | `svAmount` | `formatMoney()` | `'¥45,200'` |
| `svCustomerCountLabel` | `svCustomerCount` | `formatCount(n,'人')` | `'18人'` |
| `svConsumeLabel` | `svConsume` | `formatMoney()` | `'¥8,600'` |
| `taskRecallLabel` | `taskRecall` | `formatCount(n,'次')` | `'18次'` |
| `taskCallbackLabel` | `taskCallback` | `formatCount(n,'次')` | `'14次'` |
### 4.2 维度类型切换board-coach.ts L218-L222
`onSortChange` 根据 `SORT_TO_DIM` 映射计算 `dimType`,控制 WXML 中四种卡片模板的条件渲染。
### 4.3 滚动隐藏/显示筛选栏board-coach.ts L196-L215 `onPageScroll`
纯前端交互逻辑:累积滚动距离超过阈值(上滑 12px / 下滑 24px时切换 `filterBarVisible`
---
## 五、路由参数
**无。** `onLoad()` 未接收任何 `options` 参数。
---
## 六、WXML 硬编码文案
| 位置 | 硬编码文案 | 风险 | 建议 |
|------|-----------|------|------|
| 加载态 | `加载中...` | 🟢 低 | 通用文案,可保留 |
| 空状态 | `暂无助教数据` | 🟢 低 | 可保留 |
| 错误态 | `加载失败` | 🟢 低 | 可保留 |
| 重试按钮 | `点击重试` | 🟢 低 | 可保留 |
| Tab-财务 | `财务` | 🟢 低 | 固定 Tab 文案 |
| Tab-客户 | `客户` | 🟢 低 | 固定 Tab 文案 |
| Tab-助教 | `助教` | 🟢 低 | 固定 Tab 文案 |
| 定档维度标签 | `定档` / `折前` | 🟢 低 | 业务术语,可保留 |
| 工资维度标签 | `预估` | 🟢 低 | 业务术语 |
| 储值维度标签 | `储值` | 🟢 低 | 业务术语 |
| 召回维度标签 | `召回` | 🟢 低 | 业务术语 |
| 达标标记 | `✅ 已达标` | 🟢 低 | 可保留 |
| 工资底部 | `定档` / `折前` | 🟢 低 | 同上 |
| 储值底部 | `客户` / `消耗` / `\|` 分隔符 | 🟢 低 | 业务术语 |
| 任务底部 | `回访` | 🟢 低 | 业务术语 |
| 导航栏标题 | `助教看板`board-coach.json | 🟢 低 | 固定页面标题 |
---
## 七、引用组件清单
| 组件 | 来源 | 数据依赖 |
|------|------|----------|
| `coach-level-tag` | 自定义组件 | 接收 `level` 属性 |
| `filter-dropdown` | 自定义组件 | 接收 `label`/`options`/`value`,触发 `change` 事件 |
| `ai-float-button` | 自定义组件 | 无数据依赖 |
| `board-tab-bar` | 自定义 Tab Bar | 接收 `active="board"` |
| `t-loading` / `t-empty` | TDesign 组件 | 无业务数据依赖 |
| `dev-fab` | 开发调试组件 | `wx:if="{{false}}"` 已禁用 |
---
## 八、联调 TODO
### 🔴 P0 — 必须完成
- [ ] **替换 `MOCK_COACHES` 为 API 调用**:移除页面内联 Mock 数组,`loadData()` 改为调用后端接口
- 需后端提供:助教列表接口(含排序/技能/时间筛选参数)
- 接口需返回四种维度的全部字段perf/salary/sv/task
- 错误处理API 失败时 `setData({ pageState: 'error' })`
- [ ] **移除 `setTimeout(400ms)` 模拟延迟**:替换为真实异步请求
- [ ] **`onPullDownRefresh` 改为 API 回调中停止**`wx.stopPullDownRefresh()` 应在请求完成后调用
### 🟡 P1 — 建议完成
- [ ] **筛选联动 API**:当前 `onSortChange`/`onSkillChange`/`onTimeChange` 仅更新本地状态,未触发重新请求。联调时需在筛选变更后重新调用 API
- [ ] **头像方案确认**:当前使用 `initial`(首字)+ `avatarGradient`(渐变色)模拟头像。确认是否改为真实头像 URL
- [ ] **`perfGap` 字段来源确认**:当前为 Mock 字符串(如 `'距升档 13.8h'`),确认由后端计算还是前端根据阈值计算
- [ ] **`topCustomers` 格式确认**:当前含 emoji 亲密度标记(💖/💛),确认 API 返回格式
### 🟢 P2 — 可后续优化
- [ ] **`SKILL_OPTIONS` 是否需要动态获取**:若门店技能类型可配置,应改为 API 获取
- [ ] **`SORT_OPTIONS` / `TIME_OPTIONS` 是否需要后端控制**:当前写死,若需动态调整需改为配置化
- [ ] **清理 `utils/mock-data.ts` 中的 `CoachCard` 接口**:与页面内联 `CoachItem` 接口不一致,联调完成后统一或移除
- [ ] **`levelClass` 字段**:当前在 Mock 中预计算,联调后应由前端根据 `level` 字段通过 `LEVEL_CLASS` 映射生成

View File

@@ -0,0 +1,295 @@
# board-customer 页面数据来源排查
> 排查日期2026-03-18
> 页面路径:`pages/board-customer/board-customer`
> 引用文件:`board-customer.ts` / `.wxml` / `.wxss` / `.json`
> 外部依赖:`utils/ai-color-manager.ts``initPageAiColor`,已导入但未调用)
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 1 组3 条记录,~30 个字段/条) | 页面内联 `MOCK_CUSTOMERS` 常量,未引用 `utils/mock-data.ts` |
| B. 硬编码数据 | 5 处 | 维度选项、项目选项、Tab 列表、列表标题、加载延迟 |
| C. 已对接 API | 0 | 无任何 `wx.request` / API 调用 |
| D. 前端计算/派生 | 3 处 | `dimType` 映射、滚动隐藏筛选栏、空/错误态判断 |
| E. 路由参数 | 0 | `onLoad` 无参数读取 |
| F. WXML 硬编码文案 | 12 处 | Tab 文案、列表标题、网格标签、状态提示等 |
---
## 一、Mock 数据
### 1.1 `MOCK_CUSTOMERS`(页面内联,第 72178 行)
页面顶部定义的 `const MOCK_CUSTOMERS: CustomerItem[]`,包含 3 条客户记录(王先生 / 李女士 / 张先生),在 `loadData()` 中通过 `setTimeout(400ms)` 模拟异步加载。
**未引用 `utils/mock-data.ts`**:该文件中有 `mockCustomers: CustomerCard[]`,但 board-customer 页面使用了自己定义的 `CustomerItem` 接口(字段更丰富,支持 8 维度卡片)。
#### 字段清单(每条 CustomerItem
| 字段 | 示例值(王先生) | 数据类型 | 联调时对应 API 字段 |
|------|------------------|----------|---------------------|
| `id` | `'u1'` | string | 客户 ID`member_id` |
| `name` | `'王先生'` | string | 客户姓名(`dim_member.nickname`,注意 DQ-6 断档) |
| `initial` | `'王'` | string | 前端从 `name` 截取首字 |
| `avatarCls` | `'avatar--amber'` | string | 前端根据规则分配头像色 |
| `idealDays` | `7` | number | 理想到店间隔天数DWS 计算) |
| `elapsedDays` | `15` | number | 距上次到店已过天数DWS 计算) |
| `overdueDays` | `8` | number | 超期天数 = `elapsedDays - idealDays` |
| `visits30d` | `3` | number | 近 30 天到店次数 |
| `balance` | `'¥2,680'` | string | 当前余额(储值卡) |
| `recallIndex` | `'9.2'` | string | 召回指数DWS 综合评分) |
| `potentialTags` | `[{text:'高频',theme:'primary'}]` | array | 消费潜力标签(后端计算) |
| `spend30d` | `'¥4,200'` | string | 近 30 天消费金额 |
| `avgVisits` | `'6.2次'` | string | 月均到店次数 |
| `avgSpend` | `'¥680'` | string | 次均消费金额 |
| `lastVisit` | `'3天前'` | string | 最近到店(相对时间) |
| `monthlyConsume` | `'¥3,500'` | string | 月均消耗金额 |
| `availableMonths` | `'约0.8个月'` | string | 余额可用月数 = `balance / monthlyConsume` |
| `lastRecharge` | `'2月15日'` | string | 最后充值日期 |
| `rechargeAmount` | `'¥5,000'` | string | 最后充值金额 |
| `recharges60d` | `'2次'` | string | 近 60 天充值次数 |
| `currentBalance` | `'¥2,680'` | string | 当前余额(与 `balance` 重复) |
| `spend60d` | `'¥8,400'` | string | 近 60 天消费金额 |
| `visits60d` | `'12'` | string | 近 60 天到店次数 |
| `highSpendTag` | `true` | boolean | 是否高消费标签 |
| `avgInterval` | `'5.0天'` | string | 平均到店间隔 |
| `intimacy` | `'92'` | string | 专一度指数 |
| `topCoachName` | `'小燕'` | string | 最亲密助教姓名 |
| `topCoachHeart` | `9.2` | number | 最亲密助教关系指数 |
| `topCoachScore` | `'9.2'` | string | 同上(字符串版) |
| `coachDetails` | 2 条明细 | array | 助教服务明细表 |
| `weeklyVisits` | 8 周数据 | array | 8 周到店频率(迷你柱状图) |
| `coachName` | `'小燕'` | string | 主助教姓名 |
| `coachRatio` | `'78%'` | string | 主助教服务占比 |
| `visitFreq` | `'6.2次/月'` | string | 到店频率 |
| `daysAgo` | `3` | number | 距上次到店天数 |
| `assistants` | 2 条 | array | 助教列表(底部行) |
#### `coachDetails` 子字段
| 字段 | 示例值 | 说明 |
|------|--------|------|
| `name` | `'小燕'` | 助教姓名 |
| `cls` | `'assistant--assignee'` | 样式类(跟/弃/普通) |
| `heartScore` | `9.2` | 关系指数 |
| `badge` | `'跟'` | 跟/弃标签 |
| `avgDuration` | `'2.3h'` | 次均服务时长 |
| `serviceCount` | `'14'` | 服务次数 |
| `coachSpend` | `'¥4,200'` | 助教消费金额 |
| `relationIdx` | `9.2` | 关系指数(数值版) |
#### `assistants` 子字段
| 字段 | 示例值 | 说明 |
|------|--------|------|
| `name` | `'小燕'` | 助教姓名 |
| `cls` | `'assistant--assignee'` | 样式类 |
| `heartScore` | `9.2` | 关系指数 |
| `badge` | `'跟'` | 跟/弃标签(可选) |
| `badgeCls` | `'assistant-badge--follow'` | badge 样式类(可选) |
---
## 二、硬编码数据
### 2.1 `DIMENSION_OPTIONS`(维度下拉选项)
| 风险 | 字段 | 当前值 | 应从何处获取 |
|------|------|--------|-------------|
| 🟡 低 | `DIMENSION_OPTIONS` | 8 个维度选项recall/potential/balance/recharge/recent/spend60/freq60/loyal | 前端枚举,可保留硬编码;若后端支持动态维度则需 API |
```ts
const DIMENSION_OPTIONS = [
{ value: 'recall', text: '最应召回' },
{ value: 'potential', text: '最大消费潜力' },
{ value: 'balance', text: '最高余额' },
{ value: 'recharge', text: '最近充值' },
{ value: 'recent', text: '最近到店' },
{ value: 'spend60', text: '最高消费 近60天' },
{ value: 'freq60', text: '最频繁 近60天' },
{ value: 'loyal', text: '最专一 近60天' },
]
```
### 2.2 `PROJECT_OPTIONS`(项目筛选选项)
| 风险 | 字段 | 当前值 | 应从何处获取 |
|------|------|--------|-------------|
| 🟡 低 | `PROJECT_OPTIONS` | 5 个项目选项all/chinese/snooker/mahjong/karaoke | 前端枚举,可保留;若门店项目可配置则需 API |
```ts
const PROJECT_OPTIONS = [
{ value: 'all', text: '全部' },
{ value: 'chinese', text: '🎱 中式/追分' },
{ value: 'snooker', text: '斯诺克' },
{ value: 'mahjong', text: '🀄 麻将/棋牌' },
{ value: 'karaoke', text: '🎤 团建/K歌' },
]
```
### 2.3 `DIMENSION_TO_DIM` 映射表
| 风险 | 字段 | 说明 |
|------|------|------|
| 🟢 无 | `DIMENSION_TO_DIM` | 纯前端映射,维度 value → DimType无需替换 |
### 2.4 `loadData()` 中的 `setTimeout(400ms)`
| 风险 | 位置 | 当前值 | 说明 |
|------|------|--------|------|
| 🔴 高 | `loadData()` | `setTimeout(() => {...}, 400)` | 模拟网络延迟,联调时必须替换为真实 `wx.request` |
### 2.5 `onPullDownRefresh()` 中的 `setTimeout(500ms)`
| 风险 | 位置 | 当前值 | 说明 |
|------|------|--------|------|
| 🟡 中 | `onPullDownRefresh()` | `setTimeout(() => wx.stopPullDownRefresh(), 500)` | 联调时应在 API 回调中调用 `wx.stopPullDownRefresh()` |
---
## 三、已对接 API
**无。** 页面中没有任何 `wx.request``wx.cloud.callFunction` 或封装的 API 调用。
`loadData()` 当前实现:
```ts
loadData() {
this.setData({ pageState: 'loading' })
setTimeout(() => {
const data = MOCK_CUSTOMERS // ← 直接引用内联 mock
if (!data || data.length === 0) {
this.setData({ pageState: 'empty' })
return
}
this.setData({
allCustomers: data,
customers: data,
totalCount: data.length,
pageState: 'normal',
})
}, 400)
}
```
---
## 四、前端计算/派生数据
| 字段/逻辑 | 位置 | 说明 |
|-----------|------|------|
| `dimType` | `onDimensionChange()` | 由 `DIMENSION_TO_DIM[selectedDimension]` 映射得出,控制 WXML 卡片模板切换 |
| `filterBarVisible` | `onPageScroll()` | 滚动方向 + 累积距离判断,纯 UI 交互逻辑 |
| `pageState` | `loadData()` | 根据数据加载结果设置 `'loading'` / `'empty'` / `'normal'` / `'error'` |
| `totalCount` | `loadData()` | `data.length`,由返回数据长度计算 |
| `overdueDays > 7` | WXML | 控制超期标签颜色danger / warn纯前端判断 |
| `item.highSpendTag` | WXML | 控制是否显示"高消费"标签,值来自 mock 数据 |
| 柱状图 `opacity` | WXML | `0.2 + wIdx * 0.057`,渐变透明度,纯 UI 计算 |
| `cd.relationIdx >= 8` | WXML | 控制关系指数颜色primary / gray纯前端判断 |
---
## 五、路由参数
**无。** `onLoad()` 不接收任何参数,直接调用 `loadData()`
页面作为导航目标时的入口:
- `board-finance.ts``wx.navigateTo({ url: '/pages/board-customer/board-customer' })`
- `board-coach.ts``wx.navigateTo({ url: '/pages/board-customer/board-customer' })`
- `dev-tools.ts` → 开发工具页面列表
---
## 六、WXML 中的硬编码文案
| 位置 | 硬编码文案 | 风险 | 说明 |
|------|-----------|------|------|
| 加载态 | `加载中...` | 🟢 无 | 通用 UI 文案 |
| 空状态 | `暂无客户数据` | 🟢 无 | 通用 UI 文案 |
| 错误态 | `加载失败` | 🟢 无 | 通用 UI 文案 |
| 错误态按钮 | `点击重试` | 🟢 无 | 通用 UI 文案 |
| Tab 栏 | `财务` / `客户` / `助教` | 🟢 无 | 固定导航文案 |
| 列表标题 | `客户列表` | 🟢 无 | 固定标题 |
| 列表副标题 | `· 前100名` | 🟡 低 | 若后端支持分页/动态 TopN需改为动态值 |
| 列表计数 | `共{{totalCount}}名客户` | 🟢 无 | 动态绑定 |
| 召回维度标签 | `理想` / `已过` / `超期` / `天` | 🟢 无 | 固定维度标签 |
| 网格标签 | `30天消费` / `月均到店` / `余额` / `次均消费` / `月均消耗` / `可用` / `最后充值` / `充值` / `60天充值` / `当前余额` / `近60天消费` / `到店次数` | 🟢 无 | 固定维度标签 |
| 频率维度 | `60天到店` / `次` / `8周前` / `本周` | 🟢 无 | 固定维度标签 |
| 专一维度表头 | `助教` / `次均时长` / `服务次数` / `助教消费` / `关系指数` | 🟢 无 | 固定表头 |
| 助教标签 | `助教:` / `跟` / `弃` | 🟢 无 | 固定 UI 文案 |
| 最近到店 | `天前到店` / `理想间隔` / `60天到店` / `次均消费` | 🟢 无 | 固定维度标签 |
| 频率中间行 | `平均间隔` / `60天消费` | 🟢 无 | 固定维度标签 |
| 召回中间行 | `30天到店` / `余额` / `召回指数` | 🟢 无 | 固定维度标签 |
| 最近到店右上角 | `天前到店` | 🟢 无 | 固定维度标签 |
---
## 七、引用组件清单
| 组件 | 路径 | 数据来源 | 说明 |
|------|------|----------|------|
| `filter-dropdown` | `/components/filter-dropdown/` | 父组件传入 `options` / `value` | 纯展示组件,无自有数据 |
| `heart-icon` | `/components/heart-icon/` | 父组件传入 `score` | 纯展示组件,根据分数渲染心形图标 |
| `ai-float-button` | `/components/ai-float-button/` | 自有逻辑 | AI 悬浮按钮,独立组件 |
| `board-tab-bar` | `/custom-tab-bar/index` | `active="board"` | 底部导航栏,当前 mock 3 按钮 |
| `dev-fab` | `/components/dev-fab/` | — | 开发调试按钮,`wx:if="{{false}}"` 已隐藏 |
| `t-loading` / `t-empty` / `t-icon` / `t-tag` | TDesign | — | 第三方 UI 组件 |
---
## 八、联调 TODO
### 🔴 P0 — 必须替换
| # | 任务 | 当前状态 | 目标 |
|---|------|----------|------|
| 1 | 替换 `MOCK_CUSTOMERS` 为 API 调用 | 页面内联 3 条 mock 数据 | 调用后端客户看板 API传入 `dimension` + `project` 参数 |
| 2 | 实现 `loadData()` 真实请求 | `setTimeout(400ms)` 模拟 | `wx.request` → 后端 API处理 loading/error/empty 三态 |
| 3 | 下拉刷新对接 | `setTimeout(500ms)` 后停止 | API 回调中 `wx.stopPullDownRefresh()` |
| 4 | 维度切换重新请求 | `onDimensionChange` 仅切换 `dimType` | 切换维度后重新调用 API不同维度返回不同排序/字段) |
| 5 | 项目筛选重新请求 | `onProjectChange` 仅更新 `selectedProject` | 切换项目后重新调用 API按项目类型过滤 |
### 🟡 P1 — 建议优化
| # | 任务 | 说明 |
|---|------|------|
| 6 | 分页/滚动加载 | 当前一次性加载全部,"前 100 名"硬编码;若数据量大需分页 |
| 7 | `initPageAiColor` 调用 | 已导入但未在 `onLoad` 中调用,需补充 |
| 8 | 金额字段格式化 | Mock 中金额为预格式化字符串(`'¥2,680'`),联调后需前端格式化 |
| 9 | 相对时间计算 | `lastVisit: '3天前'``daysAgo: 3` 等,联调后需从时间戳计算 |
| 10 | `balance``currentBalance` 去重 | Mock 中两字段值相同API 设计时应统一 |
### 🟢 P2 — 可保留
| # | 项目 | 说明 |
|---|------|------|
| 11 | `DIMENSION_OPTIONS` | 前端枚举,维度列表固定,可保留硬编码 |
| 12 | `PROJECT_OPTIONS` | 前端枚举,若门店项目固定可保留 |
| 13 | `DIMENSION_TO_DIM` 映射 | 纯前端逻辑,保留 |
| 14 | WXML 固定文案 | 维度标签、表头等,保留 |
---
## 九、待确认 API 设计
联调前需与后端确认以下接口设计:
```
GET /api/v1/board/customers
Query:
dimension: string — 排序维度recall/potential/balance/...
project: string — 项目筛选all/chinese/snooker/...
limit: number — 返回条数(默认 100
site_id: string — 门店 ID从登录态获取
Response:
total: number
items: CustomerItem[]
```
**关键字段口径注意**
- 金额字段(`balance``spend30d``spend60d` 等):后端返回 `numeric(2)` 原始数值,前端负责 `¥` 前缀 + 千分位格式化
- 会员姓名:通过 `member_id` JOIN `dim_member.nickname`DQ-6 断档,禁止直接用 `member_name`
- 储值卡余额DWS 层 `balance_pay`(总额)= `recharge_card_pay` + `gift_card_pay`
- 消费金额:使用 `items_sum` 口径,禁止直接使用 `consume_money`(三种历史口径混合)

View File

@@ -0,0 +1,352 @@
# board-finance 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/board-finance/board-finance
> 排查人Kiro AI
> 状态:**全量内联 Mock零 API 对接**
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据(内联) | **120+ 个字段** | 全部财务指标写死在 `data` 对象中,无外部 mock 函数调用 |
| 硬编码数据 | **19 个字段/配置** | 筛选选项、目录导航、指标解释、板块描述等 |
| 已对接 API | **0 个接口** | 页面无任何 `request()` / `wx.request` 调用 |
| 前端计算/派生 | **8 个字段** | UI 状态、滚动位置、配色等 |
| 路由参数 | **0 个** | `onLoad` 未读取 `options` |
| WXML 硬编码文案 | **3 处** | AI 洞察文案、平台服务费说明 |
---
## 一、Mock 数据(内联在 data 对象中)
> 本页面 **不引用** `utils/mock-data.ts`(该文件虽定义了 `BoardFinanceData` 接口,但页面未 import
> 所有 mock 数据直接写死在 `Page({ data: { ... } })` 中,文件头注释标注 `TODO: 联调时替换 mock 数据为真实 API 调用`。
### 1.1 经营一览 `overview`
| 字段 | 类型 | 当前 Mock 值 | 联调替换 API | 备注 |
|------|------|-------------|-------------|------|
| `overview.occurrence` | string | `¥823,456` | 财务看板-经营一览 | 发生额/正价 |
| `overview.occurrenceCompare` | string | `12.5%` | 同上(环比) | |
| `overview.discount` | string | `-¥113,336` | 同上 | 总优惠 |
| `overview.discountCompare` | string | `3.2%` | 同上(环比) | |
| `overview.discountRate` | string | `13.8%` | 同上 | 优惠占比 |
| `overview.discountRateCompare` | string | `1.5%` | 同上(环比) | |
| `overview.confirmedRevenue` | string | `¥710,120` | 同上 | 成交/确认收入 |
| `overview.confirmedCompare` | string | `8.7%` | 同上(环比) | |
| `overview.cashIn` | string | `¥698,500` | 同上 | 实收/现金流入 |
| `overview.cashInCompare` | string | `5.3%` | 同上(环比) | |
| `overview.cashOut` | string | `¥472,300` | 同上 | 现金支出 |
| `overview.cashOutCompare` | string | `2.1%` | 同上(环比) | |
| `overview.cashBalance` | string | `¥226,200` | 同上 | 现金结余 |
| `overview.cashBalanceCompare` | string | `15.2%` | 同上(环比) | |
| `overview.balanceRate` | string | `32.4%` | 同上 | 结余率 |
| `overview.balanceRateCompare` | string | `3.8%` | 同上(环比) | |
### 1.2 预收资产 `recharge`
| 字段 | 类型 | 当前 Mock 值 | 联调替换 API | 备注 |
|------|------|-------------|-------------|------|
| `recharge.actualIncome` | string | `¥352,800` | 财务看板-预收资产 | 储值卡充值实收 |
| `recharge.actualCompare` | string | `18.5%` | 同上(环比) | |
| `recharge.firstCharge` | string | `¥188,500` | 同上 | 首充 |
| `recharge.firstChargeCompare` | string | `12.3%` | 同上(环比) | |
| `recharge.renewCharge` | string | `¥164,300` | 同上 | 续费 |
| `recharge.renewChargeCompare` | string | `8.7%` | 同上(环比) | |
| `recharge.consumed` | string | `¥238,200` | 同上 | 消耗 |
| `recharge.consumedCompare` | string | `5.2%` | 同上(环比) | |
| `recharge.cardBalance` | string | `¥642,600` | 同上 | 储值卡总余额 |
| `recharge.cardBalanceCompare` | string | `11.4%` | 同上(环比) | |
| `recharge.allCardBalance` | string | `¥586,500` | 同上 | 全类别会员卡余额合计 |
| `recharge.allCardBalanceCompare` | string | `6.2%` | 同上(环比) | |
#### 赠送卡明细 `recharge.giftRows`3 行 × 7 字段 = 21 个值)
| 行 | label | total | wine | table | coupon | 各字段均含 Compare |
|----|-------|-------|------|-------|--------|-------------------|
| 0 | 新增 | ¥108,600 | ¥43,200 | ¥54,100 | ¥11,300 | 9.8%/11.2%/8.5%/6.3% |
| 1 | 消费 | ¥75,800 | ¥32,100 | ¥32,800 | ¥10,900 | 7.2%/8.1%/6.5%/5.8% |
| 2 | 余额 | ¥243,900 | ¥118,500 | ¥109,200 | ¥16,200 | 4.5%/5.2%/3.8%/2.5% |
> 联调 API财务看板-预收资产-赠送卡统计
### 1.3 应计收入确认 `revenue`
#### 收入结构 `revenue.structureRows`9 行 × 5~6 字段)
| id | name | desc | amount | discount | booked | bookedCompare | isSub |
|----|------|------|--------|----------|--------|---------------|-------|
| `table` | 开台与包厢 | - | ¥358,600 | -¥45,200 | ¥313,400 | 9.2% | false |
| `area-a` | A区 | - | ¥118,200 | -¥11,600 | ¥106,600 | 12.1% | true |
| `area-b` | B区 | - | ¥95,800 | -¥11,200 | ¥84,600 | 8.5% | true |
| `area-c` | C区 | - | ¥72,600 | -¥11,100 | ¥61,500 | 6.3% | true |
| `team` | 团建区 | - | ¥48,200 | -¥6,800 | ¥41,400 | 5.8% | true |
| `mahjong` | 麻将区 | - | ¥23,800 | -¥4,500 | ¥19,300 | -2.1% | true |
| `coach-basic` | 助教 | 基础课 | ¥232,500 | - | ¥232,500 | 15.3% | false |
| `coach-incentive` | 助教 | 激励课 | ¥112,800 | - | ¥112,800 | 8.2% | false |
| `food` | 食品酒水 | - | ¥119,556 | -¥68,136 | ¥51,420 | 6.5% | false |
> 联调 API财务看板-应计收入-收入结构
#### 项目正价 `revenue.priceItems`4 项)
| name | value | compare |
|------|-------|---------|
| 开台消费 | ¥358,600 | 9.2% |
| 酒水商品 | ¥186,420 | 18.5% |
| 包厢费用 | ¥165,636 | 12.1% |
| 助教服务 | ¥112,800 | 15.3% |
#### 发生额合计
| 字段 | 值 |
|------|-----|
| `revenue.totalOccurrence` | ¥823,456 |
| `revenue.totalOccurrenceCompare` | 12.5% |
#### 优惠扣减 `revenue.discountItems`4 项)
| name | desc | value | compare |
|------|------|-------|---------|
| 团购优惠 | - | -¥56,200 | 5.2% |
| 手动调整 + 大客户优惠 | - | -¥34,800 | 3.1% |
| 赠送卡抵扣 | 台桌卡+酒水卡+抵用券 | -¥22,336 | 8.6% |
| 其他优惠 | 免单+抹零 | -¥0 | (空) |
#### 成交收入合计
| 字段 | 值 |
|------|-----|
| `revenue.confirmedTotal` | ¥710,120 |
| `revenue.confirmedTotalCompare` | 8.7% |
#### 收款渠道 `revenue.channelItems`3 项)
| name | desc | value | compare |
|------|------|-------|---------|
| 储值卡结算冲销 | - | ¥238,200 | 11.2% |
| 现金/线上支付 | - | ¥345,800 | 7.8% |
| 团购核销确认收入 | 团购成交价 | ¥126,120 | 5.3% |
> 联调 API财务看板-应计收入-损益链
### 1.4 现金流入 `cashflow`
#### 消费收入 `cashflow.consumeItems`3 项)
| name | desc | value | compare | isDown |
|------|------|-------|---------|--------|
| 纸币现金 | 柜台现金收款 | ¥85,600 | 12.3% | true |
| 线上收款 | 微信/支付宝/刷卡 已扣除平台服务费 | ¥260,200 | 8.5% | false |
| 团购平台 | 美团/抖音回款 已扣除平台服务费 | ¥126,120 | 15.2% | false |
#### 充值收入 `cashflow.rechargeItems`1 项)
| name | desc | value | compare |
|------|------|-------|---------|
| 会员充值到账 | 首充/续费实收 | ¥352,800 | 18.5% |
#### 合计
| 字段 | 值 |
|------|-----|
| `cashflow.total` | ¥824,720 |
| `cashflow.totalCompare` | 10.2% |
> 联调 API财务看板-现金流入
### 1.5 现金流出 `expense`
#### 进货与运营 `expense.operationItems`3 项)
| name | value | compare | isDown |
|------|-------|---------|--------|
| 食品饮料 | ¥108,200 | 4.5% | false |
| 耗材 | ¥21,850 | 2.1% | true |
| 报销 | ¥10,920 | 6.8% | false |
#### 固定支出 `expense.fixedItems`4 项)
| name | value | compare | isFlat |
|------|-------|---------|--------|
| 房租 | ¥125,000 | 持平 | true |
| 水电 | ¥24,200 | 3.2% | false |
| 物业 | ¥11,500 | 持平 | true |
| 人员工资 | ¥112,000 | 持平 | true |
#### 助教薪资 `expense.coachItems`4 项)
| name | value | compare | isDown |
|------|-------|---------|--------|
| 基础课分成 | ¥116,250 | 8.2% | false |
| 激励课分成 | ¥23,840 | 5.6% | false |
| 充值提成 | ¥12,640 | 12.3% | false |
| 额外奖金 | ¥11,500 | 3.1% | true |
#### 平台服务费 `expense.platformItems`3 项)
| name | value | compare |
|------|-------|---------|
| 汇来米 | ¥10,680 | 1.5% |
| 美团 | ¥11,240 | 2.8% |
| 抖音 | ¥10,580 | 3.5% |
#### 合计
| 字段 | 值 |
|------|-----|
| `expense.total` | ¥600,400 |
| `expense.totalCompare` | 2.1% |
> 联调 API财务看板-现金流出
### 1.6 助教分析 `coachAnalysis`
#### 基础课 `coachAnalysis.basic`
| 字段 | 值 | 环比 |
|------|-----|------|
| totalPay | ¥232,500 | 15.3% |
| totalShare | ¥116,250 | 15.3% |
| avgHourly | ¥25/h | 4.2% |
明细行4 行 × 7 字段):
| level | pay | payCompare | share | shareCompare | hourly | hourlyCompare | 特殊标记 |
|-------|-----|------------|-------|--------------|--------|---------------|----------|
| 初级 | ¥68,600 | 12.5% | ¥34,300 | 12.5% | ¥20/h | 持平 | hourlyFlat |
| 中级 | ¥82,400 | 18.2% | ¥41,200 | 18.2% | ¥25/h | 8.7% | |
| 高级 | ¥57,800 | 14.6% | ¥28,900 | 14.6% | ¥30/h | 持平 | hourlyFlat |
| 星级 | ¥23,700 | 3.2% ↓ | ¥11,850 | 3.2% ↓ | ¥35/h | 持平 | payDown, shareDown, hourlyFlat |
#### 激励课 `coachAnalysis.incentive`
| 字段 | 值 | 环比 |
|------|-----|------|
| totalPay | ¥112,800 | 8.2% |
| totalShare | ¥33,840 | 8.2% |
| avgHourly | ¥15/h | 2.1% |
明细行4 行 × 7 字段):
| level | pay | payCompare | share | shareCompare | hourly | hourlyCompare | 特殊标记 |
|-------|-----|------------|-------|--------------|--------|---------------|----------|
| 初级 | ¥32,400 | 6.8% | ¥9,720 | 6.8% | ¥12/h | 持平 | hourlyFlat |
| 中级 | ¥38,600 | 10.5% | ¥11,580 | 10.5% | ¥15/h | 5.2% | |
| 高级 | ¥28,200 | 7.3% | ¥8,460 | 7.3% | ¥18/h | 持平 | hourlyFlat |
| 星级 | ¥13,600 | 2.1% ↓ | ¥4,080 | 2.1% ↓ | ¥22/h | 持平 | payDown, shareDown, hourlyFlat |
> 联调 API财务看板-助教分析
---
## 二、硬编码数据
| 字段 | 当前值 | 应改为 | 风险 | 所在位置 |
|------|--------|--------|------|----------|
| `timeOptions` | 8 个固定选项(本月/上月/本周/上周/前3月/本季/上季/近6月 | 前端常量或后端配置 | **低** | data.timeOptions |
| `areaOptions` | 7 个固定选项(全部/大厅/A区/B区/C区/麻将房/团建房) | **API 获取门店区域列表** | **高** | data.areaOptions |
| `tocItems` | 6 个目录项emoji + title + sectionId | 前端常量(合理) | **低** | data.tocItems |
| `tipContents` | 13 个指标解释title + content | 前端常量或后端配置 | **低** | 模块级常量 |
| `_sectionDescs` | 6 条板块描述文案 | 前端常量(合理) | **低** | 实例属性 |
| `selectedTime` | `'month'` | 默认值合理,但需与 API 参数对齐 | **中** | data.selectedTime |
| `selectedTimeText` | `'本月'` | 同上 | **低** | data.selectedTimeText |
| `selectedArea` | `'all'` | 默认值合理 | **低** | data.selectedArea |
| `selectedAreaText` | `'全部区域'` | 同上 | **低** | data.selectedAreaText |
| `compareEnabled` | `false` | 默认值合理 | **低** | data.compareEnabled |
| `pageState` | `'normal'` | 联调后应初始为 `'loading'` | **高** | data.pageState |
| Tab 导航 `data-tab` | `finance/customer/coach` 三个固定值 | 前端常量(合理) | **低** | WXML |
| 导航路径 | `/pages/board-customer/board-customer` 等 | 前端常量(合理) | **低** | onTabChange |
| `onPullDownRefresh` | `setTimeout 500ms` 后停止 | 联调后应触发数据刷新 | **高** | onPullDownRefresh |
| AI 洞察文案 | 3 行硬编码文案优惠率Top/差异最大/建议关注) | **API 返回或 AI 生成** | **高** | WXML 内联 |
| 平台服务费说明 | "服务费在流水流入时,平台已经扣除。不产生支出流水。" | 前端常量(合理) | **低** | WXML 内联 |
| 板块头描述文案 | 各板块 desc如"快速了解收入与现金流的整体健康度" | 前端常量(合理) | **低** | WXML 内联 |
| `filter-dropdown label` | `"本月"` / `"全部区域"` | 应绑定 `selectedTimeText` / `selectedAreaText` | **中** | WXML 属性 |
| 激励课表格 | WXML 中只渲染了合计行,缺少明细行 `wx:for` | 需补齐明细行渲染 | **中** | WXML 板块6 |
---
## 三、已对接 API
**无。** 页面当前零 API 调用。
唯一的 `import``getRandomAiColor`(纯前端工具函数,与数据无关)。
文件头注释明确标注:
```
// TODO: 联调时替换 mock 数据为真实 API 调用
```
`onShow` 中也有注释:
```
// TODO: 联调时在此刷新看板数据
```
---
## 四、前端计算/派生数据
| 字段 | 计算逻辑 | 说明 |
|------|----------|------|
| `aiColorClass` | `getRandomAiColor().className` | onLoad 时随机选取 AI 配色 |
| `stickyHeaderVisible` | 滚动方向 + scrollTop 阈值判断 | 下滑 >80px 显示,上滑隐藏 |
| `stickyHeaderEmoji` / `Title` / `Desc` | 从 `tocItems[currentIdx]` + `_sectionDescs[idx]` 映射 | 吸顶头内容 |
| `currentSectionIndex` | 滚动位置与 `_sectionTops` 数组比较 | 当前所在板块索引 |
| `_sectionTops` | `wx.createSelectorQuery` 获取各 section 的 top | 缓存的 DOM 位置 |
| `selectedTimeText` | 从 `timeOptions` 中按 `value` 查找 `text` | 筛选文案 |
| `selectedAreaText` | 从 `areaOptions` 中按 `value` 查找 `text` | 筛选文案 |
| `tocVisible` / `tipVisible` | 用户交互切换 | UI 状态 |
---
## 五、路由参数
**无。** `onLoad()` 未读取 `options` 参数。
---
## 六、WXML 硬编码文案(业务数据)
| 位置 | 内容 | 类型 | 处理建议 |
|------|------|------|----------|
| AI 洞察区域 第1行 | `优惠率Top团购(5.2%) / 大客户(3.1%) / 赠送卡(2.5%)` | **业务数据** | 应从 API 获取 |
| AI 洞察区域 第2行 | `差异最大:酒水(+18%) / 台桌(-5%) / 包厢(+12%)` | **业务数据** | 应从 API 获取 |
| AI 洞察区域 第3行 | `建议关注:充值高但消耗低,会员活跃度需提升` | **业务数据** | 应从 API/AI 生成 |
| 平台服务费说明 | `服务费在流水流入时,平台已经扣除。不产生支出流水。` | UI 说明文案 | 可保留为前端常量 |
---
## 七、联调 TODO
### 高优先级P0 — 阻塞联调)
- [ ] **设计并对接财务看板主 API**一次请求返回全部板块数据overview / recharge / revenue / cashflow / expense / coachAnalysis或按板块拆分为 6 个接口
- [ ] **替换 `data` 中全部内联 mock**:将 120+ 个字段从 API 响应填充
- [ ] **`pageState` 初始值改为 `'loading'`**onLoad/onShow 发起请求,成功→`'normal'`,失败→`'error'`,空数据→`'empty'`
- [ ] **`onPullDownRefresh` 对接数据刷新**:替换当前的 `setTimeout 500ms` 空操作
- [ ] **`areaOptions` 从 API 获取**:当前硬编码的区域列表(大厅/A区/B区/C区/麻将房/团建房)应从门店配置接口动态获取
### 中优先级P1 — 影响数据准确性)
- [ ] **AI 洞察文案从 API 获取**:当前 WXML 中 3 行硬编码的洞察文案应由后端计算或 AI 生成
- [ ] **`filter-dropdown``label` 属性绑定动态值**:当前硬编码 `"本月"` / `"全部区域"`,应改为 `"{{selectedTimeText}}"` / `"{{selectedAreaText}}"`
- [ ] **激励课明细行补齐**WXML 板块6 激励课部分只渲染了合计行,缺少 `wx:for="{{coachAnalysis.incentive.rows}}"` 的明细行循环
- [ ] **时间/区域筛选参数传递给 API**`onTimeChange` / `onAreaChange` 变更后需重新请求数据
- [ ] **环比开关联动 API**`toggleCompare` 切换后,若环比数据需要额外请求,需对接
### 低优先级P2 — 优化项)
- [ ] **`tipContents` 指标解释考虑后端配置化**:当前 13 条解释硬编码在前端,如需动态更新可改为接口
- [ ] **`timeOptions` 考虑后端配置化**:如需支持自定义时间范围
- [ ] **加载态优化**:当前使用 toast 浮层,可考虑骨架屏
- [ ] **错误态重试**`onRetry` 当前只是重置 `pageState`,需对接真实重新请求
### 数据口径注意事项(参考 DWD-DOC 标杆文档)
- [ ] **助教费用拆分**API 返回的助教数据需区分 `assistant_pd_money`(陪打/基础课)和 `assistant_cx_money`(超休/激励课),禁止使用笼统的 `service_fee`
- [ ] **储值卡字段命名**API 应使用 `balance_pay`(总额)、`recharge_card_pay`(现金充值卡)、`gift_card_pay`(赠送卡),与 DWS 层一致
- [ ] **支付渠道恒等式**`balance_amount = recharge_card_amount + gift_card_amount`,前端展示需校验
- [ ] **折扣互斥**`discount_manual`(大客户优惠)与 `discount_other` 互斥,两者之和 = `adjust_amount`
- [ ] **consume_money 禁止直接用于计算**:前端展示的发生额应使用 `items_sum` 口径

View File

@@ -0,0 +1,142 @@
# chat-history 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/chat-history/chat-history
> 排查范围chat-history.ts / .wxml / .wxss / .json + 引用的 mock-data.ts / sort.ts / time.ts
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 5 字段 | 全部列表数据来自 `mockChatHistory`,无真实 API |
| B. 硬编码数据 | 5 处 | `ICON_GRADIENTS` 配色、延迟常量、回退值、跳转路径 |
| C. 已对接 API | 0 | 页面尚未对接任何后端接口 |
| D. 前端计算/派生 | 4 字段 | `timeLabel``iconGradient``pageState``statusBarHeight` |
| E. 路由参数 | 0 | 本页无接收路由参数(跳转时传出 `historyId` |
| F. WXML 硬编码文案 | 6 处 | 加载/错误/空态/底部提示等 UI 文案 |
---
## 一、Mock 数据(来自 mock-data.ts
数据源:`import { mockChatHistory } from '../../utils/mock-data'`
类型定义:`ChatHistoryItem`
| # | 字段 | 类型 | Mock 值示例 | 说明 |
|---|------|------|-------------|------|
| 1 | `id` | `string` | `'chat-001'` | 对话记录唯一 ID |
| 2 | `title` | `string` | `'张伟消费分析'` | 对话标题 |
| 3 | `lastMessage` | `string` | `'已记录。下次回访张伟时...'` | 最后一条消息摘要 |
| 4 | `timestamp` | `string` | `'2026-03-05T14:02:03+08:00'` | ISO 8601 时间戳 |
| 5 | `customerName` | `string?` | `'张伟'` / `undefined` | 可选,关联客户名 |
Mock 数据条数4 条(`mockChatHistory` 数组长度)
---
## 二、硬编码数据
| # | 位置 | 值 | 说明 |
|---|------|-----|------|
| 1 | `chat-history.ts` 顶部常量 `ICON_GRADIENTS` | 6 组 CSS 渐变色 | VI 规范 §6.2 AI 图标配色系统indigo/purple/red/orange/yellow/blue |
| 2 | `chat-history.ts``loadData()` | `setTimeout(..., 400)` | 模拟网络延迟 400ms联调时需替换为真实异步调用 |
| 3 | `chat-history.ts``onPullDownRefresh()` | `setTimeout(() => wx.stopPullDownRefresh(), 600)` | 下拉刷新 600ms 后停止,联调时需与真实请求回调绑定 |
| 4 | `chat-history.ts``onLoad()` | `sysInfo.statusBarHeight \|\| 44` | 状态栏高度回退值 44当前 WXML 中未消费该值) |
| 5 | `chat-history.ts``onItemTap()` | `'/pages/chat/chat?historyId=' + id` | 跳转路径硬编码 |
---
## 三、已对接 API
**无。** 页面当前 0 个真实 API 调用。
代码中有明确 TODO 注释:
```typescript
// TODO: 替换为真实 API 调用
const sorted = sortByTimestamp(mockChatHistory)
```
---
## 四、前端计算/派生数据
| # | 字段 | 计算逻辑 | 依赖 |
|---|------|----------|------|
| 1 | `timeLabel` | `formatRelativeTime(item.timestamp)` | `time.ts` → 将 ISO 时间戳转为「刚刚/N分钟前/N小时前/N天前/MM-DD/YYYY-MM-DD」 |
| 2 | `iconGradient` | `ICON_GRADIENTS[Math.floor(Math.random() * ICON_GRADIENTS.length)]` | 从 6 色渐变数组中随机取一个(每次渲染结果不同) |
| 3 | `pageState` | 状态机:`'loading'``'normal'` / `'empty'` / `'error'` | 根据 `list.length === 0` 判断空态,`catch` 进入错误态 |
| 4 | `statusBarHeight` | `wx.getWindowInfo().statusBarHeight \|\| 44` | 系统 API 获取(当前 WXML 未消费此值) |
---
## 五、路由参数
**本页无接收路由参数。** `onLoad()` 未读取 `options` 参数。
传出参数(跳转时):
| 目标页 | 参数 | 来源 |
|--------|------|------|
| `/pages/chat/chat` | `historyId={item.id}` | 列表项的 `id` 字段(当前为 Mock |
---
## 六、WXML 硬编码文案
| # | 位置 | 文案 | 说明 |
|---|------|------|------|
| 1 | 加载态 toast | `加载中...` | loading 浮层文案 |
| 2 | 错误态 emoji | `😵` | 错误图标emoji |
| 3 | 错误态文案 | `加载失败,请重试` | 错误提示 |
| 4 | 错误态按钮 | `重新加载` | 重试按钮文案 |
| 5 | 空态描述 | `暂无对话记录` | `<t-empty description="暂无对话记录" />` |
| 6 | 底部提示 | `— 已加载全部记录 —` | 列表底部 footer |
---
## 七、联调 TODO
### 7.1 必须替换的 Mock 数据
| 优先级 | 改动项 | 当前状态 | 联调目标 |
|--------|--------|----------|----------|
| P0 | `mockChatHistory` → 真实 API | `loadData()` 中直接引用 mock 数组 | 调用后端 `GET /api/chat/history` 或类似接口,返回 `ChatHistoryItem[]` |
| P0 | 移除 `setTimeout(…, 400)` 模拟延迟 | 硬编码 400ms | 替换为 `wx.request` / 封装的 `http.get()` 异步调用 |
| P1 | 下拉刷新绑定真实请求 | `onPullDownRefresh` 中 600ms 后固定停止 | 在 API 回调中调用 `wx.stopPullDownRefresh()` |
### 7.2 需确认的接口契约
| 字段 | 问题 | 建议 |
|------|------|------|
| `id` | Mock 为 `chat-001` 格式,后端实际 ID 格式? | 确认是 UUID / 自增 ID / 其他 |
| `title` | 对话标题由谁生成?后端还是前端? | 确认接口是否返回 `title` |
| `lastMessage` | 是否需要截断?最大长度? | 确认接口返回的是完整消息还是摘要 |
| `timestamp` | 后端返回格式ISO 8601 / Unix ms | `formatRelativeTime` 两种都支持 |
| `customerName` | 可选字段,后端是否一定返回? | 确认接口契约中该字段的 nullable 语义 |
### 7.3 需关注的前端逻辑
| 项目 | 说明 |
|------|------|
| `iconGradient` 随机分配 | 每次渲染/刷新颜色会变,是否需要按 `id` 哈希固定? |
| `statusBarHeight` 未使用 | `data.statusBarHeight` 已计算但 WXML 中无消费,可能是自定义导航栏残留代码 |
| 分页/无限滚动 | 当前一次性加载全部,数据量大时需要分页 |
| 错误态重试 | `onRetry` 调用 `loadData()`,联调后需确认错误处理(网络超时/401/500 等) |
| `import { mockChatHistory }` | 联调完成后需删除此 import 及 `mock-data.ts` 中对应导出 |
### 7.4 引用的工具函数(无需改动)
| 函数 | 文件 | 用途 | 联调影响 |
|------|------|------|----------|
| `sortByTimestamp()` | `utils/sort.ts` | 按 `timestamp` 降序排列 | 纯函数,无需改动 |
| `formatRelativeTime()` | `utils/time.ts` | 时间戳 → 相对时间文案 | 纯函数,无需改动 |
### 7.5 引用的组件
| 组件 | 路径 | 数据依赖 |
|------|------|----------|
| `ai-float-button` | `/components/ai-float-button/` | 无 props 传入,组件内部自治 |
| `dev-fab` | `/components/dev-fab/` | 开发调试用浮动按钮,上线前移除 |
| `t-loading` | TDesign | 无数据依赖 |
| `t-icon` | TDesign | `name`/`size`/`color` 硬编码 |
| `t-empty` | TDesign | `description` 硬编码 |

View File

@@ -0,0 +1,217 @@
# chat 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/chat/chat
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 4 | 来自 `mock-data.ts` 或页面内联 mock联调时需替换为 API |
| B. 硬编码数据 | 3 | 直接写死在 data 或方法中的值 |
| C. 已对接 API | 0 | 当前页面无任何真实 API 调用 |
| D. 前端计算/派生数据 | 6 | 由工具函数或页面逻辑计算得出 |
| E. 路由参数 | 1 | 从 `onLoad(options)` 获取 |
| F. WXML 硬编码文案 | 8 | 模板中直接写死的中文文案 |
---
## 一、Mock 数据
### A1. `mockChatMessages` — 历史消息列表
| 项目 | 内容 |
|------|------|
| 来源 | `import { mockChatMessages } from '../../utils/mock-data'` |
| 类型 | `ChatMessage[]` |
| 使用位置 | `loadMessages()``enrichMessages(mockChatMessages)``this.setData({ messages })` |
| 涉及字段 | `id`, `role`, `content`, `timestamp`, `referenceCard`(含 `type`, `title`, `summary`, `data` |
| 联调替换 | 需替换为 **AI 对话历史 API**(如 `GET /api/chat/messages?customerId=xxx`),返回分页消息列表 |
| 当前 mock 条数 | 6 条(`msg-001` ~ `msg-006` |
### A2. `mockAIReplies` — AI 回复模板(页面内联 mock
| 项目 | 内容 |
|------|------|
| 来源 | `chat.ts` 顶部常量 `const mockAIReplies = [...]` |
| 类型 | `string[]`3 条固定回复文案) |
| 使用位置 | `triggerAIReply()``mockAIReplies[this._msgCounter % mockAIReplies.length]` |
| 联调替换 | 需替换为 **AI 对话流式接口**(如 `POST /api/chat/send`SSE/WebSocket 流式返回) |
| 当前值 | `['根据数据分析,这位客户近期消费频次有所下降...', '好的,我已经记录了你的需求...', '这位客户偏好中式台球...']` |
### A3. `simulateStreamOutput` — 模拟流式输出
| 项目 | 内容 |
|------|------|
| 来源 | `import { simulateStreamOutput } from '../../utils/chat'` |
| 类型 | 工具函数,逐字 `setTimeout(tick, 50)` 模拟打字效果 |
| 使用位置 | `triggerAIReply()` 中调用,将 mock 回复逐字输出 |
| 联调替换 | 替换为真实 SSE/WebSocket 流式读取逻辑,`callback` 接口可复用 |
### A4. `referenceCard` — 页面顶部引用卡片(条件构造)
| 项目 | 内容 |
|------|------|
| 来源 | `loadMessages()` 中根据 `customerId` 内联构造 |
| 代码 | `{ title: '客户详情', summary: \`正在查看客户 ${customerId} 的相关信息\` }` |
| 联调替换 | 需从 **客户详情 API** 获取客户名称等信息填充,或由跳转页面通过路由参数传入 |
---
## 二、硬编码数据
### B1. `statusBarHeight` 回退值
| 项目 | 内容 |
|------|------|
| 字段 | `statusBarHeight` |
| 位置 | `onLoad()` → `sysInfo.statusBarHeight \|\| 44` |
| 当前值 | `44`(回退默认值) |
| 应获取自 | `wx.getWindowInfo().statusBarHeight`已获取44 仅为兜底) |
| 风险等级 | **低** — 兜底值合理,不同机型差异可忽略 |
### B2. `loadMessages` 延迟时间
| 项目 | 内容 |
|------|------|
| 字段 | `setTimeout(() => { ... }, 500)` |
| 位置 | `loadMessages()` 方法 |
| 当前值 | `500`ms 模拟网络延迟 |
| 联调处理 | 替换为真实 API 调用后移除 `setTimeout`,改用 API 响应回调 |
| 风险等级 | **中** — 联调时必须移除,否则造成不必要的 500ms 延迟 |
### B3. `triggerAIReply` 延迟时间
| 项目 | 内容 |
|------|------|
| 字段 | `setTimeout(() => { this.triggerAIReply() }, 300)` |
| 位置 | `onSendMessage()` 末尾 |
| 当前值 | `300`ms 延迟触发 AI 回复 |
| 联调处理 | 替换为发送消息 API 成功回调后触发流式接收 |
| 风险等级 | **中** — 联调时需重构为 API 驱动的流程 |
---
## 三、已对接 API
**无。** 当前页面 0 个真实 API 调用。
> 注:项目中已存在 `utils/request.ts`(封装了 `wx.request`),联调时可直接使用。
---
## 四、前端计算/派生数据
### D1. `enrichMessages()` 派生字段
对每条消息计算以下展示字段:
| 派生字段 | 计算逻辑 | 来源函数 |
|----------|----------|----------|
| `timeLabel` | 相对时间文案(刚刚/N分钟前/MM-DD | `formatRelativeTime(m.timestamp)` |
| `imTimeLabel` | IM 时间格式HH:mm / MM-DD HH:mm | `formatIMTime(m.timestamp)` |
| `showTimeDivider` | 是否显示时间分割线首条必显示间隔≥5分钟显示 | `shouldShowTimeDivider(prev, curr)` |
| `referenceCard.dataList` | 将 `Record<string,string>` 转为 `{key,value}[]` 供 `wx:for` | `toDataList(m.referenceCard.data)` |
### D2. 用户发送消息时的派生字段
| 派生字段 | 计算逻辑 |
|----------|----------|
| `userMsg.timeLabel` | 固定为 `'刚刚'` |
| `userMsg.imTimeLabel` | `formatIMTime(new Date().toISOString())` |
| `userMsg.showTimeDivider` | `shouldShowTimeDivider(prevTs, now)` |
| `userMsg.id` | `msg-user-${this._msgCounter}`(自增计数器) |
| `userMsg.timestamp` | `new Date().toISOString()`(客户端当前时间) |
### D3. AI 回复消息的派生字段
同 D2 逻辑,`id` 格式为 `msg-ai-${this._msgCounter}`。
### D4. `pageState` 状态机
| 状态值 | 触发条件 |
|--------|----------|
| `'loading'` | `loadMessages()` 开始时 |
| `'empty'` | 消息列表为空且无引用卡片 |
| `'normal'` | 消息加载成功且有内容 |
| `'error'` | `loadMessages()` 捕获异常 |
### D5. `isStreaming` / `streamingContent`
| 字段 | 说明 |
|------|------|
| `isStreaming` | AI 流式回复进行中标志,控制输入框禁用和打字指示器 |
| `streamingContent` | 流式输出的当前内容片段 |
### D6. `scrollToId`
滚动锚点,通过 `scrollToBottom()` 方法设置为 `'scroll-bottom'`,驱动 `scroll-view` 滚动到底部。
---
## 五、路由参数
### E1. `customerId`
| 项目 | 内容 |
|------|------|
| 获取方式 | `onLoad(options)` → `options?.customerId \|\| ''` |
| 用途 | 1. 传入 `loadMessages(customerId)` 加载对应客户的对话2. 决定是否显示页面顶部引用卡片 |
| 当前状态 | 仅用于 mock 逻辑判断,未传入任何 API |
| 联调需求 | 作为 API 请求参数传入消息列表接口 |
---
## 六、WXML 硬编码文案
| 编号 | 文案 | 位置 | 说明 |
|------|------|------|------|
| F1 | `加载中...` | 加载态 `g-toast-loading-text` | 通用加载文案,可保留 |
| F2 | `加载失败,请重试` | 错误态 `error-text` | 通用错误文案,可保留 |
| F3 | `重新加载` | 错误态 `retry-btn-text` | 按钮文案,可保留 |
| F4 | `引用内容` | 引用卡片 `reference-tag` | 标签文案,可保留 |
| F5 | `你好,我是 AI 助手` | 空对话提示 `empty-text` | 欢迎语,建议后续从配置获取 |
| F6 | `有什么可以帮你的?` | 空对话提示 `empty-sub` | 副标题,建议后续从配置获取 |
| F7 | `输入消息...` | 输入框 `placeholder` | 占位文案,可保留 |
| F8 | `AI 助手` | `chat.json` → `navigationBarTitleText` | 导航栏标题,可保留 |
---
## 七、联调 TODO
### 优先级 P0阻塞联调
- [ ] **替换消息列表加载**`loadMessages()` 中 `mockChatMessages` → 调用 `GET /api/chat/messages` 接口
- 入参:`customerId`(可选)、分页参数
- 出参:`ChatMessage[]`
- 移除 `setTimeout(..., 500)` 模拟延迟
- [ ] **替换 AI 回复流程**`triggerAIReply()` 中 `mockAIReplies` + `simulateStreamOutput` → 调用 `POST /api/chat/send` 流式接口
- 入参:`content`(用户消息)、`customerId`、`conversationId`
- 出参SSE/WebSocket 流式文本
- 保留 `isStreaming` 状态控制和逐字渲染逻辑
- [ ] **替换引用卡片数据**`loadMessages()` 中内联构造的 `referenceCard` → 从客户详情 API 或路由参数获取真实客户信息
### 优先级 P1联调后优化
- [ ] **消息发送 API**`onSendMessage()` 中用户消息需先通过 API 持久化,再触发 AI 回复
- [ ] **对话会话管理**:当前无 `conversationId` 概念,需增加会话创建/恢复逻辑
- [ ] **消息 ID 生成**:当前使用前端自增计数器 `_msgCounter`,联调后应使用服务端返回的 ID
### 优先级 P2体验优化
- [ ] **错误重试**`onRetry()` 当前仅重新调用 `loadMessages()`,需增加 toast 提示
- [ ] **空对话欢迎语**F5/F6 硬编码文案考虑从后端配置获取
- [ ] **移除 `simulateStreamOutput`**:联调完成后,`utils/chat.ts` 中的模拟函数可删除(保留 `simulateStreamOutputSync` 用于测试)
- [ ] **移除 `mockAIReplies`**:页面内联 mock 常量清理
- [ ] **移除 `mock-data.ts` 中 `mockChatMessages` 引用**:确认其他页面不再使用后,清理 `ChatMessage` 相关 mock
### 依赖确认
| 依赖项 | 状态 | 说明 |
|--------|------|------|
| `utils/request.ts` | ✅ 已存在 | 封装了 `wx.request`,可直接使用 |
| AI 对话 API后端 | ❌ 待确认 | 需确认接口设计REST vs WebSocket、流式协议 |
| 客户详情 API | ❌ 待确认 | 引用卡片需要客户基本信息 |
| 对话会话 API | ❌ 待确认 | 会话创建、历史会话列表 |

View File

@@ -0,0 +1,272 @@
# coach-detail 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/coach-detail/coach-detail
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据(页面内联) | 7 组 | 页面 .ts 文件顶部定义的大量 mock 常量 |
| A. Mock 数据mock-data.ts | 1 处 | 仅引用 `mockCoaches` 做 ID→名称匹配 |
| B. 硬编码数据 | 12 处 | perfCards 文案/sub、tierNodes、动画参数、avatar 路径等 |
| C. 已对接 API | 0 | 全部数据均为 mock无真实 API 调用 |
| D. 前端计算/派生 | 8 处 | perfCards 组装、incomeTotal、perfPercent、进度条参数等 |
| E. 路由参数 | 1 个 | `options.id``coachId` |
| F. WXML 硬编码文案 | 22 处 | 各 section 标题、按钮文案、空态提示等 |
---
## 一、Mock 数据
### A1. 页面内联 Mockcoach-detail.ts 顶部常量)
| 变量名 | 类型 | 字段数 | 用途 | 联调替换目标 |
|--------|------|--------|------|-------------|
| `mockCoachDetail` | `CoachDetail` | 12 个顶层字段 | 助教基本信息 + 绩效 + 收入 + 备注 | `GET /api/xcx/coaches/:id` |
| `mockVisibleTasks` | `TaskItem[]` | 6 条 | 当前可见任务列表 | `GET /api/xcx/coaches/:id/tasks` |
| `mockHiddenTasks` | `TaskItem[]` | 3 条 | 折叠区任务列表 | 同上(分页/分组) |
| `mockAbandonedTasks` | `AbandonedTask[]` | 2 条 | 已放弃任务 | 同上status=abandoned |
| `mockTopCustomers` | `TopCustomer[]` | 20 条 | 客户关系 TOP20 | `GET /api/xcx/coaches/:id/top-customers` |
| `mockServiceRecords` | `ServiceRecord[]` | 4 条 | 近期服务明细 | `GET /api/xcx/coaches/:id/service-records` |
| `mockHistoryMonths` | `HistoryMonth[]` | 5 条 | 历史月度汇总 | `GET /api/xcx/coaches/:id/history` |
#### mockCoachDetail 字段明细
| 字段路径 | Mock 值 | 说明 |
|----------|---------|------|
| `id` | `'coach-001'` | 助教 ID |
| `name` | `'小燕'` | 助教姓名 |
| `avatar` | `'/assets/images/avatar-coach.png'` | 头像(硬编码路径) |
| `level` | `'星级'` | 助教等级 |
| `skills` | `['中🎱', '🎯斯诺克']` | 技能标签 |
| `workYears` | `3` | 工龄 |
| `customerCount` | `68` | 客户数 |
| `hireDate` | `'2023-03-15'` | 入职日期 |
| `performance.monthlyHours` | `87.5` | 本月定档业绩(小时) |
| `performance.monthlySalary` | `6950` | 本月工资(预估) |
| `performance.customerBalance` | `86200` | 客源储值余额 |
| `performance.tasksCompleted` | `38` | 本月任务完成数 |
| `performance.perfCurrent` | `80` | 绩效当前值 |
| `performance.perfTarget` | `100` | 绩效目标值 |
| `income.thisMonth[]` | 4 条 | 本月收入明细(基础课时费/激励课时费/充值提成/酒水提成) |
| `income.lastMonth[]` | 4 条 | 上月收入明细 |
| `notes[]` | 3 条 | 备注记录 |
#### mockVisibleTasks / mockHiddenTasks 字段明细
| 字段 | 说明 |
|------|------|
| `typeLabel` | 任务类型文案(高优先召回/优先召回/关系构建/客户回访) |
| `typeClass` | 样式类名 |
| `customerName` | 客户姓名 |
| `noteCount` | 备注数量 |
| `pinned` | 是否置顶 |
| `notes[]` | 备注列表pinned/text/date |
#### mockAbandonedTasks 字段明细
| 字段 | 说明 |
|------|------|
| `customerName` | 客户姓名 |
| `reason` | 放弃原因(客户拒绝/超时未响应) |
#### mockTopCustomers 字段明细20 条)
| 字段 | 说明 |
|------|------|
| `id` | 客户 ID |
| `name` | 客户姓名 |
| `initial` | 姓氏首字(头像占位) |
| `avatarGradient` | 头像渐变色 key |
| `heartEmoji` | 爱心 emoji/💛/🤍) |
| `score` | 亲密度评分 |
| `scoreColor` | 评分颜色 key |
| `serviceCount` | 服务次数 |
| `balance` | 储值余额 |
| `consume` | 消费金额 |
#### mockServiceRecords 字段明细4 条)
| 字段 | 说明 |
|------|------|
| `customerId` | 客户 ID可选 |
| `customerName` | 客户姓名 |
| `initial` | 姓氏首字 |
| `avatarGradient` | 头像渐变色 key |
| `type` | 服务类型(基础课/激励课) |
| `typeClass` | 样式类名 |
| `table` | 台号 |
| `duration` | 时长 |
| `income` | 收入 |
| `date` | 日期 |
| `perfHours` | 定档绩效时长(可选) |
#### mockHistoryMonths 字段明细5 条)
| 字段 | 说明 |
|------|------|
| `month` | 月份标签(本月/上月/4月/3月/2月 |
| `estimated` | 是否预估 |
| `customers` | 服务客户数 |
| `hours` | 业绩时长 |
| `salary` | 工资 |
| `callbackDone` | 回访完成数 |
| `recallDone` | 召回完成数 |
### A2. 外部 Mock 引用mock-data.ts
| 引用 | 用途 | 代码位置 |
|------|------|----------|
| `mockCoaches` | `loadData()` 中通过 `mockCoaches.find(c => c.id === id)` 匹配助教 ID`id``name` 覆盖 mockCoachDetail | `coach-detail.ts` L197-198 |
> `mockCoaches` 来自 `utils/mock-data.ts`,类型为 `CoachCard[]`,含 3 条记录coach-001/002/003
---
## 二、硬编码数据
| 位置 | 硬编码内容 | 说明 |
|------|-----------|------|
| `loadData()` perfCards[0].sub | `'折算前 89.0h'` | 应由 API 返回折算前时长 |
| `loadData()` perfCards[1].sub | `'含预估部分'` | 固定文案 |
| `loadData()` perfCards[2].sub | `` `${detail.customerCount}位客户合计` `` | 模板拼接customerCount 来自 mock |
| `loadData()` perfCards[3].sub | `'覆盖 22 位客户'` | 应由 API 返回覆盖客户数 |
| `loadData()` tierNodes | `[0, 100, 130, 160, 190, 220]` | 绩效档位节点,注释标注"Mock实际由接口返回" |
| `loadData()` maxHours | `220` | 进度条最大值,应与 tierNodes 联动 |
| `data.taskStats` | `{ recall: 24, callback: 14 }` | 任务统计,应由 API 返回 |
| 动画常量 SHINE_SPEED | `70` | 进度条动画速度 |
| 动画常量 SPARK_DELAY_MS | `-150` | 火花延迟 |
| 动画常量 SPARK_DUR_MS | `1400` | 火花持续时间 |
| 动画常量 NEXT_LOOP_DELAY_MS | `400` | 下一轮延迟 |
| 动画常量 SHINE_WIDTH_RPX / TRACK_WIDTH_RPX | `120 / 634` | 进度条尺寸参数UI 常量,非业务数据) |
---
## 三、已对接 API
**无。** 当前页面 0 个真实 API 调用。
`loadData()` 使用 `setTimeout(500ms)` 模拟异步加载,内部全部使用 mock 数据。
代码中有两处 TODO 注释标记了待对接接口:
1. `// TODO: 替换为真实 API 调用 GET /api/coaches/:id` — `loadData()` 内
2. `// TODO: 替换为真实 API 调用 POST /api/xcx/notes` — `onNoteConfirm()` 内
---
## 四、前端计算/派生数据
| 字段 | 计算逻辑 | 依赖数据 |
|------|----------|----------|
| `perfCards[]` | 由 `detail.performance` 各字段组装为展示卡片数组 | `performance.*` (mock) |
| `perfGap` | `perfTarget - perfCurrent` | `performance.perfTarget/perfCurrent` (mock) |
| `perfPercent` | `Math.min(Math.round(perfCurrent/perfTarget*100), 100)` | 同上 |
| `pbFilledPct` | `Math.min(100, Math.round(totalHours/maxHours*1000)/10)` | `performance.monthlyHours` + 硬编码 `maxHours=220` |
| `pbCurrentTier` | 遍历 tierNodes 找到当前所在档位 | `performance.monthlyHours` + 硬编码 `tierNodes` |
| `pbTicks[]` | `buildTicks(tierNodes, maxHours)` — 计算每个档位的 left 百分比 | 硬编码 `tierNodes` + `maxHours` |
| `incomeTotal` | `items.reduce()` 累加当前 tab 的收入金额 | `detail.income.thisMonth/lastMonth` (mock) |
| `currentIncome` | 根据 `incomeTab` 切换本月/上月数据 | `detail.income.*` (mock) |
| `sortedNotes` | `sortByTimestamp(detail.notes, 'timestamp')` 按时间降序排列 | `detail.notes` (mock) |
| `pbShineDurMs` | `calcShineDur(filledPct)` — 根据填充百分比计算动画时长 | `pbFilledPct` (派生) |
| `pbClampedSparkPct` | `Math.max(0, Math.min(100, pbFilledPct))` | `pbFilledPct` (派生) |
| 新备注 `newNote` | `onNoteConfirm()` 中用 `Date.now()` 生成 ID 和时间 | 用户输入 `content` + 当前时间 |
---
## 五、路由参数
| 参数 | 来源 | 用途 |
|------|------|------|
| `options.id` | `onLoad(options)` 从页面路由 query 获取 | 赋值给 `coachId`,传入 `loadData(id)` 用于匹配 mockCoaches |
---
## 六、WXML 硬编码文案
| 文案 | 位置 | 说明 |
|------|------|------|
| `加载中...` | 加载态 toast | 状态提示 |
| `未找到助教信息` | 空态 | 状态提示 |
| `加载失败` | 错误态 | 状态提示 |
| `点击重试` | 错误态按钮 | 操作文案 |
| `绩效概览` | section 标题 | 固定文案 |
| `绩效档位进度` | 进度条标题 | 固定文案 |
| `距下一档还差 {{perfGap}}h` | 进度条提示 | 模板 + 派生数据 |
| `收入明细` | section 标题 | 固定文案 |
| `本月` / `上月` | 收入 Tab | 固定文案 |
| `预估` | 收入 Tab 标签 | 固定文案 |
| `合计(预估)` / `合计` | 收入合计行 | 条件文案 |
| `任务执行` | section 标题 | 固定文案 |
| `本月完成` | 任务统计标签 | 固定文案 |
| `回访` / `召回` + `` | 任务统计 | 固定文案 |
| `收起 ↑` / `展开全部 ↓` | 任务折叠按钮 | 条件文案 |
| `客户关系 TOP20` | section 标题 | 固定文案 |
| `近60天` | 客户关系副标题 | 固定文案(应由 API 返回时间范围) |
| `近期服务明细` | section 标题 | 固定文案 |
| `查看更多服务记录 →` | 服务明细底部 | 操作文案 |
| `更多信息` | section 标题 | 固定文案 |
| `入职日期` | 更多信息标签 | 固定文案 |
| `备注记录` + `共 {{sortedNotes.length}} 条` | section 标题 | 固定 + 动态 |
| `暂无备注` | 备注空态 | 状态提示 |
| `问问助手` | 底部按钮 | 操作文案 |
| `备注` | 底部按钮 | 操作文案 |
| `备注已保存` | Toast 提示 | 操作反馈 |
| `页面跳转失败` | 多处 navigateTo fail | 错误提示 |
| 历史表头:`月份` / `服务客户` / `访/召完成` / `业绩时长` / `工资` | 历史月度表格 | 固定文案 |
---
## 七、引用组件
| 组件 | 路径 | 用途 |
|------|------|------|
| `coach-level-tag` | `/components/coach-level-tag/` | 助教等级标签(接收 `level` prop |
| `perf-progress-bar` | `/components/perf-progress-bar/` | 绩效进度条(接收多个动画参数 prop |
| `note-modal` | `/components/note-modal/` | 备注弹窗(接收 `visible`/`customerName`emit `confirm`/`cancel` |
| `dev-fab` | `/components/dev-fab/` | 开发调试浮动按钮 |
| `t-loading` | TDesign | 加载动画 |
| `t-icon` | TDesign | 图标 |
| `t-tag` | TDesign | 标签(已注册但 WXML 中未使用) |
---
## 八、引用工具函数
| 函数 | 来源 | 用途 |
|------|------|------|
| `sortByTimestamp` | `utils/sort.ts` | 对备注列表按 `timestamp` 字段降序排序 |
---
## 九、联调 TODO 清单
### 优先级 P0 — 核心数据
| # | 待对接接口 | 替换目标 | 涉及字段 |
|---|-----------|----------|----------|
| 1 | `GET /api/xcx/coaches/:id` | `mockCoachDetail` 全部字段 | 基本信息、绩效、收入、备注 |
| 2 | `GET /api/xcx/coaches/:id/tasks` | `mockVisibleTasks` + `mockHiddenTasks` + `mockAbandonedTasks` | 任务列表(含备注) |
| 3 | `GET /api/xcx/coaches/:id/top-customers` | `mockTopCustomers` | 客户关系 TOP20 |
| 4 | `GET /api/xcx/coaches/:id/service-records` | `mockServiceRecords` | 近期服务明细 |
| 5 | `GET /api/xcx/coaches/:id/history` | `mockHistoryMonths` | 历史月度汇总 |
| 6 | `POST /api/xcx/notes` | `onNoteConfirm()` 内联构造 | 新增备注 |
### 优先级 P1 — 硬编码需接口化
| # | 硬编码项 | 当前值 | 建议 |
|---|---------|--------|------|
| 7 | `tierNodes` | `[0,100,130,160,190,220]` | 由接口 #1 返回绩效档位配置 |
| 8 | `taskStats` | `{recall:24, callback:14}` | 由接口 #2 返回任务统计汇总 |
| 9 | perfCards[0].sub `'折算前 89.0h'` | 硬编码 | 由接口 #1 返回折算前时长 |
| 10 | perfCards[3].sub `'覆盖 22 位客户'` | 硬编码 | 由接口 #1 返回覆盖客户数 |
| 11 | WXML `近60天` | 硬编码 | 由接口 #3 返回统计时间范围 |
### 优先级 P2 — 清理项
| # | 项目 | 说明 |
|---|------|------|
| 12 | 删除页面内联 mock 常量 | 7 个 mock 变量 + 接口类型定义迁移到 types |
| 13 | 移除 `import { mockCoaches }` | 联调后不再需要 mock-data.ts 依赖 |
| 14 | 移除 `setTimeout(500ms)` 模拟延迟 | 替换为真实异步请求 |
| 15 | `t-tag` 组件注册但未使用 | 从 JSON 中移除或确认是否需要 |

View File

@@ -0,0 +1,221 @@
# customer-detail 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/customer-detail/customer-detail
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 1 处 import + 1 处页面内联 | `mockCustomerDetail` 已 import 但未使用;`mockRecords` 内联定义 |
| B. 硬编码数据 | 7 个数据块 | `detail``aiInsight``clues``coachTasks``favoriteCoaches``sortedNotes``consumptionRecords` |
| C. 已对接 API | 0 | 页面无任何 `request()` 调用 |
| D. 前端计算/派生 | 3 处 | `aiColor` 随机、`phoneVisible` 切换、`pageState` 状态机 |
| E. 路由参数 | 0 | `onLoad(options)` 未读取任何参数 |
| F. WXML 硬编码文案 | 14 处 | 标题、标签、按钮文字等 |
---
## 一、Mock 数据
### 1.1 `mockCustomerDetail`(已 import 未使用)
| 项目 | 说明 |
|------|------|
| 来源 | `import { mockCustomerDetail } from "../../utils/mock-data"` |
| 状态 | **已 import 但页面内未引用**,属于死代码 |
| mock-data.ts 中的字段 | `id`, `name`, `avatar`, `tags`, `heartScore`, `phone`, `spiIndex`, `consumptionRecords[]` |
| 联调动作 | 删除此 import页面 `detail` 对象改为从 API 获取 |
### 1.2 `mockRecords`(页面内联 mock
| 项目 | 说明 |
|------|------|
| 来源 | 页面顶部 `const mockRecords: ConsumptionRecord[]` 硬编码 3 条记录 |
| 赋值位置 | `data.consumptionRecords: mockRecords` |
| 字段清单 | 每条记录含:`id`, `type`, `date`, `tableName`, `startTime`, `endTime`, `duration`, `tableFee`, `tableOrigPrice`, `coaches[]``name`, `level`, `levelColor`, `courseType`, `hours`, `perfHours`, `fee`, `foodAmount`, `foodOrigPrice`, `totalAmount`, `totalOrigPrice`, `payMethod`, `rechargeAmount` |
| 联调 API | `GET /api/xcx/customer/{member_id}/consumption-records`(待开发) |
| 风险 | 🔴 高 — 金额字段涉及 `tableFee`/`totalAmount` 等财务数据,需严格对齐 DWD-DOC 口径 |
---
## 二、硬编码数据
### 2.1 `detail` 对象(客户基础信息)
| 字段 | 硬编码值 | 应对接来源 | 风险 |
|------|----------|-----------|------|
| `id` | `"cust_001"` | API 返回 `member_id` | 🔴 高 |
| `name` | `"王先生"` | API `dim_member.nickname` | 🔴 高 |
| `avatarChar` | `"王"` | 前端从 `name` 截取首字 | 🟡 中 |
| `phone` | `"13812345678"` | API `dim_member.mobile`(注意 DQ-6需通过 `member_id` JOIN `dim_member` | 🔴 高 |
| `balance` | `"8,600"` | API 储值余额(`balance_pay` | 🔴 高 |
| `consumption60d` | `"2,800"` | API 近60天消费汇总 | 🔴 高 |
| `idealInterval` | `"7天"` | API / AI 计算 | 🟡 中 |
| `daysSinceVisit` | `"12天"` | API 最后到店距今天数 | 🔴 高 |
**联调 API**`GET /api/xcx/customer/{member_id}/profile`(待开发)
### 2.2 `aiInsight` 对象AI 智能洞察)
| 字段 | 硬编码值 | 应对接来源 | 风险 |
|------|----------|-----------|------|
| `summary` | 长文本("高价值 VIP 客户…" | AI 缓存 API `GET /api/xcx/ai-cache?cache_type=customer_insight&target_id={member_id}` | 🔴 高 |
| `strategies[]` | 3 条策略,含 `color` + `text` | 同上AI 缓存返回 | 🔴 高 |
**联调 API**`GET /api/xcx/ai-cache`(已有路由 `xcx_ai_cache.py`,需确认 `cache_type` 枚举是否包含 `customer_insight`
### 2.3 `clues` 数组(维客线索)
共 7 条硬编码线索:
| 索引 | category | text 摘要 | source | 应对接来源 | 风险 |
|------|----------|----------|--------|-----------|------|
| 0 | 客户基础 | 🎂 生日 3月15日 · VIP会员 · 注册2年 | 系统 | API 维客线索 | 🔴 高 |
| 1 | 消费习惯 | 🌙 常来夜场 · 月均4-5次 | 系统 | API 维客线索 | 🔴 高 |
| 2 | 消费习惯 | 💰 高客单价(含 detail | 系统 | API 维客线索 | 🔴 高 |
| 3 | 玩法偏好 | 🎱 偏爱中式 · 斯诺克进阶中 | 系统 | API 维客线索 | 🔴 高 |
| 4 | 促销接受 | 🍷 爱点酒水套餐(含 detail | 系统 | API 维客线索 | 🔴 高 |
| 5 | 社交关系 | 👥 常带朋友(含 detail | 系统 | API 维客线索 | 🔴 高 |
| 6 | 重要反馈 | ⚠️ 想练斯诺克走位… | 小燕 | API 维客线索 | 🔴 高 |
**联调 API**`GET /api/retention-clue/{member_id}?site_id=X`(已有路由 `member_retention_clue.py`
### 2.4 `coachTasks` 数组(助教任务分配)
共 4 条硬编码任务卡片:
| 索引 | name | taskType | 字段清单 | 风险 |
|------|------|----------|---------|------|
| 0 | 小燕 | 高优先召回 | `level`, `levelColor`, `taskColor`, `bgClass`, `status`, `lastService`, `metrics[]`近60天次数/总时长/次均时长) | 🔴 高 |
| 1 | 泡芙 | 优先召回 | 同上 + `status: "pinned"` | 🔴 高 |
| 2 | Amy | 关系构建 | 同上 | 🔴 高 |
| 3 | Lucy | 客户回访 | 同上 + `status: "abandoned"` | 🔴 高 |
**联调 API**`GET /api/xcx/customer/{member_id}/coach-tasks`(待开发;后端 `biz.coach_tasks` 表已存在)
### 2.5 `favoriteCoaches` 数组(最喜欢的助教)
共 2 条硬编码:
| 索引 | name | 字段清单 | 风险 |
|------|------|---------|------|
| 0 | 小燕 | `emoji`, `relationIndex: "9.2"`, `indexColor`, `bgClass`, `stats[]`(基础/激励/上课/充值) | 🔴 高 |
| 1 | 泡芙 | 同上,`relationIndex: "7.8"` | 🔴 高 |
**联调 API**`GET /api/xcx/customer/{member_id}/favorite-coaches`(待开发;需 DWS 层助教-客户关系聚合)
### 2.6 `sortedNotes` 数组(备注记录)
共 3 条硬编码备注:
| 索引 | tagLabel | createdAt | content 摘要 | 风险 |
|------|----------|-----------|-------------|------|
| 0 | 管理员 | 2026-03-05 14:30 | 对斯诺克课程感兴趣… | 🔴 高 |
| 1 | 小燕 | 2026-02-20 16:45 | 客户反馈服务态度很好… | 🔴 高 |
| 2 | 管理员 | 2026-02-10 10:00 | 储值活动当天即充值… | 🔴 高 |
**联调 API**`GET /api/xcx/notes?target_type=member&target_id={member_id}`(已有路由 `xcx_notes.py`
### 2.7 其他硬编码值
| 位置 | 字段 | 硬编码值 | 说明 | 风险 |
|------|------|----------|------|------|
| `data` | `pageState` | `"loading"` | 初始状态,正常 | 🟢 低 |
| `data` | `phoneVisible` | `false` | UI 状态,正常 | 🟢 低 |
| `data` | `loadingMore` | `false` | UI 状态,正常 | 🟢 低 |
| `data` | `noteModalVisible` | `false` | UI 状态,正常 | 🟢 低 |
| `loadDetail()` | `pageState` | 直接设为 `"normal"` | ⚠️ 无 API 调用,直接跳过 loading | 🔴 高 |
| WXML | 手机号掩码 | `'138****5678'` | 硬编码掩码,应从 `detail.phone` 动态生成 | 🟡 中 |
---
## 三、已对接 API
| API | 状态 |
|-----|------|
| (无) | 页面当前 **零 API 调用**`loadDetail()` 为空壳 |
---
## 四、前端计算/派生数据
| 字段 | 计算逻辑 | 说明 |
|------|----------|------|
| `aiColor` | `onLoad` 中随机从 6 色数组取值 | 用于 AI 洞察卡片配色,纯 UI 装饰 |
| `phoneVisible` | `onTogglePhone()` 切换布尔值 | 控制手机号显示/隐藏 |
| `pageState` | `loadDetail()` 中设为 `"normal"` | 应改为loading → API 成功 normal / 失败 error / 空 empty |
---
## 五、路由参数
| 参数 | 状态 | 说明 |
|------|------|------|
| `options.id` / `options.memberId` | ❌ 未读取 | `onLoad(options)` 接收了 `options` 但未使用任何参数 |
**联调时必须**:从 `options` 中读取 `memberId`(或 `id`),作为所有 API 请求的入参。
---
## 六、WXML 中的硬编码文案
| 位置 | 文案 | 是否需要动态化 | 说明 |
|------|------|---------------|------|
| 导航栏 | `"客户详情"` | ❌ 保持 | JSON 配置 `navigationBarTitleText` |
| loading 态 | `"加载中..."` | ❌ 保持 | 通用文案 |
| empty 态 | `"未找到客户信息"` | ❌ 保持 | 通用文案 |
| error 态 | `"加载失败"` | ❌ 保持 | 通用文案 |
| error 按钮 | `"点击重试"` | ❌ 保持 | 通用文案 |
| banner 统计标签 | `"储值余额"` / `"60天消费"` / `"理想间隔"` / `"距今到店"` | ❌ 保持 | 固定标签 |
| AI 卡片标题 | `"AI 智能洞察"` | ❌ 保持 | 固定标题 |
| 策略标题 | `"当前推荐策略"` | ❌ 保持 | 固定标题 |
| 维客线索标题 | `"维客线索"` | ❌ 保持 | 固定标题 |
| 助教任务标题 | `"助教任务分配"` / `"当前进行中"` | ❌ 保持 | 固定标题 |
| 最喜欢助教标题 | `"最喜欢的助教"` / `"近60天"` | ❌ 保持 | 固定标题 |
| 消费记录 | `"消费记录"` / `"商城订单"` / `"总金额"` / `"🍷 食品酒水"` | ❌ 保持 | 固定标签 |
| 底部按钮 | `"问问助手"` / `"备注"` | ❌ 保持 | 固定文案 |
| 空态提示 | `"暂无消费记录"` / `"暂无备注"` | ❌ 保持 | 通用文案 |
---
## 七、联调 TODO
### 优先级 P0页面核心数据阻塞上线
| # | 数据块 | 当前状态 | 需要的 API | 后端现状 |
|---|--------|---------|-----------|---------|
| 1 | 客户基础信息 `detail` | 全部硬编码 | `GET /api/xcx/customer/{member_id}/profile` | ❌ 待开发 |
| 2 | 消费记录 `consumptionRecords` | 内联 mock | `GET /api/xcx/customer/{member_id}/consumption-records` | ❌ 待开发 |
| 3 | 路由参数 `memberId` | 未读取 | 从 `onLoad(options)` 读取 | — |
| 4 | `loadDetail()` 空壳 | 直接设 normal | 改为真实 API 调用 + 状态管理 | — |
### 优先级 P1重要业务数据
| # | 数据块 | 当前状态 | 需要的 API | 后端现状 |
|---|--------|---------|-----------|---------|
| 5 | 维客线索 `clues` | 全部硬编码 | `GET /api/retention-clue/{member_id}` | ✅ 已有路由 |
| 6 | 备注记录 `sortedNotes` | 全部硬编码 | `GET /api/xcx/notes?target_type=member&target_id={member_id}` | ✅ 已有路由 |
| 7 | AI 智能洞察 `aiInsight` | 全部硬编码 | `GET /api/xcx/ai-cache?cache_type=customer_insight&target_id={member_id}` | ⚠️ 路由已有,需确认 cache_type |
### 优先级 P2增强功能
| # | 数据块 | 当前状态 | 需要的 API | 后端现状 |
|---|--------|---------|-----------|---------|
| 8 | 助教任务 `coachTasks` | 全部硬编码 | `GET /api/xcx/customer/{member_id}/coach-tasks` | ❌ 待开发(`biz.coach_tasks` 表已有) |
| 9 | 最喜欢助教 `favoriteCoaches` | 全部硬编码 | `GET /api/xcx/customer/{member_id}/favorite-coaches` | ❌ 待开发(需 DWS 聚合) |
### 前端清理项
| # | 项目 | 说明 |
|---|------|------|
| 10 | 删除 `import { mockCustomerDetail }` | 死代码import 后未使用 |
| 11 | 删除 `const mockRecords` 内联 mock | 替换为 API 数据 |
| 12 | 手机号掩码硬编码 `'138****5678'` | 改为从 `detail.phone` 动态生成掩码 |
| 13 | `ConsumptionRecord` interface | 页面内重复定义,应移至 `utils/types.ts` 或复用 `mock-data.ts` 中的类型 |
### 注意事项
- **DQ-6 会员手机号断档**`settlement_head.member_phone` 自 2025-12 起全为 NULL后端 API 必须通过 `member_id` JOIN `dim_member.mobile` 获取
- **金额口径**:消费记录中的金额字段需严格对齐 DWD-DOC 标杆文档,禁止直接使用 `consume_money`
- **助教费用拆分**:消费记录中的 `coaches[].fee` 需区分陪打(`assistant_pd_money`)和超休(`assistant_cx_money`

View File

@@ -0,0 +1,178 @@
# 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' })` |

View File

@@ -0,0 +1,144 @@
# login 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/login/login
> 涉及文件login.ts, login.wxml, login.wxss, login.json, utils/request.ts, utils/config.ts
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 0 | 未引用 mock-data.ts无内联 mock |
| B. 硬编码数据 | 5 | 含开发模式 openid、默认高度、错误提示文案等 |
| C. 已对接 API | 2 | dev-login + login含 token 刷新机制) |
| D. 前端计算/派生数据 | 3 | isDevMode 判断、按钮禁用态、错误消息映射 |
| E. 路由参数 | 0 | onLoad 未接收任何 options 参数 |
| F. WXML 硬编码文案 | 7 | 应用名称、功能标签、按钮文案、协议文案、底部提示 |
---
## 一、Mock 数据
**结论:无 Mock 数据。**
- `login.ts` 未 import `mock-data.ts`
- 页面内无内联 mock 对象或假数据
- 开发模式走 `/api/xcx/dev-login` 真实接口(非前端 mock
---
## 二、硬编码数据
| # | 位置 | 代码 | 值 | 风险 | 说明 |
|---|------|------|----|------|------|
| B1 | login.ts L4 | `API_BASE.startsWith("http://127.0.0.1")` | `"http://127.0.0.1"` | 🟢 低 | 开发环境判断config.ts develop 分支返回 `http://127.0.0.1:8000`,逻辑一致 |
| B2 | login.ts L8 | `data: { agreed: false, loading: false }` | `false` | 🟢 低 | UI 初始状态,合理默认值 |
| B3 | login.ts L10 | `statusBarHeight: 20` | `20` | 🟡 中 | 默认状态栏高度 fallbackonLoad 中会被 `wx.getWindowInfo()` 覆盖;但若 API 返回 0 或 undefined 则回退到 20px部分机型可能不准 |
| B4 | login.ts L39 | `data: { openid: "dev_test_openid" }` | `"dev_test_openid"` | 🟡 中 | 开发模式固定 openid仅 develop 环境生效;但若 `isDevMode` 判断逻辑被绕过则有安全隐患 |
| B5 | login.ts L93-98 | 错误提示 `msg` 三元表达式 | `"账号已被禁用"` / `"登录凭证无效,请重试"` / `"登录失败,请稍后重试"` | 🟢 低 | 用户友好提示,无需后端下发 |
---
## 三、已对接 API
### 3.1 开发模式登录 — `POST /api/xcx/dev-login`
| 项目 | 值 |
|------|----|
| 触发条件 | `isDevMode === true`API_BASE 以 `http://127.0.0.1` 开头) |
| 请求方式 | `POST` |
| 请求参数 | `{ openid: "dev_test_openid" }` |
| needAuth | `false` |
| 响应字段(使用到的) | `access_token`, `refresh_token`, `user_id`, `user_status` |
| 后续处理 | 写入 globalData + Storage → 按 `user_status` 路由跳转 |
### 3.2 正式登录 — `POST /api/xcx/login`
| 项目 | 值 |
|------|----|
| 触发条件 | `isDevMode === false`trial / release 环境) |
| 前置步骤 | `wx.login()` 获取临时 `code` |
| 请求方式 | `POST` |
| 请求参数 | `{ code: loginRes.code }` |
| needAuth | `false` |
| 响应字段(使用到的) | `access_token`, `refresh_token`, `user_id`, `user_status` |
| 后续处理 | 同 3.1 |
### 3.3 Token 刷新request.ts 内置) — `POST /api/xcx/refresh`
| 项目 | 值 |
|------|----|
| 触发条件 | 任意需认证请求返回 401 时自动触发 |
| 请求参数 | `{ refresh_token: <当前 refresh_token> }` |
| 响应字段 | `access_token`, `refresh_token` |
| 失败处理 | 清除 token → `wx.reLaunch` 跳转 login 页 |
### 登录成功后数据持久化
```
globalData.token ← data.access_token
globalData.refreshToken ← data.refresh_token
globalData.authUser ← { userId: data.user_id, status: data.user_status }
Storage: token, refreshToken, userId, userStatus
```
### 登录后路由映射
| user_status | 跳转页面 |
|-------------|----------|
| `"approved"` | `/pages/task-list/task-list` |
| `"pending"` | `/pages/reviewing/reviewing` |
| `"new"` | `/pages/apply/apply` |
| `"rejected"` | `/pages/no-permission/no-permission` |
| `"disabled"` | `/pages/no-permission/no-permission` |
| 其他/default | `/pages/apply/apply` |
---
## 四、前端计算/派生数据
| # | 变量/逻辑 | 来源 | 说明 |
|---|-----------|------|------|
| D1 | `isDevMode` | `API_BASE.startsWith("http://127.0.0.1")` | 模块顶层常量,决定走 dev-login 还是 wx.login 流程 |
| D2 | 按钮禁用态 | WXML: `(!agreed \|\| loading) ? 'login-btn--disabled' : 'login-btn--active'` | 由 `agreed``loading` 两个 data 字段派生 |
| D3 | 错误消息 `msg` | `err.statusCode` 三元映射 | 403→"账号已被禁用"401→"登录凭证无效",其他→"登录失败" |
---
## 五、路由参数
**结论:无路由参数。**
- `onLoad()` 未声明 `options` 参数
- 页面作为小程序首页(入口页),不接收外部传参
---
## 六、WXML 硬编码文案
| # | 元素 | 文案 | 建议 |
|---|------|------|------|
| F1 | `<text class="app-name">` | `球房运营助手` | 应用名称,可考虑从配置读取 |
| F2 | `<text class="app-desc">` | `为台球厅提升运营效率的内部管理工具` | 应用描述 |
| F3 | `<text class="feature-text">` ×3 | `任务管理` / `数据看板` / `智能助手` | 功能亮点标签 |
| F4 | `<text class="login-btn-text">` | `使用微信登录` | 按钮文案 |
| F5 | `<text class="agreement-text">` | `我已阅读并同意` | 协议前缀 |
| F6 | `<text class="link">` ×2 | `《用户协议》` / `《隐私政策》` | 协议链接文案(当前无跳转) |
| F7 | `<text class="footer-tip">` | `仅限球房内部员工使用` | 底部提示 |
> login.json 中 `"navigationBarTitleText": "登录"` 也是硬编码,但因使用 `"navigationStyle": "custom"` 自定义导航栏,系统标题栏不显示,实际无影响。
---
## 七、联调 TODO
| 优先级 | 项目 | 当前状态 | 待办 |
|--------|------|----------|------|
| ✅ 已完成 | 登录接口对接 | dev-login + login 均已对接 | — |
| ✅ 已完成 | Token 持久化 | globalData + Storage 双写 | — |
| ✅ 已完成 | 401 自动刷新 | request.ts 内置 refresh 机制 | — |
| ✅ 已完成 | 状态路由 | 5 种 user_status 均有对应页面 | — |
| 🟡 待确认 | 协议链接跳转 | `《用户协议》``《隐私政策》`仅为文案,无 `bindtap` 跳转 | 需补充协议页面或 webview 跳转 |
| 🟡 待确认 | dev_test_openid 安全性 | 硬编码在前端代码中 | 确认后端 dev-login 仅在开发环境可用,生产环境应禁用该端点 |
| 🟢 可优化 | statusBarHeight fallback | 默认 20px | 可改为 `wx.getSystemInfoSync().statusBarHeight` 更精确的 fallback |
| 🟢 可优化 | 错误提示国际化 | 硬编码中文 | 当前仅面向内部员工,优先级低 |

View File

@@ -0,0 +1,94 @@
# my-profile 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/my-profile/my-profile
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据 | 1 | `mockUserProfile` — 用户信息全量 mock |
| 硬编码数据 | 5 | 路由映射、弹窗文案、菜单 key 等 |
| 已对接 API | 0 | ⚠️ 无任何 API 调用 |
| 前端计算/派生 | 1 | TabBar 选中态同步 |
| 路由参数 | 0 | 无 |
| WXML 硬编码文案 | 5 | 菜单文字、弹窗文案 |
## 一、Mock 数据
### 1.1 `mockUserProfile`(🔴 高风险 — 页面核心数据全量 mock
| 属性 | 值 |
|------|-----|
| 来源文件 | `utils/mock-data.ts` |
| 导入方式 | `import { mockUserProfile } from '../../utils/mock-data'` |
| 赋值位置 | `data.userInfo = mockUserProfile`(页面初始化) |
| 类型定义 | `UserProfile { name, avatar, role, storeName }` |
**Mock 值:**
| 字段 | Mock 值 | 联调后应来自 |
|------|---------|-------------|
| `name` | `'小燕'` | API — 用户昵称 |
| `avatar` | `'/assets/images/avatar-coach.png'` | API — 用户头像 URL |
| `role` | `'助教'` | API — 用户角色 |
| `storeName` | `'朗朗桌球'` | API — 所属门店名称 |
**影响范围:** 用户信息卡片区域(头像、姓名、角色标签、门店名)全部依赖此 mock。
## 二、硬编码数据
| # | 文件 | 位置 | 内容 | 风险等级 | 说明 |
|---|------|------|------|----------|------|
| 1 | `router.ts` | MENU_ROUTE_MAP | `'chat-history'``'/pages/chat-history/chat-history'` | 🟢 低 | 菜单路由映射 |
| 2 | `router.ts` | MENU_ROUTE_MAP | `'notes'``'/pages/notes/notes'` | 🟢 低 | 菜单路由映射 |
| 3 | `router.ts` | MENU_ROUTE_MAP | `'settings'``''`(空字符串) | 🟡 中 | 设置页未实现,点击无响应 |
| 4 | `.ts` | `onLogout()` | `title: '确认退出'`, `content: '确认退出当前账号吗?'` | 🟢 低 | 弹窗文案 |
| 5 | `.ts` | `onLogout()` | `confirmColor: '#e34d59'` | 🟢 低 | 确认按钮颜色 |
| 6 | `.ts` | `onLogout()` | `url: '/pages/login/login'` | 🟢 低 | 退出后跳转路径 |
## 三、已对接 API
⚠️ **无。** 本页面当前未调用任何 API。
代码中有两处 TODO 注释明确标注:
- `.ts` L4: `// TODO: 联调时替换为真实 API 获取用户信息`
- `.ts` L11: `// TODO: 联调时在此刷新用户信息`
**预期对接 API**
| 端点(推测) | 用途 | 替换目标 |
|--------------|------|----------|
| `GET /api/xcx/me` 或专用 profile 接口 | 获取当前用户信息 | `mockUserProfile` |
## 四、前端计算/派生数据
| # | 数据 | 来源 | 说明 |
|---|------|------|------|
| 1 | TabBar `active: 'my'` | `this.getTabBar().setData()` | `onShow` 时同步 custom-tab-bar 选中态 |
| 2 | `route` | `getMenuRoute(key)` 纯函数 | 从 `MENU_ROUTE_MAP` 查找菜单 key 对应的页面路径 |
## 五、路由参数
无。本页面为 TabBar 页面,不接收路由参数。
## 六、WXML 硬编码文案
| # | 内容 | 位置 | 建议 |
|---|------|------|------|
| 1 | `"备注记录"` | menu-text | 可保留 |
| 2 | `"助手对话记录"` | menu-text | 可保留 |
| 3 | `"退出账号"` | menu-text | 可保留 |
| 4 | 菜单图标路径 `/assets/icons/menu-notes.svg` 等 | menu-icon image src | 🟢 低,静态资源 |
| 5 | `visible="{{true}}"` | ai-float-button | 🟢 低,控制 AI 按钮显示 |
## 七、联调 TODO
| # | 优先级 | 事项 | 当前状态 | 说明 |
|---|--------|------|----------|------|
| 1 | 🔴 P0 | 替换 `mockUserProfile` 为真实 API 数据 | 未开始 | 页面核心数据全量 mock需对接用户信息接口 |
| 2 | 🟡 P1 | `onShow` 中添加 API 刷新逻辑 | 未开始 | 确保每次进入页面获取最新用户信息 |
| 3 | 🟡 P1 | 确认用户信息接口字段映射 | 未开始 | `name`/`avatar`/`role`/`storeName` 需与后端字段对齐 |
| 4 | 🟢 P2 | 移除 `mock-data.ts``mockUserProfile` 的导入 | 未开始 | API 对接完成后清理 |
**结论:** my-profile 页面当前处于纯 mock 状态,用户信息卡片(姓名、头像、角色、门店)全部来自硬编码 mock 数据,是联调优先级最高的待办项。菜单跳转和退出登录逻辑已实现,无需额外对接。

View File

@@ -0,0 +1,57 @@
# 无权限页面no-permission— 接口对接审计
> 审计时间2026-03-18
> 页面路径:`apps/miniprogram/miniprogram/pages/no-permission/`
> 文件清单:`no-permission.ts` / `.wxml` / `.wxss` / `.json`
---
## 一、总览
| 指标 | 值 |
|------|-----|
| 已对接 API 数量 | 1 |
| Mock 数据引用 | 0 |
| 硬编码字段数 | 4 |
| 对接就绪度 | ✅ 已对接 |
本页面功能简单:展示"无访问权限"提示,`onShow` 时调用 `/api/xcx/me` 查询最新用户状态,状态变化时自动跳转对应页面。核心逻辑已完成 API 对接。
---
## 二、已对接 API
| # | 接口 | 方法 | 用途 | 调用位置 |
|---|------|------|------|----------|
| 1 | `/api/xcx/me` | GET | 查询用户最新状态status根据状态路由跳转 | `_checkStatus()` |
---
## 三、Mock 数据
无。本页面未引用任何 mock 数据。
---
## 四、硬编码字段清单
| # | 位置 | 硬编码内容 | 类型 | 建议处理方式 |
|---|------|-----------|------|-------------|
| 1 | wxml `.reason-footer-value` | `厉超`(管理员姓名) | 文案 | 应从后端获取门店管理员信息,或写入配置 |
| 2 | wxml `.reason-item` × 3 | 三条原因说明文案 | 文案 | 可保留前端硬编码(纯展示文案,无业务逻辑) |
| 3 | wxml `.main-title` | `无访问权限` | 文案 | 可保留(固定标题) |
| 4 | wxml `.sub-title` | 副标题说明文案 | 文案 | 可保留(固定说明) |
---
## 五、对接建议
1. **管理员姓名硬编码**`厉超` 写死在 wxml 中,建议后端 `/api/xcx/me` 返回门店管理员联系信息,或新增配置接口
2. **状态路由映射**`_checkStatus()``approved` 状态跳转 `/pages/mvp/mvp`,该路径在当前页面目录中不存在,需确认是否为 tabBar 页面或后续新增页面
3. **其余文案**:原因说明、标题等为纯展示文案,无需接口化,保留硬编码即可
---
## 六、结论
本页面已完成核心 API 对接,唯一需要关注的是管理员姓名硬编码问题。整体对接就绪度高,无需大幅改动。

View File

@@ -0,0 +1,147 @@
# notes 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/notes/notes
> 源文件notes.ts / notes.wxml / notes.wxss / notes.json
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据 | 6 个字段 | 全部列表数据来自 `mockNotes`,无真实 API |
| 硬编码数据 | 5 处 | `statusBarHeight` 默认值、`setTimeout` 延迟、Modal 文案等 |
| 已对接 API | 0 个接口 | 页面尚未对接任何后端 API |
| 前端计算/派生 | 3 个字段 | `pageState``timeLabel``NoteDisplay` 扩展 |
| 路由参数 | 0 个 | `onLoad` 未读取 `options` |
| WXML 硬编码文案 | 5 处 | 加载/错误/空态/底部提示等 UI 文案 |
---
## 一、Mock 数据
### 1.1 数据源:`mockNotes`mock-data.ts
| 序号 | 字段 | 类型 | 说明 | 联调替换 API |
|------|------|------|------|-------------|
| 1 | `id` | `string` | 备注唯一 ID | `GET /api/xcx/notes` 返回 |
| 2 | `content` | `string` | 备注正文 | 同上 |
| 3 | `tagType` | `'customer' \| 'coach'` | 标签类型(客户/助教) | 同上 |
| 4 | `tagLabel` | `string` | 标签文案,如"客户:王先生" | 同上 |
| 5 | `createdAt` | `string` | 创建时间,格式 `YYYY-MM-DD HH:mm` | 同上 |
| 6 | `score` | `number \| undefined` | 满意度评分 0-10类型定义存在但 mock 数据未使用) | 同上 |
- 引用方式:`import { mockNotes } from '../../utils/mock-data'`notes.ts 第 1 行)
- mock 数据共 12 条,模拟 H5 原型 `notes.html` 中的备注列表
- `Note` 接口在 mock-data.ts 中被定义了两次(第 2 次覆盖第 1 次),第 2 次定义不含 `score` 字段
### 1.2 Mock 调用链
```
notes.ts → loadData()
→ mockNotes.map(n => ({ ...n, timeLabel: formatRelativeTime(n.createdAt) }))
→ setData({ notes, pageState })
```
`setTimeout(…, 400)` 模拟网络延迟notes.ts 第 28 行)。
---
## 二、硬编码数据
| 序号 | 字段/值 | 当前值 | 应改为 | 风险 | 所在位置 |
|------|---------|--------|--------|------|----------|
| 1 | `statusBarHeight` | `20`(默认值) | `wx.getWindowInfo().statusBarHeight` 已在 `onLoad` 中获取20 仅为 fallback | 🟢 低 | notes.ts 第 17 行 |
| 2 | `setTimeout` 延迟 | `400` ms | 删除,改为真实 API 异步调用 | 🟡 中 | notes.ts 第 28 行 |
| 3 | Modal `title` | `'删除备注'` | 可保留UI 文案),或走 i18n | 🟢 低 | notes.ts 第 56 行 |
| 4 | Modal `content` | `'确定要删除这条备注吗?删除后无法恢复。'` | 可保留UI 文案),或走 i18n | 🟢 低 | notes.ts 第 57 行 |
| 5 | Modal `confirmColor` | `'#e34d59'` | 应统一走设计系统色值变量 | 🟢 低 | notes.ts 第 58 行 |
---
## 三、已对接 API
**无。** 页面当前 0 个真实 API 调用。
代码中的 TODO 注释notes.ts 第 30 行):
```
// TODO: 替换为真实 API 调用 GET /api/xcx/notes
```
### 需要对接的 API 清单
| 序号 | 操作 | 预期 API | 方法 | 说明 |
|------|------|----------|------|------|
| 1 | 加载备注列表 | `GET /api/xcx/notes` | GET | 替换 `mockNotes`,支持分页 |
| 2 | 删除备注 | `DELETE /api/xcx/notes/{noteId}` | DELETE | 当前仅前端 filter 删除,未持久化 |
| 3 | 下拉刷新 | 同接口 1 | GET | `onPullDownRefresh` 已有骨架 |
---
## 四、前端计算/派生数据
| 序号 | 字段 | 计算逻辑 | 所在位置 |
|------|------|----------|----------|
| 1 | `pageState` | 根据数据加载结果派生:`'loading'``'normal'` / `'empty'` / `'error'` | notes.ts 第 16/27/33/37 行 |
| 2 | `timeLabel` | `formatRelativeTime(n.createdAt)` — 将 ISO 时间转为相对时间文案(刚刚/N分钟前/MM-DD 等) | notes.ts 第 32 行 |
| 3 | `NoteDisplay` | 扩展 `Note` 接口,附加 `timeLabel: string` | notes.ts 第 8-10 行 |
### `formatRelativeTime` 规则utils/time.ts
| 时间差 | 输出 |
|--------|------|
| < 120s | 刚刚 |
| 2min ~ 59min | N分钟前 |
| 1h ~ 23h | N小时前 |
| 1d ~ 3d | N天前 |
| > 3d 同年 | MM-DD |
| > 3d 跨年 | YYYY-MM-DD |
---
## 五、路由参数
`onLoad()` 未读取任何 `options` 参数。
当前页面作为独立备注列表页,不依赖路由传参。联调时需确认:
- 是否需要按客户 ID 过滤备注(`?customerId=xxx`
- 是否需要按任务 ID 过滤备注(`?taskId=xxx`
---
## 六、WXML 硬编码文案
| 序号 | 文案 | 位置 | 建议 |
|------|------|------|------|
| 1 | `加载中...` | `g-toast-loading-text`wxml 第 6 行) | 全局加载组件文案,可保留 |
| 2 | `😵` | `error-icon`wxml 第 12 行) | emoji 作为错误图标 |
| 3 | `加载失败,请重试` | `error-text`wxml 第 13 行) | 可保留 |
| 4 | `重新加载` | `retry-btn-text`wxml 第 15 行) | 可保留 |
| 5 | `暂无备注记录` | `t-empty description`wxml 第 22 行) | 可保留 |
| 6 | `— 已加载全部记录 —` | `footer-text`wxml 第 40 行) | 联调后需改为分页加载逻辑,此文案需动态化 |
---
## 七、组件依赖
| 组件 | 来源 | 说明 |
|------|------|------|
| `t-icon` | tdesign-miniprogram | 删除图标 |
| `t-loading` | tdesign-miniprogram | 加载动画 |
| `t-empty` | tdesign-miniprogram | 空态占位 |
| `ai-float-button` | `/components/ai-float-button/` | AI 悬浮按钮(仅正常态显示) |
| `dev-fab` | `/components/dev-fab/` | 开发调试按钮(全局) |
---
## 八、联调 TODO
- [ ] **对接备注列表 API**`GET /api/xcx/notes` 替换 `mockNotes`,移除 `import { mockNotes } from '../../utils/mock-data'`
- [ ] **对接删除 API**`DELETE /api/xcx/notes/{noteId}` 替换前端 filter 逻辑,增加失败回滚
- [ ] **移除 `setTimeout` 模拟延迟**:改为真实异步请求
- [ ] **确认路由参数**:是否需要 `customerId` / `taskId` 过滤
- [ ] **分页支持**:当前一次性加载全部,联调时需确认是否需要分页/无限滚动
- [ ] **底部文案动态化**`— 已加载全部记录 —` 需根据分页状态切换为"加载更多"/"已加载全部"
- [ ] **删除确认交互**:当前仅前端删除 + Toast需确认后端删除成功后再更新 UI
- [ ] **错误处理增强**`loadData` 的 catch 仅设置 `pageState: 'error'`,需增加具体错误提示
- [ ] **清理 mock-data.ts 中 `Note` 重复定义**:接口定义了两次,联调时统一为后端返回类型
- [ ] **`confirmColor` 统一**`#e34d59` 应走设计系统变量,避免散落硬编码色值

View File

@@ -0,0 +1,225 @@
# performance-records 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/performance-records/performance-records
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据 | 1 | 从 `mock-data.ts` 导入但实际未使用于渲染 |
| B. 硬编码数据TS 内联) | 42 | `loadData()` 中手写的 dateGroups + 统计汇总,占页面数据主体 |
| C. 已对接 API | 0 | 无任何真实 API 调用 |
| D. 前端计算/派生数据 | 12 | `nameToAvatarColor()``formatMoney()``formatHours()``formatCount()`、月份切换逻辑 |
| E. 路由参数 | 0 | `onLoad()` 未读取 options |
| F. WXML 硬编码文案 | 14 | 标签文字、提示语、状态文案 |
---
## 一、Mock 数据(来自 mock-data.ts
| # | 字段/变量 | 来源 | 说明 |
|---|-----------|------|------|
| 1 | `allRecords` | `mockPerformanceRecords`mock-data.ts | `import { mockPerformanceRecords }` 导入,赋值给 `allRecords`,但 **未参与任何渲染**;页面实际展示的 `dateGroups` 是 TS 内联硬编码,与此 mock 数据结构完全不同 |
> **注意**`mockPerformanceRecords` 的类型 `PerformanceRecord`(含 `id/customerName/amount/date/type/category`)与页面实际渲染的 `RecordItem` 接口(含 `hours/timeRange/courseType/location/income` 等)字段不匹配,说明 mock 数据是早期占位,后续内联硬编码覆盖了实际展示逻辑。
---
## 二、硬编码数据TS 内联)
### 2.1 Banner 区域Page data 初始值)
| # | 字段 | 硬编码值 | 位置 |
|---|------|----------|------|
| 1 | `coachName` | `'小燕'` | data 初始化 |
| 2 | `coachLevel` | `'星级'` | data 初始化 |
| 3 | `storeName` | `'球会名称店'` | data 初始化 |
### 2.2 月份切换Page data 初始值)
| # | 字段 | 硬编码值 | 位置 |
|---|------|----------|------|
| 4 | `currentYear` | `2026` | data 初始化 |
| 5 | `currentMonth` | `2` | data 初始化 |
| 6 | `monthLabel` | `'2026年2月'` | data 初始化 |
| 7 | `canGoPrev` | `true` | data 初始化 |
| 8 | `canGoNext` | `false` | data 初始化 |
### 2.3 统计概览loadData 中 setData
| # | 字段 | 硬编码值 | 位置 |
|---|------|----------|------|
| 9 | `totalCount` | `32` | loadData → setData |
| 10 | `totalHours` | `59.0` | loadData → setData |
| 11 | `totalIncome` | `4720` | loadData → setData |
| 12 | `hasMore` | `false` | loadData → setData |
### 2.4 dateGroups 内联数据loadData 中12 个日期分组,共 30 条记录)
每条记录包含以下硬编码字段:
| 字段 | 类型 | 示例值 | 说明 |
|------|------|--------|------|
| `id` | string | `'r1'` ~ `'r30'` | 记录唯一标识 |
| `customerName` | string | `'王先生'``'李女士'` 等 | 客户姓名 |
| `avatarChar` | string | `'王'``'李'` 等 | 头像首字 |
| `timeRange` | string | `'20:00-22:00'` | 服务时间段 |
| `hours` | number | `2.0``1.5``1.0` | 折算后课时 |
| `hoursRaw` | number? | `2.5`(部分记录有) | 折算前课时 |
| `courseType` | string | `'基础课'`/`'包厢课'`/`'打赏课'` | 课程类型文案 |
| `courseTypeClass` | string | `'tag-basic'`/`'tag-vip'`/`'tag-tip'` | 课程类型样式类 |
| `location` | string | `'3号台'`/`'VIP1号房'`/`'打赏'` | 服务位置 |
| `income` | number | `160`/`190`/`120`/`80` | 预估收入(元) |
日期分组级硬编码字段:
| 字段 | 类型 | 示例值 | 说明 |
|------|------|--------|------|
| `date` | string | `'2月7日'` ~ `'1月27日'` | 日期标签 |
| `totalHours` | number | `6.0`/`3.5`/`4.0` 等 | 当日总课时 |
| `totalIncome` | number | `510`/`280`/`320`/`350`/`470` | 当日总收入 |
**完整日期分组清单**12 组30 条记录):
| # | date | 记录数 | totalHours | totalIncome | 记录 ID |
|---|------|--------|------------|-------------|---------|
| 13 | 2月7日 | 3 | 6.0 | 510 | r1, r2, r3 |
| 14 | 2月6日 | 2 | 3.5 | 280 | r4, r5 |
| 15 | 2月5日 | 2 | 4.0 | 320 | r6, r7 |
| 16 | 2月4日 | 2 | 4.0 | 350 | r8, r9 |
| 17 | 2月3日 | 2 | 3.5 | 280 | r10, r11 |
| 18 | 2月2日 | 2 | 4.0 | 350 | r12, r13 |
| 19 | 2月1日 | 6 | 6.0 | 510 | r14~r19 |
| 20 | 1月31日 | 3 | 5.5 | 470 | r20~r22 |
| 21 | 1月30日 | 2 | 3.5 | 280 | r23, r24 |
| 22 | 1月29日 | 2 | 4.0 | 320 | r25, r26 |
| 23 | 1月28日 | 2 | 4.0 | 350 | r27, r28 |
| 24 | 1月27日 | 2 | 4.0 | 350 | r29, r30 |
---
## 三、已对接 API
**无。** 页面当前 0 个 API 调用。
`loadData()` 使用 `setTimeout(500ms)` 模拟异步,内部注释标注:
```typescript
// TODO: 替换为真实 API按月份请求
const allRecords = mockPerformanceRecords
```
---
## 四、前端计算/派生数据
| # | 字段 | 计算方式 | 依赖 |
|---|------|----------|------|
| 1 | `rec.avatarColor` | `nameToAvatarColor(name)` — 基于姓名首字 charCode 取模映射到 24 色板 | `utils/avatar-color.ts` |
| 2 | `totalCountLabel` | `formatCount(32, '笔')``'32笔'` | `utils/money.ts` |
| 3 | `totalHoursLabel` | `formatHours(59.0)``'59h'` | `utils/time.ts` |
| 4 | `totalHoursRawLabel` | `formatHours(63.5)``'63.5h'` | `utils/time.ts` |
| 5 | `totalIncomeLabel` | `formatMoney(4720)``'¥4,720'` | `utils/money.ts` |
| 6 | `group.totalHoursLabel` | `formatHours(n)` — 每个日期分组的课时格式化 | `utils/time.ts` |
| 7 | `group.totalIncomeLabel` | `formatMoney(n)` — 每个日期分组的收入格式化 | `utils/money.ts` |
| 8 | `fmt.hours(rec.hours)` | WXS `hours()` — WXML 中课时展示 | `utils/format.wxs` |
| 9 | `fmt.hours(rec.hoursRaw)` | WXS `hours()` — WXML 中折前课时展示 | `utils/format.wxs` |
| 10 | `fmt.money(rec.income)` | WXS `money()` — WXML 中收入展示 | `utils/format.wxs` |
| 11 | `monthLabel` | `switchMonth()` 中拼接 `` `${currentYear}年${currentMonth}月` `` | 月份切换逻辑 |
| 12 | `canGoNext` | `switchMonth()` 中与当前日期比较,不能超过当月 | 月份切换逻辑 |
---
## 五、路由参数
**无。** `onLoad()` 未接收任何 `options` 参数。
当前页面不知道:
- 当前登录助教是谁(`coachName`/`coachLevel` 硬编码)
- 所属门店(`storeName` 硬编码)
- 初始展示月份(硬编码 2026 年 2 月)
---
## 六、WXML 硬编码文案
| # | 文案 | 位置 | 说明 |
|---|------|------|------|
| 1 | `加载中...` | toast 加载浮层 | 加载状态提示 |
| 2 | `加载失败,请点击重试` | 错误态 | 错误提示文案 |
| 3 | `重试` | 错误态按钮 | 按钮文案 |
| 4 | `总记录` | 统计概览 stat-label | 统计标签 |
| 5 | `总业绩时长` | 统计概览 stat-label | 统计标签 |
| 6 | `预估`×2 处) | 统计概览 stat-hint | 时长和收入后的提示 |
| 7 | `折前` | 统计概览 stat-hours-raw | 折前课时前缀 |
| 8 | `收入` | 统计概览 stat-label | 统计标签 |
| 9 | `暂无数据` | 空态 | 空状态提示 |
| 10 | `` | 日期分隔线 dd-date | 日期后分隔符 |
| 11 | `·` | 日期分隔线 dd-stats | 课时与收入间分隔符 |
| 12 | `预估` | 日期分隔线 dd-stats | 日分组收入前缀 |
| 13 | `我的预估收入` | record-income | 记录行收入前缀 |
| 14 | `— 已加载全部记录 —` | list-end-hint | 列表底部提示 |
---
## 七、联调 TODO
### 7.1 需要对接的 API按优先级
| 优先级 | API | 用途 | 替换目标 |
|--------|-----|------|----------|
| P0 | 获取当前助教信息 | Banner 区域展示 | `coachName`、`coachLevel`、`storeName` 硬编码 |
| P0 | 业绩明细列表(按月分页) | 核心数据 | `loadData()` 中全部 `dateGroups` 内联硬编码 |
| P0 | 月度统计汇总 | 统计概览 | `totalCount`/`totalHours`/`totalIncome` 等硬编码 |
### 7.2 需要确认的数据结构
| 问题 | 说明 |
|------|------|
| `PerformanceRecord` vs `RecordItem` 接口不匹配 | mock-data.ts 中 `PerformanceRecord` 含 `amount/date/type/category`,页面实际渲染的 `RecordItem` 含 `hours/timeRange/courseType/location/income`,需与后端确认最终字段 |
| `hoursRaw`(折前课时)是否由后端返回 | 当前仅部分记录有此字段,需确认业务规则 |
| `courseType` 枚举值 | 当前有 `基础课`/`包厢课`/`打赏课` 三种,需确认是否完整 |
| `courseTypeClass` 样式映射 | 是前端根据 `courseType` 派生,还是后端直接返回 |
| 日期分组逻辑 | 由后端返回已分组数据,还是前端按日期聚合 |
| 分页机制 | 当前 `page`/`pageSize`/`hasMore` 已定义但未实际使用 |
| 月份切换 | 切换月份后应重新请求 API当前 `loadData()` 未传入年月参数 |
### 7.3 需要清理的代码
| 项目 | 说明 |
|------|------|
| 删除 `import { mockPerformanceRecords }` | mock 数据导入 |
| 删除 `import type { PerformanceRecord }` | mock 类型导入 |
| 删除 `loadData()` 中 `setTimeout` 模拟 | 替换为真实 API 调用 |
| 删除 `loadData()` 中 12 个 dateGroups 内联数据 | 约 120 行硬编码 |
| 删除 `allRecords` 字段 | 当前赋值了 mock 但未使用 |
| 确认 `onLoad` 是否需要接收路由参数 | 如助教 ID、初始月份等 |
### 7.4 数据一致性问题
| 问题 | 详情 |
|------|------|
| 2月4日 `totalHours=4.0` 但 `totalIncome=350` | `totalIncomeLabel` 用 `formatMoney(320)` 格式化,但 `totalIncome` 赋值 `350`,数值不一致 |
| 2月2日 同上 | `totalIncomeLabel=formatMoney(320)` 但 `totalIncome=350` |
| 1月28日 同上 | `totalIncomeLabel=formatMoney(320)` 但 `totalIncome=350` |
| 1月27日 同上 | `totalIncomeLabel=formatMoney(320)` 但 `totalIncome=350` |
| 统计汇总与明细不匹配 | `totalCount=32` 但 dateGroups 中实际只有 30 条记录r1~r30 |
| `totalHours=59.0` 未验证 | 各日期 totalHours 之和 = 6+3.5+4+4+3.5+4+6+5.5+3.5+4+4+4 = 52h ≠ 59h |
| `totalIncome=4720` 未验证 | 各日期 totalIncome 之和 = 510+280+320+350+280+350+510+470+280+320+350+350 = 4370 ≠ 4720 |
### 7.5 组件依赖
| 组件 | 来源 | 说明 |
|------|------|------|
| `coach-level-tag` | 自定义组件 `/components/coach-level-tag/` | 助教等级标签 |
| `ai-float-button` | 自定义组件 `/components/ai-float-button/` | AI 悬浮按钮 |
| `dev-fab` | 自定义组件 `/components/dev-fab/` | 开发调试按钮 |
| `t-icon` | TDesign `tdesign-miniprogram/icon` | 图标 |
| `t-loading` | TDesign `tdesign-miniprogram/loading` | 加载动画 |
### 7.6 静态资源
| 资源 | 路径 | 说明 |
|------|------|------|
| Banner 背景图 | `/assets/images/banner-bg-coral-aurora.svg` | 珊瑚极光渐变背景 |
| 助教头像 | `/assets/images/avatar-coach.png` | 默认助教头像 |

View File

@@ -0,0 +1,212 @@
# performance 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/performance/performance
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| A. Mock 数据(页面内联) | 4 组 | incomeItems / thisMonthRecords / newCustomers / regularCustomers全部在 `loadData()``setTimeout` 中硬构造 |
| B. 硬编码数据 | 11 个字段 | Banner 个人信息、收入档位、月度合计等,写死在 `data` 初始值中 |
| C. 已对接 API | 0 | 页面无任何 `wx.request` 调用,无 API 对接 |
| D. 前端计算/派生数据 | 3 个 | 展开/收起状态、AI 配色、pageState |
| E. 路由参数 | 0 | `onLoad()` 未读取任何路由参数 |
| F. WXML 硬编码文案 | 28+ 处 | 标题、标签、提示文字等 |
---
## 一、Mock 数据(页面内联 mock`loadData()` 内 setTimeout 构造)
> 页面未 import `utils/mock-data.ts`,所有 mock 数据均为 `loadData()` 方法内局部变量。
| # | 字段/变量 | 类型 | 位置 | 说明 |
|---|-----------|------|------|------|
| A1 | `incomeItems` | `IncomeItem[]` | `loadData()` 局部变量 | 4 条业绩明细:基础课 / 激励课 / 充值激励 / TOP3 销冠奖icon + label + desc + value 全部硬写 |
| A2 | `thisMonthRecords` | `DateGroup[]` | `loadData()` 局部变量 | 4 个日期组、共 7 条服务记录,含客户名 / 头像 / 时间 / 课时 / 课程类型 / 台位 / 收入 |
| A3 | `newCustomers` | `Array<{...}>` | `loadData()` 局部变量 | 8 条新客数据:姓名 / 头像 / 最近服务日期 / 服务次数 |
| A4 | `regularCustomers` | `Array<{...}>` | `loadData()` 局部变量 | 8 条常客数据:姓名 / 头像 / 累计课时 / 累计收入 / 服务次数 |
| A5 | `gradients` | `string[]` | `loadData()` 局部变量 | 头像配色数组 `['blue','pink','teal',...]`,用于给 mock 数据分配 avatarColor |
### Mock 数据内部字段明细
**incomeItems 每条字段:**
| 字段 | 示例值 | 联调时对应 API 字段(待定) |
|------|--------|---------------------------|
| `icon` | `'🎱'` | — |
| `label` | `'基础课'` | 收入类型名称 |
| `desc` | `'80元/h × 75h'` | 单价 × 课时的计算描述 |
| `value` | `'¥6,000'` | 该类型收入金额 |
**thisMonthRecords → DateGroup 字段:**
| 字段 | 示例值 | 联调时对应 API 字段(待定) |
|------|--------|---------------------------|
| `date` | `'2月7日'` | 服务日期 |
| `totalHours` | `'4.0h'` | 当日总课时 |
| `totalIncome` | `'¥350'` | 当日总收入 |
**thisMonthRecords → ServiceRecord 字段:**
| 字段 | 示例值 | 联调时对应 API 字段(待定) |
|------|--------|---------------------------|
| `customerName` | `'王先生'` | 客户姓名 |
| `avatarChar` | `'王'` | 姓氏首字(前端可派生) |
| `avatarColor` | `'blue'` | 头像配色(前端可派生) |
| `timeRange` | `'20:00-22:00'` | 服务时间段 |
| `hours` | `'2.0h'` | 服务时长 |
| `courseType` | `'基础课'` | 课程类型 |
| `courseTypeClass` | `'tag-basic'` | CSS 类名(前端派生) |
| `location` | `'3号台'` | 台位/包厢 |
| `income` | `'¥160'` | 该次服务预估收入 |
**newCustomers 每条字段:**
| 字段 | 示例值 | 联调时对应 API 字段(待定) |
|------|--------|---------------------------|
| `name` | `'王先生'` | 客户姓名 |
| `avatarChar` | `'王'` | 姓氏首字(前端可派生) |
| `avatarColor` | `'blue'` | 头像配色(前端可派生) |
| `lastService` | `'2月7日'` | 最近服务日期 |
| `count` | `2` | 服务次数 |
**regularCustomers 每条字段:**
| 字段 | 示例值 | 联调时对应 API 字段(待定) |
|------|--------|---------------------------|
| `name` | `'张先生'` | 客户姓名 |
| `avatarChar` | `'张'` | 姓氏首字(前端可派生) |
| `avatarColor` | `'teal'` | 头像配色(前端可派生) |
| `hours` | `12` | 累计课时number |
| `income` | `'¥960'` | 累计收入 |
| `count` | `6` | 服务次数 |
---
## 二、硬编码数据(`Page.data` 初始值)
| # | 字段 | 硬编码值 | 说明 |
|---|------|----------|------|
| B1 | `coachName` | `'小燕'` | 助教姓名,应从登录态/API 获取 |
| B2 | `coachRole` | `'助教'` | 角色标签,应从用户信息获取 |
| B3 | `storeName` | `'广州朗朗桌球'` | 门店名称,应从用户绑定门店获取 |
| B4 | `monthlyIncome` | `'¥6,206'` | 本月预计收入,应从 API 获取 |
| B5 | `lastMonthIncome` | `'¥16,880'` | 上月收入,应从 API 获取 |
| B6 | `currentTier.basicRate` | `80` | 当前档位基础课单价(元/h |
| B7 | `currentTier.incentiveRate` | `95` | 当前档位激励课单价(元/h |
| B8 | `nextTier.basicRate` | `90` | 下一档位基础课单价(元/h |
| B9 | `nextTier.incentiveRate` | `114` | 下一档位激励课单价(元/h |
| B10 | `upgradeHoursNeeded` | `15` | 距下一阶段所需课时 |
| B11 | `upgradeBonus` | `800` | 到达下一阶段奖金(元) |
| B12 | `monthlyTotal` | `'¥6,950.5'` | 本月合计预估收入 |
| B13 | `visibleRecordGroups` | `2` | 默认显示前 N 条日期组UI 配置,可保留硬编码) |
---
## 三、已对接 API
**无。** 页面中不存在任何 `wx.request`、API 服务调用、或从 `getApp()` 获取全局数据的逻辑。
`loadData()` 使用 `setTimeout(500ms)` 模拟异步加载,内部全部为内联 mock 数据。
文件头部注释明确标注:`// TODO: 联调时替换为真实 API 调用`
---
## 四、前端计算/派生数据
| # | 字段 | 来源 | 说明 |
|---|------|------|------|
| D1 | `pageState` | 前端状态机 | `'loading' → 'normal' / 'error' / 'empty'`,由 `loadData()` 控制 |
| D2 | `aiColor` | `initPageAiColor('performance')` | 从 `utils/ai-color-manager.ts` 获取,固定返回 `'blue'` |
| D3 | `thisMonthRecordsExpanded` | 前端 toggle | 服务记录展开/收起状态,默认 `false` |
| D4 | `newCustomerExpanded` | 前端 toggle | 新客列表展开/收起状态,默认 `false` |
| D5 | `regularCustomerExpanded` | 前端 toggle | 常客列表展开/收起状态,默认 `false` |
---
## 五、路由参数
**无。** `onLoad()` 未接收任何路由参数(`options` 未使用)。
页面作为业绩总览入口,当前设计不依赖路由传参。联调后可能需要接收 `coachId` / `month` 等参数。
---
## 六、WXML 硬编码文案
| # | 文案内容 | 位置 | 说明 |
|---|----------|------|------|
| F1 | `加载中...` | 加载态 toast | 可保留 |
| F2 | `暂无业绩数据` | 空数据态 | 可保留 |
| F3 | `加载失败,请点击重试` | 错误态 | 可保留 |
| F4 | `重试` | 错误态按钮 | 可保留 |
| F5 | `本月预计收入` | Banner 收入卡片标签 | 可保留 |
| F6 | `上月收入` | Banner 收入卡片标签 | 可保留 |
| F7 | `收入情况` | section 标题 | 可保留 |
| F8 | `当前档位`×2 | 档位卡片 badge + label | 可保留 |
| F9 | `元/h`×4 | 费率单位 | 可保留 |
| F10 | `基础课到手`×2 | 费率描述 | 可保留 |
| F11 | `激励课到手`×2 | 费率描述 | 可保留 |
| F12 | `下一阶段`×2 | 档位卡片 badge + label | 可保留 |
| F13 | `距离下一阶段` | 升级提示标签 | 可保留 |
| F14 | `需完成 ... 小时` | 升级提示(含动态插值) | 可保留 |
| F15 | `到达即得` | 升级奖金标签 | 可保留 |
| F16 | `元`(奖金后缀) | 升级奖金值 | 可保留 |
| F17 | `本月业绩 预估` | section 标题 | 可保留 |
| F18 | `本月合计 预估` | 合计行标签 | 可保留 |
| F19 | `📋 我的服务记录明细` | 服务记录 header | 可保留 |
| F20 | `展开更多` / `收起` | 服务记录 toggle | 可保留 |
| F21 | `查看全部` | 跳转业绩记录按钮 | 可保留 |
| F22 | `我的预估收入` | 服务记录行内文案 | 可保留 |
| F23 | `我的新客` | section 标题 | 可保留 |
| F24 | `最近服务:` | 新客详情前缀 | 可保留 |
| F25 | `次` | 新客服务次数后缀 | 可保留 |
| F26 | `查看更多 ↓` / `收起 ↑` | 新客/常客 toggle | 可保留 |
| F27 | `我的常客` | section 标题 | 可保留 |
| F28 | `📊` / `🎯` / `⏱️` | 档位/升级提示 emoji | 可保留 |
| F29 | `业绩详情` | `performance.json``navigationBarTitleText` | 可保留 |
---
## 七、联调 TODO
### 7.1 需要对接的 API按优先级排序
| 优先级 | API 用途 | 涉及字段 | 备注 |
|--------|----------|----------|------|
| P0 | 助教个人信息 | B1 `coachName`, B2 `coachRole`, B3 `storeName` | 可从登录态/全局 store 获取 |
| P0 | 本月/上月收入概览 | B4 `monthlyIncome`, B5 `lastMonthIncome` | Banner 核心数据 |
| P0 | 收入档位信息 | B6-B9 `currentTier.*`, `nextTier.*`, B10 `upgradeHoursNeeded`, B11 `upgradeBonus` | 档位规则可能来自配置表 |
| P0 | 本月业绩明细 | A1 `incomeItems`, B12 `monthlyTotal` | 基础课/激励课/充值激励/奖金 |
| P1 | 服务记录列表(按日期分组) | A2 `thisMonthRecords` | 需支持分页或按月查询 |
| P1 | 新客列表 | A3 `newCustomers` | 本月新服务的客户 |
| P1 | 常客列表 | A4 `regularCustomers` | 累计服务数据 |
### 7.2 联调时需要处理的事项
- [ ] 移除 `loadData()` 中的 `setTimeout` 模拟延迟
- [ ] 移除所有内联 mock 数据incomeItems / thisMonthRecords / newCustomers / regularCustomers
- [ ] 替换 Banner 区域硬编码字段coachName / coachRole / storeName / monthlyIncome / lastMonthIncome
- [ ] 替换收入档位硬编码currentTier / nextTier / upgradeHoursNeeded / upgradeBonus / monthlyTotal
- [ ] 添加 `wx.request` 或封装的 API 调用
- [ ] 处理 API 错误 → 设置 `pageState: 'error'`
- [ ] 处理空数据 → 设置 `pageState: 'empty'`
- [ ] `avatarChar` / `avatarColor` / `courseTypeClass` 等前端派生字段需确认是后端返回还是前端计算
- [ ] `onLoad()` 可能需要接收路由参数(如 `coachId``month`
- [ ] 金额格式化(`¥` 前缀、千分位)确认由后端返回还是前端处理
- [ ] 服务记录的 `taskId` 字段当前 mock 中未提供,但 `onRecordTap` 已预留读取逻辑
### 7.3 页面引用的外部依赖
| 依赖 | 类型 | 来源 | 数据影响 |
|------|------|------|----------|
| `ai-color-manager.ts` | 工具函数 | `utils/` | 仅提供 AI 图标配色,无业务数据 |
| `ai-float-button` | 自定义组件 | `components/` | AI 悬浮按钮,无业务数据 |
| `dev-fab` | 自定义组件 | `components/` | 开发调试按钮,无业务数据 |
| `metric-card` | 自定义组件 | `components/` | 已注册但 WXML 中未使用 |
| `t-icon` / `t-loading` | TDesign 组件 | `tdesign-miniprogram` | UI 组件,无业务数据 |
| `mock-data.ts` | Mock 数据文件 | `utils/` | **未被本页面引用**(页面使用内联 mock |
### 7.4 注意事项
1. 页面文件头部已有 `// TODO: 联调时替换为真实 API 调用` 注释
2. `loadData()` 内部也有 `// TODO: 替换为真实 API` 注释
3. `performance.json` 中注册了 `metric-card` 组件但 WXML 中未使用,联调时可清理或启用
4. 金额字段涉及助教收入计算,联调时需参考 DWD-DOC 标杆文档中的助教费用拆分规则(`assistant_pd_money` 陪打 / `assistant_cx_money` 超休)

View File

@@ -0,0 +1,98 @@
# reviewing 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/reviewing/reviewing
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据 | 0 | 无 mock 引用 |
| 硬编码数据 | 5 | 状态跳转路径、Storage key、Toast 文案等 |
| 已对接 API | 1 | `GET /api/xcx/me` |
| 前端计算/派生 | 3 | 状态栏高度、状态条件分支、application 提取 |
| 路由参数 | 0 | 无 |
| WXML 硬编码文案 | 12 | 标题、提示语、标签等 |
## 一、Mock 数据
无。本页面未引用 `mock-data.ts`,所有数据均来自 API。
## 二、硬编码数据
| # | 文件 | 行号 | 内容 | 风险等级 | 说明 |
|---|------|------|------|----------|------|
| 1 | `.ts` | L39 | `url: "/api/xcx/me"` | 🟢 低 | API 端点路径,通常不变 |
| 2 | `.ts` | L55 | `url: "/pages/mvp/mvp"` | 🟢 低 | approved 状态跳转路径 |
| 3 | `.ts` | L61 | `url: "/pages/no-permission/no-permission"` | 🟢 低 | disabled 状态跳转路径 |
| 4 | `.ts` | L67 | `url: "/pages/no-permission/no-permission"` | 🟢 低 | rejected 状态跳转路径 |
| 5 | `.ts` | L73 | `url: "/pages/apply/apply"` | 🟢 低 | new 状态跳转路径 |
| 6 | `.ts` | L82 | `title: "获取状态失败"` | 🟡 中 | Toast 错误提示文案,建议抽取为常量 |
| 7 | `.ts` | L8693 | Storage key: `"token"`, `"refreshToken"`, `"userId"`, `"userStatus"` | 🟡 中 | 多处重复使用,建议抽取为常量统一管理 |
| 8 | `.ts` | L94 | `url: "/pages/login/login"` | 🟢 低 | 切换账号跳转路径 |
## 三、已对接 API
### 3.1 `GET /api/xcx/me`
| 属性 | 值 |
|------|-----|
| 调用位置 | `fetchStatus()` — L37-81 |
| 触发时机 | `onLoad()``onShow()``onPullDownRefresh()` |
| 请求方式 | `GET` |
| 需要鉴权 | ✅ `needAuth: true` |
| 请求参数 | 无 |
| 响应字段(使用中) | `user_id`, `status`, `nickname`, `latest_application` |
| 响应字段application 子对象) | `id`, `site_code`, `role_type`, `phone`, `status`, `reject_reason`, `created_at` |
**状态流转逻辑:**
| `data.status` 值 | 行为 |
|-------------------|------|
| `"approved"` | `wx.reLaunch``/pages/mvp/mvp` |
| `"disabled"` | `wx.reLaunch``/pages/no-permission/no-permission` |
| `"rejected"` | `wx.reLaunch``/pages/no-permission/no-permission` |
| `"new"` | `wx.reLaunch``/pages/apply/apply` |
| `"pending"` | 留在当前页,展示审核中 UI |
**副作用:**
- 同步 `globalData.authUser``userId`, `status`, `nickname`
- 写入 Storage`userId`, `userStatus`
## 四、前端计算/派生数据
| # | 数据 | 来源 | 说明 |
|---|------|------|------|
| 1 | `statusBarHeight` | `wx.getWindowInfo().statusBarHeight` | 系统 API用于自定义导航栏顶部偏移 |
| 2 | `status` | 从 API 响应 `data.status` 赋值 | 控制 pending/rejected 两种 UI 状态 |
| 3 | `application` | 从 API 响应 `data.latest_application` 提取 | 展示申请信息摘要球房ID、身份、手机号 |
## 五、路由参数
无。本页面不接收任何路由参数。
## 六、WXML 硬编码文案
| # | 内容 | 位置 | 建议 |
|---|------|------|------|
| 1 | `"加载中..."` | loading 区域 | 可保留 |
| 2 | `"申请审核中"` | main-titlepending | 可保留 |
| 3 | `"申请未通过"` | main-titlerejected | 可保留 |
| 4 | `"您的访问申请已提交成功,正在等待管理员审核,请耐心等待"` | sub-titlepending | 可保留 |
| 5 | `"很抱歉,您的申请未通过审核"` | sub-titlerejected | 可保留 |
| 6 | `"审核进度"` | progress-title | 可保留 |
| 7 | `"通常需要 1-3 个工作日"` | progress-desc | 🟡 如审核时效变化需同步修改 |
| 8 | `"已提交"` / `"审核中"` / `"通过"` | 进度步骤标签 | 可保留 |
| 9 | `"拒绝原因"` | reject-title | 可保留 |
| 10 | `"申请信息"` | info-card-title | 可保留 |
| 11 | `"球房ID"` / `"申请身份"` / `"手机号"` | info-label | 可保留 |
| 12 | `"如有疑问,请联系管理员"` | contact-text | 可保留 |
| 13 | `"更换登录账号"` | switch-btn-text | 可保留 |
## 七、联调 TODO
| # | 优先级 | 事项 | 当前状态 | 说明 |
|---|--------|------|----------|------|
| 1 | — | — | — | 本页面已完成 API 对接,无待联调项 |
**结论:** reviewing 页面已完全对接 `GET /api/xcx/me`,无 mock 数据残留,联调状态良好。唯一建议是将 Storage key 和 Toast 文案抽取为常量统一管理。

View File

@@ -0,0 +1,285 @@
# task-detail 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/task-detail/task-detail
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据 | 12 个字段 | 来自 `mock-data.ts``mockTaskDetails` + 页面内联 mock 备注 |
| 硬编码数据 | 18 个字段/数组 | 维客线索、话术参考、服务记录、服务汇总、手机号、储值等级等 |
| 已对接 API | 0 个接口 | 当前无任何真实 API 调用 |
| 前端计算/派生 | 7 个字段 | 关系等级、Banner 背景、AI 配色、时间标签等 |
| 路由参数 | 1 个 | `id`(任务 ID |
| WXML 硬编码文案 | 6 处 | 建议文案、section 标题等 |
---
## 一、Mock 数据
### 1.1 来自 mock-data.ts`mockTaskDetails`
页面通过 `import { mockTaskDetails } from '../../utils/mock-data'` 引入,在 `loadData()` 中使用 `setTimeout` 模拟异步加载。
| 字段 | 类型 | 来源路径 | 联调替换 API |
|------|------|----------|-------------|
| `detail`(整体) | `TaskDetail` | `mockTaskDetails.find(t => t.id === id) \|\| mockTaskDetails[0]` | `GET /api/tasks/{id}` |
| `detail.id` | `string` | TaskDetail.id | 同上 |
| `detail.customerName` | `string` | TaskDetail.customerName | 同上 |
| `detail.customerAvatar` | `string` | TaskDetail.customerAvatar | 同上 |
| `detail.taskType` | `TaskType` | TaskDetail.taskType`'callback' \| 'priority_recall' \| 'relationship' \| 'high_priority'` | 同上 |
| `detail.taskTypeLabel` | `string` | TaskDetail.taskTypeLabel | 同上 |
| `detail.heartScore` | `number` | TaskDetail.heartScore | 同上 |
| `detail.status` | `'pending' \| 'completed' \| 'abandoned'` | TaskDetail.status | 同上 |
| `detail.aiAnalysis.summary` | `string` | TaskDetail.aiAnalysis.summary | `GET /api/tasks/{id}/ai-analysis` |
| `detail.aiAnalysis.suggestions` | `string[]` | TaskDetail.aiAnalysis.suggestions | 同上 |
| `detail.hobbies` | `string[]` | TaskDetail.hobbies | `GET /api/tasks/{id}` |
| `detail.deadline` | `string` | TaskDetail.deadline | 同上 |
**mock-data.ts 中 TaskDetail 扩展字段(已定义但页面未直接渲染):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `lastVisitDate` | `string?` | 最后到店日期,页面未使用 |
| `lastSpendAmount` | `number?` | 最后消费金额,页面未使用 |
| `callbackReason` | `string?` | 回访原因,页面未使用 |
| `daysAbsent` | `number?` | 缺席天数,页面未使用 |
| `spendTrend` | `'up' \| 'down' \| 'flat'?` | 消费趋势,页面未使用 |
| `churnRisk` | `'high' \| 'medium' \| 'low'?` | 流失风险,页面未使用 |
| `preferences` | `string[]?` | 偏好,页面未使用 |
| `consumptionHabits` | `string?` | 消费习惯,页面未使用 |
| `socialPreference` | `string?` | 社交偏好,页面未使用 |
### 1.2 页面内联 Mock 备注
`loadData()` 方法内部硬编码了 5 条 mock 备注(`mockNotes` 局部变量L107-L113覆盖了 `mockTaskDetails` 中的 `notes` 字段:
| 字段 | 当前值 | 联调替换 API |
|------|--------|-------------|
| `mockNotes[0]` | `'已通过微信联系王先生...'`score=10 | `GET /api/tasks/{id}/notes` |
| `mockNotes[1]` | `'王先生最近出差较多...'`score=7.5 | 同上 |
| `mockNotes[2]` | `'上次到店时推荐了会员续费活动...'`score=6 | 同上 |
| `mockNotes[3]` | `'客户对今天的服务非常满意...'`score=9.5 | 同上 |
| `mockNotes[4]` | `'完成高优先召回任务...'`score=8 | 同上 |
---
## 二、硬编码数据
### 2.1 维客线索(`retentionClues`
**风险:高** — 8 条完整的维客线索数据直接写死在 `data` 对象中L68-L77应从 API 获取。
| 索引 | tag | text摘要 | source | 所在行 | 应改为 |
|------|-----|-------------|--------|--------|--------|
| 0 | 客户基础 | `'生日 3月15日 · VIP会员 · 注册2年'` | By:系统 | L69 | `GET /api/tasks/{id}/retention-clues` |
| 1 | 消费习惯 | `'常来夜场 · 月均4-5次'` | By:系统 | L70 | 同上 |
| 2 | 消费习惯 | `'高客单价'`(含 desc | By:系统 | L71 | 同上 |
| 3 | 玩法偏好 | `'偏爱中式八球 · 斯诺克进阶中...'`(含 desc | By:系统 | L72 | 同上 |
| 4 | 重要反馈 | `'上次提到想练斯诺克走位'`(含 desc | By:小燕 | L73 | 同上 |
| 5 | 社交偏好 | `'喜欢带朋友来玩 · 社交型客户'`(含 desc | By:系统 | L74 | 同上 |
| 6 | 消费习惯 | `'酒水消费占比高 · 偏好高端酒水'`(含 desc | By:系统 | L75 | 同上 |
| 7 | 重要反馈 | `'上次提到想办生日派对'`(含 desc | By:Lucy | L76 | 同上 |
> 注意:`mock-data.ts` 中已定义 `mockRetentionClues` 和 `RetentionClue` 接口,但页面未使用,而是自行硬编码了不同结构的数据。联调时应统一数据结构。
### 2.2 话术参考(`talkingPoints`
**风险:高** — 5 条话术直接写死在 `data` 对象中L80-L86应由 AI 生成或从 API 获取。
| 索引 | 内容摘要 | 所在行 | 应改为 |
|------|---------|--------|--------|
| 0 | `'王哥您好,好久不见!最近店里新到了...'` | L81 | `GET /api/tasks/{id}/talking-points` 或 AI 生成接口 |
| 1 | `'王哥,最近忙吗?这周末我们有个...'` | L82 | 同上 |
| 2 | `'王哥好呀,上次您提到想练练...'` | L83 | 同上 |
| 3 | `'王哥,好久没见您了,您的老位置...'` | L84 | 同上 |
| 4 | `'王哥您好,我们这个月推出了...'` | L85 | 同上 |
### 2.3 近期服务记录(`serviceRecords`
**风险:高** — 4 条服务记录直接写死在 `data` 对象中L91-L96应从 API 获取。
| 索引 | table | type | income | date | 所在行 | 应改为 |
|------|-------|------|--------|------|--------|--------|
| 0 | A12号台 | 基础课 | ¥200 | 2月7日 21:30 | L92 | `GET /api/tasks/{id}/service-records` |
| 1 | 3号台 | 基础课 | ¥160 | 2月1日 20:30 | L93 | 同上 |
| 2 | VIP1号房 | 包厢课 | ¥150 | 1月28日 19:00 | L94 | 同上 |
| 3 | (空) | 充值 | ¥80 | 1月15日 10:00 | L95 | 同上 |
### 2.4 服务汇总(`serviceSummary`
**风险:高** — 汇总数据直接写死L90应由后端计算或前端根据服务记录列表聚合。
| 字段 | 当前值 | 所在行 | 应改为 |
|------|--------|--------|--------|
| `totalHours` | `6.0` | L90 | 后端返回或前端聚合 `serviceRecords` |
| `totalIncome` | `510` | L90 | 同上 |
| `avgIncome` | `170` | L90 | 同上 |
### 2.5 手机号
**风险:高** — 手机号硬编码在两处。
| 位置 | 当前值 | 所在行 | 应改为 |
|------|--------|--------|--------|
| WXML 中 `phone` 显示 | `'138****5678'`(脱敏)/ `'13812345678'`(明文) | wxml L38 | 从 `detail` 对象获取API 返回脱敏版 |
| `onCopyPhone()` 方法 | `'13812345678'` | ts L175 | 调用 API 获取明文手机号 `GET /api/customers/{id}/phone` |
### 2.6 储值等级
**风险:中** — 直接写死在 `data` 中。
| 字段 | 当前值 | 所在行 | 应改为 |
|------|--------|--------|--------|
| `storageLevel` | `'非常多'` | ts L101 | 从 `detail` 或客户信息 API 获取 |
### 2.7 关系等级初始值
**风险:低** — 初始值会被 `updateRelationshipDisplay()` 覆盖,但仍属于硬编码。
| 字段 | 当前值 | 所在行 | 说明 |
|------|--------|--------|------|
| `relationLevel` | `'excellent'` | ts L104 | 初始值,`loadData` 后被覆盖 |
| `relationLevelText` | `'很好'` | ts L105 | 同上 |
| `relationColor` | `'#e91e63'` | ts L106 | 同上 |
### 2.8 任务建议文案
**风险:中** — WXML 中硬编码了建议引导文案。
| 位置 | 当前值 | 所在行 | 应改为 |
|------|--------|--------|--------|
| `suggestion-intro` | `'该客户已有 15 天未到店,存在流失风险。建议通过微信联系:'` | wxml L82 | 从 `detail.aiAnalysis` 获取或后端生成 |
---
## 三、已对接 API
**当前无任何真实 API 调用。**
`loadData()` 使用 `setTimeout` + `mockTaskDetails` 模拟异步请求L128-L155。所有数据操作放弃任务、保存备注、删除备注均为本地 `setData` 操作,未与后端通信。
涉及的伪操作:
| 操作 | 当前实现 | 需对接 API |
|------|---------|-----------|
| 加载任务详情 | `mockTaskDetails.find()` | `GET /api/tasks/{id}` |
| 放弃任务 | `setData({ 'detail.status': 'abandoned' })` | `POST /api/tasks/{id}/abandon` |
| 取消放弃 | `setData({ 'detail.status': 'pending' })` | `POST /api/tasks/{id}/cancel-abandon` |
| 保存备注 | `setData({ sortedNotes: [...] })` | `POST /api/tasks/{id}/notes` |
| 删除备注 | `filter` 本地数组 | `DELETE /api/tasks/{id}/notes/{noteId}` |
---
## 四、前端计算/派生数据
| 字段 | 计算逻辑 | 所在方法 | 依赖数据 |
|------|---------|---------|---------|
| `relationLevel` | `heartScore` 分段映射:>8.5→excellent, ≥6→good, ≥3.5→normal, <3.5→poor | `updateRelationshipDisplay()` L157 | `detail.heartScore` |
| `relationLevelText` | 同上分段映射为中文:很好/良好/一般/待发展 | 同上 | 同上 |
| `relationColor` | 同上分段映射为颜色值 | 同上 | 同上 |
| `bannerBgSvg` | 根据 `detail.taskType` 映射 SVG 路径 | `loadData()` L133-L142 | `detail.taskType` |
| `sortedNotes` | `mockNotes` → 附加 `timeLabel``formatRelativeTime`)→ `sortByTimestamp` 排序 | `loadData()` L116-L118 | mock 备注数据 |
| `aiColor` | 随机从 6 色中选取 | `onLoad()` L124 | 无(随机) |
| `copiedIndex` | 复制话术后设为当前索引2 秒后重置为 -1 | `onCopySpeech()` L189 | 用户交互 |
| `pageState` | 加载状态机loading → normal/empty/error | `loadData()` | 数据加载结果 |
| `phoneVisible` | 手机号显示/隐藏切换 | `onTogglePhone()` L173 | 用户交互 |
| `noteModalVisible` | 备注弹窗显隐 | `onAddNote()`/`onNoteConfirm()`/`onNoteCancel()` | 用户交互 |
| `abandonModalVisible` | 放弃弹窗显隐 | `onAbandon()`/`onAbandonConfirm()`/`onAbandonCancel()` | 用户交互 |
| `formatServiceDate()` 返回值 | ISO 日期 → `M月D日 HH:mm` 中文短格式 | 页面顶部函数 L55 | 服务记录日期字符串 |
---
## 五、路由参数
| 参数 | 来源 | 用途 | 所在行 |
|------|------|------|--------|
| `options.id` | `onLoad(options)` | 任务 ID用于查找 mock 数据;缺失时回退空字符串 | ts L123 |
---
## 六、WXML 中的硬编码文案
| 文案 | 位置 | 类型 | 说明 |
|------|------|------|------|
| `'该客户已有 15 天未到店,存在流失风险。建议通过微信联系:'` | wxml L82 | 业务数据 | 应从 AI 分析结果获取 |
| `'💡 建议执行'` | wxml L79 | UI 文案 | 可保留 |
| `'💬 话术参考'` | wxml L89 | UI 文案 | 可保留 |
| `'60天内服务记录'` | wxml L121 | UI 文案 | "60天"为业务规则,可保留但需确认是否动态 |
| `'未找到任务信息'` / `'加载失败'` / `'加载中...'` | wxml L7-L15 | 状态文案 | 可保留 |
| `'暂无备注'` | wxml L117 | 空态文案 | 可保留 |
---
## 七、引用的工具函数
| 函数 | 来源文件 | 用途 |
|------|---------|------|
| `mockTaskDetails` | `utils/mock-data.ts` | Mock 任务详情数据 |
| `TaskDetail`, `Note` | `utils/mock-data.ts` | 类型定义 |
| `sortByTimestamp` | `utils/sort.ts` | 备注按时间降序排序 |
| `formatRelativeTime` | `utils/time.ts` | 备注时间 → 相对时间文案 |
| `formatMoney` | `utils/money.ts` | 金额格式化(已 import 但页面 TS 中未直接调用WXML 使用 WXS 版) |
| `fmt.toFixed` | `utils/format.wxs` | WXML 中数字保留小数 |
| `fmt.money` | `utils/format.wxs` | WXML 中金额格式化 |
| `fmt.hours` | `utils/format.wxs` | WXML 中课时格式化 |
---
## 八、引用的自定义组件
| 组件 | 路径 | 用途 |
|------|------|------|
| `note-modal` | `/components/note-modal/note-modal` | 备注弹窗 |
| `abandon-modal` | `/components/abandon-modal/abandon-modal` | 放弃任务弹窗 |
| `star-rating` | `/components/star-rating/star-rating` | 星级评分展示 |
| `heart-icon` | `/components/heart-icon/heart-icon` | 爱心图标(关系等级) |
| `ai-inline-icon` | `/components/ai-inline-icon/ai-inline-icon` | AI 内联图标 |
| `ai-title-badge` | `/components/ai-title-badge/ai-title-badge` | AI 标题徽章 |
| `clue-card` | `/components/clue-card/clue-card` | 维客线索卡片 |
| `service-record-card` | `/components/service-record-card/service-record-card` | 服务记录卡片 |
| `dev-fab` | `/components/dev-fab/dev-fab` | 开发调试浮动按钮JSON 注册但 WXML 未使用) |
---
## 九、调试面板数据
页面包含一个调试面板(`showDebugPanel`),用于开发阶段切换任务类型和关系数值。联调前应移除或隐藏。
| 字段 | 用途 | 所在行 |
|------|------|--------|
| `showDebugPanel` | 调试面板显隐 | ts L110 |
| `debugTaskType` | 调试用任务类型 | ts L111 |
| `debugHeartScore` | 调试用关系数值 | ts L112 |
| `debugShowExpandBtn` | 调试用展开按钮开关 | ts L113 |
---
## 十、联调 TODO
### 高优先级(数据完全依赖 Mock/硬编码)
- [ ] 替换 `mockTaskDetails``GET /api/tasks/{id}` 真实 API 调用
- [ ] 替换页面内联 mock 备注为 `GET /api/tasks/{id}/notes` API
- [ ] 替换硬编码 `retentionClues`8 条维客线索)为 `GET /api/tasks/{id}/retention-clues` API
- [ ] 替换硬编码 `talkingPoints`5 条话术)为 `GET /api/tasks/{id}/talking-points` 或 AI 生成接口
- [ ] 替换硬编码 `serviceRecords`4 条服务记录)为 `GET /api/tasks/{id}/service-records?days=60` API
- [ ] 替换硬编码 `serviceSummary` 为后端返回或前端聚合计算
- [ ] 替换硬编码手机号 `'13812345678'` / `'138****5678'` 为 API 返回的客户手机号
- [ ] 替换硬编码 `storageLevel: '非常多'` 为客户信息 API 返回值
### 中优先级(操作类接口)
- [ ] `onAbandonConfirm` 对接 `POST /api/tasks/{id}/abandon`
- [ ] `cancelAbandon` 对接 `POST /api/tasks/{id}/cancel-abandon`
- [ ] `onNoteConfirm` 对接 `POST /api/tasks/{id}/notes`
- [ ] `onDeleteNote` 对接 `DELETE /api/tasks/{id}/notes/{noteId}`
### 低优先级(优化项)
- [ ] WXML 中 `suggestion-intro` 硬编码文案改为从 AI 分析结果动态获取
- [ ] 移除或条件隐藏调试面板(`showDebugPanel` 相关代码)
- [ ] 统一维客线索数据结构:页面硬编码的 `RetentionClue` 接口与 `mock-data.ts` 中定义的不一致
- [ ] `formatMoney` 已 import 但 TS 中未使用WXML 用 WXS 版),可清理无用 import
- [ ] `dev-fab` 组件已在 JSON 注册但 WXML 未引用,可清理

View File

@@ -0,0 +1,278 @@
# task-list 页面数据来源排查
> 排查日期2026-03-18
> 页面路径pages/task-list/task-list
> 引用文件:`utils/mock-data.ts`、`utils/money.ts`、`utils/time.ts`、`utils/request.ts`(未使用)
## 概览
| 分类 | 数量 | 说明 |
|------|------|------|
| Mock 数据 | 28 个字段 | 全部任务列表 + 业绩进度卡片均来自 mock |
| 硬编码数据 | 12 个字段 | 用户信息、头像、动画参数、AI 建议文案等 |
| 已对接 API | 0 个接口 | 页面当前无任何真实 API 调用 |
| 前端计算/派生 | 14 个字段 | enrichTask 派生字段 + 进度条动画计算 |
| 路由参数 | 0 个 | onLoad 未读取 options |
| WXML 硬编码文案 | 5 处 | 标题、分组标签等 UI 文案 |
---
## 一、Mock 数据
### 1.1 来自 mock-data.ts
#### 1.1.1 `mockTasks` — 任务列表主数据
| 字段 | 类型 | 说明 | 联调替换 API |
|------|------|------|-------------|
| `id` | `string` | 任务 ID | `GET /api/xcx/tasks` |
| `customerName` | `string` | 客户姓名 | 同上 |
| `customerAvatar` | `string` | 客户头像 URL | 同上 |
| `taskType` | `TaskType` | 任务类型枚举:`callback` / `priority_recall` / `relationship` / `high_priority` | 同上 |
| `taskTypeLabel` | `string` | 任务类型中文标签 | 同上(或前端根据 taskType 映射) |
| `deadline` | `string` | 截止日期 ISO 字符串 | 同上 |
| `heartScore` | `number` | 爱心评分 0-10 | 同上 |
| `hobbies` | `string[]` | 客户爱好标签 | 同上 |
| `isPinned` | `boolean` | 是否置顶 | 同上 |
| `hasNote` | `boolean` | 是否有备注 | 同上 |
| `status` | `'pending' \| 'completed' \| 'abandoned'` | 任务状态 | 同上 |
- 导入方式:`import { mockTasks } from '../../utils/mock-data'`task-list.ts L2
- `loadData()` 中直接展开 `mockTasks` 并追加了一条内联 mock 任务 `task-007`(见 1.2 节)
#### 1.1.2 `mockPerformance` — 绩效概览数据
| 字段 | 类型 | 当前 Mock 值 | 联调替换 API |
|------|------|-------------|-------------|
| `monthlyIncome` | `number` | `12680` | `GET /api/xcx/performance/summary` |
| `incomeChange` | `number` | `15.3` | 同上 |
| `currentTier` | `string` | `'银牌助教'` | 同上 |
| `nextTierGap` | `number` | `3320` | 同上 |
| `todayServiceCount` | `number` | `4` | 同上 |
| `weekServiceCount` | `number` | `18` | 同上 |
| `monthServiceCount` | `number` | `67` | 同上 |
- 导入方式:`import { mockPerformance } from '../../utils/mock-data'`task-list.ts L2
- 当前仅用 `mockPerformance.currentTier` 赋值给 `bannerTitle`L233
### 1.2 页面内联 Mock
#### 1.2.1 `task-007` 内联任务loadData 内,约 L213-L225
```typescript
{
id: 'task-007',
customerName: '孙丽',
customerAvatar: '/assets/images/avatar-default.png',
taskType: 'callback',
taskTypeLabel: '客户回访',
deadline: '2026-03-06',
heartScore: 3.5,
hobbies: [],
isPinned: false,
hasNote: false,
status: 'abandoned',
}
```
- 联调时需删除,改为 API 返回的完整任务列表
#### 1.2.2 `buildPerfData()` — 业绩进度卡片构造函数(约 L130-L160
| 字段 | Mock 值 | 说明 | 联调替换 API |
|------|---------|------|-------------|
| `nextTierHours` | `100` | 下一档位小时数 | `GET /api/xcx/performance/progress` |
| `remainHours` | `12.5` | 距下一档剩余小时 | 同上 |
| `currentTier` | `1` | 当前档位序号 | 同上 |
| `tierProgress` | `58` | 当前段内进度% | 同上(或前端计算) |
| `filledPct` | `39.8`87.5/220×100 | 总进度条百分比 | 同上(或前端计算) |
| `ticks` | `buildTicks([0,100,130,160,190,220], 220)` | 刻度数组 | 同上(档位节点由接口返回) |
| `basicHours` | `'77.5'` | 基础课时 | 同上 |
| `bonusHours` | `'12'` | 激励课时 | 同上 |
| `totalHours` | `'87.5'` | 总课时 | 同上 |
| `tierCompleted` | `true` | 是否达标 | 同上 |
| `bonusMoney` | `'800'` | 达标奖金(元) | 同上 |
| `incomeMonth` | `'2月'` | 收入月份 | 同上 |
| `prevMonth` | `'1月'` | 对比月份 | 同上 |
| `incomeFormatted` | `'6,206'` | 预计收入格式化 | 同上 |
| `incomeTrend` | `'↓368'` | 收入趋势文案 | 同上 |
| `incomeTrendDir` | `'down'` | 趋势方向 | 同上 |
#### 1.2.3 `enrichTask()` 内联 Mock 逻辑(约 L100-L125
| 派生字段 | Mock 算法 | 说明 |
|----------|-----------|------|
| `lastVisitDays` | `(id.charCodeAt(last) % 15) + 1` | 伪随机天数,联调时应由 API 返回 |
| `balanceLabel` | `formatMoney((id.charCodeAt(last) * 137) % 5000 + 200)` | 伪随机余额,联调时应由 API 返回 |
| `aiSuggestion` | 从 5 条硬编码文案中按 id 取模选取 | 联调时应由 AI 接口返回 |
5 条硬编码 AI 建议文案:
1. `'建议推荐斯诺克进阶课程,提升客户粘性'`
2. `'客户近期消费下降,建议电话关怀了解原因'`
3. `'适合推荐周末球友赛活动,增强社交体验'`
4. `'高价值客户建议维护关系并推荐VIP权益'`
5. `'新客户首次体验后未续费,建议跟进意向'`
#### 1.2.4 `loadData()` 中的 600ms 模拟延迟
```typescript
setTimeout(() => { /* 全部数据构造逻辑 */ }, 600)
```
- 联调时替换为真实 API 请求
---
## 二、硬编码数据
| 字段 | 当前值 | 应改为 | 风险 | 所在位置 |
|------|--------|--------|------|----------|
| `userName` | `'小燕'` | `globalData.userInfo.name` 或登录 API | 高 | data 初始化 |
| `userRole` | `'助教'` | `globalData.userInfo.role` 或登录 API | 高 | data 初始化 |
| `storeName` | `'广州朗朗桌球'` | `globalData.storeInfo.name` 或登录 API | 高 | data 初始化 |
| `avatarUrl` | `'/assets/images/avatar-coach.png'` | `globalData.userInfo.avatar` 或登录 API | 中 | data 初始化 |
| `DETAIL_ROUTE` | `'/pages/task-detail/task-detail'` | 保持硬编码(路由常量) | 低 | 常量定义 |
| `tierNodes` | `[0, 100, 130, 160, 190, 220]` | 业绩进度 API 返回 | 高 | `buildPerfData()` + `_applyDebugHours()` |
| `maxHours` | `220` | 业绩进度 API 返回 | 高 | `buildPerfData()` |
| `total` | `87.5` | 业绩进度 API 返回 | 高 | `buildPerfData()` |
| `aiColor` | 随机选取 `['red','orange','yellow','blue','indigo','purple']` | 可保持前端随机(纯 UI | 低 | `onLoad()` / `onShow()` |
| `abandonReason` | `'客户已转至其他门店'` | API 返回(放弃原因由用户输入) | 中 | `enrichTask()` |
| `suggestions[]` | 5 条固定文案 | AI 建议 API 返回 | 高 | `enrichTask()` |
| `hasMore` | `true` → 触底后 `false` | 分页 API 返回 `has_next` | 中 | `loadData()` / `onReachBottom()` |
---
## 三、已对接 API
**当前页面无任何真实 API 调用。**
- `utils/request.ts` 已导出 `request()` 函数(支持 token 自动附加、401 刷新重试)
- task-list.ts 未 import `request`,所有数据均来自 mock
---
## 四、前端计算/派生数据
### 4.1 任务分组loadData 内)
| 派生字段 | 计算逻辑 | 依赖源数据 |
|----------|----------|-----------|
| `pinnedTasks` | `enriched.filter(t => t.isPinned && !t.isAbandoned)` | `allTasks` |
| `normalTasks` | `enriched.filter(t => !t.isPinned && !t.isAbandoned && t.status === 'pending')` | `allTasks` |
| `abandonedTasks` | `enriched.filter(t => t.isAbandoned)` | `allTasks` |
| `taskCount` | `pinnedTasks.length + normalTasks.length + abandonedTasks.length` | 三组任务 |
| `pageState` | `totalCount > 0 ? 'normal' : 'empty'` | `taskCount` |
### 4.2 enrichTask 派生字段
| 派生字段 | 计算逻辑 | 依赖 |
|----------|----------|------|
| `isAbandoned` | `task.status === 'abandoned'` | `task.status` |
| `deadlineLabel` | `formatDeadline(task.deadline).text` | `task.deadline` + `utils/time.ts` |
| `deadlineStyle` | `formatDeadline(task.deadline).style` | `task.deadline` + `utils/time.ts` |
| `balanceLabel` | `formatMoney(balanceSeedNum)` | Mock 种子值 + `utils/money.ts`(联调后改为 API 余额) |
### 4.3 进度条动画计算
| 派生字段 | 计算逻辑 | 依赖 |
|----------|----------|------|
| `filledPct` | `Math.min(100, (total / 220) * 100)` | `total``maxHours` |
| `clampedSparkPct` | `Math.max(0, Math.min(100, filledPct))` | `filledPct` |
| `shineDurMs` | `calcShineDur(filledPct)` — 基于 `SHINE_SPEED` 和进度百分比 | `filledPct``SHINE_SPEED` |
| `ticks[]` | `buildTicks(tierNodes, maxHours)` — 计算每个刻度的 `left` 百分比位置 | `tierNodes``maxHours` |
### 4.4 调试面板计算(`_applyDebugHours`
| 派生字段 | 说明 |
|----------|------|
| `currentTier` | 遍历 tiers 数组确定当前档位 |
| `tierProgress` | 当前段内进度百分比 |
| `remainHours` | `nextTierHours - total` |
| `tierCompleted` | `total >= 220` |
> 调试面板仅开发阶段使用,联调时可保留或移除。
---
## 五、路由参数
`onLoad()` 未读取任何 `options` 参数。页面作为 TabBar 页面,不接收路由参数。
---
## 六、WXML 中的硬编码文案
| 文案 | 位置 | 类型 | 说明 |
|------|------|------|------|
| `'今日 客户维护'` | section-header | UI 文案 | 可保持硬编码,或由 API 返回标题 |
| `'📌 置顶'` | group-label--pinned | UI 文案 | 保持硬编码 |
| `'正常任务'` | group-label--normal | UI 文案 | 保持硬编码 |
| `'已放弃'` | group-label--abandoned | UI 文案 | 保持硬编码 |
| `'暂无待办任务'` | state-empty | UI 文案 | 保持硬编码 |
| `'加载失败,请重试'` | state-error | UI 文案 | 保持硬编码 |
| `'没有更多了'` | load-more | UI 文案 | 保持硬编码 |
| `'最近到店:{{...}}天前 · 余额:{{...}}'` | card-row-2 | 模板文案 | 保持硬编码(数据部分由变量填充) |
| `'放弃原因:{{...}}'` | card-row-abandon | 模板文案 | 保持硬编码 |
| `'基础课 \| 激励课 \| 全部'` | hours-label | UI 文案 | 保持硬编码 |
> 以上均为 UI 展示文案,非业务数据硬编码,联调时无需替换。
---
## 七、联调 TODO
### 高优先P0 — 页面核心功能)
- [ ] 替换 `mockTasks` + 内联 `task-007``GET /api/xcx/tasks` API 调用
- [ ] 替换 `buildPerfData()``GET /api/xcx/performance/progress` API 调用
- [ ]`globalData` 或登录 API 获取 `userName``userRole``storeName``avatarUrl`
- [ ] 替换 `enrichTask()` 中的 mock 派生字段(`lastVisitDays``balanceLabel`)为 API 返回字段
- [ ] 替换 `enrichTask()` 中的 `aiSuggestion` 硬编码文案为 AI 建议 API
- [ ] 替换 `mockPerformance` 引用为真实绩效 API
- [ ] 档位节点 `tierNodes``maxHours` 改为 API 返回
### 中优先P1 — 交互完整性)
- [ ] 实现分页加载:`onReachBottom` 对接分页 API`page` / `cursor` 参数)
- [ ] `onCtxPin` 置顶操作对接 `POST /api/xcx/tasks/{id}/pin` API
- [ ] `onCtxAbandon` / `onAbandonConfirm` 放弃操作对接 `POST /api/xcx/tasks/{id}/abandon` API
- [ ] `onCtxCancelAbandon` 取消放弃对接 `POST /api/xcx/tasks/{id}/cancel-abandon` API
- [ ] `onNoteConfirm` 备注保存对接 `POST /api/xcx/tasks/{id}/notes` API
- [ ] `loadData` 中移除 `setTimeout 600ms` 模拟延迟
### 低优先P2 — 可后续处理)
- [ ] 移除 `enrichTask()` 函数(字段由 API 直接返回)
- [ ] 移除 `buildPerfData()` 函数(数据由 API 直接返回)
- [ ] 移除 `buildTicks()` 函数(或保留为前端工具函数,接收 API 返回的 tierNodes
- [ ] 评估是否保留调试面板(`showDebugPanel` 相关逻辑)
- [ ] 清理 `mock-data.ts` 中 task-list 不再使用的导出
---
## 八、依赖组件清单
| 组件 | 路径 | 数据来源 |
|------|------|----------|
| `perf-progress-bar` | `/components/perf-progress-bar/` | 接收 `perfData.*` 属性(全部 mock |
| `heart-icon` | `/components/heart-icon/` | 接收 `item.heartScore`mock |
| `ai-inline-icon` | `/components/ai-inline-icon/` | 接收 `aiColor`(前端随机) |
| `ai-float-button` | `/components/ai-float-button/` | 无数据依赖 |
| `note-modal` | `/components/note-modal/` | 接收 `noteTarget.customerName`mock |
| `abandon-modal` | `/components/abandon-modal/` | 接收 `abandonTarget.customerName`mock |
| `dev-fab` | `/components/dev-fab/` | 无数据依赖(开发工具) |
| `t-icon` | TDesign 组件 | 无业务数据依赖 |
---
## 九、风险总结
| 风险项 | 等级 | 说明 |
|--------|------|------|
| 全页面零 API 调用 | 🔴 高 | 联调时需一次性替换所有数据源,工作量集中 |
| 用户信息硬编码 | 🔴 高 | `userName`/`userRole`/`storeName` 写死,多用户场景必崩 |
| AI 建议文案固定 | 🔴 高 | 5 条文案轮转,联调时需对接 AI 服务 |
| 业绩进度全量 mock | 🔴 高 | 档位、课时、收入全部硬编码,数值与真实数据无关 |
| 分页未实现 | 🟡 中 | `onReachBottom` 仅显示"没有更多了",无真实分页 |
| 操作无持久化 | 🟡 中 | 置顶/放弃/备注仅修改本地 data刷新即丢失 |
| `enrichTask` 伪随机 | 🟡 中 | `lastVisitDays`/`balanceLabel` 由 id 字符码计算,非真实数据 |

View File

@@ -0,0 +1,997 @@
# NS1小程序后端 API 补全 — xcx-backend-api
> 优先级:最高(上线关键路径)
> 预估工作量:大
> 前置条件P1-P5 已完成数据库、认证、任务、备注、AI 骨架均就绪)
> 契约基准:`docs/miniprogram-dev/API-contract.md`
---
## 一、背景与目标
小程序前端 13 个业务页面已全部完成P5.2 交付),`services/api.ts` 已统一封装 mock → real 切换机制(`USE_REAL_API` 开关)。但后端 23 个接口中仅 16 个已实现,剩余 12 个业务接口缺失,是前后端联调的最大阻塞。
本 SPEC 目标:补全全部缺失接口 + FDW 端到端验证 + 前后端联调修复,使小程序可以连接真实后端运行。
### 已实现的接口16 个)
| 模块 | 接口 | 路由文件 |
|------|------|---------|
| Auth5 | AUTH-1~5登录/dev-login/me/refresh/apply | `xcx_auth.py` |
| Tasks5 | GET tasks + pin/unpin/abandon/cancel-abandon | `xcx_tasks.py` |
| Notes3 | POST/GET/DELETE notes | `xcx_notes.py` |
| AI Chat3 | SSE stream + conversations + messages | `xcx_ai_chat.py` |
### 缺失的接口12 个)
| 接口 ID | 端点 | 页面依赖 | 数据源 |
|---------|------|---------|--------|
| TASK-1 扩展 | GET /tasks增加 performance 附带字段) | task-list | `dws_assistant_salary_calc` via FDW |
| TASK-2 | GET /tasks/{id}(完整详情) | task-detail | biz.coach_tasks + FDW 多表 + ai_cache |
| PERF-1 | GET /performance | performance | `dws_assistant_salary_calc` + `dws_assistant_daily_detail` via FDW |
| PERF-2 | GET /performance/records | performance-records | `dwd_assistant_service_log` via FDW |
| CUST-1 | GET /customers/{id} | customer-detail | `dim_member` + `dws_member_consumption_summary` via FDW |
| CUST-2 | GET /customers/{id}/records | customer-service-records | `dwd_assistant_service_log` + `dwd_table_fee_log` via FDW |
| COACH-1 | GET /coaches/{id} | coach-detail | `dim_assistant` + `dws_member_assistant_relation_index` via FDW |
| BOARD-1 | GET /board/coaches | board-coach | `dws_assistant_salary_calc` + `dws_assistant_monthly_summary` via FDW |
| BOARD-2 | GET /board/customers | board-customer | 多个 DWS 指数表 via FDW |
| BOARD-3 | GET /board/finance | board-finance | 6 个 `dws_finance_*` 表 via FDW |
| CONFIG-1 | GET /config/skill-types | board-coach | 静态配置或 cfg 表 |
| CHAT 路径对齐 | 对齐 CHAT-1/2/3 路径与契约 | chat/chat-history | 现有 xcx_ai_chat.py 路径调整 |
---
## 二、技术架构
### 2.1 后端代码模式(沿用现有模式)
```
apps/backend/app/
├── routers/ # FastAPI 路由xcx_*.py
│ ├── xcx_auth.py ✅ 已有
│ ├── xcx_tasks.py ✅ 已有(需扩展 TASK-1/TASK-2
│ ├── xcx_notes.py ✅ 已有
│ ├── xcx_ai_chat.py ✅ 已有(需路径对齐)
│ ├── xcx_performance.py 🆕 新建
│ ├── xcx_customers.py 🆕 新建
│ ├── xcx_coaches.py 🆕 新建
│ ├── xcx_board.py 🆕 新建
│ └── xcx_config.py 🆕 新建
├── services/ # 业务逻辑层
│ ├── task_manager.py ✅ 已有(需扩展)
│ ├── note_service.py ✅ 已有
│ ├── perf_service.py 🆕 新建
│ ├── customer_service.py 🆕 新建
│ ├── coach_service.py 🆕 新建
│ └── board_service.py 🆕 新建
├── schemas/ # Pydantic 响应模型
└── auth/ # 认证依赖注入 ✅ 已有
```
### 2.2 数据库连接模式
- 业务库zqyy_app`get_connection()` → psycopg2 直连,读写
- ETL 库(通过 FDW`fdw_etl.*` schema 查询,需先 `SET LOCAL app.current_site_id`
- ETL 库(直连只读):`get_etl_readonly_connection(site_id)` → 用于复杂聚合查询
### 2.3 权限控制
- 所有接口需 JWT`require_approved()`
- 看板接口需额外权限检查(`require_permission("view_board_finance")` 等)
- 权限种子数据已就绪auth.role_permissions14 条映射)
---
## 三、接口详细设计
### 3.1 TASK-1 扩展:任务列表增加绩效概览
现有 `GET /api/xcx/tasks` 返回 `list[TaskListItem]`,需扩展为:
- 增加 `performance` 字段(当月工时/收入/客户数/月份标签)
- 增加分页支持(`page`/`pageSize` 参数)
- 增加 `status` 筛选参数
数据源:
- 任务列表:`biz.coach_tasks` + `fdw_etl.v_dim_member`(客户信息)+ `fdw_etl.v_dws_member_winback_index`/`v_dws_member_newconv_index`(指数)
- 绩效概览:`fdw_etl.v_dws_assistant_salary_calc`(当月薪资计算快照)
### 3.2 TASK-2任务详情完整版
现有 `xcx_tasks.py``GET /tasks/{id}` 端点,需新增。
数据源(多表聚合):
- 基础信息:`biz.coach_tasks`
- 客户信息:`fdw_etl.v_dim_member` + `fdw_etl.v_dws_member_consumption_summary`
- 亲密度:`fdw_etl.v_dws_member_assistant_intimacy`
- 近期服务记录:`fdw_etl.v_dwd_assistant_service_log`(最近 N 条)
- 备注:`biz.notes`(最近 N 条,含 ai_score
- 维客线索:`public.member_retention_clue`
- AI 缓存:`biz.ai_cache`app4/app5/app6/app7 缓存)
- 喜好标签:`fdw_etl.v_dwd_table_fee_log`(按房间类型统计)
> ⚠️ 这是最复杂的接口,涉及 8+ 张表的聚合。建议拆分为多个 service 函数组合。
### 3.3 PERF-1/PERF-2绩效模块
PERF-1绩效概览数据源
- `fdw_etl.v_dws_assistant_salary_calc`(收入/档位/工资)
- `fdw_etl.v_dwd_assistant_service_log`(当月服务记录明细)
- `fdw_etl.v_dws_assistant_customer_stats`(新客/常客统计)
PERF-2绩效明细数据源
- `fdw_etl.v_dwd_assistant_service_log`(按月筛选,按日分组)
- `fdw_etl.v_dws_assistant_daily_detail`(定档折算惩罚字段)
### 3.4 CUST-1/CUST-2客户模块
CUST-1客户详情数据源
- `fdw_etl.v_dim_member` + `fdw_etl.v_dim_member_card_account`(基本信息 + 会员卡)
- `fdw_etl.v_dws_member_consumption_summary`(消费汇总)
- `fdw_etl.v_dws_member_assistant_relation_index`(关系指数)
- `public.member_retention_clue`(维客线索)
- 消费记录:`fdw_etl.v_dwd_settlement_head` + `fdw_etl.v_dwd_table_fee_log` + `fdw_etl.v_dwd_store_goods_sale` + `fdw_etl.v_dwd_recharge_order`
> ⚠️ 会员字段断档规则DQ-6/DQ-7`member_phone`/`member_name`/`member_card_type_name` 不可靠,必须通过 `member_id` JOIN `dim_member`/`dim_member_card_account`
CUST-2客户服务记录数据源
- `fdw_etl.v_dwd_assistant_service_log`(按客户+助教筛选)
- `fdw_etl.v_dwd_table_fee_log`(台桌信息)
### 3.5 COACH-1助教详情
数据源:
- `fdw_etl.v_dim_assistant`(基本信息)
- `fdw_etl.v_dws_member_assistant_relation_index`客户数统计RS>2
- `biz.coach_tasks`(任务统计:可见/隐藏/已放弃)
- `biz.notes`(备注列表)
### 3.6 BOARD-1/2/3看板模块
BOARD-1助教看板数据源
- `fdw_etl.v_dws_assistant_salary_calc`(定档业绩/工资)
- `fdw_etl.v_dws_assistant_monthly_summary`(月度汇总)
- `biz.coach_tasks`(任务完成数统计)
BOARD-2客户看板数据源8 个维度排序):
- 最应召回:`fdw_etl.v_dws_member_winback_index`WBI 降序)
- 最大消费潜力:`fdw_etl.v_dws_member_spending_power_index`SPI 降序)
- 最高余额:`fdw_etl.v_dws_member_consumption_summary`
- 最近充值:`fdw_etl.v_dwd_recharge_order`
- 最高消费 60 天:`fdw_etl.v_dws_member_consumption_summary`(基于 `items_sum`
- 最频繁 60 天:`fdw_etl.v_dws_member_consumption_summary`
- 最近到店:`fdw_etl.v_dws_member_visit_detail`
- 最专一:`fdw_etl.v_dws_member_assistant_relation_index`RS 最大值)
BOARD-3财务看板数据源
- `fdw_etl.v_dws_finance_daily_summary`
- `fdw_etl.v_dws_finance_income_structure`
- `fdw_etl.v_dws_finance_recharge_summary`
- `fdw_etl.v_dws_finance_discount_detail`
- `fdw_etl.v_dws_finance_expense_summary`
- `fdw_etl.v_dws_platform_settlement`
- `biz.ai_cache`app2_finance 缓存AI 洞察)
### 3.7 CONFIG-1技能类型列表
静态配置接口,返回助教技能类型列表。数据来源待定(硬编码 or cfg 表)。
### 3.8 CHAT 路径对齐
现有 `xcx_ai_chat.py` 路径:
- `POST /api/ai/chat/stream`
- `GET /api/ai/conversations`
- `GET /api/ai/conversations/{id}/messages`
契约定义路径:
- `GET /api/xcx/chat/history`
- `GET /api/xcx/chat/{chatId}/messages`
- `POST /api/xcx/chat/{chatId}/messages`
需要对齐路径,或在前端 service 层适配。
---
## 四、数据库审查与新增表建议
### 4.1 现有表结构满足度
| 接口 | 现有表是否满足 | 缺口 |
|------|--------------|------|
| TASK-1/2 | ✅ 基本满足 | 需 FDW 多表 JOIN |
| PERF-1/2 | ✅ 满足 | 无 |
| CUST-1/2 | ✅ 满足 | 消费记录需 3 种类型 UNION |
| COACH-1 | ✅ 满足 | 无 |
| BOARD-1 | ⚠️ 部分 | 任务完成数需实时统计 |
| BOARD-2 | ⚠️ 部分 | 8 维度排序需多表查询,性能风险 |
| BOARD-3 | ✅ 满足 | 环比需后端计算 |
### 4.2 建议新增的中间表/物化视图
#### 表 1`biz.board_coach_snapshot`(助教看板快照)
用途:预聚合助教看板数据,避免每次请求实时 JOIN 多张 FDW 表。
```sql
CREATE TABLE biz.board_coach_snapshot (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
assistant_id BIGINT NOT NULL,
snapshot_date DATE NOT NULL,
-- 定档业绩
performance_tier VARCHAR(20),
tier_revenue NUMERIC(12,2),
-- 工资
salary_total NUMERIC(12,2),
-- 客户数
customer_count INTEGER,
-- 高客源储值额
high_value_balance NUMERIC(12,2),
-- 任务完成数
task_completed_count INTEGER,
-- 服务时长
total_hours NUMERIC(8,2),
total_income NUMERIC(12,2),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (site_id, assistant_id, snapshot_date)
);
```
刷新策略ETL 数据更新后触发(`trigger_jobs` 事件驱动),或每日凌晨批量刷新。
#### 表 2`biz.board_customer_snapshot`(客户看板快照)
用途:预聚合客户看板 8 个维度的排序数据,避免实时查询 8 张 FDW 表。
```sql
CREATE TABLE biz.board_customer_snapshot (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
snapshot_date DATE NOT NULL,
-- 8 个维度排序值
wbi_score NUMERIC(8,4), -- 最应召回
spi_score NUMERIC(8,4), -- 最大消费潜力
balance_amount NUMERIC(12,2), -- 最高余额
last_recharge_date DATE, -- 最近充值
spend_60d NUMERIC(12,2), -- 最高消费 60 天items_sum 口径)
visit_count_60d INTEGER, -- 最频繁 60 天
last_visit_date DATE, -- 最近到店
max_rs_score NUMERIC(8,4), -- 最专一
-- 展示字段
member_name VARCHAR(100),
member_avatar VARCHAR(500),
member_tags TEXT[], -- 喜好标签
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (site_id, member_id, snapshot_date)
);
```
#### 表 3`biz.board_finance_snapshot`(财务看板快照)
用途:预聚合财务看板数据 + 环比计算结果。
```sql
CREATE TABLE biz.board_finance_snapshot (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
biz_date DATE NOT NULL,
-- 核心指标JSON 存储,灵活扩展)
metrics JSONB NOT NULL,
-- 环比数据
prev_day_metrics JSONB,
prev_week_metrics JSONB,
prev_month_metrics JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (site_id, biz_date)
);
```
#### 表 4`biz.task_detail_cache`(任务详情缓存)
用途TASK-2 接口涉及 8+ 张表聚合,首次查询后缓存结果,后续读缓存。
```sql
CREATE TABLE biz.task_detail_cache (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES biz.coach_tasks(id),
site_id BIGINT NOT NULL,
detail_json JSONB NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (task_id)
);
```
失效策略:备注新增/AI 缓存更新/任务状态变更时DELETE 对应缓存行。
---
## 五、FDW 端到端验证
### 5.1 验证范围
需验证 `fdw_etl` schema 下所有外部表可正常 SELECT重点关注
| 外部表 | 用途 | 验证 SQL |
|--------|------|---------|
| `v_dim_member` | 客户信息 | `SELECT count(*) FROM fdw_etl.v_dim_member` |
| `v_dim_assistant` | 助教信息 | `SELECT count(*) FROM fdw_etl.v_dim_assistant` |
| `v_dws_assistant_salary_calc` | 薪资计算 | `SELECT * FROM fdw_etl.v_dws_assistant_salary_calc LIMIT 5` |
| `v_dws_finance_daily_summary` | 财务日报 | `SELECT * FROM fdw_etl.v_dws_finance_daily_summary LIMIT 5` |
| `v_dws_member_winback_index` | WBI 指数 | `SELECT * FROM fdw_etl.v_dws_member_winback_index LIMIT 5` |
| `v_dws_member_spending_power_index` | SPI 指数 | `SELECT * FROM fdw_etl.v_dws_member_spending_power_index LIMIT 5` |
### 5.2 RLS 验证
```sql
-- 设置门店隔离
SET LOCAL app.current_site_id = '目标 site_id';
-- 验证只返回该门店数据
SELECT site_id, count(*) FROM fdw_etl.v_dim_member GROUP BY site_id;
-- 预期:只有一个 site_id
```
---
## 六、前后端联调
### 6.1 联调步骤
1. 后端接口开发完成后,逐个验证响应格式与 `API-contract.md` 一致
2. 小程序 `services/api.ts``USE_REAL_API = true`
3. 逐页面联调login → task-list → task-detail → performance → board → customer-detail → chat
4. 修复 snake_case → camelCase 映射差异
5. 处理 inline-mock 页面的数据替换7 个页面有 TODO 标记)
### 6.2 已知需要对齐的差异
- TASK-1 响应格式:现有返回 `list[TaskListItem]`,契约要求 `{ items, total, page, pageSize, performance }`
- CHAT 路径:现有 `/api/ai/*`,契约要求 `/api/xcx/chat/*`
- 响应包装:现有直接返回数据,契约要求 `{ code: 0, data: ... }` 包装
---
## 七、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| API 契约 | `docs/miniprogram-dev/API-contract.md` | 接口定义基准 |
| 前端 Service 层 | `apps/miniprogram/miniprogram/services/api.ts` | 前端期望的响应格式 |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |
| DWD-DOC 标杆 | `docs/reports/DWD-DOC/` | 金额口径、字段语义权威参考 |
| 数据依赖矩阵 | `docs/prd/specs/00-数据依赖矩阵.md` | 页面→数据表映射 |
| 现有 task_manager | `apps/backend/app/services/task_manager.py` | 任务服务层模式参考 |
| 现有 note_service | `apps/backend/app/services/note_service.py` | 备注服务层模式参考 |
| 权限中间件 | `apps/backend/app/middleware/permission.py` | 权限检查模式参考 |
| 数据库连接 | `apps/backend/app/database.py` | 连接方式参考 |
| FDW 配置 | `db/fdw/setup_fdw_test.sql` | FDW 外部表映射 |
---
## 八、预审查清单SPEC 启动前确认)
以下问题需要在启动 SPEC 前与用户确认,确保需求无歧义:
### 8.1 接口设计
1. **响应包装格式**:现有后端直接返回数据对象,契约要求 `{ code: 0, data: ... }` 包装。是全局统一改造(中间件),还是仅新接口使用包装格式?
2. **CHAT 路径对齐**:是修改后端路径从 `/api/ai/*` 改为 `/api/xcx/chat/*`,还是在前端 service 层适配现有路径?
3. **分页策略**TASK-1 契约要求分页,但现有实现返回全量列表。是否所有列表接口都需要分页?看板接口(前 100 名)是否需要分页?
4. **CONFIG-1 技能类型**:数据来源是硬编码还是从 `cfg_*` 表读取?如果硬编码,具体的技能类型列表是什么?
### 8.2 数据库
5. **快照表刷新策略**3 个看板快照表coach/customer/finance的刷新频率是 ETL 数据更新后触发,还是每日定时刷新,还是请求时按需刷新?
6. **任务详情缓存**`task_detail_cache` 的过期时间设多久缓存失效触发条件是否完整备注新增、AI 缓存更新、任务状态变更)?
7. **FDW 外部表命名**:现有代码中使用 `fdw_etl.v_dim_member`(带 `v_` 前缀),需确认所有 FDW 外部表的实际命名是否一致。
### 8.3 业务逻辑
8. **TASK-2 服务记录范围**:近期服务记录取最近多少条?最近 30 天还是最近 10 条?
9. **BOARD-2 客户看板**8 个维度排序是否都取前 100 名?是否需要支持类型筛选(如按会员等级筛选)?
10. **BOARD-3 财务看板**:环比计算的基准是什么?日环比、周环比、月环比都需要吗?
11. **CUST-1 消费记录**3 种消费类型(台桌/商城/充值)是混合排序还是分类展示?分页策略?
12. **COACH-1 任务统计**:可见/隐藏/已放弃的分类逻辑是什么?"隐藏"指什么状态?
### 8.4 性能与安全
13. **FDW 查询性能**:看板接口涉及多张 FDW 表的聚合查询,是否需要设置查询超时?超时后返回什么?
14. **维客线索脱敏**TASK-2 返回维客线索时,后端脱敏规则是否与 P6 spec 中定义的一致(移除 recorded_by_assistant_id/name
15. **金额口径确认**:所有涉及消费金额的接口,确认使用 `items_sum` 口径(= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money不使用 `consume_money`
---
## 八½、前置审查发现2026-03-18 Kiro 审查)
> 审查方法:对照现有代码(`xcx_auth.py`、`xcx_tasks.py`、`xcx_ai_chat.py`、`task_manager.py`、`permission.py`、`database.py`、API 契约(`API-contract.md`)、前端 Service 层(`api.ts`、FDW 配置(`setup_fdw_test.sql`、BD 手册、数据依赖矩阵、DWD-DOC 标杆文档,逐项交叉验证。
### R1接口清单准确性
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R1-1 | 接口总数算术错误 | 🟡 低 | spec 称"23 个接口中仅 16 个已实现,剩余 12 个缺失"16+12=28≠23。需修正总数或重新盘点 |
| R1-2 | Auth 已实现接口数低估 | 🟡 低 | spec 列 Auth 5 个,实际 `xcx_auth.py` 生产端点 7 个login/apply/me/me-sites/switch-site/refresh/dev-login含 dev 端点共 11 个 |
| R1-3 | TASK-4 路径不一致 | 🔴 高 | API 契约定义 `POST /tasks/{taskId}/restore`,前端 `api.ts` 调用 `/tasks/${taskId}/restore`,但后端实现的是 `/tasks/{taskId}/cancel-abandon`。联调必然失败 |
**R1-3 ✅ 已确认**:统一为 `/restore`(改后端路由路径,契约和前端不动)。后端 `xcx_tasks.py``/cancel-abandon` 端点已实现,需改路径为 `/restore`
### R2响应格式全局架构决策阻塞所有新接口开发
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R2-1 | 成功响应无包装 | 🔴 高 | 契约要求 `{ code: 0, data: ... }`,现有后端直接返回 Pydantic model。全局改造影响所有已实现接口 |
| R2-2 | 错误响应格式不一致 | 🔴 高 | 契约要求 `{ code: number, message: string }`,现有 FastAPI 默认 `{ detail: "..." }`。需自定义异常处理器 |
| R2-3 | snake_case vs camelCase | 🔴 高 | 后端返回 snake_case前端期望 camelCase。api.ts 有 TODO 标记但未实现转换。需决定转换层位置 |
**R2 ✅ 已确认:采用方案 A**(后端全局包装 `{ code: 0, data: ... }` + Pydantic `alias_generator=to_camel`
实施路径:
1. 新增全局响应包装中间件(或 `response_model` 基类),成功响应统一 `{ code: 0, data: ... }`
2. 新增全局异常处理器,`HTTPException``{ code: status_code, message: detail }`
3. 所有 Pydantic schema 加 `model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)`
4. 前端 `request()` 工具函数加 `.data` 解包(一处改动)
5. 已有接口Auth/Tasks/Notes/AI Chat逐个验证兼容性
### R3CHAT 模块路径与功能差异
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R3-1 | 路径前缀不同 | 🟠 中 | 现有 `/api/ai/*`,契约 `/api/xcx/chat/*` |
| R3-2 | 功能模式不同 | 🔴 高 | 现有是 SSE 流式(`POST /chat/stream`),契约是同步 JSON`POST /chat/{chatId}/messages` 返回 `{ userMessage, aiReply }`)。两者不可互替 |
| R3-3 | 历史列表字段不同 | 🟠 中 | 现有返回 `{ id, app_id, source_page, first_message_preview }`,契约要求 `{ id, customer_name, last_message, last_time, unread_count }` |
**R3 ✅ 已确认:采用选项 A**(保留 SSE 流式 + 新增同步端点)
实施方案:
- 保留 `POST /api/xcx/chat/stream`SSE 流式对话,路径从 `/api/ai/chat/stream` 迁移)
- 新增 `GET /api/xcx/chat/history`(历史对话列表,替代 `/api/ai/conversations`
- 新增 `GET /api/xcx/chat/{chatId}/messages`(消息查看,替代 `/api/ai/conversations/{id}/messages`
- 契约补充 SSE 端点定义
- `ai_conversations` 表无 `unread_count` 字段CHAT-1 响应中此字段暂返回 0或后续按需新增
### R4数据库新增表设计
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R4-1 | 快照表刷新时机未定义 | 🟠 中 | 3 个 board_*_snapshot 表的刷新策略ETL 后触发 / 每日定时 / 按需)直接影响数据新鲜度和实现复杂度 |
| R4-2 | task_detail_cache 命中率存疑 | 🟡 低 | TASK-2 涉及 8+ 张表,备注/AI 缓存/服务记录变更频繁,缓存频繁失效。建议评估:优化 SQL + 并行查询是否足够,不一定需要缓存表 |
| R4-3 | board_finance_snapshot.metrics 缺 schema | 🟡 低 | JSONB 灵活但无约束,建议在 spec 中定义 metrics 的 key 清单(如 `total_revenue``total_expense``net_income` 等) |
| R4-4 | board_customer_snapshot.spend_60d 窗口定义 | 🟡 低 | "60 天"是 `snapshot_date - 60``snapshot_date`,还是自然月?需明确 |
| R4-5 | 快照表是否真的需要 | 🟠 中 | 看板数据量不大(单店助教 < 50 人、活跃客户 < 5000 人FDW 直查 + 合理索引可能已够用。快照表增加了维护复杂度(刷新逻辑、数据一致性)。建议先不建快照表,性能不足时再加 |
**R4 ✅ 已确认:采用选项 A**(先直查 FDW做好索引和查询优化
- 不建 `board_coach_snapshot``board_customer_snapshot``board_finance_snapshot` 三张快照表
- 不建 `task_detail_cache` 缓存表
- 看板接口直接查 FDW 表,通过合理索引 + SQL 优化保证性能
- 如后续出现性能瓶颈,再按需引入快照/缓存机制
- spec 第四章中的 4 张新增表建议全部移除
### R5权限模型
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R5-1 | 看板接口权限映射未明确 | 🟠 中 | BOARD-1 → `view_board_coach`BOARD-2 → `view_board_customer`BOARD-3 → `view_board_finance`?需确认 |
| R5-2 | PERF/CUST/COACH 权限未定义 | 🟠 中 | 绩效/客户/助教模块是否只需 `require_approved()`?助教只能看自己的绩效,管理员能看所有人的? |
| R5-3 | 数据隔离粒度 | 🟠 中 | 助教只能看自己的任务/绩效/客户(通过 `assistant_id` 过滤),管理员能看全店。这个逻辑在 service 层实现还是中间件层? |
**R5 ✅ 已确认**:现有 5 个权限 code 够用,从上游入口控制即可。
- `view_tasks`任务模块TASK-1/2、PERF-1/2、CUST-1/2、COACH-1 均通过此权限 + `require_approved()` 控制)
- `view_board` + `view_board_finance` / `view_board_customer` / `view_board_coach`:看板模块
- 数据隔离在 service 层实现:助教通过 `user_assistant_binding` 获取 `assistant_id`,只查自己的数据;管理员角色可查全店
- 权限选项固定,角色-权限映射通过租户管理后台灵活配置
- 不新增权限 code
### R6数据源与口径
| # | 问题 | 严重度 | 说明 |
|---|------|--------|------|
| R6-1 | TASK-1 performance.total_customers 来源不明 | 🟡 低 | `dws_assistant_salary_calc` 是否有客户数字段?可能需要从 `dws_assistant_customer_stats` 单独查 |
| R6-2 | CUST-1 消费记录混合 vs 分类 | 🟡 低 | 3 种消费类型(台桌/商城/充值)在 API 契约中是单一 `consumption_records` 数组,暗示混合排序。确认? |
| R6-3 | BOARD-2 维度切换方式 | 🟡 低 | API 契约通过 `dimension` 参数切换 8 个维度,一次返回一个维度的排序结果。确认? |
### R7spec 中引用但未定义的表
| # | 表名 | 引用位置 | 说明 |
|---|------|---------|------|
| R7-1 | `biz.ai_cache` | TASK-2app4/5/6/7、BOARD-3app2 | 多处引用但 BD 手册无定义。需确认表结构或补充 BD 手册 |
| R7-2 | `biz.ai_conversations` + `biz.ai_messages` | CHAT 模块 | 现有 `xcx_ai_chat.py` 通过 `ConversationService` 操作,但 spec 未引用其结构 |
| R7-3 | `public.member_retention_clue` | TASK-2、CUST-1 | 维客线索表spec 未描述字段结构和脱敏规则 |
**R7 ✅ 已确认(查库验证)**4 张表全部已建,无需新增 DDL。
| 表名 | Schema | 字段数 | 关键字段 |
|------|--------|--------|---------|
| `ai_cache` | biz | 9 | `cache_type`app2_finance/app3_clue/app4_analysis/app5_script/app6_note/app7_task`target_id``result_json`(JSONB)、`expires_at` |
| `ai_conversations` | biz | 8 | `user_id`(varchar)、`app_id``site_id``source_page``source_context`(JSONB) |
| `ai_messages` | biz | 6 | `conversation_id`(FK)、`role``content``tokens_used` |
| `member_retention_clue` | public | 10 | `member_id``category``summary``detail``recorded_by_assistant_id``recorded_by_name``source`(默认 'manual') |
脱敏规则TASK-2/CUST-1 返回维客线索时,后端应排除 `recorded_by_assistant_id``recorded_by_name` 字段(仅返回 `category``summary``detail``source``recorded_at`)。
### R8其他待确认项
| # | 问题 | 来源 | 状态 |
|---|------|------|------|
| R8-1 | CONFIG-1 技能类型 | spec §3.7 + api.ts | ✅ 使用 cfg 表 |
| R8-2 | TASK-2 近期服务记录分页 | spec §3.2 | ✅ 一批 20 条,懒加载 |
| R8-3 | TASK-2 备注分页 | spec §3.2 | ✅ 一批 20 条,懒加载 |
| R8-4 | BOARD-2 每维度取前多少名 | spec §3.6 | ✅ 一批 20 条,懒加载,无上限 |
| R8-5 | BOARD-3 环比 | spec §3.6 | ✅ 全部月环比(详见下方) |
| R8-6 | COACH-1 "隐藏任务" | spec §3.5 + API 契约 | ✅ `inactive` 状态(详见下方) |
| R8-7 | FDW 查询超时 | 性能风险 | 待定(实施时按需设置) |
| R8-8 | 金额口径 | DWD-DOC 标杆 | ✅ `items_sum`,禁用 `consume_money` |
**R8-1 ✅ 已确认**:使用 cfg 表。后端从 ETL 库的 cfg 表读取技能类型列表,前端 api.ts 中的硬编码仅作为 mock 回退。
**R8-2/3 ✅ 已确认**:一批 20 条,懒加载。
- 前端修改需求TASK-2 页面的服务记录区域和备注区域需从一次性渲染改为懒加载模式(`page` + `pageSize=20` 参数 + 滚动到底部触发"加载更多"
**R8-4 ✅ 已确认**:一批 20 条,懒加载,无上限。
- 前端修改需求BOARD-2 客户看板需从一次性渲染改为懒加载模式
**R8-5 ✅ 已确认(环比调研完成)**
- 所有环比均为**月环比**(与上月同期对比)
- 共 60+ 个环比数据点,覆盖 6 个板块经营一览8 项、预收资产6 项 + 赠送卡矩阵 12 项)、应计收入确认(收入结构 9 项 + 正价 4 项 + 优惠 4 项 + 渠道 3 项、现金流入5 项、现金流出15 项)、助教分析(基础课 + 激励课各 6 项)
- 前端已实现环比开关(`compareEnabled`),环比值格式为百分比字符串(如 `"+12.5%"`
- API 契约缺陷BOARD-3 响应中 `trend` 字段仅支持 `up/down/flat`**未定义环比百分比值**,需补充 `compareValue: string`(如 `"12.5%"`)字段
- 后端需计算所有指标的月环比值(当前期 vs 上期),返回百分比和方向
- 页面筛选支持本月、上月、本周、上周、前3个月、本季度、上季度、最近6个月
**R8-6 ✅ 已确认**"隐藏任务"= 回访任务被顶替后,仍有一段生效时间但前端不显示。
- 映射关系:`hidden_tasks` 对应 `coach_tasks.status = 'inactive'`(被新任务顶替但未过期)
- 后端在 COACH-1 接口中按 status 分组返回:`active``visible_tasks``inactive``hidden_tasks``abandoned``abandoned_tasks`
- 前端不展示 `hidden_tasks`,但后端仍需返回(管理端可能需要查看)
---
## 八¾、看板筛选项交叉矩阵2026-03-18 Kiro 调研)
> 调研方法:逐文件阅读 `board-coach.ts`、`board-customer.ts`、`board-finance.ts` 的完整源码,提取所有筛选项定义、可选值、默认值、交叉约束、前端传参方式,并与 API 契约和 `api.ts` service 层交叉验证。
### B1BOARD-1 助教看板board-coach
#### 筛选项清单
| 参数名 | 类型 | 可选值 | 默认值 | 说明 |
|--------|------|--------|--------|------|
| `sort` | 排序维度 | `perf_desc``perf_asc``salary_desc``salary_asc``sv_desc``task_desc` | `perf_desc` | 6 种排序,决定卡片模板 |
| `skill` | 技能筛选 | `all``chinese``snooker``mahjong``karaoke` | `all` | 不限、中式/追分、斯诺克、麻将/棋牌、团建/K歌 |
| `time` | 时间范围 | `month``quarter``last_month``last_3m``last_quarter``last_6m` | `month` | 本月、本季度、上月、前3个月、上季度、最近6个月 |
#### 排序→卡片模板映射(`SORT_TO_DIM`
| sort 值 | dimType | 卡片显示内容 |
|---------|---------|------------|
| `perf_desc` / `perf_asc` | `perf` | 定档业绩工时、上期工时、距升档差距、是否达标 |
| `salary_desc` / `salary_asc` | `salary` | 工资总额、定档工时、上期工时 |
| `sv_desc` | `sv` | 客源储值额、储值客户数、储值消耗 |
| `task_desc` | `task` | 召回任务完成数、回访任务完成数 |
#### 各维度卡片所需字段(后端响应必须包含)
所有维度共享的基础字段(每个 item 都返回):
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 助教 ID |
| `name` | string | 助教姓名 |
| `initial` | string | 姓名首字(头像占位) |
| `avatarGradient` | string | 头像渐变色blue/green/pink/amber/violet/cyan |
| `level` | string | 等级 key`star`/`senior`/`middle`/`junior` |
| `levelClass` | string | 等级样式类(前端可自行映射,后端可不返回) |
| `skills` | array | 技能列表 `[{ text: '🎱', cls: 'skill--chinese' }]` |
| `topCustomers` | string[] | Top 客户列表(如 `['💖 王先生', '💛 李女士']` |
各维度专属字段:
| dimType | 维度名 | 专属字段 | 类型 | 说明 |
|---------|--------|---------|------|------|
| `perf` | 定档业绩 | `perfHours` | number | 当期定档工时 |
| | | `perfHoursBefore` | number? | 上期定档工时(可选,无上期数据时不返回) |
| | | `perfGap` | string? | 距升档差距描述(如 `"距升档 13.8h"`,已达标时不返回) |
| | | `perfReached` | boolean | 是否已达标 |
| `salary` | 工资 | `salary` | number | 工资总额(元) |
| | | `salaryPerfHours` | number | 定档工时 |
| | | `salaryPerfBefore` | number? | 上期定档工时 |
| `sv` | 客源储值 | `svAmount` | number | 客源储值总额(元) |
| | | `svCustomerCount` | number | 储值客户数 |
| | | `svConsume` | number | 储值消耗额(元) |
| `task` | 任务完成 | `taskRecall` | number | 召回任务完成数 |
| | | `taskCallback` | number | 回访任务完成数 |
> 后端设计建议:所有维度的字段统一返回在同一个 item 对象中(扁平结构),前端根据当前 `dimType` 选择性渲染。这样切换维度时无需重新请求(数据已在本地),仅切换卡片模板即可。如果数据量大或查询开销高,也可按 `sort` 参数只返回当前维度所需字段。
#### 交叉约束
| 约束 | 说明 |
|------|------|
| `time=last_6m` + `sort=sv_desc` | ⚠️ 不兼容。前端 TIME_OPTIONS 注释标注"不支持客源储值最高"。后端需返回 400 或忽略 |
| 其他组合 | 无限制3 参数自由组合 |
#### 交叉组合总数
- sort(6) × skill(5) × time(6) = **180 种**(减去 1 个不兼容 = **175 种有效组合**
#### 前端传参(`api.ts`
```typescript
fetchBoardCoaches({ skill, sort, time })
```
#### ⚠️ 前端 Bug筛选变更不触发重新请求
`onSortChange`/`onSkillChange`/`onTimeChange` 仅更新 `data` 状态(`selectedSort`/`selectedSkill`/`selectedTime`**未调用 `loadData()`**。联调时前端需修复:筛选变更后重新调用 `loadData()`
---
### B2BOARD-2 客户看板board-customer
#### 筛选项清单
| 参数名 | 类型 | 可选值 | 默认值 | 说明 |
|--------|------|--------|--------|------|
| `dimension` | 维度切换 | `recall``potential``balance``recharge``recent``spend60``freq60``loyal` | `recall` | 8 个维度,决定卡片模板和排序逻辑 |
| `project` | 项目筛选 | `all``chinese``snooker``mahjong``karaoke` | `all` | 全部、中式/追分、斯诺克、麻将/棋牌、团建/K歌 |
| `page` | 分页页码 | 正整数 | `1` | 前端待补充R8-4 决策20 条懒加载) |
| `pageSize` | 每页条数 | 正整数 | `20` | 前端待补充 |
#### 8 维度→卡片模板映射(`DIMENSION_TO_DIM`
| dimension | 中文名 | 排序依据(后端) | 数据源FDW 表) | 卡片关键字段 |
|-----------|--------|----------------|-----------------|------------|
| `recall` | 最应召回 | WBI 降序 | `v_dws_member_winback_index` | idealDays、elapsedDays、overdueDays、visits30d、balance、recallIndex |
| `potential` | 最大消费潜力 | SPI 降序 | `v_dws_member_spending_power_index` | potentialTags、spend30d、avgVisits、avgSpend |
| `balance` | 最高余额 | balance_amount 降序 | `v_dws_member_consumption_summary` | lastVisit、monthlyConsume、availableMonths |
| `recharge` | 最近充值 | last_recharge_date 降序 | `v_dwd_recharge_order` | lastRecharge、rechargeAmount、recharges60d、currentBalance |
| `recent` | 最近到店 | last_visit_date 降序 | `v_dws_member_visit_detail` | daysAgo、visitFreq、idealDays |
| `spend60` | 最高消费 近60天 | items_sum_60d 降序 | `v_dws_member_consumption_summary` | spend60d、visits60d、highSpendTag |
| `freq60` | 最频繁 近60天 | visit_count_60d 降序 | `v_dws_member_consumption_summary` | avgInterval、weeklyVisits8 周柱状图) |
| `loyal` | 最专一 近60天 | max_rs 降序 | `v_dws_member_assistant_relation_index` | intimacy、topCoachName、coachDetails助教明细表 |
#### 各维度卡片所需字段(后端响应必须包含)
所有维度共享的基础字段(每个 item 都返回):
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 客户 member_id |
| `name` | string | 客户姓名 |
| `initial` | string | 姓名首字(头像占位) |
| `avatarCls` | string | 头像样式类avatar--amber/pink/blue 等) |
| `assistants` | array | 关联助教列表 `[{ name, cls, heartScore, badge?, badgeCls? }]` |
各维度专属字段:
| dimType | 维度名 | 专属字段 | 类型 | 说明 |
|---------|--------|---------|------|------|
| `recall` | 最应召回 | `idealDays` | number | 理想到店间隔(天) |
| | | `elapsedDays` | number | 已过天数 |
| | | `overdueDays` | number | 超期天数(= elapsedDays - idealDays |
| | | `visits30d` | number | 近 30 天到店次数 |
| | | `balance` | string | 余额(格式化,如 `"¥2,680"` |
| | | `recallIndex` | string | 召回指数(如 `"9.2"` |
| `potential` | 最大消费潜力 | `potentialTags` | array | 潜力标签 `[{ text: '高频', theme: 'primary' }]` |
| | | `spend30d` | string | 近 30 天消费 |
| | | `avgVisits` | string | 月均到店(如 `"6.2次"` |
| | | `avgSpend` | string | 次均消费 |
| `balance` | 最高余额 | `balance` | string | 当前余额 |
| | | `lastVisit` | string | 最近到店(如 `"3天前"` |
| | | `monthlyConsume` | string | 月均消耗 |
| | | `availableMonths` | string | 可用月数(如 `"约0.8个月"` |
| `recharge` | 最近充值 | `lastRecharge` | string | 最后充值日期(如 `"2月15日"` |
| | | `rechargeAmount` | string | 充值金额 |
| | | `recharges60d` | string | 近 60 天充值次数 |
| | | `currentBalance` | string | 当前余额 |
| `recent` | 最近到店 | `daysAgo` | number | 距今天数(右上角大字) |
| | | `visitFreq` | string | 到店频率(如 `"6.2次/月"` |
| | | `idealDays` | number | 理想间隔 |
| | | `visits30d` | number | 近 30 天到店 |
| | | `avgSpend` | string | 次均消费 |
| `spend60` | 最高消费 近60天 | `spend60d` | string | 近 60 天消费总额 |
| | | `visits60d` | string | 近 60 天到店次数 |
| | | `highSpendTag` | boolean | 是否高消费标签 |
| | | `avgSpend` | string | 次均消费 |
| `freq60` | 最频繁 近60天 | `visits60d` | string | 近 60 天到店次数(右上角大字) |
| | | `avgInterval` | string | 平均到店间隔(如 `"5.0天"` |
| | | `weeklyVisits` | array | 8 周到店柱状图 `[{ val: number, pct: number }]` |
| | | `spend60d` | string | 近 60 天消费 |
| `loyal` | 最专一 近60天 | `intimacy` | string | 亲密度指数 |
| | | `topCoachName` | string | 最亲密助教姓名 |
| | | `topCoachHeart` | number | 最亲密助教爱心分 |
| | | `topCoachScore` | string | 最亲密助教关系指数 |
| | | `coachName` | string | 主助教姓名 |
| | | `coachRatio` | string | 主助教占比(如 `"78%"` |
| | | `coachDetails` | array | 助教服务明细表 `[{ name, cls, heartScore, badge?, avgDuration, serviceCount, coachSpend, relationIdx }]` |
> 后端设计建议:与助教看板不同,客户看板 8 个维度的字段差异很大且数据来源不同8 张不同的 FDW 表)。建议后端按 `dimension` 参数只查询和返回当前维度所需字段,切换维度时前端重新请求。这样避免一次查询 8 张表的性能开销。
#### 交叉约束
- **无交叉限制**dimension 和 project 完全独立,可自由组合
#### 交叉组合总数
- dimension(8) × project(5) = **40 种有效组合**
#### 前端传参(`api.ts`
```typescript
fetchBoardCustomers({ dimension, project, sort })
// 注意sort 参数在 api.ts 签名中存在但前端未使用(无排序筛选 UI
// page/pageSize 参数缺失需前端补充R8-4 决策)
```
#### ⚠️ 前端 Bug筛选变更不触发重新请求
`onDimensionChange`/`onProjectChange` 仅更新 `data` 状态,**未调用 `loadData()`**。联调时前端需修复。
#### ⚠️ 前端缺失:分页参数
R8-4 已确认 20 条懒加载,但 `fetchBoardCustomers` 签名和 `board-customer.ts` 均无 `page`/`pageSize` 参数和"加载更多"逻辑。联调时前端需补充。
---
### B3BOARD-3 财务看板board-finance
#### 筛选项清单
| 参数名 | 类型 | 可选值 | 默认值 | 说明 |
|--------|------|--------|--------|------|
| `time` | 时间范围 | `month``lastMonth``week``lastWeek``quarter3``quarter``lastQuarter``half6` | `month` | 8 种时间范围 |
| `area` | 区域筛选 | `all``hall``hallA``hallB``hallC``mahjong``teamBuilding` | `all` | 7 种区域 |
| `compareEnabled` | 环比开关 | `true` / `false` | `false` | 控制环比数据显示/隐藏 |
#### 6 个板块及筛选影响
| 板块 | sectionId | 受 time 影响 | 受 area 影响 | 受 compare 影响 | 特殊规则 |
|------|-----------|:-----------:|:-----------:|:--------------:|---------|
| 经营一览 | section-overview | ✅ | ✅ | ✅ | 无 |
| 预收资产 | section-recharge | ✅ | ⚠️ | ✅ | **仅 `area=all` 时显示**,选中具体区域时整个板块隐藏 |
| 应计收入确认 | section-revenue | ✅ | ✅ | ✅ | 含收入结构表(按区域分行)、正价明细、优惠明细、渠道明细 |
| 现金流入 | section-cashflow | ✅ | ✅ | ✅ | 无 |
| 现金流出 | section-expense | ✅ | ✅ | ✅ | 含经营支出、固定支出、助教分成、平台服务费 4 个子分组 |
| 助教分析 | section-coach | ✅ | ✅ | ✅ | 含基础课、激励课两个子表(各按等级分行) |
#### 环比开关行为
| 状态 | 行为 |
|------|------|
| `compareEnabled=false`(默认) | 仅显示当期数值,隐藏所有环比箭头和百分比 |
| `compareEnabled=true` | 每个数据单元格下方显示环比方向+百分比 |
环比样式规则:
- 上升:绿色 ↑ + 百分比(如 `↑12.5%`
- 下降:红色 ↓ + 百分比(如 `↓3.2%`
- 持平:灰色文字 `持平`
环比计算基准:与上一个相同时间周期对比(本月 vs 上月、本周 vs 上周、本季度 vs 上季度等)
#### 交叉约束
| 约束 | 说明 |
|------|------|
| `area ≠ all` → 预收资产板块隐藏 | 储值卡数据不按区域拆分,选中具体区域时无意义 |
| `compareEnabled` 独立 | 与 time/area 无交叉限制 |
| time + area 自由组合 | 无其他限制 |
#### 交叉组合总数
- time(8) × area(7) × compare(2) = **112 种组合**
- 其中 area≠all 时预收资产板块隐藏(不影响请求参数,仅影响前端渲染)
#### ⚠️ 重大设计缺陷:筛选参数不传后端
当前 `fetchBoardFinance` 签名:
```typescript
fetchBoardFinance({ date?: string }) // 仅 date 参数
```
前端 `onTimeChange`/`onAreaChange`/`toggleCompare` 仅更新本地 data 状态,**不重新调用 API**。mock 模式下不需要重新请求(数据内联),但联调后必须:
1. `fetchBoardFinance` 签名扩展为 `{ time, area, compareEnabled }` 三个参数
2. 筛选变更后重新调用 `fetchBoardFinance`
3. 后端根据 `time` 计算日期范围、根据 `area` 过滤区域、根据 `compareEnabled` 决定是否返回环比数据
#### 后端 API 参数设计建议
```
GET /api/xcx/board/finance?time={time}&area={area}&compare={0|1}
```
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| `time` | string | 否 | `month` | 时间范围枚举 |
| `area` | string | 否 | `all` | 区域枚举 |
| `compare` | 0/1 | 否 | `0` | 是否返回环比数据(减少不需要时的计算开销) |
---
### 前端修改需求汇总(联调前必须完成)
#### 看板页修复F1-F6
| # | 页面 | 问题 | 修改内容 |
|---|------|------|---------|
| F1 | board-coach | 筛选变更不触发重新请求 | `onSortChange`/`onSkillChange`/`onTimeChange` 末尾加 `this.loadData()` |
| F2 | board-customer | 筛选变更不触发重新请求 | `onDimensionChange`/`onProjectChange` 末尾加 `this.loadData()` |
| F3 | board-customer | 缺少分页参数 | `fetchBoardCustomers` 签名加 `page`/`pageSize`,页面加"加载更多"逻辑 |
| F4 | board-finance | 筛选参数不传后端 | `fetchBoardFinance` 签名扩展为 `{ time, area, compare }`,筛选变更后重新调用 |
| F5 | board-finance | 筛选变更不触发重新请求 | `onTimeChange`/`onAreaChange` 末尾加 `this.loadData()` |
| F6 | board-coach | `time=last_6m` + `sort=sv_desc` 不兼容 | 前端在选择 `last_6m` 时禁用 `sv_desc` 选项,或选择 `sv_desc` 时禁用 `last_6m` |
#### 列表页修复F7-F11
| # | 页面 | 问题 | 修改内容 |
|---|------|------|---------|
| F7 | task-list | status 筛选 UI 未实现 | API 支持 `status` 参数但页面无筛选控件,需添加 Tab 或筛选栏 |
| F8 | performance | 无月份切换功能 | API 支持 `year`/`month` 参数但页面固定当前月,需添加月份切换按钮 |
| F9 | performance-records | 月份切换时未重置分页 | `switchMonth()` 中需将 `page` 重置为 1 |
| F10 | customer-service-records | 月份切换采用本地筛选 | `updateMonthView()` 仅本地过滤,联调后需改为重新调用 API |
| F11 | notes | 无触底加载逻辑 | API 支持分页但页面未实现 `onReachBottom()` |
---
### 非看板列表页筛选矩阵
#### L1task-list任务列表
| 参数名 | 类型 | 可选值 | 默认值 | UI 实现 | 说明 |
|--------|------|--------|--------|:-------:|------|
| `status` | string | `pending``completed``abandoned` | 无(全部) | ❌ 缺失 | API 支持但页面无筛选控件 |
| `page` | number | 1+ | `1` | ✅ 自动 | 触底加载 |
| `pageSize` | number | 1+ | `20` | ✅ 硬编码 | — |
交互:下拉刷新重置 page=1触底 page++。前端将任务分为 pinnedTasks / normalTasks / abandonedTasks 三组渲染。
#### L2performance绩效概览
| 参数名 | 类型 | 可选值 | 默认值 | UI 实现 | 说明 |
|--------|------|--------|--------|:-------:|------|
| `year` | number | 当前年 ± N | 当前年 | ❌ 缺失 | 固定当前年 |
| `month` | number | 1-12 | 当前月 | ❌ 缺失 | 固定当前月 |
交互:无筛选 UI页面加载时固定请求当前年月。无分页。
#### L3performance-records绩效明细
| 参数名 | 类型 | 可选值 | 默认值 | UI 实现 | 说明 |
|--------|------|--------|--------|:-------:|------|
| `year` | number | 当前年 ± N | 当前年 | ✅ 月份切换按钮 | 上月/下月箭头 |
| `month` | number | 1-12 | 当前月 | ✅ 月份切换按钮 | 不可超过当前月 |
| `page` | number | 1+ | `1` | ✅ 自动 | 触底加载 |
| `pageSize` | number | 1+ | `20` | ✅ 硬编码 | — |
月份切换详细交互:
1. 页面顶部显示 `{year}年{month}月` 标签,左右各有箭头按钮
2. 点击左箭头 → `currentMonth--`(跨年自动 `currentYear--``currentMonth=12`
3. 点击右箭头 → `currentMonth++`(跨年自动 `currentYear++``currentMonth=1`
4. 边界:`canGoNext = false``currentYear == nowYear && currentMonth == nowMonth`(不可超过当前月);`canGoPrev = true`(无下限)
5. 月份变更后 → 调用 `loadData()` → 重新请求 API `fetchPerformanceRecords({ year, month, page, pageSize })`
6. ⚠️ Bug`switchMonth()` 中未将 `page` 重置为 1月份切换后可能请求第 N 页数据
后端响应结构(按月返回):
- `summary`:当月汇总(总笔数、总工时、折算前总工时、总收入)
- `date_groups`:按日期降序分组,每组含日期标签、当日总工时/总收入、记录列表
- 每条记录字段:`id``customerName``timeRange`(如 `"20:00-22:00"`)、`hours`(折算后)、`hoursRaw`(折算前,可选)、`courseType``courseTypeClass``tag-basic`/`tag-vip`/`tag-tip`)、`location`(台号)、`income`
后端时间范围处理:
- 接收 `year` + `month` → 转换为 `biz_date BETWEEN '{year}-{month}-01' AND '{year}-{month}-{lastDay}'`
- 数据源:`fdw_etl.v_dwd_assistant_service_log`(按 `assistant_id` + `biz_date` 范围查询)
#### L4customer-service-records客户服务记录
| 参数名 | 类型 | 可选值 | 默认值 | UI 实现 | 说明 |
|--------|------|--------|--------|:-------:|------|
| `customerId` | string | 客户 ID | 从 options 读取 | ✅ 自动 | 页面参数 |
| `year` | number | 当前年 ± N | 当前年 | ✅ 月份切换按钮 | 上月/下月箭头 |
| `month` | number | 1-12 | 当前月 | ✅ 月份切换按钮 | 不可超过当前月 |
| `table` | string | 台号 | 无 | ❌ 缺失 | API 支持但页面未使用 |
月份切换详细交互:
1. 页面顶部显示 `{year}年{month}月` 标签,左右各有箭头按钮
2. 点击左箭头 → `currentMonth--`(跨年处理同 L3
3. 点击右箭头 → `currentMonth++`(跨年处理同 L3
4. 边界:`canPrev` = `yearMonth > minYearMonth`(最早 12 个月前);`canNext` = `yearMonth < maxYearMonth`(不超过当前月)
5. 月份变更后 → 调用 `updateMonthView()`**仅本地过滤**(从 `allRecords` 中按 `date.startsWith('{year}-{month}')` 筛选)
6. ⚠️ 设计问题:首次 `loadData()` 一次性拉取全部记录,月份切换仅本地过滤。联调后如果数据量大(单客户数百条记录),需改为按月请求 API
本地筛选逻辑:
- `allRecords`(首次加载时获取全部)→ 按 `date` 字段前缀 `{year}-{month}` 过滤 → 转换为 `ServiceRecord` 展示格式
- 月度统计:`monthCount`(当月记录数)、`monthHours`(当月总时长)
- 记录字段:`table`(台号)、`type`(课程类型标签)、`typeClass``basic`/`vip`/`tip`/`recharge`)、`recordType``course`/`recharge`)、`duration`(折算后小时)、`income`(到手金额)、`drinks`(饮品描述)、`date`(显示日期+时间段)
后端时间范围处理(联调后建议改为按月请求):
- 当前方案:一次返回全部记录,前端本地按月过滤
- 建议方案:`fetchCustomerRecords({ customerId, year, month })` → 后端按月查询 `fdw_etl.v_dwd_assistant_service_log`(按 `member_id` + `assistant_id` + `biz_date` 范围)
#### L5notes备注列表
| 参数名 | 类型 | 可选值 | 默认值 | UI 实现 | 说明 |
|--------|------|--------|--------|:-------:|------|
| `page` | number | 1+ | `1` | ✅ 自动 | 下拉刷新重置 |
| `pageSize` | number | 1+ | `20` | ✅ 硬编码 | — |
交互:下拉刷新重置 page=1。⚠ 无触底加载(`onReachBottom()` 未实现)。
#### L6customer-detail / coach-detail详情页
无筛选项,单次加载全部数据。
---
## 八⅞、Storyboard 走查 Gap 汇总2026-03-18
> 走查方法:两个子代理分别以助教视角和管理层视角,逐页面对照前端内联 mock 数据 vs API 契约 vs 八¾节字段定义,提取所有未记录的接口需求 Gap。
> 完整报告:`docs/reports/storyboard-walkthrough-assistant-view.md`助教视角51 Gap+ `docs/reports/miniprogram-storyboard-walkthrough-gaps.md`管理层视角31 Gap
### 统计
| 严重度 | 助教视角 | 管理层视角 | 去重后合计 |
|--------|:--------:|:----------:|:----------:|
| 🔴 高(阻塞联调) | 17 | 14 | ~20 |
| 🟠 中(影响完整性) | 19 | 12 | ~18 |
| 🟡 低(可后续修复) | 15 | 5 | ~12 |
### 核心发现
1. **契约与前端严重脱节**BOARD-1/2/3、CUST-1、COACH-1、PERF-1、TASK-1 performance 的契约响应定义与前端实际需求严重不匹配,需完全重写
2. **BOARD-3 财务看板**:契约定义扁平 `metrics` 数组,前端需要 6 个独立板块overview/recharge/revenue/cashflow/expense/coachAnalysis的深度嵌套结构含 200+ 字段
3. **CUST-1 客户详情**:缺少 5 大模块aiInsight/coachTasks/favoriteCoaches/消费记录嵌套结构/备注列表)
4. **COACH-1 助教详情**:缺少 6 大模块performance/income/tierNodes/historyMonths/topCustomers 扩展/备注列表)
5. **跨页面参数传递**:多处 taskId/customerId 混淆、未传参数、用 name 代替 id 跳转(用户决策:统一用唯一 ID
6. **CHAT 模块**referenceCard 未定义、customerId→chatId 映射缺失、SSE 端点未在契约中定义
### 处置方案
全部 Gap 已纳入 **[RNS1 拆分计划](./RNS1-split-plan.md)**,分 5 个子 Spec 逐步实施。Gap 追溯矩阵见拆分计划末尾。
---
## 九、任务清单
> ⚠️ **已拆分**原始任务清单T0-1 ~ T18已被 Storyboard 走查发现的 80+ 个 Gap 大幅扩展,体量超出单个 SPEC 可控范围。
>
> 完整的拆分计划见 **[RNS1-split-plan.md](./RNS1-split-plan.md)**,将 NS1 拆为 5 个子 Spec
>
> | 子 Spec | 名称 | 任务数 | 状态 |
> |---------|------|:------:|:----:|
> | RNS1.0 | 基础设施与契约重写 | 6 | 待启动 |
> | RNS1.1 | 任务与绩效接口 | 6 | 待启动 |
> | RNS1.2 | 客户与助教接口 | 6 | 待启动 |
> | RNS1.3 | 三看板接口 | 7 | 待启动 |
> | RNS1.4 | CHAT 对齐与联调收尾 | 5 | 待启动 |
>
> 执行顺序RNS1.0 → (RNS1.1 ∥ RNS1.2 ∥ RNS1.3) → RNS1.4
>
> 需求追溯:两份走查报告(`docs/reports/storyboard-walkthrough-assistant-view.md` + `docs/reports/miniprogram-storyboard-walkthrough-gaps.md`)中的全部 Gap 已在拆分计划的追溯矩阵中映射到具体任务。
### 原始任务清单(归档参考)
<details>
<summary>点击展开原始 Batch 0~C 任务清单(已被 RNS1 拆分计划替代)</summary>
#### Batch 0基础设施改造阻塞所有新接口
- [ ] T0-1全局响应包装中间件`{ code: 0, data: ... }` + 异常处理器 `{ code, message }`
- [ ] T0-2Pydantic schema 统一 camelCase`alias_generator=to_camel`,所有现有 + 新增 schema
- [ ] T0-3后端 TASK-4 路径从 `/cancel-abandon` 改为 `/restore`(对齐契约)
- [ ] T0-4前端 `request()` 工具函数加 `.data` 解包适配
#### Batch A核心业务接口
- [ ] T1扩展 TASK-1任务列表 + 绩效概览 + 分页 + 懒加载)
- [ ] T2实现 TASK-2任务详情完整版服务记录/备注各 20 条懒加载)
- [ ] T3实现 PERF-1绩效概览
- [ ] T4实现 PERF-2绩效明细20 条懒加载)
- [ ] T5实现 CUST-1客户详情消费记录 3 类混合排序 + 懒加载)
- [ ] T6实现 CUST-2客户服务记录20 条懒加载)
- [ ] T7实现 COACH-1助教详情任务按 active/inactive/abandoned 分组)
#### Batch B看板 + 配置接口
- [ ] T8实现 BOARD-1助教看板FDW 直查)
- [ ] T9实现 BOARD-2客户看板8 维度切换20 条懒加载FDW 直查)
- [ ] T10实现 BOARD-3财务看板60+ 指标月环比计算FDW 直查)
- [ ] T11实现 CONFIG-1技能类型列表从 cfg 表读取)
#### Batch CCHAT 对齐、前端修复与联调
- [ ] T12CHAT 路径迁移(`/api/ai/*``/api/xcx/chat/*`,保留 SSE 流式端点)
- [ ] T13新增 CHAT-1/2 同步端点(历史列表 + 消息查看)
- [ ] T14API 契约补充SSE 端点定义 + BOARD-3 `compareValue` 字段 + BOARD-3 `time`/`area`/`compare` 参数)
- [ ] T15FDW 端到端验证
- [ ] T16前端懒加载改造TASK-2 服务记录/备注、BOARD-2 客户列表)
- [ ] T17前端看板筛选修复F1-F6详见八¾节
- [ ] T18前后端联调 + 修复
</details>

View File

@@ -0,0 +1,419 @@
# NS2AI Prompt 细化 — ai-prompt-refinement
> 优先级NS1 完成后立即执行Prompt 细化是 AI 功能闭环的关键)
> 预估工作量:中等
> 前置条件P5-A 已完成AI 骨架就绪、NS1 已完成(后端 API 数据结构确定)
> 参考基准:`docs/prd/specs/P5-miniapp-ai-integration.md`P5-B 阶段定义)
---
## 一、背景与目标
P5-A 阶段已交付 AI 集成管道:百炼封装、缓存 API、SSE 框架、应用 2/8 完整实现、应用 1/3/4/5/6/7 的触发机制和调用骨架。但应用 1/3/4/5/6/7 的 `build_prompt()` 函数仍为占位骨架,核心数据字段(`consumption_records``service_history``objective_data` 等)标记为 TODO。
本 SPEC 目标:将 6 个应用的 Prompt 拼接函数从骨架升级为完整实现,使 AI 应用能基于真实数据生成有价值的分析结果。
### 当前骨架状态
| 应用 | 文件 | 骨架状态 | 待细化字段 |
|------|------|---------|-----------|
| 应用 1 | `app1_chat.py` | 页面上下文文本化工具留接口 | `page_context``screen_content` 各页面文本化 |
| 应用 3 | `app3_clue.py` | `build_prompt()` 占位 | `consumption_records`DWD+DWS 订单明细全维度) |
| 应用 4 | `app4_analysis.py` | `build_prompt()` 占位 | `service_history``assistant_info` |
| 应用 5 | `app5_tactics.py` | `build_prompt()` 占位 | `service_history``assistant_info`(同应用 4 |
| 应用 6 | `app6_note.py` | `build_prompt()` 占位 | `consumption_data`(同应用 3 的 main_data |
| 应用 7 | `app7_customer.py` | `build_prompt()` 占位 | `objective_data`(同应用 3 的 main_data |
---
## 二、技术架构
### 2.1 数据获取层(新建共享模块)
Prompt 细化的核心是从数据库获取真实数据并格式化为 AI 可读的 JSON。多个应用共享相同的数据获取逻辑应抽取为共享模块
```
apps/backend/app/ai/
├── data_fetchers/ 🆕 新建
│ ├── __init__.py
│ ├── member_data.py # 客户数据获取(消费记录、会员卡、余额等)
│ ├── assistant_data.py # 助教数据获取(基本信息、服务历史)
│ ├── service_history.py # 服务记录获取(助教-客户维度)
│ └── page_context.py # 页面上下文文本化(应用 1 专用)
├── apps/
│ ├── app1_chat.py 🔧 补充页面文本化调用
│ ├── app3_clue.py 🔧 补充 consumption_records 拼接
│ ├── app4_analysis.py 🔧 补充 service_history + assistant_info
│ ├── app5_tactics.py 🔧 补充 service_history + assistant_info
│ ├── app6_note.py 🔧 补充 consumption_data
│ └── app7_customer.py 🔧 补充 objective_data
```
### 2.2 数据获取函数设计
#### `member_data.py` — 客户数据获取(应用 3/6/7 共用)
```python
async def fetch_member_consumption_data(
site_id: int, member_id: int, months: int = 3
) -> dict:
"""获取客户近 N 个月消费数据DWD+DWS 全维度)。
返回结构对应 P5 spec 中 main_data
- consumption_records: 台桌结账 + 商城订单明细列表
- member_cards: 会员卡明细
- card_balance_total: 储值卡余额合计
- stored_value_balance_total: 储值余额合计
- expected_visit_date: 预计到店日期
- days_since_last_visit: 距上次到店天数
"""
```
数据源(全部通过 FDW
- `fdw_etl.v_dwd_settlement_head` + `fdw_etl.v_dwd_table_fee_log`(台桌结账)
- `fdw_etl.v_dwd_store_goods_sale`(商城订单)
- `fdw_etl.v_dwd_recharge_order`(充值记录)
- `fdw_etl.v_dim_member_card_account`(会员卡明细)
- `fdw_etl.v_dws_member_consumption_summary`(消费汇总 + 余额)
- `fdw_etl.v_dws_member_visit_detail`(到店明细 → 计算预计到店日期)
> ⚠️ 金额口径:使用 `items_sum`= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money禁止 `consume_money`
> ⚠️ 会员字段断档DQ-6/DQ-7member_phone/member_name 通过 member_id JOIN dim_member
#### `assistant_data.py` — 助教数据获取(应用 4/5 共用)
```python
async def fetch_assistant_info(
site_id: int, assistant_id: int
) -> dict:
"""获取助教基本信息(花名、级别、工龄等)。"""
async def fetch_service_history(
site_id: int, assistant_id: int, member_id: int, months: int = 3
) -> list[dict]:
"""获取助教服务该客户的历史记录。"""
```
数据源:
- `fdw_etl.v_dim_assistant`(基本信息)
- `fdw_etl.v_dwd_assistant_service_log`(服务记录,按助教+客户筛选)
- `fdw_etl.v_dws_member_assistant_relation_index`(关系指数)
- `fdw_etl.v_dws_member_assistant_intimacy`(亲密度)
#### `page_context.py` — 页面上下文文本化(应用 1 专用)
```python
async def build_page_text(
source_page: str, context_data: dict, site_id: int
) -> str:
"""将页面数据转换为 AI 可读的结构化文本。
根据 source_page 类型,从数据库获取对应数据并格式化。
"""
```
支持的页面类型及数据获取:
| source_page | 数据获取 | 文本化内容 |
|-------------|---------|-----------|
| `task-detail` | coach_tasks + member + notes + ai_cache | 任务信息 + 客户信息 + 备注 + AI 分析 |
| `customer-detail` | member + consumption + clues | 客户全信息 + 消费记录 + 维客线索 |
| `coach-detail` | assistant + tasks + notes | 助教信息 + 任务统计 + 备注 |
| `board-finance` | finance DWS 汇总 | 财务数据摘要 |
| `board-customer` | 当前筛选维度 top 列表 | 客户排名摘要 |
| `board-coach` | 当前筛选维度排名 | 助教排名摘要 |
| `performance` | salary_calc + daily_detail | 绩效数据摘要 |
| `my-profile` | user info + assistant binding | 个人信息摘要 |
### 2.3 数据库连接模式
- 所有 FDW 查询通过 `get_etl_readonly_connection(site_id)``fdw_etl.*` 视图
- 业务库查询通过 `get_connection()`
- 查询前必须 `SET LOCAL app.current_site_id`RLS 隔离)
---
## 三、各应用 Prompt 细化详细设计
### 3.1 应用 3客户数据维客线索分析
`build_prompt()` 需要拼接的完整 JSON 结构(参考 P5 spec
```json
{
"current_time": "2026-03-08 14:30:25",
"member_nickname": "客户昵称",
"main_data": {
"consumption_records": [
{
"settle_date": "2026-03-05",
"settle_type": 1,
"items_sum": 280.00,
"table_charge_money": 180.00,
"assistant_pd_money": 80.00,
"assistant_cx_money": 0,
"goods_money": 20.00,
"room_name": "VIP-3",
"duration_minutes": 120,
"assistant_names": ["张助教"]
}
],
"member_cards": [
{"card_type": "金卡", "balance": 1500.00, "gift_balance": 200.00}
],
"card_balance_total": 1700.00,
"stored_value_balance_total": 1700.00,
"expected_visit_date": "2026-03-10",
"days_since_last_visit": 15
},
"reference": {
"app6_clues": [...],
"app8_history": [
{"generated_at": "2026-03-01", "clues": [...]},
{"generated_at": "2026-02-15", "clues": [...]}
]
}
}
```
实现要点:
- `consumption_records``v_dwd_settlement_head` + `v_dwd_table_fee_log` 获取settle_type IN (1,3)
- 金额字段逐项拆分table_charge_money / assistant_pd_money / assistant_cx_money / goods_money不使用 consume_money
- `expected_visit_date``v_dws_member_visit_detail` 的到店间隔推算
- reference 从 `ai_cache` 获取 app6 + app8 最近 2 套历史
### 3.2 应用 4关系分析/任务建议
`build_prompt()` 需要拼接的完整 JSON 结构:
```json
{
"current_time": "2026-03-08 14:30:25",
"assistant_info": {
"nickname": "张助教",
"level": "高级",
"hire_date": "2024-06-01",
"tenure_months": 21,
"monthly_customers": 45,
"performance_tier": "A档"
},
"service_history": [
{
"service_date": "2026-03-01",
"duration_minutes": 90,
"items_sum": 250.00,
"room_name": "VIP-3",
"is_pd": true
}
],
"task_assignment_basis": "优先召回",
"customer_data": {
"system_data": { /* 3 main_data */ },
"notes": [
{"recorded_by": "张助教", "content": "客户喜欢打斯诺克", "created_at": "2026-02-28"}
]
},
"reference": {
"app8_current": {...},
"app8_history": [...]
}
}
```
实现要点:
- `assistant_info``v_dim_assistant` + `v_dws_assistant_salary_calc` 获取
- `service_history``v_dwd_assistant_service_log` 按助教+客户筛选
- `customer_data.system_data` 复用 `member_data.fetch_member_consumption_data()`
- `customer_data.notes``biz.notes` 获取该客户的全部备注
- reference 从 `ai_cache` 获取 app8 最新 + 最近 2 套历史
### 3.3 应用 5话术参考
结构与应用 4 基本一致,额外增加 `task_suggestion` 字段(应用 4 的返回结果)。
实现要点:
- 复用应用 4 的 `assistant_info` + `service_history` + `customer_data` 获取逻辑
- `task_suggestion` 从应用 4 的 `run()` 返回值传入(调用链:应用 4 → 应用 5
### 3.4 应用 6备注分析
```json
{
"current_time": "2026-03-08 14:30:25",
"current_note": {
"content": "客户说下周要带朋友来打球",
"recorded_by": "张助教",
"created_at": "2026-03-08 14:25:00"
},
"reference": {
"member_nickname": "王先生",
"consumption_data": { /* 3 main_data */ },
"all_notes": [...],
"app3_clues": [...],
"app8_history": [...]
}
}
```
实现要点:
- `current_note` 从触发上下文获取(备注提交事件传入)
- `consumption_data` 复用 `member_data.fetch_member_consumption_data()`
- `all_notes``biz.notes` 获取所有助教对该客户的全部备注
- reference 从 `ai_cache` 获取 app3 + app8 历史
### 3.5 应用 7客户分析
```json
{
"current_time": "2026-03-08 14:30:25",
"member_id": 12345,
"member_nickname": "王先生",
"objective_data": { /* 3 main_data */ },
"subjective_data": {
"notes": [
{"recorded_by": "张助教", "content": "...", "created_at": "..."}
]
},
"reference": {
"app8_current": {...},
"app8_history": [...]
}
}
```
实现要点:
- `objective_data` 复用 `member_data.fetch_member_consumption_data()`
- `subjective_data.notes``biz.notes` 获取全部备注,标注创建者
- 主观信息需在 Prompt 中标注【来源XXX请甄别信息真实性】
- reference 从 `ai_cache` 获取 app8 最新 + 最近 2 套历史
### 3.6 应用 1页面上下文文本化
应用 1 的 `_build_page_context()``_build_source_context()` 需要根据 `source_page` 类型,调用对应的数据获取函数并格式化为文本。
实现要点:
- 每个页面类型对应一个文本化函数
- 文本化输出为结构化中文描述(非 JSON便于 AI 理解
- 数据量控制:每个页面上下文不超过 2000 字符,避免 token 浪费
- 敏感信息脱敏:不传入 member_phone 等断档字段
---
## 四、数据库审查
### 4.1 数据获取涉及的表
| 数据获取函数 | 涉及表 | 连接方式 |
|-------------|--------|---------|
| `fetch_member_consumption_data` | v_dwd_settlement_head, v_dwd_table_fee_log, v_dwd_store_goods_sale, v_dwd_recharge_order, v_dim_member_card_account, v_dws_member_consumption_summary, v_dws_member_visit_detail | FDW |
| `fetch_assistant_info` | v_dim_assistant, v_dws_assistant_salary_calc | FDW |
| `fetch_service_history` | v_dwd_assistant_service_log, v_dws_member_assistant_relation_index, v_dws_member_assistant_intimacy | FDW |
| `build_page_text` | 以上全部 + biz.coach_tasks, biz.notes, biz.ai_cache, public.member_retention_clue | FDW + 业务库 |
### 4.2 无需新建表
本 SPEC 不需要新建数据库表。所有数据获取基于 NS1 已建立的 FDW 映射和 P4/P5-A 已建立的业务表。
### 4.3 性能考虑
- `fetch_member_consumption_data` 涉及 7 张 FDW 表查询,建议:
- 消费记录限制最近 3 个月WHERE settle_date >= NOW() - INTERVAL '3 months'
- 单次查询最多返回 100 条记录
- 考虑使用 NS1 中建议的 `biz.task_detail_cache` 缓存聚合结果
- 页面文本化(应用 1需要实时获取数据建议设置 5 秒查询超时
---
## 五、调用链与数据流
### 5.1 消费事件链
```
新结算单到达
→ 应用 3.build_prompt(member_data) → 百炼 API → ai_cache
→ 应用 8.run() → member_retention_clue
→ 应用 7.build_prompt(member_data + notes) → 百炼 API → ai_cache
→ 应用 4.build_prompt(assistant_data + member_data) → 百炼 API → ai_cache
→ 应用 5.build_prompt(app4_result) → 百炼 API → ai_cache
```
### 5.2 备注提交事件链
```
备注提交
→ 应用 6.build_prompt(note + member_data) → 百炼 API → ai_cache
→ 应用 8.run() → member_retention_clue
```
### 5.3 应用 1 对话流
```
用户点击 AI 入口
→ 前端传入 source_page + context_data
→ 后端 build_page_text(source_page, context_data)
→ 拼接为首条 user message
→ SSE 流式调用百炼 API
```
---
## 六、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| P5 AI 集成 spec | `docs/prd/specs/P5-miniapp-ai-integration.md` | Prompt JSON 结构定义、调用链时序 |
| API 契约 | `docs/miniprogram-dev/API-contract.md` | 接口响应格式(决定数据结构) |
| DWD-DOC 标杆 | `docs/reports/DWD-DOC/` | 金额口径、字段语义权威参考 |
| AI 应用骨架代码 | `apps/backend/app/ai/apps/app*.py` | 当前骨架实现 |
| 百炼客户端 | `apps/backend/app/ai/bailian_client.py` | API 调用封装 |
| 缓存服务 | `apps/backend/app/ai/cache_service.py` | ai_cache 读写 |
| NS1 接口设计 | `docs/prd/Neo_Specs/NS1-xcx-backend-api.md` | 后端数据结构参考 |
---
## 七、预审查清单SPEC 启动前确认)
### 7.1 数据结构
1. **消费记录字段范围**`consumption_records` 中每条记录需要包含哪些字段是否需要包含折扣信息discount_manual/discount_other是否需要包含支付方式明细balance_pay/cash_pay/online_pay
2. **服务记录字段范围**`service_history` 中每条记录需要包含哪些字段是否需要包含台桌类型room_category和客户评价
3. **备注内容截断**`all_notes` 中每条备注是否需要全文传入?长备注是否截断?截断长度?
4. **会员卡明细粒度**`member_cards` 是否需要包含卡号、开卡日期、有效期等详细信息,还是只需要卡类型和余额?
### 7.2 Prompt 优化
5. **Token 预算**:每个应用的首条 Prompt 的 token 上限是多少?百炼 API 的单次请求 token 限制?
6. **数据时间窗口**:消费记录默认取近 3 个月,是否需要可配置?不同应用是否需要不同时间窗口?
7. **空数据处理**:当客户无消费记录/无备注/无服务历史时Prompt 如何处理?是否需要特殊提示词?
### 7.3 页面文本化(应用 1
8. **文本化格式**:页面上下文是输出为结构化中文文本还是 JSONAI 对哪种格式理解更好?
9. **数据量控制**:每个页面上下文的字符上限?是否需要根据页面类型动态调整?
10. **实时性要求**:应用 1 的页面上下文是否需要实时获取最新数据?还是可以使用缓存(如 task_detail_cache
### 7.4 性能与安全
11. **FDW 查询并发**多个数据获取函数是否可以并发执行asyncio.gatherFDW 连接池是否支持?
12. **数据脱敏**:传入百炼 API 的数据中哪些字段需要脱敏member_phone 已断档不传,还有其他敏感字段吗?
13. **错误降级**:某个数据获取函数失败时(如 FDW 超时),是否跳过该部分继续生成 Prompt还是整体失败
---
## 八、任务清单草案SPEC 细化后调整)
### Batch A共享数据获取层
- [ ] T1创建 `data_fetchers/member_data.py`(客户消费数据获取,应用 3/6/7 共用)
- [ ] T2创建 `data_fetchers/assistant_data.py`(助教信息 + 服务历史获取,应用 4/5 共用)
- [ ] T3创建 `data_fetchers/page_context.py`(页面上下文文本化框架,应用 1 专用)
### Batch BPrompt 拼接实现
- [ ] T4完善 `app3_clue.py``build_prompt()`(客户消费数据 → 维客线索分析)
- [ ] T5完善 `app4_analysis.py``build_prompt()`(助教+客户数据 → 关系分析)
- [ ] T6完善 `app5_tactics.py``build_prompt()`(复用应用 4 数据 + task_suggestion
- [ ] T7完善 `app6_note.py``build_prompt()`(备注+客户数据 → 备注分析)
- [ ] T8完善 `app7_customer.py``build_prompt()`(客户全量数据 → 运营策略)
### Batch C应用 1 页面文本化 + 联调
- [ ] T9实现各页面类型的文本化函数task-detail/customer-detail/board-*/performance 等)
- [ ] T10补充 `app1_chat.py``_build_page_context()` 调用文本化函数
- [ ] T11端到端联调触发事件 → Prompt 拼接 → 百炼调用 → 缓存写入 → 前端展示)

View File

@@ -0,0 +1,305 @@
# NS3MCP Server 扩展 — mcp-server-ai-extension
> 优先级:中(可与 NS1/NS2 并行,批次 A 无前置依赖)
> 预估工作量:中等
> 前置条件:批次 A 无依赖;批次 B 依赖 P5-Abiz 表已建);批次 C 依赖批次 B
> 参考基准:`docs/prd/specs/P5.1-mcp-server-ai-extension.md`
---
## 一、背景与目标
当前 MCP Server`apps/mcp-server/server.py`)仅连接 `etl_feiqiu` 数据仓库,提供 4 个工具list_tables、describe_table、describe_schemas、query_sqlschema 白名单为 ods/dwd/dws/core/meta/app。SQL 安全通过正则禁词检测实现只读保护。
查库手册(`docs/mcp/AI-DATABASE-QUERY-MANUAL.md`内容陈旧DWS 层 34 张表缺少完整字段说明业务库zqyy_app完全未覆盖。
本 SPEC 目标:
1. MCP Server 新增 `zqyy_app` 业务库连接,支持多数据库路由
2. 实现业务库敏感字段脱敏策略
3. 重写查库手册ETL 全字段 + 业务库全字段 + 常用查询模式)
4. 手册上传百炼平台验证 AI 引用效果
### 当前 MCP Server 能力
| 工具 | 功能 | 限制 |
|------|------|------|
| `list_tables` | 列出指定 schema 下的表 | 仅 etl_feiqiu |
| `describe_table` | 查看表结构(列名、类型、注释) | 仅 etl_feiqiu |
| `describe_schemas` | 列出可用 schema 及表数量 | 仅 ods/dwd/dws/core/meta/app |
| `query_sql` | 执行只读 SQL 查询 | 正则禁词检测,单 schema 限制 |
---
## 二、技术架构
### 2.1 多数据库连接
```
apps/mcp-server/
├── server.py 🔧 扩展:多连接池 + schema 路由
├── db_pool.py 🆕 新建连接池管理etl_feiqiu + zqyy_app
├── security.py 🆕 新建:敏感字段脱敏策略
└── config.py 🆕 新建数据库配置DSN、schema 白名单)
```
### 2.2 连接池设计
```python
# db_pool.py
class DatabasePool:
"""管理两个数据库的连接池。"""
pools = {
"etl": {
"dsn_env": "PG_DSN", # etl_feiqiu / test_etl_feiqiu
"schemas": ["ods", "dwd", "dws", "core", "meta", "app"],
"readonly": True,
},
"biz": {
"dsn_env": "APP_DB_DSN", # zqyy_app / test_zqyy_app
"schemas": ["auth", "biz", "public"],
"readonly": True, # MCP 只读,写操作走后端 API
},
}
```
### 2.3 Schema 自动路由
工具参数中不再需要显式指定 `database`,而是根据 schema 名称自动路由:
| Schema | 路由目标 | 说明 |
|--------|---------|------|
| `ods` / `dwd` / `dws` / `core` / `meta` / `app` | etl_feiqiu | ETL 数据仓库 |
| `auth` / `biz` / `public` | zqyy_app | 业务库 |
路由逻辑:
```python
def resolve_database(schema: str) -> str:
ETL_SCHEMAS = {"ods", "dwd", "dws", "core", "meta", "app"}
BIZ_SCHEMAS = {"auth", "biz", "public"}
if schema in ETL_SCHEMAS:
return "etl"
elif schema in BIZ_SCHEMAS:
return "biz"
else:
raise ValueError(f"未知 schema: {schema}")
```
### 2.4 工具扩展
4 个现有工具的参数不变,内部根据 schema 参数自动选择连接池:
| 工具 | 变更 |
|------|------|
| `list_tables(schema)` | schema 参数扩展接受 auth/biz/public |
| `describe_table(table, schema)` | 同上 |
| `describe_schemas()` | 返回结果增加 auth/biz/public 三个 schema |
| `query_sql(schema, sql)` | 根据 schema 路由到对应连接池 |
### 2.5 跨库查询限制
- 单次 `query_sql` 调用只能查询单个数据库(由 schema 参数决定)
- 禁止在 SQL 中引用其他数据库的 schema现有 `_reject_cross_schema` 逻辑扩展)
- 如需跨库关联AI 应分两次查询后在应用层组合
---
## 三、敏感字段脱敏策略
### 3.1 脱敏规则
| Schema | 表 | 敏感字段 | 脱敏方式 |
|--------|-----|---------|---------|
| `auth` | `users` | `wx_openid` | 前 4 后 4 保留,中间 `***` |
| `auth` | `users` | `phone` | 前 3 后 4 保留,中间 `****` |
| `auth` | `users` | `wx_session_key` | 完全隐藏,返回 `[REDACTED]` |
| `biz` | `ai_messages` | `content`role=user | 不脱敏AI 需要理解对话内容) |
| `biz` | `ai_conversations` | — | 无敏感字段 |
| `public` | `member_retention_clue` | `recorded_by_assistant_id` | 不脱敏(非 PII |
### 3.2 实现方式
`query_sql` 返回结果时,对命中脱敏规则的列进行后处理:
```python
# security.py
MASKING_RULES = {
"auth.users": {
"wx_openid": lambda v: f"{v[:4]}***{v[-4:]}" if v and len(v) > 8 else "[REDACTED]",
"phone": lambda v: f"{v[:3]}****{v[-4:]}" if v and len(v) >= 11 else "[REDACTED]",
"wx_session_key": lambda v: "[REDACTED]",
},
}
def mask_row(schema: str, table: str, columns: list[str], row: tuple) -> tuple:
"""对查询结果行应用脱敏规则。"""
```
### 3.3 脱敏绕过防护
- `SELECT *` 查询 auth.users 时自动应用脱敏
- 禁止通过 `CAST``CONCAT`、子查询等方式绕过脱敏(在 SQL 解析阶段检测)
- 对 auth.users 的查询结果始终应用列级脱敏,无论 SQL 写法
---
## 四、查库手册重写
### 4.1 手册结构
重写后的手册分为两个文件,便于维护和上传:
```
docs/mcp/
├── AI-DATABASE-QUERY-MANUAL.md 🔧 重写(总览 + ETL 库)
└── AI-DATABASE-QUERY-MANUAL-BIZ.md 🆕 新建(业务库)
```
### 4.2 ETL 库手册内容(重写)
保留现有架构流程和工具说明,重点补充:
1. **DWS 层完整表清单**34 张表按业务域分组)
- 每张表列出全部字段(字段名、类型、中文说明)
- 标注关键字段的业务含义和使用注意事项
- 按业务域分组:会员域、助教域、财务域、配置域
2. **金额口径专章**
- `consume_money` 三种历史口径说明A/B/C明确标注"禁止直接使用"
- `items_sum` 定义和计算公式
- settle_type 枚举映射表1=台桌结账 78.6%、3=商城订单 21.4%、5=充值、7=充值退款、6=结算退款)
- 助教费用拆分规则assistant_pd_money 陪打 / assistant_cx_money 超休)
- 支付渠道恒等式balance_amount = recharge_card_amount + gift_card_amount
3. **会员字段断档说明**
- DQ-6member_phone/member_name 自 2025-12 起 NULL需 JOIN dim_member
- DQ-7member_card_type_name 自 2025-07-21 起 NULL需 JOIN dim_member_card_account
4. **常用查询模式**ETL 侧)
- 客户消费汇总查询
- 助教绩效查询
- 财务日报查询
- 指数排名查询
5. **数据量统计与性能建议**
- 各表的大致数据量级
- 推荐的 WHERE 条件时间范围、site_id
- 避免全表扫描的建议
### 4.3 业务库手册内容(新建)
1. **Schema 概览**
- `auth`用户认证users、user_applications、site_code_mapping、user_assistant_binding
- `biz`业务数据coach_tasks、notes、ai_conversations、ai_messages、ai_cache、trigger_jobs、salary_adjustments、excel_upload_log
- `public`共享数据member_retention_clue
2. **每张表完整字段说明**
- 字段名、类型、是否可空、默认值、中文说明
- 关键枚举值说明(如 coach_tasks.task_type、notes.type、ai_cache.cache_type
- 外键关系和关联查询建议
3. **常用查询模式**(业务侧)
- 维客线索查询(按客户、按来源、按标签)
- 任务系统查询(按助教、按状态、按类型)
- 备注查询(按客户、按助教、含星星评分)
- AI 缓存查询(按应用类型、按 target_id
- Excel 上传记录查询
4. **跨库关联指南**
- 业务库 member_id → ETL 库 dim_member 的关联方式
- 业务库 assistant_id → ETL 库 dim_assistant 的关联方式
- 注意MCP 不支持单次跨库 JOIN需分两次查询
### 4.4 数据字段权威参考
手册中涉及的 DWD/DWS 层字段来源、金额口径、业务逻辑,以 `docs/reports/DWD-DOC/` 校准文档为准(数据快照 2026-03-06
---
## 五、百炼平台上传与验证
### 5.1 上传方式
- 将重写后的手册作为知识库文档上传至百炼平台
- 关联到应用 1通用对话使 AI 在回答数据查询问题时可引用手册
### 5.2 验证场景
| 测试场景 | 预期行为 |
|---------|---------|
| "查询本月营业额" | AI 引用手册中的 dws_finance_daily_summary 表,使用 items_sum 口径 |
| "查看客户王先生的消费记录" | AI 引用 dim_member + dwd_settlement_head通过 member_id 关联 |
| "助教张三的本月绩效" | AI 引用 dws_assistant_salary_calc正确使用 assistant_pd_money 拆分 |
| "查看维客线索" | AI 引用业务库 member_retention_clue 表 |
| "查看用户申请列表" | AI 引用 auth.user_applications对 phone 字段脱敏 |
### 5.3 验证标准
- AI 不再频繁调用 `describe_table`(手册已提供完整字段信息)
- AI 生成的 SQL 使用正确的金额口径items_sum 而非 consume_money
- AI 查询 auth.users 时返回结果中敏感字段已脱敏
- AI 能正确区分 ETL 库和业务库的查询路由
---
## 六、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| P5.1 原始 spec | `docs/prd/specs/P5.1-mcp-server-ai-extension.md` | 需求定义基准 |
| 现有 MCP Server | `apps/mcp-server/server.py` | 当前实现参考 |
| 现有查库手册 | `docs/mcp/AI-DATABASE-QUERY-MANUAL.md` | 重写基础 |
| DWD-DOC 标杆 | `docs/reports/DWD-DOC/` | 字段语义、金额口径权威参考 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| ETL BD 手册 | `apps/etl/connectors/feiqiu/docs/database/` | DWD/DWS 表结构 |
---
## 七、预审查清单SPEC 启动前确认)
### 7.1 多数据库连接
1. **连接池大小**zqyy_app 连接池的 min/max 连接数?与 etl_feiqiu 共享还是独立MCP Server 的并发查询量预估?
2. **DSN 环境变量**MCP Server 运行环境中 `APP_DB_DSN` 是否已配置?测试环境使用 `test_zqyy_app` 还是 `zqyy_app`
3. **RLS 隔离**MCP 查询 zqyy_app 时是否需要 `SET LOCAL app.current_site_id`如果需要site_id 从哪里获取MCP 调用方传入?固定值?)?
4. **连接超时**FDW 查询和业务库查询的超时时间是否需要不同设置?
### 7.2 安全与脱敏
5. **脱敏范围确认**:除 auth.users 外,还有哪些表/字段需要脱敏biz.notes 的备注内容是否需要脱敏?
6. **脱敏粒度**:是否需要根据 MCP 调用方身份区分脱敏级别如管理员可看完整数据AI 应用只看脱敏数据)
7. **SQL 注入防护**:现有正则禁词检测是否足够?是否需要升级为 SQL 解析器(如 sqlparse
8. **审计日志**MCP 查询是否需要记录审计日志(谁查了什么表、什么时间)?
### 7.3 查库手册
9. **手册格式**百炼平台对知识库文档的格式要求Markdown 是否直接支持?是否需要转换为 PDF/TXT
10. **手册拆分**ETL 库和业务库是否拆分为两个知识库文档?还是合并为一个?
11. **手册更新机制**:表结构变更后,手册如何同步更新?是否需要自动化脚本从数据库 DDL 生成手册?
12. **DWD 层覆盖**:手册是否需要覆盖 DWD 层全部表的完整字段?还是只覆盖 DWS + 常用 DWD 表?
### 7.4 部署与运维
13. **MCP Server 重启**新增连接池后MCP Server 是否需要重启?是否支持热加载配置?
14. **监控指标**:是否需要监控 MCP 查询的响应时间、错误率、查询频次?
15. **降级策略**zqyy_app 连接失败时,是否影响 etl_feiqiu 的查询?两个连接池是否需要独立故障隔离?
---
## 八、任务清单草案SPEC 细化后调整)
### 批次 A查库手册 — ETL 库(无前置依赖)
- [ ] T1重写 `AI-DATABASE-QUERY-MANUAL.md` — DWS 层 34 张表全字段说明(按业务域分组)
- [ ] T2补充金额口径专章consume_money 禁用说明 + items_sum 定义 + settle_type 映射)
- [ ] T3补充会员字段断档说明DQ-6/DQ-7+ 常用 ETL 查询模式
### 批次 BMCP Server 扩展 + 业务库手册(依赖 P5-A
- [ ] T4创建 `db_pool.py` — 双连接池管理etl_feiqiu + zqyy_app
- [ ] T5扩展 `server.py` — 4 个工具支持 schema 自动路由
- [ ] T6创建 `security.py` — 敏感字段脱敏策略实现
- [ ] T7创建 `AI-DATABASE-QUERY-MANUAL-BIZ.md` — 业务库全字段说明 + 常用查询模式
### 批次 C百炼平台验证
- [ ] T8手册上传百炼平台并关联 AI 应用
- [ ] T9执行验证场景测试5 个场景),确认 AI 引用效果

View File

@@ -0,0 +1,532 @@
# NS4租户管理后台 — tenant-admin-web
> 优先级:中(可与 NS1/NS2 并行,依赖 P1+P3
> 预估工作量:大
> 前置条件P1数据库基础、P3用户认证体系、admin-web-console 需求 11租户管理员账号管理
> 参考基准:`docs/prd/specs/P10-tenant-admin-web.md`
---
## 一、背景与目标
当前系统缺少面向租户管理员的独立管理界面。用户审核、Excel 数据上传、维客线索管理等运营操作无法自助完成,依赖开发人员手动操作数据库。
本 SPEC 目标:构建独立的租户管理 Web 应用,提供:
1. 用户审核与管理(申请审核、身份编辑、店铺归属、助教/员工绑定)
2. Excel 数据上传4 种模板:财务支出/团购收入/助教奖罚/充值业绩归属)
3. 维客线索管理(查看、修改、删除、隐藏)
### 与现有系统的关系
| 系统 | 用途 | 用户 |
|------|------|------|
| `apps/admin-web/`(系统管理后台) | 平台级管理Operator 操作) | 系统管理员 |
| `apps/tenant-admin/`(本 SPEC | 租户级管理 | 租户管理员 |
| `apps/miniprogram/`(小程序) | C 端业务 | 助教/管理者 |
租户管理员账号由系统管理后台(`apps/admin-web/`)的 Operator 创建,租户管理员不可自行注册。
---
## 二、技术架构
### 2.1 前端
- 独立 Web 应用React + Vite + Ant Design`apps/admin-web/` 同技术栈)
- 部署路径:`apps/tenant-admin/`
- 独立登录入口,与系统管理后台完全隔离
```
apps/tenant-admin/
├── src/
│ ├── pages/
│ │ ├── Login/ # 登录页
│ │ ├── UserApproval/ # 用户审核
│ │ ├── UserManagement/ # 用户管理
│ │ ├── ExcelUpload/ # Excel 上传4 种模板)
│ │ └── RetentionClues/ # 维客线索管理
│ ├── components/
│ │ ├── DiffTable/ # 冲突 diff 交互组件
│ │ └── ClueEditor/ # 线索编辑组件
│ ├── services/
│ │ └── api.ts # API 调用封装
│ ├── hooks/
│ ├── utils/
│ └── App.tsx
├── package.json
├── vite.config.ts
└── tsconfig.json
```
### 2.2 后端
复用 `apps/backend/` 的 FastAPI新增租户管理路由模块
```
apps/backend/app/routers/
├── tenant_auth.py 🆕 租户管理员登录/鉴权
├── tenant_users.py 🆕 用户审核 + 用户管理
├── tenant_excel.py 🆕 Excel 上传/校验/冲突处理
└── tenant_clues.py 🆕 维客线索管理
```
### 2.3 认证体系
- 独立凭据:用户名 + 密码(非微信登录)
- JWT 签发:与小程序 JWT 独立(不同 issuer 或 audience
- 账号创建:由系统管理后台 Operator 创建,指定用户名、初始密码、所属租户、管辖 site_id 列表
- 权限级别:
- 租户级管理员:管辖该租户下所有店铺
- 店铺级管理员:只能管理 Operator 分配的 site_id 列表内的店铺
### 2.4 数据隔离
- 所有查询附加 `site_id IN (管辖列表)` 条件
- FDW 查询需 `SET LOCAL app.current_site_id`(单店铺场景)
- 多店铺场景下,逐 site_id 查询后合并结果
---
## 三、功能详细设计
### 3.1 用户审核页面
#### 页面功能
- 申请列表:展示所有待审核/已审核的用户申请
- 状态筛选:全部 / 待审核(pending) / 已通过(approved) / 已拒绝(rejected)
- 关联建议:根据申请中的球房 ID + 手机号,同时在助教表和员工信息表中匹配
- 审核操作:通过(分配身份+关联助教/员工)/ 拒绝(填写原因)
#### 关联匹配逻辑
```
用户申请球房ID + 手机号)
→ site_code_mapping 查 site_id
→ 并行匹配:
├── fdw_etl.v_dim_assistantphone 匹配scd2_is_current=1
└── fdw_etl.v_dim_staff + v_dim_staff_exphone 匹配)
→ 返回匹配建议列表(可能多条)
→ 管理员选择关联目标
```
#### 审核通过后操作
1. 更新 `auth.users.status = 'approved'`
2. 分配角色(助教/管理者)→ 写入 `auth.user_roles`
3. 关联助教 → 写入 `auth.user_assistant_binding`(含 staff_id
4. 分配 site_id → 更新 `auth.users.site_id`
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 申请列表 | GET | `/api/tenant/applications` | 支持 status 筛选、分页 |
| 关联建议 | GET | `/api/tenant/applications/{id}/match-suggestions` | 返回助教+员工匹配结果 |
| 审核通过 | POST | `/api/tenant/applications/{id}/approve` | body: role, assistant_id, staff_id |
| 审核拒绝 | POST | `/api/tenant/applications/{id}/reject` | body: reason |
### 3.2 用户管理页面
#### 页面功能
- 用户列表:展示已通过审核的用户(姓名、角色、关联助教、店铺、状态)
- 身份编辑:修改角色(助教↔管理者)
- 店铺归属:修改用户的 site_id
- 关联助教/员工:修改绑定关系
- 禁用/启用:冻结用户账号
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 用户列表 | GET | `/api/tenant/users` | 支持角色筛选、搜索、分页 |
| 编辑用户 | PATCH | `/api/tenant/users/{id}` | body: role, site_id, status |
| 修改绑定 | PUT | `/api/tenant/users/{id}/binding` | body: assistant_id, staff_id |
### 3.3 Excel 上传
#### 4 种模板
##### 模板 1财务支出按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验,不超过当前月 |
| 支出类别 | 文本 | 是 | 枚举:房租/水电/物业/食品饮料进货/耗材/报销/固定人员工资/其他费用 |
| 金额 | 数值(2) | 是 | > 0精度 2 位小数 |
| 备注 | 文本 | 否 | 最长 500 字符 |
主键:月份 + 支出类别
写入目标:`dws.dws_finance_expense_summary`(通过后端 API 写入 ETL 库,或写入业务库 staging 表后由 ETL 同步)
##### 模板 2团购平台收入按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验 |
| 平台名称 | 文本 | 是 | 非空 |
| 收入金额 | 数值(2) | 是 | > 0 |
| 备注 | 文本 | 否 | 最长 500 字符 |
主键:月份 + 平台名称
写入目标:`dws.dws_platform_settlement`(或业务库 staging 表)
##### 模板 3助教奖罚按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 月份 | YYYY-MM | 是 | 格式校验 |
| 助教姓名 | 文本 | 是 | 非空 |
| 助教编号 | 文本 | 是 | 非空 |
| 类型 | 文本 | 是 | 枚举:扣款/奖金 |
| 金额 | 数值(2) | 是 | > 0 |
| 原因 | 文本 | 是 | 非空,最长 200 字符 |
主键:月份 + 助教姓名 + 助教编号 + 类型 + 原因(同一助教同月可多笔)
写入目标:`biz.salary_adjustments`
##### 模板 4充值业绩归属按月
| 列名 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 充值日期 | YYYY-MM-DD | 是 | 格式校验 |
| 会员名称 | 文本 | 是 | 非空 |
| 充值金额 | 数值(2) | 是 | > 0 |
| 归属助教 | 文本 | 是 | 非空 |
| 奖励金额 | 数值(2) | 是 | ≥ 0 |
主键:充值日期 + 会员名称 + 归属助教
写入目标:`dws.dws_assistant_recharge_commission`(或业务库 staging 表)
#### 人员匹配校验(模板 3/4
上传助教奖罚和充值业绩归属时,需校验助教姓名+编号是否存在:
```
助教姓名 + 助教编号
→ fdw_etl.v_dim_assistantnickname + assistant_number 匹配scd2_is_current=1
→ 如不匹配,尝试 fdw_etl.v_dim_staff + v_dim_staff_exname + staff_number 匹配)
→ 匹配失败:标记为校验警告(不阻断上传,但提示管理员确认)
```
#### 冲突处理流程
```
上传 Excel
→ 后端解析 + 格式校验
→ 返回校验结果:
├── 格式错误行 → 前端标红,要求修正后重新上传
├── 无冲突行 → 标记为"待写入"
└── 冲突行(主键已存在)→ 返回 diff 数据(旧值 vs 新值)
→ 前端展示 diff 交互表格:
- 每行显示:字段名 | 旧值 | 新值 | 操作(替换/保留)
- 支持"全部替换"/"全部保留"快捷操作
→ 用户确认后提交
→ 后端按选择写入
```
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 上传解析 | POST | `/api/tenant/excel/upload` | multipart/form-data返回校验结果+冲突列表 |
| 确认写入 | POST | `/api/tenant/excel/confirm` | body: upload_id, resolutions[] |
| 上传记录 | GET | `/api/tenant/excel/logs` | 历史上传记录列表 |
| 模板下载 | GET | `/api/tenant/excel/template/{type}` | 下载空白模板 |
### 3.4 维客线索管理
#### 页面功能
- 客户搜索:按客户姓名/手机号搜索(姓名从 dim_member.nickname手机从 dim_member.mobile
- 门店筛选:按管辖 site_id 筛选
- 线索列表:展示该客户的全部维客线索
- 标签(大类枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈)
- 摘要(含 Emoji 前缀)
- 详情
- 提供人recorded_by_name
- 来源manual / ai_consumption / ai_note
- 记录时间
- 隐藏状态
- 操作:
- 修改:编辑标签、摘要、详情
- 删除:二次确认后物理删除
- 隐藏/显示:切换 `is_hidden` 状态
#### 数据源
- `zqyy_app.public.member_retention_clue`(线索数据)
- `fdw_etl.v_dim_member`客户信息nickname、mobile通过 member_id 关联)
> ⚠️ 会员字段断档DQ-6客户姓名/手机必须从 dim_member 获取,不可使用结算单上的冗余字段
#### 接口设计
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 客户搜索 | GET | `/api/tenant/customers/search` | query: keyword, site_id |
| 线索列表 | GET | `/api/tenant/customers/{member_id}/clues` | 该客户全部线索 |
| 修改线索 | PATCH | `/api/tenant/clues/{id}` | body: category, summary, detail |
| 删除线索 | DELETE | `/api/tenant/clues/{id}` | 二次确认后物理删除 |
| 隐藏/显示 | PATCH | `/api/tenant/clues/{id}/visibility` | body: is_hidden |
---
## 四、数据库审查与新增表
### 4.1 现有表满足度
| 功能 | 现有表 | 是否满足 | 缺口 |
|------|--------|---------|------|
| 用户审核 | auth.users, auth.user_applications | ✅ 满足 | 无 |
| 用户管理 | auth.users, auth.user_roles, auth.user_assistant_binding | ✅ 满足 | 无 |
| 维客线索 | public.member_retention_clue | ⚠️ 部分 | 缺 is_hidden 字段 |
| 助教奖罚 | — | ❌ 不满足 | 需新建 biz.salary_adjustments |
| 上传记录 | — | ❌ 不满足 | 需新建 biz.excel_upload_log |
| 财务支出/团购收入/充值归属 | DWS 表 | ⚠️ 待定 | 可能需要 staging 表 |
### 4.2 需新建的表
#### 表 1`biz.salary_adjustments`(助教奖罚明细)
```sql
CREATE TABLE biz.salary_adjustments (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
assistant_id BIGINT, -- 匹配到的助教 ID可空匹配失败时为 NULL
assistant_name VARCHAR(100) NOT NULL,
assistant_number VARCHAR(50) NOT NULL,
salary_month VARCHAR(7) NOT NULL, -- YYYY-MM
adjustment_type VARCHAR(20) NOT NULL CHECK (adjustment_type IN ('deduction', 'bonus')),
amount NUMERIC(12,2) NOT NULL CHECK (amount > 0),
reason VARCHAR(200) NOT NULL,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by BIGINT -- 上传人(租户管理员 ID
);
CREATE INDEX idx_salary_adj_site_month ON biz.salary_adjustments(site_id, salary_month);
CREATE INDEX idx_salary_adj_assistant ON biz.salary_adjustments(assistant_id, salary_month);
```
#### 表 2`biz.excel_upload_log`Excel 上传记录)
```sql
CREATE TABLE biz.excel_upload_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
upload_type VARCHAR(30) NOT NULL CHECK (upload_type IN (
'expense', 'platform_income', 'salary_adj', 'recharge_commission'
)),
file_name VARCHAR(255) NOT NULL,
uploaded_by BIGINT NOT NULL, -- 租户管理员 ID
row_count INTEGER NOT NULL DEFAULT 0,
conflict_count INTEGER NOT NULL DEFAULT 0,
resolved_count INTEGER NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN (
'pending', 'confirmed', 'failed'
)),
error_detail JSONB, -- 校验错误详情
created_at TIMESTAMPTZ DEFAULT NOW(),
confirmed_at TIMESTAMPTZ
);
CREATE INDEX idx_excel_log_site ON biz.excel_upload_log(site_id, created_at DESC);
```
### 4.3 需变更的表
#### `public.member_retention_clue` — 新增字段
```sql
ALTER TABLE public.member_retention_clue
ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN public.member_retention_clue.is_hidden
IS '是否隐藏true=管理后台保留但小程序不展示)';
```
> 小程序端查询线索时需增加 `WHERE is_hidden = false` 条件
#### `public.member_retention_clue` — 确认 source 字段
P10 spec 中提到需新增 `source` 字段,需确认该字段是否已在 P4/P5 阶段建立。如未建立:
```sql
ALTER TABLE public.member_retention_clue
ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'manual'
CHECK (source IN ('manual', 'ai_consumption', 'ai_note'));
COMMENT ON COLUMN public.member_retention_clue.source
IS '线索来源manual=人工录入, ai_consumption=应用3消费分析, ai_note=应用6备注分析';
```
### 4.4 Excel 写入目标表的 staging 策略
财务支出、团购收入、充值业绩归属三种模板的数据最终需要进入 DWS 层。有两种策略:
**方案 A直接写入 DWS 表**(通过后端 API 直连 ETL 库写入)
- 优点:数据即时可用
- 缺点:绕过 ETL 流程,数据一致性风险
**方案 B写入业务库 staging 表ETL 定时同步**
- 优点:数据经过 ETL 标准流程,一致性有保障
- 缺点:数据有延迟(取决于 ETL 调度频率)
建议采用方案 B需新建 3 张 staging 表:
```sql
-- biz.stg_finance_expense财务支出 staging
CREATE TABLE biz.stg_finance_expense (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
expense_month VARCHAR(7) NOT NULL,
category VARCHAR(50) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
remark TEXT,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ, -- ETL 同步时间NULL=未同步)
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- biz.stg_platform_income团购收入 staging
CREATE TABLE biz.stg_platform_income (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
income_month VARCHAR(7) NOT NULL,
platform_name VARCHAR(100) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
remark TEXT,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- biz.stg_recharge_commission充值业绩归属 staging
CREATE TABLE biz.stg_recharge_commission (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
recharge_date DATE NOT NULL,
member_name VARCHAR(100) NOT NULL,
recharge_amount NUMERIC(12,2) NOT NULL,
assigned_assistant VARCHAR(100) NOT NULL,
reward_amount NUMERIC(12,2) NOT NULL,
upload_batch_id BIGINT REFERENCES biz.excel_upload_log(id),
synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
---
## 五、租户管理员账号体系
### 5.1 账号存储
租户管理员账号存储在 `auth.tenant_admins` 表(需新建):
```sql
CREATE TABLE auth.tenant_admins (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
tenant_id BIGINT NOT NULL, -- 所属租户
managed_site_ids BIGINT[] NOT NULL, -- 管辖的 site_id 列表
is_active BOOLEAN NOT NULL DEFAULT true,
created_by BIGINT, -- 创建该账号的 Operator ID
created_at TIMESTAMPTZ DEFAULT NOW(),
last_login_at TIMESTAMPTZ
);
CREATE INDEX idx_tenant_admin_tenant ON auth.tenant_admins(tenant_id);
```
### 5.2 与小程序用户体系的隔离
- 租户管理员使用 `auth.tenant_admins` 表,小程序用户使用 `auth.users`
- JWT 签发时使用不同的 `aud`audience字段区分
- 后端路由通过不同的认证依赖注入区分(`require_tenant_admin()` vs `require_approved()`
---
## 六、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| P10 原始 spec | `docs/prd/specs/P10-tenant-admin-web.md` | 需求定义基准 |
| admin-web 现有代码 | `apps/admin-web/` | 技术栈参考React + Vite + Ant Design |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |
| 权限矩阵 | `docs/permission_matrix/` | 角色-权限映射参考 |
| DWD-DOC 标杆 | `docs/reports/DWD-DOC/` | 金额口径权威参考 |
| 数据依赖矩阵 | `docs/prd/specs/00-数据依赖矩阵.md` | 租户管理后台数据源映射 |
| member_retention_clue DDL | `db/zqyy_app/` | 维客线索表结构 |
---
## 七、预审查清单SPEC 启动前确认)
### 7.1 账号与认证
1. **租户管理员账号模型**:是否需要独立的 `auth.tenant_admins` 表?还是复用 `auth.users` 表增加 `user_type` 字段区分?
2. **密码策略**:初始密码是否需要强制修改?密码复杂度要求?是否需要密码过期机制?
3. **多租户隔离**:一个管理员是否可以管辖多个租户?还是严格一对一?
4. **会话管理**JWT 过期时间?是否需要 refresh token是否支持多设备同时登录
### 7.2 用户审核
5. **关联匹配优先级**:助教表和员工信息表同时匹配到时,优先展示哪个?是否需要合并展示?
6. **审核拒绝后**:用户是否可以重新申请?重新申请是新建记录还是更新原记录?
7. **批量审核**:是否需要支持批量通过/拒绝?
8. **审核通知**:审核结果是否需要通知用户(小程序消息/微信模板消息)?
### 7.3 Excel 上传
9. **写入策略**:财务支出/团购收入/充值归属是直接写入 DWS 表(方案 A还是写入 staging 表由 ETL 同步(方案 B
10. **文件大小限制**:单次上传的 Excel 文件大小上限?行数上限?
11. **历史数据**:是否允许上传历史月份的数据?是否有时间范围限制?
12. **模板版本**Excel 模板是否需要版本管理?表头变更时如何兼容旧模板?
13. **助教匹配失败处理**:模板 3/4 中助教姓名+编号匹配失败时,是阻断上传还是允许上传但标记警告?
### 7.4 维客线索
14. **隐藏 vs 删除**:隐藏的线索是否可以恢复显示?删除是否需要软删除(保留记录但标记删除)?
15. **AI 线索保护**AI 生成的线索source=ai_consumption/ai_note是否允许管理员修改/删除?修改后 source 是否变更为 manual
16. **线索审计**:线索的修改/删除操作是否需要记录操作日志(谁在什么时间做了什么操作)?
17. **批量操作**:是否需要支持批量隐藏/删除线索?
### 7.5 部署与运维
18. **部署方式**:租户管理后台是独立部署还是与系统管理后台共享服务器?域名/路径如何规划?
19. **前端构建**:是否与 admin-web 共享 pnpm workspace还是完全独立的 package.json
20. **监控**:是否需要独立的访问日志和错误监控?
---
## 八、任务清单草案SPEC 细化后调整)
### Batch A基础设施
- [ ] T1创建 `apps/tenant-admin/` 项目骨架React + Vite + Ant Design
- [ ] T2创建 `auth.tenant_admins` 表 + DDL 迁移脚本
- [ ] T3实现租户管理员登录 API`tenant_auth.py`:登录/JWT 签发/鉴权中间件)
- [ ] T4创建 `biz.salary_adjustments` + `biz.excel_upload_log` 表 + DDL 迁移脚本
### Batch B用户审核与管理
- [ ] T5实现用户审核后端 API申请列表/关联建议/审核通过/审核拒绝)
- [ ] T6实现用户审核前端页面申请列表 + 状态筛选 + 关联建议展示 + 审核操作)
- [ ] T7实现用户管理后端 API用户列表/编辑/绑定修改)
- [ ] T8实现用户管理前端页面用户列表 + 身份编辑 + 店铺归属)
### Batch CExcel 上传
- [ ] T9实现 Excel 解析+校验后端4 种模板的格式校验 + 人员匹配校验)
- [ ] T10实现冲突检测后端主键匹配 + diff 数据生成)
- [ ] T11实现 Excel 上传前端(模板下载 + 上传 + 校验结果展示 + diff 交互 + 确认)
- [ ] T12创建 staging 表(如采用方案 B+ 写入逻辑
### Batch D维客线索管理
- [ ] T13`member_retention_clue` 新增 `is_hidden` 字段 + DDL 迁移
- [ ] T14实现维客线索后端 API客户搜索/线索列表/修改/删除/隐藏)
- [ ] T15实现维客线索前端页面客户搜索 + 线索列表 + 编辑/删除/隐藏操作)
- [ ] T16小程序端线索查询增加 `WHERE is_hidden = false` 条件

View File

@@ -0,0 +1,300 @@
# RNS1 拆分计划 — NS1 小程序后端 API 补全
> 创建时间2026-03-18
> 来源NS1 原始 spec + 两份 Storyboard 走查报告(助教视角 51 Gap + 管理层视角 31 Gap
> 拆分原则:每个子 spec 控制在 5-8 个任务、可独立交付验证、依赖关系清晰
---
## 拆分总览
| 子 Spec | 名称 | 任务数 | 依赖 | 交付物 |
|---------|------|:------:|------|--------|
| RNS1.0 | 基础设施与契约重写 | 6 | 无 | 全局中间件 + 重写后的 API 契约 + 前端跨页面参数修复 |
| RNS1.1 | 任务与绩效接口 | 6 | RNS1.0 | TASK-1 扩展 + TASK-2 + PERF-1 + PERF-2 + 前端适配 |
| RNS1.2 | 客户与助教接口 | 6 | RNS1.0 | CUST-1 + CUST-2 + COACH-1 + 前端适配 |
| RNS1.3 | 三看板接口 | 7 | RNS1.0 | BOARD-1 + BOARD-2 + BOARD-3 + CONFIG-1 + 前端筛选修复 |
| RNS1.4 | CHAT 对齐与联调收尾 | 5 | RNS1.1-1.3 | CHAT 迁移 + FDW 验证 + 前后端联调 |
总计30 个任务5 个子 spec。
---
## RNS1.0:基础设施与契约重写
> 阻塞所有后续子 spec必须最先完成。
> 预估2-3 天
### 目标
1. 后端全局响应包装 + camelCase 转换R2 决策)
2. API 契约按走查报告发现完全重写BOARD-1/2/3、CUST-1、COACH-1、PERF-1、TASK-1 performance
3. 前端跨页面参数传递统一用唯一 ID
### 任务清单
| # | 任务 | 说明 | 产出 |
|---|------|------|------|
| T0-1 | 全局响应包装中间件 | `{ code: 0, data }` + 异常处理器 `{ code, message }` | `apps/backend/app/middleware/` |
| T0-2 | Pydantic schema 统一 camelCase | `alias_generator=to_camel`,所有现有 + 新增 schema | `apps/backend/app/schemas/` |
| T0-3 | 后端 TASK-4 路径 `/cancel-abandon``/restore` | 对齐契约R1-3 | `xcx_tasks.py` |
| T0-4 | 前端 `request()``.data` 解包 | 适配全局包装 | `api.ts` |
| T0-5 | **API 契约完全重写** | 按走查报告重写全部接口响应定义(详见下方) | `API-contract.md` |
| T0-6 | **前端跨页面参数修复** | 统一用唯一 ID 传参(详见下方) | 多个页面 `.ts` |
#### T0-5 契约重写范围
需要完全重写响应定义的接口:
| 接口 | 问题 | 重写要点 |
|------|------|---------|
| BOARD-3 | 扁平 metrics → 6 板块嵌套 | overview/recharge/revenue/cashflow/expense/coachAnalysis 完整字段定义,赠送卡 3×4 矩阵,收入结构表 9 行含子行,助教分析按等级分行,现金流出 4 子分组环比格式xxxCompare + isDown/isFlat |
| BOARD-1 | 10 通用字段 → 20 字段含 4 维度 | skills 改为 `Array<{text,cls}>`,补充 topCustomers4 维度专属字段perf/salary/sv/tasksort 参数改为枚举 |
| BOARD-2 | 8 通用字段 → 40+ 维度专属字段 | 8 维度各自字段集,补充 assistants 关联助教列表weeklyVisits 柱状图coachDetails 明细表potentialTags |
| CUST-1 | 缺少 5 大模块 | 补充 balance/consumption60d/idealInterval/daysSinceVisitaiInsightcoachTasksfavoriteCoaches消费记录改为嵌套结构含 coaches 子数组),备注列表 |
| COACH-1 | 缺少 6 大模块 | 补充 performance6 指标income本月/上月各 4 项tierNodeshistoryMonthstopCustomers 扩展heartEmoji/score/balance备注列表 |
| PERF-1 | 扁平数组 → DateGroup | this_month_records 改为按日期分组结构补充收入档位currentTier/nextTier/upgradeHoursNeeded/upgradeBonuslastMonthIncomeincomeItems.desc |
| TASK-1 perf | 4 字段 → 15+ 字段 | 补充 tierNodes/basicHours/bonusHours/currentTier/nextTierHours/tierCompleted/bonusMoney/incomeTrend/incomeTrendDir/prevMonth |
| CHAT-1/2 | 字段名不一致 + 缺失 | timestamp→created_at 统一,补充 titlereferenceCard 结构SSE 端点定义customerId→chatId 映射 |
#### T0-6 前端跨页面参数修复范围
用户决策:统一用唯一 ID 传参。
| 来源页面 | 目标页面 | 当前问题 | 修复方案 |
|---------|---------|---------|---------|
| task-detail | chat | 传 `detail.id`taskId当 customerId | TASK-2 响应增加 `customer_id`,跳转改为 `?customerId={detail.customerId}` |
| task-detail | customer-service-records | 同上 | 同上 |
| customer-detail | customer-service-records | 未传 customerId | 跳转加 `?customerId={detail.id}` |
| customer-detail | chat | 未传 customerId | 跳转加 `?customerId={detail.id}` |
| coach-detail | customer-detail | 任务项用 name 跳转 | 改为 `?id={customerId}`(需 TaskItem 含 customerId |
| performance | task-detail | 传 customerName | 改为 `?id={taskId}`(需记录含 taskId |
| customer-detail | — | loadDetail() 用 `__route__` 解析 id | 改为从 `onLoad(options)` 获取 |
| chat | API | 传 customerId 但 API 需 chatId | 后端支持 `?customerId=` 查询参数自动查找/创建对话 |
---
## RNS1.1:任务与绩效接口
> 助教日常使用频率最高的 4 个接口。
> 依赖RNS1.0(全局中间件 + 契约)
> 预估3-4 天
### 目标
实现任务列表扩展、任务详情、绩效概览、绩效明细 4 个接口,覆盖助教视角的核心工作流。
### 任务清单
| # | 任务 | 说明 | 涉及 Gap |
|---|------|------|---------|
| T1-1 | 扩展 TASK-1任务列表 + 绩效概览) | performance 从 4 字段扩展到 15+ 字段tierNodes/basicHours/bonusHours/currentTier/nextTierHours/tierCompleted/bonusMoney/incomeTrend/incomeTrendDir/prevMonthenrichTask 补充 lastVisitDays/balance/aiSuggestion | GAP-02, GAP-03 |
| T1-2 | 实现 TASK-2任务详情完整版 | 服务记录/备注各 20 条懒加载;响应含 `customer_id`;维客线索统一 tag 格式和 source 枚举aiAnalysis 明确 cache_type 映射 | GAP-06~11 |
| T1-3 | 实现 PERF-1绩效概览 | this_month_records 按 DateGroup 分组收入档位数据currentTier/nextTier/upgradeHoursNeeded/upgradeBonuslastMonthIncomeincomeItems 含 desc | GAP-12~16 |
| T1-4 | 实现 PERF-2绩效明细 | 20 条懒加载按日期分组courseTypeClass 枚举统一(`basic`/`vip`/`tip`,不带 `tag-` 前缀) | GAP-19~20 |
| T1-5 | 实现 pin/unpin API 端点 | `POST /tasks/{id}/pin` + `POST /tasks/{id}/unpin`;前端 api.ts 补充对应函数 | GAP-04 |
| T1-6 | 前端适配(任务+绩效页面) | createNote 补充 score 参数performance 页添加月份切换F8performance-records 月份切换重置分页F9avatarChar/avatarColor 前端自行计算GAP-19 决策) | GAP-05, F8, F9 |
### 数据源
- `dws_assistant_salary_calc` via FDW → 绩效/档位/收入
- `dwd_assistant_service_log` via FDW → 服务记录
- `biz.coach_tasks` → 任务列表
- `biz.ai_cache` → AI 分析/话术
- `public.member_retention_clue` → 维客线索
### 验收标准
- 助教登录后可看到真实任务列表 + 绩效卡片
- 点击任务可查看完整详情服务记录、维客线索、AI 分析、备注)
- 绩效概览展示真实档位进度和收入明细
- 绩效明细按日期分组展示,支持月份切换
---
## RNS1.2:客户与助教接口
> 详情页是 Gap 最集中的区域,数据结构最复杂。
> 依赖RNS1.0(全局中间件 + 契约)
> 预估3-4 天
### 目标
实现客户详情、客户服务记录、助教详情 3 个接口,覆盖两个角色视角的详情查看需求。
### 任务清单
| # | 任务 | 说明 | 涉及 Gap |
|---|------|------|---------|
| T2-1 | 实现 CUST-1客户详情 | 含 balance/consumption60d/idealInterval/daysSinceVisitBanneraiInsightbiz.ai_cache app4维客线索消费记录嵌套结构含 coaches 子数组、tableFee/foodAmount 分项);备注列表 | GAP-23~30 |
| T2-2 | 实现 CUST-1 coachTasks 模块 | 关联助教任务列表:从 biz.coach_tasks + FDW 聚合,每位助教含任务类型/状态/最后服务时间/近60天服务次数/总时长/次均时长 | GAP-25 |
| T2-3 | 实现 CUST-1 favoriteCoaches 模块 | 最亲密助教:从 `v_dws_member_assistant_relation_index` 获取关系指数,聚合基础课时/激励课时/上课次数/充值金额 | GAP-26 |
| T2-4 | 实现 CUST-2客户服务记录 | 按月查询(改掉前端全量加载本地过滤);补充 recordType/isEstimate月度统计汇总monthCount/monthHourscustomerPhoneFulltotalServiceCount | GAP-32~35 |
| T2-5 | 实现 COACH-1助教详情 | performance6 指标income本月/上月各 4 项tierNodestopCustomers 扩展heartEmoji/score/balance/consume近期服务明细含 perfHours任务分组active/inactive/abandoned 含 notes 子数组和 reason备注列表 | GAP-38~44 |
| T2-6 | 实现 COACH-1 historyMonths 模块 | 历史月份统计(最近 5+ 个月):客户数/工时/工资/回访完成数/召回完成数/是否预估 | GAP-41 |
### 数据源
- `fdw_etl.v_dim_member` → 客户基本信息DQ-6member_phone 不可靠,用 dim_member.mobile
- `fdw_etl.v_dws_member_consumption_summary` → 消费汇总/余额
- `fdw_etl.v_dws_member_assistant_relation_index` → 关系指数
- `fdw_etl.v_dwd_assistant_service_log` → 服务记录明细
- `fdw_etl.v_dwd_table_fee_log` → 台费明细
- `fdw_etl.v_dim_assistant` → 助教基本信息
- `fdw_etl.v_dws_assistant_salary_calc` → 助教绩效/收入
- `biz.coach_tasks` → 任务关联
- `biz.ai_cache` → AI 洞察
- `public.member_retention_clue` → 维客线索
### 关键约束
- 金额口径:统一使用 `items_sum`,禁用 `consume_money`DWD-DOC 强制规则 1
- 助教费用拆分:`assistant_pd_money`(陪打)+ `assistant_cx_money`(超休),禁用 `service_fee`DWD-DOC 强制规则 2
- 会员信息:通过 `member_id` JOIN `dim_member`,禁用 `settlement_head.member_phone`DQ-6
### 验收标准
- 客户详情页展示真实余额、消费、AI 洞察、关联助教、最亲密助教、消费记录(含助教明细)、备注
- 客户服务记录支持按月切换,展示真实数据
- 助教详情页展示真实绩效、收入明细、档位进度、TOP20 客户、历史月份统计、任务分组
---
## RNS1.3:三看板接口
> 看板是管理层视角的核心BOARD-3 财务看板是全项目最复杂的单个接口。
> 依赖RNS1.0(全局中间件 + 契约)
> 预估4-5 天
### 目标
实现 3 个看板接口 + 技能配置接口,修复前端看板筛选 Bug。
### 任务清单
| # | 任务 | 说明 | 涉及 Gap |
|---|------|------|---------|
| T3-1 | 实现 BOARD-1助教看板 | 4 维度专属字段perf/salary/sv/taskskills 为 `Array<{text,cls}>`topCustomerssort 枚举参数time 参数日期范围计算 | G2, 八¾ B1 |
| T3-2 | 实现 BOARD-2客户看板 | 8 维度各自字段集assistants 关联助教列表weeklyVisits 柱状图freq60coachDetails 明细表loyalpotentialTagspotential20 条懒加载 | G3, 八¾ B2 |
| T3-3 | 实现 BOARD-3 经营一览 + 预收资产 | overview8 指标 + 8 环比recharge储值卡 5 指标 + 赠送卡 3×4 矩阵 = 24 字段 + 24 环比area≠all 时预收资产隐藏 | G1, G6, G10 |
| T3-4 | 实现 BOARD-3 应计收入 + 现金流 | revenue收入结构表 9 行含子行 + 正价/优惠/渠道明细cashflow消费收款 + 充值收款 + 合计) | G7 |
| T3-5 | 实现 BOARD-3 现金流出 + 助教分析 | expense4 子分组:经营/固定/助教分成/平台服务费coachAnalysis基础课 + 激励课各按 4 等级分行) | G8, G9 |
| T3-6 | 实现 CONFIG-1技能类型列表 | 从 ETL cfg 表读取,前端 api.ts 硬编码作为 mock 回退 | R8-1 |
| T3-7 | 前端看板筛选修复 | F1-F6筛选变更触发重新请求BOARD-2 补充分页参数BOARD-3 fetchBoardFinance 签名扩展为 `{time,area,compare}` | F1~F6 |
### BOARD-3 环比计算说明
- 所有环比为月环比(与上一个相同时间周期对比)
- 后端根据 `time` 参数计算当期和上期日期范围,分别查询后计算百分比
- 返回格式:每个值旁有 `xxxCompare: string`(如 `"+12.5%"`+ `isDown: boolean` + `isFlat: boolean`
- `compare=0` 时不计算环比,减少查询开销
### 数据源
- BOARD-1`v_dws_assistant_salary_calc` + `v_dws_assistant_monthly_summary` + `v_dim_assistant`
- BOARD-28 个维度对应不同 FDW 表(详见八¾ B2 维度映射)
- BOARD-36 个 `v_dws_finance_*` 表 + `v_dws_assistant_salary_calc`(助教分析)
- CONFIG-1ETL cfg 表
### 验收标准
- 助教看板 4 维度切换正常,筛选变更触发重新请求
- 客户看板 8 维度切换正常,懒加载 20 条
- 财务看板 6 板块数据正确,环比开关工作,区域筛选隐藏预收资产
- 技能类型从 cfg 表读取
---
## RNS1.4CHAT 对齐与联调收尾
> 最后阶段,依赖前面所有子 spec 完成。
> 预估2-3 天
### 目标
CHAT 模块路径迁移和功能补全FDW 端到端验证,全量前后端联调。
### 任务清单
| # | 任务 | 说明 | 涉及 Gap |
|---|------|------|---------|
| T4-1 | CHAT 路径迁移 | `/api/ai/*``/api/xcx/chat/*`;保留 SSE 流式端点 | R3, GAP-51 |
| T4-2 | 新增 CHAT-1/2 同步端点 | 历史列表(含 title 字段)+ 消息查看timestamp→created_at 统一);支持 `?customerId=` 查询参数自动查找/创建对话 | GAP-45~50 |
| T4-3 | CHAT referenceCard 支持 | 消息中附带结构化引用卡片type/title/summary/data多入口参数路由customerId/historyId/coachId | GAP-45, GAP-50 |
| T4-4 | FDW 端到端验证 | 验证所有 FDW 查询在 test_zqyy_app → test_etl_feiqiu 链路上正常工作;检查索引和查询性能 | — |
| T4-5 | 前后端联调 + 修复 | 全量联调 13 个页面修复联调中发现的问题notes 页补充触底加载F11customer-service-records 改为按月请求 APIF10 | F10, F11 |
### 验收标准
- CHAT 模块 SSE 流式 + 同步端点均可用
- 从任意入口进入 chat 页面均能正确关联上下文
- 13 个页面全部连接真实后端运行,无 mock 数据残留
---
## 执行顺序与依赖关系
```
RNS1.0(基础设施+契约)
├── RNS1.1(任务+绩效)──┐
├── RNS1.2(客户+助教)──┼── RNS1.4CHAT+联调)
└── RNS1.3(三看板)─────┘
```
- RNS1.0 必须最先完成(阻塞所有后续)
- RNS1.1 / RNS1.2 / RNS1.3 可并行(无相互依赖)
- RNS1.4 依赖 RNS1.1-1.3 全部完成
---
## 与 NS1 原始 spec 的关系
NS1 原始 spec`NS1-xcx-backend-api.md`)保留为历史参考文档,其中:
- 一~七章:技术架构和接口设计(被 RNS1.0 T0-5 契约重写替代)
- 八½:前置审查发现 R1-R8决策仍有效分散到各子 spec 执行)
- 八¾看板筛选交叉矩阵仍为权威参考RNS1.3 直接引用)
- 九:原始任务清单(被本拆分计划替代)
两份走查报告保留在 `docs/reports/` 作为需求追溯依据:
- `storyboard-walkthrough-assistant-view.md`助教视角51 Gap
- `miniprogram-storyboard-walkthrough-gaps.md`管理层视角31 Gap
---
## Gap 追溯矩阵
### 助教视角报告 Gap → 子 Spec 映射
| Gap | 描述 | 子 Spec |
|-----|------|---------|
| GAP-01 | globalData.authUser 缺少 role/store_name/coach_level/avatar | RNS1.0 T0-6前端修复/me 已返回这些字段) |
| GAP-02 | TASK-1 performance 字段不足4→15+ | RNS1.1 T1-1 |
| GAP-03 | enrichTask 需要 lastVisitDays/balance/aiSuggestion | RNS1.1 T1-1 |
| GAP-04 | pin/unpin API 端点未定义 | RNS1.1 T1-5 |
| GAP-05 | createNote 缺少 score 参数 | RNS1.1 T1-6 |
| GAP-06~08 | 维客线索 tag/source 格式 + aiAnalysis cache_type | RNS1.1 T1-2 |
| GAP-09~10 | taskId/customerId 混淆 | RNS1.0 T0-6 |
| GAP-11 | storageLevel/relationLevel 计算逻辑 | RNS1.1 T1-6前端自行计算 |
| GAP-12~16 | PERF-1 DateGroup/档位/lastMonthIncome/incomeItems | RNS1.1 T1-3 |
| GAP-17 | performance→task-detail 传参不匹配 | RNS1.0 T0-6 |
| GAP-18 | performance 无月份切换 | RNS1.1 T1-6F8 |
| GAP-19~22 | PERF-2 avatarChar/courseTypeClass/分页重置/Banner | RNS1.1 T1-4 + T1-6 |
| GAP-23~30 | CUST-1 五大缺失模块 | RNS1.2 T2-1~T2-3 |
| GAP-31 | customer-detail 跳转未传 customerId | RNS1.0 T0-6 |
| GAP-32~35 | CUST-2 扩展字段 + 按月查询 | RNS1.2 T2-4 |
| GAP-36~37 | notes 触底加载 + tagType 语义 | RNS1.4 T4-5 |
| GAP-38~44 | COACH-1 六大缺失模块 | RNS1.2 T2-5~T2-6 |
| GAP-45~51 | CHAT 模块全部 Gap | RNS1.4 T4-1~T4-3 |
### 管理层视角报告 Gap → 子 Spec 映射
| Gap | 描述 | 子 Spec |
|-----|------|---------|
| G1 | BOARD-3 响应结构完全不匹配 | RNS1.3 T3-3~T3-5 |
| G2 | BOARD-1 字段严重不足 | RNS1.3 T3-1 |
| G3 | BOARD-2 字段严重不足 | RNS1.3 T3-2 |
| G4 | COACH-1 缺少多个核心区域 | RNS1.2 T2-5~T2-6 |
| G5 | CUST-1 缺少管理层核心数据 | RNS1.2 T2-1~T2-3 |
| G6~G10 | BOARD-3 各子模块缺失 + 环比格式 | RNS1.3 T3-3~T3-5 |
| G11 | customer-detail→customer-service-records 未传 customerId | RNS1.0 T0-6 |
| G12 | coach-detail 任务项用 name 跳转 | RNS1.0 T0-6 |
| G13~G14 | api.ts 返回类型不匹配 | RNS1.0 T0-5契约重写时同步更新类型 |
| G15~G26 | 各接口中等严重度缺陷 | 分散到对应子 Spec |
| G27~G31 | 低严重度设计决策 | 分散到对应子 Spec |
### 前端修复项 → 子 Spec 映射
| 修复项 | 描述 | 子 Spec |
|--------|------|---------|
| F1~F6 | 看板筛选变更不触发重新请求 | RNS1.3 T3-7 |
| F7 | task-list status 筛选 UI 未实现 | RNS1.1 T1-6低优先级可后续 |
| F8 | performance 无月份切换 | RNS1.1 T1-6 |
| F9 | performance-records 月份切换未重置分页 | RNS1.1 T1-6 |
| F10 | customer-service-records 本地筛选改 API 请求 | RNS1.4 T4-5 |
| F11 | notes 无触底加载 | RNS1.4 T4-5 |

View File

@@ -0,0 +1,813 @@
# 小程序管理层视角 Storyboard 走查报告 — API 需求 Gap 分析
> 走查日期2026-03-18
> 走查角色:管理层/店长
> 走查路线3 个看板 → 助教详情 → 客户详情 → 服务记录
> 对比基准:前端内联 mock 数据 vs API 契约 (`API-contract.md`) vs 八¾节 (`NS1-xcx-backend-api.md`)
---
## 场景 1助教看板 → 助教详情 → 客户详情
### 1.1 board-coach`pages/board-coach/board-coach.ts`
#### API 调用
- `fetchBoardCoaches({ skill, sort, time })``CoachItem[]`
- 页面 `onLoad()``loadData()``fetchBoardCoaches()`
- 下拉刷新 `onPullDownRefresh()``loadData()`
- ⚠️ 筛选变更(`onSortChange`/`onSkillChange`/`onTimeChange`)仅更新 data 状态,**不调用 `loadData()`**(八¾节 F1 已记录)
#### 前端期望字段(从内联 MOCK_COACHES 的 CoachItem 接口提取,共 18 个字段)
| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `id` | string | 助教 ID | dim_assistant |
| `name` | string | 助教姓名 | dim_assistant |
| `initial` | string | 姓名首字(头像占位) | 前端可计算 |
| `avatarGradient` | string | 头像渐变色 (blue/green/pink/amber/violet/cyan) | 前端可计算或后端分配 |
| `level` | string | 等级 key: star/senior/middle/junior | dim_assistant |
| `levelClass` | string | 等级样式类 | 前端可映射LEVEL_CLASS 常量) |
| `skills` | `Array<{text: string, cls: string}>` | 技能列表含 emoji 和样式类 | cfg 表 |
| `topCustomers` | `string[]` | Top 客户列表含 emoji 前缀(如 `'💖 王先生'` | dws_member_assistant_relation_index |
| `perfHours` | number | 当期定档工时 | dws_assistant_salary_calc |
| `perfHoursBefore` | number? | 上期定档工时(可选) | dws_assistant_salary_calc (上期) |
| `perfGap` | string? | 距升档差距描述(如 `"距升档 13.8h"`,已达标时不返回) | 后端计算 |
| `perfReached` | boolean | 是否已达标 | 后端计算 |
| `salary` | number | 工资总额(元) | dws_assistant_salary_calc |
| `salaryPerfHours` | number | 定档工时 | 同 perfHours |
| `salaryPerfBefore` | number? | 上期定档工时 | 同 perfHoursBefore |
| `svAmount` | number | 客源储值总额(元) | dws_member_consumption_summary |
| `svCustomerCount` | number | 储值客户数 | dws_member_consumption_summary |
| `svConsume` | number | 储值消耗额(元) | dws_member_consumption_summary |
| `taskRecall` | number | 召回任务完成数 | biz.coach_tasks |
| `taskCallback` | number | 回访任务完成数 | biz.coach_tasks |
#### 筛选参数映射
| 筛选项 | 前端参数 | 后端参数 | 查询影响 |
|--------|---------|---------|---------|
| 排序维度 | `sort`: perf_desc/perf_asc/salary_desc/salary_asc/sv_desc/task_desc | `sort` | 决定排序字段和方向 + 卡片模板(dimType) |
| 技能筛选 | `skill`: all/chinese/snooker/mahjong/karaoke | `skill` | WHERE 过滤助教技能类型 |
| 时间范围 | `time`: month/quarter/last_month/last_3m/last_quarter/last_6m | `time` | 决定统计日期区间 |
交叉约束:`time=last_6m` + `sort=sv_desc` 不兼容TIME_OPTIONS 注释标注)
#### 页面跳转
- 点击助教卡片 `onCoachTap``/pages/coach-detail/coach-detail?id={coachId}`
- Tab 切换 → `board-finance`switchTab/ `board-customer`navigateTo
#### API 契约 vs Mock 数据 vs 八¾节 差异
| 差异项 | API 契约 (BOARD-1) | 前端 Mock (CoachItem) | 八¾节 (B1) | 严重度 |
|--------|--------------------|-----------------------|------------|--------|
| 字段结构 | 扁平简单:`id, name, avatar, level, level_class, skills(string[]), total_hours, total_income, customer_count, satisfaction` | 丰富结构4 维度 20 个字段skills 为对象数组,含 topCustomers | 与 Mock 一致,详细列出 4 维度字段 | 🔴 高 |
| `initial` 字段 | ❌ 无 | ✅ 有 | ✅ 有 | 🟠 中 |
| `avatarGradient` 字段 | ❌ 无(仅 `avatar` URL | ✅ 有 | ✅ 有 | 🟠 中 |
| `skills` 类型 | `string[]` | `Array<{text, cls}>` | `Array<{text, cls}>` | 🔴 高 |
| `topCustomers` 字段 | ❌ 无 | ✅ 有 | ✅ 有 | 🔴 高 |
| 4 维度专属字段 | ❌ 全部缺失(仅 total_hours/total_income/customer_count/satisfaction | ✅ 完整perf/salary/sv/task 各 2-4 个字段) | ✅ 完整 | 🔴 高 |
| `sort` 参数格式 | `sort=field:direction` | `sort=perf_desc` 等枚举 | `sort=perf_desc` 等枚举 | 🟠 中 |
| 分页 | 无分页定义 | 无分页(全量返回) | 无分页 | 🟡 低 |
#### spec 未记录的 Gap
1. 🔴 API 契约 BOARD-1 响应字段与前端实际需求严重不匹配 — 契约仅定义 10 个通用字段,前端需要 20 个含 4 维度专属字段
2. 🔴 `skills` 类型不一致 — 契约为 `string[]`,前端需要 `Array<{text, cls}>`(含 emoji 和样式类)
3. 🔴 `topCustomers` 字段契约完全缺失 — 前端每个助教卡片展示 Top 3 客户
4. 🟠 `sort` 参数格式不一致 — 契约用 `field:direction`,前端用枚举值 `perf_desc`
5. 🟠 `initial`/`avatarGradient` 字段契约缺失 — 前端用于头像渲染,需确认由前端计算还是后端返回
---
### 1.2 coach-detail`pages/coach-detail/coach-detail.ts`
#### API 调用
- `fetchCoachDetail(coachId)``CoachCard | null`api.ts 返回类型过于简单)
- 页面 `onLoad({ id })``loadData(id)``fetchCoachDetail(id)`
- 实际使用内联 mock 数据组装 CoachDetail 对象
#### 前端期望字段(从 mockCoachDetail + 各 mock 数组提取)
**CoachDetail 基础信息:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 助教 ID |
| `name` | string | 助教姓名 |
| `avatar` | string | 头像 URL |
| `level` | string | 等级(中文:星级/高级/中级/初级) |
| `skills` | string[] | 技能列表 |
| `workYears` | number | 工龄 |
| `customerCount` | number | 客户数 |
| `hireDate` | string | 入职日期 |
**performance 绩效指标:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `performance.monthlyHours` | number | 本月定档业绩工时 |
| `performance.monthlySalary` | number | 本月工资(预估) |
| `performance.customerBalance` | number | 客源储值余额 |
| `performance.tasksCompleted` | number | 本月任务完成数 |
| `performance.perfCurrent` | number | 当前绩效值 |
| `performance.perfTarget` | number | 目标绩效值 |
**income 收入明细(本月/上月 Tab 切换):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `income.thisMonth` | `IncomeItem[]` | 本月收入明细(基础课时费/激励课时费/充值提成/酒水提成) |
| `income.lastMonth` | `IncomeItem[]` | 上月收入明细 |
| IncomeItem: `label` | string | 收入项名称 |
| IncomeItem: `amount` | string | 金额(格式化,如 `"¥3,500"` |
| IncomeItem: `color` | string | 颜色标识 |
**任务执行区域(管理层视角核心):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `visibleTasks` | `TaskItem[]` | 可见任务active 状态) |
| `hiddenTasks` | `TaskItem[]` | 隐藏任务inactive 状态,被顶替但未过期) |
| `abandonedTasks` | `AbandonedTask[]` | 已放弃任务 |
| TaskItem: `typeLabel` | string | 任务类型标签(高优先召回/优先召回/关系构建/客户回访) |
| TaskItem: `typeClass` | string | 样式类high-priority/priority/relationship/callback |
| TaskItem: `customerName` | string | 客户姓名 |
| TaskItem: `noteCount` | number | 备注数量 |
| TaskItem: `pinned` | boolean | 是否置顶 |
| TaskItem: `notes` | `Array<{pinned?, text, date}>` | 备注列表(可选) |
| AbandonedTask: `customerName` | string | 客户姓名 |
| AbandonedTask: `reason` | string | 放弃原因 |
**客户关系 TOP20管理层关注指标**
| 字段 | 类型 | 说明 |
|------|------|------|
| TopCustomer: `id` | string | 客户 ID |
| TopCustomer: `name` | string | 客户姓名 |
| TopCustomer: `initial` | string | 姓名首字 |
| TopCustomer: `avatarGradient` | string | 头像渐变色 |
| TopCustomer: `heartEmoji` | string | 爱心 emoji/💛/🤍) |
| TopCustomer: `score` | string | 关系指数 |
| TopCustomer: `scoreColor` | string | 分数颜色success/warning/gray |
| TopCustomer: `serviceCount` | number | 服务次数 |
| TopCustomer: `balance` | string | 余额(格式化) |
| TopCustomer: `consume` | string | 消费额(格式化) |
**近期服务明细:**
| 字段 | 类型 | 说明 |
|------|------|------|
| ServiceRecord: `customerId` | string? | 客户 ID |
| ServiceRecord: `customerName` | string | 客户姓名 |
| ServiceRecord: `initial` | string | 姓名首字 |
| ServiceRecord: `avatarGradient` | string | 头像渐变色 |
| ServiceRecord: `type` | string | 课程类型(基础课/激励课) |
| ServiceRecord: `typeClass` | string | 样式类basic/incentive |
| ServiceRecord: `table` | string | 台桌号 |
| ServiceRecord: `duration` | string | 时长(格式化) |
| ServiceRecord: `income` | string | 收入(格式化) |
| ServiceRecord: `date` | string | 日期时间 |
| ServiceRecord: `perfHours` | string? | 定档工时(可选) |
**历史月份统计(管理层评估助教表现):**
| 字段 | 类型 | 说明 |
|------|------|------|
| HistoryMonth: `month` | string | 月份标签(本月/上月/4月... |
| HistoryMonth: `estimated` | boolean | 是否预估 |
| HistoryMonth: `customers` | string | 客户数(格式化,如 `"22人"` |
| HistoryMonth: `hours` | string | 工时(格式化,如 `"87.5h"` |
| HistoryMonth: `salary` | string | 工资(格式化) |
| HistoryMonth: `callbackDone` | number | 回访完成数 |
| HistoryMonth: `recallDone` | number | 召回完成数 |
**备注列表:**
| 字段 | 类型 | 说明 |
|------|------|------|
| NoteItem: `id` | string | 备注 ID |
| NoteItem: `content` | string | 备注内容 |
| NoteItem: `timestamp` | string | 时间戳 |
| NoteItem: `score` | number | 评分 |
| NoteItem: `customerName` | string | 关联客户/管理员名 |
| NoteItem: `tagLabel` | string | 标签 |
| NoteItem: `createdAt` | string | 创建时间(格式化) |
**绩效进度条数据(前端需要后端提供):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `tierNodes` | number[] | 档位节点数组(如 `[0, 100, 130, 160, 190, 220]` |
| `maxHours` | number | 最大工时(进度条满值) |
#### 筛选参数映射
- 无筛选项,单次加载全部数据
- 收入明细有本月/上月 Tab 切换(纯前端切换,数据一次性返回)
#### 页面跳转
- 点击任务项 `onTaskItemTap``/pages/customer-detail/customer-detail?name={customerName}`(⚠️ 用 name 而非 id
- 点击客户卡片 `onCustomerTap``/pages/customer-detail/customer-detail?id={customerId}`
- 点击服务记录 `onSvcCardTap``/pages/customer-detail/customer-detail?id={customerId}`
- 查看更多服务记录 `onViewMoreRecords``/pages/performance-records/performance-records?coachId={coachId}`
- 问问助手 `onStartChat``/pages/chat/chat?coachId={coachId}`
#### API 契约 vs Mock 数据 vs 八¾节 差异
| 差异项 | API 契约 (COACH-1) | 前端 Mock | 八¾节 | 严重度 |
|--------|--------------------|-----------|----|--------|
| 返回类型 | api.ts 返回 `CoachCard`(仅 id/name/avatar/level/keyMetrics | 实际需要 CoachDetail含 performance/income/tasks/topCustomers/serviceRecords/historyMonths/notes | 仅提到基本信息+任务分组+Top客户+服务记录 | 🔴 高 |
| `income` 本月/上月明细 | ❌ 无 | ✅ 有4 项收入分类 × 2 月) | ❌ 无 | 🔴 高 |
| `topCustomers` TOP20 | 契约仅 `top_customers: Array<{id, name, avatar, total_spend, visit_count}>` | Mock 含 20 条,每条含 heartEmoji/score/scoreColor/serviceCount/balance/consume | ❌ 未详细定义字段 | 🔴 高 |
| `historyMonths` 历史月份 | ❌ 完全缺失 | ✅ 有5 个月,含 estimated/customers/hours/salary/callbackDone/recallDone | ❌ 完全缺失 | 🔴 高 |
| `tierNodes` 档位节点 | ❌ 缺失 | ✅ 有(进度条组件需要) | ❌ 缺失 | 🟠 中 |
| 任务分组 | `visible_tasks`/`hidden_tasks`/`abandoned_tasks` | 同,但 TaskItem 结构含 notes 子数组 | ✅ 已确认 active/inactive/abandoned 映射 | 🟡 低 |
| 备注列表 | ❌ 未在 COACH-1 定义 | ✅ 有(含 score 字段) | ❌ 未提及 | 🟠 中 |
#### spec 未记录的 Gap
1. 🔴 COACH-1 响应缺少 `income` 收入明细(本月/上月 4 项分类)— 管理层评估助教收入结构的核心数据
2. 🔴 COACH-1 响应缺少 `historyMonths` 历史月份统计 — 管理层评估助教长期表现趋势
3. 🔴 `topCustomers` 字段结构不完整 — 契约仅 5 个字段,前端需要 10 个字段(含 heartEmoji/scoreColor/balance/consume
4. 🟠 COACH-1 响应缺少 `tierNodes`/`maxHours` — 绩效进度条组件需要档位节点数据
5. 🟠 COACH-1 响应缺少备注列表 — 管理层需要查看助教相关备注
6. 🟠 `onTaskItemTap``name` 而非 `id` 跳转客户详情 — 前端 Bugname 不唯一
7. 🟡 api.ts 中 `fetchCoachDetail` 返回类型为 `CoachCard`mock-data.ts 中的简单类型),与实际需要的 CoachDetail 不匹配
---
## 场景 2客户看板 → 客户详情 → 客户服务记录
### 2.1 board-customer`pages/board-customer/board-customer.ts`
#### API 调用
- `fetchBoardCustomers({ dimension, project })``CustomerItem[]`
- 页面 `onLoad()``loadData()``fetchBoardCustomers()`
- 下拉刷新 `onPullDownRefresh()``loadData()`
- ⚠️ 筛选变更(`onDimensionChange`/`onProjectChange`)仅更新 data 状态,**不调用 `loadData()`**(八¾节 F2 已记录)
- ⚠️ 缺少分页参数 `page`/`pageSize`(八¾节 F3 已记录)
#### 前端期望字段(从内联 MOCK_CUSTOMERS 的 CustomerItem 接口提取)
**所有维度共享基础字段(每个 item 都返回):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 客户 member_id |
| `name` | string | 客户姓名 |
| `initial` | string | 姓名首字 |
| `avatarCls` | string | 头像样式类avatar--amber/pink/blue 等) |
| `assistants` | `AssistantInfo[]` | 关联助教列表 |
**AssistantInfo 结构:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 助教姓名 |
| `cls` | string | 样式类assistant--assignee/abandoned/normal |
| `heartScore` | number | 爱心分 0-10 |
| `badge` | string? | 标记(跟/弃) |
| `badgeCls` | string? | 标记样式类 |
**8 维度专属字段(按 dimension 参数返回对应字段):**
| 维度 | 专属字段 | 类型 | 说明 |
|------|---------|------|------|
| recall | `idealDays` | number | 理想到店间隔(天) |
| | `elapsedDays` | number | 已过天数 |
| | `overdueDays` | number | 超期天数 |
| | `visits30d` | number | 近30天到店次数 |
| | `balance` | string | 余额(格式化) |
| | `recallIndex` | string | 召回指数 |
| potential | `potentialTags` | `Array<{text, theme}>` | 潜力标签 |
| | `spend30d` | string | 近30天消费 |
| | `avgVisits` | string | 月均到店 |
| | `avgSpend` | string | 次均消费 |
| balance | `balance` | string | 当前余额 |
| | `lastVisit` | string | 最近到店 |
| | `monthlyConsume` | string | 月均消耗 |
| | `availableMonths` | string | 可用月数 |
| recharge | `lastRecharge` | string | 最后充值日期 |
| | `rechargeAmount` | string | 充值金额 |
| | `recharges60d` | string | 近60天充值次数 |
| | `currentBalance` | string | 当前余额 |
| recent | `daysAgo` | number | 距今天数 |
| | `visitFreq` | string | 到店频率 |
| | `idealDays` | number | 理想间隔 |
| | `visits30d` | number | 近30天到店 |
| | `avgSpend` | string | 次均消费 |
| spend60 | `spend60d` | string | 近60天消费总额 |
| | `visits60d` | string | 近60天到店次数 |
| | `highSpendTag` | boolean | 是否高消费标签 |
| | `avgSpend` | string | 次均消费 |
| freq60 | `visits60d` | string | 近60天到店次数 |
| | `avgInterval` | string | 平均到店间隔 |
| | `weeklyVisits` | `Array<{val, pct}>` | 8周到店柱状图 |
| | `spend60d` | string | 近60天消费 |
| loyal | `intimacy` | string | 亲密度指数 |
| | `topCoachName` | string | 最亲密助教姓名 |
| | `topCoachHeart` | number | 最亲密助教爱心分 |
| | `topCoachScore` | string | 最亲密助教关系指数 |
| | `coachName` | string | 主助教姓名 |
| | `coachRatio` | string | 主助教占比 |
| | `coachDetails` | `Array<{name, cls, heartScore, badge?, avgDuration, serviceCount, coachSpend, relationIdx}>` | 助教服务明细表 |
#### 筛选参数映射
| 筛选项 | 前端参数 | 后端参数 | 查询影响 |
|--------|---------|---------|---------|
| 维度切换 | `dimension`: recall/potential/balance/recharge/recent/spend60/freq60/loyal | `dimension` | 决定查询的 FDW 表和排序字段 |
| 项目筛选 | `project`: all/chinese/snooker/mahjong/karaoke | `project` | WHERE 过滤客户偏好项目 |
无交叉约束8×5=40 种有效组合。
#### 页面跳转
- 点击客户卡片 `onCustomerTap``/pages/customer-detail/customer-detail?id={customerId}`
- Tab 切换 → `board-finance`switchTab/ `board-coach`navigateTo
#### API 契约 vs Mock 数据 vs 八¾节 差异
| 差异项 | API 契约 (BOARD-2) | 前端 Mock (CustomerItem) | 八¾节 (B2) | 严重度 |
|--------|--------------------|-----------------------|------------|--------|
| 字段结构 | 扁平简单:`id, name, avatar, tags(string[]), total_spend, visit_count, last_visit, relation_index` | 丰富结构8 维度 40+ 专属字段 + assistants 对象数组 | 与 Mock 一致 | 🔴 高 |
| `assistants` 字段 | ❌ 无 | ✅ 有(含 name/cls/heartScore/badge/badgeCls | ✅ 有 | 🔴 高 |
| `avatarCls` 字段 | ❌ 无(仅 `avatar` URL | ✅ 有 | ✅ 有 | 🟠 中 |
| `initial` 字段 | ❌ 无 | ✅ 有 | ✅ 有 | 🟠 中 |
| 8 维度专属字段 | ❌ 全部缺失 | ✅ 完整 | ✅ 完整 | 🔴 高 |
| `weeklyVisits` 柱状图 | ❌ 无 | ✅ 有freq60 维度8 周数据) | ✅ 有 | 🔴 高 |
| `coachDetails` 明细表 | ❌ 无 | ✅ 有loyal 维度,助教服务明细) | ✅ 有 | 🔴 高 |
| `potentialTags` 标签 | ❌ 无 | ✅ 有potential 维度,含 text+theme | ✅ 有 | 🟠 中 |
| 分页 | ✅ 有page/pageSize | ❌ 无(全量返回 3 条 mock | ✅ 已确认 20 条懒加载 | 🟡 低 |
#### spec 未记录的 Gap
1. 🔴 API 契约 BOARD-2 响应字段与前端实际需求严重不匹配 — 契约仅 8 个通用字段,前端需要 40+ 维度专属字段
2. 🔴 `assistants` 关联助教列表契约完全缺失 — 每个客户卡片展示关联助教及其爱心分/跟弃状态
3. 🔴 `weeklyVisits` 8 周柱状图数据契约缺失 — freq60 维度的核心可视化数据
4. 🔴 `coachDetails` 助教服务明细表契约缺失 — loyal 维度展示每个助教的详细服务数据
5. 🟠 `potentialTags` 标签结构契约缺失 — potential 维度展示客户潜力标签
---
### 2.2 customer-detail`pages/customer-detail/customer-detail.ts`
#### API 调用
- `fetchCustomerDetail(customerId)``MockCustomerDetail | null`
- 页面 `onLoad()``loadDetail()``fetchCustomerDetail(id)`
- ⚠️ `loadDetail()` 中 id 获取方式有 Bug`(this as any).__route__?.split('?')[1]` 无法正确解析 query 参数
#### 前端期望字段(从内联 data 提取,管理层视角)
**客户基础信息:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `detail.id` | string | 客户 ID |
| `detail.name` | string | 客户姓名 |
| `detail.avatarChar` | string | 头像字符 |
| `detail.phone` | string | 完整手机号(前端控制脱敏显示) |
| `detail.balance` | string | 余额(格式化) |
| `detail.consumption60d` | string | 近60天消费格式化 |
| `detail.idealInterval` | string | 理想到店间隔 |
| `detail.daysSinceVisit` | string | 距上次到店天数 |
**AI 洞察(管理层视角特有):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `aiInsight.summary` | string | AI 分析摘要 |
| `aiInsight.strategies` | `Array<{color, text}>` | 策略建议列表 |
**维客线索clues**
| 字段 | 类型 | 说明 |
|------|------|------|
| `clues[].category` | string | 线索大类(含换行符格式化) |
| `clues[].categoryColor` | string | 分类颜色 |
| `clues[].text` | string | 线索文本(含 emoji |
| `clues[].source` | string | 来源(系统/助教名) |
| `clues[].detail` | string? | 详情(可选,展开显示) |
**关联助教 coachTasks管理层关注多助教对同一客户的服务**
| 字段 | 类型 | 说明 |
|------|------|------|
| `coachTasks[].name` | string | 助教姓名 |
| `coachTasks[].level` | string | 等级 |
| `coachTasks[].levelColor` | string | 等级颜色 |
| `coachTasks[].taskType` | string | 任务类型标签 |
| `coachTasks[].taskColor` | string | 任务颜色 |
| `coachTasks[].bgClass` | string | 背景样式类 |
| `coachTasks[].status` | string | 状态normal/pinned/abandoned |
| `coachTasks[].lastService` | string | 最后服务时间 |
| `coachTasks[].metrics` | `Array<{label, value, color?}>` | 指标列表近60天次数/总时长/次均时长) |
**喜好助教 favoriteCoaches**
| 字段 | 类型 | 说明 |
|------|------|------|
| `favoriteCoaches[].emoji` | string | 爱心 emoji |
| `favoriteCoaches[].name` | string | 助教姓名 |
| `favoriteCoaches[].relationIndex` | string | 关系指数 |
| `favoriteCoaches[].indexColor` | string | 指数颜色 |
| `favoriteCoaches[].bgClass` | string | 背景样式类 |
| `favoriteCoaches[].stats` | `Array<{label, value, color?}>` | 统计(基础/激励/上课/充值) |
**消费记录 consumptionRecords3 种类型,管理层关注消费结构):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `ConsumptionRecord.id` | string | 记录 ID |
| `ConsumptionRecord.type` | `"table" \| "shop" \| "recharge"` | 消费类型 |
| `ConsumptionRecord.date` | string | 日期 |
| `ConsumptionRecord.tableName` | string? | 台桌名table 类型) |
| `ConsumptionRecord.startTime` | string? | 开始时间 |
| `ConsumptionRecord.endTime` | string? | 结束时间 |
| `ConsumptionRecord.duration` | string? | 时长 |
| `ConsumptionRecord.tableFee` | number? | 台费(优惠后) |
| `ConsumptionRecord.tableOrigPrice` | number? | 台费原价 |
| `ConsumptionRecord.coaches` | `Array<{name, level, levelColor, courseType, hours, perfHours?, fee}>` | 助教服务明细 |
| `ConsumptionRecord.foodAmount` | number? | 食品酒水金额(优惠后) |
| `ConsumptionRecord.foodOrigPrice` | number? | 食品酒水原价 |
| `ConsumptionRecord.totalAmount` | number? | 总金额 |
| `ConsumptionRecord.totalOrigPrice` | number? | 总原价 |
| `ConsumptionRecord.payMethod` | string? | 支付方式 |
| `ConsumptionRecord.rechargeAmount` | number? | 充值金额recharge 类型) |
**备注列表:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `sortedNotes[].id` | string | 备注 ID |
| `sortedNotes[].tagLabel` | string | 标签(管理员/助教名) |
| `sortedNotes[].createdAt` | string | 创建时间 |
| `sortedNotes[].content` | string | 备注内容 |
#### 页面跳转
- 查看服务记录 `onViewServiceRecords``/pages/customer-service-records/customer-service-records`(⚠️ 未传 customerId 参数!)
- 问问助手 `onStartChat``/pages/chat/chat`(⚠️ 未传 customerId 参数!)
#### API 契约 vs Mock 数据 差异
| 差异项 | API 契约 (CUST-1) | 前端 Mock | 严重度 |
|--------|--------------------|-----------| --------|
| `aiInsight` AI 洞察 | ❌ 完全缺失 | ✅ 有summary + strategies | 🔴 高 |
| `coachTasks` 关联助教任务 | ❌ 完全缺失 | ✅ 有4 个助教,含 metrics | 🔴 高 |
| `favoriteCoaches` 喜好助教 | ❌ 完全缺失 | ✅ 有(含 stats 统计) | 🔴 高 |
| `clues` 维客线索 | 契约有 `retention_clues` 但结构不同 | 前端用 category/categoryColor/text/source/detail | 🟠 中 |
| `consumptionRecords` 结构 | 契约为扁平 `{id, date, type, type_class, amount, table, duration}` | 前端为嵌套结构(含 coaches 子数组、tableFee/foodAmount 分项) | 🔴 高 |
| `balance`/`consumption60d`/`idealInterval`/`daysSinceVisit` | ❌ 缺失 | ✅ 有 | 🟠 中 |
| `avatarChar` | ❌ 无(仅 avatar URL | ✅ 有 | 🟡 低 |
| 备注列表 | ❌ 未在 CUST-1 定义 | ✅ 有 | 🟠 中 |
#### spec 未记录的 Gap
1. 🔴 CUST-1 缺少 `aiInsight` AI 洞察 — 管理层查看客户时的核心决策辅助信息来源biz.ai_cache app4_analysis
2. 🔴 CUST-1 缺少 `coachTasks` 关联助教任务区域 — 管理层关注多助教对同一客户的服务情况
3. 🔴 CUST-1 缺少 `favoriteCoaches` 喜好助教 — 展示客户与各助教的关系指数和服务统计
4. 🔴 CUST-1 消费记录结构严重不匹配 — 契约为扁平结构,前端需要嵌套结构(含助教明细、台费/食品分项、原价/优惠后价格)
5. 🟠 `onViewServiceRecords` 跳转未传 `customerId` — 前端 Bug
6. 🟠 `onStartChat` 跳转未传 `customerId` — 前端 Bug
7. 🟠 `loadDetail()` 中 id 解析方式有 Bug — 应从 `onLoad(options)` 的 options 中获取
---
### 2.3 customer-service-records`pages/customer-service-records/customer-service-records.ts`
#### API 调用
- `fetchCustomerRecords({ customerId })``{ records, hasMore }`
- 页面 `onLoad({ customerId, id })``loadData(id)``fetchCustomerRecords({ customerId: id })`
- ⚠️ 首次加载拉取全部记录,月份切换仅本地过滤(八¾节 F10/L4 已记录)
#### 前端期望字段
**页面头部信息:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `customerName` | string | 客户姓名 |
| `customerPhone` | string | 脱敏手机号 |
| `customerPhoneFull` | string | 完整手机号 |
| `totalServiceCount` | number | 累计服务次数 |
| `relationIndex` | string | 关系指数 |
**ServiceRecord 结构(转换后):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `table` | string | 台桌号 |
| `type` | string | 课程类型标签 |
| `typeClass` | `'basic' \| 'vip' \| 'tip' \| 'recharge'` | 样式类 |
| `recordType` | `'course' \| 'recharge'` | 记录类型 |
| `duration` | number | 折算后小时数 |
| `durationRaw` | number | 折算前小时数 |
| `income` | number | 到手金额 |
| `isEstimate` | boolean | 是否预估 |
| `drinks` | string | 饮品描述 |
| `date` | string | 显示日期+时间段 |
#### 筛选参数映射
| 筛选项 | 前端参数 | 后端参数 | 查询影响 |
|--------|---------|---------|---------|
| 月份切换 | `currentYear`/`currentMonth` | `year`/`month` | 按月筛选记录(当前为本地过滤) |
| 客户 ID | `customerId` | `customerId` | WHERE 过滤 |
#### 页面跳转
- 无跳转(详情页,仅返回上一页)
#### API 契约 vs Mock 数据 差异
| 差异项 | API 契约 (CUST-2) | 前端 Mock | 严重度 |
|--------|--------------------|-----------| --------|
| 源数据结构 | 契约返回 ServiceRecord 格式 | Mock 使用 ConsumptionRecord来自 mock-data.ts前端转换为 ServiceRecord | 🟠 中 |
| `customerPhone`/`customerPhoneFull` | 契约有 `customer_phone` | Mock 硬编码 | 🟡 低 |
| `tables` 台桌列表 | 契约有 `tables: Array<{id, name}>` | Mock 无(前端模拟台号) | 🟡 低 |
| `relationIndex` | 契约有 `relation_index` | Mock 硬编码 `"0.85"` | 🟡 低 |
| 月度统计 | ❌ 契约无 | ✅ 前端本地计算 `monthCount`/`monthHours`/`monthRelation` | 🟠 中 |
#### spec 未记录的 Gap
1. 🟠 CUST-2 响应缺少月度统计汇总 — 前端需要 `monthCount`/`monthHours`,当前本地计算,联调后应由后端返回
2. 🟠 首次加载全量 vs 按月分页 — 当前设计一次拉全部记录本地过滤,数据量大时需改为按月请求
---
## 场景 3财务看板最复杂
### 3.1 board-finance`pages/board-finance/board-finance.ts`
#### API 调用
- `fetchBoardFinance()``BoardFinanceData`(⚠️ 当前签名仅 `{ date?: string }`,缺少 time/area/compare 参数)
- 页面 `onLoad()``fetchBoardFinance()` → 设置 `pageState: 'normal'`
- ⚠️ 筛选变更(`onTimeChange`/`onAreaChange`/`toggleCompare`)仅更新 data 状态,**不调用 API**(八¾节 F4/F5 已记录)
- ⚠️ 所有 6 个板块数据完全内联在 `data` 中,`fetchBoardFinance` 返回的 `_data` 未使用
#### 前端期望字段(从内联 data 提取6 个板块完整结构)
**板块 1经营一览 (overview) — 8 个指标 + 8 个环比值**
| 字段 | 类型 | 说明 |
|------|------|------|
| `overview.occurrence` | string | 发生额/正价(格式化,如 `"¥823,456"` |
| `overview.occurrenceCompare` | string | 环比百分比(如 `"12.5%"` |
| `overview.discount` | string | 总优惠(负值,如 `"-¥113,336"` |
| `overview.discountCompare` | string | 环比 |
| `overview.discountRate` | string | 优惠率(如 `"13.8%"` |
| `overview.discountRateCompare` | string | 环比 |
| `overview.confirmedRevenue` | string | 成交/确认收入 |
| `overview.confirmedCompare` | string | 环比 |
| `overview.cashIn` | string | 实收/现金流入 |
| `overview.cashInCompare` | string | 环比 |
| `overview.cashOut` | string | 现金支出 |
| `overview.cashOutCompare` | string | 环比 |
| `overview.cashBalance` | string | 现金结余 |
| `overview.cashBalanceCompare` | string | 环比 |
| `overview.balanceRate` | string | 结余率 |
| `overview.balanceRateCompare` | string | 环比 |
**板块 2预收资产 (recharge) — 储值卡 + 赠送卡矩阵**
| 字段 | 类型 | 说明 |
|------|------|------|
| `recharge.actualIncome` | string | 储值卡充值实收 |
| `recharge.actualCompare` | string | 环比 |
| `recharge.firstCharge` | string | 首充 |
| `recharge.firstChargeCompare` | string | 环比 |
| `recharge.renewCharge` | string | 续费 |
| `recharge.renewChargeCompare` | string | 环比 |
| `recharge.consumed` | string | 消耗 |
| `recharge.consumedCompare` | string | 环比 |
| `recharge.cardBalance` | string | 储值卡总余额 |
| `recharge.cardBalanceCompare` | string | 环比 |
| `recharge.giftRows` | `Array<{label, total, totalCompare, wine, wineCompare, table, tableCompare, coupon, couponCompare}>` | 赠送卡矩阵3行新增/消费/余额 × 4列合计/酒水卡/台费卡/抵用券) |
| `recharge.allCardBalance` | string | 全类别会员卡余额合计 |
| `recharge.allCardBalanceCompare` | string | 环比 |
赠送卡矩阵结构3×4 = 12 个数据单元 + 12 个环比值 = 24 个字段):
- 行:新增 / 消费 / 余额
- 列:合计(total) / 酒水卡(wine) / 台费卡(table) / 抵用券(coupon)
**板块 3应计收入确认 (revenue) — 4 个子表**
| 字段 | 类型 | 说明 |
|------|------|------|
| `revenue.structureRows` | `Array<{id, name, desc?, amount, discount, booked, bookedCompare, isSub?}>` | 收入结构表9行含子行标记 |
| `revenue.priceItems` | `Array<{name, value, compare}>` | 正价明细4项 |
| `revenue.totalOccurrence` | string | 正价合计 |
| `revenue.totalOccurrenceCompare` | string | 环比 |
| `revenue.discountItems` | `Array<{name, desc?, value, compare}>` | 优惠明细4项 |
| `revenue.confirmedTotal` | string | 确认收入合计 |
| `revenue.confirmedTotalCompare` | string | 环比 |
| `revenue.channelItems` | `Array<{name, desc?, value, compare}>` | 渠道明细3项 |
收入结构表行结构:
- 开台与包厢(含 A区/B区/C区/团建区/麻将区 5 个子行)
- 助教基础课
- 助教激励课
- 食品酒水
**板块 4现金流入 (cashflow)**
| 字段 | 类型 | 说明 |
|------|------|------|
| `cashflow.consumeItems` | `Array<{name, desc, value, compare, isDown}>` | 消费收款3项纸币现金/线上收款/团购平台) |
| `cashflow.rechargeItems` | `Array<{name, desc, value, compare}>` | 充值收款1项会员充值到账 |
| `cashflow.total` | string | 现金流入合计 |
| `cashflow.totalCompare` | string | 环比 |
**板块 5现金流出 (expense) — 4 个子分组**
| 字段 | 类型 | 说明 |
|------|------|------|
| `expense.operationItems` | `Array<{name, value, compare, isDown}>` | 经营支出3项食品饮料/耗材/报销) |
| `expense.fixedItems` | `Array<{name, value, compare, isFlat}>` | 固定支出4项房租/水电/物业/人员工资) |
| `expense.coachItems` | `Array<{name, value, compare, isDown}>` | 助教分成4项基础课/激励课/充值提成/额外奖金) |
| `expense.platformItems` | `Array<{name, value, compare}>` | 平台服务费3项汇来米/美团/抖音) |
| `expense.total` | string | 现金流出合计 |
| `expense.totalCompare` | string | 环比 |
**板块 6助教分析 (coachAnalysis) — 基础课 + 激励课两个子表**
| 字段 | 类型 | 说明 |
|------|------|------|
| `coachAnalysis.basic.totalPay` | string | 基础课总收入 |
| `coachAnalysis.basic.totalPayCompare` | string | 环比 |
| `coachAnalysis.basic.totalShare` | string | 基础课总分成 |
| `coachAnalysis.basic.totalShareCompare` | string | 环比 |
| `coachAnalysis.basic.avgHourly` | string | 平均时薪 |
| `coachAnalysis.basic.avgHourlyCompare` | string | 环比 |
| `coachAnalysis.basic.rows` | `Array<{level, pay, payCompare, share, shareCompare, hourly, hourlyCompare, hourlyFlat?, payDown?, shareDown?}>` | 按等级分行(初级/中级/高级/星级) |
| `coachAnalysis.incentive.*` | 同 basic | 激励课(结构完全相同) |
每行字段:
- `level`: string — 等级名
- `pay`: string — 收入
- `payCompare`: string — 收入环比
- `payDown`: boolean? — 收入是否下降
- `share`: string — 分成
- `shareCompare`: string — 分成环比
- `shareDown`: boolean? — 分成是否下降
- `hourly`: string — 时薪
- `hourlyCompare`: string — 时薪环比
- `hourlyFlat`: boolean? — 时薪是否持平
#### 筛选参数映射
| 筛选项 | 前端参数 | 后端参数 | 查询影响 |
|--------|---------|---------|---------|
| 时间范围 | `selectedTime`: month/lastMonth/week/lastWeek/quarter3/quarter/lastQuarter/half6 | `time` | 决定统计日期区间 |
| 区域筛选 | `selectedArea`: all/hall/hallA/hallB/hallC/mahjong/teamBuilding | `area` | WHERE 过滤区域area≠all 时预收资产板块隐藏 |
| 环比开关 | `compareEnabled`: true/false | `compare`: 0/1 | 控制是否返回环比数据 |
时间范围→日期区间映射(后端需实现):
- `month` → 当月1日 ~ 当月末日
- `lastMonth` → 上月1日 ~ 上月末日
- `week` → 本周一 ~ 本周日
- `lastWeek` → 上周一 ~ 上周日
- `quarter3` → 前3个月不含本月
- `quarter` → 本季度第1天 ~ 本季度末日
- `lastQuarter` → 上季度
- `half6` → 最近6个月不含本月
#### 目录导航 (sectionNav)
- 6 个板块:经营一览/预收资产/应计收入确认/现金流入/现金流出/助教分析
- 每个板块有 emoji + 标题 + sectionId
- 吸顶头显示当前板块 emoji + 标题 + 描述
- 板块描述(`_sectionDescs`6 段固定文案
#### 帮助图标 (helpTip)
- 7 个指标解释occurrence/discount/confirmed/cashIn/cashOut/balance/rechargeActual/firstCharge/renewCharge/consume/cardBalance/allCardBalance
- 每个含 `title` + `content`(多行文本)
#### API 契约 vs Mock 数据 vs 八¾节 差异
| 差异项 | API 契约 (BOARD-3) | 前端 Mock (内联 data) | 八¾节 (B3) | 严重度 |
|--------|--------------------|-----------------------|------------|--------|
| 响应结构 | 扁平 `metrics: Array<{key, label, value, unit, trend, compareValue, sub_items}>` | 6 个板块独立对象,深度嵌套结构 | 仅定义了参数,未定义完整响应结构 | 🔴 高 |
| 板块数量 | 未区分板块(单一 metrics 数组) | 6 个独立板块overview/recharge/revenue/cashflow/expense/coachAnalysis | 提到 6 个板块但未定义结构 | 🔴 高 |
| 环比数据格式 | `trend: 'up'\|'down'\|'flat'` + `compareValue?: string` | 每个值旁有独立的 `xxxCompare` 字段(百分比字符串) + `isDown`/`isFlat` 布尔标记 | 已确认需补充 `compareValue` 字段 | 🔴 高 |
| 赠送卡矩阵 | ❌ 完全缺失 | ✅ 3×4 矩阵24 个字段) | ❌ 未详细定义 | 🔴 高 |
| 收入结构表 | ❌ 缺失 | ✅ 9 行含子行标记 | ❌ 未详细定义 | 🔴 高 |
| 助教分析子表 | ❌ 缺失 | ✅ 基础课+激励课各 4 行(按等级) | ❌ 未详细定义 | 🔴 高 |
| 现金流出 4 子分组 | ❌ 缺失 | ✅ 经营支出/固定支出/助教分成/平台服务费 | ❌ 未详细定义 | 🔴 高 |
| `time`/`area`/`compare` 参数 | 八¾节末尾补充了参数定义 | 前端未传参Bug | ✅ 已定义 | 🟠 中(八¾节已补充) |
| `isDown`/`isFlat` 标记 | ❌ 无 | ✅ 有(控制箭头颜色方向) | ❌ 无 | 🟠 中 |
| 帮助图标内容 | ❌ 完全缺失 | ✅ 12 个指标解释 | ❌ 未提及 | 🟡 低 |
#### spec 未记录的 Gap
1. 🔴 BOARD-3 响应结构与前端实际需求完全不匹配 — 契约为扁平 metrics 数组,前端需要 6 个独立板块的深度嵌套结构
2. 🔴 赠送卡矩阵3×4=12 单元 + 12 环比)完全未定义 — 预收资产板块的核心数据
3. 🔴 收入结构表9 行含子行层级)完全未定义 — 应计收入确认板块的核心数据
4. 🔴 助教分析子表(基础课+激励课各 4 行按等级)完全未定义
5. 🔴 现金流出 4 个子分组(经营/固定/助教分成/平台服务费)完全未定义
6. 🔴 环比数据格式不匹配 — 契约仅 `trend` + `compareValue`,前端需要每个值旁独立的 `xxxCompare` 字段 + `isDown`/`isFlat` 布尔标记
7. 🟠 `isDown`/`isFlat` 布尔标记未定义 — 前端用于控制环比箭头方向和颜色
8. 🟡 帮助图标内容12 个指标解释文案)未定义 — 可硬编码在前端,但后端可提供配置化
---
## 场景 4跨页面数据关联
### 4.1 board-coach → coach-detail → customer-detail → customer-service-records
**参数传递链:**
1. `board-coach` 点击助教卡片 → `coach-detail?id={coachId}`
2. `coach-detail` 点击客户卡片 → `customer-detail?id={customerId}`
3. `coach-detail` 点击任务项 → `customer-detail?name={customerName}`(⚠️ 用 name 而非 id
4. `customer-detail` 查看服务记录 → `customer-service-records`(⚠️ 未传 customerId
5. `coach-detail` 查看更多服务记录 → `performance-records?coachId={coachId}`
6. `coach-detail` 问问助手 → `chat?coachId={coachId}`
**跨页面 Gap**
- 🔴 步骤 3 用 `name` 跳转name 不唯一,应改为 `id`
- 🔴 步骤 4 未传 `customerId``customer-service-records` 无法知道查哪个客户
- 🟠 `customer-detail``onStartChat` 未传 `customerId`chat 页面无法关联客户上下文
### 4.2 board-customer → customer-detail → chat
**参数传递链:**
1. `board-customer` 点击客户卡片 → `customer-detail?id={customerId}`
2. `customer-detail` 问问助手 → `chat`(⚠️ 未传参数)
**跨页面 Gap**
- 🟠 `chat` 页面无法获取客户上下文customerId/customerName
### 4.3 board-finance → coach-detail 入口
**分析结果:**
- board-finance 的助教分析板块coachAnalysis展示按等级汇总的数据**无跳转到 coach-detail 的入口**
- 这是合理的设计 — 财务看板关注的是汇总数据而非个体
---
## 汇总 Gap 清单(按严重度排序)
### 🔴 高严重度(阻塞联调,必须在后端开发前解决)
| # | 类别 | Gap 描述 | 涉及接口 | 涉及页面 |
|---|------|---------|---------|---------|
| G1 | 契约缺陷 | **BOARD-3 响应结构与前端完全不匹配** — 契约为扁平 metrics 数组,前端需要 6 个独立板块的深度嵌套结构overview/recharge/revenue/cashflow/expense/coachAnalysis含 200+ 个字段 | BOARD-3 | board-finance |
| G2 | 契约缺陷 | **BOARD-1 响应字段严重不足** — 契约仅 10 个通用字段,前端需要 20 个含 4 维度专属字段perf/salary/sv/task且 skills 类型不匹配string[] vs Array<{text,cls}> | BOARD-1 | board-coach |
| G3 | 契约缺陷 | **BOARD-2 响应字段严重不足** — 契约仅 8 个通用字段,前端需要 40+ 维度专属字段8 个维度各有独立字段集),且缺少 assistants 关联助教列表 | BOARD-2 | board-customer |
| G4 | 契约缺陷 | **COACH-1 响应缺少多个核心区域** — 缺少 income 收入明细(本月/上月、historyMonths 历史月份统计、tierNodes 档位节点、备注列表topCustomers 字段结构不完整(契约 5 字段 vs 前端 10 字段) | COACH-1 | coach-detail |
| G5 | 契约缺陷 | **CUST-1 响应缺少管理层核心数据** — 缺少 aiInsight AI 洞察、coachTasks 关联助教任务、favoriteCoaches 喜好助教;消费记录结构严重不匹配(契约扁平 vs 前端嵌套含助教明细) | CUST-1 | customer-detail |
| G6 | 契约缺陷 | **BOARD-3 赠送卡矩阵完全未定义** — 预收资产板块的 3×4 矩阵(新增/消费/余额 × 合计/酒水卡/台费卡/抵用券)= 24 个数据字段 + 24 个环比字段 | BOARD-3 | board-finance |
| G7 | 契约缺陷 | **BOARD-3 收入结构表完全未定义** — 9 行含子行层级(开台与包厢含 5 个区域子行 + 助教基础课 + 助教激励课 + 食品酒水),每行含 amount/discount/booked/bookedCompare | BOARD-3 | board-finance |
| G8 | 契约缺陷 | **BOARD-3 助教分析子表完全未定义** — 基础课+激励课各含汇总行 + 4 个等级行(初级/中级/高级/星级),每行含 pay/share/hourly 及各自环比 | BOARD-3 | board-finance |
| G9 | 契约缺陷 | **BOARD-3 现金流出 4 子分组完全未定义** — 经营支出(3项)/固定支出(4项)/助教分成(4项)/平台服务费(3项) | BOARD-3 | board-finance |
| G10 | 契约缺陷 | **环比数据格式不匹配** — 契约仅 `trend` + `compareValue`,前端需要每个值旁独立的 `xxxCompare` 百分比字段 + `isDown`/`isFlat` 布尔标记 | BOARD-3 | board-finance |
| G11 | 前端 Bug | **customer-detail 跳转 customer-service-records 未传 customerId** — 服务记录页无法知道查哪个客户 | CUST-2 | customer-detail → customer-service-records |
| G12 | 前端 Bug | **coach-detail 任务项跳转用 name 而非 id**`onTaskItemTap``name={customerName}`name 不唯一 | — | coach-detail → customer-detail |
| G13 | 类型不匹配 | **api.ts 中 fetchCoachDetail 返回类型为 CoachCard** — 实际需要完整的 CoachDetail含 performance/income/tasks/topCustomers/serviceRecords/historyMonths/notes类型定义严重不足 | COACH-1 | coach-detail |
| G14 | 类型不匹配 | **mock-data.ts 中的类型与页面内联 mock 完全脱节** — BoardFinanceData 仅含 4 个 metrics实际页面需要 6 个板块 200+ 字段CustomerCard/CoachCard 过于简单 | 全部看板 | 全部看板页 |
### 🟠 中严重度(影响功能完整性,联调前需解决)
| # | 类别 | Gap 描述 | 涉及接口 | 涉及页面 |
|---|------|---------|---------|---------|
| G15 | 契约缺陷 | **BOARD-1 sort 参数格式不一致** — 契约用 `field:direction`,前端用枚举值 `perf_desc` | BOARD-1 | board-coach |
| G16 | 契约缺陷 | **BOARD-1/2 缺少 initial/avatarGradient/avatarCls 字段** — 前端用于头像渲染,需确认由前端计算还是后端返回 | BOARD-1/2 | board-coach/board-customer |
| G17 | 契约缺陷 | **CUST-1 缺少 balance/consumption60d/idealInterval/daysSinceVisit** — 客户详情页头部展示的核心指标 | CUST-1 | customer-detail |
| G18 | 契约缺陷 | **CUST-1 维客线索结构不匹配** — 契约用 tag/tag_color/emoji/text/source/desc前端用 category/categoryColor/text/source/detail | CUST-1 | customer-detail |
| G19 | 契约缺陷 | **CUST-1 缺少备注列表** — 客户详情页展示关联备注 | CUST-1 | customer-detail |
| G20 | 契约缺陷 | **COACH-1 缺少备注列表** — 助教详情页展示关联备注 | COACH-1 | coach-detail |
| G21 | 契约缺陷 | **CUST-2 缺少月度统计汇总** — 前端需要 monthCount/monthHours当前本地计算 | CUST-2 | customer-service-records |
| G22 | 契约缺陷 | **BOARD-3 isDown/isFlat 布尔标记未定义** — 前端用于控制环比箭头方向和颜色 | BOARD-3 | board-finance |
| G23 | 前端 Bug | **customer-detail 的 onStartChat 未传 customerId** — chat 页面无法关联客户上下文 | CHAT | customer-detail → chat |
| G24 | 前端 Bug | **customer-detail 的 loadDetail() 中 id 解析方式有 Bug** — 应从 onLoad(options) 获取,而非 `__route__` 解析 | CUST-1 | customer-detail |
| G25 | 设计缺陷 | **BOARD-2 potentialTags 标签结构契约缺失** — potential 维度展示客户潜力标签(含 text+theme | BOARD-2 | board-customer |
| G26 | 设计缺陷 | **COACH-1 tierNodes/maxHours 缺失** — 绩效进度条组件需要档位节点数据,当前硬编码 `[0, 100, 130, 160, 190, 220]` | COACH-1 | coach-detail |
### 🟡 低严重度(不阻塞联调,可后续优化)
| # | 类别 | Gap 描述 | 涉及接口 | 涉及页面 |
|---|------|---------|---------|---------|
| G27 | 设计决策 | **BOARD-1 无分页定义** — 当前全量返回,助教数量少(<50可接受 | BOARD-1 | board-coach |
| G28 | 设计决策 | **BOARD-3 帮助图标内容未定义** — 12 个指标解释文案,可硬编码在前端 | BOARD-3 | board-finance |
| G29 | 设计决策 | **customer-service-records 首次加载全量 vs 按月分页** — 数据量小时可接受,量大时需改为按月请求 | CUST-2 | customer-service-records |
| G30 | 类型不匹配 | **mock-data.ts ConsumptionRecord 结构过于简单** — 仅 6 个字段customer-detail 内联 mock 有 16 个字段(含嵌套 coaches 数组) | CUST-1 | customer-detail |
| G31 | 契约缺陷 | **CUST-2 tables 台桌列表** — 契约定义了但前端未使用(前端模拟台号) | CUST-2 | customer-service-records |
---
## 行动建议
### 优先级 1更新 API 契约(阻塞后端开发)
以下接口的契约需要**完全重写**,以匹配前端实际需求:
1. **BOARD-3 财务看板** — 从扁平 metrics 改为 6 板块嵌套结构,补充赠送卡矩阵、收入结构表、助教分析子表、现金流出子分组的完整字段定义
2. **BOARD-1 助教看板** — 从 10 个通用字段扩展为 20 个含 4 维度专属字段,修正 skills 类型,补充 topCustomers
3. **BOARD-2 客户看板** — 从 8 个通用字段扩展为 40+ 维度专属字段,补充 assistants/weeklyVisits/coachDetails/potentialTags
4. **COACH-1 助教详情** — 补充 income/historyMonths/tierNodes/备注列表,扩展 topCustomers 字段
5. **CUST-1 客户详情** — 补充 aiInsight/coachTasks/favoriteCoaches/备注列表,重写消费记录为嵌套结构
### 优先级 2修复前端 Bug联调前必须完成
1. G11: customer-detail → customer-service-records 传 customerId
2. G12: coach-detail onTaskItemTap 改用 id 而非 name
3. G23: customer-detail → chat 传 customerId
4. G24: customer-detail loadDetail() 修复 id 解析
5. F1-F6: 看板筛选变更触发重新请求(八¾节已记录)
### 优先级 3统一 mock-data.ts 类型定义
当前 `mock-data.ts` 中的类型BoardFinanceData/CustomerCard/CoachCard/CustomerDetail与页面内联 mock 数据严重脱节。建议:
- 将页面内联的完整接口定义CoachItem/CustomerItem/CoachDetail 等)提取到 `mock-data.ts` 或独立的 `types.ts`
- 更新 `api.ts` 中各函数的返回类型
- 确保类型定义与更新后的 API 契约一致

View File

@@ -0,0 +1,826 @@
# 小程序 Storyboard 走查报告 — 助教视角(小燕)
> 走查日期2026-03-18
> 走查角色:助教"小燕"已登录status=approved
> 对照文档:`docs/miniprogram-dev/API-contract.md`(契约)、`docs/prd/Neo_Specs/NS1-xcx-backend-api.md`spec
> 目标:提取 spec 未记录的接口需求细节
---
## 场景 1登录 → 任务列表app.ts
### API 调用
- `checkAuthStatus()``GET /api/xcx/me` → 返回 `ApiUserInfo`
- 参数来源token 从 `wx.getStorageSync('token')` 读取
- 成功后根据 `data.status` 路由:`approved``reLaunch('/pages/task-list/task-list')`
### 前端期望字段(/api/xcx/me 响应)
| 字段 | 类型 | 说明 | 契约定义 |
|------|------|------|---------|
| `user_id` | number | 用户 ID | ✅ |
| `status` | enum | `new/pending/approved/rejected/disabled` | ✅ |
| `nickname` | string | 昵称 | ✅ |
| `role` | string | 角色 | ✅ REQ-4 |
| `store_name` | string | 门店名 | ✅ REQ-4 |
| `coach_level` | string? | 助教等级 | ✅ REQ-4 |
| `avatar` | string? | 头像 URL | ✅ REQ-4 |
### globalData 结构
```typescript
globalData: {
token?: string
refreshToken?: string
authUser?: { userId: number, status: string, nickname: string }
}
```
### spec 未记录的 Gap
- **GAP-01**`globalData.authUser` 未存储 `role``store_name``coach_level``avatar`但下游页面task-list banner、performance banner、performance-records banner需要这些字段。前端需扩展 `globalData.authUser` 或各页面单独请求 `/me`
---
## 场景 2任务列表页task-list.ts
### API 调用
- `fetchTasks(params?)``GET /api/xcx/tasks?status=&page=&pageSize=`
- 返回:`{ tasks: Task[], performance: PerformanceData, total: number, hasMore: boolean }`
- 页面加载时无参数调用(获取全部任务)
### 前端期望字段 — Task任务卡片
| 字段 | 类型 | 说明 | 契约 TASK-1 |
|------|------|------|------------|
| `id` | string | 任务 ID | ✅ |
| `customerName` | string | 客户姓名 | ✅ |
| `customerAvatar` | string | 客户头像 | ✅ |
| `taskType` | enum | `callback/priority_recall/relationship/high_priority` | ✅ |
| `taskTypeLabel` | string | 类型中文标签 | ✅ |
| `deadline` | string | 截止日期 ISO 8601 | ✅ |
| `heartScore` | number | 爱心评分 0-10 | ✅ |
| `hobbies` | string[] | 爱好标签 | ✅ |
| `isPinned` | boolean | 是否置顶 | ✅ |
| `hasNote` | boolean | 是否有备注 | ✅ |
| `status` | enum | `pending/completed/abandoned` | ✅ |
### 前端期望字段 — PerformanceData绩效概览卡片
| 字段 | 类型 | 说明 | 契约 TASK-1 performance | 前端实际使用 |
|------|------|------|------------------------|-------------|
| `monthlyIncome` | number | 月收入 | ❌ 契约仅有 `total_income` | `buildPerfData()` 用 |
| `incomeChange` | number | 收入变化百分比 | ❌ 契约无 | `incomeTrend` 展示 |
| `currentTier` | string | 当前档位名称 | ❌ 契约无 | `bannerTitle` |
| `nextTierGap` | number | 距下一档差距 | ❌ 契约无 | `remainHours` |
| `todayServiceCount` | number | 今日服务次数 | ❌ 契约无 | 未使用 |
| `weekServiceCount` | number | 本周服务次数 | ❌ 契约无 | 未使用 |
| `monthServiceCount` | number | 本月服务次数 | ❌ 契约无 | 未使用 |
### 前端期望字段 — buildPerfData() 需要后端返回的绩效字段
| 字段 | 类型 | 说明 | 契约/spec 定义 |
|------|------|------|---------------|
| `tierNodes` | number[] | 档位节点数组,如 `[0,100,130,160,190,220]` | ❌ 未定义 |
| `totalHours` | number | 当月总工时(折算后) | 契约有 `total_hours` |
| `basicHours` | number | 基础课时 | ❌ 未定义 |
| `bonusHours` | number | 激励课时 | ❌ 未定义 |
| `currentTier` | number | 当前档位索引 | ❌ 未定义 |
| `nextTierHours` | number | 下一档位工时阈值 | ❌ 未定义 |
| `tierCompleted` | boolean | 是否已达标(满档) | ❌ 未定义 |
| `bonusMoney` | number | 升档奖金 | ❌ 未定义 |
| `incomeFormatted` | string | 格式化收入 | 契约有 `total_income` |
| `incomeTrend` | string | 收入趋势(如 `↓368` | ❌ 未定义 |
| `incomeTrendDir` | enum | `up/down` | ❌ 未定义 |
| `incomeMonth` | string | 当前月份标签 | 契约有 `month_label` |
| `prevMonth` | string | 上月标签 | ❌ 未定义 |
### enrichTask() 需要后端返回的扩展字段
| 字段 | 类型 | 说明 | 契约/spec 定义 |
|------|------|------|---------------|
| `lastVisitDays` | number | 距上次到店天数 | ❌ 前端 mock 计算,后端需返回 |
| `balance` | number | 客户余额 | ❌ 未定义在 TASK-1 |
| `aiSuggestion` | string | AI 建议摘要 | ❌ 未定义在 TASK-1 |
### 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 点击任务卡片 | `/pages/task-detail/task-detail` | `?id={taskId}` |
| 点击绩效卡片 | `/pages/performance/performance` | 无参数 |
| 长按→AI助手 | `/pages/ai-chat/ai-chat` | `?taskId={id}&customerName={name}` |
### 写操作
| 操作 | API | 请求体 | 成功后行为 |
|------|-----|--------|-----------|
| 长按→置顶/取消置顶 | `POST /tasks/{id}/pin``/unpin` | 无 | 本地更新 `isPinned`,重新分组 |
| 长按→放弃 | `POST /tasks/{id}/abandon` | `{ reason: string }` | 本地移至 abandonedTasks |
| 长按→取消放弃 | `POST /tasks/{id}/restore` | 无 | 本地移至 normalTasks |
| 长按→添加备注 | `POST /notes` | `{ content, taskId?, customerId? }` | Toast 提示 |
### spec 未记录的 Gap
- **GAP-02**:契约 TASK-1 `performance` 仅有 4 个字段(`total_hours/total_income/total_customers/month_label`),但前端 `buildPerfData()` 需要 15+ 个字段(`tierNodes``basicHours``bonusHours``currentTier``nextTierHours``tierCompleted``bonusMoney``incomeTrend``incomeTrendDir``prevMonth` 等)。这是最大的 Gap。
- **GAP-03**`enrichTask()` 需要 `lastVisitDays`(距上次到店天数)、`balance`(客户余额)、`aiSuggestion`AI 建议),这 3 个字段在 TASK-1 契约中未定义。后端需在任务列表 item 中附带这些字段,或前端改为进入详情页后再加载。
- **GAP-04**`pin/unpin` 操作的 API 端点在契约中未定义。spec 提到已实现 `pin/unpin`,但 `api.ts` 中无对应函数,前端仅做本地状态更新。需确认 API 路径(`POST /tasks/{id}/pin``POST /tasks/{id}/unpin`)。
- **GAP-05**`createNote``score` 参数 — 前端备注弹窗 `onNoteConfirm` 传递 `{ score, content }`,但 `api.ts``createNote` 签名仅有 `{ content, taskId?, customerId? }`,缺少 `score` 字段。
---
## 场景 3任务详情页task-detail.ts
### API 调用
- `fetchTaskDetail(taskId)``GET /api/xcx/tasks/{taskId}`
- 参数来源:`options.id`(从 task-list 跳转传入)
- `deleteNote(noteId)``DELETE /api/xcx/notes/{noteId}`
### 前端期望字段 — TaskDetail完整清单
#### 基础信息(继承 Task
同 TASK-1 item 字段,契约已覆盖。
#### 维客线索 retentionClues内联 mock8 条)
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `tag` | string | 线索大类标签(如 `"客户\n基础"` | ❌ 契约用 `tag` 但格式不同 |
| `tagColor` | enum | `primary/success/purple/error` | ✅ `tag_color` |
| `emoji` | string | 表情符号 | ✅ |
| `text` | string | 线索摘要 | ✅ |
| `source` | string | 来源(如 `"By:系统"``"By:小燕"` | ⚠️ 契约有 `source` 但格式不同 |
| `desc` | string? | 详细描述(展开后显示) | ✅ |
| `expanded` | boolean | 展开状态(前端本地) | — |
#### 话术参考 talkingPoints内联 mock5 条)
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `talkingPoints` | string[] | 话术文本数组 | ✅ |
#### 服务记录 serviceRecords内联 mock4 条)
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `table` | string | 台桌号 | ✅ |
| `type` | string | 课程类型标签 | ✅ |
| `typeClass` | enum | `basic/vip/tip/recharge/incentive` | ✅ `type_class` |
| `recordType` | enum? | `course/recharge` | ✅ `record_type` |
| `duration` | number | 折算后课时(小时) | ✅ |
| `durationRaw` | number? | 折算前课时(小时) | ✅ `duration_raw` |
| `income` | number | 收入(元) | ✅ |
| `isEstimate` | boolean? | 是否预估金额 | ✅ `is_estimate` |
| `drinks` | string | 酒水描述 | ✅ |
| `date` | string | 日期(格式化后) | ✅ |
#### 服务汇总 serviceSummary
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `totalHours` | number | 总工时 | ✅ |
| `totalIncome` | number | 总收入 | ✅ |
| `avgIncome` | number | 平均收入 | ✅ |
#### AI 分析 aiAnalysis
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `summary` | string | AI 分析摘要 | ✅ |
| `suggestions` | string[] | AI 建议列表 | ✅ |
#### 备注 notes内联 mock5 条)
| 字段 | 类型 | 说明 | 契约 TASK-2 |
|------|------|------|------------|
| `id` | string | 备注 ID | ✅ |
| `content` | string | 备注内容 | ✅ |
| `tagType` | enum | `customer/coach/system` | ✅ `tag_type` |
| `tagLabel` | string | 标签文案 | ✅ `tag_label` |
| `createdAt` | string | 创建时间 | ✅ `created_at` |
| `score` | number? | 满意度评分 0-10 | ✅ |
### 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 问问助手 | `/pages/chat/chat` | `?customerId={detail.id}` |
| 查看全部服务记录 | `/pages/customer-service-records/customer-service-records` | `?customerId={detail.id}` |
### 写操作
| 操作 | API | 请求体 | 成功后行为 |
|------|-----|--------|-----------|
| 添加备注 | `POST /notes` | `{ content }` | 本地 prepend 到 sortedNotes |
| 删除备注 | `DELETE /notes/{noteId}` | 无 | 本地 filter 移除 |
| 放弃任务 | `POST /tasks/{id}/abandon` | `{ reason }` | 更新 `detail.status='abandoned'` |
| 取消放弃 | `POST /tasks/{id}/restore` | 无 | 更新 `detail.status='pending'` |
### spec 未记录的 Gap
- **GAP-06**:维客线索的 `tag` 字段格式不一致 — 前端内联 mock 使用 `"客户\n基础"` 格式(含换行符),契约定义为普通 string。后端需明确 tag 的取值枚举和格式。
- **GAP-07**:维客线索的 `source` 字段格式不一致 — 前端显示 `"By:系统"``"By:小燕"`,但 mock-data.ts 中 `RetentionClue.source` 定义为 `'manual' | 'ai_consumption' | 'ai_note'`。前端内联 mock 与类型定义不匹配,后端需明确返回格式。
- **GAP-08**`aiAnalysis` 数据来源 — spec §3.2 提到来自 `biz.ai_cache`app4/app5/app6/app7但契约未说明 `cache_type` 映射关系。需明确:`ai_analysis.summary` 来自哪个 `cache_type``talking_points` 来自哪个 `cache_type`
- **GAP-09**task-detail 跳转 chat 时传 `customerId={detail.id}`,但 `detail.id` 实际是 **taskId** 不是 customerId。前端需要一个 `customerId` 字段TASK-2 响应中需包含 `customer_id`)。
- **GAP-10**task-detail 跳转 customer-service-records 时同样传 `customerId={detail.id}`,存在同样的 taskId/customerId 混淆问题。TASK-2 响应需包含 `customer_id`
- **GAP-11**`storageLevel`(储值等级,如"非常多")和 `relationLevel/relationLevelText/relationColor`(关系等级)在前端本地计算,但后端未定义这些字段。后端是否需要返回,还是前端根据 `heartScore` 自行计算?
---
## 场景 4绩效概览页performance.ts
### API 调用
- `fetchPerformanceOverview({ year, month })``GET /api/xcx/performance?year=&month=`
- 参数来源:`new Date()` 取当前年月
- 返回类型:`PerformanceData`mock-data.ts 定义)
### 前端期望字段 — 完整清单(对比契约 PERF-1
#### Banner 数据(内联 mock非 API 返回)
| 字段 | 类型 | 说明 | 契约 PERF-1 | 数据源 |
|------|------|------|------------|--------|
| `coachName` | string | 助教姓名 `'小燕'` | ❌ 未定义 | 应从 globalData.authUser 或 /me |
| `coachRole` | string | 角色 `'助教'` | ❌ 未定义 | 同上 |
| `storeName` | string | 门店名 `'广州朗朗桌球'` | ❌ 未定义 | 同上 |
| `monthlyIncome` | string | 本月收入 `'¥6,206'` | ⚠️ 契约有 `total_income` 但是 number | 需格式化 |
| `lastMonthIncome` | string | 上月收入 `'¥16,880'` | ❌ 未定义 | 需后端额外返回 |
#### 收入档位(内联 mock
| 字段 | 类型 | 说明 | 契约 PERF-1 |
|------|------|------|------------|
| `currentTier.basicRate` | number | 当前档基础费率 80 | ❌ 未定义 |
| `currentTier.incentiveRate` | number | 当前档激励费率 95 | ❌ 未定义 |
| `nextTier.basicRate` | number | 下一档基础费率 90 | ❌ 未定义 |
| `nextTier.incentiveRate` | number | 下一档激励费率 114 | ❌ 未定义 |
| `upgradeHoursNeeded` | number | 距升档所需工时 15 | ❌ 未定义 |
| `upgradeBonus` | number | 升档奖金 800 | ❌ 未定义 |
#### 收入明细 incomeItems内联 mock4 项)
| 项目 | icon | desc 格式 | 契约 PERF-1 |
|------|------|----------|------------|
| 基础课 | 🎱 | `80元/h × 75h` | ⚠️ 契约有 `income_items` 但字段不同 |
| 激励课 | ⭐ | `95.05元/h × 10h` | 同上 |
| 充值激励 | 💰 | `客户充值返佣` | 同上 |
| TOP3 销冠奖 | 🏆 | `全店业绩前三名奖励` | 同上 |
契约 `income_items` 定义:`Array<{ label, amount, icon }>`,缺少 `desc` 字段。
#### 服务记录 thisMonthRecords内联 mock按日期分组
| 字段 | 类型 | 说明 | 契约 PERF-1 |
|------|------|------|------------|
| `date` | string | 日期标签 `'2月7日'` | ❌ 契约 `this_month_records` 结构完全不同 |
| `totalHours` | string | 当日总工时 `'4.0h'` | ❌ |
| `totalIncome` | string | 当日总收入 `'¥350'` | ❌ |
| `records[].customerName` | string | 客户名 | ⚠️ 契约有 `customer_name` |
| `records[].avatarChar` | string | 头像首字 | ❌ |
| `records[].avatarColor` | string | 头像颜色 | ❌ |
| `records[].timeRange` | string | 时间段 `'20:00-22:00'` | ❌ |
| `records[].hours` | string | 工时 `'2.0h'` | ⚠️ 契约有 `hours` 但是 number |
| `records[].courseType` | string | 课程类型 | ❌ |
| `records[].courseTypeClass` | string | 样式类 | ❌ |
| `records[].location` | string | 台号 | ❌ |
| `records[].income` | string | 收入 `'¥160'` | ⚠️ 契约有 `income` 但是 number |
#### 新客列表 newCustomers
| 字段 | 类型 | 说明 | 契约 PERF-1 |
|------|------|------|------------|
| `name` | string | 客户名 | ✅ |
| `avatarChar` | string | 头像首字 | ❌ 契约有 `avatar_char` |
| `avatarColor` | string | 头像颜色 | ❌ 未定义 |
| `lastService` | string | 最后服务日期 | ❌ 未定义 |
| `count` | number | 服务次数 | ❌ 未定义 |
#### 常客列表 regularCustomers
| 字段 | 类型 | 说明 | 契约 PERF-1 |
|------|------|------|------------|
| `name` | string | 客户名 | ✅ |
| `avatarChar` | string | 头像首字 | ❌ 契约有 `avatar_char` |
| `avatarColor` | string | 头像颜色 | ❌ 未定义 |
| `hours` | number | 总工时 | ❌ 未定义 |
| `income` | string | 总收入 | ❌ 未定义 |
| `count` | number | 到店次数 | ⚠️ 契约有 `visits` |
### 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 查看全部记录 | `/pages/performance-records/performance-records` | 无参数 |
| 点击客户卡片 | `/pages/task-detail/task-detail` | `?customerName={name}` |
| 点击服务记录 | `/pages/task-detail/task-detail` | `?customerName={name}&taskId={taskId}` |
| 点击收入概览 | `/pages/performance-records/performance-records` | 无参数 |
### spec 未记录的 Gap
- **GAP-12**:契约 PERF-1 `this_month_records` 是扁平数组 `Array<{ customer_name, hours, income, date }>`,但前端需要 **按日期分组的 DateGroup 结构**(含 `date``totalHours``totalIncome``records[]`),且每条记录需要 `timeRange``courseType``courseTypeClass``location` 等字段。契约需大幅扩展。
- **GAP-13**:收入档位数据(`currentTier``nextTier``upgradeHoursNeeded``upgradeBonus`)在契约 PERF-1 中完全未定义。这些数据来自 `dws_assistant_salary_calc`,后端需返回。
- **GAP-14**`lastMonthIncome`(上月收入)在契约中未定义,前端 Banner 需要展示。
- **GAP-15**`incomeItems``desc` 字段(如 `"80元/h × 75h"`)在契约中未定义。后端需返回费率和工时的拆分数据,或直接返回格式化的 desc 字符串。
- **GAP-16**`newCustomers` 缺少 `lastService`(最后服务日期)和 `count`(服务次数)字段;`regularCustomers` 缺少 `hours`(总工时)和 `income`(总收入)字段。
- **GAP-17**:跳转 task-detail 时传 `customerName` 而非 `taskId`,但 task-detail 页面 `onLoad` 读取的是 `options.id`。参数传递不匹配,联调时会导致页面无法加载正确数据。
- **GAP-18**页面无月份切换功能F8API 支持 `year/month` 参数但页面固定当前月。
---
## 场景 5绩效明细页performance-records.ts
### API 调用
- `fetchPerformanceRecords({ year, month })``GET /api/xcx/performance/records?year=&month=&page=&pageSize=`
- 参数来源:`currentYear`/`currentMonth`(页面 data支持月份切换
- Banner 数据从 `getApp().globalData.authUser` 读取
### 前端期望字段 — summary
| 字段 | 类型 | 说明 | 契约 PERF-2 |
|------|------|------|------------|
| `totalCount` | number | 总笔数 32 | ✅ `total_count` |
| `totalHours` | number | 总工时 59.0 | ✅ `total_hours` |
| `totalHoursRaw` | number | 折算前总工时 63.5 | ✅ `total_hours_raw` |
| `totalIncome` | number | 总收入 4720 | ✅ `total_income` |
### 前端期望字段 — dateGroups按日期分组
| 字段 | 类型 | 说明 | 契约 PERF-2 |
|------|------|------|------------|
| `date` | string | 日期标签 `'2月7日'` | ✅ |
| `totalHours` | number | 当日总工时 | ✅ |
| `totalIncome` | number | 当日总收入 | ✅ |
| `records[].id` | string | 记录 ID | ✅ |
| `records[].customerName` | string | 客户名 | ✅ |
| `records[].avatarChar` | string | 头像首字 | ❌ 未定义 |
| `records[].avatarColor` | string | 头像颜色 | ❌ 未定义 |
| `records[].timeRange` | string | 时间段 | ✅ |
| `records[].hours` | number | 折算后工时 | ✅ |
| `records[].hoursRaw` | number? | 折算前工时 | ✅ |
| `records[].courseType` | string | 课程类型 | ✅ |
| `records[].courseTypeClass` | string | 样式类 | ✅ |
| `records[].location` | string | 台号 | ✅ |
| `records[].income` | number | 收入 | ✅ |
### courseTypeClass 完整枚举
前端使用的样式类:`tag-basic`(基础课)、`tag-vip`(包厢课)、`tag-tip`(打赏课)。
契约定义的 `course_type_class`:未明确枚举值。
### spec 未记录的 Gap
- **GAP-19**`avatarChar`(头像首字)和 `avatarColor`(头像颜色)在契约 PERF-2 中未定义。前端通过 `nameToAvatarColor()` 工具函数从姓名生成,但后端是否需要返回?建议前端自行计算。
- **GAP-20**`courseTypeClass` 的完整枚举值未在契约中明确。前端使用 `tag-basic`/`tag-vip`/`tag-tip`,但后端返回的是 `basic`/`vip`/`tip` 还是带 `tag-` 前缀?需统一。
- **GAP-21**月份切换时未重置分页F9 bug`switchMonth()``page` 未重置为 1。
- **GAP-22**Banner 字段 `coachName`/`coachLevel`/`storeName``globalData.authUser` 读取,但 `authUser` 当前未存储 `coachLevel``storeName`(见 GAP-01
---
## 场景 6客户详情页customer-detail.ts
### API 调用
- `fetchCustomerDetail(customerId)``GET /api/xcx/customers/{customerId}`
- 参数来源:页面路由参数(但当前代码从 `__route__` 解析,有 bug
### 前端期望字段 — 完整清单
#### 客户基本信息(内联 mock
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `id` | string | 客户 ID | ✅ |
| `name` | string | 客户名 | ✅ |
| `avatarChar` | string | 头像首字 | ❌ 未定义 |
| `phone` | string | 手机号(完整) | ⚠️ 契约有 `phone`(脱敏)和 `phone_full`(完整) |
| `balance` | string | 余额 `'8,600'` | ❌ 未定义 |
| `consumption60d` | string | 近60天消费 `'2,800'` | ❌ 未定义 |
| `idealInterval` | string | 理想到店间隔 `'7天'` | ❌ 未定义 |
| `daysSinceVisit` | string | 距上次到店 `'12天'` | ❌ 未定义 |
#### AI 洞察 aiInsight内联 mock
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `summary` | string | AI 分析摘要 | ❌ 未定义 |
| `strategies` | Array<{color, text}> | 策略建议列表 | ❌ 未定义 |
#### 维客线索 clues内联 mock7 条)
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `category` | string | 线索大类(含换行符) | ⚠️ 契约说"同 TASK-2 格式" |
| `categoryColor` | string | 颜色 | ⚠️ |
| `text` | string | 含 emoji 的摘要 | ⚠️ |
| `source` | string | 来源 `'系统'`/`'小燕'` | ⚠️ |
| `detail` | string? | 详细描述 | ⚠️ |
#### 关联助教 coachTasks内联 mock4 位助教)
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `name` | string | 助教名 | ❌ 完全未定义 |
| `level` | string | 等级 `senior/middle/junior` | ❌ |
| `levelColor` | string | 等级颜色 | ❌ |
| `taskType` | string | 任务类型标签 | ❌ |
| `taskColor` | string | 任务颜色 | ❌ |
| `bgClass` | string | 背景样式类 | ❌ |
| `status` | string | 状态 `normal/pinned/abandoned` | ❌ |
| `lastService` | string | 最后服务 `'02-20 21:30 · 2.5h'` | ❌ |
| `metrics` | Array<{label,value,color}> | 指标近60天次数/总时长/次均时长) | ❌ |
#### 最亲密助教 favoriteCoaches内联 mock2 位)
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `emoji` | string | 爱心 emoji | ❌ 完全未定义 |
| `name` | string | 助教名 | ❌ |
| `relationIndex` | string | 关系指数 `'9.2'` | ❌ |
| `indexColor` | string | 指数颜色 | ❌ |
| `bgClass` | string | 背景样式类 | ❌ |
| `stats` | Array<{label,value,color}> | 统计(基础/激励/上课/充值) | ❌ |
#### 消费记录 consumptionRecords内联 mock3 条3 种类型)
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `id` | string | 记录 ID | ✅ |
| `type` | enum | `table/shop/recharge` | ⚠️ 契约有 `type` 但枚举不同 |
| `date` | string | 日期 | ✅ |
| `tableName` | string? | 台桌名 | ❌ 契约有 `table` |
| `startTime` | string? | 开始时间 | ❌ 未定义 |
| `endTime` | string? | 结束时间 | ❌ 未定义 |
| `duration` | string? | 时长 `'3h 20min'` | ⚠️ 契约有 `duration` 但是 number |
| `tableFee` | number? | 台费 | ❌ 未定义 |
| `tableOrigPrice` | number? | 台费原价 | ❌ 未定义 |
| `coaches` | Array<{name,level,levelColor,courseType,hours,perfHours?,fee}> | 助教服务明细 | ❌ 完全未定义 |
| `foodAmount` | number? | 酒水金额 | ❌ 未定义 |
| `foodOrigPrice` | number? | 酒水原价 | ❌ 未定义 |
| `totalAmount` | number? | 总金额 | ⚠️ 契约有 `amount` |
| `totalOrigPrice` | number? | 总原价 | ❌ 未定义 |
| `payMethod` | string? | 支付方式 | ❌ 未定义 |
| `rechargeAmount` | number? | 充值金额 | ❌ 未定义 |
#### 备注 sortedNotes内联 mock3 条)
| 字段 | 类型 | 说明 | 契约 CUST-1 |
|------|------|------|------------|
| `id` | string | 备注 ID | ❌ 契约无备注字段 |
| `tagLabel` | string | 标签 | ❌ |
| `createdAt` | string | 创建时间 | ❌ |
| `content` | string | 内容 | ❌ |
### 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 查看服务记录 | `/pages/customer-service-records/customer-service-records` | 无参数(⚠️ 未传 customerId |
| 问问助手 | `/pages/chat/chat` | 无参数(⚠️ 未传 customerId |
### spec 未记录的 Gap
- **GAP-23**`balance`(余额)、`consumption60d`近60天消费`idealInterval`(理想到店间隔)、`daysSinceVisit`(距上次到店天数)在契约 CUST-1 中完全未定义。这些是客户详情页 Banner 的核心展示字段。
- **GAP-24**`aiInsight`AI 洞察summary + strategies在契约 CUST-1 中完全未定义。数据来源应为 `biz.ai_cache`,但 `cache_type` 未明确(可能是 `app4_analysis` 或新增类型)。
- **GAP-25**`coachTasks`(关联助教任务列表)在契约 CUST-1 中完全未定义。这是一个复杂的数据结构,需要从 `biz.coach_tasks` + `fdw_etl.v_dwd_assistant_service_log` 聚合。每位助教需返回任务类型、状态、最后服务时间、近60天服务次数/总时长/次均时长。
- **GAP-26**`favoriteCoaches`(最亲密助教)在契约 CUST-1 中完全未定义。需要从 `fdw_etl.v_dws_member_assistant_relation_index` 获取关系指数,并聚合基础课时/激励课时/上课次数/充值金额。
- **GAP-27**:消费记录的 `coaches` 子数组(助教服务明细,含 `perfHours` 折算工时)在契约中完全未定义。这需要从 `fdw_etl.v_dwd_assistant_service_log` 按结算单关联查询。
- **GAP-28**:消费记录的 `tableFee`/`tableOrigPrice`/`foodAmount`/`foodOrigPrice`/`totalOrigPrice`/`payMethod` 等拆分字段在契约中未定义。契约仅有 `amount`(总金额)。
- **GAP-29**:消费记录的 `type` 枚举不一致 — 前端使用 `table/shop/recharge`3 种),契约使用 `type_class`(未明确枚举)。
- **GAP-30**:备注列表在契约 CUST-1 中未定义。前端需要展示客户相关的备注。
- **GAP-31**:跳转 customer-service-records 和 chat 时未传 `customerId` 参数,联调时目标页面无法获取客户 ID。
---
## 场景 7客户服务记录页customer-service-records.ts
### API 调用
- `fetchCustomerRecords({ customerId, year?, month?, table? })``GET /api/xcx/customers/{customerId}/records`
- 参数来源:`options.customerId``options.id`
- 当前实现:首次加载拉取全部记录,月份切换仅本地过滤
### 前端期望字段 — ServiceRecord
| 字段 | 类型 | 说明 | 契约 CUST-2 |
|------|------|------|------------|
| `table` | string | 台桌号 | ✅ |
| `type` | string | 课程类型标签 | ✅ |
| `typeClass` | enum | `basic/vip/tip/recharge` | ✅ `type_class` |
| `recordType` | enum | `course/recharge` | ❌ 未定义 |
| `duration` | number | 折算后小时 | ✅ |
| `durationRaw` | number | 折算前小时 | ✅ `duration_raw` |
| `income` | number | 到手金额 | ✅ |
| `isEstimate` | boolean | 是否预估 | ❌ 未定义 |
| `drinks` | string | 饮品描述 | ✅ |
| `date` | string | 显示日期 | ✅ |
### 页面头部字段
| 字段 | 类型 | 说明 | 契约 CUST-2 |
|------|------|------|------------|
| `customerName` | string | 客户名 | ✅ |
| `customerPhone` | string | 脱敏手机号 | ✅ `customer_phone` |
| `customerPhoneFull` | string | 完整手机号 | ❌ 未定义(需从 CUST-1 获取) |
| `relationIndex` | string | 关系指数 | ✅ |
| `totalServiceCount` | number | 累计服务次数 | ❌ 未定义 |
| `monthCount` | string | 当月次数 | ❌ 需前端计算 |
| `monthHours` | string | 当月工时 | ❌ 需前端计算 |
| `monthRelation` | string | 当月关系指数 | ❌ 未定义 |
### spec 未记录的 Gap
- **GAP-32**`recordType``course/recharge`)和 `isEstimate`(是否预估)在契约 CUST-2 中未定义。
- **GAP-33**`customerPhoneFull`(完整手机号)在 CUST-2 响应中未定义,前端需要点击查看完整号码。
- **GAP-34**`totalServiceCount`(累计服务次数)在 CUST-2 中未定义。
- **GAP-35**月份切换采用本地筛选F10联调后数据量大时需改为按月请求 API。
---
## 场景 8备注列表页notes.ts
### API 调用
- `fetchNotes({ page?, pageSize? })``GET /api/xcx/notes?page=&pageSize=`
- `deleteNote(noteId)``DELETE /api/xcx/notes/{noteId}`
### 前端期望字段 — Note
| 字段 | 类型 | 说明 | 契约 NOTE-1 |
|------|------|------|------------|
| `id` | string | 备注 ID | ✅ |
| `content` | string | 备注内容 | ✅ |
| `tagType` | enum | `customer/coach/system` | ✅ `tag_type` |
| `tagLabel` | string | 标签文案 | ✅ `tag_label` |
| `createdAt` | string | 创建时间 | ✅ `created_at` |
| `score` | number? | 满意度评分 | ✅ |
| `timeLabel` | string | 格式化时间(前端计算) | — |
### 写操作
| 操作 | API | 成功后行为 |
|------|-----|-----------|
| 删除备注 | `DELETE /notes/{noteId}` | 本地 filter 移除 + Toast |
### spec 未记录的 Gap
- **GAP-36**无触底加载逻辑F11API 支持分页但 `onReachBottom()` 未实现。
- **GAP-37**`tagType` 枚举 — mock 数据中有 `'system'` 类型,但契约 NOTE-1 仅定义 `customer/coach/system`。需确认 `system` 类型的备注由谁创建、如何展示。
---
## 场景 9助教详情页coach-detail.ts
### API 调用
- `fetchCoachDetail(coachId)``GET /api/xcx/coaches/{coachId}`
- 参数来源:`options.id`
- 返回类型:`CoachCard`api.ts 定义),但前端实际需要远超 `CoachCard` 的数据
### 前端期望字段 — CoachDetail完整清单
#### 基本信息
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `id` | string | 助教 ID | ✅ |
| `name` | string | 助教名 | ✅ |
| `avatar` | string | 头像 | ✅ |
| `level` | string | 等级 `'星级'` | ✅ |
| `skills` | string[] | 技能 | ✅ |
| `workYears` | number | 工龄 3 | ❌ 未定义 |
| `customerCount` | number | 客户数 68 | ❌ 未定义 |
| `hireDate` | string | 入职日期 | ❌ 未定义 |
| `store_name` | string | 门店名 | ✅ |
#### 绩效指标 performance6 个指标)
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `monthlyHours` | number | 本月工时 87.5 | ❌ 未定义 |
| `monthlySalary` | number | 本月工资 6950 | ❌ 未定义 |
| `customerBalance` | number | 客源储值余额 86200 | ❌ 未定义 |
| `tasksCompleted` | number | 任务完成数 38 | ❌ 未定义 |
| `perfCurrent` | number | 当前绩效 80 | ❌ 未定义 |
| `perfTarget` | number | 目标绩效 100 | ❌ 未定义 |
#### 绩效指标卡片 perfCards4 张)
| 卡片 | 需要字段 | 契约 |
|------|---------|------|
| 本月定档业绩 | `monthlyHours` + 折算前工时 | ❌ |
| 本月工资(预估) | `monthlySalary` | ❌ |
| 客源储值余额 | `customerBalance` + `customerCount` | ❌ |
| 本月任务完成 | `tasksCompleted` + 覆盖客户数 | ❌ |
#### 进度条数据
| 字段 | 类型 | 说明 | 契约 |
|------|------|------|------|
| `tierNodes` | number[] | 档位节点 `[0,100,130,160,190,220]` | ❌ 未定义 |
#### 收入明细 income本月/上月各 4 项)
| 项目 | 说明 | 契约 |
|------|------|------|
| 基础课时费 | `¥3,500` | ❌ 未定义 |
| 激励课时费 | `¥1,800` | ❌ 未定义 |
| 充值提成 | `¥1,200` | ❌ 未定义 |
| 酒水提成 | `¥450` | ❌ 未定义 |
#### 任务执行
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `visibleTasks` | TaskItem[] | 可见任务active | ⚠️ 契约说"同 TASK-1 item 格式"但实际需要更多字段 |
| `hiddenTasks` | TaskItem[] | 隐藏任务inactive | ⚠️ |
| `abandonedTasks` | AbandonedTask[] | 已放弃任务 | ⚠️ |
TaskItem 需要的字段(超出 TASK-1 item
| 字段 | 类型 | 说明 |
|------|------|------|
| `typeLabel` | string | 任务类型标签 |
| `typeClass` | string | 样式类 `high-priority/priority/relationship/callback` |
| `customerName` | string | 客户名 |
| `noteCount` | number | 备注数量 |
| `pinned` | boolean | 是否置顶 |
| `notes` | Array<{pinned?,text,date}> | 备注列表(含置顶标记) |
AbandonedTask 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `customerName` | string | 客户名 |
| `reason` | string | 放弃原因 |
#### 客户关系 TOP20 — TopCustomer
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `id` | string | 客户 ID | ✅ |
| `name` | string | 客户名 | ✅ |
| `initial` | string | 姓名首字 | ❌ |
| `avatarGradient` | string | 头像渐变色 | ❌ |
| `heartEmoji` | string | 爱心 emoji `❤️/💛/🤍` | ❌ |
| `score` | string | 关系指数 `'9.5'` | ❌ 契约有 `total_spend`/`visit_count` 但无 score |
| `scoreColor` | string | 指数颜色 | ❌ |
| `serviceCount` | number | 服务次数 | ⚠️ 契约有 `visit_count` |
| `balance` | string | 余额 `'¥8,600'` | ❌ 未定义 |
| `consume` | string | 消费 `'¥12,800'` | ⚠️ 契约有 `total_spend` |
#### 近期服务明细 — ServiceRecord
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `customerId` | string? | 客户 ID | ❌ |
| `customerName` | string | 客户名 | ⚠️ |
| `initial` | string | 姓名首字 | ❌ |
| `avatarGradient` | string | 头像渐变色 | ❌ |
| `type` | string | 课程类型 | ⚠️ |
| `typeClass` | string | 样式类 | ⚠️ |
| `table` | string | 台号 | ⚠️ |
| `duration` | string | 时长 `'2.5h'` | ⚠️ |
| `income` | string | 收入 `'¥200'` | ⚠️ |
| `date` | string | 日期时间 | ⚠️ |
| `perfHours` | string? | 折算工时 `'2h'` | ❌ 未定义 |
#### 历史月份统计 — HistoryMonth
| 字段 | 类型 | 说明 | 契约 COACH-1 |
|------|------|------|-------------|
| `month` | string | 月份标签 `'本月'`/`'上月'`/`'4月'` | ❌ 完全未定义 |
| `estimated` | boolean | 是否预估 | ❌ |
| `customers` | string | 客户数 `'22人'` | ❌ |
| `hours` | string | 工时 `'87.5h'` | ❌ |
| `salary` | string | 工资 `'¥6,950'` | ❌ |
| `callbackDone` | number | 回访完成数 | ❌ |
| `recallDone` | number | 召回完成数 | ❌ |
### 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 点击任务项 | `/pages/customer-detail/customer-detail` | `?name={customerName}` |
| 点击客户卡片 | `/pages/customer-detail/customer-detail` | `?id={customerId}` |
| 点击服务记录 | `/pages/customer-detail/customer-detail` | `?id={customerId}` |
| 查看更多服务记录 | `/pages/performance-records/performance-records` | `?coachId={coachId}` |
| 问问助手 | `/pages/chat/chat` | `?coachId={coachId}` |
### spec 未记录的 Gap
- **GAP-38**:契约 COACH-1 严重不完整。前端需要 `performance`6 个绩效指标)、`income`(本月/上月各 4 项收入明细)、`tierNodes`(档位节点数组),契约均未定义。
- **GAP-39**`TopCustomer` 需要 `heartEmoji``score`(关系指数)、`scoreColor``balance`(余额)、`consume`(消费),契约仅有 `total_spend`/`visit_count`
- **GAP-40**`ServiceRecord.perfHours`(折算工时)在契约中未定义。
- **GAP-41**`HistoryMonth`(历史月份统计)在契约中完全未定义。需要返回最近 N 个月的汇总数据(客户数、工时、工资、回访/召回完成数)。
- **GAP-42**`TaskItem.notes`(任务关联的备注列表,含置顶标记)在契约中未定义。契约的 `visible_tasks` 说"同 TASK-1 item 格式",但前端需要额外的 `noteCount``pinned``notes[]` 字段。
- **GAP-43**`AbandonedTask.reason`(放弃原因)在契约中未定义。
- **GAP-44**`workYears`(工龄)、`hireDate`(入职日期)在契约中未定义。
---
## 场景 10对话页面chat.ts + chat-history.ts
### chat.ts — API 调用
- `fetchChatMessages(customerId)``GET /api/xcx/chat/{chatId}/messages`
- ⚠️ 注意:前端传入的是 `customerId`(或 `'default'`),但 API 路径参数是 `chatId`。参数语义不匹配。
- `sendChatMessage(chatId, content)``POST /api/xcx/chat/{chatId}/messages`
- 页面参数:`options.customerId`
### chat.ts — 前端期望字段 — ChatMessage
| 字段 | 类型 | 说明 | 契约 CHAT-2 |
|------|------|------|------------|
| `id` | string | 消息 ID | ✅ |
| `role` | enum | `user/assistant` | ✅ |
| `content` | string | 消息内容 | ✅ |
| `timestamp` | string | 时间戳 | ⚠️ 契约用 `created_at` |
| `referenceCard` | object? | 引用卡片 | ❌ 完全未定义 |
| `referenceCard.type` | enum | `customer/record` | ❌ |
| `referenceCard.title` | string | 卡片标题 | ❌ |
| `referenceCard.summary` | string | 卡片摘要 | ❌ |
| `referenceCard.data` | Record<string,string> | 键值对数据 | ❌ |
### chat.ts — SSE 流式回复
- 前端 `simulateStreamOutput()` 模拟流式输出(逐字显示)
- 联调时需替换为真实 SSE 连接(`POST /api/xcx/chat/stream`
- spec R3 已确认保留 SSE 流式端点
### chat.ts — 页面参数
| 来源页面 | 传递参数 | 用途 |
|---------|---------|------|
| task-detail | `?customerId={taskId}` | ⚠️ 实际传的是 taskId |
| customer-detail | 无参数 | ⚠️ 未传 customerId |
| coach-detail | `?coachId={coachId}` | 助教 ID |
| chat-history | `?historyId={id}` | 历史对话 ID |
### chat-history.ts — API 调用
- `fetchChatHistory({ page?, pageSize? })``GET /api/xcx/chat/history`
- 返回:`{ items: ChatHistoryItem[], total, hasMore }`
### chat-history.ts — 前端期望字段 — ChatHistoryItem
| 字段 | 类型 | 说明 | 契约 CHAT-1 |
|------|------|------|------------|
| `id` | string | 对话 ID | ✅ |
| `title` | string | 对话标题 | ❌ 契约无 `title` |
| `lastMessage` | string | 最后消息 | ✅ `last_message` |
| `timestamp` | string | 时间戳 | ⚠️ 契约用 `last_time` |
| `customerName` | string? | 关联客户名 | ✅ |
### chat-history.ts — 页面跳转
| 操作 | 目标 | 参数 |
|------|------|------|
| 点击对话记录 | `/pages/chat/chat` | `?historyId={id}` |
### spec 未记录的 Gap
- **GAP-45**`ChatMessage.referenceCard`(引用卡片)在契约 CHAT-2 中完全未定义。前端需要展示客户概览卡片(含键值对数据),后端需在消息中附带结构化引用数据。
- **GAP-46**`ChatMessage.timestamp` vs 契约 `created_at` — 字段名不一致,前端使用 `timestamp`,契约定义 `created_at`
- **GAP-47**`ChatHistoryItem.title`(对话标题)在契约 CHAT-1 中未定义。契约仅有 `customer_name`/`last_message`/`last_time`/`unread_count`
- **GAP-48**`ChatHistoryItem.timestamp` vs 契约 `last_time` — 字段名不一致。
- **GAP-49**chat 页面接收 `customerId` 参数但 API 路径需要 `chatId`。前端需要一个"根据 customerId 查找或创建对话"的机制,或 API 支持 `customerId` 作为查询参数。
- **GAP-50**chat 页面还接收 `historyId`(从 chat-history 跳转)和 `coachId`(从 coach-detail 跳转),但 `loadMessages()` 仅使用 `customerId`。多种入口参数的路由逻辑未实现。
- **GAP-51**SSE 流式端点 `POST /api/xcx/chat/stream` 在契约中未定义spec R3 已确认需补充)。
---
## 汇总Gap 清单
### 一、架构级 Gap影响多个页面
| # | Gap | 严重度 | 影响页面 | 说明 |
|---|-----|--------|---------|------|
| GAP-01 | globalData.authUser 缺少 role/store_name/coach_level/avatar | 🟠 中 | task-list, performance, performance-records | 多个页面 Banner 需要这些字段,当前硬编码 |
| GAP-04 | pin/unpin API 端点未在契约中定义 | 🔴 高 | task-list | api.ts 无对应函数,前端仅本地更新 |
| GAP-05 | createNote 缺少 score 参数 | 🟠 中 | task-list, task-detail | 前端传 score 但 api.ts 签名无此字段 |
| GAP-09/10 | task-detail 跳转 chat/customer-service-records 时传 taskId 而非 customerId | 🔴 高 | task-detail → chat/customer-service-records | TASK-2 响应需包含 customer_id |
| GAP-31 | customer-detail 跳转时未传 customerId | 🔴 高 | customer-detail → chat/customer-service-records | 联调时目标页面无法获取客户 ID |
| GAP-17 | performance 跳转 task-detail 传 customerName 而非 taskId | 🟠 中 | performance → task-detail | 参数不匹配 |
### 二、TASK-1 扩展 Gap任务列表绩效卡片
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-02 | performance 字段严重不足(需 15+ 字段,契约仅 4 个) | 🔴 高 | tierNodes、basicHours、bonusHours、currentTier、nextTierHours、tierCompleted、bonusMoney、incomeTrend、incomeTrendDir、prevMonth 等 |
| GAP-03 | enrichTask 需要 lastVisitDays/balance/aiSuggestion | 🟠 中 | 任务卡片扩展字段 |
### 三、PERF-1 Gap绩效概览
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-12 | this_month_records 需要 DateGroup 分组结构 | 🔴 高 | 契约是扁平数组,前端需按日期分组 + timeRange/courseType/location |
| GAP-13 | 收入档位数据完全未定义 | 🔴 高 | currentTier/nextTier/upgradeHoursNeeded/upgradeBonus |
| GAP-14 | lastMonthIncome 未定义 | 🟠 中 | Banner 需展示上月收入 |
| GAP-15 | incomeItems.desc 未定义 | 🟠 中 | 需费率×工时的拆分描述 |
| GAP-16 | newCustomers/regularCustomers 缺少多个字段 | 🟠 中 | lastService/count/hours/income |
| GAP-18 | 页面无月份切换功能 | 🟡 低 | F8 前端修复项 |
### 四、CUST-1 Gap客户详情 — 最大 Gap 集中区)
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-23 | balance/consumption60d/idealInterval/daysSinceVisit 未定义 | 🔴 高 | Banner 核心展示字段 |
| GAP-24 | aiInsight (summary + strategies) 未定义 | 🔴 高 | AI 洞察模块 |
| GAP-25 | coachTasks关联助教任务列表未定义 | 🔴 高 | 复杂数据结构,需多表聚合 |
| GAP-26 | favoriteCoaches最亲密助教未定义 | 🔴 高 | 需关系指数 + 服务统计 |
| GAP-27 | 消费记录 coaches 子数组(助教服务明细)未定义 | 🔴 高 | 含 perfHours 折算工时 |
| GAP-28 | 消费记录拆分字段tableFee/foodAmount/totalOrigPrice 等)未定义 | 🟠 中 | 契约仅有 amount |
| GAP-29 | 消费记录 type 枚举不一致 | 🟠 中 | 前端 table/shop/recharge vs 契约未明确 |
| GAP-30 | 备注列表未定义 | 🟠 中 | 客户详情需展示备注 |
### 五、COACH-1 Gap助教详情 — 第二大 Gap 集中区)
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-38 | performance/income/tierNodes 未定义 | 🔴 高 | 6 个绩效指标 + 本月/上月各 4 项收入 + 档位节点 |
| GAP-39 | TopCustomer 缺少 heartEmoji/score/scoreColor/balance | 🔴 高 | 客户关系 TOP20 核心字段 |
| GAP-40 | ServiceRecord.perfHours 未定义 | 🟠 中 | 折算工时 |
| GAP-41 | HistoryMonth 完全未定义 | 🔴 高 | 历史月份统计5+ 个月) |
| GAP-42 | TaskItem.notes含置顶标记未定义 | 🟠 中 | 任务关联备注 |
| GAP-43 | AbandonedTask.reason 未定义 | 🟡 低 | 放弃原因 |
| GAP-44 | workYears/hireDate 未定义 | 🟡 低 | 助教基本信息 |
### 六、CHAT 模块 Gap
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-45 | ChatMessage.referenceCard 未定义 | 🔴 高 | 引用卡片(客户概览数据) |
| GAP-46 | timestamp vs created_at 字段名不一致 | 🟠 中 | 前端用 timestamp契约用 created_at |
| GAP-47 | ChatHistoryItem.title 未定义 | 🟠 中 | 对话标题 |
| GAP-48 | ChatHistoryItem.timestamp vs last_time 不一致 | 🟠 中 | 字段名不一致 |
| GAP-49 | customerId → chatId 映射机制缺失 | 🔴 高 | 前端传 customerId 但 API 需要 chatId |
| GAP-50 | 多入口参数路由逻辑未实现 | 🟠 中 | historyId/coachId/customerId 三种入口 |
| GAP-51 | SSE 流式端点未在契约中定义 | 🟠 中 | spec 已确认需补充 |
### 七、其他 Gap
| # | Gap | 严重度 | 说明 |
|---|-----|--------|------|
| GAP-06 | 维客线索 tag 格式不一致(含换行符) | 🟡 低 | 需统一 |
| GAP-07 | 维客线索 source 格式不一致 | 🟡 低 | mock 用 `'By:系统'`,类型定义用 `'manual'` |
| GAP-08 | aiAnalysis/talkingPoints 的 ai_cache cache_type 未明确 | 🟠 中 | 需明确映射 |
| GAP-11 | storageLevel/relationLevel 计算逻辑未明确 | 🟡 低 | 前端本地计算 |
| GAP-19 | avatarChar/avatarColor 是否后端返回 | 🟡 低 | 建议前端自行计算 |
| GAP-20 | courseTypeClass 枚举值前缀不统一 | 🟡 低 | `tag-basic` vs `basic` |
| GAP-21 | 月份切换未重置分页 | 🟡 低 | F9 前端 bug |
| GAP-22 | performance-records Banner 字段来源 | 🟡 低 | 依赖 GAP-01 |
| GAP-32 | recordType/isEstimate 未定义 | 🟡 低 | CUST-2 扩展字段 |
| GAP-33 | customerPhoneFull 未定义 | 🟡 低 | CUST-2 需完整手机号 |
| GAP-34 | totalServiceCount 未定义 | 🟡 低 | CUST-2 累计次数 |
| GAP-35 | 月份切换本地筛选需改 API 请求 | 🟡 低 | F10 前端修复 |
| GAP-36 | notes 页无触底加载 | 🟡 低 | F11 前端修复 |
| GAP-37 | Note.tagType 'system' 类型语义 | 🟡 低 | 需确认 |
---
### 统计
| 严重度 | 数量 | 说明 |
|--------|------|------|
| 🔴 高 | 17 | 阻塞联调,必须在后端开发前解决 |
| 🟠 中 | 19 | 影响功能完整性,需在联调前补充 |
| 🟡 低 | 15 | 可联调后修复或前端自行处理 |
| **合计** | **51** | |
### 最高优先级修复建议
1. **契约大幅扩展 CUST-1**:客户详情是 Gap 最集中的页面GAP-23~30需新增 `balance``consumption60d``idealInterval``daysSinceVisit``aiInsight``coachTasks``favoriteCoaches`、消费记录拆分字段、备注列表。
2. **契约大幅扩展 COACH-1**:助教详情是第二大 Gap 集中区GAP-38~44需新增 `performance`6 指标)、`income`8 项)、`tierNodes``TopCustomer` 扩展字段、`HistoryMonth``ServiceRecord.perfHours`
3. **契约扩展 PERF-1**:绩效概览需要 DateGroup 分组结构GAP-12和收入档位数据GAP-13
4. **契约扩展 TASK-1 performance**:绩效卡片需要 15+ 字段GAP-02
5. **修复 customerId/taskId 混淆**TASK-2 响应需包含 `customer_id`GAP-09/10customer-detail 跳转需传参GAP-31
6. **补充 CHAT 模块**referenceCardGAP-45、customerId→chatId 映射GAP-49、SSE 端点定义GAP-51

View File

@@ -0,0 +1,236 @@
# P12赠送卡矩阵细分数据 — gift-card-breakdown
> 优先级P1BOARD-3 财务看板功能缺陷修复)
> 来源RNS1 系列审计遗留项 P1-6
> 预估工作量:中
> 依赖无新增依赖DWD 层数据已就绪)
---
## 背景
BOARD-3 财务看板的「预收资产」板块包含一个赠送卡 3×4 矩阵3 行:新增/消费/余额4 列:合计/酒水卡/台费卡/抵用券)。当前矩阵除余额行的 total 列外,所有细分单元格均返回 0。
根因DWS 层 `dws_finance_recharge_summary` 只存储赠送卡总额(`recharge_gift``gift_card_balance`),未按卡类型拆分。而 DWD 层 `dim_member_card_account` 已通过 `card_type_id` 区分三种赠送卡类型,数据源完备。
---
## 需求Requirements
### 用户故事
1. 作为门店管理者,我需要在财务看板中看到赠送卡按用途(酒水/台费/抵用券)拆分的余额、新增、消费数据,以便了解各类赠送卡的使用情况和资金分布。
### 验收标准
- AC1`dws_finance_recharge_summary` 新增 6 个字段3 种卡类型 × 余额+新增ETL 任务正确填充
- AC2BOARD-3 赠送卡矩阵「余额」行的 liquor/table_fee/voucher 列显示正确数值
- AC3赠送卡矩阵「新增」行显示各类型赠送卡的新增金额按充值订单的 `point_amount` 拆分)
- AC4赠送卡矩阵「消费」行显示各类型赠送卡的消费金额如 DWD 层可追溯)
- AC5环比数据正确计算如启用 compare 参数)
- AC6RLS 视图自动包含新字段(`CREATE OR REPLACE VIEW` 使用 `SELECT *` 或显式列名)
- AC7FDW 外部表通过 `IMPORT FOREIGN SCHEMA` 自动同步
- AC8`GET /api/xcx/board/finance` 接口返回的 `gift_rows` 矩阵包含正确的细分数据(非全 0
- AC9小程序 `board-finance` 页面赠送卡矩阵正确渲染后端返回的细分数据(替换 mock
---
## 数据源分析
### DWD 层现有数据
`dwd.dim_member_card_account` 通过 `card_type_id` 区分卡类型:
| card_type_id | 类型 | 当前记录数 | 当前余额 |
|---|---|---|---|
| 2793249295533893 | 储值卡 | 421 | 128,918.32 |
| 2791990152417157 | 台费卡(赠送) | 343 | 246,267.50 |
| 2793266846533445 | 活动抵用券(赠送) | 118 | 24,978.70 |
| 2794699703437125 | 酒水卡(赠送) | 49 | 3,708.95 |
| 2791987095408517 | 年卡 | 7 | 7.00 |
| 2793306611533637 | 月卡 | 12 | 4,938.00 |
ETL 任务 `FinanceRechargeTask._extract_card_balances()` 已硬编码三个赠送卡 ID
```python
GIFT_CARD_TYPE_IDS = [2791990152417157, 2793266846533445, 2794699703437125]
```
### 矩阵数据需求
| 行 | 数据来源 | 说明 |
|---|---|---|
| 余额 | `dim_member_card_account``card_type_id` 分组 | 当日末快照,已有数据 |
| 新增 | `dwd_recharge_order.point_amount` 按会员卡类型拆分 | 需要 JOIN `dim_member_card_account` 确定卡类型 |
| 消费 | `dwd_settlement_head.gift_card_amount` | 总额已有,但无法按卡类型拆分(结算单不记录具体使用哪种赠送卡) |
### 关键约束
- **消费行拆分可能不可行**`dwd_settlement_head.gift_card_amount` 是赠送卡消费总额,飞球上游 API 不提供按卡类型拆分的消费明细。需确认是否有 `dwd_settlement_head_ex` 或其他扩展表包含此信息。
- **新增行拆分**:充值订单 `dwd_recharge_order``tenant_member_card_id` 可关联 `dim_member_card_account.tenant_member_id`,从而确定充值到哪种卡类型。
- **card_type_id 硬编码**:当前 ETL 已硬编码,本次扩展沿用同一套 ID。后续可考虑迁移至配置表feiqiu-data-rules 规则 6 精神)。
---
## 设计要点
### DDL 变更(`dws_finance_recharge_summary`
新增 6 个字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| `gift_liquor_balance` | NUMERIC(14,2) | 酒水卡余额(当日末) |
| `gift_table_fee_balance` | NUMERIC(14,2) | 台费卡余额(当日末) |
| `gift_voucher_balance` | NUMERIC(14,2) | 抵用券余额(当日末) |
| `gift_liquor_recharge` | NUMERIC(14,2) | 酒水卡新增充值(赠送部分) |
| `gift_table_fee_recharge` | NUMERIC(14,2) | 台费卡新增充值(赠送部分) |
| `gift_voucher_recharge` | NUMERIC(14,2) | 抵用券新增充值(赠送部分) |
> 消费行拆分字段暂不新增(待确认上游数据可行性)。消费行 total 可通过 `gift_card_balance` 变化量 + 新增量反推(`consumed = prev_balance + recharge - current_balance`),但精度依赖余额快照的连续性。
### ETL 任务修改
修改 `FinanceRechargeTask._extract_card_balances()`
-`gift_balance` 拆分为 3 个细分余额
- 新增 `_extract_gift_recharge_breakdown()` 方法,通过 `dwd_recharge_order JOIN dim_member_card_account` 按卡类型拆分赠送金额
### 后端接口修改
接口路径:`GET /api/xcx/board/finance`BOARD-3 财务看板)
- 路由:`apps/backend/app/routers/xcx_board.py``get_finance_board()`
- 权限:`view_board_finance`
涉及文件与修改点:
| 文件 | 修改点 |
|---|---|
| `apps/backend/app/services/fdw_queries.py` | `get_finance_recharge()` — SQL 新增 6 个字段的 SUM填充 `gift_rows` 矩阵的余额行和新增行细分数据 |
| `apps/backend/app/services/fdw_queries.py` | `_empty_recharge_data()` — 空默认值同步新增字段 |
| `apps/backend/app/services/board_service.py` | `_build_recharge()` — 环比计算已覆盖 `gift_rows` 所有 cell无需额外修改自动适配 |
| `apps/backend/app/schemas/xcx_board.py` | `GiftCell` / `GiftRow` / `RechargePanel` — schema 无需修改(已预留 `liquor`/`table_fee`/`voucher` 字段) |
> 消费行:如果无法直接拆分,使用 `余额变化量反推法` 或保持 total only。
### 小程序页面影响
页面路径:`apps/miniprogram/miniprogram/pages/board-finance/`
| 文件 | 当前状态 | 修改点 |
|---|---|---|
| `board-finance.wxml` (L316-370) | 赠送卡矩阵已渲染 `recharge.giftRows`3 行 × 4 列(酒水卡/台费卡/抵用券) | 无需修改(模板已就绪,数据绑定字段已对齐) |
| `board-finance.ts` | 当前使用 mock 数据(`giftRows` 硬编码) | 联调时替换 mock 为真实 API 调用(非本 SPEC 范围,属于联调阶段) |
| `board-finance.wxss` (L643-717) | 赠送卡表格样式已完成 | 无需修改 |
字段映射关系(后端 → 小程序):
| 后端字段 | 小程序绑定 |
|---|---|
| `GiftRow.total.value` | `item.total` |
| `GiftRow.liquor.value` | `item.wine` |
| `GiftRow.table_fee.value` | `item.table` |
| `GiftRow.voucher.value` | `item.coupon` |
| `GiftRow.*.compare` | `item.*Compare` |
> 小程序页面模板和样式已在 RNS1 阶段完成,本次只需后端返回正确数据即可自动渲染。联调阶段需将 mock 数据替换为真实 API 调用。
### 管理后台
admin-web 无涉及赠送卡/充值统计的页面,不受影响。
### RLS 视图 & FDW
- `app.v_dws_finance_recharge_summary` 使用 `CREATE OR REPLACE VIEW`,需要更新列列表
- FDW 使用 `IMPORT FOREIGN SCHEMA`,需要重新导入(幂等脚本已支持)
---
## 数据流向
```
DWD 层
├─ dwd_recharge_order.point_amount ──→ JOIN dim_member_card_account ──→ 按 card_type_id 拆分赠送金额
├─ dim_member_card_account.balance ──→ 按 card_type_id 分组求和 ──→ 3 种赠送卡余额
└─ dwd_settlement_head.gift_card_amount ──→ 总额(无法按卡类型拆分)
↓ ETLFinanceRechargeTask
DWS 层dws_finance_recharge_summary+6 字段)
↓ RLS 视图 + FDW
业务库app.v_dws_finance_recharge_summary
↓ fdw_queries.get_finance_recharge()
FastAPIGET /api/xcx/board/finance → RechargePanel.gift_rows
↓ 小程序渲染
board-finance 页面:赠送卡矩阵 3×4 显示正确数值
```
---
## 待确认项
1. **消费行拆分**`dwd_settlement_head_ex` 或其他表是否包含赠送卡消费的卡类型明细?如果没有,消费行只能显示 total细分列保持 0 或使用反推法。
2. **card_type_id 2791987095408517 和 2793306611533637**:已确认为年卡和月卡,不纳入赠送卡矩阵。
3. **环比需求**:赠送卡矩阵的每个 cell 是否需要环比数据compare 字段)?当前 schema `GiftCell` 已预留 `compare: str | None`
---
## 任务清单
### 数据层ETL + DDL
- [ ] T1DDL 迁移 — `dws_finance_recharge_summary` 新增 6 个字段
- 迁移脚本:`db/etl_feiqiu/migrations/2026-xx-xx_add_gift_breakdown_fields.sql`
- DDL 基线同步:`docs/database/ddl/etl_feiqiu__dws.sql`
- [ ] T2ETL 修改 — `_extract_card_balances()` 拆分赠送卡余额
- 文件:`apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py`
- 返回值新增 `gift_liquor_balance``gift_table_fee_balance``gift_voucher_balance`
- [ ] T3ETL 新增 — `_extract_gift_recharge_breakdown()` 拆分赠送卡新增
- 文件:同 T2
- SQL`dwd_recharge_order JOIN dim_member_card_account``card_type_id` 分组
- [ ] T4ETL transform — `transform()` 方法写入新增 6 个字段到 record dict
### 数据库视图层
- [ ] T5RLS 视图更新 — `app.v_dws_finance_recharge_summary` 重建
- 迁移脚本:`db/etl_feiqiu/migrations/``db/zqyy_app/migrations/`
- [ ] T6FDW 外部表同步 — `IMPORT FOREIGN SCHEMA` 重新导入
### 后端接口层
- [ ] T7后端修改 — `fdw_queries.get_finance_recharge()`
- 文件:`apps/backend/app/services/fdw_queries.py`
- SQL 新增 6 个字段的 SUM
- 填充 `gift_rows` 矩阵:余额行 + 新增行的 liquor/table_fee/voucher
- [ ] T8后端修改 — `_empty_recharge_data()` 空默认值同步
### 小程序页面层
- [ ] T9小程序联调 — `board-finance.ts` 替换 mock 数据为真实 API 调用
- 文件:`apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
- 当前状态mock 数据硬编码,模板和样式已就绪
- 注意:字段映射 `liquor→wine``table_fee→table``voucher→coupon` 需在数据转换层处理
### 验证与文档
- [ ] T10验证 — 跑数后对比 `dim_member_card_account` 余额与 DWS 汇总值
- [ ] T11BD 手册更新 — `BD_manual_dws_finance_recharge_summary.md`
### 涉及文件汇总
| 模块 | 文件路径 | 操作 |
|---|---|---|
| DDL | `db/etl_feiqiu/migrations/2026-xx-xx_add_gift_breakdown_fields.sql` | 新增 |
| DDL 基线 | `docs/database/ddl/etl_feiqiu__dws.sql` | 修改 |
| ETL | `apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py` | 修改 |
| RLS 视图 | `db/etl_feiqiu/migrations/``db/zqyy_app/migrations/` | 新增 |
| 后端 | `apps/backend/app/services/fdw_queries.py` | 修改 |
| 小程序 | `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts` | 修改 |
| Schema | `apps/backend/app/schemas/xcx_board.py` | 无需修改(已预留) |
| 路由 | `apps/backend/app/routers/xcx_board.py` | 无需修改 |
| 服务 | `apps/backend/app/services/board_service.py` | 无需修改(环比自动适配) |
| BD 手册 | `apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_recharge_summary.md` | 修改 |