feat(project): add workspace_id to issue/requirement with tri-state update

This commit is contained in:
2026-05-02 14:20:15 +08:00
parent 7429a817bc
commit 0cadec343c
17 changed files with 655 additions and 51 deletions
+30 -2
View File
@@ -46,7 +46,7 @@ type Repository interface {
GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Issue, error)
ListIssues(ctx context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error)
ListIssuesByRequirement(ctx context.Context, requirementID uuid.UUID) ([]*Issue, error)
UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID) (*Issue, error)
UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error)
UpdateIssueAssignee(ctx context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error)
CloseIssue(ctx context.Context, id uuid.UUID) error
ReopenIssue(ctx context.Context, id uuid.UUID) error
@@ -59,6 +59,16 @@ func IsUniqueViolation(err error) bool {
return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
}
// IsForeignKeyViolation 报告 err 是否为 PG 外键约束冲突。常见场景:
// - issues.(project_id, workspace_id) 复合 FK 在跨 project 关联时被 PG 拒绝;
// - requirements.workspace_id 关联到不存在的 workspace 时被 PG 拒绝。
//
// Repository 层据此把错误翻成 invalid_input(HTTP 400),避免泄漏 5xx。
func IsForeignKeyViolation(err error) bool {
var pg *pgconn.PgError
return errors.As(err, &pg) && pg.Code == pgerrcode.ForeignKeyViolation
}
type pgRepo struct{ q *projectsqlc.Queries }
// NewPostgresRepository 用现有的 pgxpool 构造 Repository。
@@ -200,6 +210,7 @@ func rowToRequirement(row projectsqlc.Requirement) *Requirement {
return &Requirement{
ID: fromPgUUID(row.ID),
ProjectID: fromPgUUID(row.ProjectID),
WorkspaceID: fromPgUUIDPtr(row.WorkspaceID),
Number: int(row.Number),
Title: row.Title,
Description: row.Description,
@@ -219,11 +230,15 @@ func (r *pgRepo) CreateRequirement(ctx context.Context, in *Requirement) (*Requi
Title: in.Title,
Description: in.Description,
OwnerID: toPgUUID(in.OwnerID),
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
})
if err != nil {
if IsUniqueViolation(err) {
return nil, err // service 层据此重试一次
}
if IsForeignKeyViolation(err) {
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
}
return nil, errs.Wrap(err, errs.CodeInternal, "create requirement")
}
return rowToRequirement(row), nil
@@ -285,11 +300,15 @@ func (r *pgRepo) UpdateRequirement(ctx context.Context, in *Requirement) (*Requi
Title: in.Title,
Description: in.Description,
OwnerID: toPgUUID(in.OwnerID),
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
}
if IsForeignKeyViolation(err) {
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
}
return nil, errs.Wrap(err, errs.CodeInternal, "update requirement")
}
return rowToRequirement(row), nil
@@ -330,6 +349,7 @@ func rowToIssue(row projectsqlc.Issue) *Issue {
ID: fromPgUUID(row.ID),
ProjectID: fromPgUUID(row.ProjectID),
RequirementID: fromPgUUIDPtr(row.RequirementID),
WorkspaceID: fromPgUUIDPtr(row.WorkspaceID),
Number: int(row.Number),
Title: row.Title,
Description: row.Description,
@@ -351,11 +371,15 @@ func (r *pgRepo) CreateIssue(ctx context.Context, in *Issue) (*Issue, error) {
Description: in.Description,
AssigneeID: toPgUUIDPtr(in.AssigneeID),
CreatedBy: toPgUUID(in.CreatedBy),
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
})
if err != nil {
if IsUniqueViolation(err) {
return nil, err
}
if IsForeignKeyViolation(err) {
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
}
return nil, errs.Wrap(err, errs.CodeInternal, "create issue")
}
return rowToIssue(row), nil
@@ -420,17 +444,21 @@ func (r *pgRepo) ListIssuesByRequirement(ctx context.Context, requirementID uuid
return out, nil
}
func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID) (*Issue, error) {
func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error) {
row, err := r.q.UpdateIssueCore(ctx, projectsqlc.UpdateIssueCoreParams{
ID: toPgUUID(id),
Title: title,
Description: description,
RequirementID: toPgUUIDPtr(requirementID),
WorkspaceID: toPgUUIDPtr(workspaceID),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}
if IsForeignKeyViolation(err) {
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
}
return nil, errs.Wrap(err, errs.CodeInternal, "update issue core")
}
return rowToIssue(row), nil