配方增删改查服务层和控制器

This commit is contained in:
2025-11-24 13:25:15 +08:00
parent 1200f36d14
commit d7deaa346b
13 changed files with 1411 additions and 1 deletions

View File

@@ -61,6 +61,7 @@ type API struct {
pigBreedController *feed.PigBreedController // 猪品种控制器实例
pigTypeController *feed.PigTypeController // 猪种类控制器实例
rawMaterialController *feed.RawMaterialController // 原料控制器实例
recipeController *feed.RecipeController // 配方控制器实例
listenHandler webhook.ListenHandler // 设备上行事件监听器
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
}
@@ -83,6 +84,7 @@ func NewAPI(cfg config.ServerConfig,
pigBreedService service.PigBreedService,
pigAgeStageService service.PigAgeStageService,
pigTypeService service.PigTypeService,
recipeService service.RecipeService,
tokenGenerator token.Generator,
listenHandler webhook.ListenHandler,
) *API {
@@ -119,6 +121,7 @@ func NewAPI(cfg config.ServerConfig,
pigBreedController: feed.NewPigBreedController(logs.AddCompName(baseCtx, "PigBreedController"), pigBreedService),
pigTypeController: feed.NewPigTypeController(logs.AddCompName(baseCtx, "PigTypeController"), pigTypeService),
rawMaterialController: feed.NewRawMaterialController(logs.AddCompName(baseCtx, "RawMaterialController"), rawMaterialService),
recipeController: feed.NewRecipeController(logs.AddCompName(baseCtx, "RecipeController"), recipeService),
}
api.setupRoutes() // 设置所有路由

View File

@@ -252,6 +252,13 @@ func (a *API) setupRoutes() {
feedGroup.GET("/pig-types/:id", a.pigTypeController.GetPigType)
feedGroup.GET("/pig-types", a.pigTypeController.ListPigTypes)
feedGroup.PUT("/pig-types/:id/nutrient-requirements", a.pigTypeController.UpdatePigTypeNutrientRequirements)
// 配方 (Recipe) 路由
feedGroup.POST("/recipes", a.recipeController.CreateRecipe)
feedGroup.PUT("/recipes/:id", a.recipeController.UpdateRecipe)
feedGroup.DELETE("/recipes/:id", a.recipeController.DeleteRecipe)
feedGroup.GET("/recipes/:id", a.recipeController.GetRecipe)
feedGroup.GET("/recipes", a.recipeController.ListRecipes)
}
logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)")
}

View File

@@ -0,0 +1,196 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// RecipeController 包含配方相关的处理器
type RecipeController struct {
ctx context.Context
recipeService service.RecipeService
}
// NewRecipeController 创建一个新的 RecipeController
func NewRecipeController(ctx context.Context, recipeService service.RecipeService) *RecipeController {
return &RecipeController{
ctx: ctx,
recipeService: recipeService,
}
}
// CreateRecipe godoc
// @Summary 创建配方
// @Description 创建一个新的配方,包含其原料组成。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param recipe body dto.CreateRecipeRequest true "配方信息"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/recipes [post]
func (c *RecipeController) CreateRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateRecipe")
var req dto.CreateRecipeRequest
const actionType = "创建配方"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.recipeService.CreateRecipe(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建配方失败: %v", actionType, err)
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "配方名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建配方失败: "+err.Error(), actionType, "服务层创建配方失败", req)
}
logger.Infof("%s: 配方创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方创建成功", resp, actionType, "配方创建成功", resp)
}
// UpdateRecipe godoc
// @Summary 更新配方
// @Description 根据ID更新配方信息及其原料组成。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "配方ID"
// @Param recipe body dto.UpdateRecipeRequest true "更新后的配方信息"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/recipes/{id} [put]
func (c *RecipeController) UpdateRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateRecipe")
const actionType = "更新配方"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", idStr)
}
var req dto.UpdateRecipeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.recipeService.UpdateRecipe(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新配方失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "配方名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新配方失败: "+err.Error(), actionType, "服务层更新配方失败", req)
}
logger.Infof("%s: 配方更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "配方更新成功", resp, actionType, "配方更新成功", resp)
}
// DeleteRecipe godoc
// @Summary 删除配方
// @Description 根据ID删除配方。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param id path int true "配方ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/recipes/{id} [delete]
func (c *RecipeController) DeleteRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteRecipe")
const actionType = "删除配方"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", idStr)
}
err = c.recipeService.DeleteRecipe(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除配方失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除配方失败: "+err.Error(), actionType, "服务层删除配方失败", id)
}
logger.Infof("%s: 配方删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "配方删除成功", nil, actionType, "配方删除成功", id)
}
// GetRecipe godoc
// @Summary 获取配方详情
// @Description 根据ID获取单个配方的详细信息。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param id path int true "配方ID"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/recipes/{id} [get]
func (c *RecipeController) GetRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetRecipe")
const actionType = "获取配方详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", idStr)
}
resp, err := c.recipeService.GetRecipeByID(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取配方详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取配方详情失败: "+err.Error(), actionType, "服务层获取配方详情失败", id)
}
logger.Infof("%s: 获取配方详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取配方详情成功", resp, actionType, "获取配方详情成功", resp)
}
// ListRecipes godoc
// @Summary 获取配方列表
// @Description 获取所有配方的列表,支持分页和过滤。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRecipeRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRecipeResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/recipes [get]
func (c *RecipeController) ListRecipes(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRecipes")
const actionType = "获取配方列表"
var req dto.ListRecipeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.recipeService.ListRecipes(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取配方列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取配方列表失败: "+err.Error(), actionType, "服务层获取配方列表失败", nil)
}
logger.Infof("%s: 获取配方列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取配方列表成功", resp, actionType, "获取配方列表成功", resp)
}

View File

@@ -198,3 +198,84 @@ func ConvertPigTypeListToDTO(pigTypes []models.PigType, total int64, page, pageS
},
}
}
// ConvertRecipeToDto 将 models.Recipe 转换为 RecipeResponse DTO
func ConvertRecipeToDto(recipe *models.Recipe) *RecipeResponse {
if recipe == nil {
return nil
}
ingredients := make([]RecipeIngredientDto, len(recipe.RecipeIngredients))
for i, ri := range recipe.RecipeIngredients {
ingredients[i] = RecipeIngredientDto{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &RecipeResponse{
ID: recipe.ID,
Name: recipe.Name,
Description: recipe.Description,
RecipeIngredients: ingredients,
}
}
// ConvertRecipeListToDTO 将 []models.Recipe 转换为 ListRecipeResponse DTO
func ConvertRecipeListToDTO(recipes []models.Recipe, total int64, page, pageSize int) *ListRecipeResponse {
recipeDTOs := make([]RecipeResponse, len(recipes))
for i, r := range recipes {
recipeDTOs[i] = *ConvertRecipeToDto(&r)
}
return &ListRecipeResponse{
List: recipeDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertCreateRecipeRequestToModel 将 CreateRecipeRequest DTO 转换为 models.Recipe 模型
func ConvertCreateRecipeRequestToModel(req *CreateRecipeRequest) *models.Recipe {
if req == nil {
return nil
}
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, ri := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &models.Recipe{
Name: req.Name,
Description: req.Description,
RecipeIngredients: ingredients,
}
}
// ConvertUpdateRecipeRequestToModel 将 UpdateRecipeRequest DTO 转换为 models.Recipe 模型
func ConvertUpdateRecipeRequestToModel(req *UpdateRecipeRequest) *models.Recipe {
if req == nil {
return nil
}
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, ri := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &models.Recipe{
Name: req.Name,
Description: req.Description,
RecipeIngredients: ingredients,
}
}

View File

@@ -274,3 +274,49 @@ type PigNutrientRequirementItem struct {
MinRequirement float32 `json:"min_requirement" validate:"gte=0"` // 最低营养需求量
MaxRequirement float32 `json:"max_requirement" validate:"gte=0"` // 最高营养需求量
}
// =============================================================================================================
// 配方 (Recipe) 相关 DTO
// =============================================================================================================
// RecipeIngredientDto 代表配方中的一个原料及其百分比
type RecipeIngredientDto struct {
RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 原料ID
Percentage float32 `json:"percentage" validate:"gte=0,lte=1"` // 原料在配方中的百分比 (0-1之间)
}
// CreateRecipeRequest 创建配方的请求体
type CreateRecipeRequest struct {
Name string `json:"name" validate:"required,max=100"` // 配方名称
Description string `json:"description" validate:"max=255"` // 配方描述
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients" validate:"dive"` // 配方原料组成
}
// UpdateRecipeRequest 更新配方的请求体
type UpdateRecipeRequest struct {
Name string `json:"name" validate:"required,max=100"` // 配方名称
Description string `json:"description" validate:"max=255"` // 配方描述
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients" validate:"dive"` // 配方原料组成
}
// RecipeResponse 配方响应体
type RecipeResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients"`
}
// ListRecipeRequest 定义了获取配方列表的请求参数
type ListRecipeRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListRecipeResponse 是获取配方列表的响应结构
type ListRecipeResponse struct {
List []RecipeResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}

View File

@@ -0,0 +1,159 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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 (
ErrRecipeNameConflict = errors.New("配方名称已存在")
ErrRecipeNotFound = errors.New("配方不存在")
)
// RecipeService 定义了配方相关的应用服务接口
type RecipeService interface {
CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error)
UpdateRecipe(ctx context.Context, id uint32, req *dto.UpdateRecipeRequest) (*dto.RecipeResponse, error)
DeleteRecipe(ctx context.Context, id uint32) error
GetRecipeByID(ctx context.Context, id uint32) (*dto.RecipeResponse, error)
ListRecipes(ctx context.Context, req *dto.ListRecipeRequest) (*dto.ListRecipeResponse, error)
}
// recipeServiceImpl 是 RecipeService 接口的实现
type recipeServiceImpl struct {
ctx context.Context
recipeSvc recipe.RecipeCoreService
}
// NewRecipeService 创建一个新的 RecipeService 实例
func NewRecipeService(ctx context.Context, recipeSvc recipe.RecipeCoreService) RecipeService {
return &recipeServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreateRecipe 创建配方
func (s *recipeServiceImpl) CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe")
recipeModel := dto.ConvertCreateRecipeRequestToModel(req)
createdRecipe, err := s.recipeSvc.CreateRecipe(serviceCtx, recipeModel)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return nil, ErrRecipeNameConflict
}
return nil, fmt.Errorf("创建配方失败: %w", err)
}
// 创建成功后,获取包含完整信息的配方
fullRecipe, err := s.recipeSvc.GetRecipeByID(serviceCtx, createdRecipe.ID)
if err != nil {
// 理论上不应该发生,因为刚创建成功
return nil, fmt.Errorf("创建后获取配方信息失败: %w", err)
}
return dto.ConvertRecipeToDto(fullRecipe), nil
}
// UpdateRecipe 更新配方
func (s *recipeServiceImpl) UpdateRecipe(ctx context.Context, id uint32, req *dto.UpdateRecipeRequest) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRecipe")
// 1. 转换 DTO 为模型
recipeModel := dto.ConvertUpdateRecipeRequestToModel(req)
recipeModel.ID = id
// 2. 更新配方基础信息
_, err := s.recipeSvc.UpdateRecipe(serviceCtx, recipeModel)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return nil, ErrRecipeNameConflict
}
return nil, fmt.Errorf("更新配方基础信息失败: %w", err)
}
// 3. 更新配方原料
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, item := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RecipeID: id,
RawMaterialID: item.RawMaterialID,
Percentage: item.Percentage,
}
}
err = s.recipeSvc.UpdateRecipeIngredients(serviceCtx, id, ingredients)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("更新配方原料失败: %w", err)
}
// 4. 更新成功后,获取最新的完整配方信息并返回
updatedRecipe, err := s.recipeSvc.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
// 理论上不应该发生,因为刚更新成功
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("更新后获取配方信息失败: %w", err)
}
return dto.ConvertRecipeToDto(updatedRecipe), nil
}
// DeleteRecipe 删除配方
func (s *recipeServiceImpl) DeleteRecipe(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteRecipe")
err := s.recipeSvc.DeleteRecipe(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return ErrRecipeNotFound
}
return fmt.Errorf("删除配方失败: %w", err)
}
return nil
}
// GetRecipeByID 获取单个配方
func (s *recipeServiceImpl) GetRecipeByID(ctx context.Context, id uint32) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRecipeByID")
recipeModel, err := s.recipeSvc.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("获取配方失败: %w", err)
}
return dto.ConvertRecipeToDto(recipeModel), nil
}
// ListRecipes 列出配方
func (s *recipeServiceImpl) ListRecipes(ctx context.Context, req *dto.ListRecipeRequest) (*dto.ListRecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRecipes")
opts := repository.RecipeListOptions{
Name: req.Name,
OrderBy: req.OrderBy,
}
recipes, total, err := s.recipeSvc.ListRecipes(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取配方列表失败: %w", err)
}
return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil
}