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:
424
docs/specs/miniapp-data-perf-optimization/design.md
Normal file
424
docs/specs/miniapp-data-perf-optimization/design.md
Normal 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 分析字段
|
||||
140
docs/specs/miniapp-data-perf-optimization/requirements.md
Normal file
140
docs/specs/miniapp-data-perf-optimization/requirements.md
Normal 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` 唯一
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:Performance 页 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` 循环
|
||||
|
||||
### 需求 2:Task-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 传递
|
||||
|
||||
### 需求 5:Task-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 迁移文件中,并通过审计流程记录
|
||||
|
||||
### 需求 7:Task-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"),与折后小时数的格式保持一致
|
||||
|
||||
### 需求 8:LEFT 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/` 目录下,纳入版本控制
|
||||
|
||||
### 需求 10:AI 分析数据校对预留(后期)
|
||||
|
||||
**用户故事:** 作为开发者,我希望 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 数据基线快照(需求 9)SHALL 包含 `aiAnalysis` 和 `talkingPoints` 字段的完整内容,作为后期 AI 校对的参考基准
|
||||
301
docs/specs/miniapp-data-perf-optimization/tasks.md
Normal file
301
docs/specs/miniapp-data-perf-optimization/tasks.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# 实施计划:小程序数据获取与展示效率优化
|
||||
|
||||
## 概述
|
||||
|
||||
按"数据正确性优先于性能"原则,先拍基线快照、再做 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:\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),单元测试验证具体边界条件
|
||||
- 检查点任务确保增量验证,避免问题累积
|
||||
- 需求 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
|
||||
Reference in New Issue
Block a user