新增: 任务关联工程(module_id 数据层V41 + AI module 写工具6个 + 任务构造补字段)
This commit is contained in:
@@ -41,6 +41,8 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result<TaskRecord, rusqlite::Err
|
||||
queue: row.get("queue")?,
|
||||
parent_id: row.get("parent_id")?,
|
||||
content_json: row.get("content_json")?,
|
||||
// 工程系统 V41:任务关联具体工程(module_id),18→19 列同步之一。
|
||||
module_id: row.get("module_id")?,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
@@ -86,6 +88,10 @@ pub struct TaskQuery {
|
||||
/// 查叶子任务(parent_id IS NULL)由专用方法 get_children 之外的语义决定,本字段只做等值匹配。
|
||||
#[serde(default)]
|
||||
pub parent_id: Option<String>,
|
||||
/// 所属工程 ID 过滤(工程系统 V41):Some(id) = 查关联到某 module 的任务。
|
||||
/// 工程维度筛选数据源(任务按工程分列/过滤)。
|
||||
#[serde(default)]
|
||||
pub module_id: Option<String>,
|
||||
/// 排序字段(白名单 created_at/updated_at/priority/status,降序)。P3 基建就绪。
|
||||
pub order_by: Option<String>,
|
||||
/// 分页上限(钳制 ≤500)。P3 基建就绪。
|
||||
@@ -123,25 +129,25 @@ impl_repo!(
|
||||
from_row => |row| task_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
|
||||
"INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)",
|
||||
params![
|
||||
rec.id, rec.project_id, rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||
rec.review_rounds, rec.output_json, rec.idea_id,
|
||||
rec.queue, rec.parent_id, rec.content_json,
|
||||
rec.queue, rec.parent_id, rec.content_json, rec.module_id,
|
||||
rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
},
|
||||
update => |conn, rec| {
|
||||
conn.execute(
|
||||
"UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, idea_id = ?12, queue = ?13, parent_id = ?14, content_json = ?15, updated_at = ?16 WHERE id = ?17",
|
||||
"UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, idea_id = ?12, queue = ?13, parent_id = ?14, content_json = ?15, module_id = ?16, updated_at = ?17 WHERE id = ?18",
|
||||
params![
|
||||
rec.project_id, rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||
rec.review_rounds, rec.output_json, rec.idea_id,
|
||||
rec.queue, rec.parent_id, rec.content_json,
|
||||
rec.queue, rec.parent_id, rec.content_json, rec.module_id,
|
||||
rec.updated_at, rec.id
|
||||
],
|
||||
)
|
||||
@@ -151,14 +157,14 @@ impl_repo!(
|
||||
impl TaskRepo {
|
||||
/// 列出未删除任务(deleted_at IS NULL)— 对标 ProjectRepo::list_active
|
||||
///
|
||||
/// 显式列出全部 18 个 TaskRecord 列名(同 ProjectRepo::list_active 写法),
|
||||
/// 显式列出全部 19 个 TaskRecord 列名(同 ProjectRepo::list_active 写法),
|
||||
/// 不 SELECT deleted_at:TaskRecord 不带该字段,取了 from_row 会因未知列报错。
|
||||
pub async fn list_active(&self) -> Result<Vec<TaskRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
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, idea_id, queue, parent_id, content_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC")
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, created_at, updated_at FROM tasks WHERE deleted_at IS NULL ORDER BY created_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| task_from_row(row))
|
||||
@@ -259,7 +265,7 @@ impl TaskRepo {
|
||||
}
|
||||
// 回读更新后的记录(含新 status / 累加后的 review_rounds / 新 updated_at)。
|
||||
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, idea_id, queue, parent_id, content_json, created_at, updated_at FROM tasks WHERE id = ?1")
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, created_at, updated_at FROM tasks WHERE id = ?1")
|
||||
.map_err(storage_err)?;
|
||||
let row = stmt
|
||||
.query_row(params![id], |row| task_from_row(row))
|
||||
@@ -281,7 +287,7 @@ impl TaskRepo {
|
||||
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, idea_id, queue, parent_id, content_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL AND project_id = ?1 ORDER BY created_at DESC")
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, 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![pid], |row| task_from_row(row))
|
||||
@@ -300,15 +306,17 @@ impl TaskRepo {
|
||||
///
|
||||
/// 复用 KnowledgeRepo::search 的「动态 WHERE + 参数绑定」模式,但用累积式条件收集
|
||||
/// (Vec<String> WHERE 子句 + Vec<rusqlite::Value> 参数)替代 if-let 二分支——
|
||||
/// TaskQuery 有 4 个过滤维度(project_id/status/priority/assignee/keyword),2^n 分支不可行,
|
||||
/// 累积式天然支持任意维度组合,且每个 if-let 分支只 push 子句+参数,新增维度零样板。
|
||||
/// TaskQuery 有多个过滤维度(project_id/status/priority/assignee/keyword/queue/parent_id/
|
||||
/// module_id),2^n 分支不可行,累积式天然支持任意维度组合,且每个 if-let 分支只 push
|
||||
/// 子句+参数,新增维度零样板。
|
||||
///
|
||||
/// - 过滤维度:project_id / status / priority / assignee(精确等值)+ keyword(title/description LIKE)
|
||||
/// - 过滤维度:project_id / status / priority / assignee / queue / parent_id / module_id
|
||||
/// (精确等值)+ keyword(title/description LIKE)
|
||||
/// - keyword 拼成 `(title LIKE ?N OR description LIKE ?M)`,pattern = `%kw%`(对齐知识库 search)
|
||||
/// - order_by 白名单(validate_order_by 防 SQL 注入),默认 created_at,恒 DESC(与 list_active 一致)
|
||||
/// - limit/offset 钳制(limit ≤500 防滥用,对齐 conversation_repo::list_recent 的 limit≤200 思路)
|
||||
/// - deleted_at IS NULL 恒带(回收站任务不进结果,语义同 list_active,不可被 query 关闭)
|
||||
/// - 显式列出全部 18 列(不 SELECT deleted_at:TaskRecord 不带该字段,取了 from_row 报未知列)
|
||||
/// - 显式列出全部 19 列(不 SELECT deleted_at:TaskRecord 不带该字段,取了 from_row 报未知列)
|
||||
///
|
||||
/// 空 query(全 None)→ 等价 list_active(全量未删,created_at DESC),向后兼容。
|
||||
/// status 值合法性由上层 list_tasks 命令(TaskStatus::is_valid)兜底,本层不过滤值集
|
||||
@@ -337,6 +345,7 @@ impl TaskRepo {
|
||||
let keyword = query.keyword.clone();
|
||||
let queue = query.queue.clone();
|
||||
let parent_id = query.parent_id.clone();
|
||||
let module_id = query.module_id.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
@@ -376,6 +385,11 @@ impl TaskRepo {
|
||||
where_clauses.push(format!("parent_id = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(pid.clone()));
|
||||
}
|
||||
// module_id(工程系统 V41):所属工程等值过滤(查关联到某 module 的任务)
|
||||
if let Some(mid) = &module_id {
|
||||
where_clauses.push(format!("module_id = ?{}", params_vec.len() + 1));
|
||||
params_vec.push(Box::new(mid.clone()));
|
||||
}
|
||||
// keyword: title/description LIKE %kw%(P2,对齐知识库 search 的 LIKE 模式)
|
||||
if let Some(kw) = &keyword {
|
||||
let escaped = kw.replace('%', "\\%").replace('_', "\\_");
|
||||
@@ -399,12 +413,12 @@ impl TaskRepo {
|
||||
};
|
||||
|
||||
// 拼 SQL:?N 占位符序号与 params_vec 顺序严格对应(累积时按 +1 递增保证)。
|
||||
// 显式列出全部 18 列(含 V29 queue/parent_id/content_json,不 SELECT deleted_at:
|
||||
// TaskRecord 不带该字段,取了 from_row 会因未知列报错)。
|
||||
// 显式列出全部 19 列(含 V29 queue/parent_id/content_json 与 V41 module_id,
|
||||
// 不 SELECT deleted_at:TaskRecord 不带该字段,取了 from_row 会因未知列报错)。
|
||||
let sql = format!(
|
||||
"SELECT id, project_id, title, description, status, priority, branch_name, \
|
||||
assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, \
|
||||
queue, parent_id, content_json, created_at, updated_at \
|
||||
queue, parent_id, content_json, module_id, created_at, updated_at \
|
||||
FROM tasks WHERE {} ORDER BY {} DESC{}",
|
||||
where_clauses.join(" AND "),
|
||||
order_col,
|
||||
@@ -503,7 +517,7 @@ impl TaskRepo {
|
||||
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, idea_id, queue, parent_id, content_json, created_at, updated_at FROM tasks WHERE deleted_at IS NULL AND parent_id = ?1 ORDER BY created_at ASC")
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, created_at, updated_at FROM tasks WHERE deleted_at IS NULL AND parent_id = ?1 ORDER BY created_at ASC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid], |row| task_from_row(row))
|
||||
@@ -638,11 +652,11 @@ impl TaskRepo {
|
||||
let id = id.to_owned();
|
||||
let new_queue = new_queue.to_owned();
|
||||
let now = now_millis_str();
|
||||
// 显式列出全部 18 列(同 from_row 消费列,不 SELECT deleted_at:
|
||||
// 显式列出全部 19 列(同 from_row 消费列,不 SELECT deleted_at:
|
||||
// TaskRecord 不带该字段,取了 from_row 会因未知列报错)。
|
||||
const TASK_COLS: &str = "id, project_id, title, description, status, priority, branch_name, \
|
||||
assignee, workflow_def_id, base_branch, review_rounds, output_json, \
|
||||
idea_id, queue, parent_id, content_json, created_at, updated_at";
|
||||
idea_id, queue, parent_id, content_json, module_id, created_at, updated_at";
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut guard = conn.blocking_lock();
|
||||
let tx = guard.transaction().map_err(storage_err)?;
|
||||
@@ -715,7 +729,7 @@ impl TaskRepo {
|
||||
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, idea_id, queue, parent_id, content_json, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC")
|
||||
.prepare("SELECT id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, module_id, created_at, updated_at FROM tasks WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| task_from_row(row))
|
||||
@@ -738,8 +752,8 @@ impl TaskRepo {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crud::ProjectRepo;
|
||||
use crate::models::ProjectRecord;
|
||||
use crate::crud::{ProjectModuleRepo, ProjectRepo};
|
||||
use crate::models::{ProjectModuleRecord, ProjectRecord};
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
|
||||
/// 构造一条 TaskRecord fixture(queue/parent_id/status 可定制,V29 新维度 + 聚合测试用 status)。
|
||||
@@ -766,6 +780,7 @@ mod tests {
|
||||
queue: queue.to_string(),
|
||||
parent_id: parent_id.map(|s| s.to_string()),
|
||||
content_json: None,
|
||||
module_id: None,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
}
|
||||
@@ -871,6 +886,66 @@ mod tests {
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_by_query_module_id_filter() {
|
||||
// 工程系统 V41:按 module_id 等值过滤任务。
|
||||
// 需先建 project_modules 行满足 tasks.module_id FK(PRAGMA foreign_keys=ON)。
|
||||
let db = Database::open_in_memory().await.expect("open_in_memory");
|
||||
let project_repo = ProjectRepo::new(&db);
|
||||
project_repo
|
||||
.insert(ProjectRecord {
|
||||
id: "proj-1".to_string(),
|
||||
name: "proj-1".to_string(),
|
||||
description: String::new(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let module_repo = ProjectModuleRepo::new(&db);
|
||||
module_repo
|
||||
.insert(ProjectModuleRecord {
|
||||
id: "mod-1".to_string(),
|
||||
project_id: "proj-1".to_string(),
|
||||
name: "backend".to_string(),
|
||||
path: "/repo/backend".to_string(),
|
||||
git_url: None,
|
||||
stack: None,
|
||||
auto_detected: false,
|
||||
sort_order: 0,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
description: None,
|
||||
status: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let repo = TaskRepo::new(&db);
|
||||
|
||||
// 2 个任务关联 mod-1,1 个无关联工程
|
||||
let mut t1 = trec("t1", "todo", None);
|
||||
t1.module_id = Some("mod-1".to_string());
|
||||
let mut t2 = trec("t2", "todo", None);
|
||||
t2.module_id = Some("mod-1".to_string());
|
||||
repo.insert(t1).await.unwrap();
|
||||
repo.insert(t2).await.unwrap();
|
||||
repo.insert(trec("t3", "todo", None)).await.unwrap();
|
||||
|
||||
let q = TaskQuery {
|
||||
module_id: Some("mod-1".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = repo.list_by_query(&q).await.unwrap();
|
||||
let ids: Vec<_> = res.iter().map(|r| r.id.as_str()).collect();
|
||||
assert_eq!(ids.len(), 2, "module_id 过滤应只返回关联 mod-1 的任务");
|
||||
assert!(ids.contains(&"t1"));
|
||||
assert!(ids.contains(&"t2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_children_returns_only_direct_children() {
|
||||
let repo = setup().await;
|
||||
|
||||
Reference in New Issue
Block a user