You've already forked agentic-coding-workflow
284 lines
11 KiB
Go
284 lines
11 KiB
Go
// Package project 承载 Project / Requirement / Issue 三个 PM 聚合根的领域类型与
|
|
// Service 接口契约。本文件只做类型与签名声明,业务实现在 *_service.go。
|
|
//
|
|
// 三个聚合根共享同一个 Go 包是有意为之:它们在 schema 上紧耦合(Issue 引用
|
|
// Requirement,Requirement 属于 Project),且对外只暴露一组 chi route,拆包
|
|
// 反而要互相 import。
|
|
package project
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Phase 是 Requirement 的六阶段枚举。phase 不强制单向流动,可任意跳跃/回退。
|
|
// 字符串常量保持与 DB CHECK 约束一致。
|
|
type Phase string
|
|
|
|
// 六阶段枚举值,与最新 phase CHECK 约束一致(prototyping 由 migrations/0011 加入)。
|
|
const (
|
|
PhasePlanning Phase = "planning"
|
|
PhasePrototyping Phase = "prototyping"
|
|
PhaseAuditing Phase = "auditing"
|
|
PhaseImplementing Phase = "implementing"
|
|
PhaseReviewing Phase = "reviewing"
|
|
PhaseDone Phase = "done"
|
|
)
|
|
|
|
// AllPhases 用于校验 phase 字符串合法性,顺序也对应前端看板/流转条的列序。
|
|
var AllPhases = []Phase{PhasePlanning, PhasePrototyping, PhaseAuditing, PhaseImplementing, PhaseReviewing, PhaseDone}
|
|
|
|
// IsValid 报告 p 是否在枚举集合内。
|
|
func (p Phase) IsValid() bool {
|
|
for _, x := range AllPhases {
|
|
if p == x {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Status 是 Requirement / Issue 通用的开/关状态。
|
|
type Status string
|
|
|
|
// 开/关状态枚举值,与 DB CHECK 约束一致。
|
|
const (
|
|
StatusOpen Status = "open"
|
|
StatusClosed Status = "closed"
|
|
)
|
|
|
|
// Visibility 控制 Project 的可见域:private 仅 owner+admin 可读,
|
|
// internal 所有登录用户可读;写权限两者都仅限 owner+admin。
|
|
type Visibility string
|
|
|
|
// 可见性枚举值,与 DB CHECK 约束一致。
|
|
const (
|
|
VisibilityPrivate Visibility = "private"
|
|
VisibilityInternal Visibility = "internal"
|
|
)
|
|
|
|
// IsValid 报告 v 是否在枚举集合内。
|
|
func (v Visibility) IsValid() bool {
|
|
return v == VisibilityPrivate || v == VisibilityInternal
|
|
}
|
|
|
|
// Project 是顶层聚合根。ArchivedAt 为 nil 表示未归档。
|
|
type Project struct {
|
|
ID uuid.UUID
|
|
Slug string
|
|
Name string
|
|
Description string
|
|
Visibility Visibility
|
|
OwnerID uuid.UUID
|
|
ArchivedAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Requirement 表示一个项目内的需求。Number 在 (project_id) 内自增。
|
|
// WorkspaceID 为 nil 表示未关联任何 workspace;非 nil 时由 Service 层保证
|
|
// 该 workspace 与本 requirement 同 project(Plan 3 引入)。
|
|
type Requirement struct {
|
|
ID uuid.UUID
|
|
ProjectID uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
Number int
|
|
Title string
|
|
Description string
|
|
Phase Phase
|
|
Status Status
|
|
OwnerID uuid.UUID
|
|
ClosedAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Issue 可挂载到某个 Requirement,也可游离(RequirementID == nil)。
|
|
// AssigneeID 为 nil 表示未指派。CreatedBy 永远存在(DELETE RESTRICT)。
|
|
// WorkspaceID 为 nil 表示未关联 workspace;非 nil 时 DB 通过复合外键
|
|
// (project_id, workspace_id) 拒绝跨 project 关联(Plan 3 引入)。
|
|
type Issue struct {
|
|
ID uuid.UUID
|
|
ProjectID uuid.UUID
|
|
RequirementID *uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
Number int
|
|
Title string
|
|
Description string
|
|
Status Status
|
|
AssigneeID *uuid.UUID
|
|
CreatedBy uuid.UUID
|
|
ClosedAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Prototype 是某条 Requirement 的一个原型设计版本快照。Version 在同一 Requirement
|
|
// 内自增(max+1),CreatedBy 永远存在(DELETE RESTRICT)。
|
|
type Prototype struct {
|
|
ID uuid.UUID
|
|
RequirementID uuid.UUID
|
|
Version int
|
|
Content string
|
|
Note string
|
|
CreatedBy uuid.UUID
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// Caller 表示发起请求的用户上下文。Service 层据此判定鉴权。
|
|
type Caller struct {
|
|
UserID uuid.UUID
|
|
IsAdmin bool
|
|
}
|
|
|
|
// ProjectService 暴露 Project 聚合根的应用服务方法。包内有三个并列服务
|
|
// (Project/Requirement/Issue),这里保留聚合名前缀以避免歧义。
|
|
//
|
|
//nolint:revive // 名字带聚合根前缀比 "Service" 更准确:包里有 3 个并列服务接口
|
|
type ProjectService interface {
|
|
Create(ctx context.Context, c Caller, in CreateProjectInput) (*Project, error)
|
|
Get(ctx context.Context, c Caller, slug string) (*Project, error)
|
|
GetByID(ctx context.Context, c Caller, id uuid.UUID) (*Project, error)
|
|
List(ctx context.Context, c Caller, includeArchived bool) ([]*Project, error)
|
|
Update(ctx context.Context, c Caller, slug string, in UpdateProjectInput) (*Project, error)
|
|
Archive(ctx context.Context, c Caller, slug string) error
|
|
Unarchive(ctx context.Context, c Caller, slug string) error
|
|
}
|
|
|
|
// RequirementService 暴露 Requirement 聚合根的应用服务方法。
|
|
type RequirementService interface {
|
|
Create(ctx context.Context, c Caller, projectSlug string, in CreateRequirementInput) (*Requirement, error)
|
|
Get(ctx context.Context, c Caller, projectSlug string, number int) (*Requirement, error)
|
|
List(ctx context.Context, c Caller, projectSlug string, filter RequirementFilter) ([]*Requirement, error)
|
|
Update(ctx context.Context, c Caller, projectSlug string, number int, in UpdateRequirementInput) (*Requirement, error)
|
|
ChangePhase(ctx context.Context, c Caller, projectSlug string, number int, to Phase) (*Requirement, error)
|
|
Close(ctx context.Context, c Caller, projectSlug string, number int) error
|
|
Reopen(ctx context.Context, c Caller, projectSlug string, number int) error
|
|
}
|
|
|
|
// IssueService 暴露 Issue 聚合根的应用服务方法。
|
|
type IssueService interface {
|
|
Create(ctx context.Context, c Caller, projectSlug string, in CreateIssueInput) (*Issue, error)
|
|
Get(ctx context.Context, c Caller, projectSlug string, number int) (*Issue, error)
|
|
List(ctx context.Context, c Caller, projectSlug string, filter IssueFilter) ([]*Issue, error)
|
|
Update(ctx context.Context, c Caller, projectSlug string, number int, in UpdateIssueInput) (*Issue, error)
|
|
Assign(ctx context.Context, c Caller, projectSlug string, number int, assignee *uuid.UUID) (*Issue, error)
|
|
Close(ctx context.Context, c Caller, projectSlug string, number int) error
|
|
Reopen(ctx context.Context, c Caller, projectSlug string, number int) error
|
|
}
|
|
|
|
// PrototypeService 暴露 Requirement 原型设计版本的应用服务方法。
|
|
type PrototypeService interface {
|
|
Create(ctx context.Context, c Caller, projectSlug string, reqNumber int, in CreatePrototypeInput) (*Prototype, error)
|
|
List(ctx context.Context, c Caller, projectSlug string, reqNumber int) ([]*Prototype, error)
|
|
GetByVersion(ctx context.Context, c Caller, projectSlug string, reqNumber int, version int) (*Prototype, error)
|
|
}
|
|
|
|
// ===== 输入 DTO =====
|
|
//
|
|
// 约定(适用于全部 Update*Input):指针字段为 nil 表示"未提供,保持原值";
|
|
// 非 nil 表示"用解引用值替换"。Filter 类型使用零值(""、nil)表示"不过滤"。
|
|
|
|
// CreateProjectInput 携带创建 Project 必需的字段。Visibility 留空时由 Service 默认 private。
|
|
type CreateProjectInput struct {
|
|
Slug string
|
|
Name string
|
|
Description string
|
|
Visibility Visibility
|
|
}
|
|
|
|
// UpdateProjectInput 是 Project 的 patch 输入:每个非 nil 字段表示要替换的值;
|
|
// nil 字段保持现有值不变。Slug / Owner / ArchivedAt 不在此处修改。
|
|
type UpdateProjectInput struct {
|
|
Name *string
|
|
Description *string
|
|
Visibility *Visibility
|
|
}
|
|
|
|
// CreateRequirementInput 创建一条需求。OwnerID 为 nil 时由 Service 默认为调用者。
|
|
// WorkspaceID 为 nil 表示不关联任何 workspace;非 nil 时直接落库(FK 由 PG 校验)。
|
|
type CreateRequirementInput struct {
|
|
Title string
|
|
Description string
|
|
OwnerID *uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
}
|
|
|
|
// UpdateRequirementInput 同 UpdateProjectInput 的 patch 语义。Phase / Status 不在此处修改,
|
|
// 走专用方法 ChangePhase / Close / Reopen 以便 audit 区分。
|
|
//
|
|
// WorkspaceID 走"uuid.Nil sentinel"三态语义(与 RequirementNumber 的 **int 不同):
|
|
//
|
|
// nil —— 未提供(保持原 workspace_id 不变)
|
|
// &uuid.Nil —— 已提供,要求"解绑"(workspace_id 置 NULL)
|
|
// &id (非 Nil) —— 已提供,要求关联到指定 workspace
|
|
type UpdateRequirementInput struct {
|
|
Title *string
|
|
Description *string
|
|
OwnerID *uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
}
|
|
|
|
// RequirementFilter 控制 List 时的过滤维度。Phase / Status 留零值("" / Status(""))表示
|
|
// 不限制;非零值必须落在对应枚举集合内(IsValid 校验在 Service 层做)。
|
|
type RequirementFilter struct {
|
|
Phase Phase
|
|
Status Status
|
|
}
|
|
|
|
// CreateIssueInput 创建 Issue。RequirementNumber == nil 表示游离 Issue(不挂载到任何
|
|
// Requirement);非 nil 时 Service 会校验 1) 该 Requirement 与目标 Project 同属,
|
|
// 2) 未关闭。
|
|
// WorkspaceID 为 nil 表示不关联 workspace;非 nil 时 DB 复合 FK 保证同 project。
|
|
type CreateIssueInput struct {
|
|
Title string
|
|
Description string
|
|
RequirementNumber *int
|
|
AssigneeID *uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
}
|
|
|
|
// CreatePrototypeInput 创建一个原型设计版本。Version 由 Service 自动计算(max+1),
|
|
// 不在此处提供。Note 可为空字符串。
|
|
type CreatePrototypeInput struct {
|
|
Content string
|
|
Note string
|
|
}
|
|
|
|
// UpdateIssueInput 是 Issue 的 patch 输入。RequirementNumber 为指针的指针,承载三种状态:
|
|
//
|
|
// nil —— 未提供(不修改 issue.requirement_id)
|
|
// *x == nil —— 已提供,但要求"取消挂载"(issue.requirement_id 置 NULL)
|
|
// *x != nil —— 已提供,要求"挂载到本项目下 number=**x 的 Requirement"
|
|
//
|
|
// HTTP 层用 json.RawMessage 区分 "字段缺省 vs 显式 null vs 数字",再翻译为本三态。
|
|
//
|
|
// WorkspaceID 走"uuid.Nil sentinel"三态语义(不再嵌一层指针):
|
|
//
|
|
// nil —— 未提供(保持原 workspace_id 不变)
|
|
// &uuid.Nil —— 已提供,要求"解绑"(workspace_id 置 NULL)
|
|
// &id (非 Nil) —— 已提供,要求关联到指定 workspace(DB FK 校验同 project)
|
|
type UpdateIssueInput struct {
|
|
Title *string
|
|
Description *string
|
|
RequirementNumber **int
|
|
WorkspaceID *uuid.UUID
|
|
}
|
|
|
|
// IssueFilter 控制 List 时的过滤。Requirement 字段三态(与 sqlc requirement_filter
|
|
// narg 对应):
|
|
//
|
|
// nil —— 不按 requirement 过滤
|
|
// *Requirement == 0 —— 仅游离(requirement_id IS NULL)—— 0 是 sentinel,因为
|
|
// Issue.Number 自增从 1 起
|
|
// *Requirement > 0 —— 仅匹配 number == *Requirement 的 Requirement
|
|
//
|
|
// AssigneeID nil 表示不按 assignee 过滤;Status 留零值表示不按状态过滤。
|
|
type IssueFilter struct {
|
|
Status Status
|
|
Requirement *int
|
|
AssigneeID *uuid.UUID
|
|
}
|