You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -19,22 +19,28 @@ import (
|
||||
)
|
||||
|
||||
type requirementService struct {
|
||||
repo Repository
|
||||
audit audit.Recorder
|
||||
repo Repository
|
||||
audit audit.Recorder
|
||||
emitter PhaseEventEmitter
|
||||
gate GateRunner
|
||||
}
|
||||
|
||||
// NewRequirementService 构造 RequirementService。
|
||||
func NewRequirementService(repo Repository, rec audit.Recorder) RequirementService {
|
||||
return &requirementService{repo: repo, audit: rec}
|
||||
//
|
||||
// emitter 为 nil 时不发 requirement.phase_change 领域事件(仅审计),gate 为 nil
|
||||
// 时保持 any-to-any 旧行为(不做 entry 网关校验)——二者均允许测试/精简场景注入 nil。
|
||||
func NewRequirementService(repo Repository, rec audit.Recorder, emitter PhaseEventEmitter, gate GateRunner) RequirementService {
|
||||
return &requirementService{repo: repo, audit: rec, emitter: emitter, gate: gate}
|
||||
}
|
||||
|
||||
// loadProjectForWrite 解析 slug → 校验 caller 写权限 → 校验未归档。
|
||||
// 成员/角色 ACL:member/admin 角色叠加写授权(见 assertWritable)。
|
||||
func loadProjectForWrite(ctx context.Context, repo Repository, c Caller, slug string) (*Project, error) {
|
||||
p, err := repo.GetProjectBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assertWritable(p, c); err != nil {
|
||||
if err := assertWritable(p, c, callerRole(ctx, repo, p, c)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
@@ -45,12 +51,25 @@ func loadProjectForRead(ctx context.Context, repo Repository, c Caller, slug str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assertReadable(p, c); err != nil {
|
||||
if err := assertReadable(p, c, callerRole(ctx, repo, p, c)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// callerRole 是 loadProjectFor* 的成员角色查询 helper(不在 projectService 上,
|
||||
// 故走 repo)。owner/global-admin 隐式 admin;查询失败按 RoleNone(既有规则兜底)。
|
||||
func callerRole(ctx context.Context, repo Repository, p *Project, c Caller) Role {
|
||||
if c.IsAdmin || p.OwnerID == c.UserID {
|
||||
return RoleAdmin
|
||||
}
|
||||
role, err := repo.GetMemberRole(ctx, p.ID, c.UserID)
|
||||
if err != nil {
|
||||
return RoleNone
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
func (s *requirementService) Create(ctx context.Context, c Caller, slug string, in CreateRequirementInput) (*Requirement, error) {
|
||||
if in.Title == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "title 必填")
|
||||
@@ -193,6 +212,13 @@ func (s *requirementService) ChangePhase(ctx context.Context, c Caller, slug str
|
||||
return r, nil
|
||||
}
|
||||
from := r.Phase
|
||||
// entry 网关:目标 phase 声明的全部前置谓词必须通过。失败返回 CodePhaseGateFailed,
|
||||
// 且不写库、不发事件。gate 为 nil 时跳过(保持 any-to-any)。
|
||||
if s.gate != nil {
|
||||
if err := s.gate.Check(ctx, r, from, to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
out, err := s.repo.UpdateRequirementPhase(ctx, r.ID, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -200,6 +226,19 @@ func (s *requirementService) ChangePhase(ctx context.Context, c Caller, slug str
|
||||
s.recordAudit(ctx, c, "requirement.phase_change", out.ID.String(), map[string]any{
|
||||
"from": string(from), "to": string(to),
|
||||
})
|
||||
// 领域事件(与审计互补):fire-and-forget,投递由 emitter 异步处理,失败不阻塞流转。
|
||||
if s.emitter != nil {
|
||||
s.emitter.EmitPhaseChange(context.WithoutCancel(ctx), PhaseChangeEvent{
|
||||
ProjectID: p.ID,
|
||||
ProjectSlug: p.Slug,
|
||||
RequirementID: out.ID,
|
||||
RequirementNumber: out.Number,
|
||||
From: from,
|
||||
To: to,
|
||||
ActorID: c.UserID,
|
||||
OwnerID: out.OwnerID,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user