package listing import ( "encoding/json" "math" "net/url" "strconv" "strings" "hfb_sys/backend/internal/model" "hfb_sys/backend/pkg/money" "gorm.io/datatypes" ) type listingRow struct { model.RentalListing Title string OwnerPhone string OwnerNickname string Description string GameName string ServerRegion string LoginPlatform string RankLevel string HafCoinAmount int64 AssetSummary datatypes.JSON `gorm:"column:asset_summary"` ScreenshotURLS datatypes.JSON `gorm:"column:screenshot_urls"` } func rowsToDTO(rows []listingRow) []ListingDTO { items := make([]ListingDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items } func normalizedListingPriceCent(req CreateRequest) int64 { if req.AssetSummary != nil { if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok { buyerPrice := readSummaryNumber(breakdown["buyer_total_price"]) if buyerPrice > 0 { return yuanToCent(buyerPrice) } } } return req.PriceCent } func publicListings(items []ListingDTO) []ListingDTO { for index := range items { applyPublicListingURLs(&items[index]) } return items } func sellerListings(items []ListingDTO) []ListingDTO { for index := range items { applySellerListingPrice(&items[index]) } return items } func applyPublicListingURLs(item *ListingDTO) { item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus) if item.AssetSummary != nil { delete(item.AssetSummary, "price_breakdown") } } func applySellerListingPrice(item *ListingDTO) { if item == nil || item.AssetSummary == nil { return } breakdown, ok := item.AssetSummary["price_breakdown"].(map[string]any) if !ok { return } sellerPrice := readSummaryNumber(breakdown["seller_total_price"]) if sellerPrice > 0 { item.PriceCent = int64(math.Round(sellerPrice * 100)) } sellerRatio := readSummaryNumber(breakdown["seller_ratio"]) if sellerRatio > 0 { item.AssetSummary["publish_ratio"] = sellerRatio } delete(breakdown, "buyer_coin_base_price") delete(breakdown, "buyer_total_price") delete(breakdown, "buyer_ratio") delete(breakdown, "platform_markup_amount") delete(breakdown, "platform_rule_type") } func (row listingRow) toDTO() ListingDTO { assetSummary := decodeAssetSummary(row.AssetSummary) screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS)) reviewStatus, reviewReason := normalizedReviewState(row.Status, row.ReviewStatus, row.ReviewReason) return ListingDTO{ ID: row.ID, ListingNo: row.ListingNo, AccountID: row.AccountID, OwnerID: row.OwnerID, OwnerPhone: row.OwnerPhone, OwnerNickname: row.OwnerNickname, Title: row.Title, Description: row.Description, GameName: row.GameName, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, RankLevel: row.RankLevel, HafCoinAmount: row.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus), PriceCent: row.PriceCent, DepositAmountCent: row.DepositAmountCent, IsAccelerated: isAcceleratedSale(assetSummary), InTransaction: row.InTransaction, Status: row.Status, ReviewStatus: reviewStatus, ReviewReason: reviewReason, PublishedAt: row.PublishedAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO { assetSummary := decodeAssetSummary(account.AssetSummary) screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS)) reviewStatus, reviewReason := normalizedReviewState(listing.Status, listing.ReviewStatus, listing.ReviewReason) return &ListingDTO{ ID: listing.ID, ListingNo: listing.ListingNo, AccountID: account.ID, OwnerID: listing.OwnerID, Title: account.Title, Description: account.Description, GameName: account.GameName, ServerRegion: account.ServerRegion, LoginPlatform: account.LoginPlatform, RankLevel: account.RankLevel, HafCoinAmount: account.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus), PriceCent: listing.PriceCent, DepositAmountCent: listing.DepositAmountCent, IsAccelerated: isAcceleratedSale(assetSummary), InTransaction: listing.InTransaction, Status: listing.Status, ReviewStatus: reviewStatus, ReviewReason: reviewReason, PublishedAt: listing.PublishedAt, CreatedAt: listing.CreatedAt, UpdatedAt: listing.UpdatedAt, } } func normalizedReviewState(status string, reviewStatus string, reviewReason string) (string, string) { if status == "offline" { return "none", reviewReason } return reviewStatus, reviewReason } func marshalScreenshots(urls []string) (datatypes.JSON, error) { cleaned := cleanScreenshotURLs(urls) if len(cleaned) > 12 { cleaned = cleaned[:12] } raw, err := json.Marshal(cleaned) if err != nil { return nil, err } return datatypes.JSON(raw), nil } func marshalAssetSummary(summary map[string]any) (datatypes.JSON, error) { if summary == nil { return nil, nil } raw, err := json.Marshal(summary) if err != nil { return nil, err } return datatypes.JSON(raw), nil } func decodeScreenshots(raw datatypes.JSON) []string { if len(raw) == 0 { return []string{} } var urls []string if err := json.Unmarshal(raw, &urls); err != nil { return []string{} } return urls } func decodeAssetSummary(raw datatypes.JSON) map[string]any { if len(raw) == 0 { return nil } var summary map[string]any if err := json.Unmarshal(raw, &summary); err != nil { return nil } return summary } func isAcceleratedSale(summary map[string]any) bool { if summary == nil { return false } breakdown, ok := summary["price_breakdown"].(map[string]any) if !ok { return false } referenceRatio := readSummaryNumber(breakdown["seller_reference_ratio"]) sellerRatio := readSummaryNumber(breakdown["seller_ratio"]) acceleratedRatio := readSummaryNumber(breakdown["accelerated_sale_ratio"]) if referenceRatio <= 0 { return false } return sellerRatio > referenceRatio || acceleratedRatio > referenceRatio } func readSummaryNumber(value any) float64 { switch typed := value.(type) { case float64: return typed case float32: return float64(typed) case int: return float64(typed) case int64: return float64(typed) case json.Number: number, err := typed.Float64() if err != nil { return 0 } return number case string: number, err := strconv.ParseFloat(strings.TrimSpace(typed), 64) if err != nil { return 0 } return number default: return 0 } } func ensurePriceBreakdown(summary map[string]any) map[string]any { if summary == nil { return map[string]any{} } breakdown, ok := summary["price_breakdown"].(map[string]any) if ok { return breakdown } breakdown = map[string]any{} if raw, ok := summary["price_breakdown"].(map[string]interface{}); ok { for key, value := range raw { breakdown[key] = value } } return breakdown } func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) { if req.BuyerTotalPriceCent > 0 { buyerTotalPrice := roundMoney(centToYuan(req.BuyerTotalPriceCent)) buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice) if buyerCoinBasePrice <= 0 || coinWan <= 0 { return 0, 0, 0 } return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice) } if req.BuyerRatio <= 0 || coinWan <= 0 { return 0, 0, 0 } buyerCoinBasePrice := roundMoney(coinWan / req.BuyerRatio) buyerTotalPrice := roundMoney(buyerCoinBasePrice + consumablePrice) return buyerCoinBasePrice, buyerTotalPrice, roundRatio(coinWan / buyerCoinBasePrice) } func roundRatio(value float64) float64 { if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return math.Round(value*10) / 10 } func yuanToCent(value float64) int64 { return int64(math.Round(roundMoney(value) * 100)) } func centToYuan(value int64) float64 { return float64(value) / 100 } func cleanScreenshotURLs(urls []string) []string { cleaned := make([]string, 0, len(urls)) seen := make(map[string]struct{}, len(urls)) for _, url := range urls { url = strings.TrimSpace(url) if url == "" { continue } if _, ok := seen[url]; ok { continue } seen[url] = struct{}{} cleaned = append(cleaned, url) } return cleaned } func firstScreenshotURL(urls []string) string { if len(urls) == 0 { return "" } 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 } // roundMoney 使用统一的角精度(0.1元) func roundMoney(value float64) float64 { return money.Round(value) }