任务/项目/灵感三实体新增 list_by_query 动态 WHERE(累积式 where_clauses
+params_vec 收口)+ order_by 白名单防注入 + limit/offset 钳制。命令层
list_{tasks,projects,ideas} 吃 Option<XxxQuery> 双参向后兼容(旧无参/单参
路径等价全量)。前端 Tasks/Ideas status/keyword 筛选下沉后端 query。
F-260621-02
209 lines
7.8 KiB
Rust
209 lines
7.8 KiB
Rust
//! 任务相关命令
|
||
|
||
use serde::Deserialize;
|
||
use tauri::State;
|
||
|
||
use df_types::types::{new_id, TaskStatus};
|
||
use df_storage::crud::TaskQuery;
|
||
use df_storage::models::TaskRecord;
|
||
|
||
use crate::state::AppState;
|
||
|
||
use super::{err_str, now_millis};
|
||
|
||
/// 创建任务入参
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct CreateTaskInput {
|
||
pub project_id: String,
|
||
pub title: String,
|
||
#[serde(default)]
|
||
pub description: String,
|
||
#[serde(default = "default_priority")]
|
||
pub priority: i32,
|
||
pub branch_name: Option<String>,
|
||
pub assignee: Option<String>,
|
||
/// 关联灵感 ID(F-260619-01,1对1 单向,可空=不关联)。
|
||
/// 空字符串视为不关联(与 AI 工具层一致)。
|
||
pub idea_id: Option<String>,
|
||
}
|
||
|
||
fn default_priority() -> i32 {
|
||
2 // medium — 新任务默认中优先级(非 high),符合常识
|
||
}
|
||
|
||
/// 列出未删除任务(deleted_at IS NULL)。
|
||
///
|
||
/// **向后兼容铁律**:`project_id` 与 `query` 两参都可选,旧调用方不传(或只传 project_id)
|
||
/// 必须等价改造前的全量行为,零破坏。
|
||
///
|
||
/// F-260621-02 查询维度补全(机制优先 prompt 说教):
|
||
/// - 优先走 `query`(`TaskQuery` 多维动态 WHERE):status(P1 下沉)/ keyword(P2 LIKE)
|
||
/// /project_id/priority/assignee/order_by/limit/offset 任意组合。
|
||
/// - 旧调用方仍可直接传 `project_id`(单维度),此时走 list_active_by_project
|
||
/// (SQL 下推,命中 idx_tasks_project_id),保持等价行为。
|
||
/// - query 与 project_id 同时传时:query 优先(其内含 project_id 维度,更全),project_id 忽略。
|
||
/// - 均不传时:全量未删任务(等价改造前 list_active 行为)。
|
||
///
|
||
/// status 值合法性兜底:TaskStatus::is_valid 拦截非法值(拼写错/越界),非法值返回 Err
|
||
/// (与 update_task 的 status 校验一致),不静默返回空结果误导调用方。
|
||
#[tauri::command]
|
||
pub async fn list_tasks(
|
||
state: State<'_, AppState>,
|
||
project_id: Option<String>,
|
||
query: Option<TaskQuery>,
|
||
) -> Result<Vec<TaskRecord>, String> {
|
||
if let Some(q) = &query {
|
||
// status 值兜底校验(非法值早 Err,不进 DB 层)
|
||
if let Some(status) = &q.status {
|
||
if !TaskStatus::is_valid(status) {
|
||
return Err(format!(
|
||
"非法 status 值 {:?},合法值: {:?}",
|
||
status,
|
||
TaskStatus::valid_values()
|
||
));
|
||
}
|
||
}
|
||
return state.tasks.list_by_query(q).await.map_err(err_str);
|
||
}
|
||
// 旧调用方:单维度 project_id(SQL 下推,fallback 等价改造前行为)
|
||
let tasks = 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)?,
|
||
};
|
||
Ok(tasks)
|
||
}
|
||
|
||
/// 按 id 查任务,找不到返回 Err(供前端详情页)
|
||
#[tauri::command]
|
||
pub async fn get_task_by_id(
|
||
state: State<'_, AppState>,
|
||
id: String,
|
||
) -> Result<TaskRecord, String> {
|
||
state
|
||
.tasks
|
||
.get_by_id(&id)
|
||
.await
|
||
.map_err(err_str)?
|
||
.ok_or_else(|| format!("任务 {} 不存在", id))
|
||
}
|
||
|
||
/// 创建任务,返回完整记录
|
||
#[tauri::command]
|
||
pub async fn create_task(
|
||
state: State<'_, AppState>,
|
||
input: CreateTaskInput,
|
||
) -> Result<TaskRecord, String> {
|
||
let now = now_millis();
|
||
let record = TaskRecord {
|
||
id: new_id(),
|
||
project_id: input.project_id,
|
||
title: input.title,
|
||
description: input.description,
|
||
status: "todo".to_string(),
|
||
priority: input.priority,
|
||
branch_name: input.branch_name,
|
||
assignee: input.assignee,
|
||
workflow_def_id: None,
|
||
base_branch: None,
|
||
review_rounds: 0,
|
||
output_json: None,
|
||
// F-260619-01:空字符串视为不关联(与 AI 工具层一致)
|
||
idea_id: input.idea_id.filter(|s| !s.is_empty()),
|
||
created_at: now.clone(),
|
||
updated_at: now,
|
||
};
|
||
state
|
||
.tasks
|
||
.insert(record.clone())
|
||
.await
|
||
.map_err(err_str)?;
|
||
Ok(record)
|
||
}
|
||
|
||
/// 更新任务单个字段(字段名走 df-storage 白名单校验;status 值走枚举校验)
|
||
#[tauri::command]
|
||
pub async fn update_task(
|
||
state: State<'_, AppState>,
|
||
id: String,
|
||
field: String,
|
||
value: String,
|
||
) -> Result<bool, String> {
|
||
// status 值校验保留:仅对「非法值」(拼写错 in-progess / "in progress" / 大小写错 / 越界)
|
||
// 给出友好早错误(先于 crud.rs 白名单那串冷冰冰的「字段不在白名单」拒)。合法 status 值
|
||
// 不可经本 IPC 写入——crud.rs tasks 白名单已收口移除 status(F-03 batch64 b94e74a /
|
||
// D-260616-04),所有 status 改动须走 advance_task_atomic 状态机(CAS + can_transition +
|
||
// review_rounds 累加,唯一 status 写入路径)。即此 is_valid 校验的「通过」分支永不会触达
|
||
// update_field 的 status 写入(白名单会先拒);它只为非法值兜底 UX,不承担合法值写入职责。
|
||
// priority 值校验同理补在下方:拦截 "abc" / 999 等脏数据静默落库。
|
||
if field == "status" && !TaskStatus::is_valid(&value) {
|
||
return Err(format!(
|
||
"非法 status 值 {:?},合法值: {:?}",
|
||
value,
|
||
TaskStatus::valid_values()
|
||
));
|
||
}
|
||
// priority 值域 0..=3(0=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..=3(0=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)
|
||
.await
|
||
.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.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)
|
||
}
|