Files
Cyrene_v2/internal/runtime/memory/projectindex/project.go
T
AskaEth 61cf80d5ee
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
2026-10-04 13:32:54 +08:00

145 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package projectindex
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
)
// Project 是一个项目索引条目(design §3):链接其
// 各章节的项目标示,连同项目描述。它是层级的第 1 层(索引
// 条目)与第 2 层(描述)。零值不是
// 合法项目:ID 与 Title 必填。
type Project struct {
ID string
Title string
Desc string
CreatedAt time.Time
UpdatedAt time.Time
}
// PutProject 按 id upsert p。id 已存在时更新 title、desc 与
// updated_at,并保留原始的 created_at;id 为新值时
// 补齐缺失时间戳并插入。id 或 title 为空返回 ErrMissingProject
// 或 ErrMissingTitle。
func (s *Store) PutProject(ctx context.Context, p Project) error {
if strings.TrimSpace(p.ID) == "" {
return ErrMissingProject
}
if strings.TrimSpace(p.Title) == "" {
return ErrMissingTitle
}
now := time.Now().UTC()
if p.CreatedAt.IsZero() {
p.CreatedAt = now
}
if p.UpdatedAt.IsZero() {
p.UpdatedAt = now
}
_, err := s.db.ExecContext(ctx, `INSERT INTO project (id, title, desc, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
desc = excluded.desc,
updated_at = excluded.updated_at`,
p.ID, p.Title, p.Desc, formatTime(p.CreatedAt), formatTime(p.UpdatedAt))
if err != nil {
return fmt.Errorf("projectindex: upsert project: %w", err)
}
s.log.Debug("project upserted", "id", p.ID)
return nil
}
// GetProject 返回给定 id 的项目。id 为空或
// 不存在时返回 ErrProjectNotFound。
func (s *Store) GetProject(ctx context.Context, id string) (*Project, error) {
if strings.TrimSpace(id) == "" {
return nil, ErrProjectNotFound
}
row := s.db.QueryRowContext(ctx, `SELECT id, title, desc, created_at, updated_at
FROM project WHERE id = ?`, id)
p, err := scanProject(row)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrProjectNotFound
}
if err != nil {
return nil, fmt.Errorf("projectindex: get project: %w", err)
}
return p, nil
}
// DeleteProject 按 id 删除项目,并级联删除引用它的每个
// 章节。id 为空、或没有删除任何
// 项目行时返回 ErrProjectNotFound。删项目与删章节发生在
// 同一事务内,因此失败绝不会留下孤儿项目或章节。
func (s *Store) DeleteProject(ctx context.Context, id string) error {
if strings.TrimSpace(id) == "" {
return ErrProjectNotFound
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("projectindex: begin delete tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
res, err := tx.ExecContext(ctx, `DELETE FROM project_chapter WHERE project_id = ?`, id)
if err != nil {
return fmt.Errorf("projectindex: delete chapters: %w", err)
}
if _, err := res.RowsAffected(); err != nil {
return fmt.Errorf("projectindex: delete chapters row count: %w", err)
}
res, err = tx.ExecContext(ctx, `DELETE FROM project WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("projectindex: delete project: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("projectindex: delete row count: %w", err)
}
if n == 0 {
return ErrProjectNotFound
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("projectindex: commit delete tx: %w", err)
}
s.log.Debug("project deleted", "id", id)
return nil
}
// scanProject 读取一行 project。
func scanProject(sc rowScanner) (*Project, error) {
var (
p Project
createdAt, updatedAt string
)
if err := sc.Scan(&p.ID, &p.Title, &p.Desc, &createdAt, &updatedAt); err != nil {
return nil, err
}
var err error
if p.CreatedAt, err = parseTime(createdAt); err != nil {
return nil, err
}
if p.UpdatedAt, err = parseTime(updatedAt); err != nil {
return nil, err
}
return &p, nil
}
// newID 返回一个 128 位密码学随机 id,十六进制编码。它避开
// UUID 依赖,使本包保持自包含。rand.Read 只在
// 平台熵源损坏时失败,因此错误向上传播。
func newID() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("projectindex: generate id: %w", err)
}
return hex.EncodeToString(b), nil
}