package projectindex import ( "context" "fmt" "strings" "time" ) // Chapter 是一个项目章节(层级第 3 层):项目被切分成的 // 深层内容。ProjectID 把它链到所属项目。零值 // 不是合法章节:AddChapter 要求 title 与 // content 非空,并生成 id。 type Chapter struct { ID string ProjectID string Title string Content string CreatedAt time.Time UpdatedAt time.Time } // AddChapter 以 projectID 持久化 ch,分配 id 与缺失的 // 时间戳。它要求 projectID、title 与 content 非空;id 为空时 // 自动生成。项目行本身不必存在,因此章节可以在其父项目 // 之前或之后添加,但 DeleteProject 会把它们一并级联 // 删除。 func (s *Store) AddChapter(ctx context.Context, projectID string, ch Chapter) error { if strings.TrimSpace(projectID) == "" { return ErrMissingProject } if strings.TrimSpace(ch.Title) == "" { return ErrMissingTitle } if strings.TrimSpace(ch.Content) == "" { return ErrMissingContent } now := time.Now().UTC() if ch.ID == "" { id, err := newID() if err != nil { return err } ch.ID = id } if ch.CreatedAt.IsZero() { ch.CreatedAt = now } if ch.UpdatedAt.IsZero() { ch.UpdatedAt = now } _, err := s.db.ExecContext(ctx, `INSERT INTO project_chapter (id, project_id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, ch.ID, projectID, ch.Title, ch.Content, formatTime(ch.CreatedAt), formatTime(ch.UpdatedAt)) if err != nil { return fmt.Errorf("projectindex: insert chapter: %w", err) } s.log.Debug("chapter added", "project_id", projectID, "chapter_id", ch.ID) return nil } // ListChapters 按插入顺序(created_at ASC、rowid ASC)返回 projectID 的每个 // 章节 —— rowid 打破 created_at 并列,使同一时钟刻内写入的章节 // 保持其插入顺序。没有条目时返回空的 // (非 nil)切片;projectID 为空返回 // ErrMissingProject。 func (s *Store) ListChapters(ctx context.Context, projectID string) ([]Chapter, error) { if strings.TrimSpace(projectID) == "" { return nil, ErrMissingProject } rows, err := s.db.QueryContext(ctx, `SELECT id, project_id, title, content, created_at, updated_at FROM project_chapter WHERE project_id = ? ORDER BY created_at ASC, rowid ASC`, projectID) if err != nil { return nil, fmt.Errorf("projectindex: list chapters: %w", err) } defer func() { _ = rows.Close() }() chapters := make([]Chapter, 0) for rows.Next() { ch, err := scanChapter(rows) if err != nil { return nil, fmt.Errorf("projectindex: scan chapter: %w", err) } chapters = append(chapters, ch) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("projectindex: iterate chapters: %w", err) } return chapters, nil } // scanChapter 读取一行 project_chapter。 func scanChapter(sc rowScanner) (Chapter, error) { var ( ch Chapter createdAt, updatedAt string ) if err := sc.Scan(&ch.ID, &ch.ProjectID, &ch.Title, &ch.Content, &createdAt, &updatedAt); err != nil { return Chapter{}, err } var err error if ch.CreatedAt, err = parseTime(createdAt); err != nil { return Chapter{}, err } if ch.UpdatedAt, err = parseTime(updatedAt); err != nil { return Chapter{}, err } return ch, nil }