上传文档跨端不可访问修复(ISSUE-20260927-upload-doc-cross-device): - 迁移 00076:freeform_documents 增加 source_filename/mime/size_bytes/asset_id - freedoc:Create/Get/List 读写源文件元数据,负值拒绝 - 前端:注册前全部附件先登记 CAS 并预读文本正文;文本正文嵌入注册 markdown;跨端恢复 sourceType/fileMeta 路由回文件预览器 - 大小显示 formatFileSize(<512B 不再显示 0 KB) - E2E 新增 FS-UPLOAD-02 跨浏览器上下文可访问性用例 回归:go vet/arch-check/go test -short(隔离 PG)+ web npm test/build 全绿; 存量无元数据旧文档不可恢复(重新上传即可)。
189 lines
5.8 KiB
Go
189 lines
5.8 KiB
Go
package freedoc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func (s *Service) GetDocument(ctx context.Context, userID int, docID string) (*FreeformDocDetail, error) {
|
|
if docID == "" {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
if s.pool != nil {
|
|
var doc FreeformDocument
|
|
row := s.pool.QueryRow(ctx, `
|
|
SELECT doc_id, project_id, owner_user_id, source_message_id, source_content_sha256,
|
|
title, governance_mode, space, legacy_version_id, current_sha256,
|
|
is_deleted, deleted_at, created_at, updated_at,
|
|
source_filename, source_mime_type, source_size_bytes, COALESCE(source_asset_id, 0)
|
|
FROM freeform_documents
|
|
WHERE doc_id = $1
|
|
`, docID)
|
|
if err := row.Scan(
|
|
&doc.DocID, &doc.ProjectID, &doc.OwnerUserID, &doc.SourceMessageID,
|
|
&doc.SourceContentSHA256, &doc.Title, &doc.GovernanceMode,
|
|
&doc.Space, &doc.CurrentVersionID, &doc.CurrentSHA256,
|
|
&doc.IsDeleted, &doc.DeletedAt, &doc.CreatedAt, &doc.UpdatedAt,
|
|
&doc.SourceFilename, &doc.SourceMimeType, &doc.SourceSizeBytes, &doc.SourceAssetID,
|
|
); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if doc.IsDeleted {
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
// 私密隔离红线:my_private 只有 owner 可读
|
|
if doc.Space == "my_private" && doc.OwnerUserID != userID {
|
|
_, _ = s.pool.Exec(ctx, `
|
|
INSERT INTO document_access_audit (doc_id, user_id, action, success)
|
|
VALUES ($1, $2, 'read_denied', FALSE)
|
|
`, docID, userID)
|
|
return nil, ErrForbidden
|
|
}
|
|
|
|
var content string
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT content_markdown FROM freeform_document_versions
|
|
WHERE doc_id = $1 AND version_no = $2
|
|
`, doc.DocID, doc.CurrentVersionID).Scan(&content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read version: %w", err)
|
|
}
|
|
|
|
_, _ = s.pool.Exec(ctx, `
|
|
INSERT INTO document_access_audit (doc_id, user_id, action, success)
|
|
VALUES ($1, $2, 'read', TRUE)
|
|
`, docID, userID)
|
|
|
|
return &FreeformDocDetail{
|
|
Document: doc,
|
|
Content: content,
|
|
}, nil
|
|
}
|
|
|
|
// 内存模式
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
doc, ok := s.memDocs[docID]
|
|
if !ok || doc.IsDeleted {
|
|
return nil, ErrNotFound
|
|
}
|
|
if doc.Space == "my_private" && doc.OwnerUserID != userID {
|
|
return nil, ErrForbidden
|
|
}
|
|
|
|
versions := s.memVersions[docID]
|
|
content := ""
|
|
for _, v := range versions {
|
|
if v.VersionNo == doc.CurrentVersionID {
|
|
content = v.ContentMarkdown
|
|
break
|
|
}
|
|
}
|
|
|
|
return &FreeformDocDetail{
|
|
Document: *doc,
|
|
Content: content,
|
|
}, nil
|
|
}
|
|
|
|
// ListDocuments 按空间与权限列出自由文档概要
|
|
|
|
func (s *Service) ListDocuments(ctx context.Context, userID int, projectID int64, space string) ([]FreeformDocSummary, error) {
|
|
if space != "my_private" && space != "project_shared" && space != "trash" {
|
|
space = "my_private"
|
|
}
|
|
|
|
if s.pool != nil {
|
|
var rows pgx.Rows
|
|
var err error
|
|
if space == "my_private" {
|
|
// 我的私密:属于当前用户
|
|
if projectID > 0 {
|
|
rows, err = s.pool.Query(ctx, `
|
|
SELECT doc_id, project_id, owner_user_id, title, space, legacy_version_id, current_sha256, created_at, updated_at,
|
|
source_filename, source_mime_type, source_size_bytes, COALESCE(source_asset_id, 0)
|
|
FROM freeform_documents
|
|
WHERE owner_user_id = $1 AND space = 'my_private' AND project_id = $2 AND is_deleted = FALSE
|
|
ORDER BY updated_at DESC
|
|
`, userID, projectID)
|
|
} else {
|
|
rows, err = s.pool.Query(ctx, `
|
|
SELECT doc_id, project_id, owner_user_id, title, space, legacy_version_id, current_sha256, created_at, updated_at,
|
|
source_filename, source_mime_type, source_size_bytes, COALESCE(source_asset_id, 0)
|
|
FROM freeform_documents
|
|
WHERE owner_user_id = $1 AND space = 'my_private' AND is_deleted = FALSE
|
|
ORDER BY updated_at DESC
|
|
`, userID)
|
|
}
|
|
} else if space == "project_shared" {
|
|
// 项目共享参考:属于当前项目的所有公开参考
|
|
rows, err = s.pool.Query(ctx, `
|
|
SELECT doc_id, project_id, owner_user_id, title, space, legacy_version_id, current_sha256, created_at, updated_at,
|
|
source_filename, source_mime_type, source_size_bytes, COALESCE(source_asset_id, 0)
|
|
FROM freeform_documents
|
|
WHERE project_id = $1 AND space = 'project_shared' AND is_deleted = FALSE
|
|
ORDER BY updated_at DESC
|
|
`, projectID)
|
|
} else {
|
|
// 回收站:当前用户删除的
|
|
rows, err = s.pool.Query(ctx, `
|
|
SELECT doc_id, project_id, owner_user_id, title, space, legacy_version_id, current_sha256, created_at, updated_at,
|
|
source_filename, source_mime_type, source_size_bytes, COALESCE(source_asset_id, 0)
|
|
FROM freeform_documents
|
|
WHERE owner_user_id = $1 AND is_deleted = TRUE
|
|
ORDER BY deleted_at DESC
|
|
`, userID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []FreeformDocSummary
|
|
for rows.Next() {
|
|
var sm FreeformDocSummary
|
|
if err := rows.Scan(
|
|
&sm.DocID, &sm.ProjectID, &sm.OwnerUserID, &sm.Title, &sm.Space,
|
|
&sm.CurrentVersionID, &sm.CurrentSHA256, &sm.CreatedAt, &sm.UpdatedAt,
|
|
&sm.SourceFilename, &sm.SourceMimeType, &sm.SourceSizeBytes, &sm.SourceAssetID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, sm)
|
|
}
|
|
return results, rows.Err()
|
|
}
|
|
|
|
// 内存模式
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var results []FreeformDocSummary
|
|
for _, d := range s.memDocs {
|
|
if space == "my_private" {
|
|
if d.OwnerUserID == userID && d.Space == "my_private" && !d.IsDeleted {
|
|
if projectID <= 0 || d.ProjectID == projectID {
|
|
results = append(results, d.summary())
|
|
}
|
|
}
|
|
} else if space == "project_shared" {
|
|
if d.ProjectID == projectID && d.Space == "project_shared" && !d.IsDeleted {
|
|
results = append(results, d.summary())
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// UpdateDocument 更新文档正文或标题,支持 If-Match 乐观并发控制
|