diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index ac0c8a0..e721802 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -49,4 +49,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 # 完成事项 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 -2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 \ No newline at end of file +2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 +3. 定义配方领域, 实现营养元素的增删改查 \ No newline at end of file diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go new file mode 100644 index 0000000..5eaad96 --- /dev/null +++ b/internal/domain/recipe/recipe_service.go @@ -0,0 +1,144 @@ +package recipe + +import ( + "context" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// 定义领域特定的错误 +var ( + ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在") + ErrNutrientNotFound = fmt.Errorf("营养种类不存在") + ErrNutrientInUse = fmt.Errorf("营养种类正在被原料使用,无法删除") +) + +// Service 定义了配方与原料领域的核心业务服务接口 +type Service interface { + CreateNutrient(ctx context.Context, name, description string) (*models.Nutrient, error) + UpdateNutrient(ctx context.Context, id uint32, name, description string) (*models.Nutrient, error) + DeleteNutrient(ctx context.Context, id uint32) error + GetNutrient(ctx context.Context, id uint32) (*models.Nutrient, error) + ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) +} + +// recipeServiceImpl 是 RecipeService 的实现 +type recipeServiceImpl struct { + ctx context.Context + nutrientRepo repository.NutrientRepository +} + +// NewRecipeService 创建一个新的 RecipeService 实例 +func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository) Service { + return &recipeServiceImpl{ + ctx: ctx, + nutrientRepo: nutrientRepo, + } +} + +// CreateNutrient 实现了创建营养种类的核心业务逻辑 +func (s *recipeServiceImpl) CreateNutrient(ctx context.Context, name, description string) (*models.Nutrient, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateNutrient") + + // 检查名称是否已存在 + existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name) + if err != nil { + return nil, fmt.Errorf("检查营养种类名称失败: %w", err) + } + if existing != nil { + return nil, ErrNutrientNameConflict + } + + nutrient := &models.Nutrient{ + Name: name, + Description: description, + } + + if err := s.nutrientRepo.CreateNutrient(serviceCtx, nutrient); err != nil { + return nil, fmt.Errorf("创建营养种类失败: %w", err) + } + + return nutrient, nil +} + +// UpdateNutrient 实现了更新营养种类的核心业务逻辑 +func (s *recipeServiceImpl) UpdateNutrient(ctx context.Context, id uint32, name, description string) (*models.Nutrient, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateNutrient") + + // 检查要更新的实体是否存在 + nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) + if err != nil { + return nil, fmt.Errorf("获取待更新的营养种类失败: %w", err) + } + if nutrient == nil { + return nil, ErrNutrientNotFound + } + + // 如果名称有变动,检查新名称是否与其它记录冲突 + if nutrient.Name != name { + existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name) + if err != nil { + return nil, fmt.Errorf("检查新的营养种类名称失败: %w", err) + } + if existing != nil && existing.ID != id { + return nil, ErrNutrientNameConflict + } + } + + nutrient.Name = name + nutrient.Description = description + + if err := s.nutrientRepo.UpdateNutrient(serviceCtx, nutrient); err != nil { + return nil, fmt.Errorf("更新营养种类失败: %w", err) + } + + return nutrient, nil +} + +// DeleteNutrient 实现了删除营养种类的核心业务逻辑 +func (s *recipeServiceImpl) DeleteNutrient(ctx context.Context, id uint32) error { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteNutrient") + + // 检查实体是否存在 + nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) + if err != nil { + return fmt.Errorf("获取待删除的营养种类失败: %w", err) + } + if nutrient == nil { + return ErrNutrientNotFound + } + + if err := s.nutrientRepo.DeleteNutrient(serviceCtx, id); err != nil { + return fmt.Errorf("删除营养种类失败: %w", err) + } + + return nil +} + +// GetNutrient 实现了获取单个营养种类的逻辑 +func (s *recipeServiceImpl) GetNutrient(ctx context.Context, id uint32) (*models.Nutrient, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetNutrient") + + nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) + if err != nil { + return nil, fmt.Errorf("获取营养种类失败: %w", err) + } + if nutrient == nil { + return nil, ErrNutrientNotFound + } + return nutrient, nil +} + +// ListNutrients 实现了列出营养种类的逻辑 +func (s *recipeServiceImpl) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients") + + nutrients, total, err := s.nutrientRepo.ListNutrients(serviceCtx, page, pageSize) + if err != nil { + return nil, 0, fmt.Errorf("获取营养种类列表失败: %w", err) + } + return nutrients, total, nil +} diff --git a/internal/infra/repository/nutrient_repository.go b/internal/infra/repository/nutrient_repository.go new file mode 100644 index 0000000..70b7ef1 --- /dev/null +++ b/internal/infra/repository/nutrient_repository.go @@ -0,0 +1,134 @@ +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" +) + +// NutrientRepository 定义了与营养种类相关的数据库操作接口 +type NutrientRepository interface { + CreateNutrient(ctx context.Context, nutrient *models.Nutrient) error + GetNutrientByID(ctx context.Context, id uint32) (*models.Nutrient, error) + GetNutrientByName(ctx context.Context, name string) (*models.Nutrient, error) + ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) + UpdateNutrient(ctx context.Context, nutrient *models.Nutrient) error + DeleteNutrient(ctx context.Context, id uint32) error +} + +// gormNutrientRepository 是 NutrientRepository 的 GORM 实现 +type gormNutrientRepository struct { + ctx context.Context + db *gorm.DB +} + +// NewGormNutrientRepository 创建一个新的 NutrientRepository GORM 实现实例 +func NewGormNutrientRepository(ctx context.Context, db *gorm.DB) NutrientRepository { + return &gormNutrientRepository{ctx: ctx, db: db} +} + +// CreateNutrient 创建一个新的营养种类 +func (r *gormNutrientRepository) CreateNutrient(ctx context.Context, nutrient *models.Nutrient) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateNutrient") + return r.db.WithContext(repoCtx).Create(nutrient).Error +} + +// GetNutrientByID 根据ID获取单个营养种类 +func (r *gormNutrientRepository) GetNutrientByID(ctx context.Context, id uint32) (*models.Nutrient, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "GetNutrientByID") + var nutrient models.Nutrient + if err := r.db.WithContext(repoCtx).First(&nutrient, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 记录未找到不应视为错误 + } + return nil, err + } + return &nutrient, nil +} + +// GetNutrientByName 根据名称获取单个营养种类 +func (r *gormNutrientRepository) GetNutrientByName(ctx context.Context, name string) (*models.Nutrient, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "GetNutrientByName") + var nutrient models.Nutrient + if err := r.db.WithContext(repoCtx).Where("name = ?", name).First(&nutrient).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 记录未找到不应视为错误 + } + return nil, err + } + return &nutrient, nil +} + +// ListNutrients 列出所有营养种类(分页) +func (r *gormNutrientRepository) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListNutrients") + var nutrients []models.Nutrient + var total int64 + + db := r.db.WithContext(repoCtx).Model(&models.Nutrient{}) + + // 首先计算总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 然后应用分页并获取数据 + offset := (page - 1) * pageSize + if err := db.Offset(offset).Limit(pageSize).Find(&nutrients).Error; err != nil { + return nil, 0, err + } + + return nutrients, total, nil +} + +// UpdateNutrient 更新一个营养种类 +func (r *gormNutrientRepository) UpdateNutrient(ctx context.Context, nutrient *models.Nutrient) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateNutrient") + // 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段 + updateData := map[string]interface{}{ + "name": nutrient.Name, + "description": nutrient.Description, + } + result := r.db.WithContext(repoCtx).Model(&models.Nutrient{}).Where("id = ?", nutrient.ID).Updates(updateData) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("未找到要更新的营养种类,ID: %d", nutrient.ID) + } + return nil +} + +// DeleteNutrient 根据ID删除一个营养种类,并级联软删除关联的 RawMaterialNutrient 记录 +func (r *gormNutrientRepository) DeleteNutrient(ctx context.Context, id uint32) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteNutrient") + + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + // 1. 查找 Nutrient 记录,确保其存在 + var nutrient models.Nutrient + if err := tx.First(&nutrient, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("未找到要删除的营养种类,ID: %d", id) + } + return fmt.Errorf("查询营养种类失败: %w", err) + } + + // 2. 软删除所有关联的 RawMaterialNutrient 记录 + if err := tx.Where("nutrient_id = ?", id).Delete(&models.RawMaterialNutrient{}).Error; err != nil { + return fmt.Errorf("软删除关联的原料营养素记录失败: %w", err) + } + + // 3. 软删除 Nutrient 记录本身 + if err := tx.Delete(&nutrient).Error; err != nil { + return fmt.Errorf("软删除营养种类失败: %w", err) + } + + return nil + }) + +} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go new file mode 100644 index 0000000..da7d37c --- /dev/null +++ b/internal/infra/repository/raw_material_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "context" + + "gorm.io/gorm" +) + +// RawMaterialRepository 定义了与原料相关的数据库操作接口 +type RawMaterialRepository interface { +} + +// gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现 +type gormRawMaterialRepository struct { + ctx context.Context + db *gorm.DB +} + +// NewGormRawMaterialRepository 创建一个新的 RawMaterialRepository GORM 实现实例 +func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository { + return &gormRawMaterialRepository{ctx: ctx, db: db} +} diff --git a/project_structure.txt b/project_structure.txt index c01eab0..73cbd99 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -1,15 +1,16 @@  -.air.toml .gitignore -.golangci.yml -.swaggo AGENTS.md Makefile README.md RELAY_API.md TODO-List.txt -config.example.yml -config.yml +config/.air.toml +config/.golangci.yml +config/config.example.yml +config/config.yml +config/presets-data/nutrient.json +config/presets-data/system_plans.json design/archive/2025-11-03-verification-before-device-deletion/add_get_device_id_configs_to_task.md design/archive/2025-11-03-verification-before-device-deletion/check_before_device_deletion.md design/archive/2025-11-03-verification-before-device-deletion/device_task_association_maintenance.md @@ -33,6 +34,7 @@ design/archive/2025-11-05-provide-logger-with-mothed/task-webhook.md design/archive/2025-11-06-health-check-routing/index.md design/archive/2025-11-06-system-plan-continuously-triggered/index.md design/archive/2025-11-10-exceeding-threshold-alarm/index.md +design/archive/recipe-management/index.md docs/docs.go docs/swagger.json docs/swagger.yaml @@ -102,25 +104,28 @@ internal/domain/plan/analysis_plan_task_manager.go internal/domain/plan/plan_execution_manager.go internal/domain/plan/plan_service.go internal/domain/plan/task.go +internal/domain/recipe/recipe_service.go internal/domain/task/alarm_notification_task.go internal/domain/task/area_threshold_check_task.go internal/domain/task/delay_task.go internal/domain/task/device_threshold_check_task.go internal/domain/task/full_collection_task.go +internal/domain/task/refresh_notification_task.go internal/domain/task/release_feed_weight_task.go internal/domain/task/task.go internal/infra/config/config.go internal/infra/database/postgres.go +internal/infra/database/seeder.go internal/infra/database/storage.go internal/infra/logs/context.go internal/infra/logs/encoder.go +internal/infra/logs/logger_methods.go internal/infra/logs/logs.go internal/infra/models/alarm.go internal/infra/models/device.go internal/infra/models/device_template.go internal/infra/models/execution.go internal/infra/models/farm_asset.go -internal/infra/models/feed.go internal/infra/models/medication.go internal/infra/models/models.go internal/infra/models/notify.go @@ -129,6 +134,7 @@ internal/infra/models/pig_sick.go 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/schedule.go internal/infra/models/sensor_data.go internal/infra/models/user.go @@ -145,6 +151,7 @@ internal/infra/repository/device_template_repository.go internal/infra/repository/execution_log_repository.go internal/infra/repository/medication_log_repository.go internal/infra/repository/notification_repository.go +internal/infra/repository/nutrient_repository.go internal/infra/repository/pending_collection_repository.go internal/infra/repository/pending_task_repository.go internal/infra/repository/pig_batch_log_repository.go