Compare commits

...

33 Commits

Author SHA1 Message Date
70fad51f40 Merge pull request 'issue_72' (#73) from issue_72 into main
Reviewed-on: #73
2025-12-02 16:34:47 +08:00
6764684fe7 优化ai初始化逻辑 2025-12-02 16:34:14 +08:00
da2c296c05 去掉无效日志 2025-12-02 16:19:56 +08:00
bdf74652b3 实现ai 2025-12-02 15:51:37 +08:00
70e8627a96 增加ai配置 2025-12-02 13:38:49 +08:00
c2c6577064 格式化 2025-11-29 16:04:12 +08:00
260c7d054c 更新makefile 2025-11-29 15:54:00 +08:00
d25933cf26 Merge pull request 'issue_66' (#70) from issue_66 into main
Reviewed-on: #70
2025-11-29 15:39:49 +08:00
4aa56441ce 归档任务 2025-11-29 15:38:52 +08:00
de68151539 优化日算法 2025-11-28 22:22:39 +08:00
04b46d8025 优化日志 2025-11-28 14:37:53 +08:00
bc4355cad5 修bug 2025-11-27 22:01:37 +08:00
968d996a9b 修bug 2025-11-27 21:47:07 +08:00
d6e5d89768 优化展示 2025-11-27 21:39:09 +08:00
1b5f715dec 实现优先使用库存的配方一键生成 2025-11-27 21:06:15 +08:00
da8e1d0191 优化算法 2025-11-27 20:03:14 +08:00
33cdf7278e 增加最后一次操作类型 2025-11-27 18:32:22 +08:00
3b12802900 实现按原料是否有库存筛选 2025-11-27 17:33:28 +08:00
e6b307b0dc 修bug 2025-11-27 16:42:32 +08:00
b8e0301175 修正数据错误 2025-11-27 16:27:49 +08:00
e2da441a6d 修正数据错误 2025-11-27 15:52:38 +08:00
dca6cc5dd4 原料增加最大添加量限制 2025-11-27 00:39:01 +08:00
5c99ff7475 原料增加最大添加量限制 2025-11-26 22:56:24 +08:00
0283c250e4 重构seeder 2025-11-26 22:51:58 +08:00
5bd52df240 优化算法 2025-11-26 22:41:38 +08:00
5ad403bf86 增加原料添加量限制 2025-11-26 22:35:52 +08:00
ce3844957f 修复逻辑错误 2025-11-26 22:13:51 +08:00
6c0f655d0a 增加删除原料校验 2025-11-26 21:14:32 +08:00
29b820b846 优化展示 2025-11-26 21:08:34 +08:00
34311889e8 实现使用系统中所有可用的原料一键生成配方 2025-11-26 20:44:41 +08:00
ba60ed541c 实现配方生成器 2025-11-26 20:23:29 +08:00
35eae7b3ec 只在第一次启动平台导入预设数据 2025-11-26 15:05:34 +08:00
ca85671a4c 支持预设价格 2025-11-26 14:35:58 +08:00
41 changed files with 3166 additions and 883 deletions

View File

@@ -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目录, 里面有一些预设数据, 可以间接看作数据库内的数据, 在测试环境中他们一般会和数据库的数据保持一致
# 权限管理

View File

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

View File

@@ -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 请求超时时间 (秒)

View File

@@ -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 请求超时时间 (秒)

View File

@@ -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 %)": "猪第二限制性氨基酸,直接影响瘦肉率和生长速度。",

View File

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

View File

@@ -46,6 +46,20 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
7. 简单查看功能
- 两个配方对比页面(营养+成本对比)
# 实现总结
## 实现内容
实现库存和原料和营养和猪营养需求的管理, 支持根据库存和已录入原料和猪营养需求生成配方
## TODO
1. 发酵料管理考虑到发酵目前没有自动化流程, 不好追踪, 遂暂时不做
2. 目前的价格是根据原料的参考价设置的, 后续应当实现一个在服务供平台采集参考价, 以及使用原料采购价计算
3. 原料应该加上膨润土等, 比如膨润土的黄曲霉素含量应该是负数以表示减少饲料里的含量
4. 饲料保质期考虑到批次间管理暂时不方便, 等可以实现同一原料先进先出后再实现
5. 暂时不支持指定原料列表然后自动生成, 也不支持告诉用户当前生成不出是为什么, 等以后再做
# 完成事项
1. 定义原料表, 营养表, 原料营养表, 原料库存变更表
@@ -62,4 +76,7 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
12. 配方领域层方法
13. 重构配方领域
14. 配方增删改查服务层和控制器
15. 实现库存管理相关逻辑
15. 实现库存管理相关逻辑
16. 实现配方生成器
17. 实现使用系统中所有可用的原料一键生成配方
18. 实现优先使用库存的配方一键生成

View File

@@ -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"
]
}
},

View File

@@ -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"
]
}
},

View File

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

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

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

View File

@@ -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("饲料管理相关接口注册成功 (需要认证和审计)")

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package dto
import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
// =============================================================================================================
// 营养种类 (Nutrient) 相关 DTO
// =============================================================================================================
@@ -52,16 +54,18 @@ type ListNutrientResponse struct {
// CreateRawMaterialRequest 创建原料的请求体
type CreateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
MaxAdditionRatio float32 `json:"max_addition_ratio"` // 最大添加比例
}
// UpdateRawMaterialRequest 更新原料的请求体
type UpdateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
MaxAdditionRatio *float32 `json:"max_addition_ratio"` // 最大添加比例
}
// RawMaterialNutrientDTO 原料营养素响应体
@@ -78,6 +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 模型
}

View File

@@ -14,17 +14,20 @@ 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{
RawMaterialID: material.ID,
RawMaterialName: material.Name,
Stock: stock,
LastUpdated: lastUpdated,
RawMaterialID: material.ID,
RawMaterialName: material.Name,
Stock: stock,
LastUpdated: lastUpdated,
LastOperationSourceType: lastOperationSourceType,
}
}

View File

@@ -12,17 +12,20 @@ import (
// StockAdjustmentRequest 手动调整库存的请求体
type StockAdjustmentRequest struct {
RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 要调整的原料ID
ChangeAmount float32 `json:"change_amount" validate:"required,ne=0"` // 变动数量, 正数为入库, 负数为出库, 单位: g
Remarks string `json:"remarks" validate:"max=255"` // 备注
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"` // 备注
}
// CurrentStockResponse 单个原料及其当前库存的响应体
type CurrentStockResponse struct {
RawMaterialID uint32 `json:"raw_material_id"` // 原料ID
RawMaterialName string `json:"raw_material_name"` // 原料名称
Stock float32 `json:"stock"` // 当前库存量, 单位: g
LastUpdated string `json:"last_updated"` // 最后更新时间
RawMaterialID uint32 `json:"raw_material_id"` // 原料ID
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 是获取当前库存列表的响应结构

View File

@@ -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
@@ -75,8 +75,9 @@ func (s *inventoryServiceImpl) ListCurrentStock(ctx context.Context, req *dto.Li
// 1. 获取分页的原料列表
rawMatOpts := repository.RawMaterialListOptions{
Name: req.RawMaterialName,
OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序
Name: req.RawMaterialName,
OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序
HasStock: req.HasStock,
}
rawMaterials, total, err := s.rawMatRepo.ListRawMaterials(serviceCtx, rawMatOpts, req.Page, req.PageSize)
if err != nil {

View File

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

View File

@@ -25,22 +25,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
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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)
}
// 松弛变量的成本为 0Go 默认初始化为 0无需操作
// 填充 Row 0: 总量约束 (Sum(x) = 1)
// 系数: 所有原料对应列为 1松弛变量列为 0
for j := 0; j < numMaterials; j++ {
A.Set(0, j, 1.0)
}
b[0] = 1.0
// currentConstraintRowIndex 记录当前正在填充的约束行索引从1开始0行被总量约束占用
currentConstraintRowIndex := 1
// 填充营养约束行
for i, cons := range nutrientConstraints {
rowIndex := currentConstraintRowIndex + i
// 营养约束的松弛变量列紧跟在原料列之后
slackColIndex := numMaterials + i
b[rowIndex] = cons.limit
// 设置原料系数
for j, m := range materials {
// 获取该原料这种营养素的含量如果没有则为0
val := materialNutrients[m.ID][cons.nutrientID]
A.Set(rowIndex, j, val)
}
// 设置松弛变量系数
// 如果是下限 (>=): Sum - s = Limit => s系数为 -1
// 如果是上限 (<=): Sum + s = Limit => s系数为 +1
if cons.isMax {
A.Set(rowIndex, slackColIndex, 1.0)
} else {
A.Set(rowIndex, slackColIndex, -1.0)
}
}
currentConstraintRowIndex += numNutrientConstraints // 推进当前约束行索引
// 填充 MaxAdditionRatio 约束行
for i, cons := range maxAdditionConstraints {
rowIndex := currentConstraintRowIndex + i
// MaxAdditionRatio 约束的松弛变量列在营养约束的松弛变量之后
slackColIndex := numMaterials + numNutrientConstraints + i
// 约束形式: x_j + s_k = Limit_j (其中 x_j 是原料 j 的比例s_k 是松弛变量)
A.Set(rowIndex, cons.materialColIndex, 1.0) // 原料本身的系数
A.Set(rowIndex, slackColIndex, 1.0) // 松弛变量的系数
b[rowIndex] = cons.limit
}
// ---------------------------------------------------------
// 4. 执行单纯形法求解
// ---------------------------------------------------------
// lp.Simplex 求解: minimize c^T * x subject to A * x = b, x >= 0
_, 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
}

View File

@@ -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,14 +53,297 @@ func NewRecipeService(
pigAgeStageService PigAgeStageService,
pigTypeService PigTypeService,
recipeCoreService RecipeCoreService,
recipeGenerateManager RecipeGenerateManager,
recipeRepo repository.RecipeRepository,
ai ai.AI,
) Service {
return &recipeServiceImpl{
ctx: ctx,
NutrientService: nutrientService,
RawMaterialService: rawMaterialService,
PigBreedService: pigBreedService,
PigAgeStageService: pigAgeStageService,
PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService,
ctx: ctx,
NutrientService: nutrientService,
RawMaterialService: rawMaterialService,
PigBreedService: pigBreedService,
PigAgeStageService: pigAgeStageService,
PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService,
RecipeGenerateManager: recipeGenerateManager,
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
View 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
}

View 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
}

View 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
}

View File

@@ -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 {
// 默认值可以在这里设置,但我们优先使用配置文件中的值

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()}
}

View File

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

View File

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

View File

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

View File

@@ -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
@@ -143,9 +168,10 @@ func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMa
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial")
// 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段
updateData := map[string]interface{}{
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"reference_price": rawMaterial.ReferencePrice,
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"reference_price": rawMaterial.ReferencePrice,
"max_addition_ratio": rawMaterial.MaxAdditionRatio,
}
result := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}).Where("id = ?", rawMaterial.ID).Updates(updateData)
if result.Error != nil {
@@ -329,3 +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
}

View File

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

View File

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