// 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/implementing/reviewing),与 DB CHECK 一致。 // - Approve 给某版本打 verdict(pass/fail);verdict=pass 时把阶段指针指向该产物, // 供 RequirementService 的阶段网关放行后续 phase。 // - 已 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/implementing/reviewing 之一") } 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/implementing/reviewing 之一") } 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/implementing/reviewing 之一") } 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) Approve(ctx context.Context, c Caller, slug string, reqNumber int, phase Phase, version int, verdict Verdict) (*Artifact, error) { if !phase.IsArtifactPhase() { return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing/implementing/reviewing 之一") } if verdict != VerdictPass && verdict != VerdictFail { return nil, errs.New(errs.CodeInvalidInput, "verdict 必须是 pass 或 fail") } 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, "已关闭需求禁止批准产物") } art, err := s.repo.GetArtifactByVersion(ctx, r.ID, phase, version) if err != nil { return nil, err } out, err := s.repo.ApproveArtifact(ctx, art.ID, verdict) if err != nil { return nil, err } // verdict=pass 才把阶段指针指向此产物(网关据此放行);fail 仅落 verdict、不动指针。 if verdict == VerdictPass { if _, err := s.repo.UpsertPhasePointer(ctx, r.ID, phase, out.ID, c.UserID); err != nil { return nil, err } } s.recordAudit(ctx, c, "requirement_artifact.approve", out.ID.String(), map[string]any{ "requirement_id": r.ID.String(), "phase": string(phase), "version": version, "verdict": string(verdict), }) return out, nil } 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, }) }