You've already forked agentic-coding-workflow
122 lines
4.2 KiB
Go
122 lines
4.2 KiB
Go
// Package project 内的 artifact_service.go 实装 ArtifactService。
|
|
//
|
|
// 设计取舍:
|
|
// - Version 在 (Requirement, Phase) 内自增(max+1),由 service 计算后落库;
|
|
// UNIQUE(requirement_id, phase, version) 兜底竞争,Create 捕获 unique_violation 重试 1 次。
|
|
// - Phase 仅允许 ArtifactPhases(planning/prototyping/auditing),与 DB CHECK 一致。
|
|
// - 已 closed requirement:禁止新建产物版本,返回 CodeRequirementClosed。
|
|
// - 已归档 project:所有写操作经 loadProjectForWrite 拒绝。
|
|
// - 读操作经 loadProjectForRead 校验可见性。
|
|
package project
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
)
|
|
|
|
type artifactService struct {
|
|
repo Repository
|
|
audit audit.Recorder
|
|
}
|
|
|
|
// NewArtifactService 构造 ArtifactService。
|
|
func NewArtifactService(repo Repository, rec audit.Recorder) ArtifactService {
|
|
return &artifactService{repo: repo, audit: rec}
|
|
}
|
|
|
|
func (s *artifactService) Create(ctx context.Context, c Caller, slug string, reqNumber int, in CreateArtifactInput) (*Artifact, error) {
|
|
if !in.Phase.IsArtifactPhase() {
|
|
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing 之一")
|
|
}
|
|
if in.Content == "" {
|
|
return nil, errs.New(errs.CodeInvalidInput, "content 必填")
|
|
}
|
|
p, err := loadProjectForWrite(ctx, s.repo, c, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := s.repo.GetRequirementByNumber(ctx, p.ID, reqNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.Status == StatusClosed {
|
|
return nil, errs.New(errs.CodeRequirementClosed, "已关闭需求禁止新增产物版本")
|
|
}
|
|
out, err := s.createWithRetry(ctx, r.ID, c.UserID, in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.recordAudit(ctx, c, "requirement_artifact.create", out.ID.String(), map[string]any{
|
|
"requirement_id": r.ID.String(), "phase": string(out.Phase), "version": out.Version,
|
|
})
|
|
return out, nil
|
|
}
|
|
|
|
// createWithRetry 取 (requirement, phase) 内 max+1 作为新 version;version 撞 UNIQUE 时重试 1 次。
|
|
func (s *artifactService) createWithRetry(ctx context.Context, reqID, createdBy uuid.UUID, in CreateArtifactInput) (*Artifact, error) {
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
maxVer, err := s.repo.GetMaxArtifactVersion(ctx, reqID, in.Phase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := s.repo.CreateArtifact(ctx, &Artifact{
|
|
ID: uuid.New(), RequirementID: reqID, Phase: in.Phase,
|
|
Version: maxVer + 1, Content: in.Content, Note: in.Note,
|
|
SourceMessageID: in.SourceMessageID,
|
|
CreatedBy: createdBy,
|
|
})
|
|
if err == nil {
|
|
return out, nil
|
|
}
|
|
if !IsUniqueViolation(err) {
|
|
return nil, err
|
|
}
|
|
// version 撞了,下一轮重新取 max
|
|
}
|
|
return nil, errs.New(errs.CodeInternal, "artifact version 持续冲突")
|
|
}
|
|
|
|
func (s *artifactService) List(ctx context.Context, c Caller, slug string, reqNumber int, phase Phase) ([]*Artifact, error) {
|
|
if phase != "" && !phase.IsArtifactPhase() {
|
|
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing 之一")
|
|
}
|
|
p, err := loadProjectForRead(ctx, s.repo, c, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := s.repo.GetRequirementByNumber(ctx, p.ID, reqNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.ListArtifactsByRequirement(ctx, r.ID, phase)
|
|
}
|
|
|
|
func (s *artifactService) GetByVersion(ctx context.Context, c Caller, slug string, reqNumber int, phase Phase, version int) (*Artifact, error) {
|
|
if !phase.IsArtifactPhase() {
|
|
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing 之一")
|
|
}
|
|
p, err := loadProjectForRead(ctx, s.repo, c, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := s.repo.GetRequirementByNumber(ctx, p.ID, reqNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.GetArtifactByVersion(ctx, r.ID, phase, version)
|
|
}
|
|
|
|
func (s *artifactService) recordAudit(ctx context.Context, c Caller, action, targetID string, meta map[string]any) {
|
|
if s.audit == nil {
|
|
return
|
|
}
|
|
uid := c.UserID
|
|
_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
|
|
UserID: &uid, Action: action, TargetType: "requirement_artifact", TargetID: targetID, Metadata: meta,
|
|
})
|
|
}
|