fix: F1-5b MP-3 + MP-5 沙箱业务日小程序适配 (W1)
MP-3 customer-detail coachTasks.lastService 业务日上界裁剪:
- apps/backend/app/services/customer_service.py
- import as_runtime_today_param 从 late import 提至模块顶部
- _build_coach_tasks 开头取 ref_date,供两段 SQL 共用
- 第一条直查 biz.coach_tasks 加 `AND updated_at < (%s::date + INTERVAL '1 day')::timestamptz`
- 删除原方法内重复 ref_date 调用
- 业务影响:sandbox=2026-04-20 时,customer-detail 的"上次服务"
时间不再展示 sandbox 业务日之后的助教任务更新(沙箱不读未来)
- 测试:apps/backend/tests/test_customer_detail_mp3_lastservice.py
本地通过,因 .gitignore:71 不入仓(同 T1 / af02446 处理方式)
MP-5 coach-service-records 接入 getBusinessClock:
- apps/miniprogram/miniprogram/pages/coach-service-records/coach-service-records.ts
- import getBusinessClock + data 加 clockYear/clockMonth/clockDay 字段
- onLoad 改 async,await getBusinessClock() 取 business_year/month/date
- loadData / switchMonth 4 处 new Date() → clockYear/Month/Day
- 业务影响:sandbox=2026-04-20 时,coach-service-records 默认显示
"2026 年 4 月"业绩(而非 today 月),canGoNext=false 阻止翻到 5 月,
"前 5 日预估金额"规则按 sandbox business_date 判断
双口径验证(weixin-devtools-mcp + DB 直查):
- MP-3 4a live: lastService 最大 04-19(无未来时间)
- MP-3 4b sandbox=4-20: 5-01 任务 task_id=8348/8347 完全消失
- MP-5 4a live: clockYear/Month/Day=2026/5/5,monthLabel="2026年5月"
- MP-5 4b sandbox=4-20: monthLabel="2026年4月" + 35 笔/¥4,657
first group=2026-04-20(后端 SQL 上界裁剪生效)
审计:
- docs/audit/changes/2026-05-05__wave1_f1_5b_mp3_lastservice_upper_bound.md
- docs/audit/changes/2026-05-05__wave1_f1_5b_mp5_coach_service_records_clock.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 2026-05-05 · F1-5b MP-3 customer-detail lastService 业务日上界裁剪
|
||||
|
||||
> Wave 1 / F1-5b Wave A 第 4 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2)
|
||||
>
|
||||
> 工作量评估 M / 2h(实际 ~ 1h),按 §3 五步流程完成。
|
||||
|
||||
## 背景
|
||||
|
||||
- 路径:小程序 `pages/customer-detail/customer-detail` → 后端 `_build_coach_tasks` → `biz.coach_tasks`
|
||||
- 问题:F1-5b 排查中发现该方法第一条 SQL(直查 `biz.coach_tasks`)无任何时间过滤,sandbox 业务日下会读到"未来"更新的助教任务,违反 P20 沙箱契约"沙箱不读未来"。
|
||||
- 现象:sandbox=2026-04-20 时,coachTasks lastService 仍能展示 2026-05-01 的助教任务更新时间(实测有 2 条 task_id=8348/8347 落在 04-21 之后)。
|
||||
|
||||
## 改动清单
|
||||
|
||||
### Step 1 调研(Explore agent)
|
||||
|
||||
确认目标方法:`apps/backend/app/services/customer_service.py:454-599 _build_coach_tasks`,模块级函数,被 `get_customer_detail()` 调用。第二条 SQL(60 天统计)已正确使用 `as_runtime_today_param`,但第一条 SQL 完全无时间过滤。
|
||||
|
||||
### Step 2 TDD 红
|
||||
|
||||
新增 `test_build_coach_tasks_business_date_upper_bound`(模块顶部 import + mock + 断言 SQL + 断言 ref_date 出现在 params),首次运行 RED:`AttributeError: module 'app.services.customer_service' does not have the attribute 'as_runtime_today_param'`。
|
||||
|
||||
### Step 3 实施
|
||||
|
||||
`apps/backend/app/services/customer_service.py`:
|
||||
|
||||
1. **import 提升**:`from app.services.runtime_context import as_runtime_today_param` 从方法内 late import 提至模块顶部。
|
||||
2. **ref_date 提前**:`ref_date = as_runtime_today_param(site_id, conn=conn)` 移至方法第一行(供两段 SQL 共用)。
|
||||
3. **第一条 SQL 加上界**:`AND updated_at < (%s::date + INTERVAL '1 day')::timestamptz`,params 加 `ref_date`。
|
||||
4. 删除原 505-506 行的方法内 import 与重复 ref_date 调用。
|
||||
|
||||
### Step 4 双口径验证
|
||||
|
||||
**目标 member**: `2799207406946053`(15 条 active/inactive 任务,updated_at 跨 04-13 ~ 05-01)。
|
||||
|
||||
| 维度 | 4a live (today=05-05) | 4b sandbox=2026-04-20 |
|
||||
|------|----------------------|----------------------|
|
||||
| SQL probe 行数 | 15 | 13(裁剪 2 条) |
|
||||
| SQL probe latest updated_at | 2026-05-01 01:12 | 2026-04-19 16:36 |
|
||||
| UI coachTasks 数(RSI 过滤后) | 3 | 7 |
|
||||
| UI lastService 最大值 | 04-19 16:36 | 04-19 16:36 |
|
||||
| 是否出现 05-01 | 否(被 RSI 过滤) | 否(被 SQL 上界裁剪) |
|
||||
|
||||
**关键证据**:5-01 的两条 task(8348/8347)在 sandbox=4-20 模式下完全消失于结果集。验证脚本:
|
||||
|
||||
- `_DEL/walkthrough_f1_5b/step_mp3_4a_probe.py` — 数据探针
|
||||
- `_DEL/walkthrough_f1_5b/step_mp3_4a_4b_sql_verify.py` — SQL 双口径
|
||||
- `_DEL/walkthrough_f1_5b/step_mp3_4b_switch_sandbox.py` — sandbox 切换
|
||||
- `_DEL/walkthrough_f1_5b/step_mp3_4c_restore_live.py` — 切回 live
|
||||
|
||||
### Step 5 审计
|
||||
|
||||
本文件。
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 端 | 影响 | 验证 |
|
||||
|----|------|------|
|
||||
| 后端 `customer_service` | _build_coach_tasks 第一条 SQL 加 1 个 SQL 参数 | unit test + Playwright 走查 |
|
||||
| 小程序 customer-detail | coachTasks 列表 / lastService 显示 | weixin-devtools-mcp 实地走查 PASS |
|
||||
| FDW 链路 | 无变化(依然用既有 _fdw_context) | — |
|
||||
| 性能 | ref_date 调用提前 1 次(get_runtime_context 无缓存,每请求都查 biz.site_runtime_context),影响可忽略 | — |
|
||||
| admin-web | 无影响 | — |
|
||||
|
||||
## 测试
|
||||
|
||||
- 新增:`apps/backend/tests/tests/unit/test_customer_detail.py::test_build_coach_tasks_business_date_upper_bound` PASS
|
||||
- 既有:`test_build_coach_tasks_metrics` PASS(回归)
|
||||
- 全文件 9 passed,1 failed (`test_build_consumption_records_nesting` 预先存在的 coaches 嵌套 0 != 2,与 MP-3 无关,本次不修)
|
||||
|
||||
## 风险与未覆盖
|
||||
|
||||
- **预存 bug**:`test_build_consumption_records_nesting` 失败,登记到 Wave B 范围排查。
|
||||
- **同类待排查**:`_build_favorite_coaches` 中 `total_service_count`(全历史累计)在 sandbox 下也未裁未来,Wave B 一并处理(不在本任务范围)。
|
||||
- **RSI 视图**:`app.v_dws_member_assistant_relation_index` 在 sandbox 下取值会随 GUC `app.current_business_date` 变化,这正是 sandbox 任务数 7 ≠ live 任务数 3 的原因(由 `_fdw_context` 注入,非本次改动)。
|
||||
|
||||
## 回滚策略
|
||||
|
||||
```bash
|
||||
git revert <commit_hash>
|
||||
```
|
||||
|
||||
回滚后第一条 SQL 恢复无时间过滤;不涉及 DB schema / 配置,无副作用。
|
||||
|
||||
## Co-Authored-By
|
||||
|
||||
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
@@ -0,0 +1,99 @@
|
||||
# 2026-05-05 · F1-5b MP-5 coach-service-records 接入业务时钟
|
||||
|
||||
> Wave 1 / F1-5b Wave A 第 5 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2)
|
||||
>
|
||||
> 工作量评估 M / 2h(实际 ~ 1h),按 §3 五步流程完成。
|
||||
|
||||
## 背景
|
||||
|
||||
- 路径:小程序 `pages/coach-service-records/coach-service-records`(管理者视角的助教业绩明细)
|
||||
- 问题:页面以 `new Date()` 取"当前年月",sandbox 模式下:
|
||||
- 默认月份是真实 today 而不是 sandbox_date 所在月,展示"未来月"业绩(空)
|
||||
- `canGoNext` 防翻规则按真实 today 计算,允许跳到 sandbox business_date 之后的月份(违反"沙箱不读未来")
|
||||
- "当月预估金额规则"(每月前 5 日)按真实日期判断,sandbox=4-20 时仍按 5-05 触发,逻辑错乱
|
||||
|
||||
## 改动清单
|
||||
|
||||
### Step 1 调研
|
||||
|
||||
确认目标:`coach-service-records.ts` 共 4 处 `new Date()` 调用(L65/89/152/256),分别用于 data 默认值、onLoad 初始化、loadData "当月预估"判断、switchMonth 边界判断。
|
||||
|
||||
确认对照模板:`customer-service-records.ts` 已在 2026-05-02 接入 getBusinessClock,采用 `async onLoad` + `business_year/business_month` 字段模式。
|
||||
|
||||
### Step 2 TDD
|
||||
|
||||
小程序无单测框架,跳过 unit test,以 weixin-devtools-mcp 端到端走查作为验证主线。
|
||||
|
||||
### Step 3 实施
|
||||
|
||||
`apps/miniprogram/miniprogram/pages/coach-service-records/coach-service-records.ts`:
|
||||
|
||||
1. **import getBusinessClock**:`from '../../utils/runtime-clock'`,标注 `CHANGE 2026-05-05 | F1-5b MP-5`。
|
||||
2. **data 新增 3 字段**:`clockYear / clockMonth / clockDay`,默认 fallback 为 `new Date()` 各部分。
|
||||
3. **onLoad 改 async**:`await getBusinessClock()`,从 `business_year` / `business_month` / `business_date` 解析 day,写入 data。`clockDay` 取 `business_date.slice(8, 10)` 解析。
|
||||
4. **loadData**:`isCurrentMonth` 改用 `clockYear/clockMonth/clockDay`,删除 `new Date()`。
|
||||
5. **switchMonth**:`canGoNext / isCurrentMonth` 同上改造。
|
||||
6. 仅保留 5 处 `new Date()` 在 data 默认值与 onLoad fallback 路径,作为 getBusinessClock 失败时的兜底。
|
||||
|
||||
### Step 4 双口径验证
|
||||
|
||||
**目标 coach**:assistant_id=3148987180059141(喵喵,4 月 42 条记录 latest=04-23)
|
||||
|
||||
| 维度 | 4a live (today=2026-05-05) | 4b sandbox=2026-04-20 |
|
||||
|------|---------------------------|----------------------|
|
||||
| clockYear/Month/Day | 2026 / 5 / 5 | 2026 / 4 / 20 |
|
||||
| monthLabel | 2026年5月 | 2026年4月 |
|
||||
| canGoPrev | true | true |
|
||||
| canGoNext | false(已是当月) | **false(防翻 5 月,关键)** |
|
||||
| isCurrentMonth | true(05-05 ≤ 5) | false(20 > 5) |
|
||||
| pageState | empty(5 月无数据) | normal |
|
||||
| 总记录数 | -- | 35 笔 / ¥4,657 |
|
||||
| first_group date | — | 2026-04-20 |
|
||||
|
||||
**关键证据**:
|
||||
- sandbox=4-20 时,first dateGroup 是 04-20,后续 04-18 / 04-17(逆序),无任何 04-21 之后的记录(后端 SQL 上界裁剪正确返回)
|
||||
- `canGoNext=false` 阻止用户切到 5 月,防"读未来"
|
||||
|
||||
**走查脚本**:
|
||||
- `_DEL/walkthrough_f1_5b/step_mp5_probe_coach.py` — 找有 4 月数据的 coach
|
||||
- 复用 `step_mp3_4b_switch_sandbox.py` 切 sandbox
|
||||
- 复用 `step_mp3_4c_restore_live.py` 切回 live
|
||||
|
||||
**注意事项**:`getBusinessClock` 有 60s 内存缓存,sandbox 切换后必须 relaunch 小程序才能让缓存失效,否则页面仍显示 live 时钟。这是 utils 层面的设计(由 `clearBusinessClockCache()` 提供主动清理),非本任务问题。
|
||||
|
||||
### Step 5 审计
|
||||
|
||||
本文件。
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 端 | 影响 | 验证 |
|
||||
|----|------|------|
|
||||
| 小程序 coach-service-records | onLoad 改 async,4 处 new Date() 替换为 clockYear/Month/Day | weixin-devtools-mcp 双口径 PASS |
|
||||
| 后端 | 无改动(后端已在 F1-5b T1/A1 完成上界裁剪) | — |
|
||||
| getBusinessClock util | 无改动,沿用既有 60s 缓存 | — |
|
||||
| admin-web | 无影响 | — |
|
||||
| 其他小程序页面 | 无影响 | — |
|
||||
|
||||
## 测试
|
||||
|
||||
- 小程序无单测框架,以端到端走查作为验证主线
|
||||
- 端到端 4a/4b 双口径全部 PASS
|
||||
|
||||
## 风险与未覆盖
|
||||
|
||||
- **缓存切换体验**:用户在小程序中切 sandbox 后,需在小程序内显式 relaunch 或等 60 秒缓存过期。Wave B UI-3(AIDashboard sandbox 提示)若引入"切 sandbox 即刷新所有页面"机制,可联动调用 `clearBusinessClockCache()` 改善体验,本任务暂不处理。
|
||||
- **fallback 路径未实测**:getBusinessClock 失败时降级到 `new Date()`(localFallback),其行为继承既有 utils,本任务未单独验证。
|
||||
- **同类页面**:`performance-records.ts`(任务 tab 下助教个人页)已在 2026-05-02 接入 getBusinessClock,本次 MP-5 不涉及。
|
||||
|
||||
## 回滚策略
|
||||
|
||||
```bash
|
||||
git revert <commit_hash>
|
||||
```
|
||||
|
||||
回滚后 4 处 new Date() 恢复;不涉及后端 / DB / 配置,无副作用。
|
||||
|
||||
## Co-Authored-By
|
||||
|
||||
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
Reference in New Issue
Block a user