Files
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

101 lines
4.0 KiB
Go

package breeds
import (
_ "embed"
"encoding/json"
"fmt"
"github.com/sundynix/pets-be/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"reflect"
)
//go:embed enrichment.json
var enrichmentData []byte
type Enrichment struct {
SourceKey string `json:"source_key"`
Name string `json:"name"`
Fields map[string]string `json:"fields"`
FieldSources map[string]string `json:"field_sources"`
References []model.BreedReference `json:"references"`
}
func Enrichments() ([]Enrichment, error) {
var rows []Enrichment
err := json.Unmarshal(enrichmentData, &rows)
return rows, err
}
// Reference facts remain available even when an editor locks the display content.
func enrich(db *gorm.DB) error {
rows, err := Enrichments()
if err != nil {
return fmt.Errorf("load breed sources: %w", err)
}
defaults, err := Defaults()
if err != nil {
return err
}
baseline := map[string]model.Breed{}
for _, breed := range defaults {
baseline[*breed.SourceKey] = breed
}
for _, item := range rows {
var breed model.Breed
if err := db.Where("source_key = ?", item.SourceKey).First(&breed).Error; err != nil {
return err
}
for _, source := range item.References {
source.BreedID = breed.ID
// Source snapshots are read-only in admin; update only when factual metadata changes.
if err := db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "breed_id"}, {Name: "provider"}}, DoUpdates: clause.AssignmentColumns([]string{"external_id", "url", "retrieved_at", "facts"}), Where: clause.Where{Exprs: []clause.Expression{clause.Expr{SQL: "sundynix_breed_reference.facts IS DISTINCT FROM EXCLUDED.facts OR sundynix_breed_reference.url IS DISTINCT FROM EXCLUDED.url OR sundynix_breed_reference.retrieved_at IS DISTINCT FROM EXCLUDED.retrieved_at"}}}}).Create(&source).Error; err != nil {
return err
}
}
if breed.EditorialLocked || breed.Name != item.Name {
continue
}
original := baseline[item.SourceKey]
updates := map[string]any{"english_name": original.EnglishName, "origin": original.Origin, "breed_group": original.BreedGroup, "fci_number": original.FCINumber, "height": original.Height, "weight": original.Weight, "summary": original.Summary, "temperament": original.Temperament, "traits": original.Traits, "care_notes": original.CareNotes, "life_span": "", "exercise": "", "grooming": ""}
for field, value := range item.Fields {
switch field {
case "english_name", "origin", "breed_group", "fci_number", "height", "weight", "life_span", "exercise", "grooming", "temperament", "traits", "summary", "care_notes":
updates[field] = value
default:
return fmt.Errorf("unsupported breed field: %s", field)
}
}
origins, err := json.Marshal(item.FieldSources)
if err != nil {
return err
}
updates["field_sources"] = string(origins)
latest := original.EncyclopediaUpdatedAt
for _, ref := range item.References {
if ref.RetrievedAt > latest {
latest = ref.RetrievedAt
}
}
updates["encyclopedia_updated_at"] = latest
// Skip unchanged columns so ordinary starts do not rewrite every breed.
current := map[string]string{"english_name": breed.EnglishName, "origin": breed.Origin, "breed_group": breed.BreedGroup, "fci_number": breed.FCINumber, "height": breed.Height, "weight": breed.Weight, "life_span": breed.LifeSpan, "exercise": breed.Exercise, "grooming": breed.Grooming, "temperament": breed.Temperament, "traits": breed.Traits, "summary": breed.Summary, "care_notes": breed.CareNotes, "encyclopedia_updated_at": breed.EncyclopediaUpdatedAt}
for key, value := range updates {
if key != "field_sources" && current[key] == value {
delete(updates, key)
}
}
if reflect.DeepEqual(breed.FieldSources, item.FieldSources) {
delete(updates, "field_sources")
}
if len(updates) == 0 {
continue
}
// UpdateColumns avoids advancing UpdatedAt on each application start.
if err := db.Model(&breed).Where("editorial_locked = false AND name = ?", item.Name).UpdateColumns(updates).Error; err != nil {
return err
}
}
return nil
}