From b40eb350163ff9ca8bc1c3a70dd0afc53f24bf48 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 20:52:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9=E7=8C=AA?= =?UTF-8?q?=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- internal/infra/database/postgres.go | 14 ++ internal/infra/models/models.go | 2 + internal/infra/models/recipe.go | 29 +++ .../infra/repository/recipe_repository.go | 182 ++++++++++++++++++ project_structure.txt | 2 + 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 internal/infra/models/recipe.go create mode 100644 internal/infra/repository/recipe_repository.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 1886e75..c394a5b 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -57,4 +57,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 7. 实现配方领域关于猪模型和营养需求的增删改查 8. 实现配方领域的web接口 9. 实现修改原料营养信息 -10. 实现修改猪营养需求 \ No newline at end of file +10. 实现修改猪营养需求 +11. 配方模型定义和仓库层增删改查方法 \ No newline at end of file diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 4d2647f..fa23d75 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -376,6 +376,20 @@ func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { whereClause: "WHERE deleted_at IS NULL", description: "nutrients 表的部分唯一索引 (name 唯一)", }, + { + tableName: models.Recipe{}.TableName(), + columns: []string{"name"}, + indexName: "idx_recipes_unique_name_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "recipes 表的部分唯一索引 (name 唯一)", + }, + { + tableName: models.RecipeIngredient{}.TableName(), + columns: []string{"recipe_id", "raw_material_id"}, + indexName: "idx_recipe_ingredients_unique_recipe_raw_material_when_not_deleted", + whereClause: "WHERE deleted_at IS NULL", + description: "recipe_ingredients 表的部分唯一索引 (recipe_id, raw_material_id 组合唯一)", + }, } for _, indexDef := range uniqueIndexesToCreate { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 47324ed..3caeb56 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -70,6 +70,8 @@ func GetAllModels() []interface{} { &Nutrient{}, &RawMaterialNutrient{}, &RawMaterialStockLog{}, + &Recipe{}, + &RecipeIngredient{}, // Medication Models &Medication{}, diff --git a/internal/infra/models/recipe.go b/internal/infra/models/recipe.go new file mode 100644 index 0000000..a5278b0 --- /dev/null +++ b/internal/infra/models/recipe.go @@ -0,0 +1,29 @@ +package models + +// Recipe 配方模型 +type Recipe struct { + Model + Name string `gorm:"size:100;not null;comment:配方名称"` + Description string `gorm:"size:255;comment:配方描述"` + // RecipeIngredients 关联此配方的所有原料组成 + RecipeIngredients []RecipeIngredient `gorm:"foreignKey:RecipeID"` +} + +func (Recipe) TableName() string { + return "recipes" +} + +// RecipeIngredient 配方原料组成模型 +type RecipeIngredient struct { + Model + RecipeID uint32 `gorm:"not null;comment:关联的配方ID"` + Recipe Recipe `gorm:"foreignKey:RecipeID"` + RawMaterialID uint32 `gorm:"not null;comment:关联的原料ID"` + RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` + // 重量百分比 + Percentage float32 `gorm:"not null;comment:原料在配方中的百分比 (0-1之间的小数, 例如0.15代表15%)"` +} + +func (RecipeIngredient) TableName() string { + return "recipe_ingredients" +} diff --git a/internal/infra/repository/recipe_repository.go b/internal/infra/repository/recipe_repository.go new file mode 100644 index 0000000..4d416a6 --- /dev/null +++ b/internal/infra/repository/recipe_repository.go @@ -0,0 +1,182 @@ +package repository + +import ( + "context" + "errors" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + + "gorm.io/gorm" +) + +// RecipeListOptions 定义了查询配方列表时的筛选条件 +type RecipeListOptions struct { + Name *string + RawMaterialName *string + OrderBy string +} + +// RecipeRepository 定义了与配方相关的数据库操作接口 +type RecipeRepository interface { + CreateRecipe(ctx context.Context, recipe *models.Recipe) error + GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error) + GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error) + ListRecipes(ctx context.Context, opts RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error) + UpdateRecipe(ctx context.Context, recipe *models.Recipe) error + UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error + DeleteRecipe(ctx context.Context, id uint32) error +} + +// gormRecipeRepository 是 RecipeRepository 的 GORM 实现 +type gormRecipeRepository struct { + ctx context.Context + db *gorm.DB +} + +// NewGormRecipeRepository 创建一个新的 RecipeRepository GORM 实现实例 +func NewGormRecipeRepository(ctx context.Context, db *gorm.DB) RecipeRepository { + return &gormRecipeRepository{ctx: ctx, db: db} +} + +// CreateRecipe 创建一个新的配方,并处理其关联的配方原料 +func (r *gormRecipeRepository) CreateRecipe(ctx context.Context, recipe *models.Recipe) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRecipe") + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + if err := tx.Create(recipe).Error; err != nil { + return fmt.Errorf("创建配方失败: %w", err) + } + return nil + }) +} + +// GetRecipeByID 根据ID获取单个配方,并预加载其关联的配方原料和原料信息 +func (r *gormRecipeRepository) GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRecipeByID") + var recipe models.Recipe + // 如果记录未找到,GORM 会返回 gorm.ErrRecordNotFound 错误 + if err := r.db.WithContext(repoCtx).Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").First(&recipe, id).Error; err != nil { + return nil, err + } + return &recipe, nil +} + +// GetRecipeByName 根据名称获取单个配方,并预加载其关联的配方原料和原料信息 +func (r *gormRecipeRepository) GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRecipeByName") + var recipe models.Recipe + // 如果记录未找到,GORM 会返回 gorm.ErrRecordNotFound 错误 + if err := r.db.WithContext(repoCtx).Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").Where("name = ?", name).First(&recipe).Error; err != nil { + return nil, err + } + return &recipe, nil +} + +// ListRecipes 列出所有配方(分页),并预加载其关联的配方原料和原料信息 +func (r *gormRecipeRepository) ListRecipes(ctx context.Context, opts RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRecipes") + var recipes []models.Recipe + var total int64 + + db := r.db.WithContext(repoCtx).Model(&models.Recipe{}) + + // 应用筛选条件 + if opts.Name != nil && *opts.Name != "" { + db = db.Where("name LIKE ?", "%"+*opts.Name+"%") + } + + // 如果传入了原料名称,则使用子查询进行筛选 + if opts.RawMaterialName != nil && *opts.RawMaterialName != "" { + subQuery := r.db.Model(&models.RecipeIngredient{}). + Select("recipe_id"). + Joins("JOIN raw_materials ON raw_materials.id = recipe_ingredients.raw_material_id"). + Where("raw_materials.name LIKE ?", "%"+*opts.RawMaterialName+"%") + db = db.Where("id IN (?)", subQuery) + } + + // 首先计算总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 然后应用排序、分页并获取数据 + if opts.OrderBy != "" { + db = db.Order(opts.OrderBy) + } + offset := (page - 1) * pageSize + if err := db.Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").Offset(offset).Limit(pageSize).Find(&recipes).Error; err != nil { + return nil, 0, err + } + + return recipes, total, nil +} + +// UpdateRecipe 更新一个配方的主体信息(名称和描述) +func (r *gormRecipeRepository) UpdateRecipe(ctx context.Context, recipe *models.Recipe) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRecipe") + + updateData := map[string]interface{}{ + "name": recipe.Name, + "description": recipe.Description, + } + result := r.db.WithContext(repoCtx).Model(&models.Recipe{}).Where("id = ?", recipe.ID).Updates(updateData) + if result.Error != nil { + return fmt.Errorf("更新配方主体信息失败: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("未找到要更新的配方,ID: %d", recipe.ID) + } + return nil +} + +// UpdateRecipeIngredients 更新配方关联的原料列表 +func (r *gormRecipeRepository) UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRecipeIngredients") + + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + // 1. 删除所有旧的关联配方原料 + if err := tx.Where("recipe_id = ?", recipeID).Delete(&models.RecipeIngredient{}).Error; err != nil { + return fmt.Errorf("删除旧的配方原料失败: %w", err) + } + + // 2. 批量创建新的关联配方原料 + if len(ingredients) > 0 { + for i := range ingredients { + ingredients[i].RecipeID = recipeID + } + if err := tx.Create(&ingredients).Error; err != nil { + return fmt.Errorf("创建新的配方原料失败: %w", err) + } + } + return nil + }) +} + +// DeleteRecipe 根据ID删除一个配方,并级联软删除关联的 RecipeIngredient 记录 +func (r *gormRecipeRepository) DeleteRecipe(ctx context.Context, id uint32) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRecipe") + + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + // 1. 查找 Recipe 记录,确保其存在 + var recipe models.Recipe + if err := tx.First(&recipe, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("未找到要删除的配方,ID: %d", id) + } + return fmt.Errorf("查询配方失败: %w", err) + } + + // 2. 软删除所有关联的 RecipeIngredient 记录 + if err := tx.Where("recipe_id = ?", id).Delete(&models.RecipeIngredient{}).Error; err != nil { + return fmt.Errorf("软删除关联的配方原料记录失败: %w", err) + } + + // 3. 软删除 Recipe 记录本身 + if err := tx.Delete(&recipe).Error; err != nil { + return fmt.Errorf("软删除配方失败: %w", err) + } + + return nil + }) +} diff --git a/project_structure.txt b/project_structure.txt index d1c4654..c61b69d 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -144,6 +144,7 @@ internal/infra/models/pig_trade.go internal/infra/models/pig_transfer.go internal/infra/models/plan.go internal/infra/models/raw_material.go +internal/infra/models/recipe.go internal/infra/models/schedule.go internal/infra/models/sensor_data.go internal/infra/models/user.go @@ -173,6 +174,7 @@ internal/infra/repository/pig_transfer_log_repository.go internal/infra/repository/pig_type_repository.go internal/infra/repository/plan_repository.go internal/infra/repository/raw_material_repository.go +internal/infra/repository/recipe_repository.go internal/infra/repository/repository.go internal/infra/repository/sensor_data_repository.go internal/infra/repository/unit_of_work.go