拆分大型 Repository 文件职责
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user