实现配方生成器
This commit is contained in:
@@ -12,7 +12,7 @@ server:
|
|||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
log:
|
log:
|
||||||
level: "debug" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
|
level: "info" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
|
||||||
format: "console" # 日志格式: "console" 或 "json"
|
format: "console" # 日志格式: "console" 或 "json"
|
||||||
enable_file: true # 是否启用文件日志
|
enable_file: true # 是否启用文件日志
|
||||||
file_path: "./app_logs/app.log" # 日志文件路径
|
file_path: "./app_logs/app.log" # 日志文件路径
|
||||||
|
|||||||
@@ -63,3 +63,4 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
|
|||||||
13. 重构配方领域
|
13. 重构配方领域
|
||||||
14. 配方增删改查服务层和控制器
|
14. 配方增删改查服务层和控制器
|
||||||
15. 实现库存管理相关逻辑
|
15. 实现库存管理相关逻辑
|
||||||
|
16. 实现配方生成器
|
||||||
1
go.mod
1
go.mod
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.43.0
|
||||||
|
gonum.org/v1/gonum v0.16.0
|
||||||
google.golang.org/protobuf v1.36.9
|
google.golang.org/protobuf v1.36.9
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
|||||||
2
go.sum
2
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/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 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
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 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
213
internal/domain/recipe/recipe_generate_manager.go
Normal file
213
internal/domain/recipe/recipe_generate_manager.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -132,6 +132,7 @@ internal/domain/recipe/pig_breed_service.go
|
|||||||
internal/domain/recipe/pig_type_service.go
|
internal/domain/recipe/pig_type_service.go
|
||||||
internal/domain/recipe/raw_material_service.go
|
internal/domain/recipe/raw_material_service.go
|
||||||
internal/domain/recipe/recipe_core_service.go
|
internal/domain/recipe/recipe_core_service.go
|
||||||
|
internal/domain/recipe/recipe_generate_manager.go
|
||||||
internal/domain/recipe/recipe_service.go
|
internal/domain/recipe/recipe_service.go
|
||||||
internal/domain/task/alarm_notification_task.go
|
internal/domain/task/alarm_notification_task.go
|
||||||
internal/domain/task/area_threshold_check_task.go
|
internal/domain/task/area_threshold_check_task.go
|
||||||
|
|||||||
Reference in New Issue
Block a user