package app import ( "context" "encoding/json" "net/http" "strconv" "strings" ) // A price the app fills in for you has to be defensible: a cat in a small city // is not a dog in Shenzhen. The table lives in config so operations can move it // without a release, and the number stays editable — the suggestion is a // starting point, not a rule. type priceSuggestConfig struct { Base map[string]int `json:"base"` // 物种基准单价(分) Cities map[string]int `json:"cities"` // 城市系数(百分比),键是城市编码前两位或全码 Minutes map[string]int `json:"minutes"` // 每次时长加价(分) DogWalk int `json:"dogWalk"` // 含遛狗时的额外加价(分) Rounding int `json:"rounding"` // 取整到多少分 } const defaultPriceSuggest = `{ "base": {"cat": 3000, "dog": 4500, "other": 3000}, "cities": {"11": 120, "31": 120, "44": 115, "51": 105, "33": 110}, "minutes": {"15": -300, "20": 0, "30": 500, "45": 1200}, "dogWalk": 500, "rounding": 100 }` func (a *App) priceSuggestConfig(ctx context.Context) priceSuggestConfig { parsed := priceSuggestConfig{} raw := strings.TrimSpace(a.configPlain(ctx, "pet.feed_price_suggest_json", "")) if raw == "" || json.Unmarshal([]byte(raw), &parsed) != nil { _ = json.Unmarshal([]byte(defaultPriceSuggest), &parsed) } if parsed.Rounding < 1 { parsed.Rounding = 100 } return parsed } // suggestFeedPrice returns the per-visit price in cents, plus the band the // server will accept, so the form can warn before the user submits. func (a *App) suggestFeedPrice(ctx context.Context, species, cityCode string, minutes int, walking bool) (int, int, int) { config := a.priceSuggestConfig(ctx) price, ok := config.Base[species] if !ok { price = config.Base["other"] } if price <= 0 { price = 3000 } // 先按城市系数缩放:同一件事在不同城市值不同的钱。 factor := 0 if code := strings.TrimSpace(cityCode); code != "" { if value, hit := config.Cities[code]; hit { factor = value } else if len(code) >= 2 { if value, hit = config.Cities[code[:2]]; hit { factor = value } } } if factor <= 0 { factor = 100 } price = price * factor / 100 if extra, hit := config.Minutes[strconv.Itoa(minutes)]; hit { price += extra } if walking { price += config.DogWalk } minPrice := a.configInt(ctx, "pet.feed_min_price_cent", 2000) maxPrice := a.configInt(ctx, "pet.feed_max_price_cent", 50000) price = (price / config.Rounding) * config.Rounding if price < minPrice { price = minPrice } if price > maxPrice { price = maxPrice } return price, minPrice, maxPrice } func (a *App) feedPriceSuggestion(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() species := strings.TrimSpace(query.Get("species")) if petID, err := strconv.ParseInt(strings.TrimSpace(query.Get("petId")), 10, 64); err == nil && petID > 0 { var owned string if a.db.QueryRowContext(r.Context(), `SELECT species FROM pets WHERE id=? AND owner_user_id=? AND deleted_at IS NULL`, petID, current(r).ID).Scan(&owned) == nil { species = owned } } if _, ok := petSpecies[species]; !ok { species = "other" } minutes, _ := strconv.Atoi(strings.TrimSpace(query.Get("minMinutes"))) if minutes <= 0 { minutes = 20 } cityCode := strings.TrimSpace(query.Get("cityCode")) if cityCode == "" { if addressID, err := strconv.ParseInt(strings.TrimSpace(query.Get("addressId")), 10, 64); err == nil && addressID > 0 { _ = a.db.QueryRowContext(r.Context(), `SELECT city_code FROM pet_addresses WHERE id=? AND user_id=?`, addressID, current(r).ID).Scan(&cityCode) } } walking := strings.TrimSpace(query.Get("walk")) == "1" || species == "dog" price, minPrice, maxPrice := a.suggestFeedPrice(r.Context(), species, cityCode, minutes, walking) reply(w, map[string]any{ "priceCent": price, "minPriceCent": minPrice, "maxPriceCent": maxPrice, "species": species, "cityCode": cityCode, "minMinutes": minutes, "feePercent": a.configInt(r.Context(), "pet.feed_platform_fee_percent", 15), }) }