Compare commits

...

13 Commits

Author SHA1 Message Date
e6b307b0dc 修bug 2025-11-27 16:42:32 +08:00
b8e0301175 修正数据错误 2025-11-27 16:27:49 +08:00
e2da441a6d 修正数据错误 2025-11-27 15:52:38 +08:00
dca6cc5dd4 原料增加最大添加量限制 2025-11-27 00:39:01 +08:00
5c99ff7475 原料增加最大添加量限制 2025-11-26 22:56:24 +08:00
0283c250e4 重构seeder 2025-11-26 22:51:58 +08:00
5bd52df240 优化算法 2025-11-26 22:41:38 +08:00
5ad403bf86 增加原料添加量限制 2025-11-26 22:35:52 +08:00
ce3844957f 修复逻辑错误 2025-11-26 22:13:51 +08:00
6c0f655d0a 增加删除原料校验 2025-11-26 21:14:32 +08:00
29b820b846 优化展示 2025-11-26 21:08:34 +08:00
34311889e8 实现使用系统中所有可用的原料一键生成配方 2025-11-26 20:44:41 +08:00
ba60ed541c 实现配方生成器 2025-11-26 20:23:29 +08:00
26 changed files with 1734 additions and 950 deletions

View File

@@ -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" # 日志文件路径

View File

@@ -1741,327 +1741,408 @@
"raw_materials": {
"DL-蛋氨酸98": {
"descriptions": "饲料级合成蛋氨酸几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。",
"unit_price": 21.50
"unit_price": 21.50,
"max_ratio": 100.00
},
"L-色氨酸98": {
"descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。",
"unit_price": 68.00
"unit_price": 68.00,
"max_ratio": 100.00
},
"L-苏氨酸98": {
"descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。",
"unit_price": 10.80
"unit_price": 10.80,
"max_ratio": 100.00
},
"L-赖氨酸HCl 98": {
"descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。",
"unit_price": 11.20
"unit_price": 11.20,
"max_ratio": 100.00
},
"乳清粉": {
"descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。",
"unit_price": 6.50
"unit_price": 6.50,
"max_ratio": 40.00
},
"兔肉粉": {
"descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。",
"unit_price": 11.50
"unit_price": 11.50,
"max_ratio": 20.00
},
"全株玉米青贮": {
"descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。",
"unit_price": 0.45
"unit_price": 0.45,
"max_ratio": 30.00
},
"双低菜籽粕": {
"descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。",
"unit_price": 2.40
"unit_price": 2.40,
"max_ratio": 35.00
},
"向日葵籽": {
"descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。",
"unit_price": 5.80
"unit_price": 5.80,
"max_ratio": 20.00
},
"啤酒糟干": {
"descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。",
"unit_price": 1.90
"unit_price": 1.90,
"max_ratio": 30.00
},
"啤酒花渣": {
"descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。",
"unit_price": 0.60
"unit_price": 0.60,
"max_ratio": 10.00
},
"国产鱼粉60": {
"descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。",
"unit_price": 9.50
"unit_price": 9.50,
"max_ratio": 15.00
},
"土豆蛋白": {
"descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。",
"unit_price": 8.50
"unit_price": 8.50,
"max_ratio": 100.00
},
"大豆油": {
"descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。",
"unit_price": 8.20
"unit_price": 8.20,
"max_ratio": 10.00
},
"大豆粕44": {
"descriptions": "普通豆粕蛋白43.8%左右抗营养因子较高需关注脲酶和KOH溶解度。",
"unit_price": 3.05
"unit_price": 3.05,
"max_ratio": 65.00
},
"大豆粕46": {
"descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。",
"unit_price": 3.25
"unit_price": 3.25,
"max_ratio": 65.00
},
"大豆粕48": {
"descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。",
"unit_price": 3.60
"unit_price": 3.60,
"max_ratio": 65.00
},
"大麦": {
"descriptions": "能量稍低于玉米纤维较高可部分替代玉米注意DON毒素风险。",
"unit_price": 2.10
"unit_price": 2.10,
"max_ratio": 60.00
},
"小苏打": {
"descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。",
"unit_price": 1.60
"unit_price": 1.60,
"max_ratio": 2.00
},
"小麦": {
"descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。",
"unit_price": 2.55
"unit_price": 2.55,
"max_ratio": 60.00
},
"小麦次粉": {
"descriptions": "小麦加工副产品蛋白和磷较高但DON和ZEN风险高限量使用。",
"unit_price": 2.20
"unit_price": 2.20,
"max_ratio": 25.00
},
"小麦麸": {
"descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。",
"unit_price": 1.75
"unit_price": 1.75,
"max_ratio": 40.00
},
"木薯干": {
"descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。",
"unit_price": 2.05
"unit_price": 2.05,
"max_ratio": 50.00
},
"杂交构树叶粉": {
"descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。",
"unit_price": 2.20
"unit_price": 2.20,
"max_ratio": 5.00
},
"构树叶粉(老叶高纤维)": {
"descriptions": "老叶构树粉,纤维更高,适合母猪粗饲料使用。",
"unit_price": 1.50
"unit_price": 1.50,
"max_ratio": 10.00
},
"柠檬酸渣": {
"descriptions": "湿态副产品,适口性好,可用于母猪料降低成本。",
"unit_price": 0.50
"unit_price": 0.50,
"max_ratio": 5.00
},
"棉籽粕": {
"descriptions": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。",
"unit_price": 2.80
"unit_price": 2.80,
"max_ratio": 3.00
},
"棕榈油": {
"descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。",
"unit_price": 8.50
"unit_price": 8.50,
"max_ratio": 10.00
},
"棕榈粕": {
"descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。",
"unit_price": 1.60
"unit_price": 1.60,
"max_ratio": 20.00
},
"椰子粕": {
"descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。",
"unit_price": 2.30
"unit_price": 2.30,
"max_ratio": 25.00
},
"燕麦": {
"descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。",
"unit_price": 3.20
"unit_price": 3.20,
"max_ratio": 20.00
},
"燕麦草": {
"descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。",
"unit_price": 2.60
"unit_price": 2.60,
"max_ratio": 20.00
},
"猪肺粉": {
"descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。",
"unit_price": 9.00
"unit_price": 9.00,
"max_ratio": 20.00
},
"玉米": {
"descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。",
"unit_price": 2.30
"unit_price": 2.30,
"max_ratio": 100.00
},
"玉米DDGS": {
"descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。",
"unit_price": 2.15
"unit_price": 2.15,
"max_ratio": 40.00
},
"玉米油": {
"descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。",
"unit_price": 9.50
"unit_price": 9.50,
"max_ratio": 10.00
},
"玉米胚芽粕": {
"descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。",
"unit_price": 2.05
"unit_price": 2.05,
"max_ratio": 25.00
},
"玉米蛋白粉60": {
"descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。",
"unit_price": 4.80
"unit_price": 4.80,
"max_ratio": 20.00
},
"玉米青贮": {
"descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。",
"unit_price": 0.40
"unit_price": 0.40,
"max_ratio": 30.00
},
"瓜子粕": {
"descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。",
"unit_price": 2.10
"unit_price": 2.10,
"max_ratio": 35.00
},
"甜菜粕": {
"descriptions": "高可溶性纤维,母猪极佳的防便秘原料。",
"unit_price": 1.95
"unit_price": 1.95,
"max_ratio": 30.00
},
"石粉": {
"descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。",
"unit_price": 0.18
"unit_price": 0.18,
"max_ratio": 8.00
},
"碎米": {
"descriptions": "能量接近玉米,蛋白稍低,适口性好。",
"unit_price": 2.80
"unit_price": 2.80,
"max_ratio": 60.00
},
"磷酸氢钙": {
"descriptions": "猪最常用磷钙来源,有效磷高。",
"unit_price": 3.20
"unit_price": 3.20,
"max_ratio": 8.00
},
"稻草粉": {
"descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。",
"unit_price": 0.60
"unit_price": 0.60,
"max_ratio": 20.00
},
"稻谷": {
"descriptions": "带壳稻子,能量低于玉米,纤维高。",
"unit_price": 1.90
"unit_price": 1.90,
"max_ratio": 30.00
},
"稻谷糠": {
"descriptions": "米糠的一种,高脂肪高磷,需注意酸败。",
"unit_price": 1.60
"unit_price": 1.60,
"max_ratio": 20.00
},
"米糠": {
"descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。",
"unit_price": 1.85
"unit_price": 1.85,
"max_ratio": 25.00
},
"米糠粕": {
"descriptions": "脱脂米糠,蛋白较高,能量降低。",
"unit_price": 1.95
"unit_price": 1.95,
"max_ratio": 25.00
},
"红薯干": {
"descriptions": "高淀粉低蛋白能量原料,类似木薯。",
"unit_price": 2.20
"unit_price": 2.20,
"max_ratio": 50.00
},
"肉粉": {
"descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。",
"unit_price": 4.50
"unit_price": 4.50,
"max_ratio": 20.00
},
"肉骨粉50": {
"descriptions": "含骨较高,钙磷比例好,但蛋白较低。",
"unit_price": 4.20
"unit_price": 4.20,
"max_ratio": 20.00
},
"脱脂奶粉": {
"descriptions": "优质乳蛋白源,仔猪料黄金原料。",
"unit_price": 22.00
"unit_price": 22.00,
"max_ratio": 30.00
},
"膨化全脂大豆": {
"descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。",
"unit_price": 4.10
"unit_price": 4.10,
"max_ratio": 30.00
},
"芝麻粕": {
"descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。",
"unit_price": 2.90
"unit_price": 2.90,
"max_ratio": 20.00
},
"花生秧粉": {
"descriptions": "粗饲料,母猪用。",
"unit_price": 0.85
"unit_price": 0.85,
"max_ratio": 20.00
},
"花生粕": {
"descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。",
"unit_price": 3.70
"unit_price": 3.70,
"max_ratio": 5.00
},
"苜蓿草块": {
"descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。",
"unit_price": 2.40
"unit_price": 2.40,
"max_ratio": 20.00
},
"苜蓿草粉": {
"descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。",
"unit_price": 2.50
"unit_price": 2.50,
"max_ratio": 20.00
},
"苹果渣": {
"descriptions": "湿态副产品,适口性好,母猪喜欢。",
"unit_price": 0.55
"unit_price": 0.55,
"max_ratio": 10.00
},
"菜籽粕": {
"descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。",
"unit_price": 2.30
"unit_price": 2.30,
"max_ratio": 15.00
},
"葡萄糖": {
"descriptions": "快速能量源,教槽料常用,缓解应激。",
"unit_price": 3.80
"unit_price": 3.80,
"max_ratio": 15.00
},
"葵花籽粕": {
"descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。",
"unit_price": 2.10
"unit_price": 2.10,
"max_ratio": 35.00
},
"蔗糖": {
"descriptions": "高能量碳水,教槽料诱食用。",
"unit_price": 6.50
"unit_price": 6.50,
"max_ratio": 10.00
},
"虾粉": {
"descriptions": "优质动物蛋白,含虾青素,改善体色。",
"unit_price": 6.00
"unit_price": 6.00,
"max_ratio": 10.00
},
"蚕蛹粉": {
"descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。",
"unit_price": 8.00
"unit_price": 8.00,
"max_ratio": 15.00
},
"蚕豆": {
"descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。",
"unit_price": 3.40
"unit_price": 3.40,
"max_ratio": 50.00
},
"蟹粉": {
"descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。",
"unit_price": 4.50
"unit_price": 4.50,
"max_ratio": 10.00
},
"血浆蛋白粉": {
"descriptions": "仔猪断奶料黄金功能性蛋白IgG高促进肠道发育和免疫。",
"unit_price": 45.00
"unit_price": 45.00,
"max_ratio": 5.00
},
"血粉": {
"descriptions": "赖氨酸极高,但适口性差,需喷涂使用。",
"unit_price": 6.50
"unit_price": 6.50,
"max_ratio": 5.00
},
"豆磷脂": {
"descriptions": "高能量乳化剂,促进脂肪消化,改善皮毛。",
"unit_price": 6.80
"unit_price": 6.80,
"max_ratio": 1.00
},
"豌豆": {
"descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。",
"unit_price": 3.50
"unit_price": 3.50,
"max_ratio": 50.00
},
"豌豆蛋白": {
"descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。",
"unit_price": 9.50
"unit_price": 9.50,
"max_ratio": 25.00
},
"进口鱼粉65": {
"descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。",
"unit_price": 12.80
"unit_price": 12.80,
"max_ratio": 30.00
},
"食盐": {
"descriptions": "提供钠和氯,调节电解质平衡。",
"unit_price": 0.50
"unit_price": 0.50,
"max_ratio": 1.00
},
"饲料酵母粉": {
"descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。",
"unit_price": 6.50
"unit_price": 6.50,
"max_ratio": 20.00
},
"高粱": {
"descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。",
"unit_price": 2.20
"unit_price": 2.20,
"max_ratio": 50.00
},
"鱼油": {
"descriptions": "富含DHA和EPA促进脑发育和抗炎母猪和仔猪推荐。",
"unit_price": 18.00
"unit_price": 18.00,
"max_ratio": 3.00
},
"鸡肉粉": {
"descriptions": "优质陆基动物蛋白,消化率高,适口性好。",
"unit_price": 7.50
"unit_price": 7.50,
"max_ratio": 25.00
},
"鸭肉粉": {
"descriptions": "与鸡肉粉类似,脂肪稍高。",
"unit_price": 7.20
"unit_price": 7.20,
"max_ratio": 20.00
},
"鹅肉粉": {
"descriptions": "蛋白和脂肪中等,质量稳定。",
"unit_price": 7.00
"unit_price": 7.00,
"max_ratio": 20.00
}
},
"nutrients": {

View File

@@ -4,56 +4,56 @@
"杜长大 (DLY)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.012,
"max_requirement": 0.015
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0072,
"max_requirement": 0.0105
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0078,
"max_requirement": 0.0108
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0022,
"max_requirement": 0.0030
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.22
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.009,
"max_requirement": 0.012
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0045
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -67,56 +67,56 @@
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0094,
"max_requirement": 0.0110
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0055,
"max_requirement": 0.0073
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0058,
"max_requirement": 0.0077
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0016,
"max_requirement": 0.0022
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 0.16,
"max_requirement": 0.18
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.007,
"max_requirement": 0.009
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0040
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -130,56 +130,56 @@
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0081,
"max_requirement": 0.0090
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.0058
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0048,
"max_requirement": 0.0061
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0013,
"max_requirement": 0.0018
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 0.14,
"max_requirement": 0.16
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.006
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.0018,
"max_requirement": 0.0035
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -193,56 +193,56 @@
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0053,
"max_requirement": 0.0065
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0030,
"max_requirement": 0.0041
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0031,
"max_requirement": 0.0043
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0010,
"max_requirement": 0.0013
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 0.12,
"max_requirement": 0.14
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.004,
"max_requirement": 0.0055
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0030
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -258,56 +258,56 @@
"杜大长 (DYL)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.012,
"max_requirement": 0.015
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0072,
"max_requirement": 0.0105
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0078,
"max_requirement": 0.0108
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0022,
"max_requirement": 0.0030
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.22
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.009,
"max_requirement": 0.012
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0045
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -321,56 +321,56 @@
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0094,
"max_requirement": 0.0110
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0055,
"max_requirement": 0.0073
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0058,
"max_requirement": 0.0077
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0016,
"max_requirement": 0.0022
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 0.16,
"max_requirement": 0.18
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.007,
"max_requirement": 0.009
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0040
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -384,56 +384,56 @@
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0081,
"max_requirement": 0.0090
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.0058
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0048,
"max_requirement": 0.0061
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0013,
"max_requirement": 0.0018
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 0.14,
"max_requirement": 0.16
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.006
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.0018,
"max_requirement": 0.0035
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -447,56 +447,56 @@
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0053,
"max_requirement": 0.0065
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0030,
"max_requirement": 0.0041
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0031,
"max_requirement": 0.0043
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0010,
"max_requirement": 0.0013
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 0.12,
"max_requirement": 0.14
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.004,
"max_requirement": 0.0055
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0030
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -512,56 +512,56 @@
"皮长大 (PLY)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.012,
"max_requirement": 0.015
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0072,
"max_requirement": 0.0105
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0078,
"max_requirement": 0.0108
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0022,
"max_requirement": 0.0030
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.22
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.009,
"max_requirement": 0.012
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0045
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -575,56 +575,56 @@
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0094,
"max_requirement": 0.0110
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0055,
"max_requirement": 0.0073
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0058,
"max_requirement": 0.0077
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0016,
"max_requirement": 0.0022
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 0.16,
"max_requirement": 0.18
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.007,
"max_requirement": 0.009
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.002,
"max_requirement": 0.0040
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -638,56 +638,56 @@
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0081,
"max_requirement": 0.0090
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.0058
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0048,
"max_requirement": 0.0061
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0013,
"max_requirement": 0.0018
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 0.14,
"max_requirement": 0.16
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.006,
"max_requirement": 0.008
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.0045,
"max_requirement": 0.006
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.0018,
"max_requirement": 0.0035
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -701,56 +701,56 @@
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.0053,
"max_requirement": 0.0065
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.0030,
"max_requirement": 0.0041
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.0031,
"max_requirement": 0.0043
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.0010,
"max_requirement": 0.0013
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 0.12,
"max_requirement": 0.14
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 0.03,
"max_requirement": 0.06
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 0.02,
"max_requirement": 0.06
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.005,
"max_requirement": 0.007
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.004,
"max_requirement": 0.0055
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0030
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.0015,
"max_requirement": 0.0025
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.0025,
"max_requirement": 0.0045
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
@@ -911,4 +911,4 @@
}
}
}
}
}

View File

@@ -62,4 +62,6 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
12. 配方领域层方法
13. 重构配方领域
14. 配方增删改查服务层和控制器
15. 实现库存管理相关逻辑
15. 实现库存管理相关逻辑
16. 实现配方生成器
17. 实现使用系统中所有可用的原料一键生成配方

View File

@@ -3145,6 +3145,52 @@ const docTemplate = `{
}
}
},
"/api/v1/feed/recipes/generate-from-all-materials/{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": [
@@ -3669,7 +3715,6 @@ const docTemplate = `{
},
{
"enum": [
7,
-1,
0,
1,
@@ -3679,12 +3724,12 @@ const docTemplate = `{
5,
-1,
5,
6
6,
7
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -3694,7 +3739,8 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
"InvalidLevel",
"_numLevels"
],
"name": "level",
"in": "query"
@@ -7439,6 +7485,23 @@ const docTemplate = `{
}
}
},
"dto.GenerateRecipeResponse": {
"type": "object",
"properties": {
"description": {
"description": "新生成的配方描述",
"type": "string"
},
"id": {
"description": "新生成的配方ID",
"type": "integer"
},
"name": {
"description": "新生成的配方名称",
"type": "string"
}
}
},
"dto.HistoricalAlarmDTO": {
"type": "object",
"properties": {
@@ -10523,7 +10586,6 @@ const docTemplate = `{
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -10533,10 +10595,10 @@ const docTemplate = `{
5,
-1,
5,
6
6,
7
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -10546,7 +10608,8 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
"InvalidLevel",
"_numLevels"
]
}
},

View File

@@ -3137,6 +3137,52 @@
}
}
},
"/api/v1/feed/recipes/generate-from-all-materials/{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": [
@@ -3661,7 +3707,6 @@
},
{
"enum": [
7,
-1,
0,
1,
@@ -3671,12 +3716,12 @@
5,
-1,
5,
6
6,
7
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -3686,7 +3731,8 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
"InvalidLevel",
"_numLevels"
],
"name": "level",
"in": "query"
@@ -7431,6 +7477,23 @@
}
}
},
"dto.GenerateRecipeResponse": {
"type": "object",
"properties": {
"description": {
"description": "新生成的配方描述",
"type": "string"
},
"id": {
"description": "新生成的配方ID",
"type": "integer"
},
"name": {
"description": "新生成的配方名称",
"type": "string"
}
}
},
"dto.HistoricalAlarmDTO": {
"type": "object",
"properties": {
@@ -10515,7 +10578,6 @@
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -10525,10 +10587,10 @@
5,
-1,
5,
6
6,
7
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -10538,7 +10600,8 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel"
"InvalidLevel",
"_numLevels"
]
}
},

View File

@@ -565,6 +565,18 @@ definitions:
thresholds:
type: number
type: object
dto.GenerateRecipeResponse:
properties:
description:
description: 新生成的配方描述
type: string
id:
description: 新生成的配方ID
type: integer
name:
description: 新生成的配方名称
type: string
type: object
dto.HistoricalAlarmDTO:
properties:
alarm_code:
@@ -2719,7 +2731,6 @@ definitions:
- PlanTypeFilterSystem
zapcore.Level:
enum:
- 7
- -1
- 0
- 1
@@ -2730,10 +2741,10 @@ definitions:
- -1
- 5
- 6
- 7
format: int32
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -2744,6 +2755,7 @@ definitions:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
info:
contact:
email: divano@example.com
@@ -4722,6 +4734,32 @@ paths:
summary: 更新配方
tags:
- 饲料管理-配方
/api/v1/feed/recipes/generate-from-all-materials/{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:
@@ -4947,7 +4985,6 @@ paths:
name: end_time
type: string
- enum:
- 7
- -1
- 0
- 1
@@ -4958,12 +4995,12 @@ paths:
- -1
- 5
- 6
- 7
format: int32
in: query
name: level
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -4974,6 +5011,7 @@ paths:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
- enum:
- 邮件
- 企业微信

1
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -259,6 +259,7 @@ func (a *API) setupRoutes() {
feedGroup.DELETE("/recipes/:id", a.recipeController.DeleteRecipe)
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)
}
logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)")

View File

@@ -194,3 +194,34 @@ func (c *RecipeController) ListRecipes(ctx echo.Context) error {
logger.Infof("%s: 获取配方列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取配方列表成功", resp, actionType, "获取配方列表成功", resp)
}
// GenerateFromAllMaterials 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-from-all-materials/{pig_type_id} [post]
func (c *RecipeController) GenerateFromAllMaterials(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GenerateFromAllMaterials")
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.GenerateRecipeWithAllRawMaterials(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)
}

View File

@@ -66,6 +66,7 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse {
Name: rm.Name,
Description: rm.Description,
ReferencePrice: rm.ReferencePrice,
MaxAdditionRatio: rm.MaxAdditionRatio,
RawMaterialNutrients: rawMaterialNutrientDTOs,
}
}
@@ -280,3 +281,15 @@ func ConvertUpdateRecipeRequestToModel(req *UpdateRecipeRequest) *models.Recipe
RecipeIngredients: ingredients,
}
}
// ToGenerateRecipeResponse 将 models.Recipe 转换为 GenerateRecipeResponse DTO
func ToGenerateRecipeResponse(recipe *models.Recipe) *GenerateRecipeResponse {
if recipe == nil {
return nil
}
return &GenerateRecipeResponse{
ID: recipe.ID,
Name: recipe.Name,
Description: recipe.Description,
}
}

View File

@@ -52,16 +52,18 @@ type ListNutrientResponse struct {
// CreateRawMaterialRequest 创建原料的请求体
type CreateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
MaxAdditionRatio float32 `json:"max_addition_ratio"` // 最大添加比例
}
// UpdateRawMaterialRequest 更新原料的请求体
type UpdateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
MaxAdditionRatio *float32 `json:"max_addition_ratio"` // 最大添加比例
}
// RawMaterialNutrientDTO 原料营养素响应体
@@ -78,6 +80,7 @@ type RawMaterialResponse struct {
Name string `json:"name"`
Description string `json:"description"`
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
MaxAdditionRatio float32 `json:"max_addition_ratio"` // 最大添加比例
RawMaterialNutrients []RawMaterialNutrientDTO `json:"raw_material_nutrients"` // 关联的营养素信息
}
@@ -325,3 +328,10 @@ type ListRecipeResponse struct {
List []RecipeResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// GenerateRecipeResponse 是一键生成配方的响应体
type GenerateRecipeResponse struct {
ID uint32 `json:"id"` // 新生成的配方ID
Name string `json:"name"` // 新生成的配方名称
Description string `json:"description"` // 新生成的配方描述
}

View File

@@ -46,7 +46,7 @@ func NewRawMaterialService(ctx context.Context, recipeSvc recipe.Service) RawMat
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto.CreateRawMaterialRequest) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial")
rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description, req.ReferencePrice)
rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description, req.ReferencePrice, req.MaxAdditionRatio)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNameConflict) {
return nil, ErrRawMaterialNameConflict
@@ -61,7 +61,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, req *dto.UpdateRawMaterialRequest) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial")
rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description, req.ReferencePrice)
rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description, req.ReferencePrice, req.MaxAdditionRatio)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return nil, ErrRawMaterialNotFound

View File

@@ -25,22 +25,31 @@ type RecipeService interface {
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)
// GenerateRecipeWithAllRawMaterials 添加新方法
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
}
// recipeServiceImpl 是 RecipeService 接口的实现
type recipeServiceImpl struct {
ctx context.Context
recipeSvc recipe.RecipeCoreService
recipeSvc recipe.Service
}
// NewRecipeService 创建一个新的 RecipeService 实例
func NewRecipeService(ctx context.Context, recipeSvc recipe.RecipeCoreService) RecipeService {
func NewRecipeService(ctx context.Context, recipeSvc recipe.Service) RecipeService {
return &recipeServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// GenerateRecipeWithAllRawMaterials 实现新方法
func (s *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GenerateRecipeWithAllRawMaterials")
// 直接调用领域服务的方法
return s.recipeSvc.GenerateRecipeWithAllRawMaterials(serviceCtx, pigTypeID)
}
// CreateRecipe 创建配方
func (s *recipeServiceImpl) CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe")

View File

@@ -228,6 +228,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
pigTypeService := recipe.NewPigTypeService(logs.AddCompName(baseCtx, "PigTypeService"), infra.repos.unitOfWork, infra.repos.pigTypeRepo)
rawMaterialService := recipe.NewRawMaterialService(logs.AddCompName(baseCtx, "RawMaterialService"), infra.repos.unitOfWork, infra.repos.rawMaterialRepo, inventoryService)
recipeCoreService := recipe.NewRecipeCoreService(logs.AddCompName(baseCtx, "RecipeCoreService"), infra.repos.unitOfWork, infra.repos.recipeRepo)
recipeGenerateManager := recipe.NewRecipeGenerateManager(logs.AddCompName(baseCtx, "RecipeGenerateManager"))
recipeService := recipe.NewRecipeService(
logs.AddCompName(baseCtx, "RecipeService"),
nutrientService,
@@ -236,6 +237,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
pigAgeStageService,
pigTypeService,
recipeCoreService,
recipeGenerateManager,
)
return &DomainServices{

View File

@@ -21,15 +21,16 @@ type StockQuerier interface {
// 定义领域特定的错误
var (
ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在")
ErrRawMaterialNotFound = fmt.Errorf("原料不存在")
ErrStockNotEmpty = fmt.Errorf("原料尚有库存,无法删除")
ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在")
ErrRawMaterialNotFound = fmt.Errorf("原料不存在")
ErrStockNotEmpty = fmt.Errorf("原料尚有库存,无法删除")
ErrRawMaterialInUseByRecipe = fmt.Errorf("原料已被配方使用,无法删除")
)
// RawMaterialService 定义了原料领域的核心业务服务接口
type RawMaterialService interface {
CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error)
UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error)
CreateRawMaterial(ctx context.Context, name, description string, referencePrice, maxAdditionRatio float32) (*models.RawMaterial, error)
UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32, maxAdditionRatio *float32) (*models.RawMaterial, error)
DeleteRawMaterial(ctx context.Context, id uint32) error
GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error)
ListRawMaterials(ctx context.Context, opts repository.RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error)
@@ -55,7 +56,7 @@ func NewRawMaterialService(ctx context.Context, uow repository.UnitOfWork, rawMa
}
// CreateRawMaterial 实现了创建原料的核心业务逻辑
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error) {
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice, maxAdditionRatio float32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial")
// 检查名称是否已存在
@@ -68,9 +69,10 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de
}
rawMaterial := &models.RawMaterial{
Name: name,
Description: description,
ReferencePrice: referencePrice,
Name: name,
Description: description,
ReferencePrice: referencePrice,
MaxAdditionRatio: maxAdditionRatio,
}
if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil {
@@ -81,7 +83,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de
}
// UpdateRawMaterial 实现了更新原料的核心业务逻辑
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error) {
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32, maxAdditionRatio *float32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial")
// 检查要更新的实体是否存在
@@ -107,6 +109,9 @@ func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint3
rawMaterial.Name = name
rawMaterial.Description = description
rawMaterial.ReferencePrice = referencePrice
if maxAdditionRatio != nil {
rawMaterial.MaxAdditionRatio = *maxAdditionRatio
}
if err := s.rawMaterialRepo.UpdateRawMaterial(serviceCtx, rawMaterial); err != nil {
return nil, fmt.Errorf("更新原料失败: %w", err)
@@ -138,6 +143,15 @@ func (s *rawMaterialServiceImpl) DeleteRawMaterial(ctx context.Context, id uint3
return ErrStockNotEmpty
}
// 检查原料是否被配方使用
isUsed, err := s.rawMaterialRepo.IsRawMaterialUsedInRecipes(serviceCtx, id)
if err != nil {
return fmt.Errorf("检查原料是否被配方使用失败: %w", err)
}
if isUsed {
return ErrRawMaterialInUseByRecipe
}
if err := s.rawMaterialRepo.DeleteRawMaterial(serviceCtx, id); err != nil {
return fmt.Errorf("删除原料失败: %w", err)
}

View File

@@ -0,0 +1,338 @@
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,
}
}
const (
// internalFillerRawMaterialName 是内部虚拟填充料的名称。
// 该填充料用于线性规划计算确保总比例为100%,但不会出现在最终配方中。
internalFillerRawMaterialName = "内部填充料_InternalFiller"
// internalFillerNutrientID 是内部虚拟填充营养素的ID。
// 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值用于关联填充料。
internalFillerNutrientID = math.MaxUint32
)
// 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("无法生成配方:未提供任何原料")
}
if len(pigType.PigNutrientRequirements) == 0 {
return nil, errors.New("无法生成配方:猪类型未设置营养需求")
}
// 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的)。
// 用于后续过滤掉完全不相关的原料。
requiredNutrientIDs := make(map[uint32]bool)
for _, req := range pigType.PigNutrientRequirements {
requiredNutrientIDs[req.NutrientID] = true
}
// 过滤掉那些不包含猪类型任何所需营养素的原料。
var filteredMaterials []models.RawMaterial
for _, mat := range materials {
hasRelevantNutrient := false
for _, matNut := range mat.RawMaterialNutrients {
// 检查原料是否包含猪类型所需的任何营养素
if requiredNutrientIDs[matNut.NutrientID] {
hasRelevantNutrient = true
break
}
}
// 如果原料包含至少一个猪类型需求的营养素,则保留
if hasRelevantNutrient {
filteredMaterials = append(filteredMaterials, mat)
}
}
materials = filteredMaterials // 使用过滤后的原料列表
if len(materials) == 0 {
return nil, errors.New("无法生成配方:所有提供的原料都不包含猪类型所需的任何营养素,请检查原料配置或猪类型营养需求")
}
// 创建一个虚拟的、价格为0、不含任何实际营养素的填充料。
// 其唯一目的是在LP求解中作为“凑数”的选项确保总比例为100%,且不影响实际配方成本。
fillerRawMaterial := models.RawMaterial{
Model: models.Model{
ID: math.MaxUint32 - 1, // 使用一个极大的、不可能与实际原料ID冲突的值
},
Name: internalFillerRawMaterialName,
Description: "内部虚拟填充料用于线性规划凑足100%比例不含实际营养价格为0。",
ReferencePrice: 0.0, // 价格为0确保LP优先选择它来凑数
RawMaterialNutrients: []models.RawMaterialNutrient{
{
NutrientID: internalFillerNutrientID, // 关联一个虚拟营养素确保其在LP中被识别但其含量为0
Value: 0.0,
},
},
}
materials = append(materials, fillerRawMaterial) // 将填充料添加到原料列表中
// ---------------------------------------------------------
// 2. 准备数据结构
// ---------------------------------------------------------
// materialNutrients 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value
materialNutrients := make(map[uint32]map[uint32]float64)
// materialIndex 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料)
materialIndex := make(map[uint32]int)
// materialIDs 列表: 记录原料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)
}
}
// nutrientConstraints 存储营养素的下限和上限约束信息。
type nutrientConstraintInfo struct {
isMax bool // true=上限约束(<=), false=下限约束(>=)
nutrientID uint32
limit float64
}
var nutrientConstraints []nutrientConstraintInfo
// 添加营养约束
for _, req := range pigType.PigNutrientRequirements {
// 排除内部虚拟填充营养素的约束,因为它不应有实际需求
if req.NutrientID == internalFillerNutrientID {
continue
}
// 添加下限约束 (Value >= Min)
// 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min
nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
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("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement)
}
nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
isMax: true,
nutrientID: req.NutrientID,
limit: float64(req.MaxRequirement),
})
}
}
// maxAdditionConstraints 存储每个原料的最大添加比例约束 (x_i <= limit)。
type maxAdditionConstraintInfo struct {
materialColIndex int // 原料在 A 矩阵中的列索引
limit float64
}
var maxAdditionConstraints []maxAdditionConstraintInfo
// 遍历所有原料,包括填充料,添加 MaxAdditionRatio 约束
for _, mat := range materials {
// 填充料不应受 MaxAdditionRatio 限制
if mat.ID == fillerRawMaterial.ID {
continue
}
// 只有当 MaxAdditionRatio > 0 时才添加约束。
// 如果 MaxAdditionRatio 为 0 或负数,则表示该原料没有最大添加比例限制。
if mat.MaxAdditionRatio > 0 {
materialColIndex, ok := materialIndex[mat.ID]
if !ok {
return nil, fmt.Errorf("内部错误:未找到原料 %d (%s) 的列索引", mat.ID, mat.Name)
}
maxAdditionConstraints = append(maxAdditionConstraints, maxAdditionConstraintInfo{
materialColIndex: materialColIndex,
limit: float64(mat.MaxAdditionRatio) / 100.0,
})
}
}
// ---------------------------------------------------------
// 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c)
// ---------------------------------------------------------
numMaterials := len(materials) // 此时已包含填充料
numNutrientConstraints := len(nutrientConstraints)
numMaxAdditionConstraints := len(maxAdditionConstraints)
// 松弛变量数量 = 营养约束数量 + 最大添加比例约束数量
numSlack := numNutrientConstraints + numMaxAdditionConstraints
numCols := numMaterials + numSlack
// 行数 = 1 (总量约束) + 营养约束数量 + 最大添加比例约束数量
numRows := 1 + numNutrientConstraints + numMaxAdditionConstraints
// 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)
}
// 松弛变量的成本为 0Go 默认初始化为 0无需操作
// 填充 Row 0: 总量约束 (Sum(x) = 1)
// 系数: 所有原料对应列为 1松弛变量列为 0
for j := 0; j < numMaterials; j++ {
A.Set(0, j, 1.0)
}
b[0] = 1.0
// currentConstraintRowIndex 记录当前正在填充的约束行索引从1开始0行被总量约束占用
currentConstraintRowIndex := 1
// 填充营养约束行
for i, cons := range nutrientConstraints {
rowIndex := currentConstraintRowIndex + i
// 营养约束的松弛变量列紧跟在原料列之后
slackColIndex := numMaterials + i
b[rowIndex] = cons.limit
// 设置原料系数
for j, m := range materials {
// 获取该原料这种营养素的含量如果没有则为0
val := materialNutrients[m.ID][cons.nutrientID]
A.Set(rowIndex, j, val)
}
// 设置松弛变量系数
// 如果是下限 (>=): 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)
}
}
currentConstraintRowIndex += numNutrientConstraints // 推进当前约束行索引
// 填充 MaxAdditionRatio 约束行
for i, cons := range maxAdditionConstraints {
rowIndex := currentConstraintRowIndex + i
// MaxAdditionRatio 约束的松弛变量列在营养约束的松弛变量之后
slackColIndex := numMaterials + numNutrientConstraints + i
// 约束形式: x_j + s_k = Limit_j (其中 x_j 是原料 j 的比例s_k 是松弛变量)
A.Set(rowIndex, cons.materialColIndex, 1.0) // 原料本身的系数
A.Set(rowIndex, slackColIndex, 1.0) // 松弛变量的系数
b[rowIndex] = cons.limit
}
// ---------------------------------------------------------
// 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. 结果解析与构建
// ---------------------------------------------------------
// 统计实际原料数量(排除填充料)
actualMaterialCount := 0
for _, m := range materials {
if m.ID != fillerRawMaterial.ID {
actualMaterialCount++
}
}
recipe := &models.Recipe{
Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name),
Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", actualMaterialCount, optVal),
RecipeIngredients: []models.RecipeIngredient{},
}
// 遍历原料部分的解 (前 numMaterials 个变量)
totalPercentage := 0.0
for i := 0; i < numMaterials; i++ {
// 排除内部虚拟填充料,不将其加入最终配方
if materialIDs[i] == fillerRawMaterial.ID {
continue
}
proportion := x[i]
// 忽略极小值 (浮点数误差)。
// 调整过滤阈值到万分之一 (0.01%)即小于0.0001的比例将被忽略。
if proportion < 1e-4 {
continue
}
// 记录总和用于最后的校验
totalPercentage += proportion
recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{
RawMaterialID: materialIDs[i],
// 比例: float64 -> float32
Percentage: float32(proportion * 100.0),
})
}
// 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)。
// 允许略微超过100%的浮点误差,但不能显著超过。
if totalPercentage > 1.0+1e-3 {
return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.2f),请检查算法或数据配置", totalPercentage)
}
// 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。
// 此时需要在描述中说明需要添加的廉价填充料的百分比。
if totalPercentage < 1.0-1e-4 { // 允许微小的浮点误差
fillerPercentage := (1.0 - totalPercentage) * 100.0
recipe.Description = fmt.Sprintf("%s。注意配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage*100.0, fillerPercentage)
}
return recipe, nil
}

View File

@@ -2,6 +2,10 @@ package recipe
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
// Service 定义了配方与原料领域的核心业务服务接口
@@ -13,6 +17,8 @@ type Service interface {
PigAgeStageService
PigTypeService
RecipeCoreService
RecipeGenerateManager
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
}
// recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现
@@ -24,6 +30,7 @@ type recipeServiceImpl struct {
PigAgeStageService
PigTypeService
RecipeCoreService
RecipeGenerateManager
}
// NewRecipeService 创建一个新的 Service 实例
@@ -35,14 +42,50 @@ func NewRecipeService(
pigAgeStageService PigAgeStageService,
pigTypeService PigTypeService,
recipeCoreService RecipeCoreService,
recipeGenerateManager RecipeGenerateManager,
) Service {
return &recipeServiceImpl{
ctx: ctx,
NutrientService: nutrientService,
RawMaterialService: rawMaterialService,
PigBreedService: pigBreedService,
PigAgeStageService: pigAgeStageService,
PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService,
ctx: ctx,
NutrientService: nutrientService,
RawMaterialService: rawMaterialService,
PigBreedService: pigBreedService,
PigAgeStageService: pigAgeStageService,
PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService,
RecipeGenerateManager: recipeGenerateManager,
}
}
// GenerateRecipeWithAllRawMaterials 使用所有已知原料为特定猪类型生成一个新配方。
// pigTypeID: 目标猪类型的ID。
// 返回: 生成的配方对象指针和可能的错误。
func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) {
// 1. 获取猪只类型信息,确保包含了营养需求
pigType, err := r.GetPigTypeByID(ctx, pigTypeID)
if err != nil {
return nil, fmt.Errorf("获取猪类型信息失败: %w", err)
}
// 2. 获取所有原料
// 我们通过传递一个非常大的 pageSize 来获取所有原料,这在大多数情况下是可行的。
// 对于超大规模系统,可能需要考虑分页迭代,但目前这是一个简单有效的策略。
materials, _, err := r.ListRawMaterials(ctx, repository.RawMaterialListOptions{}, 1, 9999)
if err != nil {
return nil, fmt.Errorf("获取所有原料列表失败: %w", err)
}
// 3. 调用生成器生成配方
recipe, err := r.GenerateRecipe(ctx, *pigType, materials)
if err != nil {
return nil, fmt.Errorf("生成配方失败: %w", err)
}
// 4. 保存新生成的配方到数据库
// CreateRecipe 会处理配方及其成分的保存
if recipe, err = r.CreateRecipe(ctx, recipe); err != nil {
return nil, fmt.Errorf("保存生成的配方失败: %w", err)
}
// 5. 返回创建的配方 (现在它应该已经有了ID)
return recipe, nil
}

View File

@@ -1,17 +1,14 @@
package database
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database/seeder"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/tidwall/gjson"
"gorm.io/gorm"
@@ -95,9 +92,9 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
var seederFunc SeederFunc
switch dataTypeStr {
case "nutrient":
seederFunc = seedNutrients
seederFunc = seeder.SeedNutrients
case "pig_nutrient_requirements":
seederFunc = seedPigNutrientRequirements
seederFunc = seeder.SeedPigNutrientRequirements
default:
logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr)
continue
@@ -127,522 +124,3 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
return nil // 提交事务
})
}
// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。
type rawMaterialInfo struct {
Description string
UnitPrice float32
}
// seedNutrients 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func seedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
logger := logs.GetLogger(ctx)
// 检查 Nutrient 表是否为空,如果非空则跳过播种
isEmpty, err := isTableEmpty(tx, &models.Nutrient{})
if err != nil {
return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err)
}
if !isEmpty {
logger.Info("已存在原料数据, 跳过数据播种")
return nil
}
// 1. 严格校验JSON文件检查内部重复键
if err := validateAndParseNutrientJSON(jsonData); err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err)
}
// 2. 解析简介信息
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
rawMaterialInfos := make(map[string]rawMaterialInfo)
nutrientDescriptions := make(map[string]string)
if descriptionsNode.Exists() {
// 解析 raw_materials 描述和价格
descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool {
rawMaterialInfos[key.String()] = rawMaterialInfo{
Description: value.Get("descriptions").String(),
UnitPrice: float32(value.Get("unit_price").Float()),
}
return true
})
descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool {
nutrientDescriptions[key.String()] = value.String()
return true
})
}
// 3. 将通过校验的、干净的数据写入数据库
dataNode := gjson.GetBytes(jsonData, "data")
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
rawMaterialName := rawMaterialKey.String()
var rawMaterial models.RawMaterial
// 获取原料的描述和价格信息
info := rawMaterialInfos[rawMaterialName]
// 将 Description 和 ReferencePrice 放入 Create 对象中
err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
FirstOrCreate(&rawMaterial, models.RawMaterial{
Name: rawMaterialName,
Description: info.Description,
ReferencePrice: info.UnitPrice,
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
nutrientName := nutrientKey.String()
value := float32(nutrientValue.Float())
var nutrient models.Nutrient
// 将 Description 放入 Create 对象中
err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName,
Description: nutrientDescriptions[nutrientName],
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
linkData := models.RawMaterialNutrient{
RawMaterialID: rawMaterial.ID,
NutrientID: nutrient.ID,
}
// 使用 FirstOrCreate 确保关联的唯一性
err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{
RawMaterialID: linkData.RawMaterialID,
NutrientID: linkData.NutrientID,
Value: value,
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
return true
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
}
// seedPigNutrientRequirements 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func seedPigNutrientRequirements(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
logger := logs.GetLogger(ctx)
// 检查 PigBreed 表是否为空,如果非空则跳过播种
isEmpty, err := isTableEmpty(tx, &models.PigBreed{})
if err != nil {
return fmt.Errorf("检查 PigBreed 表是否为空失败: %w", err)
}
if !isEmpty {
logger.Info("已存在猪种数据, 跳过数据播种")
return nil
}
// 1. 严格校验JSON文件检查内部重复键
if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err)
}
// 2. 解析简介信息
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
pigBreedDescriptions := make(map[string]models.PigBreed)
pigAgeStageDescriptions := make(map[string]models.PigAgeStage)
pigTypeDescriptions := make(map[string]map[string]models.PigType)
if descriptionsNode.Exists() {
// 解析 pig_breeds 描述
descriptionsNode.Get("pig_breeds").ForEach(func(key, value gjson.Result) bool {
var pb models.PigBreed
pb.Name = key.String()
pb.Description = value.Get("description").String()
pb.ParentInfo = value.Get("parent_info").String()
pb.AppearanceFeatures = value.Get("appearance_features").String()
pb.BreedAdvantages = value.Get("breed_advantages").String()
pb.BreedDisadvantages = value.Get("breed_disadvantages").String()
pigBreedDescriptions[key.String()] = pb
return true
})
// 解析 pig_age_stages 描述
descriptionsNode.Get("pig_age_stages").ForEach(func(key, value gjson.Result) bool {
var pas models.PigAgeStage
pas.Name = key.String()
pas.Description = value.String()
pigAgeStageDescriptions[key.String()] = pas
return true
})
// 解析 pig_breed_age_stages (PigType) 描述
descriptionsNode.Get("pig_breed_age_stages").ForEach(func(breedKey, breedValue gjson.Result) bool {
if _, ok := pigTypeDescriptions[breedKey.String()]; !ok {
pigTypeDescriptions[breedKey.String()] = make(map[string]models.PigType)
}
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
var pt models.PigType
pt.Description = ageStageValue.Get("description").String()
pt.DailyFeedIntake = float32(ageStageValue.Get("daily_feed_intake").Float())
pt.DailyGainWeight = float32(ageStageValue.Get("daily_gain_weight").Float())
pt.MinDays = uint32(ageStageValue.Get("min_days").Uint())
pt.MaxDays = uint32(ageStageValue.Get("max_days").Uint())
pt.MinWeight = float32(ageStageValue.Get("min_weight").Float())
pt.MaxWeight = float32(ageStageValue.Get("max_weight").Float())
pigTypeDescriptions[breedKey.String()][ageStageKey.String()] = pt
return true
})
return true
})
}
// 3. 将通过校验的、干净的数据写入数据库
dataNode := gjson.GetBytes(jsonData, "data")
dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool {
breedName := breedKey.String()
var pigBreed models.PigBreed
// 查找或创建 PigBreed
pbDesc := pigBreedDescriptions[breedName]
err = tx.Where(models.PigBreed{Name: breedName}).
FirstOrCreate(&pigBreed, models.PigBreed{
Name: breedName,
Description: pbDesc.Description,
ParentInfo: pbDesc.ParentInfo,
AppearanceFeatures: pbDesc.AppearanceFeatures,
BreedAdvantages: pbDesc.BreedAdvantages,
BreedDisadvantages: pbDesc.BreedDisadvantages,
}).Error
if err != nil {
return false
}
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
ageStageName := ageStageKey.String()
var pigAgeStage models.PigAgeStage
// 查找或创建 PigAgeStage
pasDesc := pigAgeStageDescriptions[ageStageName]
err = tx.Where(models.PigAgeStage{Name: ageStageName}).
FirstOrCreate(&pigAgeStage, models.PigAgeStage{
Name: ageStageName,
Description: pasDesc.Description,
}).Error
if err != nil {
return false
}
var pigType models.PigType
// 查找或创建 PigType
ptDesc := pigTypeDescriptions[breedName][ageStageName]
err = tx.Where(models.PigType{BreedID: pigBreed.ID, AgeStageID: pigAgeStage.ID}).
FirstOrCreate(&pigType, models.PigType{
BreedID: pigBreed.ID,
AgeStageID: pigAgeStage.ID,
Description: ptDesc.Description,
DailyFeedIntake: ptDesc.DailyFeedIntake,
DailyGainWeight: ptDesc.DailyGainWeight,
MinDays: ptDesc.MinDays,
MaxDays: ptDesc.MaxDays,
MinWeight: ptDesc.MinWeight,
MaxWeight: ptDesc.MaxWeight,
}).Error
if err != nil {
return false
}
ageStageValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
nutrientName := nutrientKey.String()
minReq := float32(nutrientValue.Get("min_requirement").Float())
maxReq := float32(nutrientValue.Get("max_requirement").Float())
var nutrient models.Nutrient
// 查找或创建 Nutrient (这里假设 Nutrient 已经在 seedNutrients 中处理,但为了健壮性,再次 FirstOrCreate)
err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName,
// Description 字段在 nutrient seeder 中处理,这里不设置
}).Error
if err != nil {
return false
}
linkData := models.PigNutrientRequirement{
PigTypeID: pigType.ID,
NutrientID: nutrient.ID,
MinRequirement: minReq,
MaxRequirement: maxReq,
}
// 使用 FirstOrCreate 确保关联的唯一性
err = tx.Where(models.PigNutrientRequirement{
PigTypeID: pigType.ID,
NutrientID: nutrient.ID,
}).FirstOrCreate(&linkData, linkData).Error
if err != nil {
return false
}
return true
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
}
// validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件
func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error {
dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() {
return errors.New("JSON文件中缺少 'data' 字段")
}
if !dataNode.IsObject() {
return errors.New("'data' 字段必须是一个JSON对象")
}
decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("'data' 字段解析起始符失败: %v", err)
}
seenBreeds := make(map[string]bool)
for decoder.More() {
// 解析 PigBreed 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析猪品种名称失败: %w", err)
}
breedName := t.(string)
if seenBreeds[breedName] {
return fmt.Errorf("猪品种名称 '%s' 重复", breedName)
}
seenBreeds[breedName] = true
// 解析该品种的年龄阶段对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName)
}
seenAgeStages := make(map[string]bool)
for decoder.More() {
// 解析 PigAgeStage 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err)
}
ageStageName := t.(string)
if seenAgeStages[ageStageName] {
return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName)
}
seenAgeStages[ageStageName] = true
// 解析该年龄阶段的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName)
}
seenNutrients := make(map[string]bool)
for decoder.More() {
// 解析 Nutrient 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err)
}
nutrientName := t.(string)
if seenNutrients[nutrientName] {
return fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName)
}
seenNutrients[nutrientName] = true
// 解析 min_requirement 和 max_requirement 对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName)
}
for decoder.More() {
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err)
}
// key := t.(string) // 校验时不需要使用 key 的值
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望营养素 '%s' 的需求值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName)
}
}
return nil
}
// validateAndParseNutrientJSON 严格校验JSON文件
func validateAndParseNutrientJSON(jsonData []byte) error {
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
if !descriptionsNode.Exists() {
return errors.New("JSON文件中缺少 'descriptions' 字段")
}
if !descriptionsNode.IsObject() {
return errors.New("'descriptions' 字段必须是一个JSON对象")
}
rawMaterialsNode := descriptionsNode.Get("raw_materials")
if !rawMaterialsNode.Exists() {
return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段")
}
if !rawMaterialsNode.IsObject() {
return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象")
}
// 使用 json.Decoder 严格校验 raw_materials 的结构
decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err)
}
seenRawMaterials := make(map[string]bool)
for decoder.More() {
// 1. 解析原料名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料名称失败: %w", err)
}
rawMaterialName := t.(string)
if seenRawMaterials[rawMaterialName] {
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
}
seenRawMaterials[rawMaterialName] = true
// 2. 解析该原料的描述和价格对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
}
for decoder.More() {
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err)
}
key := t.(string)
switch key {
case "descriptions":
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err)
}
if _, ok := t.(string); !ok {
return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
}
case "unit_price":
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
}
default:
// 忽略其他未知字段,但仍需读取其值以继续解析
if _, err := decoder.Token(); err != nil {
return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err)
}
}
}
// 读取原料描述和价格对象的 "}"
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
}
}
// 校验 data 节点
dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() {
return errors.New("JSON文件中缺少 'data' 字段")
}
if !dataNode.IsObject() {
return errors.New("'data' 字段必须是一个JSON对象")
}
// 重新初始化 decoder 用于 data 节点的校验
decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return errors.New("'data' 字段解析起始符失败")
}
seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验
for decoder.More() {
// 1. 解析原料名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料名称失败: %w", err)
}
rawMaterialName := t.(string)
if seenRawMaterials[rawMaterialName] {
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
}
seenRawMaterials[rawMaterialName] = true
// 2. 解析该原料的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
}
seenNutrients := make(map[string]bool)
for decoder.More() {
// 解析营养素名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err)
}
nutrientName := t.(string)
if seenNutrients[nutrientName] {
return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName)
}
seenNutrients[nutrientName] = true
// 解析营养素含量
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
}
}
// 读取营养成分对象的 "}"
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
}
}
return nil
}

View File

@@ -0,0 +1,279 @@
package seeder
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/tidwall/gjson"
"gorm.io/gorm"
)
// rawMaterialInfo 用于临时存储解析后的原料描述、价格和最大添加量信息。
type rawMaterialInfo struct {
Description string
UnitPrice float32
MaxRatio float32
}
// SeedNutrients 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func SeedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
logger := logs.GetLogger(ctx)
// 检查 Nutrient 表是否为空,如果非空则跳过播种
isEmpty, err := isTableEmpty(tx, &models.Nutrient{})
if err != nil {
return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err)
}
if !isEmpty {
logger.Info("已存在原料数据, 跳过数据播种")
return nil
}
// 1. 严格校验JSON文件检查内部重复键
if err := validateAndParseNutrientJSON(jsonData); err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err)
}
// 2. 解析简介信息
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
rawMaterialInfos := make(map[string]rawMaterialInfo)
nutrientDescriptions := make(map[string]string)
if descriptionsNode.Exists() {
// 解析 raw_materials 描述、价格和最大添加量
descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool {
rawMaterialInfos[key.String()] = rawMaterialInfo{
Description: value.Get("descriptions").String(),
UnitPrice: float32(value.Get("unit_price").Float()),
MaxRatio: float32(value.Get("max_ratio").Float()),
}
return true
})
descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool {
nutrientDescriptions[key.String()] = value.String()
return true
})
}
// 3. 将通过校验的、干净的数据写入数据库
dataNode := gjson.GetBytes(jsonData, "data")
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
rawMaterialName := rawMaterialKey.String()
var rawMaterial models.RawMaterial
// 获取原料的描述、价格和最大添加量信息
info := rawMaterialInfos[rawMaterialName]
// 将 Description, ReferencePrice 和 MaxAdditionRatio 放入 Create 对象中
err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
FirstOrCreate(&rawMaterial, models.RawMaterial{
Name: rawMaterialName,
Description: info.Description,
ReferencePrice: info.UnitPrice,
MaxAdditionRatio: info.MaxRatio,
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
nutrientName := nutrientKey.String()
value := float32(nutrientValue.Float())
var nutrient models.Nutrient
// 将 Description 放入 Create 对象中
err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName,
Description: nutrientDescriptions[nutrientName],
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
linkData := models.RawMaterialNutrient{
RawMaterialID: rawMaterial.ID,
NutrientID: nutrient.ID,
}
// 使用 FirstOrCreate 确保关联的唯一性
err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{
RawMaterialID: linkData.RawMaterialID,
NutrientID: linkData.NutrientID,
Value: value,
}).Error
if err != nil {
// 返回 false 停止 ForEach 遍历
return false
}
return true
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
}
// validateAndParseNutrientJSON 严格校验JSON文件
func validateAndParseNutrientJSON(jsonData []byte) error {
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
if !descriptionsNode.Exists() {
return errors.New("JSON文件中缺少 'descriptions' 字段")
}
if !descriptionsNode.IsObject() {
return errors.New("'descriptions' 字段必须是一个JSON对象")
}
rawMaterialsNode := descriptionsNode.Get("raw_materials")
if !rawMaterialsNode.Exists() {
return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段")
}
if !rawMaterialsNode.IsObject() {
return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象")
}
// 使用 json.Decoder 严格校验 raw_materials 的结构
decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err)
}
seenRawMaterials := make(map[string]bool)
for decoder.More() {
// 1. 解析原料名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料名称失败: %w", err)
}
rawMaterialName := t.(string)
if seenRawMaterials[rawMaterialName] {
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
}
seenRawMaterials[rawMaterialName] = true
// 2. 解析该原料的描述和价格对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
}
for decoder.More() {
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err)
}
key := t.(string)
switch key {
case "descriptions":
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err)
}
if _, ok := t.(string); !ok {
return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
}
case "unit_price":
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
}
case "max_ratio":
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析原料 '%s' 的 'max_ratio' 值失败: %w", rawMaterialName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望原料 '%s' 的 'max_ratio' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t)
}
default:
// 忽略其他未知字段,但仍需读取其值以继续解析
if _, err := decoder.Token(); err != nil {
return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err)
}
}
}
// 读取原料描述和价格对象的 "}"
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
}
}
// 校验 data 节点
dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() {
return errors.New("JSON文件中缺少 'data' 字段")
}
if !dataNode.IsObject() {
return errors.New("'data' 字段必须是一个JSON对象")
}
// 重新初始化 decoder 用于 data 节点的校验
decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return errors.New("'data' 字段解析起始符失败")
}
seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验
for decoder.More() {
// 1. 解析原料名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析原料名称失败: %w", err)
}
rawMaterialName := t.(string)
if seenRawMaterials[rawMaterialName] {
return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName)
}
seenRawMaterials[rawMaterialName] = true
// 2. 解析该原料的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName)
}
seenNutrients := make(map[string]bool)
for decoder.More() {
// 解析营养素名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err)
}
nutrientName := t.(string)
if seenNutrients[nutrientName] {
return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName)
}
seenNutrients[nutrientName] = true
// 解析营养素含量
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
}
}
// 读取营养成分对象的 "}"
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName)
}
}
return nil
}

View File

@@ -0,0 +1,281 @@
package seeder
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"github.com/tidwall/gjson"
"gorm.io/gorm"
)
// SeedPigNutrientRequirements 先严格校验JSON源文件然后以“有则跳过”的模式播种数据。
func SeedPigNutrientRequirements(ctx context.Context, tx *gorm.DB, jsonData []byte) error {
logger := logs.GetLogger(ctx)
// 检查 PigBreed 表是否为空,如果非空则跳过播种
isEmpty, err := isTableEmpty(tx, &models.PigBreed{})
if err != nil {
return fmt.Errorf("检查 PigBreed 表是否为空失败: %w", err)
}
if !isEmpty {
logger.Info("已存在猪种数据, 跳过数据播种")
return nil
}
// 1. 严格校验JSON文件检查内部重复键
if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil {
return fmt.Errorf("JSON源文件校验失败: %w", err)
}
// 2. 解析简介信息
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
pigBreedDescriptions := make(map[string]models.PigBreed)
pigAgeStageDescriptions := make(map[string]models.PigAgeStage)
pigTypeDescriptions := make(map[string]map[string]models.PigType)
if descriptionsNode.Exists() {
// 解析 pig_breeds 描述
descriptionsNode.Get("pig_breeds").ForEach(func(key, value gjson.Result) bool {
var pb models.PigBreed
pb.Name = key.String()
pb.Description = value.Get("description").String()
pb.ParentInfo = value.Get("parent_info").String()
pb.AppearanceFeatures = value.Get("appearance_features").String()
pb.BreedAdvantages = value.Get("breed_advantages").String()
pb.BreedDisadvantages = value.Get("breed_disadvantages").String()
pigBreedDescriptions[key.String()] = pb
return true
})
// 解析 pig_age_stages 描述
descriptionsNode.Get("pig_age_stages").ForEach(func(key, value gjson.Result) bool {
var pas models.PigAgeStage
pas.Name = key.String()
pas.Description = value.String()
pigAgeStageDescriptions[key.String()] = pas
return true
})
// 解析 pig_breed_age_stages (PigType) 描述
descriptionsNode.Get("pig_breed_age_stages").ForEach(func(breedKey, breedValue gjson.Result) bool {
if _, ok := pigTypeDescriptions[breedKey.String()]; !ok {
pigTypeDescriptions[breedKey.String()] = make(map[string]models.PigType)
}
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
var pt models.PigType
pt.Description = ageStageValue.Get("description").String()
pt.DailyFeedIntake = float32(ageStageValue.Get("daily_feed_intake").Float())
pt.DailyGainWeight = float32(ageStageValue.Get("daily_gain_weight").Float())
pt.MinDays = uint32(ageStageValue.Get("min_days").Uint())
pt.MaxDays = uint32(ageStageValue.Get("max_days").Uint())
pt.MinWeight = float32(ageStageValue.Get("min_weight").Float())
pt.MaxWeight = float32(ageStageValue.Get("max_weight").Float())
pigTypeDescriptions[breedKey.String()][ageStageKey.String()] = pt
return true
})
return true
})
}
// 3. 将通过校验的、干净的数据写入数据库
dataNode := gjson.GetBytes(jsonData, "data")
dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool {
breedName := breedKey.String()
var pigBreed models.PigBreed
// 查找或创建 PigBreed
pbDesc := pigBreedDescriptions[breedName]
err = tx.Where(models.PigBreed{Name: breedName}).
FirstOrCreate(&pigBreed, models.PigBreed{
Name: breedName,
Description: pbDesc.Description,
ParentInfo: pbDesc.ParentInfo,
AppearanceFeatures: pbDesc.AppearanceFeatures,
BreedAdvantages: pbDesc.BreedAdvantages,
BreedDisadvantages: pbDesc.BreedDisadvantages,
}).Error
if err != nil {
return false
}
breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool {
ageStageName := ageStageKey.String()
var pigAgeStage models.PigAgeStage
// 查找或创建 PigAgeStage
pasDesc := pigAgeStageDescriptions[ageStageName]
err = tx.Where(models.PigAgeStage{Name: ageStageName}).
FirstOrCreate(&pigAgeStage, models.PigAgeStage{
Name: ageStageName,
Description: pasDesc.Description,
}).Error
if err != nil {
return false
}
var pigType models.PigType
// 查找或创建 PigType
ptDesc := pigTypeDescriptions[breedName][ageStageName]
err = tx.Where(models.PigType{BreedID: pigBreed.ID, AgeStageID: pigAgeStage.ID}).
FirstOrCreate(&pigType, models.PigType{
BreedID: pigBreed.ID,
AgeStageID: pigAgeStage.ID,
Description: ptDesc.Description,
DailyFeedIntake: ptDesc.DailyFeedIntake,
DailyGainWeight: ptDesc.DailyGainWeight,
MinDays: ptDesc.MinDays,
MaxDays: ptDesc.MaxDays,
MinWeight: ptDesc.MinWeight,
MaxWeight: ptDesc.MaxWeight,
}).Error
if err != nil {
return false
}
ageStageValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool {
nutrientName := nutrientKey.String()
minReq := float32(nutrientValue.Get("min_requirement").Float())
maxReq := float32(nutrientValue.Get("max_requirement").Float())
var nutrient models.Nutrient
// 查找或创建 Nutrient (这里假设 Nutrient 已经在 SeedNutrients 中处理,但为了健壮性,再次 FirstOrCreate)
err = tx.Where(models.Nutrient{Name: nutrientName}).
FirstOrCreate(&nutrient, models.Nutrient{
Name: nutrientName,
// Description 字段在 nutrient seeder 中处理,这里不设置
}).Error
if err != nil {
return false
}
linkData := models.PigNutrientRequirement{
PigTypeID: pigType.ID,
NutrientID: nutrient.ID,
MinRequirement: minReq,
MaxRequirement: maxReq,
}
// 使用 FirstOrCreate 确保关联的唯一性
err = tx.Where(models.PigNutrientRequirement{
PigTypeID: pigType.ID,
NutrientID: nutrient.ID,
}).FirstOrCreate(&linkData, linkData).Error
if err != nil {
return false
}
return true
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err == nil // 如果内部遍历有错误,则停止外部遍历
})
return err // 返回捕获到的错误
}
// validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件
func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error {
dataNode := gjson.GetBytes(jsonData, "data")
if !dataNode.Exists() {
return errors.New("JSON文件中缺少 'data' 字段")
}
if !dataNode.IsObject() {
return errors.New("'data' 字段必须是一个JSON对象")
}
decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw)))
decoder.UseNumber()
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("'data' 字段解析起始符失败: %v", err)
}
seenBreeds := make(map[string]bool)
for decoder.More() {
// 解析 PigBreed 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析猪品种名称失败: %w", err)
}
breedName := t.(string)
if seenBreeds[breedName] {
return fmt.Errorf("猪品种名称 '%s' 重复", breedName)
}
seenBreeds[breedName] = true
// 解析该品种的年龄阶段对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName)
}
seenAgeStages := make(map[string]bool)
for decoder.More() {
// 解析 PigAgeStage 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err)
}
ageStageName := t.(string)
if seenAgeStages[ageStageName] {
return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName)
}
seenAgeStages[ageStageName] = true
// 解析该年龄阶段的营养成分对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName)
}
seenNutrients := make(map[string]bool)
for decoder.More() {
// 解析 Nutrient 名称
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err)
}
nutrientName := t.(string)
if seenNutrients[nutrientName] {
return fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName)
}
seenNutrients[nutrientName] = true
// 解析 min_requirement 和 max_requirement 对象
if t, err := decoder.Token(); err != nil || t != json.Delim('{') {
return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName)
}
for decoder.More() {
t, err := decoder.Token()
if err != nil {
return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err)
}
// key := t.(string) // 校验时不需要使用 key 的值
t, err = decoder.Token()
if err != nil {
return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err)
}
if _, ok := t.(json.Number); !ok {
return fmt.Errorf("期望营养素 '%s' 的需求值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName)
}
}
if t, err := decoder.Token(); err != nil || t != json.Delim('}') {
return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName)
}
}
return nil
}

View File

@@ -0,0 +1,18 @@
package seeder
import (
"fmt"
"gorm.io/gorm"
)
// isTableEmpty 检查给定模型对应的数据库表是否为空。
// 注意:此函数需要从 database 包中移动过来,或者在 seeder 包中重新定义,
// 为了避免循环依赖,这里选择在 seeder 包中重新定义。
func isTableEmpty(tx *gorm.DB, model interface{}) (bool, error) {
var count int64
if err := tx.Model(model).Count(&count).Error; err != nil {
return false, fmt.Errorf("查询表记录数失败: %w", err)
}
return count == 0, nil
}

View File

@@ -21,9 +21,10 @@ const (
// RawMaterial 代表一种原料的静态定义,是系统中的原料字典。
type RawMaterial struct {
Model
Name string `gorm:"size:100;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"`
ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"`
Name string `gorm:"size:100;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"`
ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"`
MaxAdditionRatio float32 `gorm:"comment:该物质最大添加比例"`
// RawMaterialNutrients 关联此原料的所有营养素含量信息
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"`
}

View File

@@ -41,6 +41,7 @@ type RawMaterialRepository interface {
DeleteRawMaterial(ctx context.Context, id uint32) error
DeleteNutrientsByRawMaterialIDTx(ctx context.Context, db *gorm.DB, rawMaterialID uint32) error
CreateBatchRawMaterialNutrientsTx(ctx context.Context, db *gorm.DB, nutrients []models.RawMaterialNutrient) error
IsRawMaterialUsedInRecipes(ctx context.Context, rawMaterialID uint32) (bool, error)
// 库存日志相关方法
CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error
@@ -143,9 +144,10 @@ func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMa
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial")
// 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段
updateData := map[string]interface{}{
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"reference_price": rawMaterial.ReferencePrice,
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"reference_price": rawMaterial.ReferencePrice,
"max_addition_ratio": rawMaterial.MaxAdditionRatio,
}
result := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}).Where("id = ?", rawMaterial.ID).Updates(updateData)
if result.Error != nil {
@@ -329,3 +331,16 @@ func (r *gormRawMaterialRepository) ListStockLogs(ctx context.Context, opts Stoc
return logs, total, nil
}
// IsRawMaterialUsedInRecipes 检查原料是否被任何配方使用
func (r *gormRawMaterialRepository) IsRawMaterialUsedInRecipes(ctx context.Context, rawMaterialID uint32) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsRawMaterialUsedInRecipes")
var count int64
err := r.db.WithContext(repoCtx).Model(&models.RecipeIngredient{}).
Where("raw_material_id = ?", rawMaterialID).
Count(&count).Error
if err != nil {
return false, fmt.Errorf("查询原料是否被配方使用失败: %w", err)
}
return count > 0, nil
}

View File

@@ -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