Files
kefu/im/backend/internal/app/pet_feed_pricing.go
Your NameandClaude Opus 5 0031f79eb3 管理端补上运营真正要用的那几件事
原来 107 个后台接口配的是一套只能看的界面:风险中心 6 行代码全是只读,
动态和消息只能一条一条点,运营想发一句公告没有任何入口。

公告与通知:按全体、会员、城市、喂养者、有宠物的用户或指定名单发系统通知,
发之前能看到会发给多少人,可同时走离线推送。撤回只删还没读的那些——已经
读过的假装收回去只会让统计说谎。

资金总览:把"平台账上哪些钱不是自己的"单独摆出来(任务托管中 + 喂养者余额 +
申诉冻结),旁边才是收入、退款和待打款,最后是按产品和按日的收款。

退款统一到一个入口:会员、代喂、保证金都能分几次退到退完,每笔记一行;
退完最后一分钱才撤销买到的东西,半份会员仍然是会员。

风险中心能动手了:按等级和状态筛,行内直接封禁、冻结、加白(必须写理由)、
跳用户详情,事件可以标记跟进完成。

批量处置:动态、消息、举报支持勾选后批量下架/恢复/驳回,一次最多 100 条。
处罚仍然一条一条来——批量封禁是错封人最快的方式。

敏感词统一词库:消息、动态、送养共用一套,命中是拦截还是转人工由运营定;
转人工的命中会在风险中心留一条,管理端还能拿真实文案先试一遍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 13:07:58 +08:00

121 lines
4.0 KiB
Go

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),
})
}