From f261504c5164ed17a7fa50c8e656900247d8684a Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 1 May 2026 09:31:03 +0800 Subject: [PATCH] feat(project): migration 0002 + errs codes for project/requirement/issue --- internal/infra/errs/errs.go | 8 ++++- migrations/0002_project.down.sql | 3 ++ migrations/0002_project.up.sql | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 migrations/0002_project.down.sql create mode 100644 migrations/0002_project.up.sql diff --git a/internal/infra/errs/errs.go b/internal/infra/errs/errs.go index a498fd6..9b6f30d 100644 --- a/internal/infra/errs/errs.go +++ b/internal/infra/errs/errs.go @@ -27,6 +27,11 @@ const ( CodeUpstream Code = "upstream_error" CodeUnavailable Code = "unavailable" CodeInternal Code = "internal" + + // Project 模块专用细粒度 code(HTTP 仍 409,但前端可据此区分提示)。 + CodeProjectSlugTaken Code = "project_slug_taken" + CodeProjectArchived Code = "project_archived" + CodeRequirementClosed Code = "requirement_closed" ) // AppError is the application's structured error type. It carries a stable @@ -97,7 +102,8 @@ func HTTPStatus(code Code) int { return http.StatusForbidden case CodeNotFound: return http.StatusNotFound - case CodeConflict: + case CodeConflict, + CodeProjectSlugTaken, CodeProjectArchived, CodeRequirementClosed: return http.StatusConflict case CodeRateLimited: return http.StatusTooManyRequests diff --git a/migrations/0002_project.down.sql b/migrations/0002_project.down.sql new file mode 100644 index 0000000..39137e9 --- /dev/null +++ b/migrations/0002_project.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS issues; +DROP TABLE IF EXISTS requirements; +DROP TABLE IF EXISTS projects; diff --git a/migrations/0002_project.up.sql b/migrations/0002_project.up.sql new file mode 100644 index 0000000..3f04f2c --- /dev/null +++ b/migrations/0002_project.up.sql @@ -0,0 +1,57 @@ +-- ============ Project ============ +CREATE TABLE projects ( + id UUID PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL DEFAULT 'private', + owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + archived_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT projects_visibility_check CHECK (visibility IN ('private','internal')) +); +CREATE INDEX idx_projects_owner ON projects(owner_id); +CREATE INDEX idx_projects_archived ON projects(archived_at) WHERE archived_at IS NULL; + +-- ============ Requirement ============ +CREATE TABLE requirements ( + id UUID PRIMARY KEY, + project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + number INT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + phase TEXT NOT NULL DEFAULT 'planning', + status TEXT NOT NULL DEFAULT 'open', + owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + closed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (project_id, number), + CONSTRAINT requirements_phase_check CHECK (phase IN ('planning','auditing','implementing','reviewing','done')), + CONSTRAINT requirements_status_check CHECK (status IN ('open','closed')) +); +CREATE INDEX idx_requirements_project_phase ON requirements(project_id, phase); +CREATE INDEX idx_requirements_project_status ON requirements(project_id, status); +CREATE INDEX idx_requirements_owner ON requirements(owner_id); + +-- ============ Issue ============ +CREATE TABLE issues ( + id UUID PRIMARY KEY, + project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + requirement_id UUID REFERENCES requirements(id) ON DELETE SET NULL, + number INT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'open', + assignee_id UUID REFERENCES users(id) ON DELETE SET NULL, + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + closed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (project_id, number), + CONSTRAINT issues_status_check CHECK (status IN ('open','closed')) +); +CREATE INDEX idx_issues_requirement ON issues(requirement_id) WHERE requirement_id IS NOT NULL; +CREATE INDEX idx_issues_project_status ON issues(project_id, status); +CREATE INDEX idx_issues_assignee ON issues(assignee_id);