282 lines
9.7 KiB
Go
282 lines
9.7 KiB
Go
package seeder
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||
|
||
"github.com/tidwall/gjson"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// SeedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。
|
||
func SeedPigNutrientRequirements(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
|
||
logger := logs.GetLogger(ctx)
|
||
|
||
// 检查 PigBreed 表是否为空,如果非空则跳过播种
|
||
isEmpty, err := isTableEmpty(tx, &models.PigBreed{})
|
||
if err != nil {
|
||
return fmt.Errorf("检查 PigBreed 表是否为空失败: %w", err)
|
||
}
|
||
if !isEmpty {
|
||
logger.Info("已存在猪种数据, 跳过数据播种")
|
||
return nil
|
||
}
|
||
|
||
// 1. 严格校验JSON文件,检查内部重复键
|
||
if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil {
|
||
return fmt.Errorf("JSON源文件校验失败: %w", err)
|
||
}
|
||
|
||
// 2. 解析简介信息
|
||
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
||
pigBreedDescriptions := make(map[string]models.PigBreed)
|
||
pigAgeStageDescriptions := make(map[string]models.PigAgeStage)
|
||
pigTypeDescriptions := make(map[string]map[string]models.PigType)
|
||
|
||
if descriptionsNode.Exists() {
|
||
// 解析 pig_breeds 描述
|
||
descriptionsNode.Get("pig_breeds").ForEach(func(key, value gjson.Result) bool {
|
||
var pb models.PigBreed
|
||
pb.Name = key.String()
|
||
pb.Description = value.Get("description").String()
|
||
pb.ParentInfo = value.Get("parent_info").String()
|
||
pb.AppearanceFeatures = value.Get("appearance_features").String()
|
||
pb.BreedAdvantages = value.Get("breed_advantages").String()
|
||
pb.BreedDisadvantages = value.Get("breed_disadvantages").String()
|
||
pigBreedDescriptions[key.String()] = pb
|
||
return true
|
||
})
|
||
|
||
// 解析 pig_age_stages 描述
|
||
descriptionsNode.Get("pig_age_stages").ForEach(func(key, value gjson.Result) bool {
|
||
var pas models.PigAgeStage
|
||
pas.Name = key.String()
|
||
pas.Description = value.String()
|
||
pigAgeStageDescriptions[key.String()] = pas
|
||
return true
|
||
})
|
||
|
||
// 解析 pig_breed_age_stages (PigType) 描述
|
||
descriptionsNode.Get("pig_breed_age_stages").ForEach(func(breedKey, breedValue gjson.Result) bool {
|
||
if _, ok := pigTypeDescriptions[breedKey.String()]; !ok {
|
||
pigTypeDescriptions[breedKey.String()] = make(map[string]models.PigType)
|
||
}
|
||
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
|
||
var pt models.PigType
|
||
pt.Description = ageStageValue.Get("description").String()
|
||
pt.DailyFeedIntake = float32(ageStageValue.Get("daily_feed_intake").Float())
|
||
pt.DailyGainWeight = float32(ageStageValue.Get("daily_gain_weight").Float())
|
||
pt.MinDays = uint32(ageStageValue.Get("min_days").Uint())
|
||
pt.MaxDays = uint32(ageStageValue.Get("max_days").Uint())
|
||
pt.MinWeight = float32(ageStageValue.Get("min_weight").Float())
|
||
pt.MaxWeight = float32(ageStageValue.Get("max_weight").Float())
|
||
pigTypeDescriptions[breedKey.String()][ageStageKey.String()] = pt
|
||
return true
|
||
})
|
||
return true
|
||
})
|
||
}
|
||
|
||
// 3. 将通过校验的、干净的数据写入数据库
|
||
dataNode := gjson.GetBytes(jsonData, "data")
|
||
dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool {
|
||
breedName := breedKey.String()
|
||
var pigBreed models.PigBreed
|
||
// 查找或创建 PigBreed
|
||
pbDesc := pigBreedDescriptions[breedName]
|
||
err = tx.Where(models.PigBreed{Name: breedName}).
|
||
FirstOrCreate(&pigBreed, models.PigBreed{
|
||
Name: breedName,
|
||
Description: pbDesc.Description,
|
||
ParentInfo: pbDesc.ParentInfo,
|
||
AppearanceFeatures: pbDesc.AppearanceFeatures,
|
||
BreedAdvantages: pbDesc.BreedAdvantages,
|
||
BreedDisadvantages: pbDesc.BreedDisadvantages,
|
||
}).Error
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
|
||
ageStageName := ageStageKey.String()
|
||
var pigAgeStage models.PigAgeStage
|
||
// 查找或创建 PigAgeStage
|
||
pasDesc := pigAgeStageDescriptions[ageStageName]
|
||
err = tx.Where(models.PigAgeStage{Name: ageStageName}).
|
||
FirstOrCreate(&pigAgeStage, models.PigAgeStage{
|
||
Name: ageStageName,
|
||
Description: pasDesc.Description,
|
||
}).Error
|
||
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
var pigType models.PigType
|
||
// 查找或创建 PigType
|
||
ptDesc := pigTypeDescriptions[breedName][ageStageName]
|
||
err = tx.Where(models.PigType{BreedID: pigBreed.ID, AgeStageID: pigAgeStage.ID}).
|
||
FirstOrCreate(&pigType, models.PigType{
|
||
BreedID: pigBreed.ID,
|
||
AgeStageID: pigAgeStage.ID,
|
||
Description: ptDesc.Description,
|
||
DailyFeedIntake: ptDesc.DailyFeedIntake,
|
||
DailyGainWeight: ptDesc.DailyGainWeight,
|
||
MinDays: ptDesc.MinDays,
|
||
MaxDays: ptDesc.MaxDays,
|
||
MinWeight: ptDesc.MinWeight,
|
||
MaxWeight: ptDesc.MaxWeight,
|
||
}).Error
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
ageStageValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
|
||
nutrientName := nutrientKey.String()
|
||
minReq := float32(nutrientValue.Get("min_requirement").Float())
|
||
maxReq := float32(nutrientValue.Get("max_requirement").Float())
|
||
|
||
var nutrient models.Nutrient
|
||
// 查找或创建 Nutrient (这里假设 Nutrient 已经在 SeedNutrients 中处理,但为了健壮性,再次 FirstOrCreate)
|
||
err = tx.Where(models.Nutrient{Name: nutrientName}).
|
||
FirstOrCreate(&nutrient, models.Nutrient{
|
||
Name: nutrientName,
|
||
// Description 字段在 nutrient seeder 中处理,这里不设置
|
||
}).Error
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
linkData := models.PigNutrientRequirement{
|
||
PigTypeID: pigType.ID,
|
||
NutrientID: nutrient.ID,
|
||
MinRequirement: minReq,
|
||
MaxRequirement: maxReq,
|
||
}
|
||
// 使用 FirstOrCreate 确保关联的唯一性
|
||
err = tx.Where(models.PigNutrientRequirement{
|
||
PigTypeID: pigType.ID,
|
||
NutrientID: nutrient.ID,
|
||
}).FirstOrCreate(&linkData, linkData).Error
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
return err == nil // 如果内部遍历有错误,则停止外部遍历
|
||
})
|
||
return err == nil // 如果内部遍历有错误,则停止外部遍历
|
||
})
|
||
return err // 返回捕获到的错误
|
||
}
|
||
|
||
// validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件
|
||
func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error {
|
||
dataNode := gjson.GetBytes(jsonData, "data")
|
||
if !dataNode.Exists() {
|
||
return errors.New("JSON文件中缺少 'data' 字段")
|
||
}
|
||
if !dataNode.IsObject() {
|
||
return errors.New("'data' 字段必须是一个JSON对象")
|
||
}
|
||
|
||
decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
|
||
decoder.UseNumber()
|
||
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||
return fmt.Errorf("'data' 字段解析起始符失败: %v", err)
|
||
}
|
||
|
||
seenBreeds := make(map[string]bool)
|
||
|
||
for decoder.More() {
|
||
// 解析 PigBreed 名称
|
||
t, err := decoder.Token()
|
||
if err != nil {
|
||
return fmt.Errorf("解析猪品种名称失败: %w", err)
|
||
}
|
||
breedName := t.(string)
|
||
if seenBreeds[breedName] {
|
||
return fmt.Errorf("猪品种名称 '%s' 重复", breedName)
|
||
}
|
||
seenBreeds[breedName] = true
|
||
|
||
// 解析该品种的年龄阶段对象
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||
return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName)
|
||
}
|
||
|
||
seenAgeStages := make(map[string]bool)
|
||
|
||
for decoder.More() {
|
||
// 解析 PigAgeStage 名称
|
||
t, err := decoder.Token()
|
||
if err != nil {
|
||
return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err)
|
||
}
|
||
ageStageName := t.(string)
|
||
if seenAgeStages[ageStageName] {
|
||
return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName)
|
||
}
|
||
seenAgeStages[ageStageName] = true
|
||
|
||
// 解析该年龄阶段的营养成分对象
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||
return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName)
|
||
}
|
||
|
||
seenNutrients := make(map[string]bool)
|
||
|
||
for decoder.More() {
|
||
// 解析 Nutrient 名称
|
||
t, err := decoder.Token()
|
||
if err != nil {
|
||
return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err)
|
||
}
|
||
nutrientName := t.(string)
|
||
if seenNutrients[nutrientName] {
|
||
return fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName)
|
||
}
|
||
seenNutrients[nutrientName] = true
|
||
|
||
// 解析 min_requirement 和 max_requirement 对象
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||
return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName)
|
||
}
|
||
|
||
for decoder.More() {
|
||
t, err := decoder.Token()
|
||
if err != nil {
|
||
return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err)
|
||
}
|
||
// key := t.(string) // 校验时不需要使用 key 的值
|
||
|
||
t, err = decoder.Token()
|
||
if err != nil {
|
||
return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err)
|
||
}
|
||
if _, ok := t.(json.Number); !ok {
|
||
return fmt.Errorf("期望营养素 '%s' 的需求值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
|
||
}
|
||
}
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
|
||
return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName)
|
||
}
|
||
}
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
|
||
return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName)
|
||
}
|
||
}
|
||
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
|
||
return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName)
|
||
}
|
||
}
|
||
return nil
|
||
}
|