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