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