seeder支持按顺序读取

This commit is contained in:
2025-11-21 18:36:02 +08:00
parent 7829ac9931
commit f81635f997

View File

@@ -122,8 +122,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
// seedNutrients 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。 // seedNutrients 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func seedNutrients(tx *gorm.DB, jsonData []byte) error { func seedNutrients(tx *gorm.DB, jsonData []byte) error {
// 1. 严格校验JSON文件检查内部重复键 // 1. 严格校验JSON文件检查内部重复键
parsedData, err := validateAndParseNutrientJSON(jsonData) if err := validateAndParseNutrientJSON(jsonData); err != nil {
if err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err) return fmt.Errorf("JSON源文件校验失败: %w", err)
} }
@@ -144,28 +143,36 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error {
} }
// 3. 将通过校验的、干净的数据写入数据库 // 3. 将通过校验的、干净的数据写入数据库
for rawMaterialName, nutrients := range parsedData { dataNode := gjson.GetBytes(jsonData, "data")
var err error // 用于捕获 ForEach 内部的错误
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
rawMaterialName := rawMaterialKey.String()
var rawMaterial models.RawMaterial var rawMaterial models.RawMaterial
// 将 Description 放入 Create 对象中 // 将 Description 放入 Create 对象中
err := tx.Where(models.RawMaterial{Name: rawMaterialName}). err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
FirstOrCreate(&rawMaterial, models.RawMaterial{ FirstOrCreate(&rawMaterial, models.RawMaterial{
Name: rawMaterialName, Name: rawMaterialName,
Description: rawMaterialDescriptions[rawMaterialName], Description: rawMaterialDescriptions[rawMaterialName],
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设原料 '%s' 失败: %w", rawMaterialName, err) // 返回 false 停止 ForEach 遍历
return false
} }
for nutrientName, value := range nutrients { rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
nutrientName := nutrientKey.String()
value := float32(nutrientValue.Float())
var nutrient models.Nutrient var nutrient models.Nutrient
// 将 Description 放入 Create 对象中 // 将 Description 放入 Create 对象中
err := tx.Where(models.Nutrient{Name: nutrientName}). err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{ FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName, Name: nutrientName,
Description: nutrientDescriptions[nutrientName], Description: nutrientDescriptions[nutrientName],
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) // 返回 false 停止 ForEach 遍历
return false
} }
linkData := models.RawMaterialNutrient{ linkData := models.RawMaterialNutrient{
@@ -173,23 +180,27 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error {
NutrientID: nutrient.ID, NutrientID: nutrient.ID,
} }
// 使用 FirstOrCreate 确保关联的唯一性 // 使用 FirstOrCreate 确保关联的唯一性
if err := tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{
RawMaterialID: linkData.RawMaterialID, RawMaterialID: linkData.RawMaterialID,
NutrientID: linkData.NutrientID, NutrientID: linkData.NutrientID,
Value: value, Value: value,
}).Error; err != nil { }).Error
return fmt.Errorf("为原料 '%s' 和营养素 '%s' 创建关联失败: %w", rawMaterialName, nutrientName, err) if err != nil {
// 返回 false 停止 ForEach 遍历
return false
} }
} return true
} })
return nil return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
} }
// seedPigNutrientRequirements 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。 // seedPigNutrientRequirements 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error {
// 1. 严格校验JSON文件检查内部重复键 // 1. 严格校验JSON文件检查内部重复键
parsedData, err := validateAndParsePigNutrientRequirementJSON(jsonData) if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil {
if err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err) return fmt.Errorf("JSON源文件校验失败: %w", err)
} }
@@ -244,11 +255,14 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error {
} }
// 3. 将通过校验的、干净的数据写入数据库 // 3. 将通过校验的、干净的数据写入数据库
for breedName, ageStagesData := range parsedData { dataNode := gjson.GetBytes(jsonData, "data")
var err error // 用于捕获 ForEach 内部的错误
dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool {
breedName := breedKey.String()
var pigBreed models.PigBreed var pigBreed models.PigBreed
// 查找或创建 PigBreed // 查找或创建 PigBreed
pbDesc := pigBreedDescriptions[breedName] pbDesc := pigBreedDescriptions[breedName]
err := tx.Where(models.PigBreed{Name: breedName}). err = tx.Where(models.PigBreed{Name: breedName}).
FirstOrCreate(&pigBreed, models.PigBreed{ FirstOrCreate(&pigBreed, models.PigBreed{
Name: breedName, Name: breedName,
Description: pbDesc.Description, Description: pbDesc.Description,
@@ -258,20 +272,22 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error {
BreedDisadvantages: pbDesc.BreedDisadvantages, BreedDisadvantages: pbDesc.BreedDisadvantages,
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设猪品种 '%s' 失败: %w", breedName, err) return false
} }
for ageStageName, nutrientsData := range ageStagesData { breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
ageStageName := ageStageKey.String()
var pigAgeStage models.PigAgeStage var pigAgeStage models.PigAgeStage
// 查找或创建 PigAgeStage // 查找或创建 PigAgeStage
pasDesc := pigAgeStageDescriptions[ageStageName] pasDesc := pigAgeStageDescriptions[ageStageName]
err := tx.Where(models.PigAgeStage{Name: ageStageName}). err = tx.Where(models.PigAgeStage{Name: ageStageName}).
FirstOrCreate(&pigAgeStage, models.PigAgeStage{ FirstOrCreate(&pigAgeStage, models.PigAgeStage{
Name: ageStageName, Name: ageStageName,
Description: pasDesc.Description, Description: pasDesc.Description,
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设猪年龄阶段 '%s' 失败: %w", ageStageName, err) return false
} }
var pigType models.PigType var pigType models.PigType
@@ -290,243 +306,215 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error {
MaxWeight: ptDesc.MaxWeight, MaxWeight: ptDesc.MaxWeight,
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设猪类型 '%s' - '%s' 失败: %w", breedName, ageStageName, err) return false
} }
for nutrientName, requirement := range nutrientsData { 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 var nutrient models.Nutrient
// 查找或创建 Nutrient (这里假设 Nutrient 已经在 seedNutrients 中处理,但为了健壮性,再次 FirstOrCreate) // 查找或创建 Nutrient (这里假设 Nutrient 已经在 seedNutrients 中处理,但为了健壮性,再次 FirstOrCreate)
err := tx.Where(models.Nutrient{Name: nutrientName}). err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{ FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName, Name: nutrientName,
// Description 字段在 nutrient seeder 中处理,这里不设置 // Description 字段在 nutrient seeder 中处理,这里不设置
}).Error }).Error
if err != nil { if err != nil {
return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) return false
} }
linkData := models.PigNutrientRequirement{ linkData := models.PigNutrientRequirement{
PigTypeID: pigType.ID, PigTypeID: pigType.ID,
NutrientID: nutrient.ID, NutrientID: nutrient.ID,
MinRequirement: requirement.MinRequirement, MinRequirement: minReq,
MaxRequirement: requirement.MaxRequirement, MaxRequirement: maxReq,
} }
// 使用 FirstOrCreate 确保关联的唯一性 // 使用 FirstOrCreate 确保关联的唯一性
if err := tx.Where(models.PigNutrientRequirement{ err = tx.Where(models.PigNutrientRequirement{
PigTypeID: pigType.ID, PigTypeID: pigType.ID,
NutrientID: nutrient.ID, NutrientID: nutrient.ID,
}).FirstOrCreate(&linkData, linkData).Error; err != nil { }).FirstOrCreate(&linkData, linkData).Error
return fmt.Errorf("为猪类型 '%s' - '%s' 和营养素 '%s' 创建营养需求失败: %w", breedName, ageStageName, nutrientName, err) if err != nil {
return false
} }
} return true
} })
} return err == nil // 如果内部遍历有错误,则停止外部遍历
return nil })
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
} }
// validateAndParsePigNutrientRequirementJSON 严格校验并解析猪营养需求JSON文件 // validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件
func validateAndParsePigNutrientRequirementJSON(jsonData []byte) (map[string]map[string]map[string]struct { func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error {
MinRequirement float32
MaxRequirement float32
}, error) {
dataNode := gjson.GetBytes(jsonData, "data") dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() { if !dataNode.Exists() {
return nil, errors.New("JSON文件中缺少 'data' 字段") return errors.New("JSON文件中缺少 'data' 字段")
} }
if !dataNode.IsObject() { if !dataNode.IsObject() {
return nil, errors.New("'data' 字段必须是一个JSON对象") return errors.New("'data' 字段必须是一个JSON对象")
} }
decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber() decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, fmt.Errorf("'data' 字段解析起始符失败: %v", err) return fmt.Errorf("'data' 字段解析起始符失败: %v", err)
} }
result := make(map[string]map[string]map[string]struct {
MinRequirement float32
MaxRequirement float32
})
seenBreeds := make(map[string]bool) seenBreeds := make(map[string]bool)
for decoder.More() { for decoder.More() {
// 解析 PigBreed 名称 // 解析 PigBreed 名称
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("解析猪品种名称失败: %w", err) return fmt.Errorf("解析猪品种名称失败: %w", err)
} }
breedName := t.(string) breedName := t.(string)
if seenBreeds[breedName] { if seenBreeds[breedName] {
return nil, fmt.Errorf("猪品种名称 '%s' 重复", breedName) return fmt.Errorf("猪品种名称 '%s' 重复", breedName)
} }
seenBreeds[breedName] = true seenBreeds[breedName] = true
// 解析该品种的年龄阶段对象 // 解析该品种的年龄阶段对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName) return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName)
} }
ageStages := make(map[string]map[string]struct {
MinRequirement float32
MaxRequirement float32
})
seenAgeStages := make(map[string]bool) seenAgeStages := make(map[string]bool)
for decoder.More() { for decoder.More() {
// 解析 PigAgeStage 名称 // 解析 PigAgeStage 名称
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err) return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err)
} }
ageStageName := t.(string) ageStageName := t.(string)
if seenAgeStages[ageStageName] { if seenAgeStages[ageStageName] {
return nil, fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName) return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName)
} }
seenAgeStages[ageStageName] = true seenAgeStages[ageStageName] = true
// 解析该年龄阶段的营养成分对象 // 解析该年龄阶段的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName) return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName)
} }
nutrients := make(map[string]struct {
MinRequirement float32
MaxRequirement float32
})
seenNutrients := make(map[string]bool) seenNutrients := make(map[string]bool)
for decoder.More() { for decoder.More() {
// 解析 Nutrient 名称 // 解析 Nutrient 名称
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err) return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err)
} }
nutrientName := t.(string) nutrientName := t.(string)
if seenNutrients[nutrientName] { if seenNutrients[nutrientName] {
return nil, fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName) return fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName)
} }
seenNutrients[nutrientName] = true seenNutrients[nutrientName] = true
// 解析 min_requirement 和 max_requirement 对象 // 解析 min_requirement 和 max_requirement 对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName) return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName)
} }
var req struct {
MinRequirement float32
MaxRequirement float32
}
for decoder.More() { for decoder.More() {
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err) return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err)
} }
key := t.(string) // key := t.(string) // 校验时不需要使用 key 的值
t, err = decoder.Token() t, err = decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err) return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err)
} }
if value, ok := t.(json.Number); ok { if _, ok := t.(json.Number); !ok {
f64, _ := value.Float64() return fmt.Errorf("期望营养素 '%s' 的需求值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
if key == "min_requirement" {
req.MinRequirement = float32(f64)
} else if key == "max_requirement" {
req.MaxRequirement = float32(f64)
} else {
return nil, fmt.Errorf("营养素 '%s' 中存在未知键 '%s'", nutrientName, key)
}
} else {
return nil, fmt.Errorf("期望营养素 '%s' 的 '%s' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, key, t, t)
} }
} }
if t, err := decoder.Token(); err != nil || t != json.Delim('}') { if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return nil, fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName)
} }
nutrients[nutrientName] = req
} }
if t, err := decoder.Token(); err != nil || t != json.Delim('}') { if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return nil, fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName)
} }
ageStages[ageStageName] = nutrients
} }
if t, err := decoder.Token(); err != nil || t != json.Delim('}') { if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return nil, fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName)
} }
result[breedName] = ageStages
} }
return result, nil return nil
} }
// validateAndParseNutrientJSON 使用 json.Decoder 手动解析,以捕获重复的键。 // validateAndParseNutrientJSON 严格校验JSON文件
func validateAndParseNutrientJSON(jsonData []byte) (map[string]map[string]float32, error) { func validateAndParseNutrientJSON(jsonData []byte) error {
dataNode := gjson.GetBytes(jsonData, "data") dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() { if !dataNode.Exists() {
return nil, errors.New("JSON文件中缺少 'data' 字段") return errors.New("JSON文件中缺少 'data' 字段")
} }
if !dataNode.IsObject() { if !dataNode.IsObject() {
return nil, errors.New("'data' 字段必须是一个JSON对象") return errors.New("'data' 字段必须是一个JSON对象")
} }
decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber() decoder.UseNumber()
// 读取 "{" // 读取 "{"
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, errors.New("'data' 字段解析起始符失败") return errors.New("'data' 字段解析起始符失败")
} }
result := make(map[string]map[string]float32)
seenRawMaterials := make(map[string]bool) seenRawMaterials := make(map[string]bool)
for decoder.More() { for decoder.More() {
// 1. 解析原料名称 // 1. 解析原料名称
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("解析原料名称失败: %w", err) return fmt.Errorf("解析原料名称失败: %w", err)
} }
rawMaterialName := t.(string) rawMaterialName := t.(string)
if seenRawMaterials[rawMaterialName] { if seenRawMaterials[rawMaterialName] {
return nil, fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
} }
seenRawMaterials[rawMaterialName] = true seenRawMaterials[rawMaterialName] = true
// 2. 解析该原料的营养成分对象 // 2. 解析该原料的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') { if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return nil, fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
} }
nutrients := make(map[string]float32)
seenNutrients := make(map[string]bool) seenNutrients := make(map[string]bool)
for decoder.More() { for decoder.More() {
// 解析营养素名称 // 解析营养素名称
t, err := decoder.Token() t, err := decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err)
} }
nutrientName := t.(string) nutrientName := t.(string)
if seenNutrients[nutrientName] { if seenNutrients[nutrientName] {
return nil, fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName)
} }
seenNutrients[nutrientName] = true seenNutrients[nutrientName] = true
// 解析营养素含量 // 解析营养素含量
t, err = decoder.Token() t, err = decoder.Token()
if err != nil { if err != nil {
return nil, fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err)
} }
if value, ok := t.(json.Number); ok { if _, ok := t.(json.Number); !ok {
f64, _ := value.Float64() return fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
nutrients[nutrientName] = float32(f64)
} else {
return nil, fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
} }
} }
// 读取营养成分对象的 "}" // 读取营养成分对象的 "}"
if t, err := decoder.Token(); err != nil || t != json.Delim('}') { if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return nil, fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
} }
result[rawMaterialName] = nutrients
} }
return result, nil return nil
} }