package listing import ( "encoding/json" "math" "sort" "strconv" "strings" "hfb_jysh/backend/internal/modules/systemconfig" "hfb_jysh/backend/pkg/money" ) const ( // ratioBPSScale 把比例统一到基点:1.0 = 10000 bps,1% = 100 bps。 ratioBPSScale int64 = 10000 // hafCoinUnitsPerM 是 1M 哈夫币对应的整数微单位。 hafCoinUnitsPerM int64 = 1_000_000 // maxAcceleratedSaleRatioBPSIncrement 是加速出售比例相对参考比例的上限增量(10 个比例点)。 maxAcceleratedSaleRatioBPSIncrement int64 = 10 * ratioBPSScale ) // ratioToBPS 仅在读取后台旧 float 配置或表单输入时做一次边界换算。 func ratioToBPS(value float64) int64 { if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return int64(math.Round(value * float64(ratioBPSScale))) } func bpsToRatio(value int64) float64 { return float64(value) / float64(ratioBPSScale) } // centFromYuanFloat 仅在兼容旧 float 配置/金额时做一次边界换算。 func centFromYuanFloat(value float64) int64 { if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return int64(math.Round(value * float64(money.CentsPerYuan))) } func centToYuanFloat(value int64) float64 { return float64(value) / float64(money.CentsPerYuan) } func readSummaryInt64(value any) int64 { switch typed := value.(type) { case int: return int64(typed) case int32: return int64(typed) case int64: return typed case uint: return int64(typed) case uint64: if typed > math.MaxInt64 { return 0 } return int64(typed) case float64: if math.IsNaN(typed) || math.IsInf(typed, 0) { return 0 } return int64(typed) case float32: return int64(typed) case json.Number: if parsed, err := typed.Int64(); err == nil { return parsed } if parsed, err := typed.Float64(); err == nil { return int64(parsed) } return 0 case string: trimmed := strings.TrimSpace(typed) if parsed, err := strconv.ParseInt(trimmed, 10, 64); err == nil { return parsed } if parsed, err := strconv.ParseFloat(trimmed, 64); err == nil { return int64(parsed) } return 0 default: return 0 } } // parseUnitPriceCent 把「8元/个」「1元/2个」等旧文案精确解析为单价(分)。 func parseUnitPriceCent(priceText string) int64 { numbers := priceNumberPattern.FindAllString(priceText, -1) if len(numbers) == 0 { return 0 } amountCent, err := money.ParseYuan(numbers[0]) if err != nil || amountCent <= 0 { return 0 } if len(numbers) >= 2 { countCent, err := money.ParseYuan(numbers[1]) if err == nil && countCent >= money.CentsPerYuan && countCent%money.CentsPerYuan == 0 { return amountCent / (countCent / money.CentsPerYuan) } } return amountCent } func quantityItemPriceCent(item systemconfig.PublishQuantityItem) int64 { if item.PriceCent > 0 { return item.PriceCent } return parseUnitPriceCent(item.Price) } func isGridCardResource(key, label string) bool { return strings.TrimSpace(key) == "gridCard9" || strings.Contains(label, "9格体验卡") } // consumableValueCent 按 resources 的整数数量与单价(分)求和。 // 配置项 price_cent 优先;其次资源行 price_cent;最后回退旧 price 文案解析。 func consumableValueCent(summary map[string]any, quantityItems []systemconfig.PublishQuantityItem) int64 { if summary == nil { return 0 } rawResources, ok := summary["resources"].([]any) if !ok { return 0 } configByKey := make(map[string]systemconfig.PublishQuantityItem, len(quantityItems)) for _, item := range quantityItems { configByKey[item.Key] = item } insurance, _ := summary["season_insurance"].(string) total := int64(0) for _, raw := range rawResources { resource, ok := raw.(map[string]any) if !ok { continue } mode, _ := resource["mode"].(string) if strings.TrimSpace(mode) != "收费" { continue } key, _ := resource["key"].(string) label, _ := resource["label"].(string) if strings.TrimSpace(insurance) == "3*3" && isGridCardResource(key, label) { continue } quantity := readSummaryInt64(resource["quantity"]) if quantity <= 0 { continue } unitCent := int64(0) if item, exists := configByKey[strings.TrimSpace(key)]; exists && item.PriceCent > 0 { unitCent = item.PriceCent } if unitCent <= 0 { unitCent = readSummaryInt64(resource["price_cent"]) } if unitCent <= 0 { priceText, _ := resource["price"].(string) unitCent = parseUnitPriceCent(priceText) } if unitCent <= 0 { continue } total += quantity * unitCent } return total } // recommendedDepositCent 优先使用后台 cent 配置,否则对旧 float 配置做一次边界换算。 func recommendedDepositCent( config systemconfig.PublishDepositRecommend, skinGroups []systemconfig.PublishOptionGroup, selectedSkins []string, ) int64 { baseCent := config.BaseAmountCent if baseCent <= 0 { baseCent = centFromYuanFloat(config.BaseAmount) } total := baseCent for _, rule := range config.SkinGroupRules { amountCent := rule.AmountPerItemCent if amountCent <= 0 { amountCent = centFromYuanFloat(rule.AmountPerItem) } if amountCent <= 0 { continue } count := int64(0) for _, group := range skinGroups { if group.Key != rule.GroupKey { continue } for _, skin := range group.Options { if selectedSkinContains(selectedSkins, skin) { count++ } } break } if count > 0 { total += count * amountCent } } return total } func selectedSkinContains(selectedSkins []string, skin string) bool { for _, item := range selectedSkins { if item == skin { return true } } return false } func calculateSellerReferenceRatioBPS(hafCoinAmount int64, summary map[string]any, options systemconfig.PublishOptionsDTO) int64 { if hafCoinAmount <= 0 || summary == nil { return 0 } insurance := readSummaryString(summary["season_insurance"]) stamina := readSummaryString(summary["stamina_level"]) load := readSummaryString(summary["load_level"]) if insurance == "" || stamina == "" || load == "" { return 0 } baseBPS := insuranceBaseRatioBPS(options.RatioConfig.InsuranceBaseRatios, insurance) if baseBPS <= 0 { return 0 } return baseBPS + calculateRatioConfigPenaltyBPS(options, summary, stamina, load) + coinCorrectionBPS(options.RatioConfig.CoinCorrections, hafCoinAmount) + calculateDailyLossRatioAdjustmentBPS(summary) } func insuranceBaseRatioBPS(items []systemconfig.PublishInsuranceBaseRatio, insurance string) int64 { for _, item := range items { if item.Insurance != insurance { continue } if item.RatioBPS > 0 { return item.RatioBPS } return ratioToBPS(item.Ratio) } return 0 } func calculateRatioConfigPenaltyBPS(options systemconfig.PublishOptionsDTO, summary map[string]any, stamina, load string) int64 { selectedSkins := flattenSkinNames(summary) penalty := int64(0) for _, item := range options.RatioConfig.ConfigItems { if ratioConfigItemMatched(item, options, selectedSkins, stamina, load) { continue } if item.MissingPenaltyBPS > 0 { penalty += item.MissingPenaltyBPS continue } penalty += ratioToBPS(item.MissingPenalty) } return penalty } func coinCorrectionBPS(items []systemconfig.PublishCoinCorrection, hafCoinAmount int64) int64 { bestThresholdMicro := int64(-1) correction := int64(0) for _, item := range items { thresholdMicro := int64(math.Round(item.ThresholdM * float64(hafCoinUnitsPerM))) if hafCoinAmount <= thresholdMicro || thresholdMicro < bestThresholdMicro { continue } bestThresholdMicro = thresholdMicro if item.CorrectionBPS > 0 { correction = item.CorrectionBPS } else { correction = ratioToBPS(item.Correction) } } return correction } func calculateDailyLossRatioAdjustmentBPS(summary map[string]any) int64 { if summary == nil { return 0 } dailyLossM := readSummaryInt64(summary["daily_loss_m"]) if dailyLossM <= 10 { return 0 } adjustment := (dailyLossM - 10) / 10 if adjustment > 4 { adjustment = 4 } return adjustment * ratioBPSScale } func clampSaleRatioBPS(referenceBPS, requestedBPS int64) int64 { if referenceBPS <= 0 { return 0 } if requestedBPS <= 0 || requestedBPS < referenceBPS { return referenceBPS } maxBPS := referenceBPS + maxAcceleratedSaleRatioBPSIncrement if requestedBPS > maxBPS { return maxBPS } return requestedBPS } func applySiteReceiveCostDeltaBPS(referenceBPS int64, delta float64) int64 { if referenceBPS <= 0 { return 0 } return referenceBPS + ratioToBPS(NormalizeSiteReceiveCostDelta(delta)) } // sellerCoinBasePriceCentFromHafCoin 使用整数分公式:哈夫币微单位 * 100 / 比例 bps,向下取整。 func sellerCoinBasePriceCentFromHafCoin(hafCoinAmount int64, ratioBPS int64) int64 { if hafCoinAmount <= 0 || ratioBPS <= 0 || hafCoinAmount > math.MaxInt64/100 { return 0 } return hafCoinAmount * 100 / ratioBPS } type externalPlatformPricingCent struct { BuyerCoinBasePriceCent int64 BuyerTotalPriceCent int64 BuyerRatioBPS int64 PlatformMarkupCent int64 RuleType string } // calculateExternalPlatformPricingCent 是卖家发布定价的唯一算术入口,全程使用 int64。 func calculateExternalPlatformPricingCent( hafCoinAmount int64, sellerRatioBPS int64, sellerCoinBasePriceCent int64, consumablePriceCent int64, config salePriceConfig, ) externalPlatformPricingCent { if sellerCoinBasePriceCent < 0 { sellerCoinBasePriceCent = 0 } if consumablePriceCent < 0 { consumablePriceCent = 0 } if rule, ok := findRatioAdjustmentRuleCent(config.RatioAdjustmentRules, hafCoinAmount); ok && sellerRatioBPS > 0 { buyerRatioBPS := sellerRatioBPS - saleRatioSubtractBPS(rule) if buyerRatioBPS > 0 { buyerCoinBasePriceCent := sellerCoinBasePriceCentFromHafCoin(hafCoinAmount, buyerRatioBPS) if buyerCoinBasePriceCent < sellerCoinBasePriceCent { buyerCoinBasePriceCent = sellerCoinBasePriceCent } return buildExternalPlatformPricingCent( hafCoinAmount, sellerCoinBasePriceCent, consumablePriceCent, buyerCoinBasePriceCent, "ratio_subtract", ) } } if rule, ok := findFixedMarkupRuleCent(config.FixedMarkupRules, hafCoinAmount); ok { return buildExternalPlatformPricingCent( hafCoinAmount, sellerCoinBasePriceCent, consumablePriceCent, sellerCoinBasePriceCent+saleFixedMarkupCent(rule), "fixed_markup", ) } return buildExternalPlatformPricingCent( hafCoinAmount, sellerCoinBasePriceCent, consumablePriceCent, sellerCoinBasePriceCent, "none", ) } func buildExternalPlatformPricingCent( hafCoinAmount int64, anchorCoinBasePriceCent int64, consumablePriceCent int64, buyerCoinBasePriceCent int64, ruleType string, ) externalPlatformPricingCent { if buyerCoinBasePriceCent < 0 { buyerCoinBasePriceCent = 0 } buyerTotalPriceCent := buyerCoinBasePriceCent + consumablePriceCent buyerRatioBPS := int64(0) if buyerCoinBasePriceCent > 0 && hafCoinAmount > 0 && hafCoinAmount <= math.MaxInt64/100 { // 展示值:由整数分反推 bps,存在向下取整误差。 buyerRatioBPS = hafCoinAmount * 100 / buyerCoinBasePriceCent } anchorTotalCent := anchorCoinBasePriceCent + consumablePriceCent return externalPlatformPricingCent{ BuyerCoinBasePriceCent: buyerCoinBasePriceCent, BuyerTotalPriceCent: buyerTotalPriceCent, BuyerRatioBPS: buyerRatioBPS, PlatformMarkupCent: buyerTotalPriceCent - anchorTotalCent, RuleType: ruleType, } } func saleFixedMarkupCent(rule saleFixedMarkupRule) int64 { if rule.MarkupAmountCent > 0 { return rule.MarkupAmountCent } return centFromYuanFloat(rule.MarkupAmount) } func saleRatioSubtractBPS(rule saleRatioAdjustmentRule) int64 { if rule.RatioSubtractBPS > 0 { return rule.RatioSubtractBPS } return ratioToBPS(rule.RatioSubtract) } func findFixedMarkupRuleCent(rules []saleFixedMarkupRule, hafCoinAmount int64) (saleFixedMarkupRule, bool) { sorted := append([]saleFixedMarkupRule(nil), rules...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].MinM < sorted[j].MinM }) for index, rule := range sorted { if isCoinInPriceRuleRangeCent(rule.MinM, rule.MaxM, index, len(sorted), hafCoinAmount, true, false) { return rule, true } } return saleFixedMarkupRule{}, false } func findRatioAdjustmentRuleCent(rules []saleRatioAdjustmentRule, hafCoinAmount int64) (saleRatioAdjustmentRule, bool) { sorted := append([]saleRatioAdjustmentRule(nil), rules...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].MinM < sorted[j].MinM }) for index, rule := range sorted { if isCoinInPriceRuleRangeCent(rule.MinM, rule.MaxM, index, len(sorted), hafCoinAmount, false, true) { return rule, true } } return saleRatioAdjustmentRule{}, false } func isCoinInPriceRuleRangeCent( minM float64, maxM float64, index int, total int, hafCoinAmount int64, includeLastMax bool, excludeFirstMin bool, ) bool { minMicro := int64(math.Round(minM * float64(hafCoinUnitsPerM))) maxMicro := int64(math.Round(maxM * float64(hafCoinUnitsPerM))) minMatched := hafCoinAmount >= minMicro if excludeFirstMin && index == 0 { minMatched = hafCoinAmount > minMicro } if !minMatched { return false } if maxMicro <= 0 || hafCoinAmount < maxMicro { return true } return includeLastMax && index == total-1 && hafCoinAmount <= maxMicro } func salePriceConfigHasCentRules(config salePriceConfig) bool { for _, rule := range config.FixedMarkupRules { if rule.MarkupAmountCent > 0 { return true } } for _, rule := range config.RatioAdjustmentRules { if rule.RatioSubtractBPS > 0 { return true } } return false } // syncCanonicalPriceBreakdownFromLegacy 供历史审核/外部调价路径使用: // 这些路径仍以旧 float 字段为准,写回后同步派生规范字段,避免卖家读侧优先读到过期 cent/bps。 func syncCanonicalPriceBreakdownFromLegacy(breakdown map[string]any) { if breakdown == nil { return } for bpsKey, ratioKey := range map[string]string{ "seller_reference_ratio_bps": "seller_reference_ratio", "seller_ratio_bps": "seller_ratio", "accelerated_sale_ratio_bps": "accelerated_sale_ratio", "buyer_ratio_bps": "buyer_ratio", } { ratio := readSummaryNumber(breakdown[ratioKey]) if ratio > 0 { breakdown[bpsKey] = ratioToBPS(ratio) continue } if bps := readSummaryInt64(breakdown[bpsKey]); bps > 0 { breakdown[bpsKey] = bps } } for centKey, yuanKey := range map[string]string{ "seller_coin_base_price_cent": "seller_coin_base_price", "seller_total_price_cent": "seller_total_price", "consumable_price_cent": "consumable_price", "buyer_coin_base_price_cent": "buyer_coin_base_price", "buyer_total_price_cent": "buyer_total_price", "platform_markup_amount_cent": "platform_markup_amount", } { yuan := readSummaryNumber(breakdown[yuanKey]) if yuan > 0 { breakdown[centKey] = centFromYuanFloat(yuan) continue } if cent := readSummaryInt64(breakdown[centKey]); cent > 0 { breakdown[centKey] = cent } } }