chore: 文档与 IDE 配置整理

- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro)
- CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/
- 新增 /spec-close、/pre-change 两个工作流命令
- DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表)
- BD_Manual → BD_manual 命名统一(48 个文件)
- 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数)
- 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表)
- 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档)
- docs/database/README.md 索引更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:02:37 +08:00
parent 8228b3fa37
commit 70324d8542
185 changed files with 13595 additions and 1219 deletions

View File

@@ -0,0 +1,424 @@
# 技术设计文档:小程序数据获取与展示效率优化
## 概述
本设计覆盖小程序三个核心页面Performance、Task-List、Task-Detail的数据获取与展示效率优化。优化分为两个维度
1. **后端查询优化**:消除 N+1 查询、SQL 层过滤替代内存过滤、ETL 连接合并
2. **前端体验优化**:内存缓存 + 版本校验、跳转预加载、条件显示逻辑
核心设计原则:**数据正确性优先于性能**——所有优化必须通过基线快照回归验证,确保优化前后数据完全一致。
## 架构
### 当前架构问题
```mermaid
graph TD
subgraph "Performance 页 — N+1 问题"
A[_build_top_customers] -->|循环 N 次| B[get_relation_index]
B -->|每次新建 FDW 连接| C[v_dws_member_assistant_relation_index]
end
subgraph "Task-Detail 页 — 3 次独立连接"
D[get_task_detail] --> E[get_member_info — 连接1]
D --> F[get_member_balance — 连接2]
D --> G[RS 指数内联查询 — 连接3]
end
subgraph "前端 — 无缓存"
H[Task-List 页] -->|跳转| I[Task-Detail 页]
I -->|重新请求全部数据| J[后端 API]
end
```
### 优化后架构
```mermaid
graph TD
subgraph "Performance 页 — 批量查询"
A2[_build_top_customers] -->|1 次调用| B2[get_relation_index_batch]
B2 -->|单连接 WHERE member_id = ANY| C2[v_dws_member_assistant_relation_index]
end
subgraph "Task-Detail 页 — 单连接批量"
D2[get_task_detail] --> E2[batch_query_for_task_detail]
E2 -->|单 _fdw_context| F2["member_info + balance + RS 一次查完"]
end
subgraph "前端 — Cache Store"
G2[Task-List 页] -->|写入 Cache Store| H2[全局内存缓存]
H2 -->|读取缓存 + 骨架渲染| I2[Task-Detail 页]
I2 -->|异步请求完整数据| J2[后端 API]
end
```
## 组件与接口
### 后端新增/修改组件
#### 1. `fdw_queries.get_relation_index_batch()`(新增)
```python
def get_relation_index_batch(
conn: Any,
site_id: int,
assistant_id: int,
member_ids: list[int],
) -> dict[int, float]:
"""
批量查询多个客户的 RS 指数。
返回 {member_id: rs_display} 映射。
member_ids 为空时直接返回空字典,不执行 SQL。
"""
```
- 数据源:`app.v_dws_member_assistant_relation_index`
- 查询条件:`WHERE assistant_id = %s AND member_id = ANY(%s)`
- 复用已有 `_fdw_context` 连接管理
#### 2. `fdw_queries.batch_query_for_task_detail()`(新增)
```python
def batch_query_for_task_detail(
conn: Any,
site_id: int,
assistant_id: int,
member_id: int,
) -> dict:
"""
单连接批量查询任务详情所需的 ETL 数据。
返回 {member_info, balance, rs_score}。
每个子查询独立降级(返回默认值)。
"""
```
- 模式:复用 `batch_query_for_task_list``_fdw_context` 单连接模式
- 子查询:`v_dim_member`(会员信息)、`v_dim_member_card_account`(余额)、`v_dws_member_assistant_relation_index`RS 指数)
- 降级策略:每个子查询用 try/except 包裹,失败返回默认值
#### 3. `coach_service._build_top_customers()`(修改)
-`for mid in member_ids` 循环调用 `get_relation_index` 替换为单次 `get_relation_index_batch` 调用
- 返回数据结构不变
#### 4. `task_manager.get_task_detail()`(修改)
- 将 3 个独立 ETL 查询(`get_member_info``get_member_balance`、内联 RS 查询)替换为 `batch_query_for_task_detail` 单次调用
- 返回数据结构不变,新增 `updated_at` 字段
#### 5. `task_manager.get_task_list_v2()`(修改)
- 当前已在 SQL 层排除 RS 范围外的 `relationship_building` 任务CHANGE 2026-03-25
- 需求 2 要求的 SQL 层过滤已基本实现,需验证 `total` 计数与实际过滤结果一致性
- 返回每个任务项新增 `updated_at` 字段
#### 6. `coach_service.get_coach_detail()`(修改)
- `top_customers` 列表中每个客户新增 `data_updated_at` 字段(取查询时服务器时间戳 `now()`
### 前端新增组件
#### 7. `utils/cache-store.ts`(新增)
```typescript
interface CacheEntry<T> {
data: T
updatedAt: string // ISO 时间戳,来自后端 updated_at
cachedAt: number // 本地缓存时间 Date.now()
}
class CacheStore {
private store: Map<string, CacheEntry<any>>
/** 读取缓存,返回 null 表示缓存不存在 */
get<T>(key: string): CacheEntry<T> | null
/** 写入缓存 */
set<T>(key: string, data: T, updatedAt: string): void
/** 清空全部缓存 */
clear(): void
/** 批量写入Task-List → Task-Detail 预加载) */
setTaskPreload(taskId: number, data: TaskPreloadData): void
/** 读取任务预加载数据 */
getTaskPreload(taskId: number): TaskPreloadData | null
}
export const cacheStore = new CacheStore()
```
- 缓存范围:`member_info``balance``relation_index`、任务预加载数据
- 缓存 key 格式:`{dataType}:{id}`(如 `member_info:12345``task_preload:678`
- 生命周期:`onHide` 或下拉刷新时 `clear()`
#### 8. Task-List 页跳转传参(修改)
- 点击任务卡片时,将已有数据写入 `cacheStore.setTaskPreload()`
- 传递字段:`customer_name``heart_score``balance``task_type``task_type_label`
#### 9. Task-Detail 页骨架渲染(修改)
- `onLoad` 时先从 `cacheStore.getTaskPreload()` 读取预加载数据
- 有缓存立即渲染骨架内容客户名、RS 分数、余额等),同时异步请求完整数据
- 无缓存回退为当前全量加载模式loading → 请求 → 渲染)
#### 10. Performance 页跳转传参(修改)
- 点击客户卡片跳转 Task-Detail 时,将 `customer_name``heart_score` 写入 `cacheStore`
### 数据库变更
#### 11. `biz.coach_tasks` 新增 `updated_at` 列
- DDL 迁移文件:`db/zqyy_app/migrations/YYYY-MM-DD__coach_tasks_updated_at.sql`
- 列定义:`updated_at TIMESTAMPTZ NOT NULL DEFAULT now()`
- 触发器:任务状态变更时自动更新 `updated_at`
### 酒水聚合优化(低优先级)
#### 12. `fdw_queries.get_service_records_for_task()`(条件修改)
- 当服务记录 > 50 条时,使用 CTE 预聚合替代 `LEFT JOIN LATERAL`
- ≤ 50 条时保持现有 `LEFT JOIN LATERAL` 方式
- 结果数据完全一致
### 回归验证
#### 13. `tests/test_perf_optimization_baseline.py`(新增)
- pytest 形式,使用小燕的真实数据(`assistant_id: 2964673443302213``site_id: 2790685415443269`
- 基线快照存储:`tests/fixtures/perf_optimization/`
- 覆盖三个核心接口的逐字段 diff 对比
## 数据模型
### `biz.coach_tasks` 表变更
```sql
-- 新增 updated_at 列
ALTER TABLE biz.coach_tasks
ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
-- 触发器:状态变更时自动更新
CREATE OR REPLACE FUNCTION biz.coach_tasks_set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_coach_tasks_updated_at
BEFORE UPDATE ON biz.coach_tasks
FOR EACH ROW
EXECUTE FUNCTION biz.coach_tasks_set_updated_at();
```
### 接口返回结构变更
#### `get_task_list_v2` 返回项新增字段
```python
{
# ... 现有字段不变 ...
"updated_at": "2026-03-25T10:30:00+08:00", # 新增
}
```
#### `get_task_detail` 返回新增字段
```python
{
# ... 现有字段不变 ...
"updated_at": "2026-03-25T10:30:00+08:00", # 新增
}
```
#### `get_coach_detail` → `top_customers` 列表项新增字段
```python
{
# ... 现有字段不变 ...
"data_updated_at": "2026-03-25T10:30:00+08:00", # 新增,取查询时 now()
}
```
### 前端缓存数据结构
```typescript
interface TaskPreloadData {
customer_name: string
heart_score: number
balance: number
task_type: string
task_type_label: string
}
// Cache key 示例
// "task_preload:123" → TaskPreloadData
// "member_info:456" → { nickname, mobile }
// "balance:456" → number
// "rs:789:456" → number (assistant_id:member_id)
```
### 基线快照 JSON 结构
```
tests/fixtures/perf_optimization/
├── baseline_coach_detail.json # get_coach_detail 完整返回
├── baseline_task_list_v2.json # get_task_list_v2 完整返回
└── baseline_task_detail.json # get_task_detail 完整返回
```
## 正确性属性Correctness Properties
*属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: 批量 RS 查询与逐条查询等价性
*For any* `assistant_id` 和任意非空 `member_ids` 列表,`get_relation_index_batch(conn, site_id, assistant_id, member_ids)` 返回的 `{member_id: rs_display}` 映射,应与对每个 `member_id` 逐条调用 `get_relation_index(conn, site_id, member_id)` 并提取 `assistant_id` 匹配行的 `relation_index` 值完全一致。
**Validates: Requirements 1.4**
### Property 2: relationship_building 任务 RS 范围过滤
*For any* 任务列表返回结果,其中 `task_type = 'relationship_building'` 的任务,其对应 `member_id` 的 RS 指数值应在 `(rs_min, rs_max)` 开区间范围内(当 RS 排除列表查询成功时)。
**Validates: Requirements 2.2**
### Property 3: 分页 total 与实际结果集一致性
*For any* 过滤条件和分页参数(`page``page_size``get_task_list_v2` 返回的 `total` 值应等于遍历所有页(`page=1``ceil(total/page_size)`)累计的 `items` 总数。
**Validates: Requirements 2.4**
### Property 4: 接口返回包含版本时间戳
*For any* 调用 `get_task_list_v2``get_task_detail``get_coach_detail` 返回的数据每个数据项任务项、任务详情、TOP 客户项)都应包含有效的版本时间戳字段(`updated_at``data_updated_at`),且该字段为合法的 ISO 8601 时间戳。
**Validates: Requirements 3.2, 6.1, 6.2, 6.3**
### Property 5: 缓存版本校验行为
*For any* 缓存条目和对应的后端 `updated_at` 值,当缓存中的 `updatedAt` 与后端返回的 `updated_at` 相同时,`CacheStore.get()` 应返回缓存数据;当后端 `updated_at` 更新(值变大)时,缓存应被判定为过期。
**Validates: Requirements 3.3, 3.4**
### Property 6: 缓存 clear 后为空
*For any* 非空的 `CacheStore` 状态,调用 `clear()` 后,对任意 key 调用 `get()` 应返回 `null`
**Validates: Requirements 3.6**
### Property 7: 预加载数据 round-trip
*For any* `TaskPreloadData` 对象,调用 `cacheStore.setTaskPreload(taskId, data)` 后,`cacheStore.getTaskPreload(taskId)` 应返回与写入数据字段值完全一致的对象。
**Validates: Requirements 4.1**
### Property 8: Task-Detail 批量查询与独立查询等价性
*For any* `site_id``assistant_id``member_id` 组合,`batch_query_for_task_detail` 返回的 `{member_info, balance, rs_score}` 应与分别调用 `get_member_info``get_member_balance`、内联 RS 查询的结果在数据内容上完全一致。
**Validates: Requirements 5.4**
### Property 9: 任务更新后 updated_at 自动变化
*For any* `biz.coach_tasks` 中的任务记录,执行 UPDATE 操作后,该记录的 `updated_at` 值应严格大于更新前的 `updated_at` 值。
**Validates: Requirements 6.4**
### Property 10: 折前/折后条件显示逻辑
*For any* 服务记录,当 `duration_raw` 不为 null 且 `duration_raw ≠ duration` 时,显示函数应返回包含"折前"标注的字符串;当 `duration_raw` 为 null 或 `duration_raw == duration` 时,显示函数应不包含"折前"标注。
**Validates: Requirements 7.1, 7.2, 7.3**
### Property 11: 折前小时数格式化
*For any* 非负浮点数 `hours`,格式化函数应输出恰好一位小数的字符串(如 `"2.5h"`),且 `parseFloat(formatted) ≈ round(hours, 1)`
**Validates: Requirements 7.4**
### Property 12: CTE 预聚合与 LEFT JOIN LATERAL 等价性
*For any* 服务记录集合(记录数 > 50使用 CTE 预聚合方式查询的 `drinks` 字段内容应与使用 `LEFT JOIN LATERAL` 方式查询的结果完全一致。
**Validates: Requirements 8.2**
## 错误处理
### 后端降级策略
| 场景 | 降级行为 | 影响范围 |
|------|----------|----------|
| `get_relation_index_batch` 查询失败 | 返回空字典 `{}`,所有客户 RS 显示为 0 | Performance 页 TOP 客户心形图标 |
| `batch_query_for_task_detail` 某子查询失败 | 该子查询返回默认值,其他子查询正常 | Task-Detail 页部分字段显示默认值 |
| RS 排除列表查询失败 | 不排除任何任务,返回全量(已有行为) | Task-List 页可能显示 RS 范围外的保底任务 |
| `updated_at` 字段缺失(旧数据) | 前端视为缓存过期,发起网络请求 | 无功能影响,仅缓存失效 |
| CacheStore 异常 | 降级为直接网络请求 | 无功能影响,仅失去缓存加速 |
| CTE 预聚合查询失败 | 回退到 LEFT JOIN LATERAL 方式 | 无功能影响,仅性能回退 |
### 前端错误处理
- CacheStore 的所有读写操作用 try/catch 包裹,异常时静默降级
- 预加载数据不存在时Task-Detail 页回退为全量加载模式
- `updated_at` 比较异常时,视为缓存过期,重新请求
## 测试策略
### 双轨测试方法
本项目采用单元测试 + 属性测试的双轨方法:
- **单元测试**:验证具体示例、边界条件、错误降级行为
- **属性测试**验证跨所有输入的通用属性等价性、不变性、round-trip
### 属性测试配置
- **库**Python 后端使用 `hypothesis`TypeScript 前端使用 `fast-check`
- **迭代次数**:每个属性测试最少 100 次迭代
- **标签格式**`Feature: miniapp-data-perf-optimization, Property {N}: {property_text}`
- 每个正确性属性由单个属性测试实现
### 后端属性测试hypothesis
| Property | 测试文件 | 生成器 |
|----------|----------|--------|
| P1: 批量 RS 等价性 | `tests/test_perf_optimization_properties.py` | `st.lists(st.integers(min_value=1))` 生成 member_ids |
| P2: RS 范围过滤 | 同上 | 生成随机 RS 值和 rs_min/rs_max 范围 |
| P3: 分页 total 一致性 | 同上 | 生成随机 page/page_size 参数 |
| P4: 版本时间戳存在性 | 同上 | 验证接口返回结构 |
| P8: Task-Detail 批量等价性 | 同上 | 生成随机 member_id |
| P9: updated_at 自动更新 | 同上 | 生成随机任务状态变更 |
| P12: CTE vs LATERAL 等价性 | 同上 | 生成随机服务记录集合 |
### 前端属性测试fast-check
| Property | 测试文件 | 生成器 |
|----------|----------|--------|
| P5: 缓存版本校验 | `apps/miniprogram/tests/cache-store.test.ts` | `fc.string()` 生成 updatedAt 时间戳 |
| P6: 缓存 clear | 同上 | `fc.dictionary()` 生成随机缓存条目 |
| P7: 预加载 round-trip | 同上 | `fc.record()` 生成 TaskPreloadData |
| P10: 折前/折后显示 | `apps/miniprogram/tests/duration-display.test.ts` | `fc.float()` 生成 duration 和 duration_raw |
| P11: 格式化一位小数 | 同上 | `fc.float({min: 0, max: 1000})` |
### 单元测试覆盖
- **边界条件**`member_ids` 为空时 `get_relation_index_batch` 返回空字典AC 1.3
- **降级行为**RS 排除列表查询失败时不排除任何任务AC 2.3
- **降级行为**`batch_query_for_task_detail` 子查询独立降级AC 5.3
- **降级行为**CacheStore 异常时降级为网络请求AC 3.5
- **降级行为**无预加载数据时回退全量加载AC 4.4
- **null 处理**`duration_raw` 为 null 时视为与 `duration` 相等AC 7.3
### 回归验证(集成测试)
- `tests/test_perf_optimization_baseline.py`:基线快照 diff 对比AC 9.1-9.6
- 使用真实测试数据库(`test_zqyy_app` / `test_etl_feiqiu`
- 排除 `updated_at``data_updated_at` 等时间戳字段的 diff
- 覆盖到手金额、RS 指数、会员余额、服务记录条数/排序、酒水明细、TOP 客户列表、AI 分析字段

View File

@@ -0,0 +1,140 @@
# 需求文档:小程序数据获取与展示效率优化
## 简介
对小程序三个核心页面Performance 绩效页、Task-List 任务列表页、Task-Detail 任务详情页)进行数据获取和展示效率优化。优化覆盖两个维度:页面加载速度(减少 SQL 查询次数和连接开销和页面间跳转体验减少重复请求。通过后端批量查询、SQL 层过滤、前端内存缓存 + 版本校验等手段,提升整体用户体验。
## 术语表
- **小程序Miniprogram**:微信小程序客户端,面向门店助教使用
- **Performance_页**绩效概览页面展示助教当月绩效、收入、TOP 客户、服务记录等
- **Task_List_页**:任务列表页面,展示助教待办任务卡片列表及绩效进度条
- **Task_Detail_页**任务详情页面展示单个任务的客户信息、维客线索、服务记录、AI 分析等
- **ETL_库**:数据仓库(`test_etl_feiqiu`),通过 `_fdw_context` 设置 RLS 参数后访问 `app.v_*` 视图
- **业务库**:应用数据库(`test_zqyy_app`),存储 `biz.coach_tasks``biz.notes``biz.ai_cache` 等业务表
- **N_Plus_1_问题**:循环中逐条执行 SQL 查询的反模式,导致查询次数随数据量线性增长
- **Batch_Query**`fdw_queries.batch_query_for_task_list()`,单连接内批量执行多条 ETL 查询的封装函数
- **RS_指数**:关系指数(`rs_display`),来自 `v_dws_member_assistant_relation_index`,衡量助教与客户的关系紧密度
- **updated_at**:数据版本时间戳,用于前端缓存校验后端数据是否已变更
- **Cache_Store**:前端内存缓存模块,存储跨页面共享的 ETL 数据会员信息、余额、RS 指数等)
- **salary_calc**`v_dws_assistant_salary_calc` 视图,按 `assistant_id + salary_month` 唯一,包含工资计算参数
- **relation_index**`v_dws_member_assistant_relation_index` 视图,按 `assistant_id + member_id` 唯一
## 需求
### 需求 1Performance 页 N+1 查询消除
**用户故事:** 作为助教,我希望绩效页加载更快,以便在繁忙时段快速查看当月绩效数据。
#### 验收标准
1. WHEN `get_coach_detail` 被调用, THE Coach_Service SHALL 通过单条批量 SQL 查询获取所有 TOP 客户的 RS 指数,替代当前按 `member_id` 逐条循环调用 `get_relation_index` 的模式
2. THE FDW_Queries 模块 SHALL 提供 `get_relation_index_batch(conn, site_id, assistant_id, member_ids)` 函数,接受 `member_ids` 列表参数,返回 `dict[int, float]`member_id → rs_display 映射)
3. WHEN `member_ids` 列表为空, THE `get_relation_index_batch` 函数 SHALL 返回空字典,不执行任何 SQL 查询
4. WHEN 批量查询执行后, THE 返回结果 SHALL 与逐条查询 `get_relation_index` 对相同 `member_ids` 的结果在 RS 指数值上完全一致
5. THE `_build_top_customers` 函数 SHALL 调用 `get_relation_index_batch` 一次性获取所有客户的 RS 指数,替代当前的 `for mid in member_ids` 循环
### 需求 2Task-List 页服务端过滤优化
**用户故事:** 作为助教,我希望任务列表加载更快且数据准确,以便高效管理待办任务。
#### 验收标准
1. WHEN `get_task_list_v2` 查询 `biz.coach_tasks` 时, THE Task_Manager SHALL 在 SQL WHERE 子句中直接排除不满足展示条件的任务,替代拉取全量 200 条后在内存中过滤的模式
2. THE SQL 查询 SHALL 在 WHERE 子句中包含 `relationship_building` 任务的 RS 范围过滤条件(排除 RS 指数不在 `[rs_min, rs_max]` 范围内的 `member_id`
3. WHEN RS 排除列表查询失败时, THE Task_Manager SHALL 降级为不排除任何任务(当前已有的容错行为保持不变)
4. THE 分页 `total` 计数 SHALL 与实际返回的过滤后结果集一致,跨页翻页时 `total` 值保持准确
5. WHILE 前端请求 `pageSize=200` 一次性加载时, THE 后端 SHALL 仅返回满足展示条件的任务,减少无效数据传输量
### 需求 3前端内存缓存与版本校验
**用户故事:** 作为助教,我希望在页面间跳转时不重复等待相同数据加载,以获得流畅的操作体验。
#### 验收标准
1. THE Cache_Store SHALL 缓存以下跨页面共享的 ETL 数据:会员信息(`member_info`)、会员余额(`balance`、RS 指数(`relation_index`
2. THE 后端接口 SHALL 在每条返回数据中包含 `updated_at` 时间戳字段,表示该数据在后端的最后更新时间
3. WHEN 前端请求数据时, THE Cache_Store SHALL 先检查本地缓存是否存在且 `updated_at` 未过期;若缓存有效则直接使用,若缓存过期或不存在则发起网络请求
4. WHEN 后端数据发生变更(`updated_at` 更新)时, THE Cache_Store SHALL 在下次请求时检测到版本变化并重新获取最新数据
5. IF 缓存校验逻辑发生异常, THEN THE Cache_Store SHALL 降级为直接发起网络请求,确保功能可用性不受影响
6. THE Cache_Store SHALL 在小程序 `onHide`(切后台)或用户主动下拉刷新时清空全部缓存,确保回到前台时获取最新数据
### 需求 4跳转传参优化
**用户故事:** 作为助教,我希望从任务列表点击进入任务详情时,已加载的客户信息能直接展示,减少等待时间。
#### 验收标准
1. WHEN 用户从 Task_List_页 点击任务卡片跳转到 Task_Detail_页 时, THE Task_List_页 SHALL 将已加载的任务数据(`customer_name``heart_score``balance``task_type``task_type_label`)通过全局 Cache_Store 传递给 Task_Detail_页
2. WHEN Task_Detail_页 加载时, THE Task_Detail_页 SHALL 优先从 Cache_Store 读取已有数据立即渲染骨架内容,同时异步请求完整详情数据
3. WHEN 完整详情数据返回后, THE Task_Detail_页 SHALL 用完整数据覆盖骨架内容,实现无缝过渡
4. IF Cache_Store 中无对应任务的预加载数据, THEN THE Task_Detail_页 SHALL 回退为当前的全量加载模式(显示 loading → 请求 → 渲染)
5. WHEN 用户从 Performance_页 点击客户卡片跳转到 Task_Detail_页 时, THE Performance_页 SHALL 同样将已有的客户数据(`customer_name``heart_score`)通过 Cache_Store 传递
### 需求 5Task-Detail 页 ETL 查询合并
**用户故事:** 作为助教,我希望任务详情页加载更快,以便快速查看客户信息并采取行动。
#### 验收标准
1. WHEN `get_task_detail` 查询 ETL 数据时, THE Task_Manager SHALL 将 `member_info``balance``relation_index` 三个独立 ETL 连接合并为单连接批量查询
2. THE 合并后的查询 SHALL 复用 `_fdw_context` 单次连接在同一个事务中依次执行会员信息、余额、RS 指数查询
3. WHEN 批量 ETL 查询失败时, THE Task_Manager SHALL 对每个子查询独立降级(返回默认值),不影响其他子查询的结果
4. THE 合并后的查询结果 SHALL 与当前三个独立查询的结果在数据内容上完全一致
### 需求 6后端 updated_at 版本字段支持
**用户故事:** 作为开发者,我希望后端接口返回数据版本时间戳,以便前端缓存能准确判断数据是否过期。
#### 验收标准
1. THE 后端 SHALL 在 `get_task_list_v2` 返回的每个任务项中包含 `updated_at` 字段(来自 `biz.coach_tasks.updated_at`
2. THE 后端 SHALL 在 `get_coach_detail` 返回的 `top_customers` 列表中,为每个客户附加 `data_updated_at` 字段(取 ETL 查询时的服务器时间戳)
3. THE 后端 SHALL 在 `get_task_detail` 返回中包含 `updated_at` 字段
4. IF `biz.coach_tasks` 表当前缺少 `updated_at` 列, THEN THE 数据库迁移 SHALL 添加该列,默认值为 `now()`,并在任务状态变更时自动更新
5. THE DDL 变更 SHALL 合并到主 DDL 迁移文件中,并通过审计流程记录
### 需求 7Task-Detail 页折前/折后小时数条件显示
**用户故事:** 作为助教,我希望只在折前和折后小时数有差异时才看到折前小时数,以减少信息干扰。
#### 验收标准
1. WHEN 服务记录的折前小时数(`service_hours_raw`)与折后小时数(`service_hours`)不相等时, THE Task_Detail_页 SHALL 在折后小时数旁显示"折前 XX.Xh"标注
2. WHEN 折前小时数与折后小时数相等时, THE Task_Detail_页 SHALL 仅显示折后小时数,不显示折前标注
3. IF 折前小时数(`duration_raw`)为 `null` 或未返回, THEN THE Task_Detail_页 SHALL 视为与折后小时数相等,不显示折前标注
4. THE 折前小时数 SHALL 格式化为一位小数(如"折前 2.5h"),与折后小时数的格式保持一致
### 需求 8LEFT JOIN LATERAL 酒水聚合优化(低优先级)
**用户故事:** 作为开发者,我希望服务记录的酒水明细聚合查询在数据量增长时仍保持高效。
#### 验收标准
1. WHILE 单次查询涉及的服务记录超过 50 条时, THE FDW_Queries 模块 SHALL 使用 CTE 预聚合方式替代当前的 `LEFT JOIN LATERAL` 子查询聚合酒水商品明细
2. WHEN 使用 CTE 预聚合后, THE 查询结果 SHALL 与 `LEFT JOIN LATERAL` 方式返回的 `drinks` 字段内容完全一致
3. WHILE 单次查询涉及的服务记录不超过 50 条时, THE FDW_Queries 模块 SHALL 保持当前的 `LEFT JOIN LATERAL` 方式(小数据量下性能已足够)
### 需求 9数据基线快照与回归验证
**用户故事:** 作为开发者,我希望每次效率优化后能自动验证数据正确性,确保优化不引入数据偏差。
#### 验收标准
1. BEFORE 任何优化任务开始实施前, THE 验证脚本 SHALL 使用小燕assistant_id: `2964673443302213`、site_id: `2790685415443269`)的真实数据,对三个核心接口(`get_coach_detail``get_task_list_v2``get_task_detail`)各执行一次,将完整返回值序列化为 JSON 基线快照
2. AFTER 每个优化任务完成后, THE 验证脚本 SHALL 使用相同参数再次调用接口,逐字段 diff 对比优化前后的返回值
3. THE diff 对比 SHALL 覆盖以下关键校验点到手金额hours × net_rate、RS 指数、会员余额、服务记录条数和排序、酒水商品明细、TOP 客户列表
4. IF diff 发现任何数据值不一致(排除 `updated_at``data_updated_at` 等时间戳字段), THEN THE 验证脚本 SHALL 报告失败并输出差异详情
5. THE 验证脚本 SHALL 以 pytest 形式编写,放置在 `tests/` 目录下,可通过 `pytest tests/test_perf_optimization_baseline.py -v` 执行
6. THE 基线快照 JSON 文件 SHALL 存储在 `tests/fixtures/perf_optimization/` 目录下,纳入版本控制
### 需求 10AI 分析数据校对预留(后期)
**用户故事:** 作为开发者,我希望 AI 分析相关的数据字段在优化过程中保持接口兼容,以便后期校对和调整。
#### 验收标准
1. THE 优化过程 SHALL 不改变 `aiAnalysis``app4_analysis`)和 `talkingPoints``app5_talking_points`)字段的数据来源、查询逻辑和返回结构
2. THE `biz.ai_cache` 表的查询方式(按 `target_id + site_id + cache_type` 查询,`ORDER BY created_at DESC`SHALL 在优化后保持不变
3. IF 后续需要对 AI 分析数据进行校对或调整, THEN THE 接口返回结构 SHALL 保持向后兼容(新增字段可以,删除或改名已有字段不可以)
4. THE 数据基线快照(需求 9SHALL 包含 `aiAnalysis``talkingPoints` 字段的完整内容,作为后期 AI 校对的参考基准

View File

@@ -0,0 +1,301 @@
# 实施计划:小程序数据获取与展示效率优化
## 概述
按"数据正确性优先于性能"原则,先拍基线快照、再做 DDL 变更、然后后端优化、前端优化,最后收尾验证。后端使用 PythonFastAPI + 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:\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:\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单元测试验证具体边界条件
- 检查点任务确保增量验证,避免问题累积
- 需求 10AI 分析数据校对预留)通过基线快照覆盖(任务 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