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
+68 -3
View File
@@ -129,6 +129,7 @@ type projectDTO struct {
type requirementDTO struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
WorkspaceID *string `json:"workspace_id"`
Number int `json:"number"`
Title string `json:"title"`
Description string `json:"description"`
@@ -144,6 +145,7 @@ type issueDTO struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
RequirementID *string `json:"requirement_id"`
WorkspaceID *string `json:"workspace_id"`
Number int `json:"number"`
Title string `json:"title"`
Description string `json:"description"`
@@ -177,6 +179,10 @@ func toRequirementDTO(r *Requirement) requirementDTO {
CreatedAt: r.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
UpdatedAt: r.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z"),
}
if r.WorkspaceID != nil {
s := r.WorkspaceID.String()
d.WorkspaceID = &s
}
if r.ClosedAt != nil {
s := r.ClosedAt.UTC().Format("2006-01-02T15:04:05Z")
d.ClosedAt = &s
@@ -196,6 +202,10 @@ func toIssueDTO(i *Issue) issueDTO {
s := i.RequirementID.String()
d.RequirementID = &s
}
if i.WorkspaceID != nil {
s := i.WorkspaceID.String()
d.WorkspaceID = &s
}
if i.AssigneeID != nil {
s := i.AssigneeID.String()
d.AssigneeID = &s
@@ -332,6 +342,7 @@ type createRequirementReq struct {
Title string `json:"title"`
Description string `json:"description"`
OwnerID *string `json:"owner_id,omitempty"`
WorkspaceID *string `json:"workspace_id,omitempty"`
}
func (h *Handler) createRequirement(w http.ResponseWriter, r *http.Request) {
@@ -354,6 +365,14 @@ func (h *Handler) createRequirement(w http.ResponseWriter, r *http.Request) {
}
in.OwnerID = &uid
}
if req.WorkspaceID != nil {
wid, perr := uuid.Parse(*req.WorkspaceID)
if perr != nil {
writeErr(w, r, errs.New(errs.CodeInvalidInput, "workspace_id 非法"))
return
}
in.WorkspaceID = &wid
}
out, err := h.requirements.Create(r.Context(), c, chi.URLParam(r, "slug"), in)
if err != nil {
writeErr(w, r, err)
@@ -411,10 +430,12 @@ func (h *Handler) getRequirement(w http.ResponseWriter, r *http.Request) {
})
}
// updateRequirementReq 用 json.RawMessage 区分 workspace_id 的 "未传"/"显式 null"/"传值"。
type updateRequirementReq struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
OwnerID *string `json:"owner_id,omitempty"`
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
OwnerID *string `json:"owner_id,omitempty"`
WorkspaceID json.RawMessage `json:"workspace_id,omitempty"`
}
func (h *Handler) updateRequirement(w http.ResponseWriter, r *http.Request) {
@@ -442,6 +463,10 @@ func (h *Handler) updateRequirement(w http.ResponseWriter, r *http.Request) {
}
in.OwnerID = &uid
}
if err := parseWorkspaceIDPatch(req.WorkspaceID, &in.WorkspaceID); err != nil {
writeErr(w, r, err)
return
}
out, err := h.requirements.Update(r.Context(), c, chi.URLParam(r, "slug"), num, in)
if err != nil {
writeErr(w, r, err)
@@ -450,6 +475,32 @@ func (h *Handler) updateRequirement(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, toRequirementDTO(out))
}
// parseWorkspaceIDPatch 把 PATCH 请求里的 json.RawMessage workspace_id 字段
// 翻译为 *uuid.UUID 三态:
// - 字段缺省(len == 0):保留 *dst = nil
// - 显式 null:*dst = &uuid.Nil(解绑 sentinel)
// - UUID 字符串:解析并赋值;解析失败返回 invalid_input
func parseWorkspaceIDPatch(raw json.RawMessage, dst **uuid.UUID) error {
if len(raw) == 0 {
return nil
}
if string(raw) == "null" {
nilUUID := uuid.Nil
*dst = &nilUUID
return nil
}
var s string
if err := json.Unmarshal(raw, &s); err != nil {
return errs.New(errs.CodeInvalidInput, "workspace_id 非法")
}
wid, perr := uuid.Parse(s)
if perr != nil {
return errs.New(errs.CodeInvalidInput, "workspace_id 非法")
}
*dst = &wid
return nil
}
type phaseReq struct {
Phase string `json:"phase"`
}
@@ -511,6 +562,7 @@ type createIssueReq struct {
Description string `json:"description"`
RequirementNumber *int `json:"requirement_number,omitempty"`
AssigneeID *string `json:"assignee_id,omitempty"`
WorkspaceID *string `json:"workspace_id,omitempty"`
}
func (h *Handler) createIssue(w http.ResponseWriter, r *http.Request) {
@@ -533,6 +585,14 @@ func (h *Handler) createIssue(w http.ResponseWriter, r *http.Request) {
}
in.AssigneeID = &uid
}
if req.WorkspaceID != nil {
wid, perr := uuid.Parse(*req.WorkspaceID)
if perr != nil {
writeErr(w, r, errs.New(errs.CodeInvalidInput, "workspace_id 非法"))
return
}
in.WorkspaceID = &wid
}
out, err := h.issues.Create(r.Context(), c, chi.URLParam(r, "slug"), in)
if err != nil {
writeErr(w, r, err)
@@ -607,6 +667,7 @@ type updateIssueReq struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
RequirementNumber json.RawMessage `json:"requirement_number,omitempty"`
WorkspaceID json.RawMessage `json:"workspace_id,omitempty"`
}
func (h *Handler) updateIssue(w http.ResponseWriter, r *http.Request) {
@@ -641,6 +702,10 @@ func (h *Handler) updateIssue(w http.ResponseWriter, r *http.Request) {
in.RequirementNumber = &pn
}
}
if err := parseWorkspaceIDPatch(req.WorkspaceID, &in.WorkspaceID); err != nil {
writeErr(w, r, err)
return
}
out, err := h.issues.Update(r.Context(), c, chi.URLParam(r, "slug"), num, in)
if err != nil {
writeErr(w, r, err)