207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
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)
|
||
}
|