Merge pull request 'issue_66' (#70) from issue_66 into main

Reviewed-on: #70
This commit is contained in:
2025-11-29 15:39:49 +08:00
78 changed files with 17997 additions and 1972 deletions

View File

@@ -0,0 +1,9 @@
name: New MCP server
version: 0.0.1
schema: v1
mcpServers:
- name: chrome-devtools
command: node
args:
- "C:\\nvm4w\\nodejs\\node_modules\\chrome-devtools-mcp\\build\\src\\index.js"
env: {}

View File

@@ -0,0 +1,26 @@
1. **语言与沟通**
* 优先用 Go 语言提供所有代码示例。
* 使用中文进行回复, 且项目中的注释、报错、枚举、日志等所有文本内容全部使用中文。
* 遇到任何不确定或模糊不清的情况,必须直接询问,绝不进行猜测。
2. **工作流程与方案制定**
* 对于所有需求都必须优先输出详尽的文字方案,只有在获得您的明确许可后,才能进行任何代码或文件的修改。
* 制定方案时严格遵循两步流程:
1. **初步方案**: 基于需求,快速形成一个概要方案。
2. **详尽方案**: 通过搜索和阅读所有涉及的代码文件,将初步方案细化为一个不包含任何模糊信息(如“可能需要”、“我需要先查找”等)的、可直接执行的最终方案。
* 如果项目根目录存在 `project_structure.txt` 文件,必须查阅该文件以全面了解项目结构,确保在制定方案和修改文件时使用准确的文件路径。
3. **文件操作与代码修改**
* 我将不再使用任何 MCP 服务提供的能力(包括但不限于 `write_file`, `create_new_file`, `replace_text_in_file` 等)来直接修改、新增或删除文件。
* 每次提出修改前,我仍会先读取文件的最新内容,并基于最新内容提供修改建议。
* 我可以在不征得同意的情况下读取任何我需要分析的文件。
* 在需要新建文件时,我将提供文件内容和建议的文件路径,由用户手动创建。
4. **注释规范**
* 积极编写有价值的功能注释、参数注释和逻辑注释。
* 绝对禁止添加任何解释性、总结性或礼貌性的“废话”注释(例如:“这段代码修复了问题”,“优化后的代码”,“新增:xxx”“注入:xxx”等
* 不得删除或修改用户已有的任何注释,包括但不限于 TODO、FIXME 或文档注释。
5. **多工具链协同应用策略**
* 我知晓并能主动运用下列独立的 MCP 服务,以最高效、最安全的方式完成任务。
* **Chrome DevTools MCP 服务 (浏览器自动化)**: 用于所有与前端浏览器相关的任务,包括页面导航、模拟用户交互、检查 DOM 和网络状态等。

1
.gitignore vendored
View File

@@ -16,6 +16,7 @@ vendor/
# IDE-specific files
.idea/
.vscode/
*.swp
*.swo

View File

@@ -1,18 +1,11 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
# 资源地址
These instructions are for AI assistants working in this project.
1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang
2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html
3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构
4. 项目中有config/presets-data目录, 里面有一些预设数据, 可以间接看作数据库内的数据, 在测试环境中他们一般会和数据库的数据保持一致
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
# 权限管理
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->
1. 我授权你执行数据库的所有查询类sql
2. 我授权你操作浏览器访问我的项目swagger文档地址和前端项目, 并允许你进行任何操作

View File

@@ -50,18 +50,23 @@ proto:
# 运行代码检查
.PHONY: lint
lint:
golangci-lint run ./...
golangci-lint run ./... -c ./config/.golangci.yml
# 测试模式(改动文件自动重编译重启)
.PHONY: dev
dev:
air
air -c ./config/.air.toml
# 启用谷歌浏览器MCP服务器
.PHONY: mcp-chrome
mcp-chrome:
node "C:\nvm4w\nodejs\node_modules\chrome-devtools-mcp\build\src\index.js"
# 启用PostgreSQL MCP服务器
.PHONY: mcp-pgsql
mcp-pgsql:
npx mcp-postgres-server "postgresql://pig-farm-controller:pig-farm-controller@192.168.5.16:5431/pig-farm-controller"
# 生成文件目录树
.PHONY: tree
@@ -80,3 +85,4 @@ tree:
.PHONY: gemini
gemini:
gemini -m "gemini-2.5-flash"

View File

@@ -12,7 +12,7 @@ server:
# 日志配置
log:
level: "debug" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
level: "info" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal"
format: "console" # 日志格式: "console" 或 "json"
enable_file: true # 是否启用文件日志
file_path: "./app_logs/app.log" # 日志文件路径

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,914 @@
{
"type": "pig_nutrient_requirements",
"data": {
"杜长大 (DLY)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
}
},
"杜大长 (DYL)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
}
},
"皮长大 (PLY)": {
"保育期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 1.2,
"max_requirement": 1.5
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.72,
"max_requirement": 1.05
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.78,
"max_requirement": 1.08
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.22,
"max_requirement": 0.30
},
"粗蛋白 (%)": {
"min_requirement": 18.0,
"max_requirement": 22.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.9,
"max_requirement": 1.2
},
"总磷 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.45
},
"代谢能 (kcal/kg)": {
"min_requirement": 3226.5,
"max_requirement": 3585.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥前期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.94,
"max_requirement": 1.10
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.55,
"max_requirement": 0.73
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.58,
"max_requirement": 0.77
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.16,
"max_requirement": 0.22
},
"粗蛋白 (%)": {
"min_requirement": 16.0,
"max_requirement": 18.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.7,
"max_requirement": 0.9
},
"总磷 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"有效磷 (%)": {
"min_requirement": 0.2,
"max_requirement": 0.40
},
"代谢能 (kcal/kg)": {
"min_requirement": 3107.0,
"max_requirement": 3346.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"育肥后期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.81,
"max_requirement": 0.90
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.58
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.48,
"max_requirement": 0.61
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.13,
"max_requirement": 0.18
},
"粗蛋白 (%)": {
"min_requirement": 14.0,
"max_requirement": 16.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.6,
"max_requirement": 0.8
},
"总磷 (%)": {
"min_requirement": 0.45,
"max_requirement": 0.6
},
"有效磷 (%)": {
"min_requirement": 0.18,
"max_requirement": 0.35
},
"代谢能 (kcal/kg)": {
"min_requirement": 2987.5,
"max_requirement": 3226.5
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
},
"二次育肥期": {
"可消化赖氨酸 (SID %)": {
"min_requirement": 0.53,
"max_requirement": 0.65
},
"蛋+胱氨酸 (%)": {
"min_requirement": 0.30,
"max_requirement": 0.41
},
"可消化苏氨酸 (SID %)": {
"min_requirement": 0.31,
"max_requirement": 0.43
},
"可消化色氨酸 (SID %)": {
"min_requirement": 0.10,
"max_requirement": 0.13
},
"粗蛋白 (%)": {
"min_requirement": 12.0,
"max_requirement": 14.0
},
"粗脂肪 (%)": {
"min_requirement": 3.0,
"max_requirement": 6.0
},
"粗纤维 (%)": {
"min_requirement": 2.0,
"max_requirement": 6.0
},
"钙 (%)": {
"min_requirement": 0.5,
"max_requirement": 0.7
},
"总磷 (%)": {
"min_requirement": 0.4,
"max_requirement": 0.55
},
"有效磷 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.30
},
"代谢能 (kcal/kg)": {
"min_requirement": 2868.0,
"max_requirement": 3107.0
},
"钠 (%)": {
"min_requirement": 0.15,
"max_requirement": 0.25
},
"氯 (%)": {
"min_requirement": 0.25,
"max_requirement": 0.45
},
"黄曲霉毒素B1 (μg/kg)": {
"max_requirement": 10
},
"呕吐毒素DON (μg/kg)": {
"max_requirement": 1
},
"玉米赤霉烯酮ZEN (μg/kg)": {
"max_requirement": 0.15
}
}
}
},
"descriptions": {
"pig_breeds": {
"杜长大 (DLY)": {
"description": "杜长大是中国市场占有率最高的商品肉猪。通过利用杜洛克、长白、大约克三品种的杂种优势,实现高效生长和高瘦肉率。",
"parent_info": "终端父本:杜洛克 (D);二元母本:长白 (L) × 大约克 (Y)。",
"appearance_features": "全身白色,体型健壮、体躯较长,肌肉发达,背腰平直。",
"breed_advantages": "生长速度快、日增重极高、饲料转化率最优、瘦肉率稳定在60%以上、适应性良好、出栏时间最短。",
"breed_disadvantages": "抗应激能力中等,对疫病和环境变化相对敏感;无法作为种猪进行自繁。"
},
"杜大长 (DYL)": {
"description": "杜大长是另一种重要的外三元猪,与杜长大体系相似,但在母本的搭配上有所区别,同样追求高生长速度和高瘦肉率。",
"parent_info": "终端父本:杜洛克 (D);二元母本:大约克 (Y) × 长白 (L)。",
"appearance_features": "全身白色,体型比杜长大略微魁梧,肌肉丰满度高。",
"breed_advantages": "生长性能和瘦肉率与杜长大相当,同时遗传了大约克母本的良好体型和生长潜力,综合性能优秀。",
"breed_disadvantages": "与杜长大相似,无法留作种用,需要依赖稳定的种源体系;对饲养管理要求高。"
},
"皮长大 (PLY)": {
"description": "皮长大是以皮特兰作为终端父本的杂交体系,专注于生产超高瘦肉率的商品肉猪。",
"parent_info": "终端父本:皮特兰 (P);二元母本:长白 (L) × 大约克 (Y)。",
"appearance_features": "多数为白色或带有黑色斑点,肌肉极其发达,后臀饱满,体型呈方形。",
"breed_advantages": "瘦肉率极高能达到65%以上),胴体丰满,背膘薄。",
"breed_disadvantages": "生长速度和日增重逊于杜长大应激敏感性极高易发生P.S.S.管理难度大肉质易出现PSE苍白、软、渗水现象影响口感和加工性能。"
}
},
"pig_age_stages": {
"保育期": "从断奶到转入生长舍。主要目标是适应固体饲料建立肠道菌群确保健康稳定过渡体重约5kg~30kg。",
"育肥前期": "小猪转入育肥舍后到体重达到约60kg的阶段。以骨骼和肌肉生长为主是高效增重期。",
"育肥后期": "体重从约60kg到达到出栏体重约110-120kg的阶段。脂肪沉积速度开始加快是出栏前的冲刺期。",
"二次育肥期": "指收购达到常规出栏体重约100-120kg的商品猪继续饲养至更大体重140-180kg+)的阶段。其存在主要受市场价格波动驱动,目的是提高单体出肉量。"
},
"pig_breed_age_stages": {
"杜长大 (DLY)": {
"保育期": {
"description": "遗传自杜洛克的生长优势在断奶后开始显现,需精细化管理以避免断奶应激和腹泻。",
"daily_feed_intake": 400.0,
"daily_gain_weight": 350.0,
"min_days": 21,
"max_days": 70,
"min_weight": 5000.0,
"max_weight": 30000.0
},
"育肥前期": {
"description": "生长速度和饲料转化率表现良好,是瘦肉沉积效率高的阶段。",
"daily_feed_intake": 1800.0,
"daily_gain_weight": 700.0,
"min_days": 71,
"max_days": 120,
"min_weight": 30000.0,
"max_weight": 60000.0
},
"育肥后期": {
"description": "继续实现较高日增重,脂肪沉积开始加快,管理目标为达到目标出栏体重并保持料肉比。",
"daily_feed_intake": 2800.0,
"daily_gain_weight": 800.0,
"min_days": 121,
"max_days": 180,
"min_weight": 60000.0,
"max_weight": 100000.0
},
"二次育肥期": {
"description": "增重效率下降,料肉比恶化,增重多为脂肪沉积,注意热应激与蹄部问题。",
"daily_feed_intake": 3500.0,
"daily_gain_weight": 600.0,
"min_days": 181,
"max_days": 240,
"min_weight": 100000.0,
"max_weight": 140000.0
}
},
"杜大长 (DYL)": {
"保育期": {
"description": "与杜长大相近,生长潜力强,管理重点为断奶适应与稳定采食。",
"daily_feed_intake": 400.0,
"daily_gain_weight": 330.0,
"min_days": 21,
"max_days": 70,
"min_weight": 5000.0,
"max_weight": 30000.0
},
"育肥前期": {
"description": "生长期增重与料肉比接近杜长大,肌肉发展迅速。",
"daily_feed_intake": 1750.0,
"daily_gain_weight": 680.0,
"min_days": 71,
"max_days": 120,
"min_weight": 30000.0,
"max_weight": 60000.0
},
"育肥后期": {
"description": "保持较高增重速度,脂肪沉积稍快于部分 DLY 群体,需配方微调以控制背膘。",
"daily_feed_intake": 2700.0,
"daily_gain_weight": 770.0,
"min_days": 121,
"max_days": 180,
"min_weight": 60000.0,
"max_weight": 100000.0
},
"二次育肥期": {
"description": "与杜长大相似,采食量高但增重多为脂肪,注意健康与福利管理。",
"daily_feed_intake": 3500.0,
"daily_gain_weight": 580.0,
"min_days": 181,
"max_days": 240,
"min_weight": 100000.0,
"max_weight": 140000.0
}
},
"皮长大 (PLY)": {
"保育期": {
"description": "个体应激敏感性可能更高,需要稳定环境与逐步换料以减少应激性下降重。",
"daily_feed_intake": 350.0,
"daily_gain_weight": 300.0,
"min_days": 21,
"max_days": 70,
"min_weight": 5000.0,
"max_weight": 30000.0
},
"育肥前期": {
"description": "增重速度一般,但瘦肉率高;需注意高应激个体的管理以避免肉质问题。",
"daily_feed_intake": 1600.0,
"daily_gain_weight": 600.0,
"min_days": 71,
"max_days": 120,
"min_weight": 30000.0,
"max_weight": 60000.0
},
"育肥后期": {
"description": "瘦肉率高且脂肪沉积较慢,但应激易导致肉质问题,育肥管理需谨慎。",
"daily_feed_intake": 2400.0,
"daily_gain_weight": 650.0,
"min_days": 121,
"max_days": 180,
"min_weight": 60000.0,
"max_weight": 100000.0
},
"二次育肥期": {
"description": "超重育肥带来的应激和死亡风险增加,通常不推荐长期二次育肥。",
"daily_feed_intake": 3200.0,
"daily_gain_weight": 450.0,
"min_days": 181,
"max_days": 240,
"min_weight": 100000.0,
"max_weight": 140000.0
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
# 需求
饲料配方管理及自动生成配方
## issue
http://git.huangwc.com/pig/pig-farm-controller/issues/66
# 开发计划
1. 原料营养价值管理
- 增删改查
- 内置60+条常用原料玉米、豆粕43、豆粕46、发酵豆粕、麸皮、次粉、DDGS、乳清粉、鱼粉、膨化大豆、各种氨基酸、预混料、油脂等
- 每种原料固定营养值消化能、粗蛋白、赖氨酸、钙、磷等15项左右
2. 饲料库存管理(代替批次)
- 字段:饲料名、当前原料种类、当前剩余量(吨)、上次入料日期、保质期剩余天数(手动填)、是否发酵料(勾选)
- 发酵料塔额外字段:
- 发酵状态(未发酵 / 正在发酵 / 已发酵可用)
- 发酵开始日期
- 发酵几天默认37天
- 水分增加比例(默认+1020%
- 营养折损系数(可调,粗蛋白-5%、能量-3%之类)
3. 猪只阶段营养需求管理
- 预设10个常用阶段教槽、仔猪、小猪、中猪、大猪、后备、怀孕前中后、哺乳
- 每个阶段维护营养需求上下限消化能、粗蛋白、赖氨酸、钙、有效磷等12项
4. 配方管理
- 按阶段建配方
- 支持增删改查 + 复制上个配方快速新建
- 配方明细:原料 + 配比(%
5. 自动生成配方(核心功能)
- 选择阶段 → 点击“自动计算最低成本配方”
- 自动读取当前所有料塔的:
- 剩余量(不够的原料自动降配比)
- 保质期剩余天数越快过期的优先用权重×1.5
- 发酵料塔如果状态是“已发酵可用”则按发酵后营养值参与计算
- 输出:总成本、营养达标情况、发酵料占比、即将过期原料使用提示
6. 配方下发与记录
- 一键下发到喂料站/料线(生成下料曲线)
- 自动记录今天用了哪个配方
7. 简单查看功能
- 两个配方对比页面(营养+成本对比)
# 实现总结
## 实现内容
实现库存和原料和营养和猪营养需求的管理, 支持根据库存和已录入原料和猪营养需求生成配方
## TODO
1. 发酵料管理考虑到发酵目前没有自动化流程, 不好追踪, 遂暂时不做
2. 目前的价格是根据原料的参考价设置的, 后续应当实现一个在服务供平台采集参考价, 以及使用原料采购价计算
3. 原料应该加上膨润土等, 比如膨润土的黄曲霉素含量应该是负数以表示减少饲料里的含量
4. 饲料保质期考虑到批次间管理暂时不方便, 等可以实现同一原料先进先出后再实现
5. 暂时不支持指定原料列表然后自动生成, 也不支持告诉用户当前生成不出是为什么, 等以后再做
# 完成事项
1. 定义原料表, 营养表, 原料营养表, 原料库存变更表
2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库
3. 定义配方领域, 实现营养元素的增删改查
4. 实现原材料的增删改查和仓库层的原料库存记录表增查
5. 定义猪的模型和营养需求模型
6. 实现从json读取猪营养需求并写入数据库
7. 实现配方领域关于猪模型和营养需求的增删改查
8. 实现配方领域的web接口
9. 实现修改原料营养信息
10. 实现修改猪营养需求
11. 配方模型定义和仓库层增删改查方法
12. 配方领域层方法
13. 重构配方领域
14. 配方增删改查服务层和控制器
15. 实现库存管理相关逻辑
16. 实现配方生成器
17. 实现使用系统中所有可用的原料一键生成配方
18. 实现优先使用库存的配方一键生成

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

35
go.mod
View File

@@ -13,17 +13,18 @@ require (
github.com/labstack/echo/v4 v4.13.4
github.com/panjf2000/ants/v2 v2.11.3
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.11.1
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.6
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
github.com/tidwall/gjson v1.18.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0
gonum.org/v1/gonum v0.16.0
google.golang.org/protobuf v1.36.9
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0
gorm.io/datatypes v1.2.6
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.5
)
@@ -31,14 +32,7 @@ require (
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/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
@@ -57,39 +51,24 @@ require (
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-cmp v0.7.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
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/echo-swagger v1.4.1 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
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
@@ -98,7 +77,6 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // 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
@@ -108,4 +86,5 @@ require (
golang.org/x/tools v0.38.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
)

142
go.sum
View File

@@ -4,27 +4,11 @@ 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/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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=
@@ -34,87 +18,49 @@ github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC0
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY=
github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4=
github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
@@ -123,7 +69,6 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -138,12 +83,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -152,10 +91,6 @@ github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcX
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -166,19 +101,12 @@ github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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=
@@ -186,40 +114,28 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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=
@@ -238,63 +154,25 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -21,7 +21,9 @@ import (
_ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/alarm"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/feed"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/health"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/inventory"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/monitor"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan"
@@ -55,6 +57,13 @@ type API struct {
monitorController *monitor.Controller // 数据监控控制器实例
healthController *health.Controller // 健康检查控制器实例
alarmController *alarm.ThresholdAlarmController // 阈值告警控制器
nutrientController *feed.NutrientController // 营养控制器实例
pigAgeStageController *feed.PigAgeStageController // 猪龄阶段控制器实例
pigBreedController *feed.PigBreedController // 猪品种控制器实例
pigTypeController *feed.PigTypeController // 猪种类控制器实例
rawMaterialController *feed.RawMaterialController // 原料控制器实例
recipeController *feed.RecipeController // 配方控制器实例
inventoryController *inventory.InventoryController // 库存控制器实例
listenHandler webhook.ListenHandler // 设备上行事件监听器
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
}
@@ -72,6 +81,13 @@ func NewAPI(cfg config.ServerConfig,
userService service.UserService,
auditService service.AuditService,
alarmService service.ThresholdAlarmService,
nutrientService service.NutrientService,
rawMaterialService service.RawMaterialService,
pigBreedService service.PigBreedService,
pigAgeStageService service.PigAgeStageService,
pigTypeService service.PigTypeService,
recipeService service.RecipeService,
inventoryService service.InventoryService,
tokenGenerator token.Generator,
listenHandler webhook.ListenHandler,
) *API {
@@ -95,22 +111,21 @@ func NewAPI(cfg config.ServerConfig,
auditService: auditService,
config: cfg,
listenHandler: listenHandler,
// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员
userController: user.NewController(logs.AddCompName(baseCtx, "UserController"), userService),
// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员
deviceController: device.NewController(logs.AddCompName(baseCtx, "DeviceController"), deviceService),
// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员
planController: plan.NewController(logs.AddCompName(baseCtx, "PlanController"), planService),
// 在 NewAPI 中初始化猪场管理控制器
pigFarmController: management.NewPigFarmController(logs.AddCompName(baseCtx, "PigFarmController"), pigFarmService),
// 在 NewAPI 中初始化猪群控制器
pigBatchController: management.NewPigBatchController(logs.AddCompName(baseCtx, "PigBatchController"), pigBatchService),
// 在 NewAPI 中初始化数据监控控制器
monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService),
// 在 NewAPI 中初始化健康检查控制器
healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")),
// 在 NewAPI 中初始化阈
alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService),
nutrientController: feed.NewNutrientController(logs.AddCompName(baseCtx, "NutrientController"), nutrientService),
pigAgeStageController: feed.NewPigAgeStageController(logs.AddCompName(baseCtx, "PigAgeStageController"), pigAgeStageService),
pigBreedController: feed.NewPigBreedController(logs.AddCompName(baseCtx, "PigBreedController"), pigBreedService),
pigTypeController: feed.NewPigTypeController(logs.AddCompName(baseCtx, "PigTypeController"), pigTypeService),
rawMaterialController: feed.NewRawMaterialController(logs.AddCompName(baseCtx, "RawMaterialController"), rawMaterialService),
recipeController: feed.NewRecipeController(logs.AddCompName(baseCtx, "RecipeController"), recipeService),
inventoryController: inventory.NewInventoryController(logs.AddCompName(baseCtx, "InventoryController"), inventoryService),
}
api.setupRoutes() // 设置所有路由

View File

@@ -173,9 +173,6 @@ func (a *API) setupRoutes() {
monitorGroup.GET("/task-execution-logs", a.monitorController.ListTaskExecutionLogs)
monitorGroup.GET("/pending-collections", a.monitorController.ListPendingCollections)
monitorGroup.GET("/user-action-logs", a.monitorController.ListUserActionLogs)
monitorGroup.GET("/raw-material-purchases", a.monitorController.ListRawMaterialPurchases)
monitorGroup.GET("raw-material-stock-logs", a.monitorController.ListRawMaterialStockLogs)
monitorGroup.GET("/feed-usage-records", a.monitorController.ListFeedUsageRecords)
monitorGroup.GET("/medication-logs", a.monitorController.ListMedicationLogs)
monitorGroup.GET("/pig-batch-logs", a.monitorController.ListPigBatchLogs)
monitorGroup.GET("/weighing-batches", a.monitorController.ListWeighingBatches)
@@ -215,6 +212,66 @@ func (a *API) setupRoutes() {
}
}
logger.Debug("告警相关接口注册成功 (需要认证和审计)")
// 饲料管理相关路由组
feedGroup := authGroup.Group("/feed")
{
// 营养种类 (Nutrient) 路由
feedGroup.POST("/nutrients", a.nutrientController.CreateNutrient)
feedGroup.PUT("/nutrients/:id", a.nutrientController.UpdateNutrient)
feedGroup.DELETE("/nutrients/:id", a.nutrientController.DeleteNutrient)
feedGroup.GET("/nutrients/:id", a.nutrientController.GetNutrient)
feedGroup.GET("/nutrients", a.nutrientController.ListNutrients)
// 原料 (RawMaterial) 路由
feedGroup.POST("/raw-materials", a.rawMaterialController.CreateRawMaterial)
feedGroup.PUT("/raw-materials/:id", a.rawMaterialController.UpdateRawMaterial)
feedGroup.PUT("/raw-materials/:id/nutrients", a.rawMaterialController.UpdateRawMaterialNutrients)
feedGroup.DELETE("/raw-materials/:id", a.rawMaterialController.DeleteRawMaterial)
feedGroup.GET("/raw-materials/:id", a.rawMaterialController.GetRawMaterial)
feedGroup.GET("/raw-materials", a.rawMaterialController.ListRawMaterials)
// 猪品种 (PigBreed) 路由
feedGroup.POST("/pig-breeds", a.pigBreedController.CreatePigBreed)
feedGroup.PUT("/pig-breeds/:id", a.pigBreedController.UpdatePigBreed)
feedGroup.DELETE("/pig-breeds/:id", a.pigBreedController.DeletePigBreed)
feedGroup.GET("/pig-breeds/:id", a.pigBreedController.GetPigBreed)
feedGroup.GET("/pig-breeds", a.pigBreedController.ListPigBreeds)
// 猪年龄阶段 (PigAgeStage) 路由
feedGroup.POST("/pig-age-stages", a.pigAgeStageController.CreatePigAgeStage)
feedGroup.PUT("/pig-age-stages/:id", a.pigAgeStageController.UpdatePigAgeStage)
feedGroup.DELETE("/pig-age-stages/:id", a.pigAgeStageController.DeletePigAgeStage)
feedGroup.GET("/pig-age-stages/:id", a.pigAgeStageController.GetPigAgeStage)
feedGroup.GET("/pig-age-stages", a.pigAgeStageController.ListPigAgeStages)
// 猪类型 (PigType) 路由
feedGroup.POST("/pig-types", a.pigTypeController.CreatePigType)
feedGroup.PUT("/pig-types/:id", a.pigTypeController.UpdatePigType)
feedGroup.DELETE("/pig-types/:id", a.pigTypeController.DeletePigType)
feedGroup.GET("/pig-types/:id", a.pigTypeController.GetPigType)
feedGroup.GET("/pig-types", a.pigTypeController.ListPigTypes)
feedGroup.PUT("/pig-types/:id/nutrient-requirements", a.pigTypeController.UpdatePigTypeNutrientRequirements)
// 配方 (Recipe) 路由
feedGroup.POST("/recipes", a.recipeController.CreateRecipe)
feedGroup.PUT("/recipes/:id", a.recipeController.UpdateRecipe)
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)
}
logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)")
// 库存管理相关路由组
inventoryGroup := authGroup.Group("/inventory")
{
inventoryGroup.POST("/stock/adjust", a.inventoryController.AdjustStock)
inventoryGroup.GET("/stock/current", a.inventoryController.ListCurrentStock)
inventoryGroup.GET("/stock/logs", a.inventoryController.ListStockLogs)
}
logger.Debug("库存管理相关接口注册成功 (需要认证和审计)")
}
logger.Debug("所有接口注册成功")

View File

@@ -0,0 +1,195 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// NutrientController 定义了营养种类相关的控制器
type NutrientController struct {
ctx context.Context
nutrientService service.NutrientService
}
// NewNutrientController 创建一个新的 NutrientController 实例
func NewNutrientController(ctx context.Context, feedManagementService service.NutrientService) *NutrientController {
return &NutrientController{
ctx: ctx,
nutrientService: feedManagementService,
}
}
// CreateNutrient godoc
// @Summary 创建营养种类
// @Description 创建一个新的营养种类。
// @Tags 饲料管理-营养
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param nutrient body dto.CreateNutrientRequest true "营养种类信息"
// @Success 200 {object} controller.Response{data=dto.NutrientResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/nutrients [post]
func (c *NutrientController) CreateNutrient(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateNutrient")
var req dto.CreateNutrientRequest
const actionType = "创建营养种类"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.nutrientService.CreateNutrient(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建营养种类失败: %v", actionType, err)
if errors.Is(err, service.ErrNutrientNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "营养种类名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建营养种类失败: "+err.Error(), actionType, "服务层创建营养种类失败", req)
}
logger.Infof("%s: 营养种类创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "营养种类创建成功", resp, actionType, "营养种类创建成功", resp)
}
// UpdateNutrient godoc
// @Summary 更新营养种类
// @Description 根据ID更新营养种类信息。
// @Tags 饲料管理-营养
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "营养种类ID"
// @Param nutrient body dto.UpdateNutrientRequest true "更新后的营养种类信息"
// @Success 200 {object} controller.Response{data=dto.NutrientResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/nutrients/{id} [put]
func (c *NutrientController) UpdateNutrient(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateNutrient")
const actionType = "更新营养种类"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 营养种类ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的营养种类ID格式", actionType, "营养种类ID格式错误", idStr)
}
var req dto.UpdateNutrientRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.nutrientService.UpdateNutrient(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新营养种类失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrNutrientNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "营养种类不存在", id)
}
if errors.Is(err, service.ErrNutrientNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "营养种类名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新营养种类失败: "+err.Error(), actionType, "服务层更新营养种类失败", req)
}
logger.Infof("%s: 营养种类更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "营养种类更新成功", resp, actionType, "营养种类更新成功", resp)
}
// DeleteNutrient godoc
// @Summary 删除营养种类
// @Description 根据ID删除营养种类。
// @Tags 饲料管理-营养
// @Security BearerAuth
// @Produce json
// @Param id path int true "营养种类ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/nutrients/{id} [delete]
func (c *NutrientController) DeleteNutrient(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteNutrient")
const actionType = "删除营养种类"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 营养种类ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的营养种类ID格式", actionType, "营养种类ID格式错误", idStr)
}
err = c.nutrientService.DeleteNutrient(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除营养种类失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrNutrientNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "营养种类不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除营养种类失败: "+err.Error(), actionType, "服务层删除营养种类失败", id)
}
logger.Infof("%s: 营养种类删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "营养种类删除成功", nil, actionType, "营养种类删除成功", id)
}
// GetNutrient godoc
// @Summary 获取营养种类详情
// @Description 根据ID获取单个营养种类的详细信息。
// @Tags 饲料管理-营养
// @Security BearerAuth
// @Produce json
// @Param id path int true "营养种类ID"
// @Success 200 {object} controller.Response{data=dto.NutrientResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/nutrients/{id} [get]
func (c *NutrientController) GetNutrient(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetNutrient")
const actionType = "获取营养种类详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 营养种类ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的营养种类ID格式", actionType, "营养种类ID格式错误", idStr)
}
resp, err := c.nutrientService.GetNutrient(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取营养种类详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrNutrientNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "营养种类不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取营养种类详情失败: "+err.Error(), actionType, "服务层获取营养种类详情失败", id)
}
logger.Infof("%s: 获取营养种类详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取营养种类详情成功", resp, actionType, "获取营养种类详情成功", resp)
}
// ListNutrients godoc
// @Summary 获取营养种类列表
// @Description 获取所有营养种类的列表,支持分页和过滤。
// @Tags 饲料管理-营养
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListNutrientRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListNutrientResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/nutrients [get]
func (c *NutrientController) ListNutrients(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListNutrients")
const actionType = "获取营养种类列表"
var req dto.ListNutrientRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.nutrientService.ListNutrients(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取营养种类列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取营养种类列表失败: "+err.Error(), actionType, "服务层获取营养种类列表失败", nil)
}
logger.Infof("%s: 获取营养种类列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取营养种类列表成功", resp, actionType, "获取营养种类列表成功", resp)
}

View File

@@ -0,0 +1,193 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// PigAgeStageController 定义了猪年龄阶段相关的控制器
type PigAgeStageController struct {
ctx context.Context
pigAgeStageService service.PigAgeStageService
}
// NewPigAgeStageController 创建一个新的 PigAgeStageController 实例
func NewPigAgeStageController(ctx context.Context, feedManagementService service.PigAgeStageService) *PigAgeStageController {
return &PigAgeStageController{
ctx: ctx,
pigAgeStageService: feedManagementService,
}
}
// CreatePigAgeStage godoc
// @Summary 创建猪年龄阶段
// @Description 创建一个新的猪年龄阶段。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param pigAgeStage body dto.CreatePigAgeStageRequest true "猪年龄阶段信息"
// @Success 200 {object} controller.Response{data=dto.PigAgeStageResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/pig-age-stages [post]
func (c *PigAgeStageController) CreatePigAgeStage(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePigAgeStage")
var req dto.CreatePigAgeStageRequest
const actionType = "创建猪年龄阶段"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.pigAgeStageService.CreatePigAgeStage(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建猪年龄阶段失败: %v", actionType, err)
// 猪年龄阶段没有名称冲突的领域错误,这里直接返回内部错误
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪年龄阶段失败: "+err.Error(), actionType, "服务层创建猪年龄阶段失败", req)
}
logger.Infof("%s: 猪年龄阶段创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "猪年龄阶段创建成功", resp, actionType, "猪年龄阶段创建成功", resp)
}
// UpdatePigAgeStage godoc
// @Summary 更新猪年龄阶段
// @Description 根据ID更新猪年龄阶段信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "猪年龄阶段ID"
// @Param pigAgeStage body dto.UpdatePigAgeStageRequest true "更新后的猪年龄阶段信息"
// @Success 200 {object} controller.Response{data=dto.PigAgeStageResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/pig-age-stages/{id} [put]
func (c *PigAgeStageController) UpdatePigAgeStage(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePigAgeStage")
const actionType = "更新猪年龄阶段"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪年龄阶段ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪年龄阶段ID格式", actionType, "猪年龄阶段ID格式错误", idStr)
}
var req dto.UpdatePigAgeStageRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.pigAgeStageService.UpdatePigAgeStage(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新猪年龄阶段失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigAgeStageNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪年龄阶段不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪年龄阶段失败: "+err.Error(), actionType, "服务层更新猪年龄阶段失败", req)
}
logger.Infof("%s: 猪年龄阶段更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪年龄阶段更新成功", resp, actionType, "猪年龄阶段更新成功", resp)
}
// DeletePigAgeStage godoc
// @Summary 删除猪年龄阶段
// @Description 根据ID删除猪年龄阶段。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪年龄阶段ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/pig-age-stages/{id} [delete]
func (c *PigAgeStageController) DeletePigAgeStage(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePigAgeStage")
const actionType = "删除猪年龄阶段"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪年龄阶段ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪年龄阶段ID格式", actionType, "猪年龄阶段ID格式错误", idStr)
}
err = c.pigAgeStageService.DeletePigAgeStage(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除猪年龄阶段失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigAgeStageNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪年龄阶段不存在", id)
}
if errors.Is(err, service.ErrPigAgeStageInUse) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "猪年龄阶段正在被使用", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪年龄阶段失败: "+err.Error(), actionType, "服务层删除猪年龄阶段失败", id)
}
logger.Infof("%s: 猪年龄阶段删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪年龄阶段删除成功", nil, actionType, "猪年龄阶段删除成功", id)
}
// GetPigAgeStage godoc
// @Summary 获取猪年龄阶段详情
// @Description 根据ID获取单个猪年龄阶段的详细信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪年龄阶段ID"
// @Success 200 {object} controller.Response{data=dto.PigAgeStageResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/pig-age-stages/{id} [get]
func (c *PigAgeStageController) GetPigAgeStage(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPigAgeStage")
const actionType = "获取猪年龄阶段详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪年龄阶段ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪年龄阶段ID格式", actionType, "猪年龄阶段ID格式错误", idStr)
}
resp, err := c.pigAgeStageService.GetPigAgeStage(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取猪年龄阶段详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigAgeStageNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪年龄阶段不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪年龄阶段详情失败: "+err.Error(), actionType, "服务层获取猪年龄阶段详情失败", id)
}
logger.Infof("%s: 获取猪年龄阶段详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪年龄阶段详情成功", resp, actionType, "获取猪年龄阶段详情成功", resp)
}
// ListPigAgeStages godoc
// @Summary 获取猪年龄阶段列表
// @Description 获取所有猪年龄阶段的列表,支持分页和过滤。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListPigAgeStageRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListPigAgeStageResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/pig-age-stages [get]
func (c *PigAgeStageController) ListPigAgeStages(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigAgeStages")
const actionType = "获取猪年龄阶段列表"
var req dto.ListPigAgeStageRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.pigAgeStageService.ListPigAgeStages(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取猪年龄阶段列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪年龄阶段列表失败: "+err.Error(), actionType, "服务层获取猪年龄阶段列表失败", nil)
}
logger.Infof("%s: 获取猪年龄阶段列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪年龄阶段列表成功", resp, actionType, "获取猪年龄阶段列表成功", resp)
}

View File

@@ -0,0 +1,193 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// PigBreedController 定义了猪品种相关的控制器
type PigBreedController struct {
ctx context.Context
pigBreedService service.PigBreedService
}
// NewPigBreedController 创建一个新的 PigBreedController 实例
func NewPigBreedController(ctx context.Context, feedManagementService service.PigBreedService) *PigBreedController {
return &PigBreedController{
ctx: ctx,
pigBreedService: feedManagementService,
}
}
// CreatePigBreed godoc
// @Summary 创建猪品种
// @Description 创建一个新的猪品种。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param pigBreed body dto.CreatePigBreedRequest true "猪品种信息"
// @Success 200 {object} controller.Response{data=dto.PigBreedResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/pig-breeds [post]
func (c *PigBreedController) CreatePigBreed(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePigBreed")
var req dto.CreatePigBreedRequest
const actionType = "创建猪品种"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.pigBreedService.CreatePigBreed(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建猪品种失败: %v", actionType, err)
// 猪品种没有名称冲突的领域错误,这里直接返回内部错误
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪品种失败: "+err.Error(), actionType, "服务层创建猪品种失败", req)
}
logger.Infof("%s: 猪品种创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "猪品种创建成功", resp, actionType, "猪品种创建成功", resp)
}
// UpdatePigBreed godoc
// @Summary 更新猪品种
// @Description 根据ID更新猪品种信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "猪品种ID"
// @Param pigBreed body dto.UpdatePigBreedRequest true "更新后的猪品种信息"
// @Success 200 {object} controller.Response{data=dto.PigBreedResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/pig-breeds/{id} [put]
func (c *PigBreedController) UpdatePigBreed(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePigBreed")
const actionType = "更新猪品种"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪品种ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪品种ID格式", actionType, "猪品种ID格式错误", idStr)
}
var req dto.UpdatePigBreedRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.pigBreedService.UpdatePigBreed(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新猪品种失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigBreedNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪品种不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪品种失败: "+err.Error(), actionType, "服务层更新猪品种失败", req)
}
logger.Infof("%s: 猪品种更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪品种更新成功", resp, actionType, "猪品种更新成功", resp)
}
// DeletePigBreed godoc
// @Summary 删除猪品种
// @Description 根据ID删除猪品种。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪品种ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/pig-breeds/{id} [delete]
func (c *PigBreedController) DeletePigBreed(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePigBreed")
const actionType = "删除猪品种"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪品种ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪品种ID格式", actionType, "猪品种ID格式错误", idStr)
}
err = c.pigBreedService.DeletePigBreed(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除猪品种失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigBreedNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪品种不存在", id)
}
if errors.Is(err, service.ErrPigBreedInUse) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "猪品种正在被使用", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪品种失败: "+err.Error(), actionType, "服务层删除猪品种失败", id)
}
logger.Infof("%s: 猪品种删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪品种删除成功", nil, actionType, "猪品种删除成功", id)
}
// GetPigBreed godoc
// @Summary 获取猪品种详情
// @Description 根据ID获取单个猪品种的详细信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪品种ID"
// @Success 200 {object} controller.Response{data=dto.PigBreedResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/pig-breeds/{id} [get]
func (c *PigBreedController) GetPigBreed(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPigBreed")
const actionType = "获取猪品种详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪品种ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪品种ID格式", actionType, "猪品种ID格式错误", idStr)
}
resp, err := c.pigBreedService.GetPigBreed(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取猪品种详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigBreedNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪品种不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪品种详情失败: "+err.Error(), actionType, "服务层获取猪品种详情失败", id)
}
logger.Infof("%s: 获取猪品种详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪品种详情成功", resp, actionType, "获取猪品种详情成功", resp)
}
// ListPigBreeds godoc
// @Summary 获取猪品种列表
// @Description 获取所有猪品种的列表,支持分页和过滤。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListPigBreedRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListPigBreedResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/pig-breeds [get]
func (c *PigBreedController) ListPigBreeds(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigBreeds")
const actionType = "获取猪品种列表"
var req dto.ListPigBreedRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.pigBreedService.ListPigBreeds(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取猪品种列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪品种列表失败: "+err.Error(), actionType, "服务层获取猪品种列表失败", nil)
}
logger.Infof("%s: 获取猪品种列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪品种列表成功", resp, actionType, "获取猪品种列表成功", resp)
}

View File

@@ -0,0 +1,243 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// PigTypeController 定义了猪类型相关的控制器
type PigTypeController struct {
ctx context.Context
typeService service.PigTypeService
}
// NewPigTypeController 创建一个新的 PigTypeController 实例
func NewPigTypeController(ctx context.Context, feedManagementService service.PigTypeService) *PigTypeController {
return &PigTypeController{
ctx: ctx,
typeService: feedManagementService,
}
}
// CreatePigType godoc
// @Summary 创建猪类型
// @Description 创建一个新的猪类型。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param pigType body dto.CreatePigTypeRequest true "猪类型信息"
// @Success 200 {object} controller.Response{data=dto.PigTypeResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/pig-types [post]
func (c *PigTypeController) CreatePigType(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreatePigType")
var req dto.CreatePigTypeRequest
const actionType = "创建猪类型"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.typeService.CreatePigType(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建猪类型失败: %v", actionType, err)
if errors.Is(err, service.ErrPigBreedNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "关联猪品种不存在", req)
}
if errors.Is(err, service.ErrPigAgeStageNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "关联猪年龄阶段不存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪类型失败: "+err.Error(), actionType, "服务层创建猪类型失败", req)
}
logger.Infof("%s: 猪类型创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "猪类型创建成功", resp, actionType, "猪类型创建成功", resp)
}
// UpdatePigType godoc
// @Summary 更新猪类型
// @Description 根据ID更新猪类型信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "猪类型ID"
// @Param pigType body dto.UpdatePigTypeRequest true "更新后的猪类型信息"
// @Success 200 {object} controller.Response{data=dto.PigTypeResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/pig-types/{id} [put]
func (c *PigTypeController) UpdatePigType(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePigType")
const actionType = "更新猪类型"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr)
}
var req dto.UpdatePigTypeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.typeService.UpdatePigType(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新猪类型失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigTypeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪类型不存在", id)
}
if errors.Is(err, service.ErrPigBreedNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "关联猪品种不存在", req)
}
if errors.Is(err, service.ErrPigAgeStageNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "关联猪年龄阶段不存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪类型失败: "+err.Error(), actionType, "服务层更新猪类型失败", req)
}
logger.Infof("%s: 猪类型更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪类型更新成功", resp, actionType, "猪类型更新成功", resp)
}
// DeletePigType godoc
// @Summary 删除猪类型
// @Description 根据ID删除猪类型。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪类型ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/pig-types/{id} [delete]
func (c *PigTypeController) DeletePigType(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeletePigType")
const actionType = "删除猪类型"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr)
}
err = c.typeService.DeletePigType(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除猪类型失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigTypeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪类型不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪类型失败: "+err.Error(), actionType, "服务层删除猪类型失败", id)
}
logger.Infof("%s: 猪类型删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪类型删除成功", nil, actionType, "猪类型删除成功", id)
}
// GetPigType godoc
// @Summary 获取猪类型详情
// @Description 根据ID获取单个猪类型的详细信息。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param id path int true "猪类型ID"
// @Success 200 {object} controller.Response{data=dto.PigTypeResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/pig-types/{id} [get]
func (c *PigTypeController) GetPigType(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetPigType")
const actionType = "获取猪类型详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr)
}
resp, err := c.typeService.GetPigType(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取猪类型详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigTypeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪类型不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪类型详情失败: "+err.Error(), actionType, "服务层获取猪类型详情失败", id)
}
logger.Infof("%s: 获取猪类型详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪类型详情成功", resp, actionType, "获取猪类型详情成功", resp)
}
// ListPigTypes godoc
// @Summary 获取猪类型列表
// @Description 获取所有猪类型的列表,支持分页和过滤。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListPigTypeRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListPigTypeResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/pig-types [get]
func (c *PigTypeController) ListPigTypes(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListPigTypes")
const actionType = "获取猪类型列表"
var req dto.ListPigTypeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.typeService.ListPigTypes(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取猪类型列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪类型列表失败: "+err.Error(), actionType, "服务层获取猪类型列表失败", nil)
}
logger.Infof("%s: 获取猪类型列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪类型列表成功", resp, actionType, "获取猪类型列表成功", resp)
}
// UpdatePigTypeNutrientRequirements godoc
// @Summary 全量更新猪类型的营养需求
// @Description 根据猪类型ID替换其所有的营养需求信息。这是一个覆盖操作。
// @Tags 饲料管理-猪
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "猪类型ID"
// @Param nutrientRequirements body dto.UpdatePigTypeNutrientRequirementsRequest true "新的营养需求列表"
// @Success 200 {object} controller.Response{data=dto.PigTypeResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/pig-types/{id}/nutrient-requirements [put]
func (c *PigTypeController) UpdatePigTypeNutrientRequirements(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdatePigTypeNutrientRequirements")
const actionType = "更新猪类型营养需求"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr)
}
var req dto.UpdatePigTypeNutrientRequirementsRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.typeService.UpdatePigTypeNutrientRequirements(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新猪类型营养需求失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrPigTypeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "猪类型不存在", id)
}
// 这里可以根据未来可能从服务层返回的其他特定错误进行处理
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪类型营养需求失败: "+err.Error(), actionType, "服务层更新失败", req)
}
logger.Infof("%s: 猪类型营养需求更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "猪类型营养需求更新成功", resp, actionType, "猪类型营养需求更新成功", resp)
}

View File

@@ -0,0 +1,237 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// RawMaterialController 定义了原料相关的控制器
type RawMaterialController struct {
ctx context.Context
rawMaterialService service.RawMaterialService
}
// NewRawMaterialController 创建一个新的 RawMaterialController 实例
func NewRawMaterialController(ctx context.Context, feedManagementService service.RawMaterialService) *RawMaterialController {
return &RawMaterialController{
ctx: ctx,
rawMaterialService: feedManagementService,
}
}
// CreateRawMaterial godoc
// @Summary 创建原料
// @Description 创建一个新的原料。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param rawMaterial body dto.CreateRawMaterialRequest true "原料信息,包含名称、描述和参考价格"
// @Success 200 {object} controller.Response{data=dto.RawMaterialResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/raw-materials [post]
func (c *RawMaterialController) CreateRawMaterial(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateRawMaterial")
var req dto.CreateRawMaterialRequest
const actionType = "创建原料"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.rawMaterialService.CreateRawMaterial(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建原料失败: %v", actionType, err)
if errors.Is(err, service.ErrRawMaterialNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "原料名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建原料失败: "+err.Error(), actionType, "服务层创建原料失败", req)
}
logger.Infof("%s: 原料创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "原料创建成功", resp, actionType, "原料创建成功", resp)
}
// UpdateRawMaterial godoc
// @Summary 更新原料
// @Description 根据ID更新原料信息。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "原料ID"
// @Param rawMaterial body dto.UpdateRawMaterialRequest true "更新后的原料信息,包含名称、描述和参考价格"
// @Success 200 {object} controller.Response{data=dto.RawMaterialResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/raw-materials/{id} [put]
func (c *RawMaterialController) UpdateRawMaterial(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateRawMaterial")
const actionType = "更新原料"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 原料ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr)
}
var req dto.UpdateRawMaterialRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.rawMaterialService.UpdateRawMaterial(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新原料失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrRawMaterialNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "原料不存在", id)
}
if errors.Is(err, service.ErrRawMaterialNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "原料名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新原料失败: "+err.Error(), actionType, "服务层更新原料失败", req)
}
logger.Infof("%s: 原料更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "原料更新成功", resp, actionType, "原料更新成功", resp)
}
// DeleteRawMaterial godoc
// @Summary 删除原料
// @Description 根据ID删除原料。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Produce json
// @Param id path int true "原料ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/raw-materials/{id} [delete]
func (c *RawMaterialController) DeleteRawMaterial(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteRawMaterial")
const actionType = "删除原料"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 原料ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr)
}
err = c.rawMaterialService.DeleteRawMaterial(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除原料失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrRawMaterialNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "原料不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除原料失败: "+err.Error(), actionType, "服务层删除原料失败", id)
}
logger.Infof("%s: 原料删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "原料删除成功", nil, actionType, "原料删除成功", id)
}
// GetRawMaterial godoc
// @Summary 获取原料详情
// @Description 根据ID获取单个原料的详细信息。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Produce json
// @Param id path int true "原料ID"
// @Success 200 {object} controller.Response{data=dto.RawMaterialResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/raw-materials/{id} [get]
func (c *RawMaterialController) GetRawMaterial(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetRawMaterial")
const actionType = "获取原料详情"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 原料ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr)
}
resp, err := c.rawMaterialService.GetRawMaterial(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取原料详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrRawMaterialNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "原料不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料详情失败: "+err.Error(), actionType, "服务层获取原料详情失败", id)
}
logger.Infof("%s: 获取原料详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料详情成功", resp, actionType, "获取原料详情成功", resp)
}
// ListRawMaterials godoc
// @Summary 获取原料列表
// @Description 获取所有原料的列表,支持分页和过滤。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialRequest false "查询参数,支持按名称、营养名称、参考价格范围过滤"
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/raw-materials [get]
func (c *RawMaterialController) ListRawMaterials(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterials")
const actionType = "获取原料列表"
var req dto.ListRawMaterialRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.rawMaterialService.ListRawMaterials(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取原料列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料列表失败: "+err.Error(), actionType, "服务层获取原料列表失败", nil)
}
logger.Infof("%s: 获取原料列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料列表成功", resp, actionType, "获取原料列表成功", resp)
}
// UpdateRawMaterialNutrients godoc
// @Summary 全量更新原料的营养成分
// @Description 根据原料ID替换其所有的营养成分信息。这是一个覆盖操作。
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "原料ID"
// @Param nutrients body dto.UpdateRawMaterialNutrientsRequest true "新的营养成分列表"
// @Success 200 {object} controller.Response{data=dto.RawMaterialResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/raw-materials/{id}/nutrients [put]
func (c *RawMaterialController) UpdateRawMaterialNutrients(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateRawMaterialNutrients")
const actionType = "更新原料营养成分"
idStr := ctx.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
logger.Errorf("%s: 原料ID格式错误: %v, ID: %s", actionType, err, idStr)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr)
}
var req dto.UpdateRawMaterialNutrientsRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.rawMaterialService.UpdateRawMaterialNutrients(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新原料营养成分失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, service.ErrRawMaterialNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "原料不存在", id)
}
// 这里可以根据未来可能从服务层返回的其他特定错误进行处理
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新原料营养成分失败: "+err.Error(), actionType, "服务层更新失败", req)
}
logger.Infof("%s: 原料营养成分更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "原料营养成分更新成功", resp, actionType, "原料营养成分更新成功", resp)
}

View File

@@ -0,0 +1,258 @@
package feed
import (
"context"
"errors"
"strconv"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// RecipeController 包含配方相关的处理器
type RecipeController struct {
ctx context.Context
recipeService service.RecipeService
}
// NewRecipeController 创建一个新的 RecipeController
func NewRecipeController(ctx context.Context, recipeService service.RecipeService) *RecipeController {
return &RecipeController{
ctx: ctx,
recipeService: recipeService,
}
}
// CreateRecipe godoc
// @Summary 创建配方
// @Description 创建一个新的配方,包含其原料组成。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param recipe body dto.CreateRecipeRequest true "配方信息"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为201代表创建成功"
// @Router /api/v1/feed/recipes [post]
func (c *RecipeController) CreateRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "CreateRecipe")
var req dto.CreateRecipeRequest
const actionType = "创建配方"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.recipeService.CreateRecipe(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层创建配方失败: %v", actionType, err)
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "配方名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建配方失败: "+err.Error(), actionType, "服务层创建配方失败", req)
}
logger.Infof("%s: 配方创建成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方创建成功", resp, actionType, "配方创建成功", resp)
}
// UpdateRecipe godoc
// @Summary 更新配方
// @Description 根据ID更新配方信息及其原料组成。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "配方ID"
// @Param recipe body dto.UpdateRecipeRequest true "更新后的配方信息"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为200代表更新成功"
// @Router /api/v1/feed/recipes/{id} [put]
func (c *RecipeController) UpdateRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "UpdateRecipe")
const actionType = "更新配方"
idStr := ctx.Param("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)
}
var req dto.UpdateRecipeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.recipeService.UpdateRecipe(reqCtx, uint32(id), &req)
if err != nil {
logger.Errorf("%s: 服务层更新配方失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "配方名称已存在", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新配方失败: "+err.Error(), actionType, "服务层更新配方失败", req)
}
logger.Infof("%s: 配方更新成功, ID: %d", actionType, resp.ID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "配方更新成功", resp, actionType, "配方更新成功", resp)
}
// DeleteRecipe godoc
// @Summary 删除配方
// @Description 根据ID删除配方。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param id path int true "配方ID"
// @Success 200 {object} controller.Response "业务码为200代表删除成功"
// @Router /api/v1/feed/recipes/{id} [delete]
func (c *RecipeController) DeleteRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "DeleteRecipe")
const actionType = "删除配方"
idStr := ctx.Param("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)
}
err = c.recipeService.DeleteRecipe(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层删除配方失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除配方失败: "+err.Error(), actionType, "服务层删除配方失败", id)
}
logger.Infof("%s: 配方删除成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "配方删除成功", nil, actionType, "配方删除成功", id)
}
// GetRecipe godoc
// @Summary 获取配方详情
// @Description 根据ID获取单个配方的详细信息。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param id path int true "配方ID"
// @Success 200 {object} controller.Response{data=dto.RecipeResponse} "业务码为200代表成功获取"
// @Router /api/v1/feed/recipes/{id} [get]
func (c *RecipeController) GetRecipe(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GetRecipe")
const actionType = "获取配方详情"
idStr := ctx.Param("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)
}
resp, err := c.recipeService.GetRecipeByID(reqCtx, uint32(id))
if err != nil {
logger.Errorf("%s: 服务层获取配方详情失败: %v, ID: %d", actionType, err, id)
if errors.Is(err, recipe.ErrRecipeNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方不存在", id)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取配方详情失败: "+err.Error(), actionType, "服务层获取配方详情失败", id)
}
logger.Infof("%s: 获取配方详情成功, ID: %d", actionType, id)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取配方详情成功", resp, actionType, "获取配方详情成功", resp)
}
// ListRecipes godoc
// @Summary 获取配方列表
// @Description 获取所有配方的列表,支持分页和过滤。
// @Tags 饲料管理-配方
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRecipeRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRecipeResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/feed/recipes [get]
func (c *RecipeController) ListRecipes(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRecipes")
const actionType = "获取配方列表"
var req dto.ListRecipeRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.recipeService.ListRecipes(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取配方列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取配方列表失败: "+err.Error(), actionType, "服务层获取配方列表失败", nil)
}
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)
}

View File

@@ -0,0 +1,121 @@
package inventory
import (
"context"
"errors"
"git.huangwc.com/pig/pig-farm-controller/internal/app/controller"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/app/service"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"github.com/labstack/echo/v4"
)
// InventoryController 定义了库存相关的控制器
type InventoryController struct {
ctx context.Context
inventoryService service.InventoryService
}
// NewInventoryController 创建一个新的 InventoryController 实例
func NewInventoryController(ctx context.Context, inventoryService service.InventoryService) *InventoryController {
return &InventoryController{
ctx: ctx,
inventoryService: inventoryService,
}
}
// AdjustStock godoc
// @Summary 调整原料库存
// @Description 手动调整指定原料的库存量。
// @Tags 库存管理
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body dto.StockAdjustmentRequest true "库存调整请求"
// @Success 200 {object} controller.Response{data=dto.StockLogResponse} "业务码为200代表调整成功"
// @Router /api/v1/inventory/stock/adjust [post]
func (c *InventoryController) AdjustStock(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "AdjustStock")
var req dto.StockAdjustmentRequest
const actionType = "调整原料库存"
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req)
}
resp, err := c.inventoryService.AdjustStock(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层调整库存失败: %v", actionType, err)
if errors.Is(err, service.ErrInventoryRawMaterialNotFound) {
return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "原料不存在", req)
}
if errors.Is(err, service.ErrInventoryInsufficientStock) {
return controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), actionType, "原料库存不足", req)
}
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "调整库存失败: "+err.Error(), actionType, "服务层调整库存失败", req)
}
logger.Infof("%s: 库存调整成功, 原料ID: %d", actionType, resp.RawMaterialID)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "库存调整成功", resp, actionType, "库存调整成功", resp)
}
// ListCurrentStock godoc
// @Summary 获取当前库存列表
// @Description 获取所有原料的当前库存列表,支持分页和过滤。
// @Tags 库存管理
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListCurrentStockRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListCurrentStockResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/inventory/stock/current [get]
func (c *InventoryController) ListCurrentStock(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListCurrentStock")
const actionType = "获取当前库存列表"
var req dto.ListCurrentStockRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.inventoryService.ListCurrentStock(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取当前库存列表失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取当前库存列表失败: "+err.Error(), actionType, "服务层获取当前库存列表失败", nil)
}
logger.Infof("%s: 获取当前库存列表成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取当前库存列表成功", resp, actionType, "获取当前库存列表成功", resp)
}
// ListStockLogs godoc
// @Summary 获取库存变动日志
// @Description 获取原料库存变动历史记录,支持分页、过滤和时间范围查询。
// @Tags 库存管理
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListStockLogRequest false "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListStockLogResponse} "业务码为200代表成功获取列表"
// @Router /api/v1/inventory/stock/logs [get]
func (c *InventoryController) ListStockLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListStockLogs")
const actionType = "获取库存变动日志"
var req dto.ListStockLogRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 查询参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req)
}
resp, err := c.inventoryService.ListStockLogs(reqCtx, &req)
if err != nil {
logger.Errorf("%s: 服务层获取库存变动日志失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取库存变动日志失败: "+err.Error(), actionType, "服务层获取库存变动日志失败", nil)
}
logger.Infof("%s: 获取库存变动日志成功, 数量: %d", actionType, len(resp.List))
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取库存变动日志成功", resp, actionType, "获取库存变动日志成功", resp)
}

View File

@@ -231,108 +231,6 @@ func (c *Controller) ListUserActionLogs(ctx echo.Context) error {
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req)
}
// ListRawMaterialPurchases godoc
// @Summary 获取原料采购记录列表
// @Description 根据提供的过滤条件,分页获取原料采购记录
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialPurchaseRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialPurchaseResponse}
// @Router /api/v1/monitor/raw-material-purchases [get]
func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialPurchases")
const actionType = "获取原料采购记录列表"
var req dto.ListRawMaterialPurchaseRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialPurchases(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req)
}
// ListRawMaterialStockLogs godoc
// @Summary 获取原料库存日志列表
// @Description 根据提供的过滤条件,分页获取原料库存日志
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialStockLogRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListRawMaterialStockLogResponse}
// @Router /api/v1/monitor/raw-material-stock-logs [get]
func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListRawMaterialStockLogs")
const actionType = "获取原料库存日志列表"
var req dto.ListRawMaterialStockLogRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListRawMaterialStockLogs(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req)
}
// ListFeedUsageRecords godoc
// @Summary 获取饲料使用记录列表
// @Description 根据提供的过滤条件,分页获取饲料使用记录
// @Tags 数据监控
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListFeedUsageRecordRequest true "查询参数"
// @Success 200 {object} controller.Response{data=dto.ListFeedUsageRecordResponse}
// @Router /api/v1/monitor/feed-usage-records [get]
func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error {
reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "ListFeedUsageRecords")
const actionType = "获取饲料使用记录列表"
var req dto.ListFeedUsageRecordRequest
if err := ctx.Bind(&req); err != nil {
logger.Errorf("%s: 参数绑定失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req)
}
resp, err := c.monitorService.ListFeedUsageRecords(reqCtx, &req)
if err != nil {
if errors.Is(err, repository.ErrInvalidPagination) {
logger.Warnf("%s: 无效的分页参数: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", req)
}
logger.Errorf("%s: 服务层查询失败: %v", actionType, err)
return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req)
}
logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total)
return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req)
}
// ListMedicationLogs godoc
// @Summary 获取用药记录列表
// @Description 根据提供的过滤条件,分页获取用药记录

View File

@@ -0,0 +1,295 @@
package dto
import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// ConvertNutrientToDTO 将 models.Nutrient 转换为 NutrientResponse DTO
func ConvertNutrientToDTO(nutrient *models.Nutrient) *NutrientResponse {
if nutrient == nil {
return nil
}
rawMaterials := make([]NutrientRawMaterialDTO, 0, len(nutrient.RawMaterialNutrients))
for _, rmn := range nutrient.RawMaterialNutrients {
// 根据您的反馈,移除了不必要的 nil 检查,以保持代码简洁和一致性
rawMaterials = append(rawMaterials, NutrientRawMaterialDTO{
ID: rmn.RawMaterial.ID,
Name: rmn.RawMaterial.Name,
Value: rmn.Value,
})
}
return &NutrientResponse{
ID: nutrient.ID,
Name: nutrient.Name,
Description: nutrient.Description,
RawMaterials: rawMaterials,
}
}
// ConvertNutrientListToDTO 将 []models.Nutrient 转换为 ListNutrientResponse DTO
func ConvertNutrientListToDTO(nutrients []models.Nutrient, total int64, page, pageSize int) *ListNutrientResponse {
nutrientDTOs := make([]NutrientResponse, len(nutrients))
for i, n := range nutrients {
nutrientDTOs[i] = *ConvertNutrientToDTO(&n)
}
return &ListNutrientResponse{
List: nutrientDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertRawMaterialToDTO 将 models.RawMaterial 转换为 RawMaterialResponse DTO
func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse {
if rm == nil {
return nil
}
rawMaterialNutrientDTOs := make([]RawMaterialNutrientDTO, len(rm.RawMaterialNutrients))
for i, rmn := range rm.RawMaterialNutrients {
rawMaterialNutrientDTOs[i] = RawMaterialNutrientDTO{
ID: rmn.ID,
NutrientID: rmn.NutrientID,
Nutrient: rmn.Nutrient.Name, // 假设 Nutrient 已经被预加载
Value: rmn.Value,
}
}
return &RawMaterialResponse{
ID: rm.ID,
Name: rm.Name,
Description: rm.Description,
ReferencePrice: rm.ReferencePrice,
MaxAdditionRatio: rm.MaxAdditionRatio,
RawMaterialNutrients: rawMaterialNutrientDTOs,
}
}
// ConvertRawMaterialListToDTO 将 []models.RawMaterial 转换为 ListRawMaterialResponse DTO
func ConvertRawMaterialListToDTO(rawMaterials []models.RawMaterial, total int64, page, pageSize int) *ListRawMaterialResponse {
rawMaterialDTOs := make([]RawMaterialResponse, len(rawMaterials))
for i, rm := range rawMaterials {
rawMaterialDTOs[i] = *ConvertRawMaterialToDTO(&rm)
}
return &ListRawMaterialResponse{
List: rawMaterialDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertPigBreedToDTO 将 models.PigBreed 转换为 PigBreedResponse DTO
func ConvertPigBreedToDTO(breed *models.PigBreed) *PigBreedResponse {
if breed == nil {
return nil
}
return &PigBreedResponse{
ID: breed.ID,
Name: breed.Name,
Description: breed.Description,
ParentInfo: breed.ParentInfo,
AppearanceFeatures: breed.AppearanceFeatures,
BreedAdvantages: breed.BreedAdvantages,
BreedDisadvantages: breed.BreedDisadvantages,
}
}
// ConvertPigBreedListToDTO 将 []models.PigBreed 转换为 ListPigBreedResponse DTO
func ConvertPigBreedListToDTO(breeds []models.PigBreed, total int64, page, pageSize int) *ListPigBreedResponse {
breedDTOs := make([]PigBreedResponse, len(breeds))
for i, b := range breeds {
breedDTOs[i] = *ConvertPigBreedToDTO(&b)
}
return &ListPigBreedResponse{
List: breedDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertPigAgeStageToDTO 将 models.PigAgeStage 转换为 PigAgeStageResponse DTO
func ConvertPigAgeStageToDTO(ageStage *models.PigAgeStage) *PigAgeStageResponse {
if ageStage == nil {
return nil
}
return &PigAgeStageResponse{
ID: ageStage.ID,
Name: ageStage.Name,
Description: ageStage.Description,
}
}
// ConvertPigAgeStageListToDTO 将 []models.PigAgeStage 转换为 ListPigAgeStageResponse DTO
func ConvertPigAgeStageListToDTO(ageStages []models.PigAgeStage, total int64, page, pageSize int) *ListPigAgeStageResponse {
ageStageDTOs := make([]PigAgeStageResponse, len(ageStages))
for i, as := range ageStages {
ageStageDTOs[i] = *ConvertPigAgeStageToDTO(&as)
}
return &ListPigAgeStageResponse{
List: ageStageDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertPigTypeToDTO 将 models.PigType 转换为 PigTypeResponse DTO
func ConvertPigTypeToDTO(pt *models.PigType) *PigTypeResponse {
if pt == nil {
return nil
}
pigNutrientRequirementDTOs := make([]PigNutrientRequirementDTO, len(pt.PigNutrientRequirements))
for i, pnr := range pt.PigNutrientRequirements {
pigNutrientRequirementDTOs[i] = PigNutrientRequirementDTO{
ID: pnr.ID,
NutrientID: pnr.NutrientID,
NutrientName: pnr.Nutrient.Name, // 假设 Nutrient 已经被预加载
MinRequirement: pnr.MinRequirement,
MaxRequirement: pnr.MaxRequirement,
}
}
return &PigTypeResponse{
ID: pt.ID,
BreedID: pt.BreedID,
BreedName: pt.Breed.Name, // 假设 Breed 已经被预加载
AgeStageID: pt.AgeStageID,
AgeStageName: pt.AgeStage.Name, // 假设 AgeStage 已经被预加载
Description: pt.Description,
DailyFeedIntake: pt.DailyFeedIntake,
DailyGainWeight: pt.DailyGainWeight,
MinDays: pt.MinDays,
MaxDays: pt.MaxDays,
MinWeight: pt.MinWeight,
MaxWeight: pt.MaxWeight,
PigNutrientRequirements: pigNutrientRequirementDTOs,
}
}
// ConvertPigTypeListToDTO 将 []models.PigType 转换为 ListPigTypeResponse DTO
func ConvertPigTypeListToDTO(pigTypes []models.PigType, total int64, page, pageSize int) *ListPigTypeResponse {
pigTypeDTOs := make([]PigTypeResponse, len(pigTypes))
for i, pt := range pigTypes {
pigTypeDTOs[i] = *ConvertPigTypeToDTO(&pt)
}
return &ListPigTypeResponse{
List: pigTypeDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertRecipeToDto 将 models.Recipe 转换为 RecipeResponse DTO
func ConvertRecipeToDto(recipe *models.Recipe) *RecipeResponse {
if recipe == nil {
return nil
}
ingredients := make([]RecipeIngredientDto, len(recipe.RecipeIngredients))
for i, ri := range recipe.RecipeIngredients {
ingredients[i] = RecipeIngredientDto{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &RecipeResponse{
ID: recipe.ID,
Name: recipe.Name,
Description: recipe.Description,
RecipeIngredients: ingredients,
}
}
// ConvertRecipeListToDTO 将 []models.Recipe 转换为 ListRecipeResponse DTO
func ConvertRecipeListToDTO(recipes []models.Recipe, total int64, page, pageSize int) *ListRecipeResponse {
recipeDTOs := make([]RecipeResponse, len(recipes))
for i, r := range recipes {
recipeDTOs[i] = *ConvertRecipeToDto(&r)
}
return &ListRecipeResponse{
List: recipeDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}
// ConvertCreateRecipeRequestToModel 将 CreateRecipeRequest DTO 转换为 models.Recipe 模型
func ConvertCreateRecipeRequestToModel(req *CreateRecipeRequest) *models.Recipe {
if req == nil {
return nil
}
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, ri := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &models.Recipe{
Name: req.Name,
Description: req.Description,
RecipeIngredients: ingredients,
}
}
// ConvertUpdateRecipeRequestToModel 将 UpdateRecipeRequest DTO 转换为 models.Recipe 模型
func ConvertUpdateRecipeRequestToModel(req *UpdateRecipeRequest) *models.Recipe {
if req == nil {
return nil
}
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, ri := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RawMaterialID: ri.RawMaterialID,
Percentage: ri.Percentage,
}
}
return &models.Recipe{
Name: req.Name,
Description: req.Description,
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

@@ -0,0 +1,337 @@
package dto
// =============================================================================================================
// 营养种类 (Nutrient) 相关 DTO
// =============================================================================================================
// CreateNutrientRequest 创建营养种类的请求体
type CreateNutrientRequest struct {
Name string `json:"name" validate:"required,max=100"` // 营养素名称
Description string `json:"description" validate:"max=255"` // 描述
}
// UpdateNutrientRequest 更新营养种类的请求体
type UpdateNutrientRequest struct {
Name string `json:"name" validate:"required,max=100"` // 营养素名称
Description string `json:"description" validate:"max=255"` // 描述
}
// NutrientRawMaterialDTO 用于在营养素信息中展示关联的原料及其含量
type NutrientRawMaterialDTO struct {
ID uint32 `json:"id"` // 原料ID
Name string `json:"name"` // 原料名称
Value float32 `json:"value"` // 该原料中此营养素的含量
}
// NutrientResponse 营养种类响应体
type NutrientResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
RawMaterials []NutrientRawMaterialDTO `json:"raw_materials"` // 包含此营养的原料列表
}
// ListNutrientRequest 定义了获取营养种类列表的请求参数
type ListNutrientRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按营养名称模糊查询
RawMaterialName *string `json:"raw_material_name" query:"raw_material_name"` // 按原料名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListNutrientResponse 是获取营养种类列表的响应结构
type ListNutrientResponse struct {
List []NutrientResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// =============================================================================================================
// 原料 (RawMaterial) 相关 DTO
// =============================================================================================================
// 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/元)
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/元)
MaxAdditionRatio *float32 `json:"max_addition_ratio"` // 最大添加比例
}
// RawMaterialNutrientDTO 原料营养素响应体
type RawMaterialNutrientDTO struct {
ID uint32 `json:"id"`
NutrientID uint32 `json:"nutrient_id"`
Nutrient string `json:"nutrient_name"` // 营养素名称
Value float32 `json:"value"` // 营养价值含量
}
// RawMaterialResponse 原料响应体
type RawMaterialResponse struct {
ID uint32 `json:"id"`
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"` // 关联的营养素信息
}
// ListRawMaterialRequest 定义了获取原料列表的请求参数
type ListRawMaterialRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按原料名称模糊查询
NutrientName *string `json:"nutrient_name" query:"nutrient_name"` // 按营养名称模糊查询
MinReferencePrice *float32 `json:"min_reference_price" query:"min_reference_price"` // 参考价格最小值
MaxReferencePrice *float32 `json:"max_reference_price" query:"max_reference_price"` // 参考价格最大值
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListRawMaterialResponse 是获取原料列表的响应结构
type ListRawMaterialResponse struct {
List []RawMaterialResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// UpdateRawMaterialNutrientsRequest 更新原料营养成分的请求体
type UpdateRawMaterialNutrientsRequest struct {
Nutrients []RawMaterialNutrientItem `json:"nutrients" validate:"required,dive"`
}
// RawMaterialNutrientItem 代表一个营养成分及其含量
type RawMaterialNutrientItem struct {
NutrientID uint32 `json:"nutrient_id" validate:"required"` // 营养素ID
Value float32 `json:"value" validate:"gte=0"` // 含量值必须大于等于0
}
// =============================================================================================================
// 猪品种 (PigBreed) 相关 DTO
// =============================================================================================================
// CreatePigBreedRequest 创建猪品种的请求体
type CreatePigBreedRequest struct {
Name string `json:"name" validate:"required,max=50"` // 品种名称
Description string `json:"description"` // 其他描述
ParentInfo string `json:"parent_info"` // 父母信息
AppearanceFeatures string `json:"appearance_features"` // 外貌特征
BreedAdvantages string `json:"breed_advantages"` // 品种优点
BreedDisadvantages string `json:"breed_disadvantages"` // 品种缺点
}
// UpdatePigBreedRequest 更新猪品种的请求体
type UpdatePigBreedRequest struct {
Name string `json:"name" validate:"required,max=50"` // 品种名称
Description string `json:"description"` // 其他描述
ParentInfo string `json:"parent_info"` // 父母信息
AppearanceFeatures string `json:"appearance_features"` // 外貌特征
BreedAdvantages string `json:"breed_advantages"` // 品种优点
BreedDisadvantages string `json:"breed_disadvantages"` // 品种缺点
}
// PigBreedResponse 猪品种响应体
type PigBreedResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ParentInfo string `json:"parent_info"`
AppearanceFeatures string `json:"appearance_features"`
BreedAdvantages string `json:"breed_advantages"`
BreedDisadvantages string `json:"breed_disadvantages"`
}
// ListPigBreedRequest 定义了获取猪品种列表的请求参数
type ListPigBreedRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListPigBreedResponse 是获取猪品种列表的响应结构
type ListPigBreedResponse struct {
List []PigBreedResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// =============================================================================================================
// 猪年龄阶段 (PigAgeStage) 相关 DTO
// =============================================================================================================
// CreatePigAgeStageRequest 创建猪年龄阶段的请求体
type CreatePigAgeStageRequest struct {
Name string `json:"name" validate:"required,max=50"` // 年龄阶段名称
Description string `json:"description" validate:"max=255"` // 阶段描述
}
// UpdatePigAgeStageRequest 更新猪年龄阶段的请求体
type UpdatePigAgeStageRequest struct {
Name string `json:"name" validate:"required,max=50"` // 年龄阶段名称
Description string `json:"description" validate:"max=255"` // 阶段描述
}
// PigAgeStageResponse 猪年龄阶段响应体
type PigAgeStageResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
// ListPigAgeStageRequest 定义了获取猪年龄阶段列表的请求参数
type ListPigAgeStageRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListPigAgeStageResponse 是获取猪年龄阶段列表的响应结构
type ListPigAgeStageResponse struct {
List []PigAgeStageResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// =============================================================================================================
// 猪类型 (PigType) 相关 DTO
// =============================================================================================================
// CreatePigTypeRequest 创建猪类型的请求体
type CreatePigTypeRequest struct {
BreedID uint32 `json:"breed_id" validate:"required"` // 关联的猪品种ID
AgeStageID uint32 `json:"age_stage_id" validate:"required"` // 关联的猪年龄阶段ID
Description string `json:"description" validate:"max=255"` // 该猪类型的描述或特点
DailyFeedIntake float32 `json:"daily_feed_intake"` // 理论日均食量 (g/天)
DailyGainWeight float32 `json:"daily_gain_weight"` // 理论日增重 (g/天)
MinDays uint32 `json:"min_days"` // 该猪类型在该年龄阶段的最小日龄
MaxDays uint32 `json:"max_days"` // 该猪类型在该年龄阶段的最大日龄
MinWeight float32 `json:"min_weight"` // 该猪类型在该年龄阶段的最小体重 (g)
MaxWeight float32 `json:"max_weight"` // 该猪类型在该年龄阶段的最大体重 (g)
}
// UpdatePigTypeRequest 更新猪类型的请求体
type UpdatePigTypeRequest struct {
BreedID uint32 `json:"breed_id" validate:"required"` // 关联的猪品种ID
AgeStageID uint32 `json:"age_stage_id" validate:"required"` // 关联的猪年龄阶段ID
Description string `json:"description" validate:"max=255"` // 该猪类型的描述或特点
DailyFeedIntake float32 `json:"daily_feed_intake"` // 理论日均食量 (g/天)
DailyGainWeight float32 `json:"daily_gain_weight"` // 理论日增重 (g/天)
MinDays uint32 `json:"min_days"` // 该猪类型在该年龄阶段的最小日龄
MaxDays uint32 `json:"max_days"` // 该猪类型在该年龄阶段的最大日龄
MinWeight float32 `json:"min_weight"` // 该猪类型在该年龄阶段的最小体重 (g)
MaxWeight float32 `json:"max_weight"` // 该猪类型在该年龄阶段的最大体重 (g)
}
// PigNutrientRequirementDTO 猪营养需求响应体
type PigNutrientRequirementDTO struct {
ID uint32 `json:"id"`
NutrientID uint32 `json:"nutrient_id"`
NutrientName string `json:"nutrient_name"` // 营养素名称
MinRequirement float32 `json:"min_requirement"` // 最低营养需求量
MaxRequirement float32 `json:"max_requirement"` // 最高营养需求量
}
// PigTypeResponse 猪类型响应体
type PigTypeResponse struct {
ID uint32 `json:"id"`
BreedID uint32 `json:"breed_id"`
BreedName string `json:"breed_name"` // 猪品种名称
AgeStageID uint32 `json:"age_stage_id"`
AgeStageName string `json:"age_stage_name"` // 猪年龄阶段名称
Description string `json:"description"`
DailyFeedIntake float32 `json:"daily_feed_intake"`
DailyGainWeight float32 `json:"daily_gain_weight"`
MinDays uint32 `json:"min_days"`
MaxDays uint32 `json:"max_days"`
MinWeight float32 `json:"min_weight"`
MaxWeight float32 `json:"max_weight"`
PigNutrientRequirements []PigNutrientRequirementDTO `json:"pig_nutrient_requirements"` // 关联的营养需求
}
// ListPigTypeRequest 定义了获取猪类型列表的请求参数
type ListPigTypeRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
BreedID *uint32 `json:"breed_id" query:"breed_id"` // 关联的猪品种ID
AgeStageID *uint32 `json:"age_stage_id" query:"age_stage_id"` // 关联的猪年龄阶段ID
BreedName *string `json:"breed_name" query:"breed_name"` // 关联的猪品种名称 (用于模糊查询)
AgeStageName *string `json:"age_stage_name" query:"age_stage_name"` // 关联的猪年龄阶段名称 (用于模糊查询)
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListPigTypeResponse 是获取猪类型列表的响应结构
type ListPigTypeResponse struct {
List []PigTypeResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// UpdatePigTypeNutrientRequirementsRequest 更新猪类型营养需求的请求体
type UpdatePigTypeNutrientRequirementsRequest struct {
NutrientRequirements []PigNutrientRequirementItem `json:"nutrient_requirements" validate:"required,dive"`
}
// PigNutrientRequirementItem 代表一个营养需求项
type PigNutrientRequirementItem struct {
NutrientID uint32 `json:"nutrient_id" validate:"required"` // 营养素ID
MinRequirement float32 `json:"min_requirement" validate:"gte=0"` // 最低营养需求量
MaxRequirement float32 `json:"max_requirement" validate:"gte=0"` // 最高营养需求量
}
// =============================================================================================================
// 配方 (Recipe) 相关 DTO
// =============================================================================================================
// RecipeIngredientDto 代表配方中的一个原料及其百分比
type RecipeIngredientDto struct {
RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 原料ID
Percentage float32 `json:"percentage" validate:"gte=0,lte=1"` // 原料在配方中的百分比 (0-1之间)
}
// CreateRecipeRequest 创建配方的请求体
type CreateRecipeRequest struct {
Name string `json:"name" validate:"required,max=100"` // 配方名称
Description string `json:"description" validate:"max=255"` // 配方描述
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients" validate:"dive"` // 配方原料组成
}
// UpdateRecipeRequest 更新配方的请求体
type UpdateRecipeRequest struct {
Name string `json:"name" validate:"required,max=100"` // 配方名称
Description string `json:"description" validate:"max=255"` // 配方描述
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients" validate:"dive"` // 配方原料组成
}
// RecipeResponse 配方响应体
type RecipeResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
RecipeIngredients []RecipeIngredientDto `json:"recipe_ingredients"`
}
// ListRecipeRequest 定义了获取配方列表的请求参数
type ListRecipeRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
Name *string `json:"name" query:"name"` // 按名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
}
// ListRecipeResponse 是获取配方列表的响应结构
type ListRecipeResponse struct {
List []RecipeResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// GenerateRecipeResponse 是一键生成配方的响应体
type GenerateRecipeResponse struct {
ID uint32 `json:"id"` // 新生成的配方ID
Name string `json:"name"` // 新生成的配方名称
Description string `json:"description"` // 新生成的配方描述
}

View File

@@ -0,0 +1,69 @@
package dto
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// ConvertCurrentStockToDTO 将原料及其最新库存日志转换为 CurrentStockResponse DTO
func ConvertCurrentStockToDTO(material *models.RawMaterial, latestLog *models.RawMaterialStockLog) *CurrentStockResponse {
if material == nil {
return nil
}
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,
LastOperationSourceType: lastOperationSourceType,
}
}
// ConvertStockLogToDTO 将 models.RawMaterialStockLog 转换为 StockLogResponse DTO
func ConvertStockLogToDTO(log *models.RawMaterialStockLog) *StockLogResponse {
if log == nil {
return nil
}
return &StockLogResponse{
ID: log.ID,
RawMaterialID: log.RawMaterialID,
RawMaterialName: log.RawMaterial.Name, // 假设 RawMaterial 已被预加载
ChangeAmount: log.ChangeAmount,
BeforeQuantity: log.BeforeQuantity,
AfterQuantity: log.AfterQuantity,
SourceType: log.SourceType,
SourceID: log.SourceID,
HappenedAt: log.HappenedAt,
Remarks: log.Remarks,
}
}
// ConvertStockLogListToDTO 将 []models.RawMaterialStockLog 转换为 ListStockLogResponse DTO
func ConvertStockLogListToDTO(logs []models.RawMaterialStockLog, total int64, page, pageSize int) *ListStockLogResponse {
logDTOs := make([]StockLogResponse, len(logs))
for i, log := range logs {
logDTOs[i] = *ConvertStockLogToDTO(&log)
}
return &ListStockLogResponse{
List: logDTOs,
Pagination: PaginationDTO{
Page: page,
PageSize: pageSize,
Total: total,
},
}
}

View File

@@ -0,0 +1,75 @@
package dto
import (
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
)
// =============================================================================================================
// 库存 (Inventory) 相关 DTO
// =============================================================================================================
// StockAdjustmentRequest 手动调整库存的请求体
type StockAdjustmentRequest struct {
RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 要调整的原料ID
ChangeAmount float32 `json:"change_amount" validate:"required,ne=0"` // 变动数量, 正数为入库, 负数为出库, 单位: g
SourceType models.StockLogSourceType `json:"source_type" validate:"required"` // 库存变动来源类型
SourceID *uint32 `json:"source_id,omitempty"` // 来源ID, 例如: 配方ID, 采购单ID等
Remarks string `json:"remarks" validate:"max=255"` // 备注
}
// 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"` // 最后更新时间
LastOperationSourceType models.StockLogSourceType `json:"last_operation_source_type"` // 上次库存变动的来源类型
}
// ListCurrentStockRequest 定义了获取当前库存列表的请求参数
type ListCurrentStockRequest struct {
Page int `json:"page" query:"page"` // 页码
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 是获取当前库存列表的响应结构
type ListCurrentStockResponse struct {
List []CurrentStockResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// StockLogResponse 库存变动历史记录的响应体
type StockLogResponse struct {
ID uint32 `json:"id"`
RawMaterialID uint32 `json:"raw_material_id"`
RawMaterialName string `json:"raw_material_name"`
ChangeAmount float32 `json:"change_amount"`
BeforeQuantity float32 `json:"before_quantity"`
AfterQuantity float32 `json:"after_quantity"`
SourceType models.StockLogSourceType `json:"source_type"`
SourceID *uint32 `json:"source_id,omitempty"`
HappenedAt time.Time `json:"happened_at"`
Remarks string `json:"remarks"`
}
// ListStockLogRequest 定义了获取库存变动历史的请求参数
type ListStockLogRequest struct {
Page int `json:"page" query:"page"` // 页码
PageSize int `json:"page_size" query:"page_size"` // 每页数量
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"` // 按原料ID精确查询
SourceTypes []models.StockLogSourceType `json:"source_types" query:"source_types"` // 按来源类型查询
StartTime *string `json:"start_time" query:"start_time"` // 开始时间 (RFC3339格式, e.g., "2023-01-01T00:00:00Z")
EndTime *string `json:"end_time" query:"end_time"` // 结束时间 (RFC3339格式)
OrderBy string `json:"order_by" query:"order_by"` // 排序字段
}
// ListStockLogResponse 是获取库存变动历史列表的响应结构
type ListStockLogResponse struct {
List []StockLogResponse `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}

View File

@@ -170,94 +170,6 @@ func NewListUserActionLogResponse(data []models.UserActionLog, total int64, page
}
}
// NewListRawMaterialPurchaseResponse 从模型数据创建列表响应 DTO
func NewListRawMaterialPurchaseResponse(data []models.RawMaterialPurchase, total int64, page, pageSize int) *ListRawMaterialPurchaseResponse {
dtos := make([]RawMaterialPurchaseDTO, len(data))
for i, item := range data {
dtos[i] = RawMaterialPurchaseDTO{
ID: item.ID,
RawMaterialID: item.RawMaterialID,
RawMaterial: RawMaterialDTO{
ID: item.RawMaterial.ID,
Name: item.RawMaterial.Name,
},
Supplier: item.Supplier,
Amount: item.Amount,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
PurchaseDate: item.PurchaseDate,
CreatedAt: item.CreatedAt,
}
}
return &ListRawMaterialPurchaseResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListRawMaterialStockLogResponse 从模型数据创建列表响应 DTO
func NewListRawMaterialStockLogResponse(data []models.RawMaterialStockLog, total int64, page, pageSize int) *ListRawMaterialStockLogResponse {
dtos := make([]RawMaterialStockLogDTO, len(data))
for i, item := range data {
dtos[i] = RawMaterialStockLogDTO{
ID: item.ID,
RawMaterialID: item.RawMaterialID,
ChangeAmount: item.ChangeAmount,
SourceType: item.SourceType,
SourceID: item.SourceID,
HappenedAt: item.HappenedAt,
Remarks: item.Remarks,
}
}
return &ListRawMaterialStockLogResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListFeedUsageRecordResponse 从模型数据创建列表响应 DTO
func NewListFeedUsageRecordResponse(data []models.FeedUsageRecord, total int64, page, pageSize int) *ListFeedUsageRecordResponse {
dtos := make([]FeedUsageRecordDTO, len(data))
for i, item := range data {
dtos[i] = FeedUsageRecordDTO{
ID: item.ID,
PenID: item.PenID,
Pen: PenDTO{
ID: item.Pen.ID,
Name: item.Pen.PenNumber,
},
FeedFormulaID: item.FeedFormulaID,
FeedFormula: FeedFormulaDTO{
ID: item.FeedFormula.ID,
Name: item.FeedFormula.Name,
},
Amount: item.Amount,
RecordedAt: item.RecordedAt,
OperatorID: item.OperatorID,
Remarks: item.Remarks,
}
}
return &ListFeedUsageRecordResponse{
List: dtos,
Pagination: PaginationDTO{
Total: total,
Page: page,
PageSize: pageSize,
},
}
}
// NewListMedicationLogResponse 从模型数据创建列表响应 DTO
func NewListMedicationLogResponse(data []models.MedicationLog, total int64, page, pageSize int) *ListMedicationLogResponse {
dtos := make([]MedicationLogDTO, len(data))

View File

@@ -202,120 +202,6 @@ type ListUserActionLogResponse struct {
Pagination PaginationDTO `json:"pagination"`
}
// --- RawMaterialPurchase ---
// ListRawMaterialPurchaseRequest 定义了获取原料采购列表的请求参数
type ListRawMaterialPurchaseRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"`
Supplier *string `json:"supplier" query:"supplier"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// RawMaterialDTO 是用于API响应的简化版原料结构
type RawMaterialDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// RawMaterialPurchaseDTO 是用于API响应的原料采购结构
type RawMaterialPurchaseDTO struct {
ID uint32 `json:"id"`
RawMaterialID uint32 `json:"raw_material_id"`
RawMaterial RawMaterialDTO `json:"raw_material"`
Supplier string `json:"supplier"`
Amount float32 `json:"amount"`
UnitPrice float32 `json:"unit_price"`
TotalPrice float32 `json:"total_price"`
PurchaseDate time.Time `json:"purchase_date"`
CreatedAt time.Time `json:"created_at"`
}
// ListRawMaterialPurchaseResponse 是获取原料采购列表的响应结构
type ListRawMaterialPurchaseResponse struct {
List []RawMaterialPurchaseDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- RawMaterialStockLog ---
// ListRawMaterialStockLogRequest 定义了获取原料库存日志列表的请求参数
type ListRawMaterialStockLogRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
RawMaterialID *uint32 `json:"raw_material_id" query:"raw_material_id"`
SourceType *string `json:"source_type" query:"source_type"`
SourceID *uint32 `json:"source_id" query:"source_id"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// RawMaterialStockLogDTO 是用于API响应的原料库存日志结构
type RawMaterialStockLogDTO struct {
ID uint32 `json:"id"`
RawMaterialID uint32 `json:"raw_material_id"`
ChangeAmount float32 `json:"change_amount"`
SourceType models.StockLogSourceType `json:"source_type"`
SourceID uint32 `json:"source_id"`
HappenedAt time.Time `json:"happened_at"`
Remarks string `json:"remarks"`
}
// ListRawMaterialStockLogResponse 是获取原料库存日志列表的响应结构
type ListRawMaterialStockLogResponse struct {
List []RawMaterialStockLogDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- FeedUsageRecord ---
// ListFeedUsageRecordRequest 定义了获取饲料使用记录列表的请求参数
type ListFeedUsageRecordRequest struct {
Page int `json:"page" query:"page"`
PageSize int `json:"page_size" query:"page_size"`
PenID *uint32 `json:"pen_id" query:"pen_id"`
FeedFormulaID *uint32 `json:"feed_formula_id" query:"feed_formula_id"`
OperatorID *uint32 `json:"operator_id" query:"operator_id"`
StartTime *time.Time `json:"start_time" query:"start_time"`
EndTime *time.Time `json:"end_time" query:"end_time"`
OrderBy string `json:"order_by" query:"order_by"`
}
// PenDTO 是用于API响应的简化版猪栏结构
type PenDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// FeedFormulaDTO 是用于API响应的简化版饲料配方结构
type FeedFormulaDTO struct {
ID uint32 `json:"id"`
Name string `json:"name"`
}
// FeedUsageRecordDTO 是用于API响应的饲料使用记录结构
type FeedUsageRecordDTO struct {
ID uint32 `json:"id"`
PenID uint32 `json:"pen_id"`
Pen PenDTO `json:"pen"`
FeedFormulaID uint32 `json:"feed_formula_id"`
FeedFormula FeedFormulaDTO `json:"feed_formula"`
Amount float32 `json:"amount"`
RecordedAt time.Time `json:"recorded_at"`
OperatorID uint32 `json:"operator_id"`
Remarks string `json:"remarks"`
}
// ListFeedUsageRecordResponse 是获取饲料使用记录列表的响应结构
type ListFeedUsageRecordResponse struct {
List []FeedUsageRecordDTO `json:"list"`
Pagination PaginationDTO `json:"pagination"`
}
// --- MedicationLog ---
// ListMedicationLogRequest 定义了获取用药记录列表的请求参数

View File

@@ -0,0 +1,158 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/inventory"
"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"
)
// 定义库存应用服务特定的错误
var (
ErrInventoryRawMaterialNotFound = errors.New("原料不存在")
ErrInventoryInsufficientStock = errors.New("原料库存不足")
)
// InventoryService 定义了库存相关的应用服务接口
type InventoryService interface {
AdjustStock(ctx context.Context, req *dto.StockAdjustmentRequest) (*dto.StockLogResponse, error)
ListCurrentStock(ctx context.Context, req *dto.ListCurrentStockRequest) (*dto.ListCurrentStockResponse, error)
ListStockLogs(ctx context.Context, req *dto.ListStockLogRequest) (*dto.ListStockLogResponse, error)
}
// inventoryServiceImpl 是 InventoryService 接口的实现
type inventoryServiceImpl struct {
ctx context.Context
invSvc inventory.InventoryCoreService
rawMatRepo repository.RawMaterialRepository
}
// NewInventoryService 创建一个新的 InventoryService 实例
func NewInventoryService(ctx context.Context, invSvc inventory.InventoryCoreService, rawMatRepo repository.RawMaterialRepository) InventoryService {
return &inventoryServiceImpl{
ctx: ctx,
invSvc: invSvc,
rawMatRepo: rawMatRepo,
}
}
// AdjustStock 手动调整库存
func (s *inventoryServiceImpl) AdjustStock(ctx context.Context, req *dto.StockAdjustmentRequest) (*dto.StockLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AdjustStock")
// 调用领域服务执行核心业务逻辑
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
}
if errors.Is(err, inventory.ErrInsufficientStock) {
return nil, ErrInventoryInsufficientStock
}
return nil, fmt.Errorf("调整库存失败: %w", err)
}
// 手动加载 RawMaterial 信息,因为 CreateRawMaterialStockLog 不会预加载它
rawMaterial, err := s.rawMatRepo.GetRawMaterialByID(serviceCtx, log.RawMaterialID)
if err != nil {
// 理论上不应该发生,因为 AdjustStock 内部已经检查过
return nil, fmt.Errorf("获取原料信息失败: %w", err)
}
log.RawMaterial = *rawMaterial
return dto.ConvertStockLogToDTO(log), nil
}
// ListCurrentStock 列出所有原料的当前库存
func (s *inventoryServiceImpl) ListCurrentStock(ctx context.Context, req *dto.ListCurrentStockRequest) (*dto.ListCurrentStockResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListCurrentStock")
// 1. 获取分页的原料列表
rawMatOpts := repository.RawMaterialListOptions{
Name: req.RawMaterialName,
OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序
HasStock: req.HasStock,
}
rawMaterials, total, err := s.rawMatRepo.ListRawMaterials(serviceCtx, rawMatOpts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取原料列表失败: %w", err)
}
if len(rawMaterials) == 0 {
return &dto.ListCurrentStockResponse{
List: []dto.CurrentStockResponse{},
Pagination: dto.PaginationDTO{Page: req.Page, PageSize: req.PageSize, Total: total},
}, nil
}
// 2. 提取原料ID并批量获取它们的最新库存日志
materialIDs := make([]uint32, len(rawMaterials))
for i, rm := range rawMaterials {
materialIDs[i] = rm.ID
}
latestLogMap, err := s.rawMatRepo.BatchGetLatestStockLogsForMaterials(serviceCtx, materialIDs)
if err != nil {
return nil, fmt.Errorf("批量获取最新库存日志失败: %w", err)
}
// 3. 组合原料信息和库存信息
stockDTOs := make([]dto.CurrentStockResponse, len(rawMaterials))
for i, rm := range rawMaterials {
log, _ := latestLogMap[rm.ID] // 如果找不到log会是零值
stockDTOs[i] = *dto.ConvertCurrentStockToDTO(&rm, &log)
}
return &dto.ListCurrentStockResponse{
List: stockDTOs,
Pagination: dto.PaginationDTO{Page: req.Page, PageSize: req.PageSize, Total: total},
}, nil
}
// ListStockLogs 列出库存变动历史
func (s *inventoryServiceImpl) ListStockLogs(ctx context.Context, req *dto.ListStockLogRequest) (*dto.ListStockLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListStockLogs")
// 解析时间字符串
var startTime, endTime *time.Time
if req.StartTime != nil && *req.StartTime != "" {
t, err := time.Parse(time.RFC3339, *req.StartTime)
if err != nil {
return nil, fmt.Errorf("无效的开始时间格式: %w", err)
}
startTime = &t
}
if req.EndTime != nil && *req.EndTime != "" {
t, err := time.Parse(time.RFC3339, *req.EndTime)
if err != nil {
return nil, fmt.Errorf("无效的结束时间格式: %w", err)
}
endTime = &t
}
// 转换 source types
sourceTypes := make([]models.StockLogSourceType, len(req.SourceTypes))
for i, st := range req.SourceTypes {
sourceTypes[i] = models.StockLogSourceType(st)
}
opts := repository.StockLogListOptions{
RawMaterialID: req.RawMaterialID,
SourceTypes: sourceTypes,
StartTime: startTime,
EndTime: endTime,
OrderBy: req.OrderBy,
}
logs, total, err := s.invSvc.ListStockLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取库存日志列表失败: %w", err)
}
return dto.ConvertStockLogListToDTO(logs, total, req.Page, req.PageSize), nil
}

View File

@@ -17,9 +17,6 @@ type MonitorService interface {
ListTaskExecutionLogs(ctx context.Context, req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error)
ListPendingCollections(ctx context.Context, req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error)
ListUserActionLogs(ctx context.Context, req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error)
ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error)
ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error)
ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error)
ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error)
ListPigBatchLogs(ctx context.Context, req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error)
ListWeighingBatches(ctx context.Context, req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error)
@@ -40,7 +37,6 @@ type monitorService struct {
planRepository repository.PlanRepository
pendingCollectionRepo repository.PendingCollectionRepository
userActionLogRepo repository.UserActionLogRepository
rawMaterialRepo repository.RawMaterialRepository
medicationRepo repository.MedicationLogRepository
pigBatchRepo repository.PigBatchRepository
pigBatchLogRepo repository.PigBatchLogRepository
@@ -59,7 +55,6 @@ func NewMonitorService(
planRepository repository.PlanRepository,
pendingCollectionRepo repository.PendingCollectionRepository,
userActionLogRepo repository.UserActionLogRepository,
rawMaterialRepo repository.RawMaterialRepository,
medicationRepo repository.MedicationLogRepository,
pigBatchRepo repository.PigBatchRepository,
pigBatchLogRepo repository.PigBatchLogRepository,
@@ -76,7 +71,6 @@ func NewMonitorService(
planRepository: planRepository,
pendingCollectionRepo: pendingCollectionRepo,
userActionLogRepo: userActionLogRepo,
rawMaterialRepo: rawMaterialRepo,
medicationRepo: medicationRepo,
pigBatchRepo: pigBatchRepo,
pigBatchLogRepo: pigBatchLogRepo,
@@ -236,68 +230,6 @@ func (s *monitorService) ListUserActionLogs(ctx context.Context, req *dto.ListUs
return dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize), nil
}
// ListRawMaterialPurchases 负责处理查询原料采购记录列表的业务逻辑
func (s *monitorService) ListRawMaterialPurchases(ctx context.Context, req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialPurchases")
opts := repository.RawMaterialPurchaseListOptions{
RawMaterialID: req.RawMaterialID,
Supplier: req.Supplier,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize), nil
}
// ListRawMaterialStockLogs 负责处理查询原料库存日志列表的业务逻辑
func (s *monitorService) ListRawMaterialStockLogs(ctx context.Context, req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterialStockLogs")
opts := repository.RawMaterialStockLogListOptions{
RawMaterialID: req.RawMaterialID,
SourceID: req.SourceID,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
if req.SourceType != nil {
sourceType := models.StockLogSourceType(*req.SourceType)
opts.SourceType = &sourceType
}
data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize), nil
}
// ListFeedUsageRecords 负责处理查询饲料使用记录列表的业务逻辑
func (s *monitorService) ListFeedUsageRecords(ctx context.Context, req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListFeedUsageRecords")
opts := repository.FeedUsageRecordListOptions{
PenID: req.PenID,
FeedFormulaID: req.FeedFormulaID,
OperatorID: req.OperatorID,
OrderBy: req.OrderBy,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, err
}
return dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize), nil
}
// ListMedicationLogs 负责处理查询用药记录列表的业务逻辑
func (s *monitorService) ListMedicationLogs(ctx context.Context, req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListMedicationLogs")

View File

@@ -0,0 +1,116 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository"
)
// 定义营养种类服务特定的错误
var (
ErrNutrientNameConflict = errors.New("营养种类名称已存在")
ErrNutrientNotFound = errors.New("营养种类不存在")
)
// NutrientService 定义了营养种类相关的应用服务接口
type NutrientService interface {
CreateNutrient(ctx context.Context, req *dto.CreateNutrientRequest) (*dto.NutrientResponse, error)
UpdateNutrient(ctx context.Context, id uint32, req *dto.UpdateNutrientRequest) (*dto.NutrientResponse, error)
DeleteNutrient(ctx context.Context, id uint32) error
GetNutrient(ctx context.Context, id uint32) (*dto.NutrientResponse, error)
ListNutrients(ctx context.Context, req *dto.ListNutrientRequest) (*dto.ListNutrientResponse, error)
}
// nutrientServiceImpl 是 NutrientService 接口的实现
type nutrientServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewNutrientService 创建一个新的 NutrientService 实例
func NewNutrientService(ctx context.Context, recipeSvc recipe.Service) NutrientService {
return &nutrientServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreateNutrient 创建营养种类
func (s *nutrientServiceImpl) CreateNutrient(ctx context.Context, req *dto.CreateNutrientRequest) (*dto.NutrientResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateNutrient")
nutrient, err := s.recipeSvc.CreateNutrient(serviceCtx, req.Name, req.Description)
if err != nil {
if errors.Is(err, recipe.ErrNutrientNameConflict) {
return nil, ErrNutrientNameConflict
}
return nil, fmt.Errorf("创建营养种类失败: %w", err)
}
return dto.ConvertNutrientToDTO(nutrient), nil
}
// UpdateNutrient 更新营养种类
func (s *nutrientServiceImpl) UpdateNutrient(ctx context.Context, id uint32, req *dto.UpdateNutrientRequest) (*dto.NutrientResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateNutrient")
nutrient, err := s.recipeSvc.UpdateNutrient(serviceCtx, id, req.Name, req.Description)
if err != nil {
if errors.Is(err, recipe.ErrNutrientNotFound) {
return nil, ErrNutrientNotFound
}
if errors.Is(err, recipe.ErrNutrientNameConflict) {
return nil, ErrNutrientNameConflict
}
return nil, fmt.Errorf("更新营养种类失败: %w", err)
}
return dto.ConvertNutrientToDTO(nutrient), nil
}
// DeleteNutrient 删除营养种类
func (s *nutrientServiceImpl) DeleteNutrient(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteNutrient")
err := s.recipeSvc.DeleteNutrient(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrNutrientNotFound) {
return ErrNutrientNotFound
}
return fmt.Errorf("删除营养种类失败: %w", err)
}
return nil
}
// GetNutrient 获取单个营养种类
func (s *nutrientServiceImpl) GetNutrient(ctx context.Context, id uint32) (*dto.NutrientResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetNutrient")
nutrient, err := s.recipeSvc.GetNutrient(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrNutrientNotFound) {
return nil, ErrNutrientNotFound
}
return nil, fmt.Errorf("获取营养种类失败: %w", err)
}
return dto.ConvertNutrientToDTO(nutrient), nil
}
// ListNutrients 列出营养种类
func (s *nutrientServiceImpl) ListNutrients(ctx context.Context, req *dto.ListNutrientRequest) (*dto.ListNutrientResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients")
opts := repository.NutrientListOptions{
Name: req.Name,
RawMaterialName: req.RawMaterialName,
OrderBy: req.OrderBy,
}
nutrients, total, err := s.recipeSvc.ListNutrients(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取营养种类列表失败: %w", err)
}
return dto.ConvertNutrientListToDTO(nutrients, total, req.Page, req.PageSize), nil
}

View File

@@ -0,0 +1,122 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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"
)
// 定义猪年龄阶段服务特定的错误
var (
ErrPigAgeStageInUse = errors.New("猪年龄阶段正在被猪类型使用,无法删除")
ErrPigAgeStageNotFound = errors.New("猪年龄阶段不存在")
)
// PigAgeStageService 定义了猪年龄阶段相关的应用服务接口
type PigAgeStageService interface {
CreatePigAgeStage(ctx context.Context, req *dto.CreatePigAgeStageRequest) (*dto.PigAgeStageResponse, error)
UpdatePigAgeStage(ctx context.Context, id uint32, req *dto.UpdatePigAgeStageRequest) (*dto.PigAgeStageResponse, error)
DeletePigAgeStage(ctx context.Context, id uint32) error
GetPigAgeStage(ctx context.Context, id uint32) (*dto.PigAgeStageResponse, error)
ListPigAgeStages(ctx context.Context, req *dto.ListPigAgeStageRequest) (*dto.ListPigAgeStageResponse, error)
}
// pigAgeStageServiceImpl 是 PigAgeStageService 接口的实现
type pigAgeStageServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewPigAgeStageService 创建一个新的 PigAgeStageService 实例
func NewPigAgeStageService(ctx context.Context, recipeSvc recipe.Service) PigAgeStageService {
return &pigAgeStageServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreatePigAgeStage 创建猪年龄阶段
func (s *pigAgeStageServiceImpl) CreatePigAgeStage(ctx context.Context, req *dto.CreatePigAgeStageRequest) (*dto.PigAgeStageResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigAgeStage")
ageStage := &models.PigAgeStage{
Name: req.Name,
Description: req.Description,
}
if err := s.recipeSvc.CreatePigAgeStage(serviceCtx, ageStage); err != nil {
return nil, fmt.Errorf("创建猪年龄阶段失败: %w", err)
}
return dto.ConvertPigAgeStageToDTO(ageStage), nil
}
// UpdatePigAgeStage 更新猪年龄阶段
func (s *pigAgeStageServiceImpl) UpdatePigAgeStage(ctx context.Context, id uint32, req *dto.UpdatePigAgeStageRequest) (*dto.PigAgeStageResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigAgeStage")
ageStage := &models.PigAgeStage{
Model: models.Model{ID: id},
Name: req.Name,
Description: req.Description,
}
if err := s.recipeSvc.UpdatePigAgeStage(serviceCtx, ageStage); err != nil {
if errors.Is(err, recipe.ErrPigAgeStageNotFound) {
return nil, ErrPigAgeStageNotFound
}
return nil, fmt.Errorf("更新猪年龄阶段失败: %w", err)
}
return dto.ConvertPigAgeStageToDTO(ageStage), nil
}
// DeletePigAgeStage 删除猪年龄阶段
func (s *pigAgeStageServiceImpl) DeletePigAgeStage(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigAgeStage")
err := s.recipeSvc.DeletePigAgeStage(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigAgeStageNotFound) {
return ErrPigAgeStageNotFound
}
if errors.Is(err, recipe.ErrPigAgeStageInUse) {
return ErrPigAgeStageInUse
}
return fmt.Errorf("删除猪年龄阶段失败: %w", err)
}
return nil
}
// GetPigAgeStage 获取单个猪年龄阶段
func (s *pigAgeStageServiceImpl) GetPigAgeStage(ctx context.Context, id uint32) (*dto.PigAgeStageResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigAgeStage")
ageStage, err := s.recipeSvc.GetPigAgeStageByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigAgeStageNotFound) {
return nil, ErrPigAgeStageNotFound
}
return nil, fmt.Errorf("获取猪年龄阶段失败: %w", err)
}
return dto.ConvertPigAgeStageToDTO(ageStage), nil
}
// ListPigAgeStages 列出猪年龄阶段
func (s *pigAgeStageServiceImpl) ListPigAgeStages(ctx context.Context, req *dto.ListPigAgeStageRequest) (*dto.ListPigAgeStageResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigAgeStages")
opts := repository.PigAgeStageListOptions{
Name: req.Name,
OrderBy: req.OrderBy,
}
ageStages, total, err := s.recipeSvc.ListPigAgeStages(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取猪年龄阶段列表失败: %w", err)
}
return dto.ConvertPigAgeStageListToDTO(ageStages, total, req.Page, req.PageSize), nil
}

View File

@@ -0,0 +1,130 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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"
)
// 定义猪品种服务特定的错误
var (
ErrPigBreedInUse = errors.New("猪品种正在被猪类型使用,无法删除")
ErrPigBreedNotFound = errors.New("猪品种不存在")
)
// PigBreedService 定义了猪品种相关的应用服务接口
type PigBreedService interface {
CreatePigBreed(ctx context.Context, req *dto.CreatePigBreedRequest) (*dto.PigBreedResponse, error)
UpdatePigBreed(ctx context.Context, id uint32, req *dto.UpdatePigBreedRequest) (*dto.PigBreedResponse, error)
DeletePigBreed(ctx context.Context, id uint32) error
GetPigBreed(ctx context.Context, id uint32) (*dto.PigBreedResponse, error)
ListPigBreeds(ctx context.Context, req *dto.ListPigBreedRequest) (*dto.ListPigBreedResponse, error)
}
// pigBreedServiceImpl 是 PigBreedService 接口的实现
type pigBreedServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewPigBreedService 创建一个新的 PigBreedService 实例
func NewPigBreedService(ctx context.Context, recipeSvc recipe.Service) PigBreedService {
return &pigBreedServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreatePigBreed 创建猪品种
func (s *pigBreedServiceImpl) CreatePigBreed(ctx context.Context, req *dto.CreatePigBreedRequest) (*dto.PigBreedResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBreed")
breed := &models.PigBreed{
Name: req.Name,
Description: req.Description,
ParentInfo: req.ParentInfo,
AppearanceFeatures: req.AppearanceFeatures,
BreedAdvantages: req.BreedAdvantages,
BreedDisadvantages: req.BreedDisadvantages,
}
if err := s.recipeSvc.CreatePigBreed(serviceCtx, breed); err != nil {
return nil, fmt.Errorf("创建猪品种失败: %w", err)
}
return dto.ConvertPigBreedToDTO(breed), nil
}
// UpdatePigBreed 更新猪品种
func (s *pigBreedServiceImpl) UpdatePigBreed(ctx context.Context, id uint32, req *dto.UpdatePigBreedRequest) (*dto.PigBreedResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigBreed")
breed := &models.PigBreed{
Model: models.Model{ID: id},
Name: req.Name,
Description: req.Description,
ParentInfo: req.ParentInfo,
AppearanceFeatures: req.AppearanceFeatures,
BreedAdvantages: req.BreedAdvantages,
BreedDisadvantages: req.BreedDisadvantages,
}
if err := s.recipeSvc.UpdatePigBreed(serviceCtx, breed); err != nil {
if errors.Is(err, recipe.ErrPigBreedNotFound) {
return nil, ErrPigBreedNotFound
}
return nil, fmt.Errorf("更新猪品种失败: %w", err)
}
return dto.ConvertPigBreedToDTO(breed), nil
}
// DeletePigBreed 删除猪品种
func (s *pigBreedServiceImpl) DeletePigBreed(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigBreed")
err := s.recipeSvc.DeletePigBreed(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigBreedNotFound) {
return ErrPigBreedNotFound
}
if errors.Is(err, recipe.ErrPigBreedInUse) {
return ErrPigBreedInUse
}
return fmt.Errorf("删除猪品种失败: %w", err)
}
return nil
}
// GetPigBreed 获取单个猪品种
func (s *pigBreedServiceImpl) GetPigBreed(ctx context.Context, id uint32) (*dto.PigBreedResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigBreed")
breed, err := s.recipeSvc.GetPigBreedByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigBreedNotFound) {
return nil, ErrPigBreedNotFound
}
return nil, fmt.Errorf("获取猪品种失败: %w", err)
}
return dto.ConvertPigBreedToDTO(breed), nil
}
// ListPigBreeds 列出猪品种
func (s *pigBreedServiceImpl) ListPigBreeds(ctx context.Context, req *dto.ListPigBreedRequest) (*dto.ListPigBreedResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigBreeds")
opts := repository.PigBreedListOptions{
Name: req.Name,
OrderBy: req.OrderBy,
}
breeds, total, err := s.recipeSvc.ListPigBreeds(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取猪品种列表失败: %w", err)
}
return dto.ConvertPigBreedListToDTO(breeds, total, req.Page, req.PageSize), nil
}

View File

@@ -0,0 +1,203 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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"
)
// 定义猪类型服务特定的错误
var (
ErrPigTypeNotFound = errors.New("猪类型不存在")
)
// PigTypeService 定义了猪类型相关的应用服务接口
type PigTypeService interface {
CreatePigType(ctx context.Context, req *dto.CreatePigTypeRequest) (*dto.PigTypeResponse, error)
UpdatePigType(ctx context.Context, id uint32, req *dto.UpdatePigTypeRequest) (*dto.PigTypeResponse, error)
DeletePigType(ctx context.Context, id uint32) error
GetPigType(ctx context.Context, id uint32) (*dto.PigTypeResponse, error)
ListPigTypes(ctx context.Context, req *dto.ListPigTypeRequest) (*dto.ListPigTypeResponse, error)
UpdatePigTypeNutrientRequirements(ctx context.Context, id uint32, req *dto.UpdatePigTypeNutrientRequirementsRequest) (*dto.PigTypeResponse, error)
}
// pigTypeServiceImpl 是 PigTypeService 接口的实现
type pigTypeServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewPigTypeService 创建一个新的 PigTypeService 实例
func NewPigTypeService(ctx context.Context, recipeSvc recipe.Service) PigTypeService {
return &pigTypeServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreatePigType 创建猪类型
func (s *pigTypeServiceImpl) CreatePigType(ctx context.Context, req *dto.CreatePigTypeRequest) (*dto.PigTypeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigType")
pigType := &models.PigType{
BreedID: req.BreedID,
AgeStageID: req.AgeStageID,
Description: req.Description,
DailyFeedIntake: req.DailyFeedIntake,
DailyGainWeight: req.DailyGainWeight,
MinDays: req.MinDays,
MaxDays: req.MaxDays,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
}
if err := s.recipeSvc.CreatePigType(serviceCtx, pigType); err != nil {
if errors.Is(err, recipe.ErrPigBreedNotFound) {
return nil, ErrPigBreedNotFound
}
if errors.Is(err, recipe.ErrPigAgeStageNotFound) {
return nil, ErrPigAgeStageNotFound
}
return nil, fmt.Errorf("创建猪类型失败: %w", err)
}
// 创建后需要重新获取,以包含关联数据
createdPigType, err := s.recipeSvc.GetPigTypeByID(serviceCtx, pigType.ID)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) { // 理论上不应该发生,因为刚创建
return nil, ErrPigTypeNotFound
}
return nil, fmt.Errorf("创建猪类型后获取详情失败: %w", err)
}
return dto.ConvertPigTypeToDTO(createdPigType), nil
}
// UpdatePigType 更新猪类型
func (s *pigTypeServiceImpl) UpdatePigType(ctx context.Context, id uint32, req *dto.UpdatePigTypeRequest) (*dto.PigTypeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigType")
pigType := &models.PigType{
Model: models.Model{ID: id},
BreedID: req.BreedID,
AgeStageID: req.AgeStageID,
Description: req.Description,
DailyFeedIntake: req.DailyFeedIntake,
DailyGainWeight: req.DailyGainWeight,
MinDays: req.MinDays,
MaxDays: req.MaxDays,
MinWeight: req.MinWeight,
MaxWeight: req.MaxWeight,
}
if err := s.recipeSvc.UpdatePigType(serviceCtx, pigType); err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) {
return nil, ErrPigTypeNotFound
}
if errors.Is(err, recipe.ErrPigBreedNotFound) {
return nil, ErrPigBreedNotFound
}
if errors.Is(err, recipe.ErrPigAgeStageNotFound) {
return nil, ErrPigAgeStageNotFound
}
return nil, fmt.Errorf("更新猪类型失败: %w", err)
}
// 更新后需要重新获取,以包含关联数据
updatedPigType, err := s.recipeSvc.GetPigTypeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) { // 理论上不应该发生,因为刚更新成功
return nil, ErrPigTypeNotFound
}
return nil, fmt.Errorf("更新猪类型后获取详情失败: %w", err)
}
return dto.ConvertPigTypeToDTO(updatedPigType), nil
}
// DeletePigType 删除猪类型
func (s *pigTypeServiceImpl) DeletePigType(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigType")
err := s.recipeSvc.DeletePigType(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) {
return ErrPigTypeNotFound
}
return fmt.Errorf("删除猪类型失败: %w", err)
}
return nil
}
// GetPigType 获取单个猪类型
func (s *pigTypeServiceImpl) GetPigType(ctx context.Context, id uint32) (*dto.PigTypeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigType")
pigType, err := s.recipeSvc.GetPigTypeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) {
return nil, ErrPigTypeNotFound
}
return nil, fmt.Errorf("获取猪类型失败: %w", err)
}
return dto.ConvertPigTypeToDTO(pigType), nil
}
// ListPigTypes 列出猪类型
func (s *pigTypeServiceImpl) ListPigTypes(ctx context.Context, req *dto.ListPigTypeRequest) (*dto.ListPigTypeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigTypes")
opts := repository.PigTypeListOptions{
BreedID: req.BreedID,
AgeStageID: req.AgeStageID,
BreedName: req.BreedName,
AgeStageName: req.AgeStageName,
OrderBy: req.OrderBy,
}
pigTypes, total, err := s.recipeSvc.ListPigTypes(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取猪类型列表失败: %w", err)
}
return dto.ConvertPigTypeListToDTO(pigTypes, total, req.Page, req.PageSize), nil
}
// UpdatePigTypeNutrientRequirements 全量更新猪类型的营养需求
func (s *pigTypeServiceImpl) UpdatePigTypeNutrientRequirements(ctx context.Context, id uint32, req *dto.UpdatePigTypeNutrientRequirementsRequest) (*dto.PigTypeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigTypeNutrientRequirements")
// 1. 将 DTO 转换为领域模型
requirements := make([]models.PigNutrientRequirement, len(req.NutrientRequirements))
for i, item := range req.NutrientRequirements {
requirements[i] = models.PigNutrientRequirement{
PigTypeID: id, // 设置所属的 PigTypeID
NutrientID: item.NutrientID,
MinRequirement: item.MinRequirement,
MaxRequirement: item.MaxRequirement,
}
}
// 2. 调用领域服务执行更新命令
err := s.recipeSvc.UpdatePigTypeNutrientRequirements(serviceCtx, id, requirements)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) {
return nil, ErrPigTypeNotFound
}
// 此处可以根据领域层可能返回的其他特定错误进行转换
return nil, fmt.Errorf("更新猪类型营养需求失败: %w", err)
}
// 3. 更新成功后,调用查询服务获取最新的猪类型信息
updatedPigType, err := s.recipeSvc.GetPigTypeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrPigTypeNotFound) {
// 理论上不应该发生,因为刚更新成功
return nil, ErrPigTypeNotFound
}
return nil, fmt.Errorf("更新后获取猪类型信息失败: %w", err)
}
// 4. 将领域模型转换为 DTO 并返回
return dto.ConvertPigTypeToDTO(updatedPigType), nil
}

View File

@@ -0,0 +1,159 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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"
)
// 定义原料服务特定的错误
var (
ErrRawMaterialNameConflict = errors.New("原料名称已存在")
ErrRawMaterialNotFound = errors.New("原料不存在")
)
// RawMaterialService 定义了原料相关的应用服务接口
type RawMaterialService interface {
CreateRawMaterial(ctx context.Context, req *dto.CreateRawMaterialRequest) (*dto.RawMaterialResponse, error)
UpdateRawMaterial(ctx context.Context, id uint32, req *dto.UpdateRawMaterialRequest) (*dto.RawMaterialResponse, error)
DeleteRawMaterial(ctx context.Context, id uint32) error
GetRawMaterial(ctx context.Context, id uint32) (*dto.RawMaterialResponse, error)
ListRawMaterials(ctx context.Context, req *dto.ListRawMaterialRequest) (*dto.ListRawMaterialResponse, error)
UpdateRawMaterialNutrients(ctx context.Context, id uint32, req *dto.UpdateRawMaterialNutrientsRequest) (*dto.RawMaterialResponse, error)
}
// rawMaterialServiceImpl 是 RawMaterialService 接口的实现
type rawMaterialServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewRawMaterialService 创建一个新的 RawMaterialService 实例
func NewRawMaterialService(ctx context.Context, recipeSvc recipe.Service) RawMaterialService {
return &rawMaterialServiceImpl{
ctx: ctx,
recipeSvc: recipeSvc,
}
}
// CreateRawMaterial 创建原料
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, req.MaxAdditionRatio)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNameConflict) {
return nil, ErrRawMaterialNameConflict
}
return nil, fmt.Errorf("创建原料失败: %w", err)
}
return dto.ConvertRawMaterialToDTO(rawMaterial), nil
}
// UpdateRawMaterial 更新原料
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, req.MaxAdditionRatio)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return nil, ErrRawMaterialNotFound
}
if errors.Is(err, recipe.ErrRawMaterialNameConflict) {
return nil, ErrRawMaterialNameConflict
}
return nil, fmt.Errorf("更新原料失败: %w", err)
}
return dto.ConvertRawMaterialToDTO(rawMaterial), nil
}
// DeleteRawMaterial 删除原料
func (s *rawMaterialServiceImpl) DeleteRawMaterial(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteRawMaterial")
err := s.recipeSvc.DeleteRawMaterial(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return ErrRawMaterialNotFound
}
return fmt.Errorf("删除原料失败: %w", err)
}
return nil
}
// GetRawMaterial 获取单个原料
func (s *rawMaterialServiceImpl) GetRawMaterial(ctx context.Context, id uint32) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRawMaterial")
rawMaterial, err := s.recipeSvc.GetRawMaterial(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return nil, ErrRawMaterialNotFound
}
return nil, fmt.Errorf("获取原料失败: %w", err)
}
return dto.ConvertRawMaterialToDTO(rawMaterial), nil
}
// ListRawMaterials 列出原料
func (s *rawMaterialServiceImpl) ListRawMaterials(ctx context.Context, req *dto.ListRawMaterialRequest) (*dto.ListRawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials")
opts := repository.RawMaterialListOptions{
Name: req.Name,
NutrientName: req.NutrientName,
MinReferencePrice: req.MinReferencePrice,
MaxReferencePrice: req.MaxReferencePrice,
OrderBy: req.OrderBy,
}
rawMaterials, total, err := s.recipeSvc.ListRawMaterials(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取原料列表失败: %w", err)
}
return dto.ConvertRawMaterialListToDTO(rawMaterials, total, req.Page, req.PageSize), nil
}
// UpdateRawMaterialNutrients 全量更新原料的营养成分
func (s *rawMaterialServiceImpl) UpdateRawMaterialNutrients(ctx context.Context, id uint32, req *dto.UpdateRawMaterialNutrientsRequest) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterialNutrients")
// 1. 将 DTO 转换为领域模型
nutrients := make([]models.RawMaterialNutrient, len(req.Nutrients))
for i, item := range req.Nutrients {
nutrients[i] = models.RawMaterialNutrient{
RawMaterialID: id,
NutrientID: item.NutrientID,
Value: item.Value,
}
}
// 2. 调用领域服务执行更新命令
err := s.recipeSvc.UpdateRawMaterialNutrients(serviceCtx, id, nutrients)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return nil, ErrRawMaterialNotFound
}
// 此处可以根据领域层可能返回的其他特定错误进行转换
return nil, fmt.Errorf("更新原料营养成分失败: %w", err)
}
// 3. 更新成功后,调用查询服务获取最新的原料信息
updatedRawMaterial, err := s.recipeSvc.GetRawMaterial(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
// 理论上不应该发生,因为刚更新成功
return nil, ErrRawMaterialNotFound
}
return nil, fmt.Errorf("更新后获取原料信息失败: %w", err)
}
// 4. 将领域模型转换为 DTO 并返回
return dto.ConvertRawMaterialToDTO(updatedRawMaterial), nil
}

View File

@@ -0,0 +1,177 @@
package service
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/app/dto"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe"
"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"
)
// 定义配方服务特定的错误
var (
ErrRecipeNameConflict = errors.New("配方名称已存在")
ErrRecipeNotFound = errors.New("配方不存在")
)
// RecipeService 定义了配方相关的应用服务接口
type RecipeService interface {
CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error)
UpdateRecipe(ctx context.Context, id uint32, req *dto.UpdateRecipeRequest) (*dto.RecipeResponse, error)
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)
}
// recipeServiceImpl 是 RecipeService 接口的实现
type recipeServiceImpl struct {
ctx context.Context
recipeSvc recipe.Service
}
// NewRecipeService 创建一个新的 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")
recipeModel := dto.ConvertCreateRecipeRequestToModel(req)
createdRecipe, err := s.recipeSvc.CreateRecipe(serviceCtx, recipeModel)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return nil, ErrRecipeNameConflict
}
return nil, fmt.Errorf("创建配方失败: %w", err)
}
// 创建成功后,获取包含完整信息的配方
fullRecipe, err := s.recipeSvc.GetRecipeByID(serviceCtx, createdRecipe.ID)
if err != nil {
// 理论上不应该发生,因为刚创建成功
return nil, fmt.Errorf("创建后获取配方信息失败: %w", err)
}
return dto.ConvertRecipeToDto(fullRecipe), nil
}
// UpdateRecipe 更新配方
func (s *recipeServiceImpl) UpdateRecipe(ctx context.Context, id uint32, req *dto.UpdateRecipeRequest) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRecipe")
// 1. 转换 DTO 为模型
recipeModel := dto.ConvertUpdateRecipeRequestToModel(req)
recipeModel.ID = id
// 2. 更新配方基础信息
_, err := s.recipeSvc.UpdateRecipe(serviceCtx, recipeModel)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
if errors.Is(err, recipe.ErrRecipeNameConflict) {
return nil, ErrRecipeNameConflict
}
return nil, fmt.Errorf("更新配方基础信息失败: %w", err)
}
// 3. 更新配方原料
ingredients := make([]models.RecipeIngredient, len(req.RecipeIngredients))
for i, item := range req.RecipeIngredients {
ingredients[i] = models.RecipeIngredient{
RecipeID: id,
RawMaterialID: item.RawMaterialID,
Percentage: item.Percentage,
}
}
err = s.recipeSvc.UpdateRecipeIngredients(serviceCtx, id, ingredients)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("更新配方原料失败: %w", err)
}
// 4. 更新成功后,获取最新的完整配方信息并返回
updatedRecipe, err := s.recipeSvc.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
// 理论上不应该发生,因为刚更新成功
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("更新后获取配方信息失败: %w", err)
}
return dto.ConvertRecipeToDto(updatedRecipe), nil
}
// DeleteRecipe 删除配方
func (s *recipeServiceImpl) DeleteRecipe(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteRecipe")
err := s.recipeSvc.DeleteRecipe(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return ErrRecipeNotFound
}
return fmt.Errorf("删除配方失败: %w", err)
}
return nil
}
// GetRecipeByID 获取单个配方
func (s *recipeServiceImpl) GetRecipeByID(ctx context.Context, id uint32) (*dto.RecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRecipeByID")
recipeModel, err := s.recipeSvc.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, recipe.ErrRecipeNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("获取配方失败: %w", err)
}
return dto.ConvertRecipeToDto(recipeModel), nil
}
// ListRecipes 列出配方
func (s *recipeServiceImpl) ListRecipes(ctx context.Context, req *dto.ListRecipeRequest) (*dto.ListRecipeResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRecipes")
opts := repository.RecipeListOptions{
Name: req.Name,
OrderBy: req.OrderBy,
}
recipes, total, err := s.recipeSvc.ListRecipes(serviceCtx, opts, req.Page, req.PageSize)
if err != nil {
return nil, fmt.Errorf("获取配方列表失败: %w", err)
}
return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil
}

View File

@@ -14,6 +14,7 @@ import (
// Application 是整个应用的核心,封装了所有组件和生命周期。
type Application struct {
cfgPath string
Config *config.Config
Ctx context.Context
API *api.API
@@ -62,12 +63,20 @@ func NewApplication(configPath string) (*Application, error) {
appServices.userService,
appServices.auditService,
appServices.thresholdAlarmService,
appServices.nutrientService,
appServices.rawMaterialService,
appServices.pigBreedService,
appServices.pigAgeStageService,
appServices.pigTypeService,
appServices.recipeService,
appServices.inventoryService,
infra.tokenGenerator,
infra.lora.listenHandler,
)
// 4. 组装 Application 对象
app := &Application{
cfgPath: configPath,
Config: cfg,
Ctx: selfCtx,
API: apiServer,
@@ -90,7 +99,7 @@ func (app *Application) Start() error {
}
// 2. 初始化应用状态 (清理、刷新任务等)
if err := app.initializeState(startCtx); err != nil {
if err := app.initializeState(startCtx, app.cfgPath); err != nil {
return fmt.Errorf("初始化应用状态失败: %w", err)
}

View File

@@ -9,9 +9,11 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/app/webhook"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/alarm"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/device"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/inventory"
domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/pig"
"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"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/config"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
@@ -24,6 +26,7 @@ import (
"git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
// Infrastructure 聚合了所有基础设施层的组件。
@@ -79,9 +82,12 @@ type Repositories struct {
pigTradeRepo repository.PigTradeRepository
pigSickPigLogRepo repository.PigSickLogRepository
medicationLogRepo repository.MedicationLogRepository
rawMaterialRepo repository.RawMaterialRepository
notificationRepo repository.NotificationRepository
alarmRepo repository.AlarmRepository
pigTypeRepo repository.PigTypeRepository
rawMaterialRepo repository.RawMaterialRepository
nutrientRepo repository.NutrientRepository
recipeRepo repository.RecipeRepository
unitOfWork repository.UnitOfWork
}
@@ -108,9 +114,12 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories {
pigTradeRepo: repository.NewGormPigTradeRepository(logs.AddCompName(baseCtx, "PigTradeRepo"), db),
pigSickPigLogRepo: repository.NewGormPigSickLogRepository(logs.AddCompName(baseCtx, "PigSickPigLogRepo"), db),
medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
notificationRepo: repository.NewGormNotificationRepository(logs.AddCompName(baseCtx, "NotificationRepo"), db),
alarmRepo: repository.NewGormAlarmRepository(logs.AddCompName(baseCtx, "AlarmRepo"), db),
pigTypeRepo: repository.NewGormPigTypeRepository(logs.AddCompName(baseCtx, "PigTypeRepo"), db),
rawMaterialRepo: repository.NewGormRawMaterialRepository(logs.AddCompName(baseCtx, "RawMaterialRepo"), db),
nutrientRepo: repository.NewGormNutrientRepository(logs.AddCompName(baseCtx, "NutrientRepo"), db),
recipeRepo: repository.NewGormRecipeRepository(logs.AddCompName(baseCtx, "RecipeRepo"), db),
unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db),
}
}
@@ -128,6 +137,8 @@ type DomainServices struct {
planService plan.Service
notifyService domain_notify.Service
alarmService alarm.AlarmService
recipeService recipe.Service
inventoryService inventory.InventoryCoreService
}
// initDomainServices 初始化所有的领域服务。
@@ -139,6 +150,9 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
return nil, fmt.Errorf("初始化通知服务失败: %w", err)
}
// 库存管理
inventoryService := inventory.NewInventoryCoreService(logs.AddCompName(baseCtx, "InventoryCoreService"), infra.repos.unitOfWork, infra.repos.rawMaterialRepo)
// 猪群管理相关
pigPenTransferManager := pig.NewPigPenTransferManager(logs.AddCompName(baseCtx, "PigPenTransferManager"), infra.repos.pigPenRepo, infra.repos.pigTransferLogRepo, infra.repos.pigBatchRepo)
pigTradeManager := pig.NewPigTradeManager(logs.AddCompName(baseCtx, "PigTradeManager"), infra.repos.pigTradeRepo)
@@ -207,6 +221,25 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
taskFactory,
)
// 配方管理相关
nutrientService := recipe.NewNutrientService(logs.AddCompName(baseCtx, "NutrientService"), infra.repos.nutrientRepo)
pigAgeStageService := recipe.NewPigAgeStageService(logs.AddCompName(baseCtx, "PigAgeStageService"), infra.repos.pigTypeRepo)
pigBreedService := recipe.NewPigBreedService(logs.AddCompName(baseCtx, "PigBreedService"), infra.repos.pigTypeRepo)
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,
rawMaterialService,
pigBreedService,
pigAgeStageService,
pigTypeService,
recipeCoreService,
recipeGenerateManager,
)
return &DomainServices{
pigPenTransferManager: pigPenTransferManager,
pigTradeManager: pigTradeManager,
@@ -219,6 +252,8 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
planService: planService,
notifyService: notifyService,
alarmService: alarmService,
recipeService: recipeService,
inventoryService: inventoryService,
}, nil
}
@@ -232,6 +267,13 @@ type AppServices struct {
userService service.UserService
auditService service.AuditService
thresholdAlarmService service.ThresholdAlarmService
nutrientService service.NutrientService
pigAgeStageService service.PigAgeStageService
pigBreedService service.PigBreedService
pigTypeService service.PigTypeService
rawMaterialService service.RawMaterialService
recipeService service.RecipeService
inventoryService service.InventoryService
}
// initAppServices 初始化所有的应用服务。
@@ -247,7 +289,6 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
infra.repos.planRepo,
infra.repos.pendingCollectionRepo,
infra.repos.userActionLogRepo,
infra.repos.rawMaterialRepo,
infra.repos.medicationLogRepo,
infra.repos.pigBatchRepo,
infra.repos.pigBatchLogRepo,
@@ -280,6 +321,13 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
auditService := service.NewAuditService(logs.AddCompName(baseCtx, "AuditService"), infra.repos.userActionLogRepo)
planService := service.NewPlanService(logs.AddCompName(baseCtx, "AppPlanService"), domainServices.planService)
userService := service.NewUserService(logs.AddCompName(baseCtx, "UserService"), infra.repos.userRepo, infra.tokenGenerator, domainServices.notifyService)
nutrientService := service.NewNutrientService(logs.AddCompName(baseCtx, "NutrientService"), domainServices.recipeService)
pigAgeStageService := service.NewPigAgeStageService(logs.AddCompName(baseCtx, "PigAgeStageService"), domainServices.recipeService)
pigBreedService := service.NewPigBreedService(logs.AddCompName(baseCtx, "PigBreedService"), domainServices.recipeService)
pigTypeService := service.NewPigTypeService(logs.AddCompName(baseCtx, "PigTypeService"), domainServices.recipeService)
rawMaterialService := service.NewRawMaterialService(logs.AddCompName(baseCtx, "RawMaterialService"), domainServices.recipeService)
recipeService := service.NewRecipeService(logs.AddCompName(baseCtx, "RecipeService"), domainServices.recipeService)
inventoryService := service.NewInventoryService(logs.AddCompName(baseCtx, "InventoryService"), domainServices.inventoryService, infra.repos.rawMaterialRepo)
return &AppServices{
pigFarmService: pigFarmService,
@@ -290,6 +338,13 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
planService: planService,
userService: userService,
thresholdAlarmService: thresholdAlarmService,
nutrientService: nutrientService,
pigAgeStageService: pigAgeStageService,
pigBreedService: pigBreedService,
pigTypeService: pigTypeService,
rawMaterialService: rawMaterialService,
recipeService: recipeService,
inventoryService: inventoryService,
}
}
@@ -424,6 +479,8 @@ func initNotifyService(
// initStorage 封装了数据库的初始化、连接和迁移逻辑。
func initStorage(ctx context.Context, cfg config.DatabaseConfig) (database.Storage, error) {
logger := logs.GetLogger(ctx)
// 创建存储实例
storage := database.NewStorage(logs.AddCompName(context.Background(), "Storage"), cfg)
if err := storage.Connect(ctx); err != nil {
@@ -431,8 +488,20 @@ func initStorage(ctx context.Context, cfg config.DatabaseConfig) (database.Stora
return nil, fmt.Errorf("数据库连接失败: %w", err)
}
// 获取所有模型
allModels := models.GetAllModels()
// -- 启动时检查:确保所有模型都实现了 schema.Tabler 接口 --
// 这是一个硬性要求,用于保证代码质量和表名定义的明确性。
// 如果一个模型没有实现 TableName() string 方法,程序将在此处 panic。
for _, model := range allModels {
if _, ok := model.(schema.Tabler); !ok {
logger.Panicf(fmt.Sprintf("启动失败:模型 %T 未实现 schema.Tabler 接口。请为该模型添加 TableName() string 方法,以显式指定其数据库表名。", model))
}
}
// 执行数据库迁移
if err := storage.Migrate(ctx, models.GetAllModels()...); err != nil {
if err := storage.Migrate(ctx, allModels...); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}

View File

@@ -3,8 +3,10 @@ package core
import (
"context"
"fmt"
"path/filepath"
"git.huangwc.com/pig/pig-farm-controller/internal/domain/task"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/database"
"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"
@@ -12,27 +14,35 @@ import (
// initializeState 在应用启动时准备其初始数据状态。
// 它遵循一个严格的顺序:清理 -> 更新 -> 刷新,以确保数据的一致性和正确性。
func (app *Application) initializeState(ctx context.Context) error {
func (app *Application) initializeState(ctx context.Context, cfgPath string) error {
appCtx, logger := logs.Trace(ctx, app.Ctx, "InitializeState")
// 1. 清理所有上次运行时遗留的待执行任务和相关日志。
// 1. 播种预设数据
logger.Info("开始播种预设数据...")
presetDir := filepath.Join(filepath.Dir(cfgPath), "presets-data")
if err := database.SeedFromPreset(appCtx, app.Infra.storage.GetDB(appCtx), presetDir); err != nil {
return fmt.Errorf("预设数据播种失败: %w", err)
}
logger.Info("预设数据播种成功。")
// 2. 清理所有上次运行时遗留的待执行任务和相关日志。
// 这一步必须在任何可能修改计划结构的操作之前执行,以避免外键约束冲突。
if err := app.cleanupStaleTasksAndLogs(appCtx); err != nil {
return fmt.Errorf("清理过期的任务及日志失败: %w", err)
}
// 2. 清理待采集任务 (非致命错误)。
// 3. 清理待采集任务 (非致命错误)。
if err := app.initializePendingCollections(appCtx); err != nil {
logger.Errorw("清理待采集任务时发生非致命错误", "error", err)
}
// 3. 初始化并更新系统计划。
// 4. 初始化并更新系统计划。
// 此时,所有旧的待执行任务已被清除,可以安全地更新计划结构。
if err := app.initializeSystemPlans(ctx); err != nil {
return fmt.Errorf("初始化预定义系统计划失败: %w", err)
}
// 4. 最后,根据最新的计划状态,统一刷新所有计划的触发器。
// 5. 最后,根据最新的计划状态,统一刷新所有计划的触发器。
// 这一步确保了新创建或更新的系统计划能够被正确地调度。
logger.Info("正在刷新所有计划的触发器...")
if err := app.Domain.planService.RefreshPlanTriggers(appCtx); err != 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

@@ -0,0 +1,170 @@
package inventory
import (
"context"
"errors"
"fmt"
"sync"
"time"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrRawMaterialNotFound = errors.New("原料不存在")
ErrInsufficientStock = errors.New("原料库存不足")
)
// InventoryCoreService 定义了库存领域的核心业务服务接口
type InventoryCoreService interface {
// AdjustStock 调整指定原料的库存
AdjustStock(ctx context.Context, rawMaterialID uint32, changeAmount float32, sourceType models.StockLogSourceType, sourceID *uint32, remarks string) (*models.RawMaterialStockLog, error)
// GetCurrentStock 获取单个原料的当前库存量
GetCurrentStock(ctx context.Context, rawMaterialID uint32) (float32, error)
// BatchGetCurrentStock 批量获取多个原料的当前库存量
BatchGetCurrentStock(ctx context.Context, rawMaterialIDs []uint32) (map[uint32]float32, error)
// ListStockLogs 分页查询库存变动日志
ListStockLogs(ctx context.Context, opts repository.StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
}
// inventoryCoreServiceImpl 是 InventoryCoreService 的实现
type inventoryCoreServiceImpl struct {
ctx context.Context
uow repository.UnitOfWork
rawMatRepo repository.RawMaterialRepository
// 全局库存调整锁,确保所有 AdjustStock 操作串行执行
adjustStockMutex sync.Mutex
}
// NewInventoryCoreService 创建一个新的 InventoryCoreService 实例
func NewInventoryCoreService(ctx context.Context, uow repository.UnitOfWork, rawMatRepo repository.RawMaterialRepository) InventoryCoreService {
return &inventoryCoreServiceImpl{
ctx: ctx,
uow: uow,
rawMatRepo: rawMatRepo,
}
}
// AdjustStock 调整指定原料的库存
func (s *inventoryCoreServiceImpl) AdjustStock(ctx context.Context, rawMaterialID uint32, changeAmount float32, sourceType models.StockLogSourceType, sourceID *uint32, remarks string) (*models.RawMaterialStockLog, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "AdjustStock")
// 使用全局锁确保所有库存调整操作串行执行
s.adjustStockMutex.Lock()
defer s.adjustStockMutex.Unlock()
var createdLog *models.RawMaterialStockLog
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 在事务中创建 RawMaterialRepository 的新实例
txRawMatRepo := repository.NewGormRawMaterialRepository(serviceCtx, tx)
// 1. 检查原料是否存在
_, err := txRawMatRepo.GetRawMaterialByID(serviceCtx, rawMaterialID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRawMaterialNotFound
}
return fmt.Errorf("检查原料是否存在时出错: %w", err)
}
// 2. 获取当前库存 (在程序锁的保护下,这里是安全的)
latestLog, err := txRawMatRepo.GetLatestRawMaterialStockLog(serviceCtx, rawMaterialID)
if err != nil {
return fmt.Errorf("获取最新库存日志失败: %w", err)
}
var beforeQuantity float32 = 0
if latestLog != nil {
beforeQuantity = latestLog.AfterQuantity
}
// 3. 计算新库存并检查是否充足
afterQuantity := beforeQuantity + changeAmount
if afterQuantity < 0 {
return ErrInsufficientStock
}
// 4. 创建新的库存日志
newLog := &models.RawMaterialStockLog{
RawMaterialID: rawMaterialID,
ChangeAmount: changeAmount,
BeforeQuantity: beforeQuantity,
AfterQuantity: afterQuantity,
SourceType: sourceType,
SourceID: sourceID,
HappenedAt: time.Now(),
Remarks: remarks,
}
if err := txRawMatRepo.CreateRawMaterialStockLog(serviceCtx, newLog); err != nil {
return fmt.Errorf("创建库存日志失败: %w", err)
}
createdLog = newLog
return nil
})
if err != nil {
return nil, err // 直接返回事务中发生的错误
}
return createdLog, nil
}
// GetCurrentStock 获取单个原料的当前库存量
func (s *inventoryCoreServiceImpl) GetCurrentStock(ctx context.Context, rawMaterialID uint32) (float32, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetCurrentStock")
latestLog, err := s.rawMatRepo.GetLatestRawMaterialStockLog(serviceCtx, rawMaterialID)
if err != nil {
return 0, fmt.Errorf("获取最新库存日志失败: %w", err)
}
if latestLog == nil {
// 如果没有日志说明从未入库库存为0
return 0, nil
}
return latestLog.AfterQuantity, nil
}
// BatchGetCurrentStock 批量获取多个原料的当前库存量
func (s *inventoryCoreServiceImpl) BatchGetCurrentStock(ctx context.Context, rawMaterialIDs []uint32) (map[uint32]float32, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "BatchGetCurrentStock")
logMap, err := s.rawMatRepo.BatchGetLatestStockLogsForMaterials(serviceCtx, rawMaterialIDs)
if err != nil {
return nil, fmt.Errorf("批量获取最新库存日志失败: %w", err)
}
stockMap := make(map[uint32]float32, len(rawMaterialIDs))
for _, id := range rawMaterialIDs {
if log, ok := logMap[id]; ok {
stockMap[id] = log.AfterQuantity
} else {
// 如果某个原料在 logMap 中不存在说明它没有任何库存记录库存为0
stockMap[id] = 0
}
}
return stockMap, nil
}
// ListStockLogs 分页查询库存变动日志
func (s *inventoryCoreServiceImpl) ListStockLogs(ctx context.Context, opts repository.StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListStockLogs")
logs, total, err := s.rawMatRepo.ListStockLogs(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取库存日志列表失败: %w", err)
}
return logs, total, nil
}

View File

@@ -0,0 +1,146 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在")
ErrNutrientNotFound = fmt.Errorf("营养种类不存在")
)
// NutrientService 定义了营养种类领域的核心业务服务接口
type NutrientService interface {
CreateNutrient(ctx context.Context, name, description string) (*models.Nutrient, error)
UpdateNutrient(ctx context.Context, id uint32, name, description string) (*models.Nutrient, error)
DeleteNutrient(ctx context.Context, id uint32) error
GetNutrient(ctx context.Context, id uint32) (*models.Nutrient, error)
ListNutrients(ctx context.Context, opts repository.NutrientListOptions, page, pageSize int) ([]models.Nutrient, int64, error)
}
// nutrientServiceImpl 是 NutrientService 的实现
type nutrientServiceImpl struct {
ctx context.Context
nutrientRepo repository.NutrientRepository
}
// NewNutrientService 创建一个新的 NutrientService 实例
func NewNutrientService(ctx context.Context, nutrientRepo repository.NutrientRepository) NutrientService {
return &nutrientServiceImpl{
ctx: ctx,
nutrientRepo: nutrientRepo,
}
}
// CreateNutrient 实现了创建营养种类的核心业务逻辑
func (s *nutrientServiceImpl) CreateNutrient(ctx context.Context, name, description string) (*models.Nutrient, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateNutrient")
// 检查名称是否已存在
existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // 只有不是记录未找到的错误才返回
return nil, fmt.Errorf("检查营养种类名称失败: %w", err)
}
if existing != nil {
return nil, ErrNutrientNameConflict
}
nutrient := &models.Nutrient{
Name: name,
Description: description,
}
if err := s.nutrientRepo.CreateNutrient(serviceCtx, nutrient); err != nil {
return nil, fmt.Errorf("创建营养种类失败: %w", err)
}
return nutrient, nil
}
// UpdateNutrient 实现了更新营养种类的核心业务逻辑
func (s *nutrientServiceImpl) UpdateNutrient(ctx context.Context, id uint32, name, description string) (*models.Nutrient, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateNutrient")
// 检查要更新的实体是否存在
nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { // 如果是记录未找到错误,则返回领域错误
return nil, ErrNutrientNotFound
}
return nil, fmt.Errorf("获取待更新的营养种类失败: %w", err)
}
// 如果名称有变动,检查新名称是否与其它记录冲突
if nutrient.Name != name {
existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("检查新的营养种类名称失败: %w", err)
}
if existing != nil && existing.ID != id {
return nil, ErrNutrientNameConflict
}
}
nutrient.Name = name
nutrient.Description = description
if err := s.nutrientRepo.UpdateNutrient(serviceCtx, nutrient); err != nil {
return nil, fmt.Errorf("更新营养种类失败: %w", err)
}
return nutrient, nil
}
// DeleteNutrient 实现了删除营养种类的核心业务逻辑
func (s *nutrientServiceImpl) DeleteNutrient(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteNutrient")
// 检查实体是否存在
_, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrNutrientNotFound
}
return fmt.Errorf("获取待删除的营养种类失败: %w", err)
}
if err := s.nutrientRepo.DeleteNutrient(serviceCtx, id); err != nil {
return fmt.Errorf("删除营养种类失败: %w", err)
}
return nil
}
// GetNutrient 实现了获取单个营养种类的逻辑
func (s *nutrientServiceImpl) GetNutrient(ctx context.Context, id uint32) (*models.Nutrient, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetNutrient")
nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNutrientNotFound
}
return nil, fmt.Errorf("获取营养种类失败: %w", err)
}
return nutrient, nil
}
// ListNutrients 实现了列出营养种类的逻辑
func (s *nutrientServiceImpl) ListNutrients(ctx context.Context, opts repository.NutrientListOptions, page, pageSize int) ([]models.Nutrient, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients")
nutrients, total, err := s.nutrientRepo.ListNutrients(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取营养种类列表失败: %w", err)
}
return nutrients, total, nil
}

View File

@@ -0,0 +1,113 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrPigAgeStageInUse = fmt.Errorf("猪年龄阶段正在被猪类型使用,无法删除")
ErrPigAgeStageNotFound = fmt.Errorf("猪年龄阶段不存在")
)
// PigAgeStageService 定义了猪年龄阶段领域的核心业务服务接口
type PigAgeStageService interface {
CreatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error
GetPigAgeStageByID(ctx context.Context, id uint32) (*models.PigAgeStage, error)
UpdatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error
DeletePigAgeStage(ctx context.Context, id uint32) error
ListPigAgeStages(ctx context.Context, opts repository.PigAgeStageListOptions, page, pageSize int) ([]models.PigAgeStage, int64, error)
}
// pigAgeStageServiceImpl 是 PigAgeStageService 的实现
type pigAgeStageServiceImpl struct {
ctx context.Context
pigTypeRepo repository.PigTypeRepository // PigAgeStage 相关的操作目前在 PigTypeRepository 中
}
// NewPigAgeStageService 创建一个新的 PigAgeStageService 实例
func NewPigAgeStageService(ctx context.Context, pigTypeRepo repository.PigTypeRepository) PigAgeStageService {
return &pigAgeStageServiceImpl{
ctx: ctx,
pigTypeRepo: pigTypeRepo,
}
}
// CreatePigAgeStage 实现了创建猪年龄阶段的核心业务逻辑
func (s *pigAgeStageServiceImpl) CreatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigAgeStage")
if err := s.pigTypeRepo.CreatePigAgeStage(serviceCtx, ageStage); err != nil {
return fmt.Errorf("创建猪年龄阶段失败: %w", err)
}
return nil
}
// GetPigAgeStageByID 实现了获取单个猪年龄阶段的逻辑
func (s *pigAgeStageServiceImpl) GetPigAgeStageByID(ctx context.Context, id uint32) (*models.PigAgeStage, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigAgeStageByID")
ageStage, err := s.pigTypeRepo.GetPigAgeStageByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPigAgeStageNotFound
}
return nil, fmt.Errorf("获取猪年龄阶段失败: %w", err)
}
return ageStage, nil
}
// UpdatePigAgeStage 实现了更新猪年龄阶段的核心业务逻辑
func (s *pigAgeStageServiceImpl) UpdatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigAgeStage")
if err := s.pigTypeRepo.UpdatePigAgeStage(serviceCtx, ageStage); err != nil {
return fmt.Errorf("更新猪年龄阶段失败: %w", err)
}
return nil
}
// DeletePigAgeStage 实现了删除猪年龄阶段的核心业务逻辑
func (s *pigAgeStageServiceImpl) DeletePigAgeStage(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigAgeStage")
// 检查是否有猪类型关联到该年龄阶段
opts := repository.PigTypeListOptions{AgeStageID: &id}
pigTypes, _, err := s.pigTypeRepo.ListPigTypes(serviceCtx, opts, 1, 1) // 只需检查是否存在所以取1条
if err != nil {
return fmt.Errorf("检查猪年龄阶段关联失败: %w", err)
}
if len(pigTypes) > 0 {
return ErrPigAgeStageInUse
}
// 检查实体是否存在
_, err = s.pigTypeRepo.GetPigAgeStageByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigAgeStageNotFound
}
return fmt.Errorf("获取待删除的猪年龄阶段失败: %w", err)
}
if err := s.pigTypeRepo.DeletePigAgeStage(serviceCtx, id); err != nil {
return fmt.Errorf("删除猪年龄阶段失败: %w", err)
}
return nil
}
// ListPigAgeStages 实现了列出猪年龄阶段的逻辑
func (s *pigAgeStageServiceImpl) ListPigAgeStages(ctx context.Context, opts repository.PigAgeStageListOptions, page, pageSize int) ([]models.PigAgeStage, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigAgeStages")
ageStages, total, err := s.pigTypeRepo.ListPigAgeStages(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取猪年龄阶段列表失败: %w", err)
}
return ageStages, total, nil
}

View File

@@ -0,0 +1,112 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrPigBreedInUse = fmt.Errorf("猪品种正在被猪类型使用,无法删除")
ErrPigBreedNotFound = fmt.Errorf("猪品种不存在")
)
// PigBreedService 定义了猪品种领域的核心业务服务接口
type PigBreedService interface {
CreatePigBreed(ctx context.Context, breed *models.PigBreed) error
GetPigBreedByID(ctx context.Context, id uint32) (*models.PigBreed, error)
UpdatePigBreed(ctx context.Context, breed *models.PigBreed) error
DeletePigBreed(ctx context.Context, id uint32) error
ListPigBreeds(ctx context.Context, opts repository.PigBreedListOptions, page, pageSize int) ([]models.PigBreed, int64, error)
}
// pigBreedServiceImpl 是 PigBreedService 的实现
type pigBreedServiceImpl struct {
ctx context.Context
pigTypeRepo repository.PigTypeRepository // PigBreed 相关的操作目前在 PigTypeRepository 中
}
// NewPigBreedService 创建一个新的 PigBreedService 实例
func NewPigBreedService(ctx context.Context, pigTypeRepo repository.PigTypeRepository) PigBreedService {
return &pigBreedServiceImpl{
ctx: ctx,
pigTypeRepo: pigTypeRepo,
}
}
// CreatePigBreed 实现了创建猪品种的核心业务逻辑
func (s *pigBreedServiceImpl) CreatePigBreed(ctx context.Context, breed *models.PigBreed) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBreed")
if err := s.pigTypeRepo.CreatePigBreed(serviceCtx, breed); err != nil {
return fmt.Errorf("创建猪品种失败: %w", err)
}
return nil
}
// GetPigBreedByID 实现了获取单个猪品种的逻辑
func (s *pigBreedServiceImpl) GetPigBreedByID(ctx context.Context, id uint32) (*models.PigBreed, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigBreedByID")
breed, err := s.pigTypeRepo.GetPigBreedByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPigBreedNotFound
}
return nil, fmt.Errorf("获取猪品种失败: %w", err)
}
return breed, nil
}
// UpdatePigBreed 实现了更新猪品种的核心业务逻辑
func (s *pigBreedServiceImpl) UpdatePigBreed(ctx context.Context, breed *models.PigBreed) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigBreed")
if err := s.pigTypeRepo.UpdatePigBreed(serviceCtx, breed); err != nil {
return fmt.Errorf("更新猪品种失败: %w", err)
}
return nil
}
// DeletePigBreed 实现了删除猪品种的核心业务逻辑
func (s *pigBreedServiceImpl) DeletePigBreed(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigBreed")
// 检查是否有猪类型关联到该品种
opts := repository.PigTypeListOptions{BreedID: &id}
pigTypes, _, err := s.pigTypeRepo.ListPigTypes(serviceCtx, opts, 1, 1) // 只需检查是否存在所以取1条
if err != nil {
return fmt.Errorf("检查猪品种关联失败: %w", err)
}
if len(pigTypes) > 0 {
return ErrPigBreedInUse
}
// 检查实体是否存在
_, err = s.pigTypeRepo.GetPigBreedByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigBreedNotFound
}
return fmt.Errorf("获取待删除的猪品种失败: %w", err)
}
if err := s.pigTypeRepo.DeletePigBreed(serviceCtx, id); err != nil {
return fmt.Errorf("删除猪品种失败: %w", err)
}
return nil
}
// ListPigBreeds 实现了列出猪品种的逻辑
func (s *pigBreedServiceImpl) ListPigBreeds(ctx context.Context, opts repository.PigBreedListOptions, page, pageSize int) ([]models.PigBreed, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigBreeds")
breeds, total, err := s.pigTypeRepo.ListPigBreeds(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取猪品种列表失败: %w", err)
}
return breeds, total, nil
}

View File

@@ -0,0 +1,139 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrPigTypeNotFound = fmt.Errorf("猪类型不存在")
)
// PigTypeService 定义了猪类型领域的核心业务服务接口
type PigTypeService interface {
CreatePigType(ctx context.Context, pigType *models.PigType) error
GetPigTypeByID(ctx context.Context, id uint32) (*models.PigType, error)
UpdatePigType(ctx context.Context, pigType *models.PigType) error
DeletePigType(ctx context.Context, id uint32) error
ListPigTypes(ctx context.Context, opts repository.PigTypeListOptions, page, pageSize int) ([]models.PigType, int64, error)
UpdatePigTypeNutrientRequirements(ctx context.Context, pigTypeID uint32, requirements []models.PigNutrientRequirement) error
}
// pigTypeServiceImpl 是 PigTypeService 的实现
type pigTypeServiceImpl struct {
ctx context.Context
uow repository.UnitOfWork
pigTypeRepo repository.PigTypeRepository
}
// NewPigTypeService 创建一个新的 PigTypeService 实例
func NewPigTypeService(ctx context.Context, uow repository.UnitOfWork, pigTypeRepo repository.PigTypeRepository) PigTypeService {
return &pigTypeServiceImpl{
ctx: ctx,
uow: uow,
pigTypeRepo: pigTypeRepo,
}
}
// CreatePigType 实现了创建猪类型的核心业务逻辑
func (s *pigTypeServiceImpl) CreatePigType(ctx context.Context, pigType *models.PigType) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigType")
if err := s.pigTypeRepo.CreatePigType(serviceCtx, pigType); err != nil {
return fmt.Errorf("创建猪类型失败: %w", err)
}
return nil
}
// GetPigTypeByID 实现了获取单个猪类型的逻辑
func (s *pigTypeServiceImpl) GetPigTypeByID(ctx context.Context, id uint32) (*models.PigType, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetPigTypeByID")
pigType, err := s.pigTypeRepo.GetPigTypeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPigTypeNotFound
}
return nil, fmt.Errorf("获取猪类型失败: %w", err)
}
return pigType, nil
}
// UpdatePigType 实现了更新猪类型的核心业务逻辑
func (s *pigTypeServiceImpl) UpdatePigType(ctx context.Context, pigType *models.PigType) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigType")
if err := s.pigTypeRepo.UpdatePigType(serviceCtx, pigType); err != nil {
return fmt.Errorf("更新猪类型失败: %m", err)
}
return nil
}
// DeletePigType 实现了删除猪类型的核心业务逻辑
func (s *pigTypeServiceImpl) DeletePigType(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeletePigType")
// 检查实体是否存在
_, err := s.pigTypeRepo.GetPigTypeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigTypeNotFound
}
return fmt.Errorf("获取待删除的猪类型失败: %w", err)
}
if err := s.pigTypeRepo.DeletePigType(serviceCtx, id); err != nil {
return fmt.Errorf("删除猪类型失败: %w", err)
}
return nil
}
// ListPigTypes 实现了列出猪类型的逻辑
func (s *pigTypeServiceImpl) ListPigTypes(ctx context.Context, opts repository.PigTypeListOptions, page, pageSize int) ([]models.PigType, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListPigTypes")
pigTypes, total, err := s.pigTypeRepo.ListPigTypes(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取猪类型列表失败: %w", err)
}
return pigTypes, total, nil
}
// UpdatePigTypeNutrientRequirements 实现了全量更新猪类型营养需求的核心业务逻辑
func (s *pigTypeServiceImpl) UpdatePigTypeNutrientRequirements(ctx context.Context, pigTypeID uint32, requirements []models.PigNutrientRequirement) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdatePigTypeNutrientRequirements")
// 1. 检查猪类型是否存在
if _, err := s.pigTypeRepo.GetPigTypeByID(serviceCtx, pigTypeID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPigTypeNotFound
}
return fmt.Errorf("获取待更新营养需求的猪类型失败: %w", err)
}
// 2. 在事务中执行替换操作
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 2.1. 删除旧的关联记录
if err := s.pigTypeRepo.DeletePigNutrientRequirementsByPigTypeIDTx(serviceCtx, tx, pigTypeID); err != nil {
return err // 错误已在仓库层封装,直接返回
}
// 2.2. 创建新的关联记录
if err := s.pigTypeRepo.CreateBatchPigNutrientRequirementsTx(serviceCtx, tx, requirements); err != nil {
return err // 错误已在仓库层封装,直接返回
}
return nil
})
if err != nil {
return fmt.Errorf("更新猪类型营养需求事务执行失败: %w", err)
}
// 3. 操作成功,直接返回 nil
return nil
}

View File

@@ -0,0 +1,220 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// StockQuerier 定义了从外部领域查询库存的接口。
// 这样,配方领域就不需要知道库存是如何存储或计算的。
type StockQuerier interface {
// GetCurrentStock 根据原料ID获取当前库存量。
GetCurrentStock(ctx context.Context, rawMaterialID uint32) (float32, error)
}
// 定义领域特定的错误
var (
ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在")
ErrRawMaterialNotFound = fmt.Errorf("原料不存在")
ErrStockNotEmpty = fmt.Errorf("原料尚有库存,无法删除")
ErrRawMaterialInUseByRecipe = fmt.Errorf("原料已被配方使用,无法删除")
)
// RawMaterialService 定义了原料领域的核心业务服务接口
type RawMaterialService interface {
CreateRawMaterial(ctx context.Context, name, description string, referencePrice, 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)
UpdateRawMaterialNutrients(ctx context.Context, rawMaterialID uint32, nutrients []models.RawMaterialNutrient) error
}
// rawMaterialServiceImpl 是 RawMaterialService 的实现
type rawMaterialServiceImpl struct {
ctx context.Context
uow repository.UnitOfWork
rawMaterialRepo repository.RawMaterialRepository
stockQuerier StockQuerier
}
// NewRawMaterialService 创建一个新的 RawMaterialService 实例
func NewRawMaterialService(ctx context.Context, uow repository.UnitOfWork, rawMaterialRepo repository.RawMaterialRepository, stockQuerier StockQuerier) RawMaterialService {
return &rawMaterialServiceImpl{
ctx: ctx,
uow: uow,
rawMaterialRepo: rawMaterialRepo,
stockQuerier: stockQuerier,
}
}
// CreateRawMaterial 实现了创建原料的核心业务逻辑
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice, maxAdditionRatio float32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial")
// 检查名称是否已存在
existing, err := s.rawMaterialRepo.GetRawMaterialByName(serviceCtx, name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("检查原料名称失败: %w", err)
}
if existing != nil {
return nil, ErrRawMaterialNameConflict
}
rawMaterial := &models.RawMaterial{
Name: name,
Description: description,
ReferencePrice: referencePrice,
MaxAdditionRatio: maxAdditionRatio,
}
if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil {
return nil, fmt.Errorf("创建原料失败: %w", err)
}
return rawMaterial, nil
}
// UpdateRawMaterial 实现了更新原料的核心业务逻辑
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")
// 检查要更新的实体是否存在
rawMaterial, err := s.rawMaterialRepo.GetRawMaterialByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRawMaterialNotFound
}
return nil, fmt.Errorf("获取待更新的原料失败: %w", err)
}
// 如果名称有变动,检查新名称是否与其它记录冲突
if rawMaterial.Name != name {
existing, err := s.rawMaterialRepo.GetRawMaterialByName(serviceCtx, name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("检查新的原料名称失败: %w", err)
}
if existing != nil && existing.ID != id {
return nil, ErrRawMaterialNameConflict
}
}
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)
}
return rawMaterial, nil
}
// DeleteRawMaterial 实现了删除原料的核心业务逻辑
func (s *rawMaterialServiceImpl) DeleteRawMaterial(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteRawMaterial")
// 检查实体是否存在
_, err := s.rawMaterialRepo.GetRawMaterialByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRawMaterialNotFound
}
return fmt.Errorf("获取待删除的原料失败: %w", err)
}
// 检查原料是否有库存
stock, err := s.stockQuerier.GetCurrentStock(serviceCtx, id)
if err != nil {
return fmt.Errorf("检查原料库存失败: %w", err)
}
if stock > 0 {
// 如果库存大于0返回业务错误阻止删除
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)
}
return nil
}
// GetRawMaterial 实现了获取单个原料的逻辑
func (s *rawMaterialServiceImpl) GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRawMaterial")
rawMaterial, err := s.rawMaterialRepo.GetRawMaterialByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRawMaterialNotFound
}
return nil, fmt.Errorf("获取原料失败: %w", err)
}
return rawMaterial, nil
}
// ListRawMaterials 实现了列出原料的逻辑
func (s *rawMaterialServiceImpl) ListRawMaterials(ctx context.Context, opts repository.RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials")
rawMaterials, total, err := s.rawMaterialRepo.ListRawMaterials(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取原料列表失败: %w", err)
}
return rawMaterials, total, nil
}
// UpdateRawMaterialNutrients 实现了全量更新原料营养成分的业务逻辑
func (s *rawMaterialServiceImpl) UpdateRawMaterialNutrients(ctx context.Context, rawMaterialID uint32, nutrients []models.RawMaterialNutrient) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterialNutrients")
// 1. 检查原料是否存在
if _, err := s.rawMaterialRepo.GetRawMaterialByID(serviceCtx, rawMaterialID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRawMaterialNotFound
}
return fmt.Errorf("获取待更新的原料失败: %w", err)
}
// 2. 在事务中执行替换操作
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 2.1. 删除旧的关联记录
if err := s.rawMaterialRepo.DeleteNutrientsByRawMaterialIDTx(serviceCtx, tx, rawMaterialID); err != nil {
return err // 错误已在仓库层封装,直接返回
}
// 2.2. 创建新的关联记录
if err := s.rawMaterialRepo.CreateBatchRawMaterialNutrientsTx(serviceCtx, tx, nutrients); err != nil {
return err // 错误已在仓库层封装,直接返回
}
return nil
})
if err != nil {
return fmt.Errorf("更新原料营养成分事务执行失败: %w", err)
}
// 3. 操作成功,直接返回 nil
return nil
}

View File

@@ -0,0 +1,189 @@
package recipe
import (
"context"
"errors"
"fmt"
"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"
"gorm.io/gorm"
)
// 定义领域特定的错误
var (
ErrRecipeNotFound = fmt.Errorf("配方不存在")
ErrRecipeNameConflict = fmt.Errorf("配方名称已存在")
)
// RecipeCoreService 定义了配方领域的核心业务服务接口
type RecipeCoreService interface {
CreateRecipe(ctx context.Context, recipe *models.Recipe) (*models.Recipe, error)
GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error)
GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error)
UpdateRecipe(ctx context.Context, recipe *models.Recipe) (*models.Recipe, error)
DeleteRecipe(ctx context.Context, id uint32) error
ListRecipes(ctx context.Context, opts repository.RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error)
UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error
}
// recipeCoreServiceImpl 是 RecipeCoreService 的实现
type recipeCoreServiceImpl struct {
ctx context.Context
uow repository.UnitOfWork
recipeRepo repository.RecipeRepository
}
// NewRecipeCoreService 创建一个新的 RecipeCoreService 实例
func NewRecipeCoreService(ctx context.Context, uow repository.UnitOfWork, recipeRepo repository.RecipeRepository) RecipeCoreService {
return &recipeCoreServiceImpl{
ctx: ctx,
uow: uow,
recipeRepo: recipeRepo,
}
}
// CreateRecipe 实现了创建配方的核心业务逻辑
func (s *recipeCoreServiceImpl) CreateRecipe(ctx context.Context, recipe *models.Recipe) (*models.Recipe, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe")
// 检查名称是否已存在
existing, err := s.recipeRepo.GetRecipeByName(serviceCtx, recipe.Name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("检查配方名称失败: %w", err)
}
if existing != nil {
return nil, ErrRecipeNameConflict
}
if err := s.recipeRepo.CreateRecipe(serviceCtx, recipe); err != nil {
return nil, fmt.Errorf("创建配方失败: %w", err)
}
return recipe, nil
}
// GetRecipeByID 实现了获取单个配方的逻辑
func (s *recipeCoreServiceImpl) GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRecipeByID")
recipe, err := s.recipeRepo.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("获取配方失败: %w", err)
}
return recipe, nil
}
// GetRecipeByName 实现了根据名称获取单个配方的逻辑
func (s *recipeCoreServiceImpl) GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "GetRecipeByName")
recipe, err := s.recipeRepo.GetRecipeByName(serviceCtx, name)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("获取配方失败: %w", err)
}
return recipe, nil
}
// UpdateRecipe 实现了更新配方的核心业务逻辑
func (s *recipeCoreServiceImpl) UpdateRecipe(ctx context.Context, recipe *models.Recipe) (*models.Recipe, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRecipe")
// 检查要更新的实体是否存在
existingRecipe, err := s.recipeRepo.GetRecipeByID(serviceCtx, recipe.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRecipeNotFound
}
return nil, fmt.Errorf("获取待更新的配方失败: %w", err)
}
// 如果名称有变动,检查新名称是否与其它记录冲突
if existingRecipe.Name != recipe.Name {
existing, err := s.recipeRepo.GetRecipeByName(serviceCtx, recipe.Name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("检查新的配方名称失败: %w", err)
}
if existing != nil && existing.ID != recipe.ID {
return nil, ErrRecipeNameConflict
}
}
if err := s.recipeRepo.UpdateRecipe(serviceCtx, recipe); err != nil {
return nil, fmt.Errorf("更新配方失败: %w", err)
}
return recipe, nil
}
// DeleteRecipe 实现了删除配方的核心业务逻辑
func (s *recipeCoreServiceImpl) DeleteRecipe(ctx context.Context, id uint32) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteRecipe")
// 检查实体是否存在
_, err := s.recipeRepo.GetRecipeByID(serviceCtx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRecipeNotFound
}
return fmt.Errorf("获取待删除的配方失败: %w", err)
}
if err := s.recipeRepo.DeleteRecipe(serviceCtx, id); err != nil {
return fmt.Errorf("删除配方失败: %w", err)
}
return nil
}
// ListRecipes 实现了列出配方的逻辑
func (s *recipeCoreServiceImpl) ListRecipes(ctx context.Context, opts repository.RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRecipes")
recipes, total, err := s.recipeRepo.ListRecipes(serviceCtx, opts, page, pageSize)
if err != nil {
return nil, 0, fmt.Errorf("获取配方列表失败: %w", err)
}
return recipes, total, nil
}
// UpdateRecipeIngredients 实现了全量更新配方原料的业务逻辑
func (s *recipeCoreServiceImpl) UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRecipeIngredients")
// 1. 检查配方是否存在
if _, err := s.recipeRepo.GetRecipeByID(serviceCtx, recipeID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrRecipeNotFound
}
return fmt.Errorf("获取待更新原料的配方失败: %w", err)
}
// 2. 在事务中执行替换操作
err := s.uow.ExecuteInTransaction(serviceCtx, func(tx *gorm.DB) error {
// 2.1. 删除旧的关联记录
if err := s.recipeRepo.DeleteRecipeIngredientsByRecipeIDTx(serviceCtx, tx, recipeID); err != nil {
return err // 错误已在仓库层封装,直接返回
}
// 2.2. 创建新的关联记录
// 确保每个原料都设置了正确的 RecipeID
for i := range ingredients {
ingredients[i].RecipeID = recipeID
}
if err := s.recipeRepo.CreateBatchRecipeIngredientsTx(serviceCtx, tx, ingredients); err != nil {
return err // 错误已在仓库层封装,直接返回
}
return nil
})
if err != nil {
return fmt.Errorf("更新配方原料事务执行失败: %w", err)
}
// 3. 操作成功,直接返回 nil
return nil
}

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

@@ -0,0 +1,227 @@
package recipe
import (
"context"
"fmt"
"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 定义了配方与原料领域的核心业务服务接口
// 该接口聚合了所有子领域的服务接口
type Service interface {
NutrientService
RawMaterialService
PigBreedService
PigAgeStageService
PigTypeService
RecipeCoreService
RecipeGenerateManager
GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
// GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料
GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error)
}
// recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现
type recipeServiceImpl struct {
ctx context.Context
NutrientService
RawMaterialService
PigBreedService
PigAgeStageService
PigTypeService
RecipeCoreService
RecipeGenerateManager
}
// NewRecipeService 创建一个新的 Service 实例
func NewRecipeService(
ctx context.Context,
nutrientService NutrientService,
rawMaterialService RawMaterialService,
pigBreedService PigBreedService,
pigAgeStageService PigAgeStageService,
pigTypeService PigTypeService,
recipeCoreService RecipeCoreService,
recipeGenerateManager RecipeGenerateManager,
) Service {
return &recipeServiceImpl{
ctx: ctx,
NutrientService: nutrientService,
RawMaterialService: rawMaterialService,
PigBreedService: pigBreedService,
PigAgeStageService: pigAgeStageService,
PigTypeService: pigTypeService,
RecipeCoreService: recipeCoreService,
RecipeGenerateManager: recipeGenerateManager,
}
}
// 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
}

View File

@@ -6,6 +6,7 @@ package database
import (
"context"
"fmt"
"strings"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -167,9 +168,6 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
{models.TaskExecutionLog{}, "created_at"},
{models.PendingCollection{}, "created_at"},
{models.UserActionLog{}, "time"},
{models.RawMaterialPurchase{}, "purchase_date"},
{models.RawMaterialStockLog{}, "happened_at"},
{models.FeedUsageRecord{}, "recorded_at"},
{models.MedicationLog{}, "happened_at"},
{models.PigBatchLog{}, "happened_at"},
{models.WeighingBatch{}, "weighing_time"},
@@ -180,6 +178,7 @@ func (ps *PostgresStorage) creatingHyperTable(ctx context.Context) error {
{models.PigSale{}, "sale_date"},
{models.Notification{}, "alarm_timestamp"},
{models.HistoricalAlarm{}, "trigger_time"},
{models.RawMaterialStockLog{}, "happened_at"},
}
for _, table := range tablesToConvert {
@@ -210,9 +209,6 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
{models.TaskExecutionLog{}, "task_id"},
{models.PendingCollection{}, "device_id"},
{models.UserActionLog{}, "user_id"},
{models.RawMaterialPurchase{}, "raw_material_id"},
{models.RawMaterialStockLog{}, "raw_material_id"},
{models.FeedUsageRecord{}, "pen_id"},
{models.MedicationLog{}, "pig_batch_id"},
{models.PigBatchLog{}, "pig_batch_id"},
{models.WeighingBatch{}, "pig_batch_id"},
@@ -223,6 +219,7 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
{models.PigSale{}, "pig_batch_id"},
{models.Notification{}, "user_id"},
{models.HistoricalAlarm{}, "source_id"},
{models.RawMaterialStockLog{}, "raw_material_id"},
}
for _, policy := range policies {
@@ -254,27 +251,198 @@ func (ps *PostgresStorage) applyCompressionPolicies(ctx context.Context) error {
// creatingIndex 用于创建gorm无法处理的索引, 如gin索引
func (ps *PostgresStorage) creatingIndex(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingIndex")
storageCtx := logs.AddFuncName(ctx, ps.ctx, "creatingIndex")
// 使用 IF NOT EXISTS 保证幂等性
// 如果索引已存在,此命令不会报错
// 为 sensor_data 表的 data 字段创建 GIN 索引
logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引")
ginSensorDataIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);"
if err := ps.db.WithContext(storageCtx).Exec(ginSensorDataIndexSQL).Error; err != nil {
logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err)
return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err)
if err := ps.creatingUniqueIndex(storageCtx); err != nil {
return err
}
logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
// 为 tasks.parameters 创建 GIN 索引
logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引")
taskGinIndexSQL := "CREATE INDEX IF NOT EXISTS idx_tasks_parameters_gin ON tasks USING GIN (parameters);"
if err := ps.db.WithContext(storageCtx).Exec(taskGinIndexSQL).Error; err != nil {
logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err)
return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err)
if err := ps.createGinIndexes(storageCtx); err != nil {
return err
}
return nil
}
// uniqueIndexDefinition 结构体定义了唯一索引的详细信息
type uniqueIndexDefinition struct {
tableName string // 索引所属的表名
columns []string // 构成唯一索引的列名
indexName string // 唯一索引的名称
whereClause string // 可选的 WHERE 子句,用于创建部分索引
description string // 索引的描述,用于日志记录
}
// ginIndexDefinition 结构体定义了 GIN 索引的详细信息
type ginIndexDefinition struct {
tableName string // 索引所属的表名
columnName string // 需要创建 GIN 索引的列名
indexName string // GIN 索引的名称
description string // 索引的描述,用于日志记录
}
func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex")
// 定义所有需要创建的唯一索引
uniqueIndexesToCreate := []uniqueIndexDefinition{
{
tableName: models.RawMaterialNutrient{}.TableName(),
columns: []string{"raw_material_id", "nutrient_id"},
indexName: "idx_raw_material_nutrients_unique_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "确保同一原料中的每种营养成分不重复",
},
{
tableName: models.PigBreed{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_breeds_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_breeds 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigAgeStage{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_age_stages_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_age_stages 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigType{}.TableName(),
columns: []string{"breed_id", "age_stage_id"},
indexName: "idx_pig_types_unique_breed_age_stage_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_types 表的部分唯一索引 (breed_id, age_stage_id 组合唯一)",
},
{
tableName: models.PigNutrientRequirement{}.TableName(),
columns: []string{"pig_type_id", "nutrient_id"},
indexName: "idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_nutrient_requirements 表的部分唯一索引 (pig_type_id, nutrient_id 组合唯一)",
},
{
tableName: models.User{}.TableName(),
columns: []string{"username"},
indexName: "idx_users_unique_username_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "users 表的部分唯一索引 (username 唯一)",
},
{
tableName: models.AreaController{}.TableName(),
columns: []string{"name"},
indexName: "idx_area_controllers_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "area_controllers 表的部分唯一索引 (Name 唯一)",
},
{
tableName: models.AreaController{}.TableName(),
columns: []string{"network_id"},
indexName: "idx_area_controllers_unique_network_id_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "area_controllers 表的部分唯一索引 (NetworkID 唯一)",
},
{
tableName: models.DeviceTemplate{}.TableName(),
columns: []string{"name"},
indexName: "idx_device_templates_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "device_templates 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigBatch{}.TableName(),
columns: []string{"batch_number"},
indexName: "idx_pig_batches_unique_batch_number_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_batches 表的部分唯一索引 (batch_number 唯一)",
},
{
tableName: models.PigHouse{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_houses_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_houses 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.RawMaterial{}.TableName(),
columns: []string{"name"},
indexName: "idx_raw_materials_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "raw_materials 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.Nutrient{}.TableName(),
columns: []string{"name"},
indexName: "idx_nutrients_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "nutrients 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.Recipe{}.TableName(),
columns: []string{"name"},
indexName: "idx_recipes_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "recipes 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.RecipeIngredient{}.TableName(),
columns: []string{"recipe_id", "raw_material_id"},
indexName: "idx_recipe_ingredients_unique_recipe_raw_material_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "recipe_ingredients 表的部分唯一索引 (recipe_id, raw_material_id 组合唯一)",
},
}
for _, indexDef := range uniqueIndexesToCreate {
logger.Debugw("正在为表创建部分唯一索引", "表名", indexDef.tableName, "索引名", indexDef.indexName, "描述", indexDef.description)
// 拼接列名字符串
columnsStr := strings.Join(indexDef.columns, ", ")
// 构建 SQL 语句
sql := fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s) %s;",
indexDef.indexName, indexDef.tableName, columnsStr, indexDef.whereClause)
if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil {
logger.Errorw("创建部分唯一索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err)
return fmt.Errorf("为 %s 表创建部分唯一索引 %s 失败: %w", indexDef.tableName, indexDef.indexName, err)
}
logger.Debugw("成功为表创建部分唯一索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName)
}
return nil
}
func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes")
// 定义所有需要创建的 GIN 索引
ginIndexesToCreate := []ginIndexDefinition{
{
tableName: "sensor_data",
columnName: "data",
indexName: "idx_sensor_data_data_gin",
description: "为 sensor_data 表的 data 字段创建 GIN 索引",
},
{
tableName: "tasks",
columnName: "parameters",
indexName: "idx_tasks_parameters_gin",
description: "为 tasks 表的 parameters 字段创建 GIN 索引",
},
}
for _, indexDef := range ginIndexesToCreate {
logger.Debugw("正在创建 GIN 索引", "表名", indexDef.tableName, "列名", indexDef.columnName, "描述", indexDef.description)
// 构建 SQL 语句
sql := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s USING GIN (%s);",
indexDef.indexName, indexDef.tableName, indexDef.columnName)
if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil {
logger.Errorw("创建 GIN 索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err)
return fmt.Errorf("为 %s 表的 %s 字段创建 GIN 索引 %s 失败: %w", indexDef.tableName, indexDef.columnName, indexDef.indexName, err)
}
logger.Debugw("成功创建 GIN 索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName)
}
logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
return nil
}

View File

@@ -0,0 +1,126 @@
package database
import (
"context"
"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"
"github.com/tidwall/gjson"
"gorm.io/gorm"
)
// SeederFunc 定义了处理一种特定类型预设数据文件的函数签名。
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 {
seedCtx, logger := logs.Trace(ctx, ctx, "SeedFromPreset")
// 定义必须存在的预设数据类型及其处理顺序
// 确保 "nutrient" 在 "pig_nutrient_requirements" 之前处理,因为后者依赖于前者。
processingOrder := []string{"nutrient", "pig_nutrient_requirements"}
requiredTypes := make(map[string]bool)
for _, t := range processingOrder {
requiredTypes[t] = true
}
processedTypes := make(map[string]bool)
typeToFileMap := make(map[string]string) // 用于检测重复的 type并存储每个 type 对应的文件路径
groupedFiles := make(map[string][][]byte) // 按 type 分组存储 jsonData
files, err := os.ReadDir(presetDir)
if err != nil {
return fmt.Errorf("读取预设数据目录 '%s' 失败: %w", presetDir, err)
}
// 第一阶段:读取所有文件并按 type 分组
for _, file := range files {
if filepath.Ext(file.Name()) != ".json" {
continue
}
filePath := filepath.Join(presetDir, file.Name())
jsonData, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("读取文件 '%s' 失败: %w", filePath, err)
}
dataType := gjson.GetBytes(jsonData, "type")
if !dataType.Exists() {
logger.Warnf("警告: 文件 '%s' 中缺少 'type' 字段,已跳过", filePath)
continue
}
dataTypeStr := dataType.String()
// 检查是否存在重复的 type
if existingFile, found := typeToFileMap[dataTypeStr]; found {
return fmt.Errorf("预设数据校验失败: type '%s' 在文件 '%s' 和 '%s' 中重复定义", dataTypeStr, existingFile, filePath)
}
typeToFileMap[dataTypeStr] = filePath // 记录该 type 对应的文件路径
groupedFiles[dataTypeStr] = append(groupedFiles[dataTypeStr], jsonData)
}
// 第二阶段:按照预定义顺序处理分组后的数据
return db.Transaction(func(tx *gorm.DB) error {
for _, dataTypeStr := range processingOrder {
jsonDatas, ok := groupedFiles[dataTypeStr]
if !ok {
// 如果是必需类型但没有找到文件,则报错
if requiredTypes[dataTypeStr] {
return fmt.Errorf("预设数据校验失败: 缺少必需的预设文件类型: '%s'", dataTypeStr)
}
continue // 非必需类型,跳过
}
var seederFunc SeederFunc
switch dataTypeStr {
case "nutrient":
seederFunc = seeder.SeedNutrients
case "pig_nutrient_requirements":
seederFunc = seeder.SeedPigNutrientRequirements
default:
logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr)
continue
}
for _, jsonData := range jsonDatas {
// 获取原始文件路径用于错误报告
originalFilePath := typeToFileMap[dataTypeStr]
if err := seederFunc(seedCtx, tx, jsonData); err != nil {
return fmt.Errorf("处理文件 (type: %s, path: %s) 时发生错误: %w", dataTypeStr, originalFilePath, err)
}
}
processedTypes[dataTypeStr] = true
}
// 校验所有必需的类型是否都已处理
var missingTypes []string
for reqType := range requiredTypes {
if !processedTypes[reqType] {
missingTypes = append(missingTypes, reqType)
}
}
if len(missingTypes) > 0 {
return fmt.Errorf("预设数据校验失败: 缺少必需的预设文件类型: [%s]", strings.Join(missingTypes, ", "))
}
return nil // 提交事务
})
}

View File

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

View File

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

View File

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

View File

@@ -21,11 +21,11 @@ type AreaController struct {
Model
// Name 是主控的业务名称,例如 "1号猪舍主控"
Name string `gorm:"not null;unique" json:"name"`
Name string `gorm:"not null" json:"name"`
// NetworkID 是主控在通信网络中的唯一标识,例如 LoRaWAN 的 DevEUI。
// 这是 transport 层用来寻址的关键。
NetworkID string `gorm:"not null;unique;index" json:"network_id"`
NetworkID string `gorm:"not null;index" json:"network_id"`
// Location 描述了主控的物理安装位置。
Location string `gorm:"index" json:"location"`

View File

@@ -108,7 +108,7 @@ type DeviceTemplate struct {
Model
// Name 是此模板的唯一名称, 例如 "FanModel-XYZ-2000" 或 "TempSensor-T1"
Name string `gorm:"not null;unique" json:"name"`
Name string `gorm:"not null" json:"name"`
// Manufacturer 是设备的制造商。
Manufacturer string `json:"manufacturer"`

View File

@@ -7,11 +7,15 @@ package models
// PigHouse 定义了猪舍,是猪栏的集合
type PigHouse struct {
Model
Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"`
Name string `gorm:"size:100;not null;comment:猪舍名称, 如 '育肥舍A栋'"`
Description string `gorm:"size:255;comment:描述信息"`
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
}
func (ph PigHouse) TableName() string {
return "pig_houses"
}
// PenStatus 定义了猪栏的当前状态
type PenStatus string
@@ -33,3 +37,7 @@ type Pen struct {
Capacity int `gorm:"not null;comment:设计容量 (头)"`
Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
}
func (p Pen) TableName() string {
return "pens"
}

View File

@@ -1,111 +0,0 @@
package models
import (
"time"
)
/*
饲料和饲喂相关的模型
*/
// RawMaterial 代表饲料的原料。
// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。
type RawMaterial struct {
Model
Name string `gorm:"size:100;unique;not null;comment:原料名称"`
Description string `gorm:"size:255;comment:描述"`
Quantity float32 `gorm:"not null;comment:库存总量, 单位: g"`
}
func (RawMaterial) TableName() string {
return "raw_materials"
}
// RawMaterialPurchase 记录了原料的每一次采购。
type RawMaterialPurchase struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Supplier string `gorm:"size:100;comment:供应商"`
Amount float32 `gorm:"not null;comment:采购数量, 单位: g"`
UnitPrice float32 `gorm:"comment:单价"`
TotalPrice float32 `gorm:"comment:总价"`
PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"`
CreatedAt time.Time
}
func (RawMaterialPurchase) TableName() string {
return "raw_material_purchases"
}
// StockLogSourceType 定义了库存日志来源的类型
type StockLogSourceType string
const (
StockLogSourcePurchase StockLogSourceType = "采购入库"
StockLogSourceFeeding StockLogSourceType = "饲喂出库"
StockLogSourceDeteriorate StockLogSourceType = "变质出库"
StockLogSourceSale StockLogSourceType = "售卖出库"
StockLogSourceMiscellaneous StockLogSourceType = "杂用领取"
StockLogSourceManual StockLogSourceType = "手动盘点"
)
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
type RawMaterialStockLog struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"`
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
SourceID uint32 `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"`
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注, 如主动领取的理由等"`
}
func (RawMaterialStockLog) TableName() string {
return "raw_material_stock_logs"
}
// FeedFormula 代表饲料配方。
// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。
type FeedFormula struct {
Model
Name string `gorm:"size:100;unique;not null;comment:配方名称"`
Description string `gorm:"size:255;comment:描述"`
Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"`
}
func (FeedFormula) TableName() string {
return "feed_formulas"
}
// FeedFormulaComponent 代表配方中的一种原料及其占比。
type FeedFormulaComponent struct {
Model
FeedFormulaID uint32 `gorm:"not null;index;comment:外键到 FeedFormula"`
RawMaterialID uint32 `gorm:"not null;index;comment:外键到 RawMaterial"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
Percentage float32 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"`
}
func (FeedFormulaComponent) TableName() string {
return "feed_formula_components"
}
// FeedUsageRecord 代表饲料使用记录。
// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula,
// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。
type FeedUsageRecord struct {
Model
PenID uint32 `gorm:"not null;index;comment:关联的猪栏ID"`
Pen Pen `gorm:"foreignKey:PenID"`
FeedFormulaID uint32 `gorm:"not null;index;comment:使用的饲料配方ID"`
FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"`
Amount float32 `gorm:"not null;comment:使用数量, 单位: g"`
RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"`
OperatorID uint32 `gorm:"not null;comment:操作员"`
Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"`
}
func (FeedUsageRecord) TableName() string {
return "feed_usage_records"
}

View File

@@ -56,6 +56,10 @@ func GetAllModels() []interface{} {
&WeighingRecord{},
&PigTransferLog{},
&PigSickLog{},
&PigBreed{},
&PigAgeStage{},
&PigType{},
&PigNutrientRequirement{},
// Pig Buy & Sell
&PigPurchase{},
@@ -63,11 +67,11 @@ func GetAllModels() []interface{} {
// Feed Models
&RawMaterial{},
&RawMaterialPurchase{},
&Nutrient{},
&RawMaterialNutrient{},
&RawMaterialStockLog{},
&FeedFormula{},
&FeedFormulaComponent{},
&FeedUsageRecord{},
&Recipe{},
&RecipeIngredient{},
// Medication Models
&Medication{},
@@ -121,7 +125,7 @@ func (a *UintArray) Scan(src interface{}) error {
case string:
srcStr = v
default:
return errors.New("无法扫描非字符串或字节类型的源到 UintArray")
return errors.New("无法将值 %v (类型 %T) 扫描为 UintArray")
}
// 去掉花括号

View File

@@ -0,0 +1,48 @@
package models
// PigBreed 猪品种模型
type PigBreed struct {
Model
Name string `gorm:"size:50;not null;comment:品种名称"`
Description string `gorm:"type:text" json:"description"` // 其他描述
ParentInfo string `gorm:"type:text" json:"parent_info"` // 父母信息
AppearanceFeatures string `gorm:"type:text" json:"appearance_features"` // 外貌特征
BreedAdvantages string `gorm:"type:text" json:"breed_advantages"` // 品种优点
BreedDisadvantages string `gorm:"type:text" json:"breed_disadvantages"` // 品种缺点
}
func (PigBreed) TableName() string {
return "pig_breeds"
}
// PigAgeStage 猪年龄阶段模型
type PigAgeStage struct {
Model
Name string `gorm:"size:50;not null;comment:年龄阶段名称 (如: 仔猪, 生长猪, 育肥猪)"`
Description string `gorm:"size:255;comment:阶段描述"`
}
func (PigAgeStage) TableName() string {
return "pig_age_stages"
}
// PigType 猪类型模型,代表特定品种和年龄阶段的组合
type PigType struct {
Model
BreedID uint32 `gorm:"not null;index;comment:关联的猪品种ID"`
Breed PigBreed `gorm:"foreignKey:BreedID"`
AgeStageID uint32 `gorm:"not null;index;comment:关联的猪年龄阶段ID"`
AgeStage PigAgeStage `gorm:"foreignKey:AgeStageID"`
Description string `gorm:"size:255;comment:该猪类型的描述或特点"`
DailyFeedIntake float32 `gorm:"comment:理论日均食量 (g/天)"`
DailyGainWeight float32 `gorm:"comment:理论日增重 (g/天)"`
MinDays uint32 `gorm:"comment:该猪类型在该年龄阶段的最小日龄"`
MaxDays uint32 `gorm:"comment:该猪类型在该年龄阶段的最大日龄"`
MinWeight float32 `gorm:"comment:该猪类型在该年龄阶段的最小体重 (g)"`
MaxWeight float32 `gorm:"comment:该猪类型在该年龄阶段的最大体重 (g)"`
PigNutrientRequirements []PigNutrientRequirement `gorm:"foreignKey:PigTypeID"`
}
func (PigType) TableName() string {
return "pig_types"
}

View File

@@ -12,9 +12,7 @@ import (
type PigBatchStatus string
const (
BatchStatusWeaning PigBatchStatus = "保育" // 从断奶到保育结束
BatchStatusGrowing PigBatchStatus = "生长" // 生长育肥阶段
BatchStatusFinishing PigBatchStatus = "育肥" // 最后的育肥阶段
BatchStatusActive PigBatchStatus = "生产中" // 饲养中
BatchStatusForSale PigBatchStatus = "待售" // 达到出栏标准
BatchStatusSold PigBatchStatus = "已出售"
BatchStatusArchived PigBatchStatus = "已归档" // 批次结束(如全群淘汰等)
@@ -31,7 +29,7 @@ const (
// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪
type PigBatch struct {
Model
BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"`
BatchNumber string `gorm:"size:50;not null;comment:批次编号,如 2024-W25-A01"`
OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"`
StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"`
EndDate time.Time `gorm:"not null;comment:批次结束日期 (全部淘汰或售出)"`

View File

@@ -0,0 +1,16 @@
package models
// PigNutrientRequirement 猪营养需求模型
type PigNutrientRequirement struct {
Model
PigTypeID uint32 `gorm:"not null;index;comment:关联的猪类型ID"`
PigType PigType `gorm:"foreignKey:PigTypeID"`
NutrientID uint32 `gorm:"not null;index;comment:关联的营养素ID"`
Nutrient Nutrient `gorm:"foreignKey:NutrientID"`
MinRequirement float32 `gorm:"not null;comment:最低营养需求量"`
MaxRequirement float32 `gorm:"not null;comment:最高营养需求量"`
}
func (PigNutrientRequirement) TableName() string {
return "pig_nutrient_requirements"
}

View File

@@ -0,0 +1,88 @@
package models
import (
"time"
)
// StockLogSourceType 定义了库存日志来源的类型
type StockLogSourceType string
const (
StockLogSourcePurchase StockLogSourceType = "采购入库"
StockLogSourceFeeding StockLogSourceType = "饲喂出库"
StockLogSourceDeteriorate StockLogSourceType = "变质出库"
StockLogSourceSale StockLogSourceType = "售卖出库"
StockLogSourceMiscellaneous StockLogSourceType = "杂用领取"
StockLogSourceManual StockLogSourceType = "手动盘点"
StockLogSourceFermentStart StockLogSourceType = "发酵出库" // 原料投入发酵,从库存中扣除
StockLogSourceFermentEnd StockLogSourceType = "发酵入库" // 发酵料产出,作为新原料计入库存
)
// RawMaterial 代表一种原料的静态定义,是系统中的原料字典。
type RawMaterial struct {
Model
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"`
}
func (RawMaterial) TableName() string {
return "raw_materials"
}
// Nutrient 代表一种营养素的静态定义,是系统中的营养素字典。
// 注意本系统强制统一营养单位不再单独设置Unit字段。
// 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。
type Nutrient struct {
Model
Name string `gorm:"size:100;not null;comment:营养素名称"`
Description string `gorm:"size:255;comment:描述"`
// RawMaterialNutrients 记录营养在哪些原料中存在且比例是多少
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:NutrientID"`
}
func (Nutrient) TableName() string {
return "nutrients"
}
// RawMaterialNutrient 存储了特定原料的特定营养素的含量值。
// 这是连接原料和营养素的“营养价值表”。
// 注意:其唯一性由 postgres.go 中的部分唯一索引保证,以兼容软删除。
type RawMaterialNutrient struct {
Model
RawMaterialID uint32 `gorm:"not null;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
NutrientID uint32 `gorm:"not null;comment:关联的营养素ID"`
Nutrient Nutrient `gorm:"foreignKey:NutrientID"`
// Value 存储营养价值含量。单位遵循 Nutrient 表中定义的系统级约定。
Value float32 `gorm:"not null;comment:营养价值含量"`
}
func (RawMaterialNutrient) TableName() string {
return "raw_material_nutrients"
}
// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。
// 它是保证数据一致性和可审计性的核心。
type RawMaterialStockLog struct {
Model
RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库, 单位: g"`
BeforeQuantity float32 `gorm:"not null;comment:变动前库存数量, 单位: g"`
AfterQuantity float32 `gorm:"not null;comment:变动后库存数量, 单位: g"`
// SourceType 告知 SourceID 关联的是哪种类型的业务单据。
SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"`
// SourceID 是一个多态外键关联到触发此次变动的业务单据ID (如采购单ID)。
// 对于无单据的业务(如手动盘点)此字段可为NULL。
SourceID *uint32 `gorm:"index;comment:来源业务单据的ID"`
HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"`
Remarks string `gorm:"comment:备注"`
}
func (RawMaterialStockLog) TableName() string {
return "raw_material_stock_logs"
}

View File

@@ -0,0 +1,51 @@
package models
// Recipe 配方模型
type Recipe struct {
Model
Name string `gorm:"size:100;not null;comment:配方名称"`
Description string `gorm:"size:255;comment:配方描述"`
// RecipeIngredients 关联此配方的所有原料组成
RecipeIngredients []RecipeIngredient `gorm:"foreignKey:RecipeID"`
}
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
RecipeID uint32 `gorm:"not null;comment:关联的配方ID"`
Recipe Recipe `gorm:"foreignKey:RecipeID"`
RawMaterialID uint32 `gorm:"not null;comment:关联的原料ID"`
RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"`
// 重量百分比
Percentage float32 `gorm:"not null;comment:原料在配方中的百分比 (0-1之间的小数, 例如0.15代表15%)"`
}
func (RecipeIngredient) TableName() string {
return "recipe_ingredients"
}

View File

@@ -44,7 +44,7 @@ type User struct {
// Username 是用户的登录名,应该是唯一的
// 修正了 gorm 标签的拼写错误 (移除了 gorm 后面的冒号)
Username string `gorm:"unique;not null" json:"username"`
Username string `gorm:"not null" json:"username"`
// Password 存储的是加密后的密码哈希,而不是明文
// json:"-" 标签确保此字段在序列化为 JSON 时被忽略,防止密码泄露

View File

@@ -0,0 +1,154 @@
package repository
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// NutrientListOptions 定义了查询营养种类列表时的筛选条件
type NutrientListOptions struct {
Name *string
RawMaterialName *string
OrderBy string
}
// NutrientRepository 定义了与营养种类相关的数据库操作接口
type NutrientRepository interface {
CreateNutrient(ctx context.Context, nutrient *models.Nutrient) error
GetNutrientByID(ctx context.Context, id uint32) (*models.Nutrient, error)
GetNutrientByName(ctx context.Context, name string) (*models.Nutrient, error)
ListNutrients(ctx context.Context, opts NutrientListOptions, page, pageSize int) ([]models.Nutrient, int64, error)
UpdateNutrient(ctx context.Context, nutrient *models.Nutrient) error
DeleteNutrient(ctx context.Context, id uint32) error
}
// gormNutrientRepository 是 NutrientRepository 的 GORM 实现
type gormNutrientRepository struct {
ctx context.Context
db *gorm.DB
}
// NewGormNutrientRepository 创建一个新的 NutrientRepository GORM 实现实例
func NewGormNutrientRepository(ctx context.Context, db *gorm.DB) NutrientRepository {
return &gormNutrientRepository{ctx: ctx, db: db}
}
// CreateNutrient 创建一个新的营养种类
func (r *gormNutrientRepository) CreateNutrient(ctx context.Context, nutrient *models.Nutrient) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateNutrient")
return r.db.WithContext(repoCtx).Create(nutrient).Error
}
// GetNutrientByID 根据ID获取单个营养种类并预加载关联的原料信息
func (r *gormNutrientRepository) GetNutrientByID(ctx context.Context, id uint32) (*models.Nutrient, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetNutrientByID")
var nutrient models.Nutrient
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.RawMaterial").First(&nutrient, id).Error; err != nil {
return nil, err
}
return &nutrient, nil
}
// GetNutrientByName 根据名称获取单个营养种类,并预加载关联的原料信息
func (r *gormNutrientRepository) GetNutrientByName(ctx context.Context, name string) (*models.Nutrient, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetNutrientByName")
var nutrient models.Nutrient
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.RawMaterial").Where("name = ?", name).First(&nutrient).Error; err != nil {
return nil, err
}
return &nutrient, nil
}
// ListNutrients 列出所有营养种类(分页),并预加载关联的原料信息
func (r *gormNutrientRepository) ListNutrients(ctx context.Context, opts NutrientListOptions, page, pageSize int) ([]models.Nutrient, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListNutrients")
var nutrients []models.Nutrient
var total int64
db := r.db.WithContext(repoCtx).Model(&models.Nutrient{})
// 应用筛选条件
if opts.Name != nil && *opts.Name != "" {
db = db.Where("name LIKE ?", "%"+*opts.Name+"%")
}
// 如果传入了原料名称,则使用子查询进行筛选
if opts.RawMaterialName != nil && *opts.RawMaterialName != "" {
subQuery := r.db.Model(&models.RawMaterialNutrient{}).
Select("nutrient_id").
Joins("JOIN raw_materials ON raw_materials.id = raw_material_nutrients.raw_material_id").
Where("raw_materials.name LIKE ?", "%"+*opts.RawMaterialName+"%")
db = db.Where("id IN (?)", subQuery)
}
// 首先计算总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 然后应用排序、分页并获取数据
if opts.OrderBy != "" {
db = db.Order(opts.OrderBy)
}
offset := (page - 1) * pageSize
if err := db.Preload("RawMaterialNutrients.RawMaterial").Offset(offset).Limit(pageSize).Find(&nutrients).Error; err != nil {
return nil, 0, err
}
return nutrients, total, nil
}
// UpdateNutrient 更新一个营养种类
func (r *gormNutrientRepository) UpdateNutrient(ctx context.Context, nutrient *models.Nutrient) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateNutrient")
// 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段
updateData := map[string]interface{}{
"name": nutrient.Name,
"description": nutrient.Description,
}
result := r.db.WithContext(repoCtx).Model(&models.Nutrient{}).Where("id = ?", nutrient.ID).Updates(updateData)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("未找到要更新的营养种类ID: %d", nutrient.ID)
}
return nil
}
// DeleteNutrient 根据ID删除一个营养种类并级联软删除关联的 RawMaterialNutrient 记录
func (r *gormNutrientRepository) DeleteNutrient(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteNutrient")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 1. 查找 Nutrient 记录,确保其存在
var nutrient models.Nutrient
if err := tx.First(&nutrient, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("未找到要删除的营养种类ID: %d", id)
}
return fmt.Errorf("查询营养种类失败: %w", err)
}
// 2. 软删除所有关联的 RawMaterialNutrient 记录
if err := tx.Where("nutrient_id = ?", id).Delete(&models.RawMaterialNutrient{}).Error; err != nil {
return fmt.Errorf("软删除关联的原料营养素记录失败: %w", err)
}
// 3. 软删除 Nutrient 记录本身
if err := tx.Delete(&nutrient).Error; err != nil {
return fmt.Errorf("软删除营养种类失败: %w", err)
}
return nil
})
}

View File

@@ -0,0 +1,310 @@
package repository
import (
"context"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// PigBreedListOptions 定义了查询猪品种记录时的可选参数
type PigBreedListOptions struct {
Name *string // 品种名称
OrderBy string // 例如 "name asc"
}
// PigAgeStageListOptions 定义了查询猪年龄阶段记录时的可选参数
type PigAgeStageListOptions struct {
Name *string // 年龄阶段名称
OrderBy string // 例如 "name asc"
}
// PigTypeListOptions 定义了查询猪类型记录时的可选参数
type PigTypeListOptions struct {
BreedID *uint32 // 关联的猪品种ID
AgeStageID *uint32 // 关联的猪年龄阶段ID
BreedName *string // 关联的猪品种名称 (用于模糊查询)
AgeStageName *string // 关联的猪年龄阶段名称 (用于模糊查询)
OrderBy string // 例如 "id desc"
}
// PigTypeRepository 定义了猪品种、猪年龄阶段和猪类型数据持久化的接口。
type PigTypeRepository interface {
// <-- 猪品种相关接口 -->
// CreatePigBreed 在数据库中创建一条猪品种记录。
CreatePigBreed(ctx context.Context, breed *models.PigBreed) error
// GetPigBreedByID 根据ID获取猪品种记录。
GetPigBreedByID(ctx context.Context, id uint32) (*models.PigBreed, error)
// UpdatePigBreed 更新猪品种记录。
UpdatePigBreed(ctx context.Context, breed *models.PigBreed) error
// DeletePigBreed 根据ID删除猪品种记录。
DeletePigBreed(ctx context.Context, id uint32) error
// ListPigBreeds 支持分页和过滤的猪品种记录列表查询。
ListPigBreeds(ctx context.Context, opts PigBreedListOptions, page, pageSize int) ([]models.PigBreed, int64, error)
// <-- 猪年龄阶段相关接口 -->
// CreatePigAgeStage 在数据库中创建一条猪年龄阶段记录。
CreatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error
// GetPigAgeStageByID 根据ID获取猪年龄阶段记录。
GetPigAgeStageByID(ctx context.Context, id uint32) (*models.PigAgeStage, error)
// UpdatePigAgeStage 更新猪年龄阶段记录。
UpdatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error
// DeletePigAgeStage 根据ID删除猪年龄阶段记录。
DeletePigAgeStage(ctx context.Context, id uint32) error
// ListPigAgeStages 支持分页和过滤的猪年龄阶段记录列表查询。
ListPigAgeStages(ctx context.Context, opts PigAgeStageListOptions, page, pageSize int) ([]models.PigAgeStage, int64, error)
// <-- 猪类型相关接口 -->
// CreatePigType 在数据库中创建一条猪类型记录。
CreatePigType(ctx context.Context, pigType *models.PigType) error
// GetPigTypeByID 根据ID获取猪类型记录。
GetPigTypeByID(ctx context.Context, id uint32) (*models.PigType, error)
// UpdatePigType 更新猪类型记录。
UpdatePigType(ctx context.Context, pigType *models.PigType) error
// DeletePigType 根据ID删除猪类型记录。
DeletePigType(ctx context.Context, id uint32) error
// ListPigTypes 支持分页和过滤的猪类型记录列表查询。
ListPigTypes(ctx context.Context, opts PigTypeListOptions, page, pageSize int) ([]models.PigType, int64, error)
// DeletePigNutrientRequirementsByPigTypeIDTx 在事务中软删除指定猪类型的所有营养需求记录。
DeletePigNutrientRequirementsByPigTypeIDTx(ctx context.Context, db *gorm.DB, pigTypeID uint32) error
// CreateBatchPigNutrientRequirementsTx 在事务中批量创建猪营养需求记录。
CreateBatchPigNutrientRequirementsTx(ctx context.Context, db *gorm.DB, requirements []models.PigNutrientRequirement) error
}
// gormPigTypeRepository 是 PigTypeRepository 接口的 GORM 实现。
type gormPigTypeRepository struct {
ctx context.Context
db *gorm.DB
}
// NewGormPigTypeRepository 创建一个新的 PigTypeRepository GORM 实现实例。
func NewGormPigTypeRepository(ctx context.Context, db *gorm.DB) PigTypeRepository {
return &gormPigTypeRepository{ctx: ctx, db: db}
}
// CreatePigBreed 实现了在数据库中创建猪品种记录的逻辑。
func (r *gormPigTypeRepository) CreatePigBreed(ctx context.Context, breed *models.PigBreed) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigBreed")
return r.db.WithContext(repoCtx).Create(breed).Error
}
// GetPigBreedByID 实现了根据ID获取猪品种记录的逻辑。
func (r *gormPigTypeRepository) GetPigBreedByID(ctx context.Context, id uint32) (*models.PigBreed, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigBreedByID")
var breed models.PigBreed
err := r.db.WithContext(repoCtx).First(&breed, id).Error
if err != nil {
return nil, err
}
return &breed, nil
}
// UpdatePigBreed 实现了更新猪品种记录的逻辑。
func (r *gormPigTypeRepository) UpdatePigBreed(ctx context.Context, breed *models.PigBreed) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePigBreed")
return r.db.WithContext(repoCtx).Save(breed).Error
}
// DeletePigBreed 实现了根据ID删除猪品种记录的逻辑。
func (r *gormPigTypeRepository) DeletePigBreed(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigBreed")
return r.db.WithContext(repoCtx).Delete(&models.PigBreed{}, id).Error
}
// ListPigBreeds 实现了分页和过滤查询猪品种记录的功能。
func (r *gormPigTypeRepository) ListPigBreeds(ctx context.Context, opts PigBreedListOptions, page, pageSize int) ([]models.PigBreed, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigBreeds")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.PigBreed
var total int64
query := r.db.WithContext(repoCtx).Model(&models.PigBreed{})
if opts.Name != nil {
query = query.Where("name LIKE ?", "%"+*opts.Name+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "id DESC" // 默认排序
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// CreatePigAgeStage 实现了在数据库中创建猪年龄阶段记录的逻辑。
func (r *gormPigTypeRepository) CreatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigAgeStage")
return r.db.WithContext(repoCtx).Create(ageStage).Error
}
// GetPigAgeStageByID 实现了根据ID获取猪年龄阶段记录的逻辑。
func (r *gormPigTypeRepository) GetPigAgeStageByID(ctx context.Context, id uint32) (*models.PigAgeStage, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigAgeStageByID")
var ageStage models.PigAgeStage
err := r.db.WithContext(repoCtx).First(&ageStage, id).Error
if err != nil {
return nil, err
}
return &ageStage, nil
}
// UpdatePigAgeStage 实现了更新猪年龄阶段记录的逻辑。
func (r *gormPigTypeRepository) UpdatePigAgeStage(ctx context.Context, ageStage *models.PigAgeStage) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePigAgeStage")
return r.db.WithContext(repoCtx).Save(ageStage).Error
}
// DeletePigAgeStage 实现了根据ID删除猪年龄阶段记录的逻辑。
func (r *gormPigTypeRepository) DeletePigAgeStage(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigAgeStage")
return r.db.WithContext(repoCtx).Delete(&models.PigAgeStage{}, id).Error
}
// ListPigAgeStages 实现了分页和过滤查询猪年龄阶段记录的功能。
func (r *gormPigTypeRepository) ListPigAgeStages(ctx context.Context, opts PigAgeStageListOptions, page, pageSize int) ([]models.PigAgeStage, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigAgeStages")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.PigAgeStage
var total int64
query := r.db.WithContext(repoCtx).Model(&models.PigAgeStage{})
if opts.Name != nil {
query = query.Where("name LIKE ?", "%"+*opts.Name+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "id DESC" // 默认排序
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// CreatePigType 实现了在数据库中创建猪类型记录的逻辑。
func (r *gormPigTypeRepository) CreatePigType(ctx context.Context, pigType *models.PigType) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreatePigType")
return r.db.WithContext(repoCtx).Create(pigType).Error
}
// GetPigTypeByID 实现了根据ID获取猪类型记录的逻辑。
func (r *gormPigTypeRepository) GetPigTypeByID(ctx context.Context, id uint32) (*models.PigType, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetPigTypeByID")
var pigType models.PigType
err := r.db.WithContext(repoCtx).Preload("Breed").Preload("AgeStage").Preload("PigNutrientRequirements.Nutrient").First(&pigType, id).Error
if err != nil {
return nil, err
}
return &pigType, nil
}
// UpdatePigType 实现了更新猪类型记录的逻辑。
func (r *gormPigTypeRepository) UpdatePigType(ctx context.Context, pigType *models.PigType) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdatePigType")
return r.db.WithContext(repoCtx).Save(pigType).Error
}
// DeletePigType 实现了根据ID删除猪类型记录的逻辑。
func (r *gormPigTypeRepository) DeletePigType(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigType")
return r.db.WithContext(repoCtx).Delete(&models.PigType{}, id).Error
}
// ListPigTypes 实现了分页和过滤查询猪类型记录的功能。
func (r *gormPigTypeRepository) ListPigTypes(ctx context.Context, opts PigTypeListOptions, page, pageSize int) ([]models.PigType, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListPigTypes")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.PigType
var total int64
query := r.db.WithContext(repoCtx).Model(&models.PigType{})
if opts.BreedID != nil {
query = query.Where("breed_id = ?", *opts.BreedID)
}
if opts.AgeStageID != nil {
query = query.Where("age_stage_id = ?", *opts.AgeStageID)
}
if opts.BreedName != nil {
query = query.Joins("left join pig_breeds on pig_types.breed_id = pig_breeds.id").
Where("pig_breeds.name LIKE ?", "%"+*opts.BreedName+"%")
}
if opts.AgeStageName != nil {
query = query.Joins("left join pig_age_stages on pig_types.age_stage_id = pig_age_stages.id").
Where("pig_age_stages.name LIKE ?", "%"+*opts.AgeStageName+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "id DESC" // 默认排序
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Preload("Breed").Preload("AgeStage").Preload("PigNutrientRequirements.Nutrient").Find(&results).Error
return results, total, err
}
// DeletePigNutrientRequirementsByPigTypeIDTx 实现了在事务中软删除指定猪类型的所有营养需求记录的逻辑。
func (r *gormPigTypeRepository) DeletePigNutrientRequirementsByPigTypeIDTx(ctx context.Context, db *gorm.DB, pigTypeID uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeletePigNutrientRequirementsByPigTypeIDTx")
tx := db.WithContext(repoCtx)
if err := tx.Where("pig_type_id = ?", pigTypeID).Delete(&models.PigNutrientRequirement{}).Error; err != nil {
return fmt.Errorf("软删除猪营养需求失败: %w", err)
}
return nil
}
// CreateBatchPigNutrientRequirementsTx 实现了在事务中批量创建猪营养需求记录的逻辑。
func (r *gormPigTypeRepository) CreateBatchPigNutrientRequirementsTx(ctx context.Context, db *gorm.DB, requirements []models.PigNutrientRequirement) error {
// 如果没有要创建的记录直接返回成功避免执行空的Create语句
if len(requirements) == 0 {
return nil
}
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchPigNutrientRequirementsTx")
tx := db.WithContext(repoCtx)
if err := tx.Create(&requirements).Error; err != nil {
return fmt.Errorf("批量创建猪营养需求失败: %w", err)
}
return nil
}

View File

@@ -2,6 +2,8 @@ package repository
import (
"context"
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -10,40 +12,45 @@ import (
"gorm.io/gorm"
)
// RawMaterialPurchaseListOptions 定义了查询原料采购记录时的可选参数
type RawMaterialPurchaseListOptions struct {
RawMaterialID *uint32
Supplier *string
StartTime *time.Time // 基于 purchase_date 字段
EndTime *time.Time // 基于 purchase_date 字段
OrderBy string // 例如 "purchase_date asc"
// RawMaterialListOptions 定义了查询原料列表时的筛选条件
type RawMaterialListOptions struct {
Name *string
NutrientName *string
MinReferencePrice *float32 // 参考价格最小值
MaxReferencePrice *float32 // 参考价格最大值
HasStock *bool
OrderBy string
}
// RawMaterialStockLogListOptions 定义了查询原料库存日志时的可选参数
type RawMaterialStockLogListOptions struct {
// StockLogListOptions 定义了查询库存日志列表时的筛选条件
type StockLogListOptions struct {
RawMaterialID *uint32
SourceType *models.StockLogSourceType
SourceID *uint32
StartTime *time.Time // 基于 happened_at 字段
EndTime *time.Time // 基于 happened_at 字段
OrderBy string // 例如 "happened_at asc"
}
// FeedUsageRecordListOptions 定义了查询饲料使用记录时的可选参数
type FeedUsageRecordListOptions struct {
PenID *uint32
FeedFormulaID *uint32
OperatorID *uint32
StartTime *time.Time // 基于 recorded_at 字段
EndTime *time.Time // 基于 recorded_at 字段
OrderBy string // 例如 "recorded_at asc"
RawMaterialName *string
SourceTypes []models.StockLogSourceType
StartTime *time.Time
EndTime *time.Time
OrderBy string
}
// RawMaterialRepository 定义了与原料相关的数据库操作接口
type RawMaterialRepository interface {
ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error)
ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error)
CreateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error
GetRawMaterialByID(ctx context.Context, id uint32) (*models.RawMaterial, error)
GetRawMaterialByName(ctx context.Context, name string) (*models.RawMaterial, error)
ListRawMaterials(ctx context.Context, opts RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error)
UpdateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error
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
GetLatestRawMaterialStockLog(ctx context.Context, rawMaterialID uint32) (*models.RawMaterialStockLog, error)
// BatchGetLatestStockLogsForMaterials 批量获取一组原料的最新库存日志
BatchGetLatestStockLogsForMaterials(ctx context.Context, materialIDs []uint32) (map[uint32]models.RawMaterialStockLog, error)
// ListStockLogs 分页列出库存变动日志
ListStockLogs(ctx context.Context, opts StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error)
}
// gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现
@@ -57,131 +64,307 @@ func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialR
return &gormRawMaterialRepository{ctx: ctx, db: db}
}
// ListRawMaterialPurchases 实现了分页和过滤查询原料采购记录的功能
func (r *gormRawMaterialRepository) ListRawMaterialPurchases(ctx context.Context, opts RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialPurchases")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
// CreateRawMaterial 创建一个新的原料
func (r *gormRawMaterialRepository) CreateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRawMaterial")
return r.db.WithContext(repoCtx).Create(rawMaterial).Error
}
var results []models.RawMaterialPurchase
// GetRawMaterialByID 根据ID获取单个原料并预加载关联的营养素信息
func (r *gormRawMaterialRepository) GetRawMaterialByID(ctx context.Context, id uint32) (*models.RawMaterial, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRawMaterialByID")
var rawMaterial models.RawMaterial
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.Nutrient").First(&rawMaterial, id).Error; err != nil {
return nil, err
}
return &rawMaterial, nil
}
// GetRawMaterialByName 根据名称获取单个原料,并预加载关联的营养素信息
func (r *gormRawMaterialRepository) GetRawMaterialByName(ctx context.Context, name string) (*models.RawMaterial, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRawMaterialByName")
var rawMaterial models.RawMaterial
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RawMaterialNutrients.Nutrient").Where("name = ?", name).First(&rawMaterial).Error; err != nil {
return nil, err
}
return &rawMaterial, nil
}
// ListRawMaterials 列出所有原料(分页),支持按名称和营养名称筛选
func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterials")
var rawMaterials []models.RawMaterial
var total int64
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialPurchase{})
db := r.db.WithContext(repoCtx).Model(&models.RawMaterial{})
// 应用筛选条件
if opts.Name != nil && *opts.Name != "" {
db = db.Where("name LIKE ?", "%"+*opts.Name+"%")
}
// 如果传入了营养名称,则使用子查询进行筛选
if opts.NutrientName != nil && *opts.NutrientName != "" {
// 子查询:从 raw_material_nutrients 和 nutrients 表中找到所有包含该营养的 raw_material_id
subQuery := r.db.Model(&models.RawMaterialNutrient{}).
Select("raw_material_id").
Joins("JOIN nutrients ON nutrients.id = raw_material_nutrients.nutrient_id").
Where("nutrients.name LIKE ?", "%"+*opts.NutrientName+"%")
db = db.Where("id IN (?)", subQuery)
}
// 筛选参考价格
if opts.MinReferencePrice != nil {
db = db.Where("reference_price >= ?", *opts.MinReferencePrice)
}
if opts.MaxReferencePrice != nil {
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
}
// 然后应用排序、分页并获取数据
if opts.OrderBy != "" {
db = db.Order(opts.OrderBy)
}
offset := (page - 1) * pageSize
if err := db.Preload("RawMaterialNutrients.Nutrient").Offset(offset).Limit(pageSize).Find(&rawMaterials).Error; err != nil {
return nil, 0, err
}
return rawMaterials, total, nil
}
// UpdateRawMaterial 更新一个原料
func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial")
// 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段
updateData := map[string]interface{}{
"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 {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("未找到要更新的原料ID: %d", rawMaterial.ID)
}
return nil
}
// DeleteRawMaterial 根据ID删除一个原料并级联软删除关联的 RawMaterialNutrient 和 RawMaterialStockLog 记录
func (r *gormRawMaterialRepository) DeleteRawMaterial(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRawMaterial")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 1. 查找 RawMaterial 记录,确保其存在
var rawMaterial models.RawMaterial
if err := tx.First(&rawMaterial, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("未找到要删除的原料ID: %d", id)
}
return fmt.Errorf("查询原料失败: %w", err)
}
// 2. 软删除所有关联的 RawMaterialNutrient 记录
if err := tx.Where("raw_material_id = ?", id).Delete(&models.RawMaterialNutrient{}).Error; err != nil {
return fmt.Errorf("软删除关联的原料营养素记录失败: %w", err)
}
// 3. 软删除所有关联的 RawMaterialStockLog 记录
if err := tx.Where("raw_material_id = ?", id).Delete(&models.RawMaterialStockLog{}).Error; err != nil {
return fmt.Errorf("软删除关联的原料库存日志记录失败: %w", err)
}
// 4. 软删除 RawMaterial 记录本身
if err := tx.Delete(&rawMaterial).Error; err != nil {
return fmt.Errorf("软删除原料失败: %w", err)
}
return nil
})
}
// DeleteNutrientsByRawMaterialIDTx 在事务中软删除指定原料的所有营养成分
func (r *gormRawMaterialRepository) DeleteNutrientsByRawMaterialIDTx(ctx context.Context, db *gorm.DB, rawMaterialID uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteNutrientsByRawMaterialIDTx")
tx := db.WithContext(repoCtx)
if err := tx.Where("raw_material_id = ?", rawMaterialID).Delete(&models.RawMaterialNutrient{}).Error; err != nil {
return fmt.Errorf("软删除原料营养成分失败: %w", err)
}
return nil
}
// CreateBatchRawMaterialNutrientsTx 在事务中批量创建原料营养成分
func (r *gormRawMaterialRepository) CreateBatchRawMaterialNutrientsTx(ctx context.Context, db *gorm.DB, nutrients []models.RawMaterialNutrient) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchRawMaterialNutrientsTx")
// 如果没有要创建的记录直接返回成功避免执行空的Create语句
if len(nutrients) == 0 {
return nil
}
// 确保每个营养都关联到正确的原料ID
// 注意:这里假设传入的 nutrients 已经设置了正确的 RawMaterialID
for i := range nutrients {
if nutrients[i].RawMaterialID == 0 {
return fmt.Errorf("创建原料营养时 RecipeID 不能为空")
}
}
if err := db.WithContext(repoCtx).Create(&nutrients).Error; err != nil {
return fmt.Errorf("批量创建原料营养成分失败: %w", err)
}
return nil
}
// CreateRawMaterialStockLog 创建一条新的原料库存日志
func (r *gormRawMaterialRepository) CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRawMaterialStockLog")
return r.db.WithContext(repoCtx).Create(log).Error
}
// GetLatestRawMaterialStockLog 获取指定原料的最新一条库存日志
func (r *gormRawMaterialRepository) GetLatestRawMaterialStockLog(ctx context.Context, rawMaterialID uint32) (*models.RawMaterialStockLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetLatestRawMaterialStockLog")
var latestLog models.RawMaterialStockLog
err := r.db.WithContext(repoCtx).
Where("raw_material_id = ?", rawMaterialID).
Order("happened_at DESC, id DESC"). // 优先按时间降序然后按ID降序确保唯一最新
First(&latestLog).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 如果没有日志记录不视为错误返回nil
}
return nil, err
}
return &latestLog, nil
}
// BatchGetLatestStockLogsForMaterials 批量获取一组原料的最新库存日志
func (r *gormRawMaterialRepository) BatchGetLatestStockLogsForMaterials(ctx context.Context, materialIDs []uint32) (map[uint32]models.RawMaterialStockLog, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "BatchGetLatestStockLogsForMaterials")
if len(materialIDs) == 0 {
return make(map[uint32]models.RawMaterialStockLog), nil
}
var latestLogs []models.RawMaterialStockLog
// 使用窗口函数 ROW_NUMBER() 来为每个原料的日志分区,并按时间倒序排名。
// 这样可以高效地一次性查询出每个原料的最新一条日志。
subQuery := r.db.Model(&models.RawMaterialStockLog{}).
Select("*, ROW_NUMBER() OVER(PARTITION BY raw_material_id ORDER BY happened_at DESC, id DESC) as rn").
Where("raw_material_id IN ?", materialIDs)
err := r.db.WithContext(repoCtx).
Table("(?) as sub", subQuery).
Where("rn = 1").
Find(&latestLogs).Error
if err != nil {
return nil, fmt.Errorf("批量获取最新库存日志失败: %w", err)
}
// 将结果转换为 map[uint32]models.RawMaterialStockLog 以方便查找
logMap := make(map[uint32]models.RawMaterialStockLog, len(latestLogs))
for _, log := range latestLogs {
logMap[log.RawMaterialID] = log
}
return logMap, nil
}
// ListStockLogs 分页列出库存变动日志
func (r *gormRawMaterialRepository) ListStockLogs(ctx context.Context, opts StockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListStockLogs")
var logs []models.RawMaterialStockLog
var total int64
db := r.db.WithContext(repoCtx).Model(&models.RawMaterialStockLog{})
// 应用筛选条件
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
db = db.Where("raw_material_id = ?", *opts.RawMaterialID)
}
if opts.Supplier != nil {
query = query.Where("supplier LIKE ?", "%"+*opts.Supplier+"%")
// 新增:按原料名称模糊搜索
if opts.RawMaterialName != nil && *opts.RawMaterialName != "" {
// 使用子查询找到匹配的原料ID
subQuery := r.db.Model(&models.RawMaterial{}).Select("id").Where("name LIKE ?", "%"+*opts.RawMaterialName+"%")
db = db.Where("raw_material_id IN (?)", subQuery)
}
if len(opts.SourceTypes) > 0 {
db = db.Where("source_type IN ?", opts.SourceTypes)
}
if opts.StartTime != nil {
query = query.Where("purchase_date >= ?", *opts.StartTime)
db = db.Where("happened_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("purchase_date <= ?", *opts.EndTime)
db = db.Where("happened_at <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
// 首先计算总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("统计库存日志总数失败: %w", err)
}
orderBy := "purchase_date DESC"
// 然后应用排序、分页并获取数据
if opts.OrderBy != "" {
orderBy = opts.OrderBy
db = db.Order(opts.OrderBy)
} else {
// 默认排序
db = db.Order("happened_at DESC, id DESC")
}
query = query.Order(orderBy).Preload("RawMaterial")
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
if err := db.Preload("RawMaterial").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
return nil, 0, fmt.Errorf("查询库存日志列表失败: %w", err)
}
return results, total, err
return logs, total, nil
}
// ListRawMaterialStockLogs 实现了分页和过滤查询原料库存日志的功能
func (r *gormRawMaterialRepository) ListRawMaterialStockLogs(ctx context.Context, opts RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterialStockLogs")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
// 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)
}
var results []models.RawMaterialStockLog
var total int64
query := r.db.WithContext(repoCtx).Model(&models.RawMaterialStockLog{})
if opts.RawMaterialID != nil {
query = query.Where("raw_material_id = ?", *opts.RawMaterialID)
}
if opts.SourceType != nil {
query = query.Where("source_type = ?", *opts.SourceType)
}
if opts.SourceID != nil {
query = query.Where("source_id = ?", *opts.SourceID)
}
if opts.StartTime != nil {
query = query.Where("happened_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("happened_at <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "happened_at DESC"
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// ListFeedUsageRecords 实现了分页和过滤查询饲料使用记录的功能
func (r *gormRawMaterialRepository) ListFeedUsageRecords(ctx context.Context, opts FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListFeedUsageRecords")
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.FeedUsageRecord
var total int64
query := r.db.WithContext(repoCtx).Model(&models.FeedUsageRecord{})
if opts.PenID != nil {
query = query.Where("pen_id = ?", *opts.PenID)
}
if opts.FeedFormulaID != nil {
query = query.Where("feed_formula_id = ?", *opts.FeedFormulaID)
}
if opts.OperatorID != nil {
query = query.Where("operator_id = ?", *opts.OperatorID)
}
if opts.StartTime != nil {
query = query.Where("recorded_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
query = query.Where("recorded_at <= ?", *opts.EndTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "recorded_at DESC"
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy).Preload("Pen").Preload("FeedFormula")
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
return count > 0, nil
}

View File

@@ -0,0 +1,190 @@
package repository
import (
"context"
"errors"
"fmt"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// RecipeListOptions 定义了查询配方列表时的筛选条件
type RecipeListOptions struct {
Name *string
RawMaterialName *string
OrderBy string
}
// RecipeRepository 定义了与配方相关的数据库操作接口
type RecipeRepository interface {
CreateRecipe(ctx context.Context, recipe *models.Recipe) error
GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error)
GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error)
ListRecipes(ctx context.Context, opts RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error)
UpdateRecipe(ctx context.Context, recipe *models.Recipe) error
DeleteRecipe(ctx context.Context, id uint32) error
// 在事务中删除配方原料
DeleteRecipeIngredientsByRecipeIDTx(ctx context.Context, tx *gorm.DB, recipeID uint32) error
// 在事务中批量创建配方原料
CreateBatchRecipeIngredientsTx(ctx context.Context, tx *gorm.DB, ingredients []models.RecipeIngredient) error
}
// gormRecipeRepository 是 RecipeRepository 的 GORM 实现
type gormRecipeRepository struct {
ctx context.Context
db *gorm.DB
}
// NewGormRecipeRepository 创建一个新的 RecipeRepository GORM 实现实例
func NewGormRecipeRepository(ctx context.Context, db *gorm.DB) RecipeRepository {
return &gormRecipeRepository{ctx: ctx, db: db}
}
// CreateRecipe 创建一个新的配方,并处理其关联的配方原料
func (r *gormRecipeRepository) CreateRecipe(ctx context.Context, recipe *models.Recipe) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRecipe")
// 注意:这里的事务只针对 Recipe 本身,如果 RecipeIngredient 也在同一个 Create 中GORM 会自动处理。
// 但如果 RecipeIngredient 是单独操作,则需要外部事务。
if err := r.db.WithContext(repoCtx).Create(recipe).Error; err != nil {
return fmt.Errorf("创建配方失败: %w", err)
}
return nil
}
// GetRecipeByID 根据ID获取单个配方并预加载其关联的配方原料和原料信息
func (r *gormRecipeRepository) GetRecipeByID(ctx context.Context, id uint32) (*models.Recipe, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRecipeByID")
var recipe models.Recipe
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").First(&recipe, id).Error; err != nil {
return nil, err
}
return &recipe, nil
}
// GetRecipeByName 根据名称获取单个配方,并预加载其关联的配方原料和原料信息
func (r *gormRecipeRepository) GetRecipeByName(ctx context.Context, name string) (*models.Recipe, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetRecipeByName")
var recipe models.Recipe
// 如果记录未找到GORM 会返回 gorm.ErrRecordNotFound 错误
if err := r.db.WithContext(repoCtx).Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").Where("name = ?", name).First(&recipe).Error; err != nil {
return nil, err
}
return &recipe, nil
}
// ListRecipes 列出所有配方(分页),并预加载其关联的配方原料和原料信息
func (r *gormRecipeRepository) ListRecipes(ctx context.Context, opts RecipeListOptions, page, pageSize int) ([]models.Recipe, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRecipes")
var recipes []models.Recipe
var total int64
db := r.db.WithContext(repoCtx).Model(&models.Recipe{})
// 应用筛选条件
if opts.Name != nil && *opts.Name != "" {
db = db.Where("name LIKE ?", "%"+*opts.Name+"%")
}
// 如果传入了原料名称,则使用子查询进行筛选
if opts.RawMaterialName != nil && *opts.RawMaterialName != "" {
subQuery := r.db.Model(&models.RecipeIngredient{}).
Select("recipe_id").
Joins("JOIN raw_materials ON raw_materials.id = recipe_ingredients.raw_material_id").
Where("raw_materials.name LIKE ?", "%"+*opts.RawMaterialName+"%")
db = db.Where("id IN (?)", subQuery)
}
// 首先计算总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 然后应用排序、分页并获取数据
if opts.OrderBy != "" {
db = db.Order(opts.OrderBy)
}
offset := (page - 1) * pageSize
if err := db.Preload("RecipeIngredients.RawMaterial.RawMaterialNutrients.Nutrient").Offset(offset).Limit(pageSize).Find(&recipes).Error; err != nil {
return nil, 0, err
}
return recipes, total, nil
}
// UpdateRecipe 更新一个配方的主体信息(名称和描述)
func (r *gormRecipeRepository) UpdateRecipe(ctx context.Context, recipe *models.Recipe) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRecipe")
updateData := map[string]interface{}{
"name": recipe.Name,
"description": recipe.Description,
}
result := r.db.WithContext(repoCtx).Model(&models.Recipe{}).Where("id = ?", recipe.ID).Updates(updateData)
if result.Error != nil {
return fmt.Errorf("更新配方主体信息失败: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("未找到要更新的配方ID: %d", recipe.ID)
}
return nil
}
// DeleteRecipe 根据ID删除一个配方并级联软删除关联的 RecipeIngredient 记录
func (r *gormRecipeRepository) DeleteRecipe(ctx context.Context, id uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRecipe")
return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error {
// 1. 查找 Recipe 记录,确保其存在
var recipe models.Recipe
if err := tx.First(&recipe, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("未找到要删除的配方ID: %d", id)
}
return fmt.Errorf("查询配方失败: %w", err)
}
// 2. 软删除所有关联的 RecipeIngredient 记录
if err := tx.Where("recipe_id = ?", id).Delete(&models.RecipeIngredient{}).Error; err != nil {
return fmt.Errorf("软删除关联的配方原料记录失败: %w", err)
}
// 3. 软删除 Recipe 记录本身
if err := tx.Delete(&recipe).Error; err != nil {
return fmt.Errorf("软删除配方失败: %w", err)
}
return nil
})
}
// DeleteRecipeIngredientsByRecipeIDTx 在给定事务中删除配方原料
func (r *gormRecipeRepository) DeleteRecipeIngredientsByRecipeIDTx(ctx context.Context, tx *gorm.DB, recipeID uint32) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRecipeIngredientsByRecipeIDTx")
if err := tx.WithContext(repoCtx).Where("recipe_id = ?", recipeID).Delete(&models.RecipeIngredient{}).Error; err != nil {
return fmt.Errorf("删除配方 %d 的原料失败: %w", recipeID, err)
}
return nil
}
// CreateBatchRecipeIngredientsTx 在给定事务中批量创建配方原料
func (r *gormRecipeRepository) CreateBatchRecipeIngredientsTx(ctx context.Context, tx *gorm.DB, ingredients []models.RecipeIngredient) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchRecipeIngredientsTx")
if len(ingredients) == 0 {
return nil // 没有原料需要创建
}
// 确保每个原料都关联到正确的配方ID
// 注意:这里假设传入的 ingredients 已经设置了正确的 RecipeID
for i := range ingredients {
if ingredients[i].RecipeID == 0 {
return fmt.Errorf("创建配方原料时 RecipeID 不能为空")
}
}
if err := tx.WithContext(repoCtx).Create(&ingredients).Error; err != nil {
return fmt.Errorf("批量创建配方原料失败: %w", err)
}
return 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

@@ -10,7 +10,7 @@ import (
func main() {
// 1. 创建应用实例
// 所有复杂的初始化逻辑都已封装在 NewApplication 中
app, err := core.NewApplication("config.yml")
app, err := core.NewApplication("config/config.yml")
if err != nil {
// 在应用启动的最早期阶段,如果核心组件创建失败,
// 我们的自定义 logger 可能还未初始化,因此这里使用标准库的 log.Fatalf

View File

@@ -1,15 +1,19 @@

.air.toml
.continue/mcpServers/new-mcp-server.yaml
.continue/rules/new-rule.md
.gitignore
.golangci.yml
.swaggo
AGENTS.md
Makefile
README.md
RELAY_API.md
TODO-List.txt
config.example.yml
config.yml
config/.air.toml
config/.golangci.yml
config/config.example.yml
config/config.yml
config/presets-data/nutrient.json
config/presets-data/pig_nutrient_requirement.json
config/presets-data/system_plans.json
design/archive/2025-11-03-verification-before-device-deletion/add_get_device_id_configs_to_task.md
design/archive/2025-11-03-verification-before-device-deletion/check_before_device_deletion.md
design/archive/2025-11-03-verification-before-device-deletion/device_task_association_maintenance.md
@@ -33,6 +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
docs/docs.go
docs/swagger.json
docs/swagger.yaml
@@ -43,7 +48,14 @@ internal/app/api/router.go
internal/app/controller/alarm/threshold_alarm_controller.go
internal/app/controller/auth_utils.go
internal/app/controller/device/device_controller.go
internal/app/controller/feed/nutrient_controller.go
internal/app/controller/feed/pig_age_stage_controller.go
internal/app/controller/feed/pig_breed_controller.go
internal/app/controller/feed/pig_type_controller.go
internal/app/controller/feed/raw_material_controller.go
internal/app/controller/feed/recipe_controller.go
internal/app/controller/health/health_controller.go
internal/app/controller/inventory/inventory_controller.go
internal/app/controller/management/controller_helpers.go
internal/app/controller/management/pig_batch_controller.go
internal/app/controller/management/pig_batch_health_controller.go
@@ -59,6 +71,10 @@ internal/app/dto/alarm_dto.go
internal/app/dto/device_converter.go
internal/app/dto/device_dto.go
internal/app/dto/dto.go
internal/app/dto/feed_converter.go
internal/app/dto/feed_dto.go
internal/app/dto/inventory_converter.go
internal/app/dto/inventory_dto.go
internal/app/dto/monitor_converter.go
internal/app/dto/monitor_dto.go
internal/app/dto/notification_converter.go
@@ -72,11 +88,18 @@ internal/app/middleware/audit.go
internal/app/middleware/auth.go
internal/app/service/audit_service.go
internal/app/service/device_service.go
internal/app/service/inventory_service.go
internal/app/service/monitor_service.go
internal/app/service/nutrient_service.go
internal/app/service/pig_age_stage_service.go
internal/app/service/pig_batch_service.go
internal/app/service/pig_breed_service.go
internal/app/service/pig_farm_service.go
internal/app/service/pig_service.go
internal/app/service/pig_type_service.go
internal/app/service/plan_service.go
internal/app/service/raw_material_service.go
internal/app/service/recipe_service.go
internal/app/service/threshold_alarm_service.go
internal/app/service/user_service.go
internal/app/webhook/chirp_stack.go
@@ -89,6 +112,7 @@ internal/core/data_initializer.go
internal/domain/alarm/alarm_service.go
internal/domain/device/device_service.go
internal/domain/device/general_device_service.go
internal/domain/inventory/inventory_service.go
internal/domain/notify/notify.go
internal/domain/pig/pen_transfer_manager.go
internal/domain/pig/pig_batch_service.go
@@ -102,33 +126,50 @@ internal/domain/plan/analysis_plan_task_manager.go
internal/domain/plan/plan_execution_manager.go
internal/domain/plan/plan_service.go
internal/domain/plan/task.go
internal/domain/recipe/nutrient_service.go
internal/domain/recipe/pig_age_stage_service.go
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
internal/domain/task/delay_task.go
internal/domain/task/device_threshold_check_task.go
internal/domain/task/full_collection_task.go
internal/domain/task/refresh_notification_task.go
internal/domain/task/release_feed_weight_task.go
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
internal/infra/logs/logger_methods.go
internal/infra/logs/logs.go
internal/infra/models/alarm.go
internal/infra/models/device.go
internal/infra/models/device_template.go
internal/infra/models/execution.go
internal/infra/models/farm_asset.go
internal/infra/models/feed.go
internal/infra/models/medication.go
internal/infra/models/models.go
internal/infra/models/notify.go
internal/infra/models/pig.go
internal/infra/models/pig_batch.go
internal/infra/models/pig_nutrient.go
internal/infra/models/pig_sick.go
internal/infra/models/pig_trade.go
internal/infra/models/pig_transfer.go
internal/infra/models/plan.go
internal/infra/models/raw_material.go
internal/infra/models/recipe.go
internal/infra/models/schedule.go
internal/infra/models/sensor_data.go
internal/infra/models/user.go
@@ -145,6 +186,7 @@ internal/infra/repository/device_template_repository.go
internal/infra/repository/execution_log_repository.go
internal/infra/repository/medication_log_repository.go
internal/infra/repository/notification_repository.go
internal/infra/repository/nutrient_repository.go
internal/infra/repository/pending_collection_repository.go
internal/infra/repository/pending_task_repository.go
internal/infra/repository/pig_batch_log_repository.go
@@ -154,8 +196,10 @@ internal/infra/repository/pig_pen_repository.go
internal/infra/repository/pig_sick_repository.go
internal/infra/repository/pig_trade_repository.go
internal/infra/repository/pig_transfer_log_repository.go
internal/infra/repository/pig_type_repository.go
internal/infra/repository/plan_repository.go
internal/infra/repository/raw_material_repository.go
internal/infra/repository/recipe_repository.go
internal/infra/repository/repository.go
internal/infra/repository/sensor_data_repository.go
internal/infra/repository/unit_of_work.go