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

退款原来只有会员那条路,而且只能全额、只改本地状态。现在代喂任务的退款
能分几次退到退完:每一笔单独记一行(谁退的、多少、走哪条通道、为什么),
订单上的 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
+39 -9
View File
@@ -395,8 +395,10 @@ func (a *App) adminPets(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusInternalServerError, 50001, "查询宠物失败")
return
}
// 后台要能改档案,所以这里返回完整字段,而不是只够列表显示的那几个。
rows, err := a.db.QueryContext(r.Context(), `SELECT p.id,p.owner_user_id,profile.nickname,u.public_id,p.name,p.species,p.breed,p.gender,
p.neutered,p.vaccinated_at,p.avatar_url,p.moderation_status,p.moderation_note,p.created_at`+from+` ORDER BY p.id DESC LIMIT ? OFFSET ?`,
p.neutered,p.vaccinated_at,p.avatar_url,p.moderation_status,p.moderation_note,p.created_at,
p.birthday,p.dewormed_at,p.weight_g,p.chip_no,p.temperament`+from+` ORDER BY p.id DESC LIMIT ? OFFSET ?`,
append(append([]any{}, args...), size, offset)...)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "查询宠物失败")
@@ -404,26 +406,54 @@ func (a *App) adminPets(w http.ResponseWriter, r *http.Request) {
}
defer rows.Close()
items := []map[string]any{}
ids := []any{}
placeholders := []string{}
for rows.Next() {
var id, ownerID int64
var nickname, publicID, name, species, breed, avatar, note string
var gender, neutered, moderation int
var vaccinated sql.NullTime
var nickname, publicID, name, species, breed, avatar, note, chipNo, temperament string
var gender, neutered, moderation, weight int
var vaccinated, birthday, dewormed sql.NullTime
var created time.Time
if rows.Scan(&id, &ownerID, &nickname, &publicID, &name, &species, &breed, &gender, &neutered,
&vaccinated, &avatar, &moderation, &note, &created) != nil {
&vaccinated, &avatar, &moderation, &note, &created, &birthday, &dewormed, &weight, &chipNo, &temperament) != nil {
continue
}
vaccinatedAt := ""
if vaccinated.Valid {
vaccinatedAt = vaccinated.Time.Format("2006-01-02")
day := func(value sql.NullTime) string {
if !value.Valid {
return ""
}
return value.Time.Format("2006-01-02")
}
items = append(items, map[string]any{
"id": id, "ownerUserId": ownerID, "ownerNickname": nickname, "ownerPublicId": publicID,
"name": name, "species": species, "speciesLabel": petSpecies[species], "breed": breed,
"gender": gender, "neutered": neutered == 1, "vaccinatedAt": vaccinatedAt, "avatar": avatar,
"gender": gender, "neutered": neutered == 1, "vaccinatedAt": day(vaccinated), "avatar": avatar,
"moderationStatus": moderation, "moderationNote": note, "createdAt": created.Format(time.RFC3339),
"birthday": day(birthday), "dewormedAt": day(dewormed), "weightG": weight,
"chipNo": chipNo, "temperament": temperament, "photos": []string{},
})
ids = append(ids, id)
placeholders = append(placeholders, "?")
}
// 照片单独取一遍,编辑弹窗要按整组替换,列表里就得先有整组。
if len(ids) > 0 {
mediaRows, mediaErr := a.db.QueryContext(r.Context(),
`SELECT pet_id,media_url FROM pet_media WHERE pet_id IN (`+strings.Join(placeholders, ",")+`) ORDER BY pet_id,sort_order,id`, ids...)
if mediaErr == nil {
defer mediaRows.Close()
for mediaRows.Next() {
var petID int64
var url string
if mediaRows.Scan(&petID, &url) != nil {
continue
}
for index := range items {
if items[index]["id"] == petID {
items[index]["photos"] = append(items[index]["photos"].([]string), url)
}
}
}
}
}
reply(w, map[string]any{"items": items, "total": total, "page": page, "size": size})
}