修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:58 +08:00
parent 8bc5380ecb
commit fbd8fae44b
30 changed files with 829 additions and 138 deletions
+83 -2
View File
@@ -6,6 +6,8 @@ use tauri::State;
use df_types::types::{new_id, TaskStatus};
use df_storage::crud::TaskQuery;
use df_storage::models::{ProjectEventRecord, TaskLinkRecord, TaskRecord};
// LW-9(BE-CMD-8):父任务聚合重算(df-nodes 共享层,与 advance_task 同源,消除双轨)。
use df_nodes::task_advance_node::recompute_parent_status;
use crate::state::AppState;
@@ -245,6 +247,19 @@ pub async fn create_task(
state: State<'_, AppState>,
input: CreateTaskInput,
) -> Result<TaskRecord, String> {
// ── BE-CMD-1:空 title trim 拒空 + priority 值域校验(对齐前端 PRIORITY_LABELS 与
// update_task 校验,拦截空白标题 / 99 等脏数据静默落库)。 ──
let title = input.title.trim().to_string();
if title.is_empty() {
return Err("任务标题不能为空".to_string());
}
if !(0..=3).contains(&input.priority) {
return Err(format!(
"priority 必须在 0..=3 (0=critical/1=high/2=medium/3=low),收到 {}",
input.priority
));
}
// ── queue 校验(白名单 + 空串默认 todo)──
// 空字符串视为默认 todo(向后兼容,与 idea_id 空串处理一致)
let queue = if input.queue.trim().is_empty() {
@@ -296,7 +311,7 @@ pub async fn create_task(
let record = TaskRecord {
id: new_id(),
project_id: input.project_id,
title: input.title,
title,
description: input.description,
status: TaskStatus::Todo,
priority: input.priority,
@@ -333,6 +348,18 @@ pub async fn create_task(
Some(record.status.as_str()),
)
.await;
// LW-9(BE-CMD-8):建子任务后触发父聚合重算(容器模型,父 status 由子任务聚合)。
// 聚合失败仅 warn 不阻断(对齐 advance_task 宽容语义)。
if let Some(pid) = &record.parent_id {
if let Err(e) = recompute_parent_status(&state.tasks, pid).await {
tracing::warn!(
task_id = %record.id,
parent_id = %pid,
error = %e,
"[父聚合] create_task 后重算父 status 失败(不阻断)"
);
}
}
Ok(record)
}
@@ -412,12 +439,27 @@ pub async fn update_task(
}
// 空字符串 = None(解除父),合法放行
}
// LW-4(BE-CMD-1):queue 字段校验(白名单 + queue/status 一致性,读当前 status)。
// update_task 只改单字段不改 status,故用严格一致性校验(与 move_task_queue 自动联动不同):
// 设置不兼容的 queue 直接拒绝,调用方需走 move_task_queue 自动联动调整 status。
if field == "queue" {
let q = value.trim();
validate_queue(q)?;
let current = state
.tasks
.get_by_id(&id)
.await
.map_err(err_str)?
.ok_or_else(|| format!("任务 ID {id} 不存在或已删除"))?;
assert_queue_status_consistent(q, current.status.as_str())?;
}
// B-260801-01(P0-1):update_field 返 affected>0;false = id 不存在或已软删(0 行)。
// 不可静默返 false——前端 store.runWithCatch 把 Err 转 toast,而 false 会被忽略致假成功。
//
// 任务字段更新是高频「项目活跃」信号(改 title/priority/assignee 等),埋点 task_updated
// 推动项目最近活跃排序反映真实业务(问题3)。best-effort 不阻断。
// 读当前 project_id(一次轻量读):update_field 返 bool 不带 project_id,无法直接埋点。
// LW-6(BE-CMD-5):update_field_active 过滤软删(deleted_at IS NULL),回收站任务不可改字段。
let current = state
.tasks
.get_by_id(&id)
@@ -425,12 +467,37 @@ pub async fn update_task(
.map_err(err_str)?;
let updated = state
.tasks
.update_field(&id, &field, &value)
.update_field_active(&id, &field, &value)
.await
.map_err(err_str)?;
if !updated {
return Err(format!("任务 ID {id} 不存在或已删除"));
}
// LW-9(BE-CMD-8):改 parent_id 后重算父聚合——旧父(任务已离开,计数减少)与新父(任务挂上)。
// 从 current 读旧 parent_id(pre-update),value 是新父值。聚合失败仅 warn 不阻断。
if field == "parent_id" {
if let Some(old_pid) = current.as_ref().and_then(|c| c.parent_id.clone()) {
if let Err(e) = recompute_parent_status(&state.tasks, &old_pid).await {
tracing::warn!(
task_id = %id,
parent_id = %old_pid,
error = %e,
"[父聚合] 解除父后重算旧父 status 失败(不阻断)"
);
}
}
let new_pid = value.trim();
if !new_pid.is_empty() {
if let Err(e) = recompute_parent_status(&state.tasks, new_pid).await {
tracing::warn!(
task_id = %id,
parent_id = %new_pid,
error = %e,
"[父聚合] 挂载父后重算新父 status 失败(不阻断)"
);
}
}
}
if let Some(rec) = current {
emit_event(
&state,
@@ -741,6 +808,20 @@ pub async fn move_task_queue(
.await;
}
// LW-9(BE-CMD-8):跨池移动后子任务 status 可能联动变化(一致性联动)→ 触发其父聚合重算。
// 仅当被移动任务是子任务;父任务自身 move 不重算自身聚合(容器 status 由子任务推进时重算)。
// 聚合失败仅 warn 不阻断。
if let Some(pid) = &updated.parent_id {
if let Err(e) = recompute_parent_status(&state.tasks, pid).await {
tracing::warn!(
task_id = %updated.id,
parent_id = %pid,
error = %e,
"[父聚合] move_task_queue 后重算父 status 失败(不阻断)"
);
}
}
Ok(updated)
}