新增: 任务推进链(7态状态机+advance_task CAS原子写)+软删除+前后端7态对齐

This commit is contained in:
2026-06-16 02:33:15 +08:00
parent f30df333b3
commit d2cb38cdac
21 changed files with 1628 additions and 167 deletions

View File

@@ -27,17 +27,22 @@ fn default_priority() -> i32 {
2 // medium — 新任务默认中优先级(非 high),符合常识
}
/// 列出任务,可按 project_id 过滤
/// 列出未删除任务(deleted_at IS NULL),可按 project_id 过滤
///
/// 软删对标 projects:默认过滤回收站(语义与 list_projects 一致)。
/// 无 project_id 时调 list_active(WHERE deleted_at IS NULL);有 project_id 时
/// 先 list_active 再内存过滤 project_id(任务量小,无需 SQL 下推,避免新增专用查询方法)。
/// 注意:不能用通用 query 宏——它不带 deleted_at 过滤,会把回收站任务也返回。
#[tauri::command]
pub async fn list_tasks(
state: State<'_, AppState>,
project_id: Option<String>,
) -> Result<Vec<TaskRecord>, String> {
let result = match project_id {
Some(pid) => state.tasks.query("project_id", &pid).await,
None => state.tasks.list_all().await,
};
result.map_err(err_str)
let mut tasks = state.tasks.list_active().await.map_err(err_str)?;
if let Some(pid) = project_id {
tasks.retain(|t| t.project_id == pid);
}
Ok(tasks)
}
/// 按 id 查任务,找不到返回 Err(供前端详情页)
@@ -72,6 +77,7 @@ pub async fn create_task(
assignee: input.assignee,
workflow_def_id: None,
base_branch: None,
review_rounds: 0,
created_at: now.clone(),
updated_at: now,
};
@@ -91,8 +97,8 @@ pub async fn update_task(
field: String,
value: String,
) -> Result<bool, String> {
// 字段名注入由 df-storage 白名单兜底;这里补 status 值校验,
// 拦截拼写错误in-progess / "in progress" / 大小写错)静默落库。
// 字段名注入由 df-storage 白名单兜底;这里补 status / priority 值校验,
// 拦截拼写错误in-progess / "in progress" / 大小写错与越界数值999 / "abc")静默落库。
if field == "status" && !TaskStatus::is_valid(&value) {
return Err(format!(
"非法 status 值 {:?},合法值: {:?}",
@@ -100,6 +106,28 @@ pub async fn update_task(
TaskStatus::valid_values()
));
}
// priority 值域 0..=30=critical, 1=high, 2=medium, 3=low与前端 <select> 及
// constants/project.ts 的 PRIORITY_LABELS 一致。拦截 "abc" / 999 等脏数据。
if field == "priority" {
match value.parse::<i32>() {
Ok(p) if (0..=3).contains(&p) => {}
_ => {
return Err(format!(
"非法 priority 值 {:?},合法值: 整数 0..=30=critical, 1=high, 2=medium, 3=low",
value
));
}
}
}
// project_id 跨表存在性校验(B-260616-16 收尾)。
// crud.rs tasks 白名单保留 project_id(支持跨项目移动),跨表约束由本命令层兜底。
// 拦截移动到不存在的项目(手输脏 id / 已物理删除的项目),避免 tasks.project_id 悬空。
if field == "project_id" {
let exists = state.projects.get_by_id(&value).await.map_err(err_str)?;
if exists.is_none() {
return Err(format!("非法 project_id 值 {:?},目标项目不存在", value));
}
}
state
.tasks
.update_field(&id, &field, &value)
@@ -107,8 +135,34 @@ pub async fn update_task(
.map_err(err_str)
}
/// 删除任务
/// 删除任务(软删 → 回收站,可恢复)。对标 delete_project(SET deleted_at=now)。
#[tauri::command]
pub async fn delete_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
state.tasks.delete(&id).await.map_err(err_str)
state.tasks.soft_delete(&id).await.map_err(err_str)
}
/// 恢复任务(从回收站还原,清 deleted_at)。对标 restore_project。
#[tauri::command]
pub async fn restore_task(state: State<'_, AppState>, id: String) -> Result<bool, String> {
state.tasks.restore(&id).await.map_err(err_str)
}
/// 推进任务状态(任务推进链 F-260616-02,推进链唯一 status 写入路径)。
///
/// thin 入口(D-260616-03):业务逻辑(状态机校验 + 原子 CAS + review_rounds 累加)
/// 落 df-nodes::task_advance_node::advance_task_atomic,本命令只做参数转发与错误串化。
///
/// 流程:读当前态 → can_transition 校验 → 下沉 SQL `WHERE id AND status=expected`
/// 防 TOCTOU → 退回转换一并 review_rounds+=1。失败均返回 Err(状态机/TOCTOU/任务不存在)。
///
/// 返回:推进成功后的最新 TaskRecord(含新 status / 累加后的 review_rounds)。
#[tauri::command]
pub async fn advance_task(
state: State<'_, AppState>,
id: String,
target_status: String,
) -> Result<TaskRecord, String> {
df_nodes::task_advance_node::advance_task_atomic(&state.tasks, &id, &target_status)
.await
.map_err(err_str)
}