package pricing import ( "math" "regexp" "sort" "strconv" "strings" ) var ( fractionPricePattern = regexp.MustCompile(`(\d+(?:\.\d+)?)\s*元\s*/\s*(\d+(?:\.\d+)?)`) singlePricePattern = regexp.MustCompile(`(\d+(?:\.\d+)?)\s*元`) ) // Normalize 保证配置始终可计算,并修补线上同步/手工编辑产生的空字段。 func Normalize(config Config) Config { defaults := DefaultConfig() config.SchemaVersion = 1 insurances := make([]InsuranceBaseRatio, 0, len(config.InsuranceBaseRatios)) seenInsurance := make(map[string]struct{}, len(config.InsuranceBaseRatios)) for _, item := range config.InsuranceBaseRatios { item.Insurance = strings.TrimSpace(item.Insurance) if item.Insurance == "" || !isFinitePositive(item.Ratio) { continue } if _, exists := seenInsurance[item.Insurance]; exists { continue } seenInsurance[item.Insurance] = struct{}{} item.Ratio = RoundRatio(item.Ratio) insurances = append(insurances, item) } if len(insurances) == 0 { insurances = defaults.InsuranceBaseRatios } config.InsuranceBaseRatios = insurances ratioItems := make([]RatioConfigItem, 0, len(config.RatioConfigItems)) seenRatioKey := make(map[string]struct{}, len(config.RatioConfigItems)) for _, item := range config.RatioConfigItems { item.Key = strings.TrimSpace(item.Key) item.Label = strings.TrimSpace(item.Label) item.Kind = strings.TrimSpace(item.Kind) item.GroupKey = strings.TrimSpace(item.GroupKey) if item.Key == "" || item.Label == "" || !isFiniteNonNegative(item.MissingPenalty) { continue } if _, exists := seenRatioKey[item.Key]; exists { continue } seenRatioKey[item.Key] = struct{}{} configKind := item.Kind if configKind != "skin_group" && configKind != "max_stamina" && configKind != "max_load" { configKind = "skin_group" } item.Kind = configKind item.MissingPenalty = RoundRatio(item.MissingPenalty) ratioItems = append(ratioItems, item) } if len(ratioItems) == 0 { ratioItems = defaults.RatioConfigItems } config.RatioConfigItems = ratioItems corrections := make([]CoinCorrection, 0, len(config.CoinCorrections)) for _, item := range config.CoinCorrections { if !isFiniteNonNegative(item.ThresholdM) || !isFiniteNonNegative(item.Correction) { continue } item.ThresholdM = RoundRatio(item.ThresholdM) item.Correction = RoundRatio(item.Correction) corrections = append(corrections, item) } if len(corrections) == 0 { corrections = defaults.CoinCorrections } sort.SliceStable(corrections, func(i, j int) bool { return corrections[i].ThresholdM < corrections[j].ThresholdM }) config.CoinCorrections = corrections quantityItems := make([]QuantityItem, 0, len(config.QuantityItems)) seenQuantityKey := make(map[string]struct{}, len(config.QuantityItems)) for _, item := range config.QuantityItems { item.Key = strings.TrimSpace(item.Key) item.Label = strings.TrimSpace(item.Label) item.Price = strings.TrimSpace(item.Price) item.Unit = strings.TrimSpace(item.Unit) if item.Key == "" || item.Label == "" || item.Price == "" { continue } if _, exists := seenQuantityKey[item.Key]; exists { continue } seenQuantityKey[item.Key] = struct{}{} if item.Unit == "" { item.Unit = defaultUnit(item.Key, item.Label) } quantityItems = append(quantityItems, item) } if len(quantityItems) == 0 { quantityItems = defaults.QuantityItems } config.QuantityItems = quantityItems lossOptions := make([]float64, 0, len(config.DailyLossOptions)) seenLoss := make(map[float64]struct{}, len(config.DailyLossOptions)) for _, value := range config.DailyLossOptions { if !isFinitePositive(value) { continue } value = RoundRatio(value) if _, exists := seenLoss[value]; exists { continue } seenLoss[value] = struct{}{} lossOptions = append(lossOptions, value) } if len(lossOptions) == 0 { lossOptions = defaults.DailyLossOptions } sort.Float64s(lossOptions) config.DailyLossOptions = lossOptions if !isFinitePositive(config.AcceleratedMaxRatio) { config.AcceleratedMaxRatio = defaults.AcceleratedMaxRatio } config.AcceleratedMaxRatio = math.Min(RoundRatio(config.AcceleratedMaxRatio), 50) config.SyncURL = strings.TrimRight(strings.TrimSpace(config.SyncURL), "/") if config.SyncURL == "" { config.SyncURL = defaults.SyncURL } return config } // ParseUnitPrice 解析「0.6元/发」「6元/60发」这类单价文本,返回单个物品的价格。 func ParseUnitPrice(price string) float64 { normalized := strings.ReplaceAll(strings.TrimSpace(price), "/", "/") normalized = strings.ReplaceAll(normalized, ",", ",") if match := fractionPricePattern.FindStringSubmatch(normalized); len(match) == 3 { amount := parseFloat(match[1]) count := parseFloat(match[2]) if amount > 0 && count > 0 { return amount / count } } if match := singlePricePattern.FindStringSubmatch(normalized); len(match) == 2 { return math.Max(parseFloat(match[1]), 0) } return 0 } func RoundMoney(value float64) float64 { return math.Round(value*10) / 10 } func RoundRatio(value float64) float64 { return math.Round(value*10) / 10 } func isItemDisabledForInsurance(item QuantityItem, insurance string) bool { if item.Disabled { return true } if insurance != "3*3" { return false } return item.Key == "gridCard9" || strings.Contains(item.Label, "9格") || strings.Contains(item.Label, "顶级保险卡") } func defaultUnit(key, label string) string { switch key { case "awmAmmo", "level6Ammo", "barrettAmmo": return "发" case "gridCard9": return "张" } if strings.Contains(label, "子弹") { return "发" } if strings.Contains(label, "卡") { return "张" } return "个" } func parseFloat(value string) float64 { result, err := strconv.ParseFloat(strings.TrimSpace(value), 64) if err != nil || math.IsNaN(result) || math.IsInf(result, 0) { return 0 } return result } func isFinitePositive(value float64) bool { return value > 0 && !math.IsNaN(value) && !math.IsInf(value, 0) } func isFiniteNonNegative(value float64) bool { return value >= 0 && !math.IsNaN(value) && !math.IsInf(value, 0) }