重构seeder
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user