From 4022a776a6057ef1990ef6e86bbd1604a4c382f5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 12 Jan 2026 15:33:39 +0800 Subject: [PATCH] fix: scope creator contents and bind uploads --- backend/app/http/v1/dto/content.go | 2 + backend/app/http/v1/tenant.go | 10 ++--- backend/app/services/common.go | 69 +++++++++++++++++++++++++----- backend/app/services/content.go | 3 ++ 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/backend/app/http/v1/dto/content.go b/backend/app/http/v1/dto/content.go index 16f6007..abf9fea 100644 --- a/backend/app/http/v1/dto/content.go +++ b/backend/app/http/v1/dto/content.go @@ -11,6 +11,8 @@ type ContentListFilter struct { Genre *string `query:"genre"` // TenantID 租户ID筛选(内容所属店铺)。 TenantID *int64 `query:"tenant_id"` + // AuthorID 作者用户ID筛选(用于筛选创作者作品)。 + AuthorID *int64 `query:"author_id"` // Sort 排序规则(latest/hot/price_asc)。 Sort *string `query:"sort"` // IsPinned 置顶内容筛选(true 仅返回置顶)。 diff --git a/backend/app/http/v1/tenant.go b/backend/app/http/v1/tenant.go index 37deb71..d71bfa5 100644 --- a/backend/app/http/v1/tenant.go +++ b/backend/app/http/v1/tenant.go @@ -21,7 +21,7 @@ type Tenant struct{} // @Tags TenantPublic // @Accept json // @Produce json -// @Param id path int64 true "Tenant ID" +// @Param id path int64 true "Creator User ID" // @Param page query int false "Page" // @Param limit query int false "Limit" // @Success 200 {object} requests.Pager @@ -29,13 +29,13 @@ type Tenant struct{} // @Bind filter query func (t *Tenant) ListContents(ctx fiber.Ctx, id int64, filter *dto.ContentListFilter) (*requests.Pager, error) { tenantID := getTenantID(ctx) - if tenantID > 0 && id != tenantID { - return nil, errorx.ErrForbidden.WithMsg("租户不匹配") - } if filter == nil { filter = &dto.ContentListFilter{} } - filter.TenantID = &tenantID + if tenantID > 0 { + filter.TenantID = &tenantID + } + filter.AuthorID = &id return services.Content.List(ctx, tenantID, filter) } diff --git a/backend/app/services/common.go b/backend/app/services/common.go index 2f03ee6..6810a34 100644 --- a/backend/app/services/common.go +++ b/backend/app/services/common.go @@ -104,9 +104,16 @@ func (s *common) CheckHash(ctx context.Context, tenantID, userID int64, hash str } type UploadMeta struct { - Filename string - Type string - MimeType string + // Filename 原始文件名,用于生成对象路径。 + Filename string `json:"filename"` + // Type 业务媒体类型(video/audio/image 等)。 + Type string `json:"type"` + // MimeType 上传文件的 MIME 类型。 + MimeType string `json:"mime_type"` + // TenantID 上传所属租户ID,用于归属校验。 + TenantID int64 `json:"tenant_id"` + // UserID 上传发起用户ID,用于归属校验。 + UserID int64 `json:"user_id"` } func (s *common) buildObjectKey(tenant *models.Tenant, hash, filename string) string { @@ -119,6 +126,31 @@ func (s *common) buildObjectKey(tenant *models.Tenant, hash, filename string) st return path.Join("quyun", tenantUUID, hash+ext) } +func (s *common) loadUploadMeta(tempDir string) (*UploadMeta, error) { + metaFile, err := os.Open(filepath.Join(tempDir, "meta.json")) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, errorx.ErrRecordNotFound.WithCause(err).WithMsg("上传会话不存在") + } + return nil, errorx.ErrInternalError.WithCause(err) + } + defer metaFile.Close() + + var meta UploadMeta + if err := json.NewDecoder(metaFile).Decode(&meta); err != nil { + return nil, errorx.ErrDataCorrupted.WithCause(err).WithMsg("上传会话元信息损坏") + } + return &meta, nil +} + +func (s *common) verifyUploadOwner(meta *UploadMeta, tenantID, userID int64) error { + // 校验上传会话归属,避免同租户猜测 upload_id 进行越权操作。 + if meta.TenantID != tenantID || meta.UserID != userID { + return errorx.ErrForbidden.WithMsg("无权访问该上传会话") + } + return nil +} + func (s *common) resolveTenant(ctx context.Context, tenantID, userID int64) (*models.Tenant, error) { if tenantID > 0 { tbl, q := models.TenantQuery.QueryContext(ctx) @@ -169,6 +201,8 @@ func (s *common) InitUpload(ctx context.Context, tenantID, userID int64, form *c Filename: form.Filename, Type: form.Type, // Ensure form has Type MimeType: form.MimeType, + TenantID: tenantID, + UserID: userID, } metaFile, _ := os.Create(filepath.Join(tempDir, "meta.json")) json.NewEncoder(metaFile).Encode(meta) @@ -185,7 +219,15 @@ func (s *common) UploadPart(ctx context.Context, tenantID, userID int64, file *m if localPath == "" { localPath = "./storage" } - partPath := filepath.Join(s.uploadTempDir(localPath, tenantID, form.UploadID), strconv.Itoa(form.PartNumber)) + tempDir := s.uploadTempDir(localPath, tenantID, form.UploadID) + meta, err := s.loadUploadMeta(tempDir) + if err != nil { + return err + } + if err := s.verifyUploadOwner(meta, tenantID, userID); err != nil { + return err + } + partPath := filepath.Join(tempDir, strconv.Itoa(form.PartNumber)) src, err := file.Open() if err != nil { @@ -212,14 +254,14 @@ func (s *common) CompleteUpload(ctx context.Context, tenantID, userID int64, for } tempDir := s.uploadTempDir(localPath, tenantID, form.UploadID) - // Read Meta - var meta UploadMeta - metaFile, err := os.Open(filepath.Join(tempDir, "meta.json")) + // 校验上传会话归属,避免同租户猜测 upload_id 进行越权操作。 + meta, err := s.loadUploadMeta(tempDir) if err != nil { - return nil, errorx.ErrRecordNotFound.WithMsg("Upload session expired or invalid") + return nil, err + } + if err := s.verifyUploadOwner(meta, tenantID, userID); err != nil { + return nil, err } - json.NewDecoder(metaFile).Decode(&meta) - metaFile.Close() // List parts entries, err := os.ReadDir(tempDir) @@ -373,6 +415,13 @@ func (s *common) AbortUpload(ctx context.Context, tenantID, userID int64, upload localPath = "./storage" } tempDir := s.uploadTempDir(localPath, tenantID, uploadId) + meta, err := s.loadUploadMeta(tempDir) + if err != nil { + return err + } + if err := s.verifyUploadOwner(meta, tenantID, userID); err != nil { + return err + } return os.RemoveAll(tempDir) } diff --git a/backend/app/services/content.go b/backend/app/services/content.go index 28810bf..9b18dae 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -31,6 +31,9 @@ func (s *content) List(ctx context.Context, tenantID int64, filter *content_dto. if tenantID > 0 { q = q.Where(tbl.TenantID.Eq(tenantID)) } + if filter.AuthorID != nil && *filter.AuthorID > 0 { + q = q.Where(tbl.UserID.Eq(*filter.AuthorID)) + } if filter.Genre != nil && *filter.Genre != "" { q = q.Where(tbl.Genre.Eq(*filter.Genre)) }