领养是信息服务:没有金额字段,文案里出现收费话术或短期内连续送养都会转人工, 送出时按 30/90 天建好回访任务。配种同样只做信息展示,且默认关闭。 代喂是这个模块里唯一动钱的部分,顺序是它的安全边界:先付款再进大厅,选定 之后才解密门牌与电话,每次上门按到达/喂食/离开留照片,主人确认或超时自动确认 后才结算。结算只走账本,(user, biz_type, biz_id) 唯一键让重复结算付不出第二次; 申诉会冻结自动确认,由客服裁定全付、部分付或全退。 提现按方案默认关闭:收益先只记账,等分账通道与资质到位再在管理端开闸。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
160 lines
6.1 KiB
Go
160 lines
6.1 KiB
Go
package app
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Mating is information only: two owners find each other and take it offline.
|
|
// The platform takes no fee and gives no guarantee, which is what keeps it out
|
|
// of the business of breeding. It ships switched off — see the plan.
|
|
|
|
func (a *App) createMatingListing(w http.ResponseWriter, r *http.Request) {
|
|
if !a.configBool(r.Context(), "pet.mating_enabled", false) {
|
|
fail(w, http.StatusForbidden, 30002, "配种撮合暂未开放")
|
|
return
|
|
}
|
|
var req struct {
|
|
PetID int64 `json:"petId"`
|
|
CityCode string `json:"cityCode"`
|
|
CityName string `json:"cityName"`
|
|
Expectation string `json:"expectation"`
|
|
VaccineMedia string `json:"vaccineMedia"`
|
|
}
|
|
if decode(r, &req) != nil || req.PetID == 0 {
|
|
fail(w, http.StatusBadRequest, 20001, "配种信息格式错误")
|
|
return
|
|
}
|
|
req.Expectation = strings.TrimSpace(req.Expectation)
|
|
if len([]rune(req.Expectation)) > 500 {
|
|
fail(w, http.StatusBadRequest, 20001, "内容过长")
|
|
return
|
|
}
|
|
// Proof of vaccination is not optional here: an unvaccinated mating is the
|
|
// case that turns into a public-health story.
|
|
if !validMessageMediaURL(req.VaccineMedia, a.config.Environment == "production") {
|
|
fail(w, http.StatusBadRequest, 20001, "请上传免疫证明照片")
|
|
return
|
|
}
|
|
var birthday sql.NullTime
|
|
var neutered int
|
|
var species string
|
|
if a.db.QueryRowContext(r.Context(), `SELECT birthday,neutered,species FROM pets WHERE id=? AND owner_user_id=? AND status=1 AND deleted_at IS NULL`,
|
|
req.PetID, current(r).ID).Scan(&birthday, &neutered, &species) != nil {
|
|
fail(w, http.StatusNotFound, 30001, "宠物不存在")
|
|
return
|
|
}
|
|
if neutered == 1 {
|
|
fail(w, http.StatusBadRequest, 20001, "已绝育的宠物无法发布配种信息")
|
|
return
|
|
}
|
|
if !birthday.Valid {
|
|
fail(w, http.StatusBadRequest, 20001, "请先在档案中填写出生日期")
|
|
return
|
|
}
|
|
if minAge := a.configInt(r.Context(), "pet.mating_min_age_months", 12); monthsSince(birthday.Time) < minAge {
|
|
fail(w, http.StatusBadRequest, 20001, fmt.Sprintf("未满 %d 月龄的宠物不能发布配种信息", minAge))
|
|
return
|
|
}
|
|
var existing int
|
|
_ = a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM pet_mating_listings WHERE pet_id=? AND status=1 AND deleted_at IS NULL`, req.PetID).Scan(&existing)
|
|
if existing > 0 {
|
|
fail(w, http.StatusBadRequest, 20001, "这只宠物已经发布过配种信息")
|
|
return
|
|
}
|
|
result, err := a.db.ExecContext(r.Context(), `INSERT INTO pet_mating_listings(pet_id,owner_user_id,city_code,city_name,expectation,vaccine_media)
|
|
VALUES(?,?,?,?,?,?)`, req.PetID, current(r).ID, strings.TrimSpace(req.CityCode), strings.TrimSpace(req.CityName), req.Expectation, strings.TrimSpace(req.VaccineMedia))
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, 50001, "发布失败")
|
|
return
|
|
}
|
|
id, _ := result.LastInsertId()
|
|
reply(w, map[string]any{"id": id, "pendingReview": true})
|
|
}
|
|
|
|
func (a *App) matingListings(w http.ResponseWriter, r *http.Request) {
|
|
if !a.configBool(r.Context(), "pet.mating_enabled", false) {
|
|
reply(w, map[string]any{"items": []any{}, "enabled": false})
|
|
return
|
|
}
|
|
_, size, offset := pageOptions(r)
|
|
where := " WHERE m.deleted_at IS NULL"
|
|
args := []any{}
|
|
if strings.TrimSpace(r.URL.Query().Get("mine")) == "1" {
|
|
where += " AND m.owner_user_id=?"
|
|
args = append(args, current(r).ID)
|
|
} else {
|
|
where += " AND m.status=1 AND m.moderation_status=1"
|
|
if city := strings.TrimSpace(r.URL.Query().Get("cityCode")); city != "" {
|
|
where += " AND m.city_code=?"
|
|
args = append(args, city)
|
|
}
|
|
if species := strings.TrimSpace(r.URL.Query().Get("species")); species != "" {
|
|
where += " AND p.species=?"
|
|
args = append(args, species)
|
|
}
|
|
}
|
|
rows, err := a.db.QueryContext(r.Context(), `SELECT m.id,m.pet_id,m.owner_user_id,owner.nickname,m.city_name,m.expectation,m.status,m.moderation_status,m.created_at,
|
|
p.name,p.species,p.breed,p.gender,p.birthday,p.avatar_url,p.vaccinated_at
|
|
FROM pet_mating_listings m JOIN pets p ON p.id=m.pet_id JOIN user_profiles owner ON owner.user_id=m.owner_user_id`+where+
|
|
` ORDER BY m.id DESC LIMIT ? OFFSET ?`, append(args, size+1, offset)...)
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, 50001, "查询配种信息失败")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
items := []map[string]any{}
|
|
for rows.Next() {
|
|
var id, petID, ownerID int64
|
|
var ownerName, cityName, expectation, petName, species, breed, avatar string
|
|
var status, moderation, gender int
|
|
var birthday, vaccinated sql.NullTime
|
|
var created time.Time
|
|
if rows.Scan(&id, &petID, &ownerID, &ownerName, &cityName, &expectation, &status, &moderation, &created,
|
|
&petName, &species, &breed, &gender, &birthday, &avatar, &vaccinated) != nil {
|
|
continue
|
|
}
|
|
ageMonths := 0
|
|
if birthday.Valid {
|
|
ageMonths = monthsSince(birthday.Time)
|
|
}
|
|
vaccinatedAt := ""
|
|
if vaccinated.Valid {
|
|
vaccinatedAt = vaccinated.Time.Format("2006-01-02")
|
|
}
|
|
items = append(items, map[string]any{
|
|
"id": id, "petId": petID, "ownerUserId": ownerID, "ownerName": ownerName, "cityName": cityName,
|
|
"expectation": expectation, "status": status, "moderationStatus": moderation,
|
|
"petName": petName, "species": species, "speciesLabel": petSpecies[species], "breed": breed,
|
|
"gender": gender, "ageMonths": ageMonths, "avatar": avatar, "vaccinatedAt": vaccinatedAt,
|
|
"mine": ownerID == current(r).ID, "createdAt": created.Format(time.RFC3339),
|
|
})
|
|
}
|
|
hasMore := len(items) > size
|
|
if hasMore {
|
|
items = items[:size]
|
|
}
|
|
reply(w, map[string]any{"items": items, "hasMore": hasMore, "enabled": true})
|
|
}
|
|
|
|
func (a *App) closeMatingListing(w http.ResponseWriter, r *http.Request) {
|
|
id, err := pathID(r)
|
|
if err != nil {
|
|
fail(w, http.StatusBadRequest, 20001, "编号无效")
|
|
return
|
|
}
|
|
result, err := a.db.ExecContext(r.Context(), `UPDATE pet_mating_listings SET status=0 WHERE id=? AND owner_user_id=? AND status=1`, id, current(r).ID)
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, 50001, "操作失败")
|
|
return
|
|
}
|
|
if affected, _ := result.RowsAffected(); affected == 0 {
|
|
fail(w, http.StatusNotFound, 30001, "配种信息不存在")
|
|
return
|
|
}
|
|
reply(w, map[string]bool{"success": true})
|
|
}
|