This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+44 -4
View File
@@ -3,7 +3,9 @@
// 设计取舍:
// - Version 在 (Requirement, Phase) 内自增(max+1),由 service 计算后落库;
// UNIQUE(requirement_id, phase, version) 兜底竞争,Create 捕获 unique_violation 重试 1 次。
// - Phase 仅允许 ArtifactPhases(planning/prototyping/auditing),与 DB CHECK 一致。
// - Phase 仅允许 ArtifactPhases(planning/prototyping/auditing/implementing/reviewing),与 DB CHECK 一致。
// - Approve 给某版本打 verdict(pass/fail);verdict=pass 时把阶段指针指向该产物,
// 供 RequirementService 的阶段网关放行后续 phase。
// - 已 closed requirement:禁止新建产物版本,返回 CodeRequirementClosed。
// - 已归档 project:所有写操作经 loadProjectForWrite 拒绝。
// - 读操作经 loadProjectForRead 校验可见性。
@@ -30,7 +32,7 @@ func NewArtifactService(repo Repository, rec audit.Recorder) ArtifactService {
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 之一")
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing/implementing/reviewing 之一")
}
if in.Content == "" {
return nil, errs.New(errs.CodeInvalidInput, "content 必填")
@@ -82,7 +84,7 @@ func (s *artifactService) createWithRetry(ctx context.Context, reqID, createdBy
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 之一")
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing/implementing/reviewing 之一")
}
p, err := loadProjectForRead(ctx, s.repo, c, slug)
if err != nil {
@@ -97,7 +99,7 @@ func (s *artifactService) List(ctx context.Context, c Caller, slug string, reqNu
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 之一")
return nil, errs.New(errs.CodeInvalidInput, "phase 必须是 planning/prototyping/auditing/implementing/reviewing 之一")
}
p, err := loadProjectForRead(ctx, s.repo, c, slug)
if err != nil {
@@ -110,6 +112,44 @@ func (s *artifactService) GetByVersion(ctx context.Context, c Caller, slug strin
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