文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
This commit is contained in:
453
docs/02-架构设计/专项设计/查询效率优化方案-2026-06-19.md
Normal file
453
docs/02-架构设计/专项设计/查询效率优化方案-2026-06-19.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# PERF-260619-01 查询效率优化方案
|
||||
|
||||
> **状态**:方案设计完成,待评审
|
||||
> **日期**:2026-06-19
|
||||
> **范围**:DevFlow 前后端数据查询链路
|
||||
|
||||
---
|
||||
|
||||
## 一、探索分析
|
||||
|
||||
### 1.1 问题全景
|
||||
|
||||
当前 DevFlow 的数据查询链路存在 **6 个核心问题**,分布在后端 SQL 层、前端 store 层、前端视图层三个层面。
|
||||
|
||||
### 1.2 问题清单
|
||||
|
||||
| # | 层 | 问题 | 严重度 | 代码位置 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | 后端 SQL | `list_tasks` 全量查询后内存 `retain` 过滤 project_id | 中 | `commands/task.rs:32-37` |
|
||||
| P2 | 前端视图 | `ProjectDetail.vue` 全量 `loadTasks()` 后 computed 过滤 | 中 | `ProjectDetail.vue onMounted` |
|
||||
| P3 | 前端视图 | `TaskDetail.vue` 独立 `projectApi.list()` 拉全量项目仅解析单个项目名 | 低 | `TaskDetail.vue load()` |
|
||||
| P4 | 前端视图 | `Dashboard.vue` 每次进入全量加载 projects+tasks+ideas | 低 | `Dashboard.vue loadAll()` |
|
||||
| P5 | 前端 store | 无跨视图缓存/去重,路由切换重复 IPC | 中 | `stores/project.ts` 全局 |
|
||||
| P6 | 后端 SQL | 无字段投影,列表场景返回完整 Record(含 description 大字段) | 低 | CRUD 层全局 |
|
||||
|
||||
### 1.3 问题详解
|
||||
|
||||
#### P1:`list_tasks` 内存过滤(中)
|
||||
|
||||
**现状**:
|
||||
```rust
|
||||
// commands/task.rs:32-37
|
||||
pub async fn list_tasks(state, project_id: Option<String>) -> Result<Vec<TaskRecord>, String> {
|
||||
let mut tasks = state.tasks.list_active().await.map_err(err_str)?; // SELECT * FROM tasks WHERE deleted_at IS NULL
|
||||
if let Some(pid) = project_id {
|
||||
tasks.retain(|t| t.project_id == pid); // 内存过滤
|
||||
}
|
||||
Ok(tasks)
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:每次按项目筛选任务时,从 SQLite 拉取**全部未删除任务**(含 14 个字段含 description),然后在 Rust 端 `retain` 过滤。当前 6 个项目 ~15 个任务时影响极小,但随任务增长线性退化。
|
||||
|
||||
**注释中的判断**(task.rs:28):*"任务量小,无需 SQL 下推,避免新增专用查询方法"* — 当前成立,但需提前规划。
|
||||
|
||||
#### P2:`ProjectDetail.vue` 全量 loadTasks(中)
|
||||
|
||||
**现状**:
|
||||
```typescript
|
||||
// ProjectDetail.vue onMounted
|
||||
await store.loadProjects()
|
||||
await store.loadTasks() // 无参 → 全量拉取所有项目的任务
|
||||
// 然后 computed 前端过滤
|
||||
const projectTasks = computed(() =>
|
||||
store.tasks.filter(t => t.project_id === projectId.value)
|
||||
)
|
||||
```
|
||||
|
||||
**影响**:进入项目详情页时拉取**所有项目的全部任务**,仅为展示当前项目的任务。这与 `Tasks.vue` 的按项目筛选加载模式(B-260615-29 契约)不一致。
|
||||
|
||||
**根因**:`ProjectDetail.vue` 使用全局 `store.tasks` 单例 + computed 过滤,而非按 project_id 精确拉取。设计意图是"不覆盖全局 tasks 单例"(避免破坏 Tasks 视图的筛选状态),但代价是全量拉取。
|
||||
|
||||
#### P3:`TaskDetail.vue` 独立拉全量项目列表(低)
|
||||
|
||||
**现状**:
|
||||
```typescript
|
||||
// TaskDetail.vue load()
|
||||
const [t, ps] = await Promise.all([
|
||||
taskApi.get(taskId.value), // 拉单个任务 ✓
|
||||
projectApi.list(), // 拉全部项目 ✗ 仅为解析 project_id → name
|
||||
])
|
||||
```
|
||||
|
||||
**影响**:TaskDetail 绕过 store 直接调 `projectApi.list()`,拉取全部项目(含 description 大字段),仅为从 `projects.find(p => p.id === task.project_id)` 解析一个项目名。
|
||||
|
||||
**根因**:TaskDetail 设计为"绕 store 独立入口"(有本地 `df-data-changed` 监听补偿),不依赖全局 store 的 projects 数组。
|
||||
|
||||
#### P4:`Dashboard.vue` 全量三实体加载(低)
|
||||
|
||||
**现状**:
|
||||
```typescript
|
||||
// Dashboard.vue loadAll()
|
||||
await Promise.all([store.loadProjects(), store.loadTasks(), store.loadIdeas()])
|
||||
```
|
||||
|
||||
**影响**:每次进入 Dashboard 全量拉取三个实体。Dashboard 只需要:
|
||||
- 项目数 + 活跃任务数(统计)
|
||||
- 项目列表(名称 + 状态 + updated_at)
|
||||
- 灵感列表前 4 条(标题 + score + status)
|
||||
|
||||
但拉取了完整 Record(含 description / ai_analysis / scores 等大字段)。
|
||||
|
||||
**缓解因素**:store 是全局单例,如果用户先访问过其他页面(Tasks/Projects),数据已在 store 中。但 Dashboard 的 `loadAll` 无条件覆盖刷新。
|
||||
|
||||
#### P5:无跨视图缓存/去重(中)
|
||||
|
||||
**现状**:
|
||||
- `Tasks.vue onMounted` → `loadProjects() + loadTasks()`
|
||||
- `ProjectDetail.vue onMounted` → `loadProjects() + loadTasks()`
|
||||
- `Dashboard.vue onMounted` → `loadProjects() + loadTasks() + loadIdeas()`
|
||||
- `TaskDetail.vue load()` → `projectApi.list()`(绕 store)
|
||||
|
||||
**影响**:路由切换时(如 Tasks → ProjectDetail → Tasks),每次 onMounted 都重新发起 IPC 全量拉取。store 虽然是单例(数据在内存),但 `loadXxx` 方法无条件覆盖刷新,没有"数据未过期则跳过"的判断。
|
||||
|
||||
**根因**:store 的 load 方法无 dirty/timestamp 标记,无法判断"已有数据是否新鲜"。
|
||||
|
||||
#### P6:无字段投影(低)
|
||||
|
||||
**现状**:所有查询返回完整 Record。
|
||||
|
||||
- `ProjectRecord`:9 字段(含 description 可能很长)
|
||||
- `TaskRecord`:14 字段(含 description + output_json 可能很长)
|
||||
- `IdeaRecord`:13 字段(含 ai_analysis + scores + description)
|
||||
|
||||
列表场景(Tasks/Dashboard/Projects)只需要 id/name/status/priority 等少量字段,详情页才需要全字段。
|
||||
|
||||
**影响**:IPC 传输 + JSON 序列化开销。当前数据量小,影响不明显。
|
||||
|
||||
### 1.4 关键约束
|
||||
|
||||
| # | 约束 | 来源 | 影响 |
|
||||
|---|---|---|---|
|
||||
| C1 | `store.tasks` 必须反映 Tasks 视图当前筛选(activeProject),不能被无参 `loadTasks()` 覆盖为全量 | B-260615-29 契约 | P2 修复不能简单改为 `loadTasks(projectId)`,因为会覆盖全局 tasks 单例 |
|
||||
| C2 | 后端 AI 工具执行后 emit `df-data-changed`,前端按 entity 刷新对应列表 | AR-11 事件机制 | 优化时需保留联动;缓存策略需响应此事件失效 |
|
||||
| C3 | TaskDetail 绕 store 直接调 API,有本地 `df-data-changed` 监听补偿 | 设计决策 | P3 修复需考虑是否回归 store 或保持独立 |
|
||||
| C4 | 任务 status 只能走 `advance_task_atomic` 状态机 | 状态机收口 | 不影响查询优化,但 update_task 白名单已移除 status |
|
||||
| C5 | SQLite 本地存储,当前数据量小(~6 项目 ~15 任务) | 实际数据 | 短期性能影响极小,优化面向中长期数据增长 |
|
||||
| C6 | CRUD 层(`impl_repo!` 宏)无 SELECT 列裁剪、无 LIMIT/OFFSET 支持 | 架构现状 | P6 需扩展宏或新增专用查询方法 |
|
||||
|
||||
---
|
||||
|
||||
## 二、方案设计
|
||||
|
||||
### 2.1 设计原则
|
||||
|
||||
1. **渐进式**:分阶段实施,每阶段独立可交付、可验证
|
||||
2. **低风险**:优先做不改 API 契约的内部优化,外部行为不变
|
||||
3. **数据量驱动**:当前数据量小,不做过度设计;预留扩展点但不提前实现
|
||||
4. **不破坏 AR-11**:所有缓存/优化方案必须保留 `df-data-changed` 事件联动
|
||||
|
||||
### 2.2 分阶段实施计划
|
||||
|
||||
---
|
||||
|
||||
### Phase 1:SQL 下推 + 精确查询(低风险,高 ROI)
|
||||
|
||||
**目标**:消除后端全量查询 + 内存过滤,前端视图精确拉取所需数据。
|
||||
|
||||
**改动点**:
|
||||
|
||||
#### 1.1 TaskRepo 新增 `list_active_by_project`(后端)
|
||||
|
||||
```rust
|
||||
// crates/df-storage/src/crud/task_repo.rs
|
||||
impl TaskRepo {
|
||||
/// 列出指定项目的未删除任务
|
||||
pub async fn list_active_by_project(&self, project_id: &str) -> Result<Vec<TaskRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
let project_id = project_id.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL AND project_id = ?1 ORDER BY created_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt.query_map(params![project_id], |row| task_from_row(row)).map_err(storage_err)?;
|
||||
// ...collect
|
||||
}).await.map_err(storage_err)?
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **依赖注意(F-260619-01 idea_id)**:上方 SQL 硬编码 14 列,与 `list_active()` 完全重复。若 F-260619-01(TaskRecord 新增 `idea_id` 字段)先于本方案落地,`task_from_row` 会调 `row.get("idea_id")`,但 SQL 列表无此列 → rusqlite 报错。两案并行时须同步加列,或改用 `SELECT * FROM tasks WHERE ...`(rusqlite 允许结果集含 from_row 未消费的多余列)。
|
||||
|
||||
#### 1.2 `list_tasks` 命令层路由(后端)
|
||||
|
||||
```rust
|
||||
// commands/task.rs
|
||||
pub async fn list_tasks(state, project_id: Option<String>) -> Result<Vec<TaskRecord>, String> {
|
||||
match project_id {
|
||||
Some(pid) => state.tasks.list_active_by_project(&pid).await.map_err(err_str),
|
||||
None => state.tasks.list_active().await.map_err(err_str),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**外部行为不变**:API 签名 + 返回类型一致,前端零改动。
|
||||
|
||||
#### 1.3 `ProjectDetail.vue` 按项目精确拉取(前端)
|
||||
|
||||
**问题**:不能直接 `store.loadTasks(projectId)`,因为会覆盖全局 tasks 单例(违反 C1)。
|
||||
|
||||
**方案**:ProjectDetail 不用全局 `store.tasks`,改为本地 ref + 精确拉取:
|
||||
|
||||
```typescript
|
||||
// ProjectDetail.vue
|
||||
const localTasks = ref<TaskRecord[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadProjects()
|
||||
localTasks.value = await taskApi.list(projectId.value) // 精确拉取
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**收益**:从全量拉取 → 仅拉取当前项目任务。
|
||||
|
||||
⚠️ **必须配套(评审升级)**:ProjectDetail 当前**没有** `df-data-changed` 监听——它靠全局 `store.tasks`(App.vue 的 `startDataChangedListener` 刷新 store → computed 自动响应)间接获得 AR-11 联动。改为本地 ref 后这条链路**断裂**。必须在 onMounted 中新增本地 `df-data-changed` 监听:
|
||||
|
||||
```typescript
|
||||
// ProjectDetail.vue onMounted 新增(必须,非可选)
|
||||
let _unlistenDataChanged: (() => void) | null = null
|
||||
onMounted(async () => {
|
||||
await store.loadProjects()
|
||||
localTasks.value = await taskApi.list(projectId.value)
|
||||
// AR-11 本地监听:entity=task 时按当前 projectId 精确刷新
|
||||
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
|
||||
if (event.payload.entity === 'task') {
|
||||
taskApi.list(projectId.value).then(ts => { localTasks.value = ts })
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
_unlistenDataChanged?.()
|
||||
// ...existing unlisten cleanup
|
||||
})
|
||||
```
|
||||
|
||||
> **注意**:ProjectDetail 现有的 `store.startEventListener()` 是**工作流节点事件**监听(`workflowStore.startEventListener`),不是数据变更监听,两者不可混淆。
|
||||
|
||||
#### 1.4 `TaskDetail.vue` 改用 store 或 get_project(前端)
|
||||
|
||||
**方案 A**:改用 `store.projects` 复用全局缓存,不再独立 `projectApi.list()`:
|
||||
|
||||
```typescript
|
||||
// TaskDetail.vue
|
||||
const store = useProjectStore()
|
||||
const projectName = computed(() =>
|
||||
store.projects.find(p => p.id === task.value?.project_id)?.name ?? task.value?.project_id ?? '—'
|
||||
)
|
||||
// load() 只拉单个任务,不再拉全量项目
|
||||
async function load() {
|
||||
task.value = await taskApi.get(taskId.value)
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **前置条件(评审纠错)**:原方案声称"App.vue 根 onMounted 已有 projects 预加载",经核验**不成立**——App.vue onMounted 只做 appSettings/migrate/theme/locale/Agentic 轮次恢复/AR-11 listener 启动,**未调用 `store.loadProjects()`**。当前 projects 加载分散在各视图 onMounted 中(Tasks/ProjectDetail/Dashboard 各自调)。
|
||||
|
||||
采用方案 A 必须二选一补齐前置:
|
||||
- **A-1(推荐)**:App.vue onMounted 加 `await store.loadProjects()`(一行改动,全局受益,所有视图共享缓存)。
|
||||
- **A-2(保守)**:TaskDetail load() 内加 `if (store.projects.length === 0) await store.loadProjects()` lazy 补偿,不依赖全局预加载。
|
||||
|
||||
**方案 B**:两步查询单个项目,替代全量 `projectApi.list()`:
|
||||
|
||||
```typescript
|
||||
async function load() {
|
||||
task.value = await taskApi.get(taskId.value)
|
||||
// 拿到 task 后按 project_id 查单个项目
|
||||
if (task.value?.project_id) {
|
||||
const p = await projectApi.get(task.value.project_id)
|
||||
projectName.value = p?.name ?? task.value.project_id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
方案 B 无先后依赖问题(串行两步,先 task 后 project),IPC 开销为查 1 条 vs 查全量。**不依赖全局 store 状态**,保持 TaskDetail"绕 store 独立入口"的设计一致性。
|
||||
|
||||
**推荐**:
|
||||
- 若同时实施 Phase 2 缓存 → **方案 A-1**(App.vue 预加载 + store 缓存,全链路最优)
|
||||
- 若只实施 Phase 1 → **方案 B**(零全局依赖,改动最小,不引入 store 耦合)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2:前端缓存 + 去重(中等风险,中 ROI)
|
||||
|
||||
**目标**:避免路由切换时的重复 IPC,引入脏标记/时间戳缓存。
|
||||
|
||||
**改动点**:
|
||||
|
||||
#### 2.1 store 加载方法增加新鲜度判断
|
||||
|
||||
⚠️ **评审补充 — tasks 缓存筛选维度冲突**:`loadTasks(projectId?)` 带筛选参数。若 Tasks 视图选了项目 A(`store.tasks` 只含项目 A),30s 内切到 Dashboard 调 `loadTasks()` 无参全量——缓存命中跳过,但 `store.tasks` 仍是项目 A 子集,Dashboard 的 `stats.activeTasks` 统计错误。
|
||||
|
||||
**解决方案**:tasks 缓存须记录筛选维度,筛选变化时强制刷新:
|
||||
|
||||
```typescript
|
||||
// stores/project/state.ts 新增
|
||||
export const state = reactive({
|
||||
// ...existing...
|
||||
_projectsLoadedAt: 0,
|
||||
_tasksLoadedAt: 0,
|
||||
_ideasLoadedAt: 0,
|
||||
_tasksFilter: undefined as string | undefined, // 当前 tasks 缓存对应的筛选(undefined=全量)
|
||||
})
|
||||
|
||||
// stores/project/projects.ts
|
||||
const STALE_MS = 30_000 // 30s 内不重复拉取
|
||||
|
||||
async function loadProjects(force = false) {
|
||||
if (!force && Date.now() - state._projectsLoadedAt < STALE_MS && state.projects.length > 0) {
|
||||
return // 数据新鲜,跳过
|
||||
}
|
||||
state.loading = true
|
||||
state.projects = await projectApi.list()
|
||||
state._projectsLoadedAt = Date.now()
|
||||
}
|
||||
|
||||
// stores/project/tasks.ts — 筛选维度 + TTL 双判断
|
||||
async function loadTasks(projectId?: string, force = false) {
|
||||
const filter = projectId // undefined=全量, string=按项目
|
||||
// 筛选变化 → 强制刷新(防 Dashboard 拿到 Tasks 视图的子集)
|
||||
const filterChanged = state._tasksFilter !== filter
|
||||
if (!force && !filterChanged && Date.now() - state._tasksLoadedAt < STALE_MS) {
|
||||
return // 数据新鲜 + 筛选一致,跳过
|
||||
}
|
||||
state.loading = true
|
||||
state.tasks = await taskApi.list(filter)
|
||||
state._tasksLoadedAt = Date.now()
|
||||
state._tasksFilter = filter
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 AR-11 事件联动失效缓存
|
||||
|
||||
⚠️ **评审补充 — AR-11 条件刷新与缓存交互**:当前 AR-11 listener 的 task 分支有 `_activeTaskProject !== undefined` 守卫(CR-260615-26)。若 Tasks 视图未挂载(undefined),entity=task 时不触发 load,缓存也不失效。用户之后进 Tasks 时 onMounted 调 `loadTasks`,若缓存未过期则命中跳过——拿到过期数据。
|
||||
|
||||
**解决方案**:AR-11 失效缓存与条件刷新分离——缓存时间戳无条件清零(下次 load 必拉新),load 调用仍守卫:
|
||||
|
||||
```typescript
|
||||
// stores/project.ts startDataChangedListener
|
||||
if (entity === 'project') {
|
||||
state._projectsLoadedAt = 0 // 无条件失效缓存
|
||||
void projectsStore.loadProjects(true) // force 刷新
|
||||
} else if (entity === 'task') {
|
||||
state._tasksLoadedAt = 0 // 无条件失效缓存(下次任意视图 loadTasks 必拉新)
|
||||
if (_activeTaskProject !== undefined) {
|
||||
void tasksStore.loadTasks(_activeTaskProject === 'all' ? undefined : _activeTaskProject)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Dashboard loadAll 利用缓存
|
||||
|
||||
⚠️ **评审补充 — Dashboard stats 对 tasks 全量的依赖**:Dashboard 的 `stats.activeTasks`(`project.ts:105`)从 `state.tasks` 算 `filter(t => t.status === 'in_progress').length`。若 `state.tasks` 是某项目子集(Tasks 视图留下的),统计偏小。Dashboard loadAll 须确保 tasks 全量:
|
||||
|
||||
```typescript
|
||||
// Dashboard.vue
|
||||
async function loadAll() {
|
||||
await Promise.all([
|
||||
store.loadProjects(), // 命中缓存则跳过
|
||||
store.loadTasks(), // 无参=全量;筛选维度变化时强制刷新(2.1 逻辑保证)
|
||||
store.loadIdeas(),
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
> Dashboard 调 `loadTasks()` 无参(filter=undefined),若 Tasks 视图留下的 filter 是某项目 id,`filterChanged=true` → 强制全量刷新,stats 正确。
|
||||
|
||||
**风险**:
|
||||
- `df-data-changed` 必须可靠失效缓存(当前 AR-11 已覆盖 create/update/delete/restore/purge/bind_directory)
|
||||
- 用户在外部修改 SQLite(如直接操作数据库)后缓存不新鲜 — 桌面应用场景概率极低
|
||||
|
||||
---
|
||||
|
||||
### Phase 3:字段投影(低优先级,远期)
|
||||
|
||||
**目标**:列表场景只返回需要的字段,减少 IPC 传输 + 序列化开销。
|
||||
|
||||
**现状评估**:当前数据量极小(6 项目 15 任务),description 字段平均 ~500 字节,全量传输 < 10KB。**Phase 3 在数据量达到百级之前不需要实施。**
|
||||
|
||||
**预留方案**(不实施,仅记录):
|
||||
|
||||
```rust
|
||||
// 方向:impl_repo! 宏扩展 columns 参数,或新增 list_summary 方法
|
||||
pub async fn list_active_summary(&self) -> Result<Vec<ProjectSummary>> {
|
||||
// SELECT id, name, status, created_at, updated_at FROM projects WHERE ...
|
||||
}
|
||||
```
|
||||
|
||||
前端新增 `ProjectSummary` / `TaskSummary` 类型,列表视图用 Summary,详情页用完整 Record。
|
||||
|
||||
**成本**:需新增 DTO 类型 + 前端类型同步 + 视图适配,复杂度较高。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 优先级矩阵
|
||||
|
||||
| Phase | 问题覆盖 | 风险 | ROI | 建议时机 |
|
||||
|-------|---------|------|-----|---------|
|
||||
| Phase 1 | P1 + P2 + P3 | 低(不改 API 契约) | 高 | **立即实施** |
|
||||
| Phase 2 | P4 + P5 | 中(缓存一致性) | 中 | Phase 1 验证后 |
|
||||
| Phase 3 | P6 | 低(纯新增) | 低(当前数据量) | 数据量达百级时 |
|
||||
|
||||
### 2.4 涉及文件
|
||||
|
||||
| Phase | 文件 | 改动类型 |
|
||||
|-------|------|---------|
|
||||
| 1.1 | `crates/df-storage/src/crud/task_repo.rs` | 新增 `list_active_by_project` 方法 |
|
||||
| 1.2 | `src-tauri/src/commands/task.rs` | `list_tasks` 路由分支 |
|
||||
| 1.3 | `src/views/ProjectDetail.vue` | 改用本地 ref + 精确拉取 + **新增 df-data-changed 本地监听** |
|
||||
| 1.4 | `src/views/TaskDetail.vue` | 方案 B:改用 `projectApi.get(id)` 单查;或方案 A-1/A-2 复用 store.projects |
|
||||
| 1.4-A1 | `src/App.vue` | (方案 A-1)onMounted 新增 `store.loadProjects()` 预加载 |
|
||||
| 2.1 | `src/stores/project/state.ts` | 新增时间戳 + `_tasksFilter` 字段 |
|
||||
| 2.2 | `src/stores/project/projects.ts` | loadProjects 加新鲜度判断 |
|
||||
| 2.3 | `src/stores/project/tasks.ts` | loadTasks 加筛选维度 + TTL 双判断 |
|
||||
| 2.4 | `src/stores/project.ts` | AR-11 listener 无条件失效缓存 + 条件刷新分离 |
|
||||
| 2.5 | `src/views/Dashboard.vue` | loadAll 利用缓存(筛选维度保证全量 tasks) |
|
||||
|
||||
### 2.5 验收标准
|
||||
|
||||
#### Phase 1
|
||||
1. `list_tasks(Some(pid))` 走 SQL WHERE 下推,不再内存过滤
|
||||
2. `ProjectDetail.vue` 不再全量 loadTasks,改为按 projectId 精确拉取
|
||||
3. `ProjectDetail.vue` 新增本地 `df-data-changed` 监听,entity=task 时刷新 localTasks
|
||||
4. `TaskDetail.vue` 不再独立 `projectApi.list()`(改用方案 B 单查或方案 A 复用 store)
|
||||
5. B-260615-29 契约不破坏(store.tasks 仍反映 Tasks 视图筛选)
|
||||
6. AR-11 事件联动正常(create/update/delete 后 ProjectDetail + Tasks 列表均刷新)
|
||||
7. 若选方案 A-1:App.vue onMounted 成功预加载 projects,TaskDetail projectName 正确解析
|
||||
8. 若 F-260619-01 已先行:`list_active_by_project` SQL 含 idea_id 列(或用 SELECT *)
|
||||
9. `cargo check --workspace` EXIT 0 + `vue-tsc` EXIT 0
|
||||
|
||||
#### Phase 2
|
||||
10. 30s 内路由切换不重复 IPC(store 缓存命中)
|
||||
11. `df-data-changed` 事件正确失效缓存并刷新
|
||||
12. Dashboard 统计数据准确(Tasks 视图筛选某项目后切 Dashboard,stats 不偏小)
|
||||
13. Tasks 视图未挂载时 AR-11 entity=task 事件仍清零缓存(下次进 Tasks 拿新数据)
|
||||
|
||||
### 2.6 风险评估
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| ProjectDetail 改本地 ref 后 AR-11 不刷新任务 | **高** | 中 | **必须配套**:onMounted 新增本地 `df-data-changed` 监听,entity=task 时重新 `taskApi.list(projectId)`(详见 1.3) |
|
||||
| TaskDetail 改用 store.projects 时 projects 未加载 | 中 | 低 | App.vue onMounted **新增** `store.loadProjects()`(A-1),或 TaskDetail 内 lazy 补偿(A-2),或改用方案 B 完全规避 |
|
||||
| Phase 2 缓存筛选维度冲突致 Dashboard stats 错误 | 中 | 中 | tasks 缓存记录 `_tasksFilter`,筛选变化时 `filterChanged=true` 强制刷新(详见 2.1) |
|
||||
| Phase 2 AR-11 守卫 + 缓存致 Tasks 拿过期数据 | 中 | 低 | AR-11 无条件清零 `_tasksLoadedAt`(下次 load 必拉新),load 调用仍守卫(详见 2.2) |
|
||||
| Phase 2 缓存导致用户看到过期数据 | 低 | 低 | 30s TTL 短 + AR-11 强制失效 + 手动刷新按钮兜底 |
|
||||
| list_active_by_project SQL 列与 idea_id 不同步 | 中 | 中 | 两案并行时同步加列,或改用 `SELECT *`(详见 1.1) |
|
||||
| list_active_by_project SQL 注入 | 极低 | 高 | 使用参数化查询 `params![project_id]`,不走 `query` 宏的 field 拼接 |
|
||||
|
||||
---
|
||||
|
||||
## 三、决策记录
|
||||
|
||||
### D1:Phase 1 先行,Phase 2 视需
|
||||
|
||||
**理由**:Phase 1 是纯内部优化(SQL 下推 + 精确拉取),不改 API 契约,风险极低,ROI 明确。Phase 2 引入缓存层有一致性风险,待 Phase 1 验证后再评估必要性。
|
||||
|
||||
### D2:不实施 Phase 3(字段投影)
|
||||
|
||||
**理由**:当前数据量极小(< 10KB 全量传输),字段投影需新增 DTO 类型 + 前端类型同步 + 视图适配,成本远高于收益。当任务数达到 ~200+ 或 description 平均长度 > 5KB 时再评估。
|
||||
|
||||
### D3:ProjectDetail 用本地 ref 而非改 store 契约
|
||||
|
||||
**理由**:store.tasks 全局单例 + B-260615-29 筛选契约是硬约束(C1)。改 store 支持"多任务列表"会引入复杂度(哪个视图的数据?切换时怎么办?)。ProjectDetail 用本地 ref + 精确拉取更简单清晰,且 AR-11 可通过本地监听补偿。
|
||||
Reference in New Issue
Block a user