整改三期:退款、后台增改删、悬赏

退款原来只有会员那条路,而且只能全额、只改本地状态。现在代喂任务的退款
能分几次退到退完:每一笔单独记一行(谁退的、多少、走哪条通道、为什么),
订单上的 refunded_cent 跟着累加,退完且未结算的任务才关闭。渠道接通时按
金额原路退回并带独立幂等键,没接通或沙箱支付的登记为"线下已退"——假装
调用了渠道比说实话更糟。已结算的任务不能顺手退:钱在喂养者账上,要退只能
由平台承担,必须显式勾选。

后台从"只能通过或驳回"变成能改能删:宠物档案可代建、可编辑、可软删除
(有进行中任务的删不掉),送养与配种能改文案和城市、能下架能删,代喂任务
能改备注、能取消(托管中的钱必须先退)。每一个写操作都进审计日志。

悬赏按方案里的 A 做:主人自愿加的钱跟着任务一起托管,结算时全额给喂养者,
平台服务费只算在基础报酬上。抽悬赏是喂养者一眼能算出来的事。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-07 11:25:02 +08:00
co-authored by Claude Opus 5
parent ebf7189533
commit a889cb42e4
8 changed files with 1011 additions and 22 deletions
+22 -8
View File
@@ -37,8 +37,10 @@ type feedTaskView struct {
VisitsPerDay int `json:"visitsPerDay"`
MinMinutes int `json:"minMinutes"`
PriceCent int64 `json:"priceCent"`
TipCent int64 `json:"tipCent"`
GrossCent int64 `json:"grossCent"`
PayoutCent int64 `json:"payoutCent"`
RefundedCent int64 `json:"refundedCent"`
Note string `json:"note"`
Status string `json:"status"`
SitterUserID int64 `json:"sitterUserId"`
@@ -238,6 +240,7 @@ func (a *App) createFeedTask(w http.ResponseWriter, r *http.Request) {
VisitsPerDay int `json:"visitsPerDay"`
MinMinutes int `json:"minMinutes"`
PriceCent int64 `json:"priceCent"`
TipCent int64 `json:"tipCent"`
Note string `json:"note"`
Items []struct {
Kind string `json:"kind"`
@@ -308,9 +311,19 @@ func (a *App) createFeedTask(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusNotFound, 30001, "宠物不存在")
return
}
if req.TipCent < 0 {
req.TipCent = 0
}
if maxTip := int64(a.configInt(r.Context(), "pet.feed_tip_max_cent", 20000)); req.TipCent > maxTip {
fail(w, http.StatusBadRequest, 20001, fmt.Sprintf("悬赏最多 %.0f 元", float64(maxTip)/100))
return
}
totalVisits := days * req.VisitsPerDay
gross := req.PriceCent * int64(totalVisits)
fee, payout := a.feedFeeSplit(r.Context(), gross)
base := req.PriceCent * int64(totalVisits)
// 服务费只算在基础报酬上:悬赏是主人给喂养者的,平台不从中抽成。
fee, basePayout := a.feedFeeSplit(r.Context(), base)
gross := base + req.TipCent
payout := basePayout + req.TipCent
tx, err := a.db.BeginTx(r.Context(), nil)
if err != nil {
@@ -319,10 +332,10 @@ func (a *App) createFeedTask(w http.ResponseWriter, r *http.Request) {
}
defer func() { _ = tx.Rollback() }()
result, err := tx.ExecContext(r.Context(), `INSERT INTO pet_feed_tasks
(owner_user_id,pet_id,address_id,city_code,start_date,end_date,visits_per_day,min_minutes,price_cent,gross_cent,platform_fee_cent,payout_cent,note,total_visits)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
(owner_user_id,pet_id,address_id,city_code,start_date,end_date,visits_per_day,min_minutes,price_cent,tip_cent,gross_cent,platform_fee_cent,payout_cent,note,total_visits)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
current(r).ID, req.PetID, req.AddressID, cityCode, start, end, req.VisitsPerDay, req.MinMinutes,
req.PriceCent, gross, fee, payout, strings.TrimSpace(req.Note), totalVisits)
req.PriceCent, req.TipCent, gross, fee, payout, strings.TrimSpace(req.Note), totalVisits)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "创建失败")
return
@@ -350,11 +363,12 @@ func (a *App) createFeedTask(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusInternalServerError, 50001, "创建失败")
return
}
reply(w, map[string]any{"id": taskID, "grossCent": gross, "platformFeeCent": fee, "payoutCent": payout, "totalVisits": totalVisits})
reply(w, map[string]any{"id": taskID, "grossCent": gross, "tipCent": req.TipCent,
"platformFeeCent": fee, "payoutCent": payout, "totalVisits": totalVisits})
}
const feedTaskColumns = `t.id,t.owner_user_id,owner.nickname,t.pet_id,p.name,p.species,p.avatar_url,addr.city_name,addr.area_text,addr.latitude IS NOT NULL,
t.start_date,t.end_date,t.visits_per_day,t.min_minutes,t.price_cent,t.gross_cent,t.payout_cent,t.note,t.status,
t.start_date,t.end_date,t.visits_per_day,t.min_minutes,t.price_cent,t.tip_cent,t.gross_cent,t.payout_cent,t.refunded_cent,t.note,t.status,
COALESCE(t.sitter_user_id,0),COALESCE(sitter.nickname,''),COALESCE(t.conversation_id,0),t.applications_count,t.total_visits,t.completed_visits,t.created_at`
func (a *App) scanFeedTasks(ctx context.Context, rows *sql.Rows, viewer int64) []feedTaskView {
@@ -367,7 +381,7 @@ func (a *App) scanFeedTasks(ctx context.Context, rows *sql.Rows, viewer int64) [
var created time.Time
if rows.Scan(&item.ID, &item.OwnerUserID, &item.OwnerName, &item.PetID, &item.PetName, &item.Species, &item.PetAvatar,
&item.CityName, &item.AreaText, &item.Located, &start, &end, &item.VisitsPerDay, &item.MinMinutes, &item.PriceCent,
&item.GrossCent, &item.PayoutCent, &item.Note, &item.Status, &item.SitterUserID, &item.SitterName,
&item.TipCent, &item.GrossCent, &item.PayoutCent, &item.RefundedCent, &item.Note, &item.Status, &item.SitterUserID, &item.SitterName,
&item.Conversation, &item.Applications, &item.TotalVisits, &item.Done, &created) != nil {
continue
}