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 MaxRatio 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()), MaxRatio: float32(value.Get("max_ratio").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 和 MaxAdditionRatio 放入 Create 对象中 err = tx.Where(models.RawMaterial{Name: rawMaterialName}). FirstOrCreate(&rawMaterial, models.RawMaterial{ Name: rawMaterialName, Description: info.Description, ReferencePrice: info.UnitPrice, MaxAdditionRatio: info.MaxRatio, }).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) } case "max_ratio": t, err = decoder.Token() if err != nil { return fmt.Errorf("解析原料 '%s' 的 'max_ratio' 值失败: %w", rawMaterialName, err) } if _, ok := t.(json.Number); !ok { return fmt.Errorf("期望原料 '%s' 的 'max_ratio' 值是数字, 但实际得到的类型是 %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 }