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:
WeiCHEN
2026-09-23 00:42:30 +08:00
parent 1ff3d81e1b
commit 2b901a61de
6 changed files with 65 additions and 7 deletions
@@ -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;
+19
View File
@@ -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{
+1 -1
View File
@@ -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"
} }
+4 -4
View File
@@ -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'
+4 -2
View File
@@ -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)