fix(freedoc): widen freeform project_id to BIGINT
Snowflake project IDs exceed int4; upload registration failed with pgx encode errors. Align column and Go types with projects.id.
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "freeform project_id BIGINT 对齐 projects.id"
|
||||||
|
task_id: "freeform-project-id-bigint-2026-09-22"
|
||||||
|
status: in_progress
|
||||||
|
created: 2026-09-22
|
||||||
|
owner: Cursor
|
||||||
|
task_type: implementation
|
||||||
|
branch: feature/freeform-project-id-bigint-2026-09-22
|
||||||
|
worktree: .worktrees/freeform-project-id-bigint-2026-09-22
|
||||||
|
write_scope: ["deploy/postgres/goose", "internal/freedoc", "internal/server", ".doc"]
|
||||||
|
allowed_environments: ["dev"]
|
||||||
|
mutex: "deploy/postgres/goose + freeform"
|
||||||
|
---
|
||||||
|
|
||||||
|
# freeform project_id BIGINT
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
`POST /api/v1/freeform-documents` 在雪花 `project_id`(>int4 max)时 pgx 编码失败(OID 23),阻塞 UI-11 附件注册。
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
- goose `00063`:`freeform_documents.project_id` → BIGINT
|
||||||
|
- Go `ProjectID`/`ListDocuments` → int64;list query 用 `ParseInt`
|
||||||
|
- 活库已手工 ALTER(schema 仍报 61);合入后 goose 幂等加固
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
- `go test ./internal/freedoc/ -short` PASS(含 snowflake 回归)
|
||||||
|
- 活库 column = bigint
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- freeform_documents.project_id must match projects.id (BIGINT snowflake).
|
||||||
|
-- Prior INT rejected inserts once project IDs exceeded int4 max (~2.1e9).
|
||||||
|
|
||||||
|
ALTER TABLE freeform_documents
|
||||||
|
ALTER COLUMN project_id TYPE BIGINT;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE freeform_documents
|
||||||
|
ALTER COLUMN project_id TYPE INT;
|
||||||
@@ -59,6 +59,25 @@ func TestCreateDocument_ClientKeyIdempotentAndBootstrapsLedger(t *testing.T) {
|
|||||||
assert.NotEqual(t, detail.Document.DocID, otherUser.Document.DocID)
|
assert.NotEqual(t, detail.Document.DocID, otherUser.Document.DocID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateDocument_AcceptsSnowflakeProjectID(t *testing.T) {
|
||||||
|
// projects.id is BIGINT; freeform must accept IDs above int4 max (regression: int4 encode 500).
|
||||||
|
const snowflakeProjectID int64 = 1789095399489
|
||||||
|
svc := freedoc.NewService(nil, nil)
|
||||||
|
detail, err := svc.CreateDocument(context.Background(), 42, freedoc.CreateFreeformDocRequest{
|
||||||
|
ProjectID: snowflakeProjectID,
|
||||||
|
Title: "upload-snowflake.txt",
|
||||||
|
ContentMarkdown: "# snowflake project id\n",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, detail)
|
||||||
|
assert.Equal(t, snowflakeProjectID, detail.Document.ProjectID)
|
||||||
|
|
||||||
|
list, err := svc.ListDocuments(context.Background(), 42, snowflakeProjectID, "my_private")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, list)
|
||||||
|
assert.Equal(t, snowflakeProjectID, list[0].ProjectID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateDocument_RejectsEmptyContent(t *testing.T) {
|
func TestCreateDocument_RejectsEmptyContent(t *testing.T) {
|
||||||
svc := freedoc.NewService(nil, nil)
|
svc := freedoc.NewService(nil, nil)
|
||||||
_, err := svc.CreateDocument(context.Background(), 1, freedoc.CreateFreeformDocRequest{
|
_, err := svc.CreateDocument(context.Background(), 1, freedoc.CreateFreeformDocRequest{
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ func (s *Service) GetDocument(ctx context.Context, userID int, docID string) (*F
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListDocuments 按空间与权限列出自由文档概要
|
// ListDocuments 按空间与权限列出自由文档概要
|
||||||
func (s *Service) ListDocuments(ctx context.Context, userID int, projectID int, space string) ([]FreeformDocSummary, error) {
|
func (s *Service) ListDocuments(ctx context.Context, userID int, projectID int64, space string) ([]FreeformDocSummary, error) {
|
||||||
if space != "my_private" && space != "project_shared" && space != "trash" {
|
if space != "my_private" && space != "project_shared" && space != "trash" {
|
||||||
space = "my_private"
|
space = "my_private"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ var (
|
|||||||
// FreeformDocument 自由私密/共享文档元数据
|
// FreeformDocument 自由私密/共享文档元数据
|
||||||
type FreeformDocument struct {
|
type FreeformDocument struct {
|
||||||
DocID string `json:"doc_id"`
|
DocID string `json:"doc_id"`
|
||||||
ProjectID int `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
OwnerUserID int `json:"owner_user_id"`
|
OwnerUserID int `json:"owner_user_id"`
|
||||||
SourceMessageID *int64 `json:"source_message_id,omitempty"`
|
SourceMessageID *int64 `json:"source_message_id,omitempty"`
|
||||||
SourceContentSHA256 string `json:"source_content_sha256"`
|
SourceContentSHA256 string `json:"source_content_sha256"`
|
||||||
@@ -53,7 +53,7 @@ type FreeformDocDetail struct {
|
|||||||
// FreeformDocSummary 列表项概要
|
// FreeformDocSummary 列表项概要
|
||||||
type FreeformDocSummary struct {
|
type FreeformDocSummary struct {
|
||||||
DocID string `json:"doc_id"`
|
DocID string `json:"doc_id"`
|
||||||
ProjectID int `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
OwnerUserID int `json:"owner_user_id"`
|
OwnerUserID int `json:"owner_user_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Space string `json:"space"`
|
Space string `json:"space"`
|
||||||
@@ -66,14 +66,14 @@ type FreeformDocSummary struct {
|
|||||||
// CreateFromMessageRequest 从单条消息保存到私密文档请求
|
// CreateFromMessageRequest 从单条消息保存到私密文档请求
|
||||||
type CreateFromMessageRequest struct {
|
type CreateFromMessageRequest struct {
|
||||||
MessageID int64 `json:"message_id"`
|
MessageID int64 `json:"message_id"`
|
||||||
ProjectID int `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
ExpectedSHA256 string `json:"expected_sha256,omitempty"`
|
ExpectedSHA256 string `json:"expected_sha256,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFreeformDocRequest 直接创建空白/自定义自由文档请求(上传附件即时注册事实源)
|
// CreateFreeformDocRequest 直接创建空白/自定义自由文档请求(上传附件即时注册事实源)
|
||||||
type CreateFreeformDocRequest struct {
|
type CreateFreeformDocRequest struct {
|
||||||
ProjectID int `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
ContentMarkdown string `json:"content_markdown"`
|
ContentMarkdown string `json:"content_markdown"`
|
||||||
Space string `json:"space,omitempty"` // 默认 'my_private'
|
Space string `json:"space,omitempty"` // 默认 'my_private'
|
||||||
|
|||||||
@@ -181,9 +181,11 @@ func (s *Server) handleListFreeformDocs(w http.ResponseWriter, r *http.Request)
|
|||||||
if space == "" {
|
if space == "" {
|
||||||
space = "my_private"
|
space = "my_private"
|
||||||
}
|
}
|
||||||
projectID := 0
|
var projectID int64
|
||||||
if pStr := r.URL.Query().Get("project_id"); pStr != "" {
|
if pStr := r.URL.Query().Get("project_id"); pStr != "" {
|
||||||
projectID, _ = strconv.Atoi(pStr)
|
if parsed, err := strconv.ParseInt(pStr, 10, 64); err == nil {
|
||||||
|
projectID = parsed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := s.freeDoc.ListDocuments(r.Context(), userID, projectID, space)
|
list, err := s.freeDoc.ListDocuments(r.Context(), userID, projectID, space)
|
||||||
|
|||||||
Reference in New Issue
Block a user