From 1b5f715dec43b0694d13d419e69c6c67e3224ae1 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 21:06:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=85=88=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=BA=93=E5=AD=98=E7=9A=84=E9=85=8D=E6=96=B9=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/recipe-management/index.md | 3 +- docs/docs.go | 62 ++++++++++-- docs/swagger.json | 62 ++++++++++-- docs/swagger.yaml | 34 ++++++- internal/app/api/router.go | 1 + .../app/controller/feed/recipe_controller.go | 31 ++++++ internal/app/service/recipe_service.go | 9 ++ internal/domain/recipe/recipe_service.go | 98 +++++++++++++++++++ 8 files changed, 279 insertions(+), 21 deletions(-) diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index 165ec05..7e4caa8 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -64,4 +64,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 14. 配方增删改查服务层和控制器 15. 实现库存管理相关逻辑 16. 实现配方生成器 -17. 实现使用系统中所有可用的原料一键生成配方 \ No newline at end of file +17. 实现使用系统中所有可用的原料一键生成配方 +18. 实现优先使用库存的配方一键生成 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 97adb7f..0e42018 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3191,6 +3191,52 @@ const docTemplate = `{ } } }, + "/api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用优先有库存原料的策略生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3721,7 +3767,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -3731,12 +3776,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3746,7 +3791,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -10625,7 +10671,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10635,10 +10680,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10648,7 +10693,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 450e877..f6e3d7b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3183,6 +3183,52 @@ } } }, + "/api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用优先有库存原料的策略生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3713,7 +3759,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -3723,12 +3768,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3738,7 +3783,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -10617,7 +10663,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10627,10 +10672,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10640,7 +10685,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 08f9ea2..8fd239e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2752,7 +2752,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2763,10 +2762,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2777,6 +2776,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -4781,6 +4781,32 @@ paths: summary: 使用系统中所有可用的原料一键生成配方 tags: - 饲料管理-配方 + /api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}: + post: + description: 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。 + parameters: + - description: 猪类型ID + in: path + name: pig_type_id + required: true + type: integer + produces: + - application/json + responses: + "201": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.GenerateRecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 使用优先有库存原料的策略生成配方 + tags: + - 饲料管理-配方 /api/v1/inventory/stock/adjust: post: consumes: @@ -5010,7 +5036,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -5021,12 +5046,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -5037,6 +5062,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 0ad8681..4189c06 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -260,6 +260,7 @@ func (a *API) setupRoutes() { feedGroup.GET("/recipes/:id", a.recipeController.GetRecipe) feedGroup.GET("/recipes", a.recipeController.ListRecipes) feedGroup.POST("/recipes/generate-from-all-materials/:pig_type_id", a.recipeController.GenerateFromAllMaterials) + feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateFromAllMaterials) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go index 1d44d00..6374454 100644 --- a/internal/app/controller/feed/recipe_controller.go +++ b/internal/app/controller/feed/recipe_controller.go @@ -225,3 +225,34 @@ func (c *RecipeController) GenerateFromAllMaterials(ctx echo.Context) error { logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID) return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp) } + +// GenerateRecipeWithPrioritizedStockRawMaterials godoc +// @Summary 使用优先有库存原料的策略生成配方 +// @Description 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。 +// @Tags 饲料管理-配方 +// @Security BearerAuth +// @Produce json +// @Param pig_type_id path int true "猪类型ID" +// @Success 201 {object} controller.Response{data=dto.GenerateRecipeResponse} "业务码为201代表创建成功" +// @Router /api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id} [post] +func (c *RecipeController) GenerateRecipeWithPrioritizedStockRawMaterials(ctx echo.Context) error { + reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials") + const actionType = "生成优先使用库存原料的配方" + + idStr := ctx.Param("pig_type_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) + } + + recipe, err := c.recipeService.GenerateRecipeWithPrioritizedStockRawMaterials(reqCtx, uint32(id)) + if err != nil { + logger.Errorf("%s: 服务层生成配方失败: %v, PigTypeID: %d", actionType, err, id) + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "生成配方失败: "+err.Error(), actionType, "服务层生成配方失败", id) + } + + resp := dto.ToGenerateRecipeResponse(recipe) + logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID) + return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp) +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 0e1cdfe..1e86f46 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -27,6 +27,8 @@ type RecipeService interface { ListRecipes(ctx context.Context, req *dto.ListRecipeRequest) (*dto.ListRecipeResponse, error) // GenerateRecipeWithAllRawMaterials 添加新方法 GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) + // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 + GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) } // recipeServiceImpl 是 RecipeService 接口的实现 @@ -50,6 +52,13 @@ func (s *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex return s.recipeSvc.GenerateRecipeWithAllRawMaterials(serviceCtx, pigTypeID) } +// GenerateRecipeWithPrioritizedStockRawMaterials 实现生成优先使用库存原料配方的方法 +func (s *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials") + // 直接调用领域服务的方法 + return s.recipeSvc.GenerateRecipeWithPrioritizedStockRawMaterials(serviceCtx, pigTypeID) +} + // CreateRecipe 创建配方 func (s *recipeServiceImpl) CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe") diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index f1a39b3..190f278 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -20,6 +20,8 @@ type Service interface { RecipeCoreService RecipeGenerateManager GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) + // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 + GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) } // recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 @@ -123,3 +125,99 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex // 6. 返回创建的配方 (现在它应该已经有了ID) return recipe, nil } + +// GenerateRecipeWithPrioritizedStockRawMaterials 使用优先有库存原料的策略为特定猪类型生成一个新配方。 +// 通过大幅调低有库存原料的参考价格,诱导生成器优先使用。 +// pigTypeID: 目标猪类型的ID。 +// 返回: 生成的配方对象指针和可能的错误。 +func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) { + serviceCtx, logger := logs.Trace(ctx, r.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials") + + // 1. 获取猪只类型信息,确保包含了营养需求 + pigType, err := r.GetPigTypeByID(serviceCtx, pigTypeID) + if err != nil { + return nil, fmt.Errorf("获取猪类型信息失败: %w", err) + } + + // 2. 获取所有原料,并区分有库存和无库存的原料 + // 获取有库存的原料 + hasStock := true + stockOpts := repository.RawMaterialListOptions{HasStock: &hasStock} + stockMaterials, _, err := r.ListRawMaterials(serviceCtx, stockOpts, 1, 9999) + if err != nil { + return nil, fmt.Errorf("获取有库存原料列表失败: %w", err) + } + + // 获取无库存的原料 + hasStock = false + noStockOpts := repository.RawMaterialListOptions{HasStock: &hasStock} + noStockMaterials, _, err := r.ListRawMaterials(serviceCtx, noStockOpts, 1, 9999) + if err != nil { + return nil, fmt.Errorf("获取无库存原料列表失败: %w", err) + } + + // 合并有库存和无库存的原料,作为所有原始原料的列表,用于后续计算最终参考价格 + allOriginalMaterials := make([]models.RawMaterial, 0, len(stockMaterials)+len(noStockMaterials)) + allOriginalMaterials = append(allOriginalMaterials, stockMaterials...) + allOriginalMaterials = append(allOriginalMaterials, noStockMaterials...) + + // 3. 创建一个用于配方生成的原料列表,并调整有库存原料的价格 + var materialsForGeneration []models.RawMaterial + + // 先添加有库存的原料,并调整价格 + for _, mat := range stockMaterials { + adjustedMat := mat // 复制一份 + // 大幅调低有库存原料的参考价格,诱导生成器优先使用 + adjustedMat.ReferencePrice = 0.01 // 设置一个非常小的价格 + materialsForGeneration = append(materialsForGeneration, adjustedMat) + logger.Debugf("原料 '%s' (ID: %d) 有库存,生成配方时参考价格调整为 %.2f", mat.Name, mat.ID, adjustedMat.ReferencePrice) + } + // 再添加无库存的原料,保持原价 + for _, mat := range noStockMaterials { + materialsForGeneration = append(materialsForGeneration, mat) + } + + // 4. 调用生成器生成配方 + recipe, err := r.GenerateRecipe(serviceCtx, *pigType, materialsForGeneration) + if err != nil { + return nil, fmt.Errorf("生成配方失败: %w", err) + } + + // 5. 丰富配方描述:计算并添加参考价格信息 + // 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格 + // rawMaterialMap 从 allOriginalMaterials 构建,确保使用原始价格 + rawMaterialMap := make(map[uint32]models.RawMaterial) + for _, mat := range allOriginalMaterials { + rawMaterialMap[mat.ID] = mat + } + + // 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本 + for i := range recipe.RecipeIngredients { + if rawMat, ok := rawMaterialMap[recipe.RecipeIngredients[i].RawMaterialID]; ok { + recipe.RecipeIngredients[i].RawMaterial = rawMat + } else { + logger.Warnf("未找到 RecipeIngredient (RawMaterialID: %d) 对应的原始 RawMaterial,成本计算可能不准确", recipe.RecipeIngredients[i].RawMaterialID) + } + } + + referencePrice := recipe.CalculateReferencePricePerKilogram() / 100 + recipe.Description = fmt.Sprintf("%s 计算时预估成本: %.2f元/kg。", recipe.Description, referencePrice) + + // 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。 + // 此时需要在描述中说明需要添加的廉价填充料的百分比。 + totalPercentage := recipe.CalculateTotalRawMaterialProportion() + if totalPercentage < 99.99 { // 允许微小的浮点误差 + fillerPercentage := 100 - totalPercentage + recipe.Description = fmt.Sprintf("%s 注意:配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage, fillerPercentage) + } + + // 6. 保存新生成的配方到数据库 + if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil { + + return nil, fmt.Errorf("保存生成的配方失败: %w", err) + } + logger.Infof("成功生成优先使用库存原料的配方: %+v", recipe) + + // 7. 返回创建的配方 + return recipe, nil +}