重构seeder
This commit is contained in:
@@ -1,17 +1,14 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database/seeder"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -95,9 +92,9 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
|
|||||||
var seederFunc SeederFunc
|
var seederFunc SeederFunc
|
||||||
switch dataTypeStr {
|
switch dataTypeStr {
|
||||||
case "nutrient":
|
case "nutrient":
|
||||||
seederFunc = seedNutrients
|
seederFunc = seeder.SeedNutrients
|
||||||
case "pig_nutrient_requirements":
|
case "pig_nutrient_requirements":
|
||||||
seederFunc = seedPigNutrientRequirements
|
seederFunc = seeder.SeedPigNutrientRequirements
|
||||||
default:
|
default:
|
||||||
logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr)
|
logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr)
|
||||||
continue
|
continue
|
||||||
@@ -127,522 +124,3 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
|
|||||||
return nil // 提交事务
|
return nil // 提交事务
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。
|
|
||||||
type rawMaterialInfo struct {
|
|
||||||
Description string
|
|
||||||
UnitPrice float32
|
|
||||||
}
|
|
||||||
|
|
||||||
// seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。
|
|
||||||
func seedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
|
|
||||||
logger := logs.GetLogger(ctx)
|
|
||||||
|
|
||||||
// 检查 Nutrient 表是否为空,如果非空则跳过播种
|
|
||||||
isEmpty, err := isTableEmpty(tx, &models.Nutrient{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err)
|
|
||||||
}
|
|
||||||
if !isEmpty {
|
|
||||||
logger.Info("已存在原料数据, 跳过数据播种")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 严格校验JSON文件,检查内部重复键
|
|
||||||
if err := validateAndParseNutrientJSON(jsonData); err != nil {
|
|
||||||
return fmt.Errorf("JSON源文件校验失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 解析简介信息
|
|
||||||
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
|
||||||
rawMaterialInfos := make(map[string]rawMaterialInfo)
|
|
||||||
nutrientDescriptions := make(map[string]string)
|
|
||||||
|
|
||||||
if descriptionsNode.Exists() {
|
|
||||||
// 解析 raw_materials 描述和价格
|
|
||||||
descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool {
|
|
||||||
rawMaterialInfos[key.String()] = rawMaterialInfo{
|
|
||||||
Description: value.Get("descriptions").String(),
|
|
||||||
UnitPrice: float32(value.Get("unit_price").Float()),
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool {
|
|
||||||
nutrientDescriptions[key.String()] = value.String()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 将通过校验的、干净的数据写入数据库
|
|
||||||
dataNode := gjson.GetBytes(jsonData, "data")
|
|
||||||
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
|
|
||||||
rawMaterialName := rawMaterialKey.String()
|
|
||||||
var rawMaterial models.RawMaterial
|
|
||||||
|
|
||||||
// 获取原料的描述和价格信息
|
|
||||||
info := rawMaterialInfos[rawMaterialName]
|
|
||||||
|
|
||||||
// 将 Description 和 ReferencePrice 放入 Create 对象中
|
|
||||||
err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
|
|
||||||
FirstOrCreate(&rawMaterial, models.RawMaterial{
|
|
||||||
Name: rawMaterialName,
|
|
||||||
Description: info.Description,
|
|
||||||
ReferencePrice: info.UnitPrice,
|
|
||||||
}).Error
|
|
||||||
if err != nil {
|
|
||||||
// 返回 false 停止 ForEach 遍历
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
|
|
||||||
nutrientName := nutrientKey.String()
|
|
||||||
value := float32(nutrientValue.Float())
|
|
||||||
|
|
||||||
var nutrient models.Nutrient
|
|
||||||
// 将 Description 放入 Create 对象中
|
|
||||||
err = tx.Where(models.Nutrient{Name: nutrientName}).
|
|
||||||
FirstOrCreate(&nutrient, models.Nutrient{
|
|
||||||
Name: nutrientName,
|
|
||||||
Description: nutrientDescriptions[nutrientName],
|
|
||||||
}).Error
|
|
||||||
if err != nil {
|
|
||||||
// 返回 false 停止 ForEach 遍历
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
linkData := models.RawMaterialNutrient{
|
|
||||||
RawMaterialID: rawMaterial.ID,
|
|
||||||
NutrientID: nutrient.ID,
|
|
||||||
}
|
|
||||||
// 使用 FirstOrCreate 确保关联的唯一性
|
|
||||||
err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{
|
|
||||||
RawMaterialID: linkData.RawMaterialID,
|
|
||||||
NutrientID: linkData.NutrientID,
|
|
||||||
Value: value,
|
|
||||||
}).Error
|
|
||||||
if err != nil {
|
|
||||||
// 返回 false 停止 ForEach 遍历
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return err == nil // 如果内部遍历有错误,则停止外部遍历
|
|
||||||
})
|
|
||||||
|
|
||||||
return err // 返回捕获到的错误
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateAndParseNutrientJSON 严格校验JSON文件
|
|
||||||
func validateAndParseNutrientJSON(jsonData []byte) error {
|
|
||||||
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
|
||||||
if !descriptionsNode.Exists() {
|
|
||||||
return errors.New("JSON文件中缺少 'descriptions' 字段")
|
|
||||||
}
|
|
||||||
if !descriptionsNode.IsObject() {
|
|
||||||
return errors.New("'descriptions' 字段必须是一个JSON对象")
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMaterialsNode := descriptionsNode.Get("raw_materials")
|
|
||||||
if !rawMaterialsNode.Exists() {
|
|
||||||
return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段")
|
|
||||||
}
|
|
||||||
if !rawMaterialsNode.IsObject() {
|
|
||||||
return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 json.Decoder 严格校验 raw_materials 的结构
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw)))
|
|
||||||
decoder.UseNumber()
|
|
||||||
|
|
||||||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
|
||||||
return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
seenRawMaterials := make(map[string]bool)
|
|
||||||
|
|
||||||
for decoder.More() {
|
|
||||||
// 1. 解析原料名称
|
|
||||||
t, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析原料名称失败: %w", err)
|
|
||||||
}
|
|
||||||
rawMaterialName := t.(string)
|
|
||||||
if seenRawMaterials[rawMaterialName] {
|
|
||||||
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
|
|
||||||
}
|
|
||||||
seenRawMaterials[rawMaterialName] = true
|
|
||||||
|
|
||||||
// 2. 解析该原料的描述和价格对象
|
|
||||||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
|
||||||
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
|
|
||||||
}
|
|
||||||
|
|
||||||
for decoder.More() {
|
|
||||||
t, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err)
|
|
||||||
}
|
|
||||||
key := t.(string)
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case "descriptions":
|
|
||||||
t, err = decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err)
|
|
||||||
}
|
|
||||||
if _, ok := t.(string); !ok {
|
|
||||||
return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
|
|
||||||
}
|
|
||||||
case "unit_price":
|
|
||||||
t, err = decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err)
|
|
||||||
}
|
|
||||||
if _, ok := t.(json.Number); !ok {
|
|
||||||
return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// 忽略其他未知字段,但仍需读取其值以继续解析
|
|
||||||
if _, err := decoder.Token(); err != nil {
|
|
||||||
return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取原料描述和价格对象的 "}"
|
|
||||||
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
|
|
||||||
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验 data 节点
|
|
||||||
dataNode := gjson.GetBytes(jsonData, "data")
|
|
||||||
if !dataNode.Exists() {
|
|
||||||
return errors.New("JSON文件中缺少 'data' 字段")
|
|
||||||
}
|
|
||||||
if !dataNode.IsObject() {
|
|
||||||
return errors.New("'data' 字段必须是一个JSON对象")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新初始化 decoder 用于 data 节点的校验
|
|
||||||
decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
|
|
||||||
decoder.UseNumber()
|
|
||||||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
|
||||||
return errors.New("'data' 字段解析起始符失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验
|
|
||||||
|
|
||||||
for decoder.More() {
|
|
||||||
// 1. 解析原料名称
|
|
||||||
t, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析原料名称失败: %w", err)
|
|
||||||
}
|
|
||||||
rawMaterialName := t.(string)
|
|
||||||
if seenRawMaterials[rawMaterialName] {
|
|
||||||
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
|
|
||||||
}
|
|
||||||
seenRawMaterials[rawMaterialName] = true
|
|
||||||
|
|
||||||
// 2. 解析该原料的营养成分对象
|
|
||||||
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
|
||||||
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
|
|
||||||
}
|
|
||||||
|
|
||||||
seenNutrients := make(map[string]bool)
|
|
||||||
for decoder.More() {
|
|
||||||
// 解析营养素名称
|
|
||||||
t, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err)
|
|
||||||
}
|
|
||||||
nutrientName := t.(string)
|
|
||||||
if seenNutrients[nutrientName] {
|
|
||||||
return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName)
|
|
||||||
}
|
|
||||||
seenNutrients[nutrientName] = true
|
|
||||||
|
|
||||||
// 解析营养素含量
|
|
||||||
t, err = decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, 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' 的值结束符 '}' 失败", rawMaterialName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
268
internal/infra/database/seeder/nutrient_seeder.go
Normal file
268
internal/infra/database/seeder/nutrient_seeder.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。
|
||||||
|
type rawMaterialInfo struct {
|
||||||
|
Description string
|
||||||
|
UnitPrice float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。
|
||||||
|
func SeedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
|
||||||
|
logger := logs.GetLogger(ctx)
|
||||||
|
|
||||||
|
// 检查 Nutrient 表是否为空,如果非空则跳过播种
|
||||||
|
isEmpty, err := isTableEmpty(tx, &models.Nutrient{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err)
|
||||||
|
}
|
||||||
|
if !isEmpty {
|
||||||
|
logger.Info("已存在原料数据, 跳过数据播种")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 严格校验JSON文件,检查内部重复键
|
||||||
|
if err := validateAndParseNutrientJSON(jsonData); err != nil {
|
||||||
|
return fmt.Errorf("JSON源文件校验失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析简介信息
|
||||||
|
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
||||||
|
rawMaterialInfos := make(map[string]rawMaterialInfo)
|
||||||
|
nutrientDescriptions := make(map[string]string)
|
||||||
|
|
||||||
|
if descriptionsNode.Exists() {
|
||||||
|
// 解析 raw_materials 描述和价格
|
||||||
|
descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool {
|
||||||
|
rawMaterialInfos[key.String()] = rawMaterialInfo{
|
||||||
|
Description: value.Get("descriptions").String(),
|
||||||
|
UnitPrice: float32(value.Get("unit_price").Float()),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool {
|
||||||
|
nutrientDescriptions[key.String()] = value.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 将通过校验的、干净的数据写入数据库
|
||||||
|
dataNode := gjson.GetBytes(jsonData, "data")
|
||||||
|
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
|
||||||
|
rawMaterialName := rawMaterialKey.String()
|
||||||
|
var rawMaterial models.RawMaterial
|
||||||
|
|
||||||
|
// 获取原料的描述和价格信息
|
||||||
|
info := rawMaterialInfos[rawMaterialName]
|
||||||
|
|
||||||
|
// 将 Description 和 ReferencePrice 放入 Create 对象中
|
||||||
|
err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
|
||||||
|
FirstOrCreate(&rawMaterial, models.RawMaterial{
|
||||||
|
Name: rawMaterialName,
|
||||||
|
Description: info.Description,
|
||||||
|
ReferencePrice: info.UnitPrice,
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
// 返回 false 停止 ForEach 遍历
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
|
||||||
|
nutrientName := nutrientKey.String()
|
||||||
|
value := float32(nutrientValue.Float())
|
||||||
|
|
||||||
|
var nutrient models.Nutrient
|
||||||
|
// 将 Description 放入 Create 对象中
|
||||||
|
err = tx.Where(models.Nutrient{Name: nutrientName}).
|
||||||
|
FirstOrCreate(&nutrient, models.Nutrient{
|
||||||
|
Name: nutrientName,
|
||||||
|
Description: nutrientDescriptions[nutrientName],
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
// 返回 false 停止 ForEach 遍历
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
linkData := models.RawMaterialNutrient{
|
||||||
|
RawMaterialID: rawMaterial.ID,
|
||||||
|
NutrientID: nutrient.ID,
|
||||||
|
}
|
||||||
|
// 使用 FirstOrCreate 确保关联的唯一性
|
||||||
|
err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{
|
||||||
|
RawMaterialID: linkData.RawMaterialID,
|
||||||
|
NutrientID: linkData.NutrientID,
|
||||||
|
Value: value,
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
// 返回 false 停止 ForEach 遍历
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return err == nil // 如果内部遍历有错误,则停止外部遍历
|
||||||
|
})
|
||||||
|
|
||||||
|
return err // 返回捕获到的错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAndParseNutrientJSON 严格校验JSON文件
|
||||||
|
func validateAndParseNutrientJSON(jsonData []byte) error {
|
||||||
|
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
||||||
|
if !descriptionsNode.Exists() {
|
||||||
|
return errors.New("JSON文件中缺少 'descriptions' 字段")
|
||||||
|
}
|
||||||
|
if !descriptionsNode.IsObject() {
|
||||||
|
return errors.New("'descriptions' 字段必须是一个JSON对象")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMaterialsNode := descriptionsNode.Get("raw_materials")
|
||||||
|
if !rawMaterialsNode.Exists() {
|
||||||
|
return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段")
|
||||||
|
}
|
||||||
|
if !rawMaterialsNode.IsObject() {
|
||||||
|
return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 json.Decoder 严格校验 raw_materials 的结构
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw)))
|
||||||
|
decoder.UseNumber()
|
||||||
|
|
||||||
|
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||||||
|
return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seenRawMaterials := make(map[string]bool)
|
||||||
|
|
||||||
|
for decoder.More() {
|
||||||
|
// 1. 解析原料名称
|
||||||
|
t, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析原料名称失败: %w", err)
|
||||||
|
}
|
||||||
|
rawMaterialName := t.(string)
|
||||||
|
if seenRawMaterials[rawMaterialName] {
|
||||||
|
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
|
||||||
|
}
|
||||||
|
seenRawMaterials[rawMaterialName] = true
|
||||||
|
|
||||||
|
// 2. 解析该原料的描述和价格对象
|
||||||
|
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||||||
|
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for decoder.More() {
|
||||||
|
t, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err)
|
||||||
|
}
|
||||||
|
key := t.(string)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "descriptions":
|
||||||
|
t, err = decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err)
|
||||||
|
}
|
||||||
|
if _, ok := t.(string); !ok {
|
||||||
|
return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
|
||||||
|
}
|
||||||
|
case "unit_price":
|
||||||
|
t, err = decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err)
|
||||||
|
}
|
||||||
|
if _, ok := t.(json.Number); !ok {
|
||||||
|
return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// 忽略其他未知字段,但仍需读取其值以继续解析
|
||||||
|
if _, err := decoder.Token(); err != nil {
|
||||||
|
return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取原料描述和价格对象的 "}"
|
||||||
|
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
|
||||||
|
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 data 节点
|
||||||
|
dataNode := gjson.GetBytes(jsonData, "data")
|
||||||
|
if !dataNode.Exists() {
|
||||||
|
return errors.New("JSON文件中缺少 'data' 字段")
|
||||||
|
}
|
||||||
|
if !dataNode.IsObject() {
|
||||||
|
return errors.New("'data' 字段必须是一个JSON对象")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化 decoder 用于 data 节点的校验
|
||||||
|
decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
|
||||||
|
decoder.UseNumber()
|
||||||
|
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||||||
|
return errors.New("'data' 字段解析起始符失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验
|
||||||
|
|
||||||
|
for decoder.More() {
|
||||||
|
// 1. 解析原料名称
|
||||||
|
t, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析原料名称失败: %w", err)
|
||||||
|
}
|
||||||
|
rawMaterialName := t.(string)
|
||||||
|
if seenRawMaterials[rawMaterialName] {
|
||||||
|
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
|
||||||
|
}
|
||||||
|
seenRawMaterials[rawMaterialName] = true
|
||||||
|
|
||||||
|
// 2. 解析该原料的营养成分对象
|
||||||
|
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
|
||||||
|
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
|
||||||
|
}
|
||||||
|
|
||||||
|
seenNutrients := make(map[string]bool)
|
||||||
|
for decoder.More() {
|
||||||
|
// 解析营养素名称
|
||||||
|
t, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err)
|
||||||
|
}
|
||||||
|
nutrientName := t.(string)
|
||||||
|
if seenNutrients[nutrientName] {
|
||||||
|
return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName)
|
||||||
|
}
|
||||||
|
seenNutrients[nutrientName] = true
|
||||||
|
|
||||||
|
// 解析营养素含量
|
||||||
|
t, err = decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, 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' 的值结束符 '}' 失败", rawMaterialName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
18
internal/infra/database/seeder/utils.go
Normal file
18
internal/infra/database/seeder/utils.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package seeder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isTableEmpty 检查给定模型对应的数据库表是否为空。
|
||||||
|
// 注意:此函数需要从 database 包中移动过来,或者在 seeder 包中重新定义,
|
||||||
|
// 为了避免循环依赖,这里选择在 seeder 包中重新定义。
|
||||||
|
func isTableEmpty(tx *gorm.DB, model interface{}) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(model).Count(&count).Error; err != nil {
|
||||||
|
return false, fmt.Errorf("查询表记录数失败: %w", err)
|
||||||
|
}
|
||||||
|
return count == 0, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user