diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 7e72a5a6..32078497 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -361,6 +361,27 @@ func (db *DB) GetConversationLite(id string) (*Conversation, error) { return &conv, nil } +// CountConversations 统计对话数量。 +func (db *DB) CountConversations(search string) (int, error) { + var count int + var err error + if search != "" { + searchPattern := "%" + search + "%" + err = db.QueryRow( + `SELECT COUNT(*) FROM conversations c + WHERE c.title LIKE ? + OR EXISTS (SELECT 1 FROM messages m WHERE m.conversation_id = c.id AND m.content LIKE ?)`, + searchPattern, searchPattern, + ).Scan(&count) + } else { + err = db.QueryRow(`SELECT COUNT(*) FROM conversations`).Scan(&count) + } + if err != nil { + return 0, fmt.Errorf("统计对话失败: %w", err) + } + return count, nil +} + // ListConversations 列出所有对话 func (db *DB) ListConversations(limit, offset int, search string) ([]*Conversation, error) { var rows *sql.Rows @@ -430,6 +451,73 @@ func (db *DB) ListConversations(limit, offset int, search string) ([]*Conversati return conversations, nil } +const ungroupedConversationsSQL = ` + FROM conversations c + WHERE NOT EXISTS ( + SELECT 1 FROM conversation_group_mappings cgm WHERE cgm.conversation_id = c.id + )` + +// CountUngroupedConversations 统计不在任何分组中的对话数量。 +func (db *DB) CountUngroupedConversations() (int, error) { + var count int + if err := db.QueryRow(`SELECT COUNT(*) ` + ungroupedConversationsSQL).Scan(&count); err != nil { + return 0, fmt.Errorf("统计未分组对话失败: %w", err) + } + return count, nil +} + +// ListUngroupedConversations 列出不在任何分组中的对话(最近对话侧栏)。 +func (db *DB) ListUngroupedConversations(limit, offset int) ([]*Conversation, error) { + rows, err := db.Query( + `SELECT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, c.project_id `+ + ungroupedConversationsSQL+` + ORDER BY c.updated_at DESC + LIMIT ? OFFSET ?`, + limit, offset, + ) + if err != nil { + return nil, fmt.Errorf("查询未分组对话失败: %w", err) + } + defer rows.Close() + + var conversations []*Conversation + for rows.Next() { + var conv Conversation + var createdAt, updatedAt string + var pinned int + var projectID sql.NullString + + if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &projectID); err != nil { + return nil, fmt.Errorf("扫描对话失败: %w", err) + } + if projectID.Valid { + conv.ProjectID = strings.TrimSpace(projectID.String) + } + + var err1, err2 error + conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err1 != nil { + conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err1 != nil { + conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + } + + conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt) + if err2 != nil { + conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt) + } + if err2 != nil { + conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + } + + conv.Pinned = pinned != 0 + conversations = append(conversations, &conv) + } + + return conversations, rows.Err() +} + // UpdateConversationTitle 更新对话标题 func (db *DB) UpdateConversationTitle(id, title string) error { // 注意:不更新 updated_at,因为重命名操作不应该改变对话的更新时间 diff --git a/internal/database/project.go b/internal/database/project.go index 9f21bc79..4a675de6 100644 --- a/internal/database/project.go +++ b/internal/database/project.go @@ -112,10 +112,30 @@ func (db *DB) GetProject(id string) (*Project, error) { return &p, nil } +// CountProjects 统计项目数量。 +func (db *DB) CountProjects(status, search string) (int, error) { + query := `SELECT COUNT(*) FROM projects WHERE 1=1` + args := []interface{}{} + if s := strings.TrimSpace(status); s != "" { + query += " AND status = ?" + args = append(args, s) + } + if q := strings.TrimSpace(search); q != "" { + pattern := "%" + q + "%" + query += " AND (name LIKE ? OR COALESCE(description,'') LIKE ?)" + args = append(args, pattern, pattern) + } + var count int + if err := db.QueryRow(query, args...).Scan(&count); err != nil { + return 0, fmt.Errorf("统计项目失败: %w", err) + } + return count, nil +} + // ListProjects 列出项目。 -func (db *DB) ListProjects(status string, limit, offset int) ([]*Project, error) { +func (db *DB) ListProjects(status, search string, limit, offset int) ([]*Project, error) { if limit <= 0 { - limit = 200 + limit = 50 } query := `SELECT id, name, COALESCE(description,''), COALESCE(scope_json,''), status, pinned, created_at, updated_at FROM projects WHERE 1=1` @@ -124,6 +144,11 @@ func (db *DB) ListProjects(status string, limit, offset int) ([]*Project, error) query += " AND status = ?" args = append(args, s) } + if q := strings.TrimSpace(search); q != "" { + pattern := "%" + q + "%" + query += " AND (name LIKE ? OR COALESCE(description,'') LIKE ?)" + args = append(args, pattern, pattern) + } query += " ORDER BY pinned DESC, updated_at DESC LIMIT ? OFFSET ?" args = append(args, limit, offset) diff --git a/internal/database/project_time_test.go b/internal/database/project_time_test.go index c064ee49..b8303c5c 100644 --- a/internal/database/project_time_test.go +++ b/internal/database/project_time_test.go @@ -37,7 +37,7 @@ func TestListProjectFacts_updatedAtJSON(t *testing.T) { if err != nil { t.Fatal(err) } - projects, err := db.ListProjects("", 1, 0) + projects, err := db.ListProjects("", "", 1, 0) if err != nil || len(projects) == 0 { t.Skip("no projects") }