修复: 前端UX一致性(Dashboard状态映射/状态徽章全局/Knowledge下一条与失败反馈/Ideas筛选与错误条/ConfirmDialog安全/快捷菜单状态机/分页越界) + 后端校验(queue/count-list一致/软删拒改/父聚合/promote CAS/MCP状态机收口/update白名单剔除id/created_at) + 销账
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user