Files

531 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}
}