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

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

@@ -60,4 +60,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
10. 实现修改猪营养需求
11. 配方模型定义和仓库层增删改查方法
12. 配方领域层方法
13. 重构配方领域
13. 重构配方领域
14. 配方增删改查服务层和控制器

View File

@@ -3021,6 +3021,252 @@ const docTemplate = `{
}
}
},
"/api/v1/feed/recipes": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取所有配方的列表,支持分页和过滤。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "获取配方列表",
"parameters": [
{
"type": "string",
"description": "按名称模糊查询",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "排序字段,例如 \"id DESC\"",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListRecipeResponse"
}
}
}
]
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "创建一个新的配方,包含其原料组成。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "创建配方",
"parameters": [
{
"description": "配方信息",
"name": "recipe",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.CreateRecipeRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为201代表创建成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/feed/recipes/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID获取单个配方的详细信息。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "获取配方详情",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID更新配方信息及其原料组成。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "更新配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "更新后的配方信息",
"name": "recipe",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateRecipeRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为200代表更新成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID删除配方。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "删除配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表删除成功",
"schema": {
"$ref": "#/definitions/controller.Response"
}
}
}
}
},
"/api/v1/monitor/device-command-logs": {
"get": {
"security": [
@@ -6761,6 +7007,31 @@ const docTemplate = `{
}
}
},
"dto.CreateRecipeRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"description": "配方描述",
"type": "string",
"maxLength": 255
},
"name": {
"description": "配方名称",
"type": "string",
"maxLength": 100
},
"recipe_ingredients": {
"description": "配方原料组成",
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.CreateUserRequest": {
"type": "object",
"required": [
@@ -7241,6 +7512,20 @@ const docTemplate = `{
}
}
},
"dto.ListRecipeResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListSensorDataResponse": {
"type": "object",
"properties": {
@@ -8214,6 +8499,44 @@ const docTemplate = `{
}
}
},
"dto.RecipeIngredientDto": {
"type": "object",
"required": [
"raw_material_id"
],
"properties": {
"percentage": {
"description": "原料在配方中的百分比 (0-1之间)",
"type": "number",
"maximum": 1,
"minimum": 0
},
"raw_material_id": {
"description": "原料ID",
"type": "integer"
}
}
},
"dto.RecipeResponse": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"recipe_ingredients": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.ReclassifyPenToNewBatchRequest": {
"type": "object",
"required": [
@@ -9150,6 +9473,31 @@ const docTemplate = `{
}
}
},
"dto.UpdateRecipeRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"description": "配方描述",
"type": "string",
"maxLength": 255
},
"name": {
"description": "配方名称",
"type": "string",
"maxLength": 100
},
"recipe_ingredients": {
"description": "配方原料组成",
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.UserActionLogDTO": {
"type": "object",
"properties": {

View File

@@ -3013,6 +3013,252 @@
}
}
},
"/api/v1/feed/recipes": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取所有配方的列表,支持分页和过滤。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "获取配方列表",
"parameters": [
{
"type": "string",
"description": "按名称模糊查询",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "排序字段,例如 \"id DESC\"",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListRecipeResponse"
}
}
}
]
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "创建一个新的配方,包含其原料组成。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "创建配方",
"parameters": [
{
"description": "配方信息",
"name": "recipe",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.CreateRecipeRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为201代表创建成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/feed/recipes/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID获取单个配方的详细信息。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "获取配方详情",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID更新配方信息及其原料组成。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "更新配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "更新后的配方信息",
"name": "recipe",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateRecipeRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为200代表更新成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.RecipeResponse"
}
}
}
]
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID删除配方。",
"produces": [
"application/json"
],
"tags": [
"饲料管理-配方"
],
"summary": "删除配方",
"parameters": [
{
"type": "integer",
"description": "配方ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "业务码为200代表删除成功",
"schema": {
"$ref": "#/definitions/controller.Response"
}
}
}
}
},
"/api/v1/monitor/device-command-logs": {
"get": {
"security": [
@@ -6753,6 +6999,31 @@
}
}
},
"dto.CreateRecipeRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"description": "配方描述",
"type": "string",
"maxLength": 255
},
"name": {
"description": "配方名称",
"type": "string",
"maxLength": 100
},
"recipe_ingredients": {
"description": "配方原料组成",
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.CreateUserRequest": {
"type": "object",
"required": [
@@ -7233,6 +7504,20 @@
}
}
},
"dto.ListRecipeResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListSensorDataResponse": {
"type": "object",
"properties": {
@@ -8206,6 +8491,44 @@
}
}
},
"dto.RecipeIngredientDto": {
"type": "object",
"required": [
"raw_material_id"
],
"properties": {
"percentage": {
"description": "原料在配方中的百分比 (0-1之间)",
"type": "number",
"maximum": 1,
"minimum": 0
},
"raw_material_id": {
"description": "原料ID",
"type": "integer"
}
}
},
"dto.RecipeResponse": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"recipe_ingredients": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.ReclassifyPenToNewBatchRequest": {
"type": "object",
"required": [
@@ -9142,6 +9465,31 @@
}
}
},
"dto.UpdateRecipeRequest": {
"type": "object",
"required": [
"name"
],
"properties": {
"description": {
"description": "配方描述",
"type": "string",
"maxLength": 255
},
"name": {
"description": "配方名称",
"type": "string",
"maxLength": 100
},
"recipe_ingredients": {
"description": "配方原料组成",
"type": "array",
"items": {
"$ref": "#/definitions/dto.RecipeIngredientDto"
}
}
}
},
"dto.UserActionLogDTO": {
"type": "object",
"properties": {

View File

@@ -423,6 +423,24 @@ definitions:
required:
- name
type: object
dto.CreateRecipeRequest:
properties:
description:
description: 配方描述
maxLength: 255
type: string
name:
description: 配方名称
maxLength: 100
type: string
recipe_ingredients:
description: 配方原料组成
items:
$ref: '#/definitions/dto.RecipeIngredientDto'
type: array
required:
- name
type: object
dto.CreateUserRequest:
properties:
password:
@@ -735,6 +753,15 @@ definitions:
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListRecipeResponse:
properties:
list:
items:
$ref: '#/definitions/dto.RecipeResponse'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListSensorDataResponse:
properties:
list:
@@ -1379,6 +1406,32 @@ definitions:
$ref: '#/definitions/dto.RawMaterialNutrientDTO'
type: array
type: object
dto.RecipeIngredientDto:
properties:
percentage:
description: 原料在配方中的百分比 (0-1之间)
maximum: 1
minimum: 0
type: number
raw_material_id:
description: 原料ID
type: integer
required:
- raw_material_id
type: object
dto.RecipeResponse:
properties:
description:
type: string
id:
type: integer
name:
type: string
recipe_ingredients:
items:
$ref: '#/definitions/dto.RecipeIngredientDto'
type: array
type: object
dto.ReclassifyPenToNewBatchRequest:
properties:
pen_id:
@@ -2024,6 +2077,24 @@ definitions:
required:
- name
type: object
dto.UpdateRecipeRequest:
properties:
description:
description: 配方描述
maxLength: 255
type: string
name:
description: 配方名称
maxLength: 100
type: string
recipe_ingredients:
description: 配方原料组成
items:
$ref: '#/definitions/dto.RecipeIngredientDto'
type: array
required:
- name
type: object
dto.UserActionLogDTO:
properties:
action_type:
@@ -4386,6 +4457,150 @@ paths:
summary: 全量更新原料的营养成分
tags:
- 饲料管理-原料
/api/v1/feed/recipes:
get:
description: 获取所有配方的列表,支持分页和过滤。
parameters:
- description: 按名称模糊查询
in: query
name: name
type: string
- description: 排序字段,例如 "id DESC"
in: query
name: order_by
type: string
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: page_size
type: integer
produces:
- application/json
responses:
"200":
description: 业务码为200代表成功获取列表
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ListRecipeResponse'
type: object
security:
- BearerAuth: []
summary: 获取配方列表
tags:
- 饲料管理-配方
post:
consumes:
- application/json
description: 创建一个新的配方,包含其原料组成。
parameters:
- description: 配方信息
in: body
name: recipe
required: true
schema:
$ref: '#/definitions/dto.CreateRecipeRequest'
produces:
- application/json
responses:
"200":
description: 业务码为201代表创建成功
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.RecipeResponse'
type: object
security:
- BearerAuth: []
summary: 创建配方
tags:
- 饲料管理-配方
/api/v1/feed/recipes/{id}:
delete:
description: 根据ID删除配方。
parameters:
- description: 配方ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 业务码为200代表删除成功
schema:
$ref: '#/definitions/controller.Response'
security:
- BearerAuth: []
summary: 删除配方
tags:
- 饲料管理-配方
get:
description: 根据ID获取单个配方的详细信息。
parameters:
- description: 配方ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: 业务码为200代表成功获取
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.RecipeResponse'
type: object
security:
- BearerAuth: []
summary: 获取配方详情
tags:
- 饲料管理-配方
put:
consumes:
- application/json
description: 根据ID更新配方信息及其原料组成。
parameters:
- description: 配方ID
in: path
name: id
required: true
type: integer
- description: 更新后的配方信息
in: body
name: recipe
required: true
schema:
$ref: '#/definitions/dto.UpdateRecipeRequest'
produces:
- application/json
responses:
"200":
description: 业务码为200代表更新成功
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.RecipeResponse'
type: object
security:
- BearerAuth: []
summary: 更新配方
tags:
- 饲料管理-配方
/api/v1/monitor/device-command-logs:
get:
description: 根据提供的过滤条件,分页获取设备命令日志

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
}

View File

@@ -68,6 +68,7 @@ func NewApplication(configPath string) (*Application, error) {
appServices.pigBreedService,
appServices.pigAgeStageService,
appServices.pigTypeService,
appServices.recipeService,
infra.tokenGenerator,
infra.lora.listenHandler,
)

View File

@@ -264,6 +264,7 @@ type AppServices struct {
pigBreedService service.PigBreedService
pigTypeService service.PigTypeService
rawMaterialService service.RawMaterialService
recipeService service.RecipeService
}
// initAppServices 初始化所有的应用服务。
@@ -316,6 +317,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
pigBreedService := service.NewPigBreedService(logs.AddCompName(baseCtx, "PigBreedService"), domainServices.recipeService)
pigTypeService := service.NewPigTypeService(logs.AddCompName(baseCtx, "PigTypeService"), domainServices.recipeService)
rawMaterialService := service.NewRawMaterialService(logs.AddCompName(baseCtx, "RawMaterialService"), domainServices.recipeService)
recipeService := service.NewRecipeService(logs.AddCompName(baseCtx, "RecipeService"), domainServices.recipeService)
return &AppServices{
pigFarmService: pigFarmService,
@@ -331,6 +333,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
pigBreedService: pigBreedService,
pigTypeService: pigTypeService,
rawMaterialService: rawMaterialService,
recipeService: recipeService,
}
}

View File

@@ -53,6 +53,7 @@ internal/app/controller/feed/pig_age_stage_controller.go
internal/app/controller/feed/pig_breed_controller.go
internal/app/controller/feed/pig_type_controller.go
internal/app/controller/feed/raw_material_controller.go
internal/app/controller/feed/recipe_controller.go
internal/app/controller/health/health_controller.go
internal/app/controller/management/controller_helpers.go
internal/app/controller/management/pig_batch_controller.go
@@ -94,6 +95,7 @@ internal/app/service/pig_service.go
internal/app/service/pig_type_service.go
internal/app/service/plan_service.go
internal/app/service/raw_material_service.go
internal/app/service/recipe_service.go
internal/app/service/threshold_alarm_service.go
internal/app/service/user_service.go
internal/app/webhook/chirp_stack.go