# 实施计划：小程序数据获取与展示效率优化

## 概述

按"数据正确性优先于性能"原则，先拍基线快照、再做 DDL 变更、然后后端优化、前端优化，最后收尾验证。后端使用 Python（FastAPI + pytest/hypothesis），前端使用 TypeScript（微信小程序 + fast-check），数据库使用 PostgreSQL DDL。

## 任务

- [ ] 1. 数据基线快照（优化前）
  - [x] 1.1 创建基线快照测试脚本 `tests/test_perf_optimization_baseline.py`
    - 使用小燕真实数据（`assistant_id: 2964673443302213`、`site_id: 2790685415443269`）
    - 对 `get_coach_detail`、`get_task_list_v2`、`get_task_detail` 各执行一次
    - 将完整返回值序列化为 JSON 存储到 `tests/fixtures/perf_optimization/`
    - 实现逐字段 diff 对比函数，排除 `updated_at`、`data_updated_at` 等时间戳字段
    - diff 覆盖：到手金额、RS 指数、会员余额、服务记录条数/排序、酒水明细、TOP 客户列表、`aiAnalysis`、`talkingPoints`
    - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 10.4_

  - [x] 1.2 执行基线快照采集
    - 运行 `pytest tests/test_perf_optimization_baseline.py -v` 生成基线 JSON 文件
    - 确认三个快照文件已生成并纳入版本控制
    - _Requirements: 9.1, 9.6_

- [ ] 2. 数据库 DDL 变更
  - [ ] 2.1 创建迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__coach_tasks_updated_at.sql`
    - `ALTER TABLE biz.coach_tasks ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now()`
    - 创建触发器函数 `biz.coach_tasks_set_updated_at()` 和触发器 `trg_coach_tasks_updated_at`
    - 触发器在 `BEFORE UPDATE` 时自动设置 `NEW.updated_at = now()`
    - _Requirements: 6.4, 6.5_

  - [ ] 2.2 编写属性测试：updated_at 自动更新
    - **Property 9: 任务更新后 updated_at 自动变化**
    - 使用 hypothesis 生成随机任务状态变更
    - 验证 UPDATE 后 `updated_at` 严格大于更新前的值
    - **验证: 需求 6.4**

- [ ] 3. 检查点 — DDL 变更验证
  - 执行迁移脚本到测试库，验证新字段和触发器已正确创建
  - 确保 Property 9 测试通过，ask the user if questions arise.

- [ ] 4. 后端优化：Performance 页 N+1 查询消除
  - [ ] 4.1 新增 `fdw_queries.get_relation_index_batch()` 函数
    - 接受 `conn, site_id, assistant_id, member_ids` 参数
    - 返回 `dict[int, float]`（member_id → rs_display 映射）
    - `member_ids` 为空时直接返回空字典，不执行 SQL
    - 使用 `WHERE assistant_id = %s AND member_id = ANY(%s)` 查询
    - 复用已有 `_fdw_context` 连接管理
    - _Requirements: 1.2, 1.3_

  - [ ] 4.2 编写属性测试：批量 RS 查询与逐条查询等价性
    - **Property 1: 批量 RS 查询与逐条查询等价性**
    - 使用 hypothesis `st.lists(st.integers(min_value=1))` 生成 member_ids
    - 验证 `get_relation_index_batch` 返回与逐条 `get_relation_index` 结果完全一致
    - **验证: 需求 1.4**

  - [ ] 4.3 修改 `coach_service._build_top_customers()` 使用批量查询
    - 将 `for mid in member_ids` 循环替换为单次 `get_relation_index_batch` 调用
    - 返回数据结构不变
    - _Requirements: 1.1, 1.5_

  - [ ] 4.4 修改 `coach_service.get_coach_detail()` 添加 `data_updated_at` 字段
    - `top_customers` 列表中每个客户新增 `data_updated_at`（取查询时 `now()`）
    - _Requirements: 6.2_

  - [ ] 4.5 编写单元测试：`member_ids` 为空返回空字典
    - 验证边界条件：空列表不执行 SQL，直接返回 `{}`
    - _Requirements: 1.3_

- [ ] 5. 后端优化：Task-List 页服务端过滤
  - [ ] 5.1 验证并完善 `task_manager.get_task_list_v2()` SQL 层过滤
    - 确认 SQL WHERE 子句已包含 `relationship_building` 任务的 RS 范围过滤
    - 验证 `total` 计数与实际过滤后结果集一致
    - 返回每个任务项新增 `updated_at` 字段
    - _Requirements: 2.1, 2.2, 2.4, 2.5, 6.1_

  - [ ] 5.2 编写属性测试：RS 范围过滤
    - **Property 2: relationship_building 任务 RS 范围过滤**
    - 生成随机 RS 值和 rs_min/rs_max 范围
    - 验证返回的 `relationship_building` 任务 RS 指数在 `(rs_min, rs_max)` 范围内
    - **验证: 需求 2.2**

  - [ ] 5.3 编写属性测试：分页 total 一致性
    - **Property 3: 分页 total 与实际结果集一致性**
    - 生成随机 page/page_size 参数
    - 验证 `total` 等于遍历所有页累计的 `items` 总数
    - **验证: 需求 2.4**

  - [ ] 5.4 编写单元测试：RS 排除列表查询失败降级
    - 验证查询失败时降级为不排除任何任务
    - _Requirements: 2.3_

- [ ] 6. 后端优化：Task-Detail 页 ETL 查询合并
  - [ ] 6.1 新增 `fdw_queries.batch_query_for_task_detail()` 函数
    - 接受 `conn, site_id, assistant_id, member_id` 参数
    - 单连接批量查询 `member_info`、`balance`、`rs_score`
    - 每个子查询用 try/except 包裹，失败返回默认值
    - 复用 `_fdw_context` 单连接模式
    - _Requirements: 5.1, 5.2, 5.3_

  - [ ] 6.2 编写属性测试：Task-Detail 批量查询等价性
    - **Property 8: Task-Detail 批量查询与独立查询等价性**
    - 生成随机 member_id
    - 验证 `batch_query_for_task_detail` 返回与三个独立查询结果一致
    - **验证: 需求 5.4**

  - [ ] 6.3 修改 `task_manager.get_task_detail()` 使用批量查询
    - 将 3 个独立 ETL 查询替换为 `batch_query_for_task_detail` 单次调用
    - 返回数据结构不变，新增 `updated_at` 字段
    - _Requirements: 5.1, 6.3_

  - [ ] 6.4 编写属性测试：接口返回包含版本时间戳
    - **Property 4: 接口返回包含版本时间戳**
    - 验证 `get_task_list_v2`、`get_task_detail`、`get_coach_detail` 返回数据包含有效 ISO 8601 时间戳
    - **验证: 需求 3.2, 6.1, 6.2, 6.3**

  - [ ] 6.5 编写单元测试：子查询独立降级
    - 验证某子查询失败时返回默认值，不影响其他子查询
    - _Requirements: 5.3_

- [ ] 7. 后端优化：酒水聚合 CTE 预聚合（低优先级）
  - [ ] 7.1 修改 `fdw_queries.get_service_records_for_task()` 添加 CTE 分支
    - 服务记录 > 50 条时使用 CTE 预聚合替代 `LEFT JOIN LATERAL`
    - ≤ 50 条时保持现有方式
    - _Requirements: 8.1, 8.3_

  - [ ] 7.2 编写属性测试：CTE 预聚合与 LEFT JOIN LATERAL 等价性
    - **Property 12: CTE 预聚合与 LEFT JOIN LATERAL 等价性**
    - 生成随机服务记录集合（> 50 条）
    - 验证两种方式返回的 `drinks` 字段内容完全一致
    - **验证: 需求 8.2**

- [ ] 8. 后端回归验证
  - [ ] 8.1 运行基线快照 diff 对比
    - 使用相同参数再次调用三个核心接口
    - 逐字段 diff 对比优化前后返回值
    - 确认 `aiAnalysis`、`talkingPoints` 字段未被改变
    - _Requirements: 9.2, 9.3, 9.4, 10.1, 10.2, 10.3, 10.4_

- [ ] 9. 检查点 — 后端优化完成验证
  - 运行 Monorepo 属性测试：`cd C:\Project\NeoZQYY && pytest tests/ -v`
  - 运行后端单元测试：`cd apps/backend && pytest tests/ -v`
  - 确保所有属性测试（Property 1-4, 8-9, 12）和单元测试全部通过
  - 确保基线快照 diff 无数据偏差
  - ask the user if questions arise.

- [ ] 10. 前端优化：CacheStore 缓存模块
  - [ ] 10.1 创建 `utils/cache-store.ts` 缓存模块
    - 实现 `CacheEntry<T>` 接口（`data`、`updatedAt`、`cachedAt`）
    - 实现 `CacheStore` 类：`get()`、`set()`、`clear()`、`setTaskPreload()`、`getTaskPreload()`
    - 缓存 key 格式：`{dataType}:{id}`
    - 所有读写操作用 try/catch 包裹，异常时静默降级
    - _Requirements: 3.1, 3.3, 3.4, 3.5, 3.6_

  - [ ] 10.2 编写属性测试：缓存版本校验行为
    - **Property 5: 缓存版本校验行为**
    - 使用 fast-check `fc.string()` 生成 updatedAt 时间戳
    - 验证缓存命中/过期判定逻辑
    - **验证: 需求 3.3, 3.4**

  - [ ] 10.3 编写属性测试：缓存 clear 后为空
    - **Property 6: 缓存 clear 后为空**
    - 使用 fast-check `fc.dictionary()` 生成随机缓存条目
    - 验证 `clear()` 后所有 `get()` 返回 `null`
    - **验证: 需求 3.6**

  - [ ] 10.4 编写属性测试：预加载数据 round-trip
    - **Property 7: 预加载数据 round-trip**
    - 使用 fast-check `fc.record()` 生成 TaskPreloadData
    - 验证 `setTaskPreload` → `getTaskPreload` 数据完全一致
    - **验证: 需求 4.1**

  - [ ] 10.5 编写单元测试：CacheStore 异常降级
    - 验证缓存读写异常时静默降级为网络请求
    - _Requirements: 3.5_

- [ ] 11. 前端优化：跳转传参与骨架渲染
  - [ ] 11.1 修改 Task-List 页跳转传参
    - 点击任务卡片时调用 `cacheStore.setTaskPreload()` 写入预加载数据
    - 传递字段：`customer_name`、`heart_score`、`balance`、`task_type`、`task_type_label`
    - _Requirements: 4.1_

  - [ ] 11.2 修改 Task-Detail 页骨架渲染
    - `onLoad` 时先从 `cacheStore.getTaskPreload()` 读取预加载数据
    - 有缓存：立即渲染骨架内容，同时异步请求完整详情数据
    - 完整数据返回后覆盖骨架内容，实现无缝过渡
    - 无缓存：回退为当前全量加载模式
    - _Requirements: 4.2, 4.3, 4.4_

  - [ ] 11.3 修改 Performance 页跳转传参
    - 点击客户卡片跳转 Task-Detail 时，将 `customer_name`、`heart_score` 写入 `cacheStore`
    - _Requirements: 4.5_

  - [ ] 11.4 集成 `onHide` 和下拉刷新清空缓存
    - 在 App 级 `onHide` 回调中调用 `cacheStore.clear()`
    - 在支持下拉刷新的页面 `onPullDownRefresh` 中调用 `cacheStore.clear()`
    - _Requirements: 3.6_

- [ ] 12. 前端优化：折前/折后小时数条件显示
  - [ ] 12.1 实现折前/折后条件显示逻辑
    - `duration_raw` 不为 null 且 `duration_raw ≠ duration` 时显示"折前 XX.Xh"标注
    - `duration_raw` 为 null 或等于 `duration` 时仅显示折后小时数
    - 格式化为一位小数（如"折前 2.5h"）
    - _Requirements: 7.1, 7.2, 7.3, 7.4_

  - [ ] 12.2 编写属性测试：折前/折后条件显示逻辑
    - **Property 10: 折前/折后条件显示逻辑**
    - 使用 fast-check `fc.float()` 生成 duration 和 duration_raw
    - 验证显示/隐藏"折前"标注的条件判断
    - **验证: 需求 7.1, 7.2, 7.3**

  - [ ] 12.3 编写属性测试：折前小时数格式化
    - **Property 11: 折前小时数格式化**
    - 使用 fast-check `fc.float({min: 0, max: 1000})`
    - 验证输出恰好一位小数且 `parseFloat(formatted) ≈ round(hours, 1)`
    - **验证: 需求 7.4**

- [ ] 13. 检查点 — 前端优化完成验证
  - 运行前端测试：`cd apps/miniprogram && npm test`
  - 确保所有属性测试（Property 5-7, 10-11）和单元测试全部通过
  - ask the user if questions arise.

- [ ] 14. 前后端联调与集成验证
  - [ ] 14.1 启动后端服务，验证各端点完整请求-响应链路
    - 使用真实 FDW 连接验证 SQL 查询正确性
    - 验证 JSON 响应结构与 Schema 定义一致（camelCase 序列化）
    - 验证 `updated_at` / `data_updated_at` 字段在响应中正确返回
    - _Requirements: 6.1, 6.2, 6.3_
  - [ ] 14.2 前端联调验证
    - 确认前端页面能正确调用优化后的 API 并渲染数据
    - 验证缓存命中/过期场景下前端行为正确
    - 验证空数据/降级场景下前端不崩溃
    - _Requirements: 3.3, 3.4, 3.5, 4.2, 4.3, 4.4_

- [ ] 15. 数据库变更审计与 DDL 合并
  - [ ] 15.1 审计本次实现中对数据库的所有改动
    - 检查新增字段（`updated_at`）、新增触发器（`trg_coach_tasks_updated_at`）
    - 确认无遗漏的 DDL 变更
    - _Requirements: 6.4, 6.5_
  - [ ] 15.2 执行迁移脚本到测试库
    - 验证新字段和触发器已正确创建（使用 BD 手册中的验证 SQL）
    - _Requirements: 6.4_
  - [ ] 15.3 合并到主 DDL 基线文件
    - 业务库 → `docs/database/ddl/zqyy_app__biz.sql`
    - _Requirements: 6.5_
  - [ ] 15.4 编写回滚脚本（逆序 DROP TRIGGER / DROP FUNCTION / DROP COLUMN）
    - _Requirements: 6.5_

- [ ] 16. BD 手册更新
  - [ ] 16.1 更新 BD 手册 `docs/database/BD_Manual_coach_tasks.md`
    - 新增 `updated_at` 字段明细、触发器说明
    - 包含：字段明细、约束与索引、验证 SQL（≥3 条）、兼容性影响、回滚策略
    - 记录变更原因和影响范围
    - _Requirements: 6.4, 6.5_

- [ ] 17. 文档同步更新
  - [ ] 17.1 更新后端 API 参考文档
    - 在 `apps/backend/docs/API-REFERENCE.md` 更新接口返回结构变更（`updated_at`、`data_updated_at` 字段）
    - 更新 `apps/backend/README.md` 相关模块摘要
    - _Requirements: 6.1, 6.2, 6.3_
  - [ ] 17.2 更新小程序接口契约文档
    - 在 `docs/miniprogram-dev/API-contract.md` 更新接口返回结构
    - _Requirements: 3.2, 6.1, 6.2, 6.3_
  - [ ] 17.3 更新文档地图
    - 在 `docs/DOCUMENTATION-MAP.md` 新增本次变更相关条目
    - _Requirements: 6.5_

- [ ] 18. 最终检查点 — 全量验证
  - 运行 Monorepo 属性测试：`cd C:\Project\NeoZQYY && pytest tests/ -v`
  - 运行后端单元测试：`cd apps/backend && pytest tests/ -v`
  - 运行前端测试：`cd apps/miniprogram && npm test`
  - 确保所有属性测试（Property 1-12）和单元测试全部通过
  - 确保基线快照 diff 无数据偏差
  - 确保 DDL 迁移已合并到主基线
  - 确保 BD 手册已同步更新
  - 确保 API 文档、后端 README、文档地图均已更新
  - 确保前端页面连接真实后端运行正常
  - ask the user if questions arise.

## 备注

- 标记 `*` 的子任务为可选（属性测试/单元测试），可跳过以加速 MVP
- 每个任务引用了具体的需求编号以确保可追溯性
- 属性测试验证通用正确性属性（Property 1-12），单元测试验证具体边界条件
- 检查点任务确保增量验证，避免问题累积
- 需求 10（AI 分析数据校对预留）通过基线快照覆盖（任务 1.1 和 8.1），无需独立实施任务

## 硬性规定：属性测试执行规范

1. **分组执行**：每次只运行单个属性测试函数（`-k "test_property_N"`），禁止一次性跑全部
2. **后台 CLI**：使用 `controlPwshProcess` 启动测试，`getProcessOutput` 轮询结果（间隔 ≥ 15 秒）
3. **确认完成再继续**：必须在输出中看到明确的 `passed`/`failed`/`error` 后才可进入下一步，禁止"启动测试后立即继续编码"
4. **超时保护**：`executePwsh` 的 `timeout` 设为预估时间 3 倍（最少 120 秒），hypothesis `max_examples=100`
5. **详见**：`.kiro/steering/testing-env.md` 属性测试执行规范章节

## 硬性规定：前后端联调期间的效率优化框架

当用户在联调过程中提及"效率优化"或"本页已完成，进行效率优化"时，所有调试和改动必须在本 spec 的框架下进行：

1. **数据正确性优先**：任何优化改动前先确认基线快照存在（任务 1），改动后跑 diff 验证（任务 8）
2. **AI 数据不动**：`aiAnalysis`、`talkingPoints` 的查询逻辑和返回结构保持不变（需求 10），后期单独校对
3. **联调中的样式/交互调整**：属于联调范畴，不受效率优化 spec 约束，但改动不得破坏优化后的数据链路
4. **联调中发现的数据问题**：如果是优化引入的偏差，回退到基线快照对比定位；如果是原有问题，单独记录不混入本 spec
