Files
sundynix-pets/pets-be/internal/breeds/breeds.go
T
Blizzard 9d7cf720a5
ci-cd / verify (push) Successful in 1m59s
ci-cd / deploy (push) Skipped
feat: add multi-source breed encyclopedia and two-hour refresh sessions
2026-10-05 13:38:24 +08:00

103 lines
3.9 KiB
Go

package breeds
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"unicode"
"github.com/mozillazg/go-pinyin"
"github.com/sundynix/pets-be/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
//go:embed data.json
var data []byte
// SortFields provides both full pinyin and initials, retaining Latin breed names.
func SortFields(name string) (string, string, string) {
args := pinyin.NewArgs()
args.Fallback = func(r rune, _ pinyin.Args) []string {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return []string{strings.ToLower(string(r))}
}
return nil
}
words := pinyin.LazyPinyin(name, args)
// Breed-name phrases with common alternative pronunciations.
if strings.HasPrefix(name, "重庆") && len(words) > 0 {
words[0] = "chong"
}
if strings.HasPrefix(name, "长毛") && len(words) > 0 {
words[0] = "chang"
}
full := strings.Join(words, "")
var initials strings.Builder
for _, word := range words {
if word != "" {
initials.WriteByte(word[0])
}
}
initial := "#"
if len(full) > 0 && full[0] >= 'a' && full[0] <= 'z' {
initial = strings.ToUpper(full[:1])
}
return full, initials.String(), initial
}
func Defaults() ([]model.Breed, error) {
var rows []struct {
Origin string `json:"origin"`
BreedGroup string `json:"breed_group"`
FCINumber string `json:"fci_number"`
EnglishName string `json:"english_name"`
Height string `json:"height"`
Weight string `json:"weight"`
Summary string `json:"summary"`
Temperament string `json:"temperament"`
Traits string `json:"traits"`
CareNotes string `json:"care_notes"`
EncyclopediaUpdatedAt string `json:"encyclopedia_updated_at"`
Name string `json:"name"`
Aliases string `json:"aliases"`
Source string `json:"source"`
SourceKey string `json:"source_key"`
SourceURL string `json:"source_url"`
}
if err := json.Unmarshal(data, &rows); err != nil {
return nil, err
}
result := make([]model.Breed, 0, len(rows))
for _, row := range rows {
full, short, initial := SortFields(row.Name)
key := row.SourceKey
result = append(result, model.Breed{Origin: row.Origin, BreedGroup: row.BreedGroup, FCINumber: row.FCINumber, EnglishName: row.EnglishName, Height: row.Height, Weight: row.Weight, Summary: row.Summary, Temperament: row.Temperament, Traits: row.Traits, CareNotes: row.CareNotes, EncyclopediaUpdatedAt: row.EncyclopediaUpdatedAt, Name: row.Name, Aliases: row.Aliases, Pinyin: full, PinyinInitials: short, Initial: initial, Enabled: true, Source: row.Source, SourceKey: &key, SourceURL: row.SourceURL})
}
return result, nil
}
// Existing entries (including edits and disabled breeds) are never overwritten.
func Seed(db *gorm.DB) (int64, error) {
rows, err := Defaults()
if err != nil {
return 0, fmt.Errorf("load breeds: %w", err)
}
result := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(rows, 100)
if result.Error != nil {
return result.RowsAffected, result.Error
}
// Backfill newly introduced encyclopedia fields exactly once on untouched seed rows.
// Future starts never overwrite administrator changes.
for _, row := range rows {
if row.Summary == "" {
continue
}
if err := db.Model(&model.Breed{}).Where("source_key = ? AND name = ? AND (encyclopedia_updated_at IS NULL OR encyclopedia_updated_at = '') AND updated_at = created_at", row.SourceKey, row.Name).Updates(map[string]any{"origin": row.Origin, "breed_group": row.BreedGroup, "fci_number": row.FCINumber, "english_name": row.EnglishName, "height": row.Height, "weight": row.Weight, "summary": row.Summary, "temperament": row.Temperament, "traits": row.Traits, "care_notes": row.CareNotes, "encyclopedia_updated_at": row.EncyclopediaUpdatedAt}).Error; err != nil {
return result.RowsAffected, err
}
}
return result.RowsAffected, enrich(db)
}