Compare commits
33 Commits
d7e2777c13
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 70fad51f40 | |||
| 6764684fe7 | |||
| da2c296c05 | |||
| bdf74652b3 | |||
| 70e8627a96 | |||
| c2c6577064 | |||
| 260c7d054c | |||
| d25933cf26 | |||
| 4aa56441ce | |||
| de68151539 | |||
| 04b46d8025 | |||
| bc4355cad5 | |||
| 968d996a9b | |||
| d6e5d89768 | |||
| 1b5f715dec | |||
| da8e1d0191 | |||
| 33cdf7278e | |||
| 3b12802900 | |||
| e6b307b0dc | |||
| b8e0301175 | |||
| e2da441a6d | |||
| dca6cc5dd4 | |||
| 5c99ff7475 | |||
| 0283c250e4 | |||
| 5bd52df240 | |||
| 5ad403bf86 | |||
| ce3844957f | |||
| 6c0f655d0a | |||
| 29b820b846 | |||
| 34311889e8 | |||
| ba60ed541c | |||
| 35eae7b3ec | |||
| ca85671a4c |
@@ -3,6 +3,7 @@
|
||||
1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang
|
||||
2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html
|
||||
3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构
|
||||
4. 项目中有config/presets-data目录, 里面有一些预设数据, 可以间接看作数据库内的数据, 在测试环境中他们一般会和数据库的数据保持一致
|
||||
|
||||
# 权限管理
|
||||
|
||||
|
||||
9
Makefile
9
Makefile
@@ -10,10 +10,15 @@ help:
|
||||
@echo " build Build the application"
|
||||
@echo " clean Clean generated files"
|
||||
@echo " test Run all tests"
|
||||
@echo " swag Generate swagger docs"
|
||||
@echo " help Show this help message"
|
||||
@echo " swag Generate Swagger docs"
|
||||
@echo " proto Generate protobuf files"
|
||||
@echo " lint Lint the code"
|
||||
@echo " dev Run in development mode with hot-reload"
|
||||
@echo " mcp-chrome Start the Google Chrome MCP server"
|
||||
@echo " mcp-pgsql Start the PostgreSQL MCP server"
|
||||
@echo " tree Generate the project file structure list"
|
||||
@echo " gemini Start the gemini-cli"
|
||||
@echo " help Show this help message"
|
||||
|
||||
# 运行应用
|
||||
.PHONY: run
|
||||
|
||||
@@ -125,3 +125,11 @@ alarm_notification:
|
||||
dpanic: 1
|
||||
panic: 1
|
||||
fatal: 1
|
||||
|
||||
# AI 服务配置
|
||||
ai:
|
||||
model: "gemini" # 不指定就是不用AI
|
||||
gemini:
|
||||
api_key: "YOUR_GEMINI_API_KEY" # 替换为你的 Gemini API Key
|
||||
model_name: "gemini-2.5-flash" # Gemini 模型名称,例如 "gemini-pro"
|
||||
timeout: 30 # AI 请求超时时间 (秒)
|
||||
@@ -103,3 +103,11 @@ alarm_notification:
|
||||
dpanic: 1
|
||||
panic: 1
|
||||
fatal: 1
|
||||
|
||||
# AI 服务配置
|
||||
ai:
|
||||
model: Gemini
|
||||
gemini:
|
||||
api_key: "AIzaSyAJdXUmoN07LIswDac6YxPeRnvXlR73OO8" # 替换为你的 Gemini API Key
|
||||
model_name: "gemini-2.0-flash" # Gemini 模型名称,例如 "gemini-pro"
|
||||
timeout: 30 # AI 请求超时时间 (秒)
|
||||
|
||||
@@ -1739,87 +1739,411 @@
|
||||
},
|
||||
"descriptions": {
|
||||
"raw_materials": {
|
||||
"DL-蛋氨酸98": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。",
|
||||
"L-色氨酸98": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。",
|
||||
"L-苏氨酸98": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。",
|
||||
"L-赖氨酸HCl 98": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。",
|
||||
"乳清粉": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。",
|
||||
"兔肉粉": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。",
|
||||
"全株玉米青贮": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。",
|
||||
"双低菜籽粕": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。",
|
||||
"向日葵籽": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。",
|
||||
"啤酒糟干": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。",
|
||||
"啤酒花渣": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。",
|
||||
"国产鱼粉60": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。",
|
||||
"土豆蛋白": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。",
|
||||
"大豆油": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。",
|
||||
"大豆粕44": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。",
|
||||
"大豆粕46": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。",
|
||||
"大豆粕48": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。",
|
||||
"大麦": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。",
|
||||
"小苏打": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。",
|
||||
"小麦": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。",
|
||||
"小麦次粉": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。",
|
||||
"小麦麸": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。",
|
||||
"木薯干": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。",
|
||||
"杂交构树叶粉": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。",
|
||||
"构树叶粉(老叶高纤维)": "老叶构树粉,纤维更高,适合母猪粗饲料使用。",
|
||||
"柠檬酸渣": "湿态副产品,适口性好,可用于母猪料降低成本。",
|
||||
"棉籽粕": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。",
|
||||
"棕榈油": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。",
|
||||
"棕榈粕": "高纤维高脂肪副产品,能量一般,多用于母猪料。",
|
||||
"椰子粕": "蛋白和能量中等,适口性好,可部分替代豆粕。",
|
||||
"燕麦": "能量和脂肪较高,适口性佳,但价格贵,一般少用。",
|
||||
"燕麦草": "粗饲料,母猪用以增加饱腹感和肠道健康。",
|
||||
"猪肺粉": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。",
|
||||
"玉米": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。",
|
||||
"玉米DDGS": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。",
|
||||
"玉米油": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。",
|
||||
"玉米胚芽粕": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。",
|
||||
"玉米蛋白粉60": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。",
|
||||
"玉米青贮": "粗饲料,母猪用以调节肠道,降低饲料成本。",
|
||||
"瓜子粕": "葵花籽粕的别称,蛋白较高,纤维也高。",
|
||||
"甜菜粕": "高可溶性纤维,母猪极佳的防便秘原料。",
|
||||
"石粉": "最常用的钙源,价格低廉,注意粒度影响吸收率。",
|
||||
"碎米": "能量接近玉米,蛋白稍低,适口性好。",
|
||||
"磷酸氢钙": "猪最常用磷钙来源,有效磷高。",
|
||||
"稻草粉": "最廉价粗纤维来源,母猪限量使用防便秘。",
|
||||
"稻谷": "带壳稻子,能量低于玉米,纤维高。",
|
||||
"稻谷糠": "米糠的一种,高脂肪高磷,需注意酸败。",
|
||||
"米糠": "高能量高磷副产品,注意黄曲霉毒素和酸败。",
|
||||
"米糠粕": "脱脂米糠,蛋白较高,能量降低。",
|
||||
"红薯干": "高淀粉低蛋白能量原料,类似木薯。",
|
||||
"肉粉": "普通肉粉,蛋白和灰分波动大,质量不稳定。",
|
||||
"肉骨粉50": "含骨较高,钙磷比例好,但蛋白较低。",
|
||||
"脱脂奶粉": "优质乳蛋白源,仔猪料黄金原料。",
|
||||
"膨化全脂大豆": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。",
|
||||
"芝麻粕": "蛋白高,蛋氨酸丰富,但草酸高,需限量。",
|
||||
"花生秧粉": "粗饲料,母猪用。",
|
||||
"花生粕": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。",
|
||||
"苜蓿草块": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。",
|
||||
"苜蓿草粉": "蛋白较高,但皂苷和香豆素可能影响采食。",
|
||||
"苹果渣": "湿态副产品,适口性好,母猪喜欢。",
|
||||
"菜籽粕": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。",
|
||||
"葡萄糖": "快速能量源,教槽料常用,缓解应激。",
|
||||
"葵花籽粕": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。",
|
||||
"蔗糖": "高能量碳水,教槽料诱食用。",
|
||||
"虾粉": "优质动物蛋白,含虾青素,改善体色。",
|
||||
"蚕蛹粉": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。",
|
||||
"蚕豆": "蛋白较高,淀粉消化率好,但含抗营养因子。",
|
||||
"蟹粉": "高蛋白高灰分动物蛋白,钙磷丰富。",
|
||||
"血浆蛋白粉": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。",
|
||||
"血粉": "赖氨酸极高,但适口性差,需喷涂使用。",
|
||||
"豆磷脂": "高能量乳化剂,促进脂肪消化,改善皮毛。",
|
||||
"豌豆": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。",
|
||||
"豌豆蛋白": "豌豆浓缩蛋白,蛋白高,抗营养因子低。",
|
||||
"进口鱼粉65": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。",
|
||||
"食盐": "提供钠和氯,调节电解质平衡。",
|
||||
"饲料酵母粉": "富含核苷酸和小肽,促进肠道健康和免疫。",
|
||||
"高粱": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。",
|
||||
"鱼油": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。",
|
||||
"鸡肉粉": "优质陆基动物蛋白,消化率高,适口性好。",
|
||||
"鸭肉粉": "与鸡肉粉类似,脂肪稍高。",
|
||||
"鹅肉粉": "蛋白和脂肪中等,质量稳定。"
|
||||
"DL-蛋氨酸98": {
|
||||
"descriptions": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。",
|
||||
"unit_price": 21.50,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"L-色氨酸98": {
|
||||
"descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。",
|
||||
"unit_price": 68.00,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"L-苏氨酸98": {
|
||||
"descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。",
|
||||
"unit_price": 10.80,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"L-赖氨酸HCl 98": {
|
||||
"descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。",
|
||||
"unit_price": 11.20,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"乳清粉": {
|
||||
"descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。",
|
||||
"unit_price": 6.50,
|
||||
"max_ratio": 40.00
|
||||
},
|
||||
"兔肉粉": {
|
||||
"descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。",
|
||||
"unit_price": 11.50,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"全株玉米青贮": {
|
||||
"descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。",
|
||||
"unit_price": 0.45,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"双低菜籽粕": {
|
||||
"descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。",
|
||||
"unit_price": 2.40,
|
||||
"max_ratio": 35.00
|
||||
},
|
||||
"向日葵籽": {
|
||||
"descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。",
|
||||
"unit_price": 5.80,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"啤酒糟干": {
|
||||
"descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。",
|
||||
"unit_price": 1.90,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"啤酒花渣": {
|
||||
"descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。",
|
||||
"unit_price": 0.60,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"国产鱼粉60": {
|
||||
"descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。",
|
||||
"unit_price": 9.50,
|
||||
"max_ratio": 15.00
|
||||
},
|
||||
"土豆蛋白": {
|
||||
"descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。",
|
||||
"unit_price": 8.50,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"大豆油": {
|
||||
"descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。",
|
||||
"unit_price": 8.20,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"大豆粕44": {
|
||||
"descriptions": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。",
|
||||
"unit_price": 3.05,
|
||||
"max_ratio": 65.00
|
||||
},
|
||||
"大豆粕46": {
|
||||
"descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。",
|
||||
"unit_price": 3.25,
|
||||
"max_ratio": 65.00
|
||||
},
|
||||
"大豆粕48": {
|
||||
"descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。",
|
||||
"unit_price": 3.60,
|
||||
"max_ratio": 65.00
|
||||
},
|
||||
"大麦": {
|
||||
"descriptions": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。",
|
||||
"unit_price": 2.10,
|
||||
"max_ratio": 60.00
|
||||
},
|
||||
"小苏打": {
|
||||
"descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。",
|
||||
"unit_price": 1.60,
|
||||
"max_ratio": 2.00
|
||||
},
|
||||
"小麦": {
|
||||
"descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。",
|
||||
"unit_price": 2.55,
|
||||
"max_ratio": 60.00
|
||||
},
|
||||
"小麦次粉": {
|
||||
"descriptions": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。",
|
||||
"unit_price": 2.20,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"小麦麸": {
|
||||
"descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。",
|
||||
"unit_price": 1.75,
|
||||
"max_ratio": 40.00
|
||||
},
|
||||
"木薯干": {
|
||||
"descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。",
|
||||
"unit_price": 2.05,
|
||||
"max_ratio": 50.00
|
||||
},
|
||||
"杂交构树叶粉": {
|
||||
"descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。",
|
||||
"unit_price": 2.20,
|
||||
"max_ratio": 5.00
|
||||
},
|
||||
"构树叶粉(老叶高纤维)": {
|
||||
"descriptions": "老叶构树粉,纤维更高,适合母猪粗饲料使用。",
|
||||
"unit_price": 1.50,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"柠檬酸渣": {
|
||||
"descriptions": "湿态副产品,适口性好,可用于母猪料降低成本。",
|
||||
"unit_price": 0.50,
|
||||
"max_ratio": 5.00
|
||||
},
|
||||
"棉籽粕": {
|
||||
"descriptions": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。",
|
||||
"unit_price": 2.80,
|
||||
"max_ratio": 3.00
|
||||
},
|
||||
"棕榈油": {
|
||||
"descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。",
|
||||
"unit_price": 8.50,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"棕榈粕": {
|
||||
"descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。",
|
||||
"unit_price": 1.60,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"椰子粕": {
|
||||
"descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。",
|
||||
"unit_price": 2.30,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"燕麦": {
|
||||
"descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。",
|
||||
"unit_price": 3.20,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"燕麦草": {
|
||||
"descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。",
|
||||
"unit_price": 2.60,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"猪肺粉": {
|
||||
"descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。",
|
||||
"unit_price": 9.00,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"玉米": {
|
||||
"descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。",
|
||||
"unit_price": 2.30,
|
||||
"max_ratio": 100.00
|
||||
},
|
||||
"玉米DDGS": {
|
||||
"descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。",
|
||||
"unit_price": 2.15,
|
||||
"max_ratio": 40.00
|
||||
},
|
||||
"玉米油": {
|
||||
"descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。",
|
||||
"unit_price": 9.50,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"玉米胚芽粕": {
|
||||
"descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。",
|
||||
"unit_price": 2.05,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"玉米蛋白粉60": {
|
||||
"descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。",
|
||||
"unit_price": 4.80,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"玉米青贮": {
|
||||
"descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。",
|
||||
"unit_price": 0.40,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"瓜子粕": {
|
||||
"descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。",
|
||||
"unit_price": 2.10,
|
||||
"max_ratio": 35.00
|
||||
},
|
||||
"甜菜粕": {
|
||||
"descriptions": "高可溶性纤维,母猪极佳的防便秘原料。",
|
||||
"unit_price": 1.95,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"石粉": {
|
||||
"descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。",
|
||||
"unit_price": 0.18,
|
||||
"max_ratio": 8.00
|
||||
},
|
||||
"碎米": {
|
||||
"descriptions": "能量接近玉米,蛋白稍低,适口性好。",
|
||||
"unit_price": 2.80,
|
||||
"max_ratio": 60.00
|
||||
},
|
||||
"磷酸氢钙": {
|
||||
"descriptions": "猪最常用磷钙来源,有效磷高。",
|
||||
"unit_price": 3.20,
|
||||
"max_ratio": 8.00
|
||||
},
|
||||
"稻草粉": {
|
||||
"descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。",
|
||||
"unit_price": 0.60,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"稻谷": {
|
||||
"descriptions": "带壳稻子,能量低于玉米,纤维高。",
|
||||
"unit_price": 1.90,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"稻谷糠": {
|
||||
"descriptions": "米糠的一种,高脂肪高磷,需注意酸败。",
|
||||
"unit_price": 1.60,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"米糠": {
|
||||
"descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。",
|
||||
"unit_price": 1.85,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"米糠粕": {
|
||||
"descriptions": "脱脂米糠,蛋白较高,能量降低。",
|
||||
"unit_price": 1.95,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"红薯干": {
|
||||
"descriptions": "高淀粉低蛋白能量原料,类似木薯。",
|
||||
"unit_price": 2.20,
|
||||
"max_ratio": 50.00
|
||||
},
|
||||
"肉粉": {
|
||||
"descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。",
|
||||
"unit_price": 4.50,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"肉骨粉50": {
|
||||
"descriptions": "含骨较高,钙磷比例好,但蛋白较低。",
|
||||
"unit_price": 4.20,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"脱脂奶粉": {
|
||||
"descriptions": "优质乳蛋白源,仔猪料黄金原料。",
|
||||
"unit_price": 22.00,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"膨化全脂大豆": {
|
||||
"descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。",
|
||||
"unit_price": 4.10,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"芝麻粕": {
|
||||
"descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。",
|
||||
"unit_price": 2.90,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"花生秧粉": {
|
||||
"descriptions": "粗饲料,母猪用。",
|
||||
"unit_price": 0.85,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"花生粕": {
|
||||
"descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。",
|
||||
"unit_price": 3.70,
|
||||
"max_ratio": 5.00
|
||||
},
|
||||
"苜蓿草块": {
|
||||
"descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。",
|
||||
"unit_price": 2.40,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"苜蓿草粉": {
|
||||
"descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。",
|
||||
"unit_price": 2.50,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"苹果渣": {
|
||||
"descriptions": "湿态副产品,适口性好,母猪喜欢。",
|
||||
"unit_price": 0.55,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"菜籽粕": {
|
||||
"descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。",
|
||||
"unit_price": 2.30,
|
||||
"max_ratio": 15.00
|
||||
},
|
||||
"葡萄糖": {
|
||||
"descriptions": "快速能量源,教槽料常用,缓解应激。",
|
||||
"unit_price": 3.80,
|
||||
"max_ratio": 15.00
|
||||
},
|
||||
"葵花籽粕": {
|
||||
"descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。",
|
||||
"unit_price": 2.10,
|
||||
"max_ratio": 35.00
|
||||
},
|
||||
"蔗糖": {
|
||||
"descriptions": "高能量碳水,教槽料诱食用。",
|
||||
"unit_price": 6.50,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"虾粉": {
|
||||
"descriptions": "优质动物蛋白,含虾青素,改善体色。",
|
||||
"unit_price": 6.00,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"蚕蛹粉": {
|
||||
"descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。",
|
||||
"unit_price": 8.00,
|
||||
"max_ratio": 15.00
|
||||
},
|
||||
"蚕豆": {
|
||||
"descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。",
|
||||
"unit_price": 3.40,
|
||||
"max_ratio": 50.00
|
||||
},
|
||||
"蟹粉": {
|
||||
"descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。",
|
||||
"unit_price": 4.50,
|
||||
"max_ratio": 10.00
|
||||
},
|
||||
"血浆蛋白粉": {
|
||||
"descriptions": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。",
|
||||
"unit_price": 45.00,
|
||||
"max_ratio": 5.00
|
||||
},
|
||||
"血粉": {
|
||||
"descriptions": "赖氨酸极高,但适口性差,需喷涂使用。",
|
||||
"unit_price": 6.50,
|
||||
"max_ratio": 5.00
|
||||
},
|
||||
"豆磷脂": {
|
||||
"descriptions": "高能量乳化剂,促进脂肪消化,改善皮毛。",
|
||||
"unit_price": 6.80,
|
||||
"max_ratio": 1.00
|
||||
},
|
||||
"豌豆": {
|
||||
"descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。",
|
||||
"unit_price": 3.50,
|
||||
"max_ratio": 50.00
|
||||
},
|
||||
"豌豆蛋白": {
|
||||
"descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。",
|
||||
"unit_price": 9.50,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"进口鱼粉65": {
|
||||
"descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。",
|
||||
"unit_price": 12.80,
|
||||
"max_ratio": 30.00
|
||||
},
|
||||
"食盐": {
|
||||
"descriptions": "提供钠和氯,调节电解质平衡。",
|
||||
"unit_price": 0.50,
|
||||
"max_ratio": 1.00
|
||||
},
|
||||
"饲料酵母粉": {
|
||||
"descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。",
|
||||
"unit_price": 6.50,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"高粱": {
|
||||
"descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。",
|
||||
"unit_price": 2.20,
|
||||
"max_ratio": 50.00
|
||||
},
|
||||
"鱼油": {
|
||||
"descriptions": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。",
|
||||
"unit_price": 18.00,
|
||||
"max_ratio": 3.00
|
||||
},
|
||||
"鸡肉粉": {
|
||||
"descriptions": "优质陆基动物蛋白,消化率高,适口性好。",
|
||||
"unit_price": 7.50,
|
||||
"max_ratio": 25.00
|
||||
},
|
||||
"鸭肉粉": {
|
||||
"descriptions": "与鸡肉粉类似,脂肪稍高。",
|
||||
"unit_price": 7.20,
|
||||
"max_ratio": 20.00
|
||||
},
|
||||
"鹅肉粉": {
|
||||
"descriptions": "蛋白和脂肪中等,质量稳定。",
|
||||
"unit_price": 7.00,
|
||||
"max_ratio": 20.00
|
||||
}
|
||||
},
|
||||
"nutrients": {
|
||||
"可消化蛋氨酸 (SID %)": "猪第二限制性氨基酸,直接影响瘦肉率和生长速度。",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,6 +46,20 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
|
||||
7. 简单查看功能
|
||||
- 两个配方对比页面(营养+成本对比)
|
||||
|
||||
# 实现总结
|
||||
|
||||
## 实现内容
|
||||
|
||||
实现库存和原料和营养和猪营养需求的管理, 支持根据库存和已录入原料和猪营养需求生成配方
|
||||
|
||||
## TODO
|
||||
|
||||
1. 发酵料管理考虑到发酵目前没有自动化流程, 不好追踪, 遂暂时不做
|
||||
2. 目前的价格是根据原料的参考价设置的, 后续应当实现一个在服务供平台采集参考价, 以及使用原料采购价计算
|
||||
3. 原料应该加上膨润土等, 比如膨润土的黄曲霉素含量应该是负数以表示减少饲料里的含量
|
||||
4. 饲料保质期考虑到批次间管理暂时不方便, 等可以实现同一原料先进先出后再实现
|
||||
5. 暂时不支持指定原料列表然后自动生成, 也不支持告诉用户当前生成不出是为什么, 等以后再做
|
||||
|
||||
# 完成事项
|
||||
|
||||
1. 定义原料表, 营养表, 原料营养表, 原料库存变更表
|
||||
@@ -63,3 +77,6 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
|
||||
13. 重构配方领域
|
||||
14. 配方增删改查服务层和控制器
|
||||
15. 实现库存管理相关逻辑
|
||||
16. 实现配方生成器
|
||||
17. 实现使用系统中所有可用的原料一键生成配方
|
||||
18. 实现优先使用库存的配方一键生成
|
||||
245
docs/docs.go
245
docs/docs.go
@@ -3145,6 +3145,98 @@ 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/generate-prioritized-stock/{pig_type_id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"饲料管理-配方"
|
||||
],
|
||||
"summary": "使用优先有库存原料的策略生成配方",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "猪类型ID",
|
||||
"name": "pig_type_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "业务码为201代表创建成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.GenerateRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3279,6 +3371,59 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/{id}/ai-diagnose": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "使用AI对指定配方进行点评,并针对目标猪类型给出建议。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"饲料管理-配方"
|
||||
],
|
||||
"summary": "AI点评配方",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "配方ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "猪类型ID",
|
||||
"name": "pig_type_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表AI点评成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ReviewRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inventory/stock/adjust": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -3346,6 +3491,12 @@ const docTemplate = `{
|
||||
],
|
||||
"summary": "获取当前库存列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "只查询有库存的原料",
|
||||
"name": "has_stock",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "排序字段, 例如 \"stock DESC\"",
|
||||
@@ -3669,7 +3820,6 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -3679,12 +3829,12 @@ const docTemplate = `{
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -3694,7 +3844,8 @@ const docTemplate = `{
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
],
|
||||
"name": "level",
|
||||
"in": "query"
|
||||
@@ -7221,6 +7372,10 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
@@ -7290,6 +7445,14 @@ const docTemplate = `{
|
||||
"dto.CurrentStockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"last_operation_source_type": {
|
||||
"description": "上次库存变动的来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"last_updated": {
|
||||
"description": "最后更新时间",
|
||||
"type": "string"
|
||||
@@ -7439,6 +7602,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": {
|
||||
@@ -8761,6 +8941,10 @@ const docTemplate = `{
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9034,6 +9218,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ReviewRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ai_model": {
|
||||
"description": "使用的 AI 模型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.AIModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"review_message": {
|
||||
"description": "点评内容",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SellPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -9134,7 +9335,8 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"change_amount",
|
||||
"raw_material_id"
|
||||
"raw_material_id",
|
||||
"source_type"
|
||||
],
|
||||
"properties": {
|
||||
"change_amount": {
|
||||
@@ -9149,6 +9351,18 @@ const docTemplate = `{
|
||||
"description": "备注",
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"source_id": {
|
||||
"description": "来源ID, 例如: 配方ID, 采购单ID等",
|
||||
"type": "integer"
|
||||
},
|
||||
"source_type": {
|
||||
"description": "库存变动来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9801,6 +10015,10 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
@@ -9936,6 +10154,15 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.AIModel": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Gemini"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"AI_MODEL_GEMINI"
|
||||
]
|
||||
},
|
||||
"models.AlarmCode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -10523,7 +10750,6 @@ const docTemplate = `{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -10533,10 +10759,10 @@ const docTemplate = `{
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -10546,7 +10772,8 @@ const docTemplate = `{
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3137,6 +3137,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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/generate-prioritized-stock/{pig_type_id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"饲料管理-配方"
|
||||
],
|
||||
"summary": "使用优先有库存原料的策略生成配方",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "猪类型ID",
|
||||
"name": "pig_type_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "业务码为201代表创建成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.GenerateRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3271,6 +3363,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/{id}/ai-diagnose": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "使用AI对指定配方进行点评,并针对目标猪类型给出建议。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"饲料管理-配方"
|
||||
],
|
||||
"summary": "AI点评配方",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "配方ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "猪类型ID",
|
||||
"name": "pig_type_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表AI点评成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ReviewRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inventory/stock/adjust": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -3338,6 +3483,12 @@
|
||||
],
|
||||
"summary": "获取当前库存列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "只查询有库存的原料",
|
||||
"name": "has_stock",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "排序字段, 例如 \"stock DESC\"",
|
||||
@@ -3661,7 +3812,6 @@
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -3671,12 +3821,12 @@
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -3686,7 +3836,8 @@
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
],
|
||||
"name": "level",
|
||||
"in": "query"
|
||||
@@ -7213,6 +7364,10 @@
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
@@ -7282,6 +7437,14 @@
|
||||
"dto.CurrentStockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"last_operation_source_type": {
|
||||
"description": "上次库存变动的来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"last_updated": {
|
||||
"description": "最后更新时间",
|
||||
"type": "string"
|
||||
@@ -7431,6 +7594,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.GenerateRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "新生成的配方描述",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "新生成的配方ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "新生成的配方名称",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.HistoricalAlarmDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8753,6 +8933,10 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9026,6 +9210,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ReviewRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ai_model": {
|
||||
"description": "使用的 AI 模型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.AIModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"review_message": {
|
||||
"description": "点评内容",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SellPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -9126,7 +9327,8 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"change_amount",
|
||||
"raw_material_id"
|
||||
"raw_material_id",
|
||||
"source_type"
|
||||
],
|
||||
"properties": {
|
||||
"change_amount": {
|
||||
@@ -9141,6 +9343,18 @@
|
||||
"description": "备注",
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"source_id": {
|
||||
"description": "来源ID, 例如: 配方ID, 采购单ID等",
|
||||
"type": "integer"
|
||||
},
|
||||
"source_type": {
|
||||
"description": "库存变动来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9793,6 +10007,10 @@
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
@@ -9928,6 +10146,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.AIModel": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Gemini"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"AI_MODEL_GEMINI"
|
||||
]
|
||||
},
|
||||
"models.AlarmCode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -10515,7 +10742,6 @@
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"enum": [
|
||||
7,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
@@ -10525,10 +10751,10 @@
|
||||
5,
|
||||
-1,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"_numLevels",
|
||||
"DebugLevel",
|
||||
"InfoLevel",
|
||||
"WarnLevel",
|
||||
@@ -10538,7 +10764,8 @@
|
||||
"FatalLevel",
|
||||
"_minLevel",
|
||||
"_maxLevel",
|
||||
"InvalidLevel"
|
||||
"InvalidLevel",
|
||||
"_numLevels"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -416,6 +416,9 @@ definitions:
|
||||
description: 描述
|
||||
maxLength: 255
|
||||
type: string
|
||||
max_addition_ratio:
|
||||
description: 最大添加比例
|
||||
type: number
|
||||
name:
|
||||
description: 原料名称
|
||||
maxLength: 100
|
||||
@@ -467,6 +470,10 @@ definitions:
|
||||
type: object
|
||||
dto.CurrentStockResponse:
|
||||
properties:
|
||||
last_operation_source_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.StockLogSourceType'
|
||||
description: 上次库存变动的来源类型
|
||||
last_updated:
|
||||
description: 最后更新时间
|
||||
type: string
|
||||
@@ -565,6 +572,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:
|
||||
@@ -1434,6 +1453,9 @@ definitions:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
max_addition_ratio:
|
||||
description: 最大添加比例
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
raw_material_nutrients:
|
||||
@@ -1626,6 +1648,16 @@ definitions:
|
||||
- quantity
|
||||
- treatment_location
|
||||
type: object
|
||||
dto.ReviewRecipeResponse:
|
||||
properties:
|
||||
ai_model:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.AIModel'
|
||||
description: 使用的 AI 模型
|
||||
review_message:
|
||||
description: 点评内容
|
||||
type: string
|
||||
type: object
|
||||
dto.SellPigsRequest:
|
||||
properties:
|
||||
pen_id:
|
||||
@@ -1705,9 +1737,17 @@ definitions:
|
||||
description: 备注
|
||||
maxLength: 255
|
||||
type: string
|
||||
source_id:
|
||||
description: '来源ID, 例如: 配方ID, 采购单ID等'
|
||||
type: integer
|
||||
source_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.StockLogSourceType'
|
||||
description: 库存变动来源类型
|
||||
required:
|
||||
- change_amount
|
||||
- raw_material_id
|
||||
- source_type
|
||||
type: object
|
||||
dto.StockLogResponse:
|
||||
properties:
|
||||
@@ -2148,6 +2188,9 @@ definitions:
|
||||
description: 描述
|
||||
maxLength: 255
|
||||
type: string
|
||||
max_addition_ratio:
|
||||
description: 最大添加比例
|
||||
type: number
|
||||
name:
|
||||
description: 原料名称
|
||||
maxLength: 100
|
||||
@@ -2241,6 +2284,12 @@ definitions:
|
||||
weight:
|
||||
type: number
|
||||
type: object
|
||||
models.AIModel:
|
||||
enum:
|
||||
- Gemini
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- AI_MODEL_GEMINI
|
||||
models.AlarmCode:
|
||||
enum:
|
||||
- 温度阈值
|
||||
@@ -2719,7 +2768,6 @@ definitions:
|
||||
- PlanTypeFilterSystem
|
||||
zapcore.Level:
|
||||
enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -2730,10 +2778,10 @@ definitions:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -2744,6 +2792,7 @@ definitions:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
info:
|
||||
contact:
|
||||
email: divano@example.com
|
||||
@@ -4722,6 +4771,89 @@ paths:
|
||||
summary: 更新配方
|
||||
tags:
|
||||
- 饲料管理-配方
|
||||
/api/v1/feed/recipes/{id}/ai-diagnose:
|
||||
get:
|
||||
description: 使用AI对指定配方进行点评,并针对目标猪类型给出建议。
|
||||
parameters:
|
||||
- description: 配方ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: 猪类型ID
|
||||
in: query
|
||||
name: pig_type_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 业务码为200代表AI点评成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.ReviewRecipeResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: AI点评配方
|
||||
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/feed/recipes/generate-prioritized-stock/{pig_type_id}:
|
||||
post:
|
||||
description: 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。
|
||||
parameters:
|
||||
- description: 猪类型ID
|
||||
in: path
|
||||
name: pig_type_id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: 业务码为201代表创建成功
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/controller.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/dto.GenerateRecipeResponse'
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 使用优先有库存原料的策略生成配方
|
||||
tags:
|
||||
- 饲料管理-配方
|
||||
/api/v1/inventory/stock/adjust:
|
||||
post:
|
||||
consumes:
|
||||
@@ -4755,6 +4887,10 @@ paths:
|
||||
get:
|
||||
description: 获取所有原料的当前库存列表,支持分页和过滤。
|
||||
parameters:
|
||||
- description: 只查询有库存的原料
|
||||
in: query
|
||||
name: has_stock
|
||||
type: boolean
|
||||
- description: 排序字段, 例如 "stock DESC"
|
||||
in: query
|
||||
name: order_by
|
||||
@@ -4947,7 +5083,6 @@ paths:
|
||||
name: end_time
|
||||
type: string
|
||||
- enum:
|
||||
- 7
|
||||
- -1
|
||||
- 0
|
||||
- 1
|
||||
@@ -4958,12 +5093,12 @@ paths:
|
||||
- -1
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
format: int32
|
||||
in: query
|
||||
name: level
|
||||
type: integer
|
||||
x-enum-varnames:
|
||||
- _numLevels
|
||||
- DebugLevel
|
||||
- InfoLevel
|
||||
- WarnLevel
|
||||
@@ -4974,6 +5109,7 @@ paths:
|
||||
- _minLevel
|
||||
- _maxLevel
|
||||
- InvalidLevel
|
||||
- _numLevels
|
||||
- enum:
|
||||
- 邮件
|
||||
- 企业微信
|
||||
|
||||
35
go.mod
35
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/go-openapi/swag v0.25.1
|
||||
github.com/go-openapi/validate v0.24.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/generative-ai-go v0.20.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/panjf2000/ants/v2 v2.11.3
|
||||
@@ -19,7 +20,9 @@ require (
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gonum.org/v1/gonum v0.16.0
|
||||
google.golang.org/api v0.256.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/datatypes v1.2.6
|
||||
@@ -28,11 +31,18 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.7 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
@@ -51,7 +61,9 @@ require (
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
@@ -71,18 +83,25 @@ require (
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||
|
||||
87
go.sum
87
go.sum
@@ -1,17 +1,38 @@
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
|
||||
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||
@@ -67,10 +88,20 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
|
||||
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -107,6 +138,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -138,14 +171,22 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
@@ -160,19 +201,33 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
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/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -259,6 +259,9 @@ 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)
|
||||
feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateRecipeWithPrioritizedStockRawMaterials)
|
||||
feedGroup.GET("/recipes/:id/ai-diagnose", a.recipeController.AIDiagnoseRecipe)
|
||||
}
|
||||
logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)")
|
||||
|
||||
|
||||
@@ -194,3 +194,110 @@ 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)
|
||||
}
|
||||
|
||||
// GenerateRecipeWithPrioritizedStockRawMaterials godoc
|
||||
// @Summary 使用优先有库存原料的策略生成配方
|
||||
// @Description 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。
|
||||
// @Tags 饲料管理-配方
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param pig_type_id path int true "猪类型ID"
|
||||
// @Success 201 {object} controller.Response{data=dto.GenerateRecipeResponse} "业务码为201代表创建成功"
|
||||
// @Router /api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id} [post]
|
||||
func (c *RecipeController) GenerateRecipeWithPrioritizedStockRawMaterials(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials")
|
||||
const actionType = "生成优先使用库存原料的配方"
|
||||
|
||||
idStr := ctx.Param("pig_type_id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr)
|
||||
}
|
||||
|
||||
recipe, err := c.recipeService.GenerateRecipeWithPrioritizedStockRawMaterials(reqCtx, uint32(id))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层生成配方失败: %v, PigTypeID: %d", actionType, err, id)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "生成配方失败: "+err.Error(), actionType, "服务层生成配方失败", id)
|
||||
}
|
||||
|
||||
resp := dto.ToGenerateRecipeResponse(recipe)
|
||||
logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp)
|
||||
}
|
||||
|
||||
// AIDiagnoseRecipe godoc
|
||||
// @Summary AI点评配方
|
||||
// @Description 使用AI对指定配方进行点评,并针对目标猪类型给出建议。
|
||||
// @Tags 饲料管理-配方
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "配方ID"
|
||||
// @Param pig_type_id query int true "猪类型ID"
|
||||
// @Success 200 {object} controller.Response{data=dto.ReviewRecipeResponse} "业务码为200代表AI点评成功"
|
||||
// @Router /api/v1/feed/recipes/{id}/ai-diagnose [get]
|
||||
func (c *RecipeController) AIDiagnoseRecipe(ctx echo.Context) error {
|
||||
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "AIDiagnoseRecipe")
|
||||
const actionType = "AI点评配方"
|
||||
|
||||
// 从路径参数中获取配方ID
|
||||
recipeIDStr := ctx.Param("id")
|
||||
recipeID, err := strconv.ParseUint(recipeIDStr, 10, 32)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, recipeIDStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", recipeIDStr)
|
||||
}
|
||||
|
||||
// 从查询参数中获取猪类型ID
|
||||
pigTypeIDStr := ctx.QueryParam("pig_type_id")
|
||||
pigTypeID, err := strconv.ParseUint(pigTypeIDStr, 10, 32)
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, pigTypeIDStr)
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", pigTypeIDStr)
|
||||
}
|
||||
|
||||
// 调用应用服务进行AI点评
|
||||
reviewResponse, err := c.recipeService.AIDiagnoseRecipe(reqCtx, uint32(recipeID), uint32(pigTypeID))
|
||||
if err != nil {
|
||||
logger.Errorf("%s: 服务层AI点评失败: %v, RecipeID: %d, PigTypeID: %d", actionType, err, recipeID, pigTypeID)
|
||||
if errors.Is(err, service.ErrRecipeNotFound) {
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方或猪类型不存在", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)})
|
||||
}
|
||||
// 对于其他错误,统一返回内部服务器错误
|
||||
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "AI点评失败: "+err.Error(), actionType, "服务层AI点评失败", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)})
|
||||
}
|
||||
|
||||
logger.Infof("%s: AI点评成功, RecipeID: %d, PigTypeID: %d", actionType, recipeID, pigTypeID)
|
||||
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "AI点评成功", reviewResponse, actionType, "AI点评成功", reviewResponse)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dto
|
||||
|
||||
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
// =============================================================================================================
|
||||
// 营养种类 (Nutrient) 相关 DTO
|
||||
// =============================================================================================================
|
||||
@@ -55,6 +57,7 @@ type CreateRawMaterialRequest struct {
|
||||
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 更新原料的请求体
|
||||
@@ -62,6 +65,7 @@ type UpdateRawMaterialRequest struct {
|
||||
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 +82,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 +330,21 @@ 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"` // 新生成的配方描述
|
||||
}
|
||||
|
||||
// ReviewRecipeRequest 定义了点评配方的请求体
|
||||
type ReviewRecipeRequest struct {
|
||||
PigTypeID uint32 `json:"pig_type_id" binding:"required"` // 猪类型ID
|
||||
}
|
||||
|
||||
// ReviewRecipeResponse 定义了点评配方的响应体
|
||||
type ReviewRecipeResponse struct {
|
||||
ReviewMessage string `json:"review_message"` // 点评内容
|
||||
AIModel models.AIModel `json:"ai_model"` // 使用的 AI 模型
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ func ConvertCurrentStockToDTO(material *models.RawMaterial, latestLog *models.Ra
|
||||
|
||||
stock := float32(0)
|
||||
lastUpdated := material.CreatedAt.Format(time.RFC3339) // 默认使用创建时间
|
||||
var lastOperationSourceType models.StockLogSourceType
|
||||
|
||||
if latestLog != nil {
|
||||
stock = latestLog.AfterQuantity
|
||||
lastUpdated = latestLog.HappenedAt.Format(time.RFC3339)
|
||||
lastOperationSourceType = latestLog.SourceType
|
||||
}
|
||||
|
||||
return &CurrentStockResponse{
|
||||
@@ -25,6 +27,7 @@ func ConvertCurrentStockToDTO(material *models.RawMaterial, latestLog *models.Ra
|
||||
RawMaterialName: material.Name,
|
||||
Stock: stock,
|
||||
LastUpdated: lastUpdated,
|
||||
LastOperationSourceType: lastOperationSourceType,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
type StockAdjustmentRequest struct {
|
||||
RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 要调整的原料ID
|
||||
ChangeAmount float32 `json:"change_amount" validate:"required,ne=0"` // 变动数量, 正数为入库, 负数为出库, 单位: g
|
||||
SourceType models.StockLogSourceType `json:"source_type" validate:"required"` // 库存变动来源类型
|
||||
SourceID *uint32 `json:"source_id,omitempty"` // 来源ID, 例如: 配方ID, 采购单ID等
|
||||
Remarks string `json:"remarks" validate:"max=255"` // 备注
|
||||
}
|
||||
|
||||
@@ -23,6 +25,7 @@ type CurrentStockResponse struct {
|
||||
RawMaterialName string `json:"raw_material_name"` // 原料名称
|
||||
Stock float32 `json:"stock"` // 当前库存量, 单位: g
|
||||
LastUpdated string `json:"last_updated"` // 最后更新时间
|
||||
LastOperationSourceType models.StockLogSourceType `json:"last_operation_source_type"` // 上次库存变动的来源类型
|
||||
}
|
||||
|
||||
// ListCurrentStockRequest 定义了获取当前库存列表的请求参数
|
||||
@@ -31,6 +34,7 @@ type ListCurrentStockRequest struct {
|
||||
PageSize int `json:"page_size" query:"page_size"` // 每页数量
|
||||
RawMaterialName *string `json:"raw_material_name" query:"raw_material_name"` // 按原料名称模糊查询
|
||||
OrderBy string `json:"order_by" query:"order_by"` // 排序字段, 例如 "stock DESC"
|
||||
HasStock *bool `json:"has_stock" query:"has_stock"` // 只查询有库存的原料
|
||||
}
|
||||
|
||||
// ListCurrentStockResponse 是获取当前库存列表的响应结构
|
||||
|
||||
@@ -47,7 +47,7 @@ func (s *inventoryServiceImpl) AdjustStock(ctx context.Context, req *dto.StockAd
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AdjustStock")
|
||||
|
||||
// 调用领域服务执行核心业务逻辑
|
||||
log, err := s.invSvc.AdjustStock(serviceCtx, req.RawMaterialID, req.ChangeAmount, models.StockLogSourceManual, nil, req.Remarks)
|
||||
log, err := s.invSvc.AdjustStock(serviceCtx, req.RawMaterialID, req.ChangeAmount, req.SourceType, req.SourceID, req.Remarks)
|
||||
if err != nil {
|
||||
if errors.Is(err, inventory.ErrRawMaterialNotFound) {
|
||||
return nil, ErrInventoryRawMaterialNotFound
|
||||
@@ -77,6 +77,7 @@ func (s *inventoryServiceImpl) ListCurrentStock(ctx context.Context, req *dto.Li
|
||||
rawMatOpts := repository.RawMaterialListOptions{
|
||||
Name: req.RawMaterialName,
|
||||
OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序
|
||||
HasStock: req.HasStock,
|
||||
}
|
||||
rawMaterials, total, err := s.rawMatRepo.ListRawMaterials(serviceCtx, rawMatOpts, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,22 +25,42 @@ 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)
|
||||
// GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料
|
||||
GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
|
||||
// AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果和使用的AI模型
|
||||
AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, 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)
|
||||
}
|
||||
|
||||
// GenerateRecipeWithPrioritizedStockRawMaterials 实现生成优先使用库存原料配方的方法
|
||||
func (s *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials")
|
||||
// 直接调用领域服务的方法
|
||||
return s.recipeSvc.GenerateRecipeWithPrioritizedStockRawMaterials(serviceCtx, pigTypeID)
|
||||
}
|
||||
|
||||
// CreateRecipe 创建配方
|
||||
func (s *recipeServiceImpl) CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe")
|
||||
@@ -157,3 +177,18 @@ func (s *recipeServiceImpl) ListRecipes(ctx context.Context, req *dto.ListRecipe
|
||||
|
||||
return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil
|
||||
}
|
||||
|
||||
// AIDiagnoseRecipe 实现智能诊断配方的方法
|
||||
func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, error) {
|
||||
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AIDiagnoseRecipe")
|
||||
|
||||
reviewMessage, aiModel, err := s.recipeSvc.AIDiagnoseRecipe(serviceCtx, recipeID, pigTypeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI 诊断配方失败: %w", err)
|
||||
}
|
||||
|
||||
return &dto.ReviewRecipeResponse{
|
||||
ReviewMessage: reviewMessage,
|
||||
AIModel: aiModel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/plan"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
|
||||
infra_ai "git.huangwc.com/pig/pig-farm-controller/internal/infra/ai"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
@@ -34,6 +35,7 @@ type Infrastructure struct {
|
||||
storage database.Storage
|
||||
repos *Repositories
|
||||
lora *LoraComponents
|
||||
ai infra_ai.AI
|
||||
tokenGenerator token.Generator
|
||||
}
|
||||
|
||||
@@ -53,10 +55,17 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur
|
||||
|
||||
tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret))
|
||||
|
||||
// 初始化 AI
|
||||
ai, err := initAI(ctx, cfg.AI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化 AI 管理器失败: %w", err)
|
||||
}
|
||||
|
||||
return &Infrastructure{
|
||||
storage: storage,
|
||||
repos: repos,
|
||||
lora: lora,
|
||||
ai: ai,
|
||||
tokenGenerator: tokenGenerator,
|
||||
}, nil
|
||||
}
|
||||
@@ -228,6 +237,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 +246,9 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
|
||||
pigAgeStageService,
|
||||
pigTypeService,
|
||||
recipeCoreService,
|
||||
recipeGenerateManager,
|
||||
infra.repos.recipeRepo,
|
||||
infra.ai,
|
||||
)
|
||||
|
||||
return &DomainServices{
|
||||
@@ -506,3 +519,12 @@ func initStorage(ctx context.Context, cfg config.DatabaseConfig) (database.Stora
|
||||
logs.GetLogger(ctx).Info("数据库初始化完成。")
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func initAI(ctx context.Context, cfg config.AIConfig) (infra_ai.AI, error) {
|
||||
switch cfg.Model {
|
||||
case models.AI_MODEL_GEMINI:
|
||||
return infra_ai.NewGeminiAI(ctx, cfg.Gemini)
|
||||
default:
|
||||
return infra_ai.NewNoneAI(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, areaControllerID uin
|
||||
logger.Errorf("创建待采集请求失败 (CorrelationID: %s): %v", correlationID, err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID)
|
||||
logger.Debugf("成功创建待采集请求 (CorrelationID: %s, DeviceID: %d)", correlationID, areaController.ID)
|
||||
|
||||
// 5. 构建最终的空中载荷
|
||||
batchCmd := &proto.BatchCollectCommand{
|
||||
@@ -240,12 +240,12 @@ func (g *GeneralDeviceService) Collect(ctx context.Context, areaControllerID uin
|
||||
logger.Errorf("序列化采集指令失败 (CorrelationID: %s): %v", correlationID, err)
|
||||
return err
|
||||
}
|
||||
logger.Infof("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction)
|
||||
logger.Debugf("构造空中载荷成功: networkID: %v, payload: %v", networkID, instruction)
|
||||
if _, err := g.comm.Send(serviceCtx, networkID, payload); err != nil {
|
||||
logger.DPanicf("待采集请求 (CorrelationID: %s) 已创建,但发送到设备失败: %v。数据可能不一致!", correlationID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID)
|
||||
logger.Debugf("成功将采集请求 (CorrelationID: %s) 发送到设备 %s", correlationID, networkID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ var (
|
||||
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")
|
||||
|
||||
// 检查名称是否已存在
|
||||
@@ -71,6 +72,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de
|
||||
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)
|
||||
}
|
||||
|
||||
332
internal/domain/recipe/recipe_generate_manager.go
Normal file
332
internal/domain/recipe/recipe_generate_manager.go
Normal file
@@ -0,0 +1,332 @@
|
||||
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)
|
||||
}
|
||||
// 松弛变量的成本为 0,Go 默认初始化为 0,无需操作
|
||||
|
||||
// 填充 Row 0: 总量约束 (Sum(x) = 1)
|
||||
// 系数: 所有原料对应列为 1,松弛变量列为 0
|
||||
for j := 0; j < numMaterials; j++ {
|
||||
A.Set(0, j, 1.0)
|
||||
}
|
||||
b[0] = 1.0
|
||||
|
||||
// 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
|
||||
_, 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 种原料计算的最优成本配方。", actualMaterialCount), // 提供一个默认的描述
|
||||
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)
|
||||
}
|
||||
|
||||
return recipe, nil
|
||||
}
|
||||
@@ -2,6 +2,14 @@ package recipe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/ai"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
|
||||
)
|
||||
|
||||
// Service 定义了配方与原料领域的核心业务服务接口
|
||||
@@ -13,6 +21,12 @@ type Service interface {
|
||||
PigAgeStageService
|
||||
PigTypeService
|
||||
RecipeCoreService
|
||||
RecipeGenerateManager
|
||||
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
|
||||
// GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料
|
||||
GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
|
||||
// AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果
|
||||
AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error)
|
||||
}
|
||||
|
||||
// recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现
|
||||
@@ -24,6 +38,10 @@ type recipeServiceImpl struct {
|
||||
PigAgeStageService
|
||||
PigTypeService
|
||||
RecipeCoreService
|
||||
RecipeGenerateManager
|
||||
|
||||
recipeRepo repository.RecipeRepository
|
||||
ai ai.AI
|
||||
}
|
||||
|
||||
// NewRecipeService 创建一个新的 Service 实例
|
||||
@@ -35,6 +53,9 @@ func NewRecipeService(
|
||||
pigAgeStageService PigAgeStageService,
|
||||
pigTypeService PigTypeService,
|
||||
recipeCoreService RecipeCoreService,
|
||||
recipeGenerateManager RecipeGenerateManager,
|
||||
recipeRepo repository.RecipeRepository,
|
||||
ai ai.AI,
|
||||
) Service {
|
||||
return &recipeServiceImpl{
|
||||
ctx: ctx,
|
||||
@@ -44,5 +65,285 @@ func NewRecipeService(
|
||||
PigAgeStageService: pigAgeStageService,
|
||||
PigTypeService: pigTypeService,
|
||||
RecipeCoreService: recipeCoreService,
|
||||
RecipeGenerateManager: recipeGenerateManager,
|
||||
recipeRepo: recipeRepo,
|
||||
ai: ai,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateRecipeWithAllRawMaterials 使用所有已知原料为特定猪类型生成一个新配方。
|
||||
// pigTypeID: 目标猪类型的ID。
|
||||
// 返回: 生成的配方对象指针和可能的错误。
|
||||
func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, r.ctx, "GenerateRecipeWithAllRawMaterials")
|
||||
|
||||
// 1. 获取猪只类型信息,确保包含了营养需求
|
||||
pigType, err := r.GetPigTypeByID(serviceCtx, pigTypeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取猪类型信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取所有原料
|
||||
// 我们通过传递一个非常大的 pageSize 来获取所有原料,这在大多数情况下是可行的。
|
||||
// 对于超大规模系统,可能需要考虑分页迭代,但目前这是一个简单有效的策略。
|
||||
materials, _, err := r.ListRawMaterials(serviceCtx, repository.RawMaterialListOptions{}, 1, 9999)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取所有原料列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 调用生成器生成配方
|
||||
recipe, err := r.GenerateRecipe(serviceCtx, *pigType, materials)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成配方失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 丰富配方描述:计算并添加参考价格信息
|
||||
recipe.Name = fmt.Sprintf("%s - 使用所有已知原料", recipe.Name)
|
||||
|
||||
// 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本
|
||||
rawMaterialMap := make(map[uint32]models.RawMaterial)
|
||||
for _, mat := range materials {
|
||||
rawMaterialMap[mat.ID] = mat
|
||||
}
|
||||
for i := range recipe.RecipeIngredients {
|
||||
if rawMat, ok := rawMaterialMap[recipe.RecipeIngredients[i].RawMaterialID]; ok {
|
||||
recipe.RecipeIngredients[i].RawMaterial = rawMat
|
||||
} else {
|
||||
// 理论上 GenerateRecipe 应该只使用传入的 materials 中的 RawMaterialID
|
||||
// 如果出现此情况,说明 GenerateRecipe 生成了不在当前 materials 列表中的 RawMaterialID
|
||||
// 这可能是一个数据不一致或逻辑错误,记录警告以便排查
|
||||
logger.Warnf("未找到 RecipeIngredient (RawMaterialID: %d) 对应的 RawMaterial,成本计算可能不准确", recipe.RecipeIngredients[i].RawMaterialID)
|
||||
}
|
||||
}
|
||||
|
||||
referencePrice := recipe.CalculateReferencePricePerKilogram() / 100
|
||||
recipe.Description = fmt.Sprintf("%s 计算时预估成本: %.2f元/kg。", recipe.Description, referencePrice)
|
||||
|
||||
// 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。
|
||||
// 此时需要在描述中说明需要添加的廉价填充料的百分比。
|
||||
totalPercentage := recipe.CalculateTotalRawMaterialProportion()
|
||||
if totalPercentage < 99.99 { // 允许微小的浮点误差
|
||||
fillerPercentage := 100 - totalPercentage
|
||||
recipe.Description = fmt.Sprintf("%s 注意:配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage, fillerPercentage)
|
||||
|
||||
}
|
||||
|
||||
// 5. 保存新生成的配方到数据库
|
||||
// CreateRecipe 会处理配方及其成分的保存
|
||||
if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil {
|
||||
return nil, fmt.Errorf("保存生成的配方失败: %w", err)
|
||||
}
|
||||
logger.Infof("成功生成配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description)
|
||||
|
||||
// 6. 返回创建的配方 (现在它应该已经有了ID)
|
||||
return recipe, nil
|
||||
}
|
||||
|
||||
// GenerateRecipeWithPrioritizedStockRawMaterials 使用优先有库存原料的策略为特定猪类型生成一个新配方。
|
||||
// 通过大幅调低有库存原料的参考价格,诱导生成器优先使用。
|
||||
// pigTypeID: 目标猪类型的ID。
|
||||
// 返回: 生成的配方对象指针和可能的错误。
|
||||
func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, r.ctx, "GenerateRecipeWithPrioritizedStockRawMaterials")
|
||||
|
||||
// 1. 获取猪只类型信息,确保包含了营养需求
|
||||
pigType, err := r.GetPigTypeByID(serviceCtx, pigTypeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取猪类型信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 获取所有原料,并区分有库存和无库存的原料
|
||||
// 获取有库存的原料
|
||||
hasStock := true
|
||||
stockOpts := repository.RawMaterialListOptions{HasStock: &hasStock}
|
||||
stockMaterials, _, err := r.ListRawMaterials(serviceCtx, stockOpts, 1, 9999)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取有库存原料列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取无库存的原料
|
||||
hasStock = false
|
||||
noStockOpts := repository.RawMaterialListOptions{HasStock: &hasStock}
|
||||
noStockMaterials, _, err := r.ListRawMaterials(serviceCtx, noStockOpts, 1, 9999)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取无库存原料列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 合并有库存和无库存的原料,作为所有原始原料的列表,用于后续计算最终参考价格
|
||||
allOriginalMaterials := make([]models.RawMaterial, 0, len(stockMaterials)+len(noStockMaterials))
|
||||
allOriginalMaterials = append(allOriginalMaterials, stockMaterials...)
|
||||
allOriginalMaterials = append(allOriginalMaterials, noStockMaterials...)
|
||||
|
||||
// 3. 创建一个用于配方生成的原料列表,并调整有库存原料的价格
|
||||
var materialsForGeneration []models.RawMaterial
|
||||
|
||||
// 先添加有库存的原料,并调整价格
|
||||
for _, mat := range stockMaterials {
|
||||
adjustedMat := mat // 复制一份
|
||||
// 大幅调低有库存原料的参考价格,诱导生成器优先使用
|
||||
// TODO 按理说应该尽量优先使用已有原料, 但如果搭配后购买缺失原料花的钱还不如不用已有原料的另一个组合钱少怎么办
|
||||
adjustedMat.ReferencePrice = adjustedMat.ReferencePrice * 0.1
|
||||
materialsForGeneration = append(materialsForGeneration, adjustedMat)
|
||||
logger.Debugf("原料 '%s' (ID: %d) 有库存,生成配方时参考价格调整为 %.2f", mat.Name, mat.ID, adjustedMat.ReferencePrice)
|
||||
}
|
||||
// 再添加无库存的原料,保持原价
|
||||
for _, mat := range noStockMaterials {
|
||||
materialsForGeneration = append(materialsForGeneration, mat)
|
||||
}
|
||||
|
||||
// 4. 调用生成器生成配方
|
||||
recipe, err := r.GenerateRecipe(serviceCtx, *pigType, materialsForGeneration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成配方失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 丰富配方描述:计算并添加参考价格信息
|
||||
recipe.Name = fmt.Sprintf("%s - 优先使用库存已有原料", recipe.Name)
|
||||
|
||||
// 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格
|
||||
// rawMaterialMap 从 allOriginalMaterials 构建,确保使用原始价格
|
||||
rawMaterialMap := make(map[uint32]models.RawMaterial)
|
||||
for _, mat := range allOriginalMaterials {
|
||||
rawMaterialMap[mat.ID] = mat
|
||||
}
|
||||
|
||||
// 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本
|
||||
for i := range recipe.RecipeIngredients {
|
||||
if rawMat, ok := rawMaterialMap[recipe.RecipeIngredients[i].RawMaterialID]; ok {
|
||||
recipe.RecipeIngredients[i].RawMaterial = rawMat
|
||||
} else {
|
||||
logger.Warnf("未找到 RecipeIngredient (RawMaterialID: %d) 对应的原始 RawMaterial,成本计算可能不准确", recipe.RecipeIngredients[i].RawMaterialID)
|
||||
}
|
||||
}
|
||||
|
||||
referencePrice := recipe.CalculateReferencePricePerKilogram() / 100
|
||||
recipe.Description = fmt.Sprintf("使用 %v 种有库存原料和 %v 种无库存原料计算的库存原料优先使用的配方。 计算时预估成本: %.2f元/kg。", len(stockMaterials), len(noStockMaterials), referencePrice)
|
||||
|
||||
// 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。
|
||||
// 此时需要在描述中说明需要添加的廉价填充料的百分比。
|
||||
totalPercentage := recipe.CalculateTotalRawMaterialProportion()
|
||||
if totalPercentage < 99.99 { // 允许微小的浮点误差
|
||||
fillerPercentage := 100 - totalPercentage
|
||||
recipe.Description = fmt.Sprintf("%s 注意:配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage, fillerPercentage)
|
||||
}
|
||||
|
||||
// 6. 保存新生成的配方到数据库
|
||||
if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil {
|
||||
|
||||
return nil, fmt.Errorf("保存生成的配方失败: %w", err)
|
||||
}
|
||||
logger.Infof("成功生成优先使用库存原料的配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description)
|
||||
|
||||
// 7. 返回创建的配方
|
||||
return recipe, nil
|
||||
}
|
||||
|
||||
// AIDiagnoseRecipe 使用 AI 为指定食谱生成诊断。
|
||||
func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, context.Background(), "AIDiagnoseRecipe")
|
||||
|
||||
// 1. 根据 recipeID 获取配方详情
|
||||
recipe, err := s.recipeRepo.GetRecipeByID(serviceCtx, recipeID)
|
||||
if err != nil {
|
||||
logger.Errorf("获取配方详情失败: %v", err)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("获取配方详情失败: %w", err)
|
||||
}
|
||||
if recipe == nil {
|
||||
logger.Warnf("未找到配方,ID: %d", recipeID)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("未找到配方,ID: %d", recipeID)
|
||||
}
|
||||
|
||||
// 2. 获取目标猪只类型信息
|
||||
pigType, err := s.GetPigTypeByID(serviceCtx, pigTypeID)
|
||||
if err != nil {
|
||||
logger.Errorf("获取猪只类型信息失败: %v", err)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("获取猪只类型信息失败: %w", err)
|
||||
}
|
||||
if pigType == nil {
|
||||
logger.Warnf("未找到猪只类型,ID: %d", pigTypeID)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("未找到猪只类型,ID: %d", pigTypeID)
|
||||
}
|
||||
|
||||
// 3. 定义 AI 输入结构体
|
||||
type ingredientNutrient struct {
|
||||
NutrientName string `json:"nutrient_name"`
|
||||
Value float32 `json:"value"`
|
||||
}
|
||||
|
||||
type recipeIngredient struct {
|
||||
RawMaterialName string `json:"raw_material_name"`
|
||||
Percentage float32 `json:"percentage"`
|
||||
Nutrients []ingredientNutrient `json:"nutrients"`
|
||||
}
|
||||
|
||||
type aiDiagnosisInput struct {
|
||||
RecipeName string `json:"recipe_name"`
|
||||
TargetPigType struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"target_pig_type"`
|
||||
Ingredients []recipeIngredient `json:"ingredients"`
|
||||
}
|
||||
|
||||
// 4. 填充 AI 输入结构体
|
||||
input := aiDiagnosisInput{
|
||||
RecipeName: recipe.Name,
|
||||
}
|
||||
|
||||
input.TargetPigType.Name = fmt.Sprintf("%s-%s", pigType.Breed.Name, pigType.AgeStage.Name)
|
||||
|
||||
for _, ingredient := range recipe.RecipeIngredients {
|
||||
if ingredient.RawMaterial.ID == 0 {
|
||||
logger.Warnf("配方成分中存在未加载的原料信息,RecipeIngredientID: %d", ingredient.ID)
|
||||
continue
|
||||
}
|
||||
ing := recipeIngredient{
|
||||
RawMaterialName: ingredient.RawMaterial.Name,
|
||||
Percentage: ingredient.Percentage,
|
||||
}
|
||||
for _, rmn := range ingredient.RawMaterial.RawMaterialNutrients {
|
||||
if rmn.Nutrient.ID == 0 {
|
||||
logger.Warnf("原料营养成分中存在未加载的营养素信息,RawMaterialNutrientID: %d", rmn.ID)
|
||||
continue
|
||||
}
|
||||
ing.Nutrients = append(ing.Nutrients, ingredientNutrient{
|
||||
NutrientName: rmn.Nutrient.Name,
|
||||
Value: rmn.Value,
|
||||
})
|
||||
}
|
||||
input.Ingredients = append(input.Ingredients, ing)
|
||||
}
|
||||
|
||||
// 5. 序列化为 JSON 字符串
|
||||
jsonBytes, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
logger.Errorf("序列化配方和猪只类型信息为 JSON 失败: %v", err)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("序列化数据失败: %w", err)
|
||||
}
|
||||
jsonString := string(jsonBytes)
|
||||
|
||||
// 6. 构建 AI Prompt
|
||||
var promptBuilder strings.Builder
|
||||
promptBuilder.WriteString(`
|
||||
你是一个专业的动物营养师。请根据以下猪饲料配方数据,生成一份详细的、对养殖户友好的说明报告。
|
||||
说明报告应包括以下部分:
|
||||
1. 诊断猪只配方是否合理,如合理需要说明为什么合理, 如不合理需给出详细的改进建议。
|
||||
2. 关键成分分析:分析主要原料和营养成分的作用
|
||||
3. 使用建议:提供使用此配方的最佳实践和注意事项。
|
||||
\n`)
|
||||
promptBuilder.WriteString("```")
|
||||
promptBuilder.WriteString(jsonString)
|
||||
promptBuilder.WriteString("```")
|
||||
prompt := promptBuilder.String()
|
||||
|
||||
logger.Debugf("生成的 AI 诊断 Prompt: \n%s", prompt)
|
||||
|
||||
// 7. 调用 AI Manager 进行诊断
|
||||
diagnosisResult, err := s.ai.GenerateReview(serviceCtx, prompt)
|
||||
if err != nil {
|
||||
logger.Errorf("调用 AI Manager 诊断配方失败: %v", err)
|
||||
return "", s.ai.AIModel(), fmt.Errorf("AI 诊断失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("成功对配方 ID: %d (目标猪只类型 ID: %d) 进行 AI 诊断。", recipeID, pigTypeID)
|
||||
return diagnosisResult, s.ai.AIModel(), nil
|
||||
}
|
||||
|
||||
19
internal/infra/ai/ai.go
Normal file
19
internal/infra/ai/ai.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
// AI 定义了通用的 AI 管理接口。
|
||||
// 它可以用于处理各种 AI 相关的任务,例如文本生成、内容审核等。
|
||||
type AI interface {
|
||||
// GenerateReview 根据提供的文本内容生成评论。
|
||||
// prompt: 用于生成评论的输入文本。
|
||||
// 返回生成的评论字符串和可能发生的错误。
|
||||
GenerateReview(ctx context.Context, prompt string) (string, error)
|
||||
|
||||
// AIModel 返回当前使用的 AI 模型。
|
||||
AIModel() models.AIModel
|
||||
}
|
||||
73
internal/infra/ai/gemini.go
Normal file
73
internal/infra/ai/gemini.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// geminiImpl 是 Gemini AI 服务的实现。
|
||||
type geminiImpl struct {
|
||||
client *genai.GenerativeModel
|
||||
cfg config.Gemini
|
||||
}
|
||||
|
||||
// NewGeminiAI 创建一个新的 geminiImpl 实例。
|
||||
func NewGeminiAI(ctx context.Context, cfg config.Gemini) (AI, error) {
|
||||
// 检查 API Key 是否存在
|
||||
if cfg.APIKey == "" {
|
||||
return nil, fmt.Errorf("Gemini API Key 未配置")
|
||||
}
|
||||
|
||||
// 创建 Gemini 客户端
|
||||
genaiClient, err := genai.NewClient(ctx, option.WithAPIKey(cfg.APIKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 Gemini 客户端失败: %w", err)
|
||||
}
|
||||
|
||||
return &geminiImpl{
|
||||
client: genaiClient.GenerativeModel(cfg.ModelName),
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateReview 根据提供的文本内容生成评论。
|
||||
func (g *geminiImpl) GenerateReview(ctx context.Context, prompt string) (string, error) {
|
||||
serviceCtx, logger := logs.Trace(ctx, context.Background(), "GenerateReview")
|
||||
logger.Debugf("开始调用 Gemini 生成评论,prompt: %s", prompt)
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(serviceCtx, time.Duration(g.cfg.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := g.client.GenerateContent(timeoutCtx, genai.Text(prompt))
|
||||
if err != nil {
|
||||
logger.Errorf("调用 Gemini API 失败: %v", err)
|
||||
return "", fmt.Errorf("调用 Gemini API 失败: %w", err)
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
|
||||
logger.Warn("Gemini API 返回空内容或无候选评论")
|
||||
return "", fmt.Errorf("Gemini API 返回空内容或无候选评论")
|
||||
}
|
||||
|
||||
var review string
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
review += string(txt)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debugf("成功从 Gemini 生成评论: %s", review)
|
||||
return review, nil
|
||||
}
|
||||
|
||||
func (g *geminiImpl) AIModel() models.AIModel {
|
||||
return models.AI_MODEL_GEMINI
|
||||
}
|
||||
31
internal/infra/ai/no_ai.go
Normal file
31
internal/infra/ai/no_ai.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
)
|
||||
|
||||
var NoneAIError = errors.New("当前没有配置AI, 暂不支持此功能")
|
||||
|
||||
type NoneAI struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewNoneAI(ctx context.Context) AI {
|
||||
return &NoneAI{
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NoneAI) GenerateReview(ctx context.Context, prompt string) (string, error) {
|
||||
logger := logs.TraceLogger(ctx, n.ctx, "GenerateReview")
|
||||
logger.Warnf("当前没有配置AI, 无法处理AI请求, 消息: %s", prompt)
|
||||
return "", NoneAIError
|
||||
}
|
||||
|
||||
func (n *NoneAI) AIModel() models.AIModel {
|
||||
return models.AI_MODEL_NONE
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -50,6 +51,9 @@ type Config struct {
|
||||
|
||||
// AlarmNotification 告警通知配置
|
||||
AlarmNotification AlarmNotificationConfig `yaml:"alarm_notification"`
|
||||
|
||||
// AI AI服务配置
|
||||
AI AIConfig `yaml:"ai"`
|
||||
}
|
||||
|
||||
// AppConfig 代表应用基础配置
|
||||
@@ -231,6 +235,19 @@ type AlarmNotificationConfig struct {
|
||||
NotificationIntervals NotificationIntervalsConfig `yaml:"notification_intervals"`
|
||||
}
|
||||
|
||||
// AIConfig AI 服务配置
|
||||
type AIConfig struct {
|
||||
Model models.AIModel `yaml:"model"`
|
||||
Gemini Gemini `yaml:"gemini"`
|
||||
}
|
||||
|
||||
// Gemini 代表 Gemini AI 服务的配置
|
||||
type Gemini struct {
|
||||
APIKey string `yaml:"api_key"` // Gemini API Key
|
||||
ModelName string `yaml:"model_name"` // Gemini 模型名称,例如 "gemini-pro"
|
||||
Timeout int `yaml:"timeout"` // AI 请求超时时间 (秒)
|
||||
}
|
||||
|
||||
// NewConfig 创建并返回一个新的配置实例
|
||||
func NewConfig() *Config {
|
||||
// 默认值可以在这里设置,但我们优先使用配置文件中的值
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// SeederFunc 定义了处理一种特定类型预设数据文件的函数签名。
|
||||
type SeederFunc func(tx *gorm.DB, jsonData []byte) error
|
||||
type SeederFunc func(ctx context.Context, tx *gorm.DB, jsonData []byte) error
|
||||
|
||||
// isTableEmpty 检查给定模型对应的数据库表是否为空。
|
||||
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
|
||||
}
|
||||
|
||||
// SeedFromPreset 是一个通用的数据播种函数。
|
||||
// 它会读取指定目录下的所有 .json 文件,并根据文件内容中的 "type" 字段进行分发。
|
||||
// 同时,它会校验所有必需的预设类型是否都已成功加载。
|
||||
func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
|
||||
logger := logs.TraceLogger(ctx, ctx, "SeedFromPreset")
|
||||
seedCtx, logger := logs.Trace(ctx, ctx, "SeedFromPreset")
|
||||
|
||||
// 定义必须存在的预设数据类型及其处理顺序
|
||||
// 确保 "nutrient" 在 "pig_nutrient_requirements" 之前处理,因为后者依赖于前者。
|
||||
@@ -86,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
|
||||
@@ -97,7 +103,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
|
||||
for _, jsonData := range jsonDatas {
|
||||
// 获取原始文件路径用于错误报告
|
||||
originalFilePath := typeToFileMap[dataTypeStr]
|
||||
if err := seederFunc(tx, jsonData); err != nil {
|
||||
if err := seederFunc(seedCtx, tx, jsonData); err != nil {
|
||||
return fmt.Errorf("处理文件 (type: %s, path: %s) 时发生错误: %w", dataTypeStr, originalFilePath, err)
|
||||
}
|
||||
}
|
||||
@@ -118,403 +124,3 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error {
|
||||
return nil // 提交事务
|
||||
})
|
||||
}
|
||||
|
||||
// seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。
|
||||
func seedNutrients(tx *gorm.DB, jsonData []byte) error {
|
||||
// 1. 严格校验JSON文件,检查内部重复键
|
||||
if err := validateAndParseNutrientJSON(jsonData); err != nil {
|
||||
return fmt.Errorf("JSON源文件校验失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 解析简介信息
|
||||
descriptionsNode := gjson.GetBytes(jsonData, "descriptions")
|
||||
rawMaterialDescriptions := make(map[string]string)
|
||||
nutrientDescriptions := make(map[string]string)
|
||||
|
||||
if descriptionsNode.Exists() {
|
||||
descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool {
|
||||
rawMaterialDescriptions[key.String()] = value.String()
|
||||
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")
|
||||
var err error // 用于捕获 ForEach 内部的错误
|
||||
dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool {
|
||||
rawMaterialName := rawMaterialKey.String()
|
||||
var rawMaterial models.RawMaterial
|
||||
// 将 Description 放入 Create 对象中
|
||||
err = tx.Where(models.RawMaterial{Name: rawMaterialName}).
|
||||
FirstOrCreate(&rawMaterial, models.RawMaterial{
|
||||
Name: rawMaterialName,
|
||||
Description: rawMaterialDescriptions[rawMaterialName],
|
||||
}).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(tx *gorm.DB, jsonData []byte) error {
|
||||
// 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")
|
||||
var err error // 用于捕获 ForEach 内部的错误
|
||||
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 {
|
||||
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 errors.New("'data' 字段解析起始符失败")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
279
internal/infra/database/seeder/nutrient_seeder.go
Normal file
279
internal/infra/database/seeder/nutrient_seeder.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
18
internal/infra/database/seeder/utils.go
Normal file
18
internal/infra/database/seeder/utils.go
Normal 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
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
@@ -65,8 +66,8 @@ func NewLogger(cfg config.LogConfig) *Logger {
|
||||
|
||||
// 5. 构建 Logger
|
||||
// zap.AddCaller() 会记录调用日志的代码行
|
||||
// zap.AddCallerSkip(1) 可以向上跳一层调用栈,如果我们将 logger.Info 等方法再封装一层,这个选项会很有用
|
||||
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
// zap.AddCallerSkip(2) 可以向上跳两层调用栈,因为我们的日志方法被封装了两层 (Logger.Info -> Logger.logWithTrace)
|
||||
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2))
|
||||
|
||||
return &Logger{sl: zapLogger.Sugar()}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AIModel string
|
||||
|
||||
const (
|
||||
AI_MODEL_NONE AIModel = "None"
|
||||
AI_MODEL_GEMINI AIModel = "Gemini"
|
||||
)
|
||||
|
||||
// Model 用于代替gorm.Model, 使用uint32以节约空间
|
||||
type Model struct {
|
||||
ID uint32 `gorm:"primarykey"`
|
||||
|
||||
@@ -24,6 +24,7 @@ type RawMaterial struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,28 @@ func (Recipe) TableName() string {
|
||||
return "recipes"
|
||||
}
|
||||
|
||||
// CalculateTotalRawMaterialProportion 计算配方中所有原料的总比例
|
||||
func (r Recipe) CalculateTotalRawMaterialProportion() float32 {
|
||||
var totalPercentage float32
|
||||
for _, ingredient := range r.RecipeIngredients {
|
||||
totalPercentage += ingredient.Percentage
|
||||
}
|
||||
return totalPercentage
|
||||
}
|
||||
|
||||
// CalculateReferencePricePerKilogram 根据原料参考价计算配方每公斤的成本
|
||||
func (r Recipe) CalculateReferencePricePerKilogram() float32 {
|
||||
var totalCost float32
|
||||
for _, ingredient := range r.RecipeIngredients {
|
||||
// 确保 RawMaterial 已经被加载
|
||||
if ingredient.RawMaterial.ID == 0 {
|
||||
return 0.0
|
||||
}
|
||||
totalCost += ingredient.RawMaterial.ReferencePrice * ingredient.Percentage
|
||||
}
|
||||
return totalCost
|
||||
}
|
||||
|
||||
// RecipeIngredient 配方原料组成模型
|
||||
type RecipeIngredient struct {
|
||||
Model
|
||||
|
||||
@@ -18,6 +18,7 @@ type RawMaterialListOptions struct {
|
||||
NutrientName *string
|
||||
MinReferencePrice *float32 // 参考价格最小值
|
||||
MaxReferencePrice *float32 // 参考价格最大值
|
||||
HasStock *bool
|
||||
OrderBy string
|
||||
}
|
||||
|
||||
@@ -41,6 +42,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
|
||||
@@ -121,6 +123,29 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R
|
||||
db = db.Where("reference_price <= ?", *opts.MaxReferencePrice)
|
||||
}
|
||||
|
||||
// 筛选有/无库存的原料
|
||||
if opts.HasStock != nil {
|
||||
// 内部子查询:生成带有 rn 的结果集,GORM 会自动为 models.RawMaterialStockLog 添加 deleted_at IS NULL
|
||||
rankedLogsQuery := r.db.Model(&models.RawMaterialStockLog{}).
|
||||
Select("raw_material_id, after_quantity, ROW_NUMBER() OVER(PARTITION BY raw_material_id ORDER BY happened_at DESC, id DESC) as rn")
|
||||
|
||||
// 外部子查询:从 ranked_logs 中筛选 rn=1 的 raw_material_id
|
||||
latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery).
|
||||
Select("raw_material_id").
|
||||
Where("rn = 1").
|
||||
Where("after_quantity > 0")
|
||||
|
||||
if *opts.HasStock {
|
||||
// 筛选有库存的原料 (ID 在有正库存的集合中)
|
||||
db = db.Where("id IN (?)", latestStockLogSubQuery)
|
||||
} else {
|
||||
// 筛选无库存的原料 (ID 不在有正库存的集合中)
|
||||
// 包含了最新库存为0 和 没有库存日志的原料。
|
||||
db = db.Where("id NOT IN (?)", latestStockLogSubQuery)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 首先计算总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
@@ -146,6 +171,7 @@ func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMa
|
||||
"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 +355,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
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func (t *LoRaMeshUartPassthroughTransport) executeSend(ctx context.Context, req
|
||||
frame.WriteByte(currentChunk) // 当前包序号
|
||||
frame.Write(chunk) // 数据块
|
||||
|
||||
logger.Infof("构建LoRa数据包: %v", frame.Bytes())
|
||||
logger.Debugf("构建LoRa数据包: %v", frame.Bytes())
|
||||
_, err := t.port.Write(frame.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("写入串口失败: %w", err)
|
||||
|
||||
@@ -37,7 +37,7 @@ design/archive/2025-11-05-provide-logger-with-mothed/task-webhook.md
|
||||
design/archive/2025-11-06-health-check-routing/index.md
|
||||
design/archive/2025-11-06-system-plan-continuously-triggered/index.md
|
||||
design/archive/2025-11-10-exceeding-threshold-alarm/index.md
|
||||
design/recipe-management/index.md
|
||||
design/archive/2025-11-29-recipe-management/index.md
|
||||
docs/docs.go
|
||||
docs/swagger.json
|
||||
docs/swagger.yaml
|
||||
@@ -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
|
||||
@@ -144,6 +145,9 @@ internal/domain/task/task.go
|
||||
internal/infra/config/config.go
|
||||
internal/infra/database/postgres.go
|
||||
internal/infra/database/seeder.go
|
||||
internal/infra/database/seeder/nutrient_seeder.go
|
||||
internal/infra/database/seeder/pig_nutrient_requirement_seeder.go
|
||||
internal/infra/database/seeder/utils.go
|
||||
internal/infra/database/storage.go
|
||||
internal/infra/logs/context.go
|
||||
internal/infra/logs/encoder.go
|
||||
|
||||
Reference in New Issue
Block a user