package database import ( "bytes" "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "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" ) // SeederFunc 定义了处理一种特定类型预设数据文件的函数签名。 type SeederFunc func(tx *gorm.DB, jsonData []byte) error // SeedFromPreset 是一个通用的数据播种函数。 // 它会读取指定目录下的所有 .json 文件,并根据文件内容中的 "type" 字段进行分发。 // 同时,它会校验所有必需的预设类型是否都已成功加载。 func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { logger := logs.TraceLogger(ctx, ctx, "SeedFromPreset") // 定义必须存在的预设数据类型及其处理顺序 // 确保 "nutrient" 在 "pig_nutrient_requirements" 之前处理,因为后者依赖于前者。 processingOrder := []string{"nutrient", "pig_nutrient_requirements"} requiredTypes := make(map[string]bool) for _, t := range processingOrder { requiredTypes[t] = true } processedTypes := make(map[string]bool) typeToFileMap := make(map[string]string) // 用于检测重复的 type,并存储每个 type 对应的文件路径 groupedFiles := make(map[string][][]byte) // 按 type 分组存储 jsonData files, err := os.ReadDir(presetDir) if err != nil { return fmt.Errorf("读取预设数据目录 '%s' 失败: %w", presetDir, err) } // 第一阶段:读取所有文件并按 type 分组 for _, file := range files { if filepath.Ext(file.Name()) != ".json" { continue } filePath := filepath.Join(presetDir, file.Name()) jsonData, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("读取文件 '%s' 失败: %w", filePath, err) } dataType := gjson.GetBytes(jsonData, "type") if !dataType.Exists() { logger.Warnf("警告: 文件 '%s' 中缺少 'type' 字段,已跳过", filePath) continue } dataTypeStr := dataType.String() // 检查是否存在重复的 type if existingFile, found := typeToFileMap[dataTypeStr]; found { return fmt.Errorf("预设数据校验失败: type '%s' 在文件 '%s' 和 '%s' 中重复定义", dataTypeStr, existingFile, filePath) } typeToFileMap[dataTypeStr] = filePath // 记录该 type 对应的文件路径 groupedFiles[dataTypeStr] = append(groupedFiles[dataTypeStr], jsonData) } // 第二阶段:按照预定义顺序处理分组后的数据 return db.Transaction(func(tx *gorm.DB) error { for _, dataTypeStr := range processingOrder { jsonDatas, ok := groupedFiles[dataTypeStr] if !ok { // 如果是必需类型但没有找到文件,则报错 if requiredTypes[dataTypeStr] { return fmt.Errorf("预设数据校验失败: 缺少必需的预设文件类型: '%s'", dataTypeStr) } continue // 非必需类型,跳过 } var seederFunc SeederFunc switch dataTypeStr { case "nutrient": seederFunc = seedNutrients case "pig_nutrient_requirements": seederFunc = seedPigNutrientRequirements default: logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr) continue } for _, jsonData := range jsonDatas { // 获取原始文件路径用于错误报告 originalFilePath := typeToFileMap[dataTypeStr] if err := seederFunc(tx, jsonData); err != nil { return fmt.Errorf("处理文件 (type: %s, path: %s) 时发生错误: %w", dataTypeStr, originalFilePath, err) } } processedTypes[dataTypeStr] = true } // 校验所有必需的类型是否都已处理 var missingTypes []string for reqType := range requiredTypes { if !processedTypes[reqType] { missingTypes = append(missingTypes, reqType) } } if len(missingTypes) > 0 { return fmt.Errorf("预设数据校验失败: 缺少必需的预设文件类型: [%s]", strings.Join(missingTypes, ", ")) } return nil // 提交事务 }) } // seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 func seedNutrients(tx *gorm.DB, jsonData []byte) error { // 1. 严格校验JSON文件,检查内部重复键 parsedData, err := validateAndParseNutrientJSON(jsonData) if err != nil { return fmt.Errorf("JSON源文件校验失败: %w", err) } // 2. 解析简介信息 descriptionsNode := gjson.GetBytes(jsonData, "descriptions") rawMaterialDescriptions := make(map[string]string) nutrientDescriptions := make(map[string]string) if descriptionsNode.Exists() { descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool { rawMaterialDescriptions[key.String()] = value.String() return true }) descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool { nutrientDescriptions[key.String()] = value.String() return true }) } // 3. 将通过校验的、干净的数据写入数据库 for rawMaterialName, nutrients := range parsedData { var rawMaterial models.RawMaterial // 将 Description 放入 Create 对象中 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) } for nutrientName, value := range nutrients { 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 { return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) } linkData := models.RawMaterialNutrient{ RawMaterialID: rawMaterial.ID, NutrientID: nutrient.ID, } // 使用 FirstOrCreate 确保关联的唯一性 if 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) } } } return nil } // seedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { // 1. 严格校验JSON文件,检查内部重复键 parsedData, err := validateAndParsePigNutrientRequirementJSON(jsonData) if 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. 将通过校验的、干净的数据写入数据库 for breedName, ageStagesData := range parsedData { 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 fmt.Errorf("预设猪品种 '%s' 失败: %w", breedName, err) } for ageStageName, nutrientsData := range ageStagesData { 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 fmt.Errorf("预设猪年龄阶段 '%s' 失败: %w", ageStageName, err) } 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 fmt.Errorf("预设猪类型 '%s' - '%s' 失败: %w", breedName, ageStageName, err) } for nutrientName, requirement := range nutrientsData { 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 fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) } linkData := models.PigNutrientRequirement{ PigTypeID: pigType.ID, NutrientID: nutrient.ID, MinRequirement: requirement.MinRequirement, MaxRequirement: requirement.MaxRequirement, } // 使用 FirstOrCreate 确保关联的唯一性 if 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) } } } } return nil } // validateAndParsePigNutrientRequirementJSON 严格校验并解析猪营养需求JSON文件 func validateAndParsePigNutrientRequirementJSON(jsonData []byte) (map[string]map[string]map[string]struct { MinRequirement float32 MaxRequirement float32 }, error) { dataNode := gjson.GetBytes(jsonData, "data") if !dataNode.Exists() { return nil, errors.New("JSON文件中缺少 'data' 字段") } if !dataNode.IsObject() { return nil, 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) } 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) } breedName := t.(string) if seenBreeds[breedName] { return nil, fmt.Errorf("猪品种名称 '%s' 重复", breedName) } seenBreeds[breedName] = true // 解析该品种的年龄阶段对象 if t, err := decoder.Token(); err != nil || t != json.Delim('{') { return nil, 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) } ageStageName := t.(string) if seenAgeStages[ageStageName] { return nil, 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) } 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) } nutrientName := t.(string) if seenNutrients[nutrientName] { return nil, 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) } 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) } key := t.(string) t, err = decoder.Token() if err != nil { return nil, 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 t, err := decoder.Token(); err != nil || t != json.Delim('}') { return nil, fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) } nutrients[nutrientName] = req } if t, err := decoder.Token(); err != nil || t != json.Delim('}') { return nil, fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) } ageStages[ageStageName] = nutrients } if t, err := decoder.Token(); err != nil || t != json.Delim('}') { return nil, fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) } result[breedName] = ageStages } return result, nil } // validateAndParseNutrientJSON 使用 json.Decoder 手动解析,以捕获重复的键。 func validateAndParseNutrientJSON(jsonData []byte) (map[string]map[string]float32, error) { dataNode := gjson.GetBytes(jsonData, "data") if !dataNode.Exists() { return nil, errors.New("JSON文件中缺少 'data' 字段") } if !dataNode.IsObject() { return nil, 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' 字段解析起始符失败") } 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) } rawMaterialName := t.(string) if seenRawMaterials[rawMaterialName] { return nil, 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) } 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) } nutrientName := t.(string) if seenNutrients[nutrientName] { return nil, 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) } 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 t, err := decoder.Token(); err != nil || t != json.Delim('}') { return nil, fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) } result[rawMaterialName] = nutrients } return result, nil }