整改一期:发布这条链路先跑顺
四条改动都在"发布一个代喂任务"这条路上: 定位拿不到不再把人卡死。地址可以不带坐标保存,界面标成"未核验定位", 这条地址的打卡照常,只是不比对距离——总比逼着用户放弃发布要好。客户端 另外给了两条退路:去系统设置开权限,或者地图选点。 价格从写死的 30 元改成服务端按物种、城市、每次时长算的推荐价,规则放在 配置里,运营改一次不用等发版;用户改过之后就以他填的为准,不再被覆盖。 发布与支付并成一步:底部常驻"合计 ¥x = n 次 × ¥y",主按钮是"支付并发布"。 没付成的任务留在"我的任务"里能接着付,超过 24 小时由后台关掉,不再变成 既看不见又删不掉的孤儿。 选宠物换成带头像的卡片,缺免疫记录或照片的当场标出来,不用等提交才知道。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9345805133
commit
ebf7189533
@@ -0,0 +1,120 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user