From d661a22b8a96a04a84175382f36c3161c2a0c772 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 23 May 2026 17:48:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=91=E5=B8=83=E5=90=8E?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=97=A0=E6=B3=95=E7=9C=8B=E5=88=B0bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/listing/handler.go | 59 ++++++++++++- .../internal/modules/listing/repository.go | 88 ++++++++++++++++++- backend/internal/modules/listing/service.go | 14 +++ backend/internal/router/router.go | 6 +- 4 files changed, 159 insertions(+), 8 deletions(-) diff --git a/backend/internal/modules/listing/handler.go b/backend/internal/modules/listing/handler.go index 50ce2c1..528fbc3 100644 --- a/backend/internal/modules/listing/handler.go +++ b/backend/internal/modules/listing/handler.go @@ -6,6 +6,7 @@ import ( "strconv" "hfb_sys/backend/internal/middleware" + filemodule "hfb_sys/backend/internal/modules/file" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" @@ -13,10 +14,11 @@ import ( type Handler struct { service *Service + storage *filemodule.Storage } -func NewHandler(service *Service) *Handler { - return &Handler{service: service} +func NewHandler(service *Service, storage *filemodule.Storage) *Handler { + return &Handler{service: service, storage: storage} } func (h *Handler) Create(c *gin.Context) { @@ -215,6 +217,59 @@ func (h *Handler) FindPublic(c *gin.Context) { response.OK(c, item) } +func (h *Handler) Cover(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + key, err := h.service.FindPublicCoverKey(id) + if err != nil { + writeListingError(c, err) + return + } + h.writePublicObject(c, key) +} + +func (h *Handler) Screenshot(c *gin.Context) { + id, ok := parseID(c) + if !ok { + return + } + index, err := strconv.Atoi(c.Param("index")) + if err != nil || index < 0 { + response.BadRequest(c, "截图序号不正确") + return + } + key, err := h.service.FindPublicScreenshotKey(id, index) + if err != nil { + writeListingError(c, err) + return + } + h.writePublicObject(c, key) +} + +func (h *Handler) writePublicObject(c *gin.Context, key string) { + if h.storage == nil { + response.ServiceUnavailable(c, "文件存储未连接") + return + } + object, err := h.storage.Get(c.Request.Context(), key) + if err != nil { + response.Error(c, http.StatusNotFound, "not_found", "图片不存在或暂不可访问") + return + } + defer func() { + _ = object.Reader.Close() + }() + contentType := object.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + c.Header("Content-Type", contentType) + c.Header("Cache-Control", "public, max-age=300") + c.DataFromReader(http.StatusOK, object.Size, contentType, object.Reader, nil) +} + func (h *Handler) ListMine(c *gin.Context) { ownerID, ok := currentUserID(c) if !ok { diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index 2c933dc..22c909d 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -3,6 +3,8 @@ package listing import ( "encoding/json" "errors" + "net/url" + "strconv" "strings" "time" @@ -366,7 +368,7 @@ func (r *Repository) ListPublic() ([]ListingDTO, error) { if err != nil { return nil, err } - return rowsToDTO(rows), nil + return publicListings(rowsToDTO(rows)), nil } func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) { @@ -382,7 +384,33 @@ func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) { } func (r *Repository) FindPublic(id uint64) (*ListingDTO, error) { - return r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ?", id, "published", "approved") + dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ?", id, "published", "approved") + if err != nil { + return nil, err + } + applyPublicListingURLs(dto) + return dto, nil +} + +func (r *Repository) FindPublicCoverKey(id uint64) (string, error) { + return r.FindPublicScreenshotKey(id, 0) +} + +func (r *Repository) FindPublicScreenshotKey(id uint64, index int) (string, error) { + if index < 0 { + return "", gorm.ErrRecordNotFound + } + dto, err := r.findDTO("l.id = ? AND l.status = ? AND l.review_status = ?", id, "published", "approved") + if err != nil { + return "", err + } + if index >= len(dto.ScreenshotURLS) { + return "", gorm.ErrRecordNotFound + } + if key := extractListingObjectKey(dto.ScreenshotURLS[index]); key != "" { + return key, nil + } + return "", gorm.ErrRecordNotFound } func (r *Repository) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) { @@ -456,6 +484,17 @@ func rowsToDTO(rows []listingRow) []ListingDTO { return items } +func publicListings(items []ListingDTO) []ListingDTO { + for index := range items { + applyPublicListingURLs(&items[index]) + } + return items +} + +func applyPublicListingURLs(item *ListingDTO) { + item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus) +} + func (row listingRow) toDTO() ListingDTO { assetSummary := decodeAssetSummary(row.AssetSummary) screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS)) @@ -474,7 +513,7 @@ func (row listingRow) toDTO() ListingDTO { HafCoinAmount: row.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, - CoverURL: firstScreenshotURL(screenshotURLS), + CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus), PriceHourly: row.PriceHourly, PriceDaily: row.PriceDaily, PriceWeekly: row.PriceWeekly, @@ -504,7 +543,7 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO { HafCoinAmount: account.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, - CoverURL: firstScreenshotURL(screenshotURLS), + CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus), PriceHourly: listing.PriceHourly, PriceDaily: listing.PriceDaily, PriceWeekly: listing.PriceWeekly, @@ -587,6 +626,47 @@ func firstScreenshotURL(urls []string) string { return urls[0] } +func publicCoverURL(listingID uint64, urls []string, status string, reviewStatus string) string { + fallback := firstScreenshotURL(urls) + if status != "published" || reviewStatus != "approved" || extractListingObjectKey(fallback) == "" { + return fallback + } + return "/api/listings/" + strconv.FormatUint(listingID, 10) + "/cover" +} + +func publicScreenshotURLs(listingID uint64, urls []string, status string, reviewStatus string) []string { + if status != "published" || reviewStatus != "approved" { + return urls + } + publicURLs := make([]string, 0, len(urls)) + for index, fileURL := range urls { + if extractListingObjectKey(fileURL) == "" { + publicURLs = append(publicURLs, fileURL) + continue + } + publicURLs = append(publicURLs, "/api/listings/"+strconv.FormatUint(listingID, 10)+"/screenshots/"+strconv.Itoa(index)) + } + return publicURLs +} + +func extractListingObjectKey(fileURL string) string { + if fileURL == "" { + return "" + } + parsed, err := url.Parse(fileURL) + if err != nil { + return "" + } + key := parsed.Query().Get("key") + if key == "" { + return "" + } + if !strings.HasPrefix(key, "listing/") || strings.Contains(key, "..") { + return "" + } + return key +} + func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error { raw, err := json.Marshal(detail) if err != nil { diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 8204424..6091080 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -179,6 +179,20 @@ func (s *Service) FindPublic(id uint64) (*ListingDTO, error) { return s.repo.FindPublic(id) } +func (s *Service) FindPublicCoverKey(id uint64) (string, error) { + if s.repo == nil { + return "", ErrDependencyUnavailable + } + return s.repo.FindPublicCoverKey(id) +} + +func (s *Service) FindPublicScreenshotKey(id uint64, index int) (string, error) { + if s.repo == nil { + return "", ErrDependencyUnavailable + } + return s.repo.FindPublicScreenshotKey(id, index) +} + func (s *Service) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 2a89a0c..6e20a35 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -107,8 +107,6 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } systemConfigService := systemconfig.NewService(systemConfigRepo) systemConfigHandler := systemconfig.NewHandler(systemConfigService) - listingService := listing.NewService(listingRepo, systemConfigRepo) - listingHandler := listing.NewHandler(listingService) var fileStorage *filemodule.Storage if cfg.Storage.Endpoint != "" && cfg.Storage.Bucket != "" { var err error @@ -119,6 +117,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } fileService := filemodule.NewService(fileStorage) fileHandler := filemodule.NewHandler(fileService, fileStorage) + listingService := listing.NewService(listingRepo, systemConfigRepo) + listingHandler := listing.NewHandler(listingService, fileStorage) requireAuth := middleware.Auth(jwtManager) requireAdmin := middleware.AdminAuth(jwtManager) requireRealname := middleware.RequireRealname(userRepo) @@ -145,6 +145,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { listingRoutes := api.Group("/listings") { listingRoutes.GET("", listingHandler.ListPublic) + listingRoutes.GET("/:id/cover", listingHandler.Cover) + listingRoutes.GET("/:id/screenshots/:index", listingHandler.Screenshot) listingRoutes.GET("/:id", listingHandler.FindPublic) listingRoutes.POST("", requireAuth, requireRealname, listingHandler.Create) listingRoutes.PUT("/:id", requireAuth, requireRealname, listingHandler.Update)