新增: 项目归档
This commit is contained in:
@@ -278,6 +278,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
@@ -426,6 +427,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
|
||||
@@ -255,6 +255,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: Some("/tmp".into()),
|
||||
stack: Some("rust".into()),
|
||||
archived: false,
|
||||
created_at: "0".to_string(),
|
||||
updated_at: "0".to_string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! 项目域 Repo:ProjectRepo / BranchRepo / ReleaseRepo / WorkflowRepo / NodeExecutionRepo
|
||||
//! 项目域 Repo:ProjectRepo / BranchRepo / ReleaseRepo / WorkflowRepo / NodeExecutionRepo
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -125,6 +126,7 @@ fn project_from_row(row: &Row<'_>) -> std::result::Result<ProjectRecord, rusqlit
|
||||
idea_id: row.get("idea_id")?,
|
||||
path: row.get("path")?,
|
||||
stack: row.get("stack")?,
|
||||
archived: row.get::<_, i32>("archived")? != 0,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
@@ -200,20 +202,20 @@ impl_repo!(
|
||||
from_row => |row| project_from_row(row),
|
||||
insert => |conn, rec| {
|
||||
conn.execute(
|
||||
"INSERT INTO projects (id, name, description, status, idea_id, path, stack, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
"INSERT INTO projects (id, name, description, status, idea_id, path, stack, archived, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
params![
|
||||
rec.id, rec.name, rec.description, rec.status.as_str(), rec.idea_id,
|
||||
rec.path, rec.stack, rec.created_at, rec.updated_at
|
||||
rec.path, rec.stack, rec.archived as i32, rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
},
|
||||
update => |conn, rec| {
|
||||
conn.execute(
|
||||
"UPDATE projects SET name = ?1, description = ?2, status = ?3, idea_id = ?4, path = ?5, stack = ?6, updated_at = ?7 WHERE id = ?8",
|
||||
"UPDATE projects SET name = ?1, description = ?2, status = ?3, idea_id = ?4, path = ?5, stack = ?6, archived = ?7, updated_at = ?8 WHERE id = ?9",
|
||||
params![
|
||||
rec.name, rec.description, rec.status.as_str(), rec.idea_id,
|
||||
rec.path, rec.stack, rec.updated_at, rec.id
|
||||
rec.path, rec.stack, rec.archived as i32, rec.updated_at, rec.id
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -226,7 +228,7 @@ impl ProjectRepo {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare("SELECT id, name, description, status, idea_id, path, stack, created_at, updated_at FROM projects WHERE deleted_at IS NULL ORDER BY created_at DESC")
|
||||
.prepare("SELECT id, name, description, status, idea_id, path, stack, archived, created_at, updated_at FROM projects WHERE deleted_at IS NULL AND archived = 0 ORDER BY created_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| project_from_row(row))
|
||||
@@ -261,10 +263,10 @@ impl ProjectRepo {
|
||||
// 保证所有项目有可比活跃时间。ORDER BY 该列 DESC,同时间 created_at DESC 兜底稳定。
|
||||
let mut stmt = guard
|
||||
.prepare(
|
||||
"SELECT id, name, description, status, idea_id, path, stack, created_at, updated_at, \
|
||||
"SELECT id, name, description, status, idea_id, path, stack, archived, created_at, updated_at, \
|
||||
COALESCE((SELECT MAX(created_at) FROM project_events WHERE project_id = projects.id), \
|
||||
updated_at) AS last_active_at \
|
||||
FROM projects WHERE deleted_at IS NULL \
|
||||
FROM projects WHERE deleted_at IS NULL AND archived = 0 \
|
||||
ORDER BY last_active_at DESC, created_at DESC",
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
@@ -315,8 +317,8 @@ impl ProjectRepo {
|
||||
// 动态拼 WHERE + 参数。WHERE 1=1 起点 + deleted_at IS NULL 固定基础条件,
|
||||
// 后续可选条件 AND 拼接(对齐 KnowledgeRepo::search 的 if-let 分支模式)。
|
||||
let mut sql = String::from(
|
||||
"SELECT id, name, description, status, idea_id, path, stack, created_at, updated_at \
|
||||
FROM projects WHERE deleted_at IS NULL",
|
||||
"SELECT id, name, description, status, idea_id, path, stack, archived, created_at, updated_at \
|
||||
FROM projects WHERE deleted_at IS NULL AND archived = 0",
|
||||
);
|
||||
// keyword 用 ?N 占位两次(name LIKE ? AND description LIKE ? 同 pattern),
|
||||
// 对齐 KnowledgeRepo::search 的 `params![pattern, pattern, ...]`。
|
||||
@@ -421,7 +423,7 @@ impl ProjectRepo {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare("SELECT id, name, description, status, idea_id, path, stack, created_at, updated_at FROM projects WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC")
|
||||
.prepare("SELECT id, name, description, status, idea_id, path, stack, archived, created_at, updated_at FROM projects WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| project_from_row(row))
|
||||
@@ -500,6 +502,54 @@ impl ProjectRepo {
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 归档/取消归档(归档功能 P2,V48)。
|
||||
///
|
||||
/// 纯元数据标记,仅作用于未软删项目(回收站优先级高于归档,先恢复再操作)。
|
||||
/// archived=true 后项目退出主视图(list_active/list_active_with_activity/list_by_query
|
||||
/// 恒带 archived = 0),仅在归档视图可见。对标 IdeaRepo::set_archived。返回是否命中。
|
||||
/// 注:archived 不进 update_field 通用白名单,防工具层旁路改写(布尔语义走专用通道)。
|
||||
pub async fn set_archived(&self, id: &str, archived: bool) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(
|
||||
"UPDATE projects SET archived = ?1, updated_at = ?2 WHERE id = ?3 AND deleted_at IS NULL",
|
||||
params![if archived { 1 } else { 0 }, now, id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 列出归档项目(archived = 1 且未软删),按更新时间降序(最近归档在前)。
|
||||
///
|
||||
/// 归档视图数据源,与 list_active 对偶(恒带 archived = 1)。
|
||||
/// 对标 IdeaRepo::list_archived。
|
||||
pub async fn list_archived(&self) -> Result<Vec<ProjectRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare("SELECT id, name, description, status, idea_id, path, stack, archived, created_at, updated_at FROM projects WHERE deleted_at IS NULL AND archived = 1 ORDER BY updated_at DESC")
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| project_from_row(row))
|
||||
.map_err(storage_err)?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(r.map_err(storage_err)?);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 彻底删除:事务级联删全部关联子表→projects(不可恢复)
|
||||
///
|
||||
/// SQLite 已开 PRAGMA foreign_keys=ON 但表无 ON DELETE CASCADE,ALTER 改不了 FK 约束,
|
||||
@@ -718,6 +768,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! 任务横向关联 Repo:TaskLinkRepo(task_links 表,知识图谱 Phase 1 V29)
|
||||
//! 任务横向关联 Repo:TaskLinkRepo(task_links 表,知识图谱 Phase 1 V29)
|
||||
//!
|
||||
//! 表达任务间 depends_on / blocks / relates_to 关系,AI 拓扑排序编排调度的基础。
|
||||
//! 对标设计 docs/02-架构设计/专项设计/项目知识图谱与任务队列系统-2026-06-26.md §2.2。
|
||||
@@ -376,6 +377,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! 任务域 Repo:TaskRepo(含 advance_status_atomic 状态机收口)
|
||||
//! 任务域 Repo:TaskRepo(含 advance_status_atomic 状态机收口)
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -987,6 +988,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
@@ -1089,6 +1091,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
|
||||
@@ -103,6 +103,7 @@ mod tests {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! 数据库迁移 — 建表 SQL 与版本管理
|
||||
//! 数据库迁移 — 建表 SQL 与版本管理
|
||||
|
||||
use crate::migrations_helpers::column_exists;
|
||||
use anyhow::Result;
|
||||
@@ -55,7 +56,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 发起时一次声明存续期生效,随会话同亡)。
|
||||
// V46/V47 = 任务/灵感归档(archived INTEGER NOT NULL DEFAULT 0,归档功能 P1,
|
||||
// 对标 ai_conversations.archived V4;归档与软删正交:回收站=删除缓冲,归档=低频可见保检索)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 47] = [
|
||||
// V48 = 项目归档(projects.archived 列,归档功能 P2,同款三方对齐)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 48] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -105,6 +107,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// V47 = 灵感归档(ideas.archived 列,同款)。归档与软删正交:回收站=删除缓冲,归档=低频可见保检索。
|
||||
(46, migrate_v46),
|
||||
(47, migrate_v47),
|
||||
// V48 = 项目归档(projects.archived 列,归档功能 P2,对标 V46/V47 同款)。
|
||||
(48, migrate_v48),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -1404,6 +1408,25 @@ fn migrate_v47(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V48: 幂等补 projects.archived 列(项目归档,归档功能 P2)
|
||||
///
|
||||
/// 对标 V46 tasks / V47 ideas 同款:归档与软删正交(回收站=删除缓冲,归档=低频可见保检索)。
|
||||
/// archived 不进 update_field 通用白名单,走专用 set_archived(布尔语义 + 防旁路)。
|
||||
/// 活跃查询(list_active/list_active_with_activity/list_by_query)恒带 `archived = 0`。
|
||||
/// - **幂等**:column_exists 守卫;**可逆**:`ALTER TABLE projects DROP COLUMN archived`。
|
||||
fn migrate_v48(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "projects", "archived") {
|
||||
conn.execute(
|
||||
"ALTER TABLE projects ADD COLUMN archived INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
tracing::info!("v48: 补建 projects.archived 列(项目归档)");
|
||||
}
|
||||
conn.execute("INSERT OR IGNORE INTO schema_version (version) VALUES (?)", [48])?;
|
||||
tracing::info!("迁移 v48 完成: projects 加 archived 列");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
@@ -2417,8 +2440,13 @@ mod tests {
|
||||
column_exists(&conn, "ideas", "archived"),
|
||||
"ideas.archived 列缺失(V47 加)"
|
||||
);
|
||||
// projects.archived(V48 项目归档 P2)
|
||||
assert!(
|
||||
column_exists(&conn, "projects", "archived"),
|
||||
"projects.archived 列缺失(V48 加)"
|
||||
);
|
||||
|
||||
// 3. schema_version 应推进到 45(全量迁移成功落版本号)
|
||||
// 3. schema_version 应推进到 48(全量迁移成功落版本号)
|
||||
let max_version: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(version), 0) FROM schema_version",
|
||||
@@ -2427,8 +2455,8 @@ mod tests {
|
||||
)
|
||||
.expect("查 schema_version 应成功");
|
||||
assert_eq!(
|
||||
max_version, 47,
|
||||
"全量迁移后 schema_version 应为 47(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version, 48,
|
||||
"全量迁移后 schema_version 应为 48(实际 {}),说明某条 migrate_vN 链路断在中间",
|
||||
max_version
|
||||
);
|
||||
|
||||
@@ -2505,7 +2533,7 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v, 47, "首轮应推进到 47");
|
||||
assert_eq!(max_v, 48, "首轮应推进到 48");
|
||||
|
||||
// 清空版本表强制全链第二遍(每步 execute 第二次)
|
||||
conn.execute("DELETE FROM schema_version", []).unwrap();
|
||||
@@ -2517,7 +2545,7 @@ mod tests {
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(max_v2, 47, "重跑后应重新推进到 47");
|
||||
assert_eq!(max_v2, 48, "重跑后应重新推进到 48");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -64,6 +64,11 @@ pub struct ProjectRecord {
|
||||
pub path: Option<String>,
|
||||
/// 技术栈 JSON 数组字符串(如 ["rust","vue","tauri"],由探测填充,可空)
|
||||
pub stack: Option<String>,
|
||||
/// 项目归档标记(归档功能 P2,V48)。true=归档态,退出主视图(list_active/
|
||||
/// list_active_with_activity/list_by_query 恒带 archived = 0),仅在归档视图可见。
|
||||
/// 与软删正交:回收站=删除缓冲,归档=低频可见保检索。#[serde(default)] 兼容旧前端 JSON。
|
||||
#[serde(default)]
|
||||
pub archived: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fn project(id: &str) -> ProjectRecord {
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
archived: false,
|
||||
created_at: now_ts(),
|
||||
updated_at: now_ts(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user