package listing import ( "encoding/json" "regexp" "strconv" "strings" ) var priceNumberPattern = regexp.MustCompile(`(\d+(?:\.\d+)?)`) type publishRules struct { FireLevelMin int } func validateRequest(req CreateRequest, rules publishRules) error { if strings.TrimSpace(req.Title) == "" { return ErrMissingTitle } if strings.TrimSpace(req.ServerRegion) == "" { return ErrMissingServerRegion } if normalizedListingPriceCent(req) <= 0 { return ErrInvalidPrice } if req.DepositAmountCent < 0 { return ErrInvalidDeposit } if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmountCent <= yuanToCent(consumables) { return ErrDepositTooLow } if req.HafCoinAmount < 0 { return ErrInvalidHafCoin } if !hasScreenshotURL(req.ScreenshotURLS) { return ErrMissingScreenshot } if fireLevel, ok := readFireLevel(req.AssetSummary); ok && fireLevel < rules.FireLevelMin { return FireLevelTooLowError{Min: rules.FireLevelMin} } return nil } func validateRequiredOnlineTime(req CreateRequest) error { start, end, ok := readOnlineTime(req.AssetSummary) if !ok { return ErrMissingOnlineTime } startMinute, ok := parseOnlineMinute(start) if !ok { return ErrInvalidOnlineTime } endMinute, ok := parseOnlineMinute(end) if !ok { return ErrInvalidOnlineTime } if startMinute >= endMinute { return ErrInvalidOnlineTime } return nil } func readOnlineTime(summary map[string]any) (string, string, bool) { if summary == nil { return "", "", false } raw, ok := summary["online_time"] if !ok { return "", "", false } onlineTime, ok := raw.(map[string]any) if !ok { return "", "", false } start, okStart := onlineTime["start"].(string) end, okEnd := onlineTime["end"].(string) start = strings.TrimSpace(start) end = strings.TrimSpace(end) return start, end, okStart && okEnd && start != "" && end != "" } func parseOnlineMinute(value string) (int, bool) { parts := strings.Split(strings.TrimSpace(value), ":") if len(parts) != 2 { return 0, false } hour, err := strconv.Atoi(parts[0]) if err != nil { return 0, false } minute, err := strconv.Atoi(parts[1]) if err != nil { return 0, false } if hour < 0 || hour > 23 || minute < 0 || minute > 59 { return 0, false } return hour*60 + minute, true } func consumableValue(summary map[string]any) float64 { if summary == nil { return 0 } rawResources, ok := summary["resources"] if !ok { return 0 } resources, ok := rawResources.([]any) if !ok { return 0 } total := 0.0 for _, raw := range resources { resource, ok := raw.(map[string]any) if !ok { continue } mode, _ := resource["mode"].(string) if strings.TrimSpace(mode) != "收费" { continue } quantity := readSummaryFloat(resource["quantity"]) if quantity <= 0 { continue } priceText, _ := resource["price"].(string) total += quantity * readUnitPrice(priceText) } return roundMoney(total) } func readSummaryFloat(value any) float64 { switch current := value.(type) { case float64: return current case int: return float64(current) case int64: return float64(current) case json.Number: parsed, err := current.Float64() if err != nil { return 0 } return parsed case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(current), 64) if err != nil { return 0 } return parsed default: return 0 } } func readUnitPrice(priceText string) float64 { numbers := priceNumberPattern.FindAllString(priceText, -1) if len(numbers) == 0 { return 0 } amount, err := strconv.ParseFloat(numbers[0], 64) if err != nil { return 0 } if len(numbers) >= 2 { count, err := strconv.ParseFloat(numbers[1], 64) if err == nil && count > 0 { return amount / count } } return amount } func readFireLevel(summary map[string]any) (int, bool) { if summary == nil { return 0, false } value, ok := summary["fire_level"] if !ok { return 0, false } switch current := value.(type) { case float64: return int(current), true case int: return current, true case string: parsed, err := strconv.Atoi(strings.TrimSpace(current)) return parsed, err == nil default: return 0, false } } func hasScreenshotURL(urls []string) bool { for _, url := range urls { if strings.TrimSpace(url) != "" { return true } } return false }