1.0.1
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package pricing
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Calculate 只计算号主到手价:参考比例、加速比例、纯币价、物品价和合计。
|
||||
func (config Config) Calculate(input Input) Result {
|
||||
cfg := Normalize(config)
|
||||
coinM := math.Max(input.CoinM, 0)
|
||||
coinWan := coinM * 100
|
||||
result := Result{
|
||||
Items: []ItemLine{},
|
||||
Valid: false,
|
||||
}
|
||||
|
||||
baseRatio, ok := insuranceBaseRatio(cfg.InsuranceBaseRatios, input.Insurance)
|
||||
if !ok {
|
||||
result.Message = "未找到该保险格数的基础比例,请检查规则配置"
|
||||
result.ItemPrice, result.Items = calculateItemLines(cfg, input)
|
||||
result.TotalPrice = result.ItemPrice
|
||||
return result
|
||||
}
|
||||
|
||||
result.InsuranceBaseRatio = baseRatio
|
||||
result.ConfigPenalty = calculateConfigPenalty(cfg.RatioConfigItems, input.States)
|
||||
result.CoinCorrection = coinCorrection(cfg.CoinCorrections, coinM)
|
||||
result.DailyLossAdjustment = dailyLossRatioAdjustment(input.DailyLossM)
|
||||
|
||||
referenceRatio := RoundRatio(
|
||||
baseRatio +
|
||||
result.ConfigPenalty +
|
||||
result.CoinCorrection +
|
||||
result.DailyLossAdjustment,
|
||||
)
|
||||
result.ReferenceRatio = referenceRatio
|
||||
result.MaxRatio = RoundRatio(referenceRatio + cfg.AcceleratedMaxRatio)
|
||||
result.FinalRatio = referenceRatio
|
||||
if input.SaleRatio > 0 {
|
||||
result.FinalRatio = RoundRatio(clampSaleRatio(input.SaleRatio, referenceRatio, result.MaxRatio))
|
||||
}
|
||||
|
||||
if coinWan > 0 && result.FinalRatio > 0 {
|
||||
result.CoinBasePrice = RoundMoney(coinWan / result.FinalRatio)
|
||||
}
|
||||
result.ItemPrice, result.Items = calculateItemLines(cfg, input)
|
||||
result.TotalPrice = RoundMoney(result.CoinBasePrice + result.ItemPrice)
|
||||
result.Valid = referenceRatio > 0
|
||||
if coinM <= 0 {
|
||||
result.Message = "请输入哈弗币数量"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func insuranceBaseRatio(items []InsuranceBaseRatio, insurance string) (float64, bool) {
|
||||
insurance = strings.TrimSpace(insurance)
|
||||
for _, item := range items {
|
||||
if item.Insurance == insurance {
|
||||
return item.Ratio, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func calculateConfigPenalty(items []RatioConfigItem, states map[string]bool) float64 {
|
||||
penalty := 0.0
|
||||
for _, item := range items {
|
||||
if states[item.Key] {
|
||||
continue
|
||||
}
|
||||
penalty += item.MissingPenalty
|
||||
}
|
||||
return RoundRatio(penalty)
|
||||
}
|
||||
|
||||
// coinCorrection 命中最高阈值档,且使用严格大于,与主站发布逻辑保持一致。
|
||||
func coinCorrection(items []CoinCorrection, coinM float64) float64 {
|
||||
bestThreshold := -1.0
|
||||
correction := 0.0
|
||||
for _, item := range items {
|
||||
if coinM > item.ThresholdM && item.ThresholdM >= bestThreshold {
|
||||
bestThreshold = item.ThresholdM
|
||||
correction = item.Correction
|
||||
}
|
||||
}
|
||||
return correction
|
||||
}
|
||||
|
||||
func dailyLossRatioAdjustment(dailyLossM float64) float64 {
|
||||
if dailyLossM <= 0 {
|
||||
return 0
|
||||
}
|
||||
adjustment := (dailyLossM - 10) / 10
|
||||
if adjustment < 0 {
|
||||
return 0
|
||||
}
|
||||
floored := math.Floor(adjustment)
|
||||
if floored > 4 {
|
||||
return 4
|
||||
}
|
||||
return floored
|
||||
}
|
||||
|
||||
func clampSaleRatio(requested, reference, maxRatio float64) float64 {
|
||||
if requested < reference {
|
||||
return reference
|
||||
}
|
||||
if requested > maxRatio {
|
||||
return maxRatio
|
||||
}
|
||||
return requested
|
||||
}
|
||||
|
||||
func calculateItemLines(config Config, input Input) (float64, []ItemLine) {
|
||||
lines := make([]ItemLine, 0, len(config.QuantityItems))
|
||||
rawTotal := 0.0
|
||||
for _, item := range config.QuantityItems {
|
||||
quantity := math.Max(input.Quantities[item.Key], 0)
|
||||
unitPrice := ParseUnitPrice(item.Price)
|
||||
free := input.FreeItems[item.Key]
|
||||
disabled := isItemDisabledForInsurance(item, input.Insurance)
|
||||
amount := 0.0
|
||||
if !disabled && !free && quantity > 0 && unitPrice > 0 {
|
||||
rawTotal += quantity * unitPrice
|
||||
amount = RoundMoney(quantity * unitPrice)
|
||||
}
|
||||
lines = append(lines, ItemLine{
|
||||
Key: item.Key,
|
||||
Label: item.Label,
|
||||
Quantity: quantity,
|
||||
Unit: item.Unit,
|
||||
UnitPrice: unitPrice,
|
||||
Amount: amount,
|
||||
Free: free,
|
||||
Disabled: disabled,
|
||||
})
|
||||
}
|
||||
return RoundMoney(rawTotal), lines
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package pricing
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScreenshotExample(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
result := config.Calculate(Input{
|
||||
CoinM: 226,
|
||||
Insurance: "3*3",
|
||||
States: map[string]bool{},
|
||||
DailyLossM: 10,
|
||||
})
|
||||
if result.ReferenceRatio != 46 {
|
||||
t.Fatalf("reference ratio = %v, want 46", result.ReferenceRatio)
|
||||
}
|
||||
if result.FinalRatio != 46 {
|
||||
t.Fatalf("final ratio = %v, want 46", result.FinalRatio)
|
||||
}
|
||||
if result.CoinBasePrice != 491.3 {
|
||||
t.Fatalf("coin base price = %v, want 491.3", result.CoinBasePrice)
|
||||
}
|
||||
if result.ItemPrice != 0 {
|
||||
t.Fatalf("item price = %v, want 0", result.ItemPrice)
|
||||
}
|
||||
if result.TotalPrice != 491.3 {
|
||||
t.Fatalf("total price = %v, want 491.3", result.TotalPrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllStatesMatched(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
result := config.Calculate(Input{
|
||||
CoinM: 100,
|
||||
Insurance: "3*3",
|
||||
States: map[string]bool{
|
||||
"melee": true,
|
||||
"operatorRed": true,
|
||||
"staminaMax": true,
|
||||
"loadMax": true,
|
||||
"weapon": true,
|
||||
},
|
||||
DailyLossM: 10,
|
||||
})
|
||||
if result.ReferenceRatio != 40 {
|
||||
t.Fatalf("reference ratio = %v, want 40", result.ReferenceRatio)
|
||||
}
|
||||
if result.CoinBasePrice != 250 {
|
||||
t.Fatalf("coin base price = %v, want 250", result.CoinBasePrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingPenalty(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
result := config.Calculate(Input{
|
||||
CoinM: 100,
|
||||
Insurance: "3*3",
|
||||
States: map[string]bool{
|
||||
"melee": true,
|
||||
"operatorRed": true,
|
||||
"staminaMax": true,
|
||||
"loadMax": true,
|
||||
},
|
||||
DailyLossM: 10,
|
||||
})
|
||||
if result.ConfigPenalty != 1 {
|
||||
t.Fatalf("config penalty = %v, want 1", result.ConfigPenalty)
|
||||
}
|
||||
if result.ReferenceRatio != 41 {
|
||||
t.Fatalf("reference ratio = %v, want 41", result.ReferenceRatio)
|
||||
}
|
||||
if result.CoinBasePrice != 243.9 {
|
||||
t.Fatalf("coin base price = %v, want 243.9", result.CoinBasePrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinCorrectionUsesHighestCrossedThreshold(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
cases := []struct {
|
||||
coinM float64
|
||||
want float64
|
||||
}{
|
||||
{130, 0},
|
||||
{130.1, 1},
|
||||
{230, 1},
|
||||
{230.1, 2.5},
|
||||
{380.1, 4},
|
||||
{600, 5},
|
||||
}
|
||||
for _, item := range cases {
|
||||
result := config.Calculate(Input{CoinM: item.coinM, Insurance: "3*3", DailyLossM: 10})
|
||||
if result.CoinCorrection != item.want {
|
||||
t.Fatalf("coinM=%v correction=%v, want %v", item.coinM, result.CoinCorrection, item.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyLossAdjustment(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
cases := []struct {
|
||||
loss float64
|
||||
want float64
|
||||
}{
|
||||
{0, 0},
|
||||
{10, 0},
|
||||
{20, 1},
|
||||
{30, 2},
|
||||
{40, 3},
|
||||
{50, 4},
|
||||
{90, 4},
|
||||
}
|
||||
for _, item := range cases {
|
||||
result := config.Calculate(Input{CoinM: 100, Insurance: "3*3", DailyLossM: item.loss})
|
||||
if result.DailyLossAdjustment != item.want {
|
||||
t.Fatalf("dailyLoss=%v adjustment=%v, want %v", item.loss, result.DailyLossAdjustment, item.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceleratedRatioClamp(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
base := Input{CoinM: 226, Insurance: "3*3", DailyLossM: 10}
|
||||
|
||||
result := config.Calculate(withSaleRatio(base, 50))
|
||||
if result.FinalRatio != 50 {
|
||||
t.Fatalf("final ratio = %v, want 50", result.FinalRatio)
|
||||
}
|
||||
if result.MaxRatio != 56 {
|
||||
t.Fatalf("max ratio = %v, want 56", result.MaxRatio)
|
||||
}
|
||||
|
||||
result = config.Calculate(withSaleRatio(base, 100))
|
||||
if result.FinalRatio != 56 {
|
||||
t.Fatalf("clamped ratio = %v, want 56", result.FinalRatio)
|
||||
}
|
||||
|
||||
result = config.Calculate(withSaleRatio(base, 30))
|
||||
if result.FinalRatio != 46 {
|
||||
t.Fatalf("ratio below reference = %v, want 46", result.FinalRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumablePriceParsing(t *testing.T) {
|
||||
cases := []struct {
|
||||
price string
|
||||
want float64
|
||||
}{
|
||||
{"0.6元/发", 0.6},
|
||||
{"6元/60发", 0.1},
|
||||
{"2.5元/个", 2.5},
|
||||
{"3元/张", 3},
|
||||
{"2元", 2},
|
||||
{"", 0},
|
||||
}
|
||||
for _, item := range cases {
|
||||
if got := ParseUnitPrice(item.price); math.Abs(got-item.want) > 1e-9 {
|
||||
t.Fatalf("ParseUnitPrice(%q)=%v, want %v", item.price, got, item.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumablesAndFreeItems(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
result := config.Calculate(Input{
|
||||
CoinM: 100,
|
||||
Insurance: "3*3",
|
||||
DailyLossM: 10,
|
||||
States: map[string]bool{
|
||||
"melee": true,
|
||||
"operatorRed": true,
|
||||
"staminaMax": true,
|
||||
"loadMax": true,
|
||||
"weapon": true,
|
||||
},
|
||||
Quantities: map[string]float64{
|
||||
"awmAmmo": 10,
|
||||
"helmet6": 2,
|
||||
},
|
||||
FreeItems: map[string]bool{
|
||||
"helmet6": true,
|
||||
},
|
||||
})
|
||||
if result.ItemPrice != 6 {
|
||||
t.Fatalf("item price = %v, want 6", result.ItemPrice)
|
||||
}
|
||||
if result.TotalPrice != 256 {
|
||||
t.Fatalf("total price = %v, want 256", result.TotalPrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGridCardDisabledAtThreeByThree(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
result := config.Calculate(Input{
|
||||
CoinM: 100,
|
||||
Insurance: "3*3",
|
||||
DailyLossM: 10,
|
||||
Quantities: map[string]float64{"gridCard9": 3},
|
||||
})
|
||||
if result.ItemPrice != 0 {
|
||||
t.Fatalf("item price = %v, want 0", result.ItemPrice)
|
||||
}
|
||||
for _, item := range result.Items {
|
||||
if item.Key == "gridCard9" && !item.Disabled {
|
||||
t.Fatal("gridCard9 should be disabled at 3*3")
|
||||
}
|
||||
}
|
||||
|
||||
result = config.Calculate(Input{
|
||||
CoinM: 100,
|
||||
Insurance: "2*3",
|
||||
DailyLossM: 10,
|
||||
Quantities: map[string]float64{"gridCard9": 3},
|
||||
})
|
||||
if result.ItemPrice != 9 {
|
||||
t.Fatalf("item price at 2*3 = %v, want 9", result.ItemPrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRestoresEmptySections(t *testing.T) {
|
||||
normalized := Normalize(Config{})
|
||||
if len(normalized.InsuranceBaseRatios) == 0 {
|
||||
t.Fatal("insurance ratios should fall back to defaults")
|
||||
}
|
||||
if len(normalized.RatioConfigItems) == 0 {
|
||||
t.Fatal("ratio config items should fall back to defaults")
|
||||
}
|
||||
if len(normalized.QuantityItems) == 0 {
|
||||
t.Fatal("quantity items should fall back to defaults")
|
||||
}
|
||||
if normalized.AcceleratedMaxRatio != 10 {
|
||||
t.Fatalf("accelerated max = %v, want 10", normalized.AcceleratedMaxRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func withSaleRatio(input Input, ratio float64) Input {
|
||||
input.SaleRatio = ratio
|
||||
return input
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package pricing
|
||||
|
||||
const DefaultSyncURL = "https://hao858.com"
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
SchemaVersion: 1,
|
||||
InsuranceBaseRatios: []InsuranceBaseRatio{
|
||||
{Insurance: "3*3", Ratio: 40},
|
||||
{Insurance: "2*3", Ratio: 42},
|
||||
{Insurance: "2*2", Ratio: 47},
|
||||
{Insurance: "2*1", Ratio: 48},
|
||||
},
|
||||
RatioConfigItems: []RatioConfigItem{
|
||||
{Key: "melee", Label: "刀皮", Kind: "skin_group", GroupKey: "melee", MissingPenalty: 1},
|
||||
{Key: "operatorRed", Label: "角色红皮", Kind: "skin_group", GroupKey: "operatorRed", MissingPenalty: 1},
|
||||
{Key: "staminaMax", Label: "满体力", Kind: "max_stamina", MissingPenalty: 1},
|
||||
{Key: "loadMax", Label: "满负重", Kind: "max_load", MissingPenalty: 1},
|
||||
{Key: "weapon", Label: "武器皮肤", Kind: "skin_group", GroupKey: "weapon", MissingPenalty: 1},
|
||||
},
|
||||
CoinCorrections: []CoinCorrection{
|
||||
{ThresholdM: 130, Correction: 1},
|
||||
{ThresholdM: 230, Correction: 2.5},
|
||||
{ThresholdM: 380, Correction: 4},
|
||||
{ThresholdM: 530, Correction: 5},
|
||||
},
|
||||
QuantityItems: []QuantityItem{
|
||||
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.6元/发", Unit: "发"},
|
||||
{Key: "level6Ammo", Label: "6级子弹", Price: "0.1元/发", Unit: "发", Placeholder: "按单发数量填写,60发=6元"},
|
||||
{Key: "helmet6", Label: "6级头盔", Price: "1.5元/个", Unit: "个"},
|
||||
{Key: "armor6", Label: "6级护甲", Price: "2.5元/个", Unit: "个"},
|
||||
{Key: "barrettAmmo", Label: "巴雷特子弹", Price: "0.3元/发", Unit: "发"},
|
||||
{Key: "gridCard9", Label: "顶级保险卡", Price: "3元/张", Unit: "张", Placeholder: "3*3 保险下不可用"},
|
||||
{Key: "coffeeBean", Label: "高级咖啡豆", Price: "2.5元/个", Unit: "个"},
|
||||
{Key: "bulletPart", Label: "高级子弹零件", Price: "3元/个", Unit: "个"},
|
||||
{Key: "redPacket45", Label: "45格大红包", Price: "1.5元/个", Unit: "个"},
|
||||
},
|
||||
DailyLossOptions: []float64{10, 20, 30, 40, 50},
|
||||
AcceleratedMaxRatio: 10,
|
||||
SyncURL: DefaultSyncURL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package pricing
|
||||
|
||||
type InsuranceBaseRatio struct {
|
||||
Insurance string `json:"insurance"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
}
|
||||
|
||||
type RatioConfigItem struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Kind string `json:"kind"`
|
||||
GroupKey string `json:"group_key,omitempty"`
|
||||
MissingPenalty float64 `json:"missing_penalty"`
|
||||
}
|
||||
|
||||
type CoinCorrection struct {
|
||||
ThresholdM float64 `json:"threshold_m"`
|
||||
Correction float64 `json:"correction"`
|
||||
}
|
||||
|
||||
type QuantityItem struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Price string `json:"price"`
|
||||
Unit string `json:"unit"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
InsuranceBaseRatios []InsuranceBaseRatio `json:"insurance_base_ratios"`
|
||||
RatioConfigItems []RatioConfigItem `json:"ratio_config_items"`
|
||||
CoinCorrections []CoinCorrection `json:"coin_corrections"`
|
||||
QuantityItems []QuantityItem `json:"quantity_items"`
|
||||
DailyLossOptions []float64 `json:"daily_loss_options"`
|
||||
AcceleratedMaxRatio float64 `json:"accelerated_max_ratio"`
|
||||
SyncURL string `json:"sync_url"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
CoinM float64 `json:"coin_m"`
|
||||
Insurance string `json:"insurance"`
|
||||
States map[string]bool `json:"states"`
|
||||
DailyLossM float64 `json:"daily_loss_m"`
|
||||
SaleRatio float64 `json:"sale_ratio"`
|
||||
Quantities map[string]float64 `json:"quantities"`
|
||||
FreeItems map[string]bool `json:"free_items"`
|
||||
}
|
||||
|
||||
type ItemLine struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Unit string `json:"unit"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
Amount float64 `json:"amount"`
|
||||
Free bool `json:"free"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ReferenceRatio float64 `json:"reference_ratio"`
|
||||
FinalRatio float64 `json:"final_ratio"`
|
||||
MaxRatio float64 `json:"max_ratio"`
|
||||
InsuranceBaseRatio float64 `json:"insurance_base_ratio"`
|
||||
ConfigPenalty float64 `json:"config_penalty"`
|
||||
CoinCorrection float64 `json:"coin_correction"`
|
||||
DailyLossAdjustment float64 `json:"daily_loss_adjustment"`
|
||||
CoinBasePrice float64 `json:"coin_base_price"`
|
||||
ItemPrice float64 `json:"item_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
Items []ItemLine `json:"items"`
|
||||
Valid bool `json:"valid"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user