From ba60ed541cb28a80ce23ad4def60ae409647dd82 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 20:23:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=85=8D=E6=96=B9=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yml | 2 +- design/recipe-management/index.md | 3 +- go.mod | 1 + go.sum | 2 + .../domain/recipe/recipe_generate_manager.go | 213 ++++++++++++++++++ project_structure.txt | 1 + 6 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 internal/domain/recipe/recipe_generate_manager.go diff --git a/config/config.yml b/config/config.yml index 92b0866..ab89f66 100644 --- a/config/config.yml +++ b/config/config.yml @@ -12,7 +12,7 @@ server: # 日志配置 log: - level: "debug" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal" + level: "info" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal" format: "console" # 日志格式: "console" 或 "json" enable_file: true # 是否启用文件日志 file_path: "./app_logs/app.log" # 日志文件路径 diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index a1cbae1..ce7e430 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -62,4 +62,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 12. 配方领域层方法 13. 重构配方领域 14. 配方增删改查服务层和控制器 -15. 实现库存管理相关逻辑 \ No newline at end of file +15. 实现库存管理相关逻辑 +16. 实现配方生成器 \ No newline at end of file diff --git a/go.mod b/go.mod index 34117dc..f2a4bd9 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/tidwall/gjson v1.18.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.43.0 + gonum.org/v1/gonum v0.16.0 google.golang.org/protobuf v1.36.9 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index a955b19..d2b2334 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go new file mode 100644 index 0000000..c5fa384 --- /dev/null +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -0,0 +1,213 @@ +package recipe + +import ( + "context" + "errors" + "fmt" + "math" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gonum.org/v1/gonum/mat" + "gonum.org/v1/gonum/optimize/convex/lp" +) + +// RecipeGenerateManager 定义了配方生成器的能力。 +// 它可以有多种实现,例如基于成本优化、基于生长性能优化等。 +type RecipeGenerateManager interface { + // GenerateRecipe 根据猪的营养需求和可用原料,生成一个配方。 + GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error) +} + +// recipeGenerateManagerImpl 是 RecipeGenerateManager 的默认实现。 +// 它实现了基于成本最优的配方生成逻辑。 +type recipeGenerateManagerImpl struct { + ctx context.Context +} + +// NewRecipeGenerateManager 创建一个默认的配方生成器实例。 +func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager { + return &recipeGenerateManagerImpl{ + ctx: ctx, + } +} + +// GenerateRecipe 根据猪的营养需求和可用原料,使用线性规划计算出成本最低的饲料配方。 +func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error) { + // 1. 基础校验 + if len(materials) == 0 { + return nil, errors.New("cannot generate recipe: no raw materials provided") + } + if len(pigType.PigNutrientRequirements) == 0 { + return nil, errors.New("cannot generate recipe: pig type has no nutrient requirements") + } + + // --------------------------------------------------------- + // 2. 准备数据结构 + // --------------------------------------------------------- + + // 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value + materialNutrients := make(map[uint32]map[uint32]float64) + // 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料) + materialIndex := make(map[uint32]int) + // 列表: 记录原料ID以便结果回溯 + materialIDs := make([]uint32, len(materials)) + + for i, m := range materials { + materialIndex[m.ID] = i + materialIDs[i] = m.ID + materialNutrients[m.ID] = make(map[uint32]float64) + for _, n := range m.RawMaterialNutrients { + // 注意:这里假设 float32 转 float64 精度足够 + materialNutrients[m.ID][n.NutrientID] = float64(n.Value) + } + } + + // 识别约束数量 + // 约束 1: 总重量 = 1 (100%) + // 约束 2..N: 营养素下限 (Min) + // 约束 N..M: 营养素上限 (Max, 仅当 Max > 0 时) + type constraintInfo struct { + isMax bool // true=上限约束(<=), false=下限约束(>=) + nutrientID uint32 + limit float64 + } + var constraints []constraintInfo + + // 添加营养约束 + for _, req := range pigType.PigNutrientRequirements { + // 下限约束 (Value >= Min) + // 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min + constraints = append(constraints, constraintInfo{ + isMax: false, + nutrientID: req.NutrientID, + limit: float64(req.MinRequirement), + }) + + // 上限约束 (Value <= Max) + // 逻辑: Sum(Mat * x) <= Max -> Sum(Mat * x) + slack = Max + if req.MaxRequirement > 0 { + // 简单的校验,如果 Min > Max 则是逻辑矛盾,直接报错 + if req.MinRequirement > req.MaxRequirement { + return nil, fmt.Errorf("invalid requirement for nutrient %d: min > max", req.NutrientID) + } + constraints = append(constraints, constraintInfo{ + isMax: true, + nutrientID: req.NutrientID, + limit: float64(req.MaxRequirement), + }) + } + } + + // --------------------------------------------------------- + // 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c) + // --------------------------------------------------------- + + // 变量总数 = 原料数量 + 松弛变量数量 + // 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量) + // 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b + numMaterials := len(materials) + numSlack := len(constraints) + numCols := numMaterials + numSlack + + // 行数 = 1 (总量约束) + 营养约束数量 + numRows := 1 + len(constraints) + + // A: 约束系数矩阵 + A := mat.NewDense(numRows, numCols, nil) + // b: 约束值向量 + b := make([]float64, numRows) + // c: 成本向量 (目标函数系数) + c := make([]float64, numCols) + + // --- 填充 c (成本) --- + for i, m := range materials { + c[i] = float64(m.ReferencePrice) + } + // 松弛变量的成本为 0,Go 默认初始化为 0,无需操作 + + // --- 填充 Row 0: 总量约束 (Sum(x) = 1) --- + // 系数: 所有原料对应列为 1,松弛变量列为 0 + for j := 0; j < numMaterials; j++ { + A.Set(0, j, 1.0) + } + b[0] = 1.0 + + // --- 填充营养约束行 --- + for i, cons := range constraints { + rowIndex := i + 1 // 0行被总量约束占用,所以从1开始 + slackColIndex := numMaterials + i // 松弛变量列紧跟在原料列之后 + + b[rowIndex] = cons.limit + + // 1. 设置原料系数 + for j, m := range materials { + // 获取该原料这种营养素的含量,如果没有则为0 + val := materialNutrients[m.ID][cons.nutrientID] + A.Set(rowIndex, j, val) + } + + // 2. 设置松弛变量系数 + // 如果是下限 (>=): Sum - s = Limit => s系数为 -1 + // 如果是上限 (<=): Sum + s = Limit => s系数为 +1 + if cons.isMax { + A.Set(rowIndex, slackColIndex, 1.0) + } else { + A.Set(rowIndex, slackColIndex, -1.0) + } + } + + // --------------------------------------------------------- + // 4. 执行单纯形法求解 + // --------------------------------------------------------- + + // lp.Simplex 求解: minimize c^T * x subject to A * x = b, x >= 0 + optVal, x, err := lp.Simplex(c, A, b, 1e-8, nil) + + if err != nil { + if errors.Is(err, lp.ErrInfeasible) { + return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解)") + } + if errors.Is(err, lp.ErrUnbounded) { + return nil, errors.New("计算错误:解无界 (可能数据配置有误)") + } + return nil, fmt.Errorf("配方计算失败: %w", err) + } + + // --------------------------------------------------------- + // 5. 结果解析与构建 + // --------------------------------------------------------- + + recipe := &models.Recipe{ + Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), + Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f", len(materials), optVal), + RecipeIngredients: []models.RecipeIngredient{}, + } + + // 遍历原料部分的解 (前 numMaterials 个变量) + totalPercentage := 0.0 + for i := 0; i < numMaterials; i++ { + proportion := x[i] + + // 忽略极小值 (浮点数误差) + if proportion < 1e-6 { + continue + } + + // 记录总和用于最后的校验 + totalPercentage += proportion + + recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{ + RawMaterialID: materialIDs[i], + // 数据库可能需要 RawMaterial 对象,这里只填ID,由调用方或ORM处理加载 + // 比例: float64 -> float32 + Percentage: float32(proportion), + }) + } + + // 二次校验: 确保总量约为 1 + if math.Abs(totalPercentage-1.0) > 1e-3 { + return nil, fmt.Errorf("计算结果异常:原料总量不为 100%% (计算值: %.4f)", totalPercentage) + } + + return recipe, nil +} diff --git a/project_structure.txt b/project_structure.txt index 01895d2..5ef3c45 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -132,6 +132,7 @@ internal/domain/recipe/pig_breed_service.go internal/domain/recipe/pig_type_service.go internal/domain/recipe/raw_material_service.go internal/domain/recipe/recipe_core_service.go +internal/domain/recipe/recipe_generate_manager.go internal/domain/recipe/recipe_service.go internal/domain/task/alarm_notification_task.go internal/domain/task/area_threshold_check_task.go