220 lines
8.8 KiB
Go
220 lines
8.8 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/sundynix/pets-be/internal/breeds"
|
||
"github.com/sundynix/pets-be/internal/model"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
func (a *API) breedQuery(c *gin.Context) *gorm.DB {
|
||
return search(a.db.Model(&model.Breed{}), c, "name", "english_name", "aliases", "pinyin", "pinyin_initials")
|
||
}
|
||
func (a *API) listBreeds(c *gin.Context) {
|
||
if len([]rune(c.Query("q"))) > 128 {
|
||
fail(c, 400, "搜索内容过长")
|
||
return
|
||
}
|
||
query := a.breedQuery(c).Where("enabled = true")
|
||
var rows []model.Breed
|
||
if err := query.Order("initial ASC, pinyin ASC, name ASC").Find(&rows).Error; err != nil {
|
||
fail(c, 500, "读取品种失败")
|
||
return
|
||
}
|
||
// Users only need the selection fields; no internal source metadata.
|
||
items := make([]gin.H, 0, len(rows))
|
||
for _, row := range rows {
|
||
items = append(items, gin.H{"id": row.ID, "name": row.Name, "english_name": row.EnglishName, "aliases": row.Aliases, "pinyin": row.Pinyin, "pinyin_initials": row.PinyinInitials, "initial": row.Initial, "has_details": row.Summary != "" || row.Origin != "", "origin": row.Origin})
|
||
}
|
||
ok(c, items)
|
||
}
|
||
func (a *API) adminBreeds(c *gin.Context) {
|
||
query := a.breedQuery(c).Preload("References")
|
||
if status := c.Query("status"); status != "" {
|
||
if status != "enabled" && status != "disabled" {
|
||
fail(c, 400, "状态无效")
|
||
return
|
||
}
|
||
query = query.Where("enabled = ?", status == "enabled")
|
||
}
|
||
if initial := c.Query("initial"); initial != "" {
|
||
if len(initial) != 1 || (initial != "#" && (initial < "A" || initial > "Z")) {
|
||
fail(c, 400, "字母无效")
|
||
return
|
||
}
|
||
query = query.Where("initial = ?", initial)
|
||
}
|
||
rows, page, valid := adminRows[model.Breed](c, query, "initial ASC, pinyin ASC, name ASC")
|
||
if !valid {
|
||
return
|
||
}
|
||
page.Items = rows
|
||
ok(c, page)
|
||
}
|
||
|
||
type breedInput struct {
|
||
LifeSpan string `json:"life_span"`
|
||
Exercise string `json:"exercise"`
|
||
Grooming string `json:"grooming"`
|
||
Traits map[string]int `json:"traits"`
|
||
FCINumber string `json:"fci_number"`
|
||
Origin string `json:"origin"`
|
||
BreedGroup string `json:"breed_group"`
|
||
Height string `json:"height"`
|
||
Weight string `json:"weight"`
|
||
Summary string `json:"summary"`
|
||
Temperament string `json:"temperament"`
|
||
CareNotes string `json:"care_notes"`
|
||
Name string `json:"name" binding:"required"`
|
||
EnglishName string `json:"english_name"`
|
||
Aliases string `json:"aliases"`
|
||
Enabled *bool `json:"enabled" binding:"required"`
|
||
}
|
||
|
||
func bindBreed(c *gin.Context) (model.Breed, bool) {
|
||
var in breedInput
|
||
if c.ShouldBindJSON(&in) != nil {
|
||
fail(c, 400, "请填写品种名称和状态")
|
||
return model.Breed{}, false
|
||
}
|
||
in.Name = strings.TrimSpace(in.Name)
|
||
in.EnglishName = strings.TrimSpace(in.EnglishName)
|
||
in.Aliases = strings.TrimSpace(in.Aliases)
|
||
if in.Name == "" || len([]rune(in.Name)) > 128 || len([]rune(in.EnglishName)) > 128 || len([]rune(in.Aliases)) > 512 {
|
||
fail(c, 400, "品种资料长度无效")
|
||
return model.Breed{}, false
|
||
}
|
||
if len([]rune(in.Origin)) > 128 || len([]rune(in.BreedGroup)) > 255 || len([]rune(in.Height)) > 128 || len([]rune(in.Weight)) > 128 || len([]rune(in.Summary)) > 2000 || len([]rune(in.Temperament)) > 255 || len([]rune(in.CareNotes)) > 2000 {
|
||
fail(c, 400, "百科资料长度无效")
|
||
return model.Breed{}, false
|
||
}
|
||
if len([]rune(in.LifeSpan)) > 128 || len([]rune(in.Exercise)) > 255 || len([]rune(in.Grooming)) > 255 || len(in.FCINumber) > 16 {
|
||
fail(c, 400, "照顾资料长度无效")
|
||
return model.Breed{}, false
|
||
}
|
||
validTraits := map[string]bool{"运动需求": true, "运动活力": true, "训练接受度": true, "掉毛程度": true, "吠叫倾向": true, "梳毛需求": true, "亲近家人": true}
|
||
for key, value := range in.Traits {
|
||
if !validTraits[key] || value < 1 || value > 5 {
|
||
fail(c, 400, "指数需要为 1–5")
|
||
return model.Breed{}, false
|
||
}
|
||
}
|
||
scores, _ := json.Marshal(in.Traits)
|
||
full, short, initial := breeds.SortFields(in.Name)
|
||
return model.Breed{LifeSpan: strings.TrimSpace(in.LifeSpan), Exercise: strings.TrimSpace(in.Exercise), Grooming: strings.TrimSpace(in.Grooming), FCINumber: strings.TrimSpace(in.FCINumber), Traits: string(scores), Origin: strings.TrimSpace(in.Origin), BreedGroup: strings.TrimSpace(in.BreedGroup), Height: strings.TrimSpace(in.Height), Weight: strings.TrimSpace(in.Weight), Summary: strings.TrimSpace(in.Summary), Temperament: strings.TrimSpace(in.Temperament), CareNotes: strings.TrimSpace(in.CareNotes), Name: in.Name, EnglishName: in.EnglishName, Aliases: in.Aliases, Pinyin: full, PinyinInitials: short, Initial: initial, Enabled: *in.Enabled}, true
|
||
}
|
||
func breedWriteError(c *gin.Context, err error) {
|
||
// pgx PostgreSQL unique_violation, without disclosing DB details.
|
||
type sqlState interface{ SQLState() string }
|
||
var state sqlState
|
||
if errors.As(err, &state) && state.SQLState() == "23505" {
|
||
fail(c, http.StatusConflict, "该品种名称已存在")
|
||
return
|
||
}
|
||
fail(c, 500, "保存品种失败")
|
||
}
|
||
func (a *API) createBreed(c *gin.Context) {
|
||
row, valid := bindBreed(c)
|
||
if !valid {
|
||
return
|
||
}
|
||
row.Source = "管理端录入"
|
||
row.EditorialLocked = true
|
||
row.EncyclopediaUpdatedAt = time.Now().Format("2006-01-02")
|
||
// GORM defaults true for zero values on Create; explicitly select false after insertion.
|
||
err := a.db.Transaction(func(tx *gorm.DB) error {
|
||
enabled := row.Enabled
|
||
if err := tx.Create(&row).Error; err != nil {
|
||
return err
|
||
}
|
||
if !enabled {
|
||
row.Enabled = false
|
||
return tx.Model(&row).Update("enabled", false).Error
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
breedWriteError(c, err)
|
||
return
|
||
}
|
||
created(c, row)
|
||
}
|
||
func (a *API) updateBreed(c *gin.Context) {
|
||
if !validID(c.Param("id")) {
|
||
fail(c, 404, "品种不存在")
|
||
return
|
||
}
|
||
update, valid := bindBreed(c)
|
||
if !valid {
|
||
return
|
||
}
|
||
var row model.Breed
|
||
if a.db.First(&row, "id = ?", c.Param("id")).Error != nil {
|
||
fail(c, 404, "品种不存在")
|
||
return
|
||
}
|
||
if row.FieldSources == nil {
|
||
row.FieldSources = map[string]string{}
|
||
}
|
||
before := map[string]string{"name": row.Name, "english_name": row.EnglishName, "aliases": row.Aliases, "origin": row.Origin, "breed_group": row.BreedGroup, "height": row.Height, "weight": row.Weight, "summary": row.Summary, "temperament": row.Temperament, "care_notes": row.CareNotes, "life_span": row.LifeSpan, "exercise": row.Exercise, "grooming": row.Grooming, "fci_number": row.FCINumber, "traits": row.Traits}
|
||
after := map[string]string{"name": update.Name, "english_name": update.EnglishName, "aliases": update.Aliases, "origin": update.Origin, "breed_group": update.BreedGroup, "height": update.Height, "weight": update.Weight, "summary": update.Summary, "temperament": update.Temperament, "care_notes": update.CareNotes, "life_span": update.LifeSpan, "exercise": update.Exercise, "grooming": update.Grooming, "fci_number": update.FCINumber, "traits": update.Traits}
|
||
var previousTraits map[string]int
|
||
if json.Unmarshal([]byte(row.Traits), &previousTraits) == nil {
|
||
normalized, _ := json.Marshal(previousTraits)
|
||
before["traits"] = string(normalized)
|
||
}
|
||
for key, value := range after {
|
||
if value != before[key] && !(key == "traits" && value == "null") {
|
||
row.FieldSources[key] = "editorial"
|
||
}
|
||
}
|
||
row.Origin, row.BreedGroup, row.Height, row.Weight, row.Summary, row.Temperament, row.CareNotes = update.Origin, update.BreedGroup, update.Height, update.Weight, update.Summary, update.Temperament, update.CareNotes
|
||
row.LifeSpan, row.Exercise, row.Grooming, row.FCINumber = update.LifeSpan, update.Exercise, update.Grooming, update.FCINumber
|
||
if update.Traits != "null" {
|
||
row.Traits = update.Traits
|
||
}
|
||
row.EditorialLocked = true
|
||
row.EncyclopediaUpdatedAt = time.Now().Format("2006-01-02")
|
||
row.Name, row.EnglishName, row.Aliases = update.Name, update.EnglishName, update.Aliases
|
||
row.Pinyin, row.PinyinInitials, row.Initial, row.Enabled = update.Pinyin, update.PinyinInitials, update.Initial, update.Enabled
|
||
if err := a.db.Save(&row).Error; err != nil {
|
||
breedWriteError(c, err)
|
||
return
|
||
}
|
||
ok(c, row)
|
||
}
|
||
func (a *API) setBreedEnabled(c *gin.Context) {
|
||
var in struct {
|
||
Enabled *bool `json:"enabled" binding:"required"`
|
||
}
|
||
if !validID(c.Param("id")) || c.ShouldBindJSON(&in) != nil {
|
||
fail(c, 400, "状态参数无效")
|
||
return
|
||
}
|
||
result := a.db.Model(&model.Breed{}).Where("id = ?", c.Param("id")).Update("enabled", *in.Enabled)
|
||
if result.Error != nil {
|
||
fail(c, 500, "更新状态失败")
|
||
return
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
fail(c, 404, "品种不存在")
|
||
return
|
||
}
|
||
ok(c, gin.H{"id": c.Param("id"), "enabled": *in.Enabled})
|
||
}
|
||
|
||
func (a *API) breedDetail(c *gin.Context) {
|
||
var row model.Breed
|
||
if !validID(c.Param("id")) || a.db.Preload("References").Where("id = ? AND enabled = true", c.Param("id")).First(&row).Error != nil {
|
||
fail(c, 404, "品种不存在或已停用")
|
||
return
|
||
}
|
||
ok(c, row)
|
||
}
|