diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 612428d..e5bbe80 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -60,4 +60,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 10. 实现修改猪营养需求 11. 配方模型定义和仓库层增删改查方法 12. 配方领域层方法 -13. 重构配方领域 \ No newline at end of file +13. 重构配方领域 +14. 配方增删改查服务层和控制器 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 48a8945..e645141 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index ee1aa18..a01c4f3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 31b7882..e52e437 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: 根据提供的过滤条件,分页获取设备命令日志 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 1a2cea4..830c322 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -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() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index e5d04bd..ce940d0 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -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("饲料管理相关接口注册成功 (需要认证和审计)") } diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go new file mode 100644 index 0000000..ebe371b --- /dev/null +++ b/internal/app/controller/feed/recipe_controller.go @@ -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) +} diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index bca9492..f255b8b 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -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, + } +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 1fcb10c..395eeef 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -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"` +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go new file mode 100644 index 0000000..01265e1 --- /dev/null +++ b/internal/app/service/recipe_service.go @@ -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 +} diff --git a/internal/core/application.go b/internal/core/application.go index 2511771..b21e53f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -68,6 +68,7 @@ func NewApplication(configPath string) (*Application, error) { appServices.pigBreedService, appServices.pigAgeStageService, appServices.pigTypeService, + appServices.recipeService, infra.tokenGenerator, infra.lora.listenHandler, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index bc5339d..6878729 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -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, } } diff --git a/project_structure.txt b/project_structure.txt index ed9a36d..c1e1dee 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -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