Compare commits

...

6 Commits

Author SHA1 Message Date
d7e2777c13 使用枚举 2025-11-25 20:22:38 +08:00
566f2d9a15 注入对象 2025-11-25 20:05:52 +08:00
c01ce6d1e6 原料模型增加参考价 2025-11-25 20:03:36 +08:00
c66671bf5f 原料删除校验 2025-11-25 18:54:11 +08:00
44ff3b19d6 实现库存管理相关逻辑 2025-11-25 18:10:28 +08:00
ae27eb142d agents.md 2025-11-25 15:25:52 +08:00
24 changed files with 1830 additions and 76 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,18 +1,10 @@
<!-- 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, 你需要先阅读此文件了解项目目录结构
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

@@ -62,6 +62,11 @@ dev:
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
@@ -79,4 +84,5 @@ tree:
# 启用gemini-cli
.PHONY: gemini
gemini:
gemini -m "gemini-2.5-flash"
gemini -m "gemini-2.5-flash"

View File

@@ -61,4 +61,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66
11. 配方模型定义和仓库层增删改查方法
12. 配方领域层方法
13. 重构配方领域
14. 配方增删改查服务层和控制器
14. 配方增删改查服务层和控制器
15. 实现库存管理相关逻辑

View File

@@ -2727,6 +2727,18 @@ const docTemplate = `{
],
"summary": "获取原料列表",
"parameters": [
{
"type": "number",
"description": "参考价格最大值",
"name": "max_reference_price",
"in": "query"
},
{
"type": "number",
"description": "参考价格最小值",
"name": "min_reference_price",
"in": "query"
},
{
"type": "string",
"description": "按原料名称模糊查询",
@@ -2798,7 +2810,7 @@ const docTemplate = `{
"summary": "创建原料",
"parameters": [
{
"description": "原料信息",
"description": "原料信息,包含名称、描述和参考价格",
"name": "rawMaterial",
"in": "body",
"required": true,
@@ -2900,7 +2912,7 @@ const docTemplate = `{
"required": true
},
{
"description": "更新后的原料信息",
"description": "更新后的原料信息,包含名称、描述和参考价格",
"name": "rawMaterial",
"in": "body",
"required": true,
@@ -3267,6 +3279,215 @@ const docTemplate = `{
}
}
},
"/api/v1/inventory/stock/adjust": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "手动调整指定原料的库存量。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "调整原料库存",
"parameters": [
{
"description": "库存调整请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.StockAdjustmentRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为200代表调整成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.StockLogResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/current": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取所有原料的当前库存列表,支持分页和过滤。",
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "获取当前库存列表",
"parameters": [
{
"type": "string",
"description": "排序字段, 例如 \"stock DESC\"",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "按原料名称模糊查询",
"name": "raw_material_name",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListCurrentStockResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/logs": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取原料库存变动历史记录,支持分页、过滤和时间范围查询。",
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "获取库存变动日志",
"parameters": [
{
"type": "string",
"description": "结束时间 (RFC3339格式)",
"name": "end_time",
"in": "query"
},
{
"type": "string",
"description": "排序字段",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
},
{
"type": "integer",
"description": "按原料ID精确查询",
"name": "raw_material_id",
"in": "query"
},
{
"type": "array",
"items": {
"enum": [
"采购入库",
"饲喂出库",
"变质出库",
"售卖出库",
"杂用领取",
"手动盘点",
"发酵出库",
"发酵入库"
],
"type": "string"
},
"collectionFormat": "csv",
"description": "按来源类型查询",
"name": "source_types",
"in": "query"
},
{
"type": "string",
"description": "开始时间 (RFC3339格式, e.g., \"2023-01-01T00:00:00Z\")",
"name": "start_time",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListStockLogResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/monitor/device-command-logs": {
"get": {
"security": [
@@ -3448,6 +3669,7 @@ const docTemplate = `{
},
{
"enum": [
7,
-1,
0,
1,
@@ -3457,12 +3679,12 @@ const docTemplate = `{
5,
-1,
5,
6,
7
6
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -3472,8 +3694,7 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
],
"name": "level",
"in": "query"
@@ -7004,6 +7225,10 @@ const docTemplate = `{
"description": "原料名称",
"type": "string",
"maxLength": 100
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -7062,6 +7287,27 @@ const docTemplate = `{
}
}
},
"dto.CurrentStockResponse": {
"type": "object",
"properties": {
"last_updated": {
"description": "最后更新时间",
"type": "string"
},
"raw_material_id": {
"description": "原料ID",
"type": "integer"
},
"raw_material_name": {
"description": "原料名称",
"type": "string"
},
"stock": {
"description": "当前库存量, 单位: g",
"type": "number"
}
}
},
"dto.DeleteDeviceThresholdAlarmDTO": {
"type": "object",
"required": [
@@ -7259,6 +7505,20 @@ const docTemplate = `{
}
}
},
"dto.ListCurrentStockResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.CurrentStockResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListDeviceCommandLogResponse": {
"type": "object",
"properties": {
@@ -7540,6 +7800,20 @@ const docTemplate = `{
}
}
},
"dto.ListStockLogResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.StockLogResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListTaskExecutionLogResponse": {
"type": "object",
"properties": {
@@ -8496,6 +8770,10 @@ const docTemplate = `{
"items": {
"$ref": "#/definitions/dto.RawMaterialNutrientDTO"
}
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -8852,6 +9130,63 @@ const docTemplate = `{
}
}
},
"dto.StockAdjustmentRequest": {
"type": "object",
"required": [
"change_amount",
"raw_material_id"
],
"properties": {
"change_amount": {
"description": "变动数量, 正数为入库, 负数为出库, 单位: g",
"type": "number"
},
"raw_material_id": {
"description": "要调整的原料ID",
"type": "integer"
},
"remarks": {
"description": "备注",
"type": "string",
"maxLength": 255
}
}
},
"dto.StockLogResponse": {
"type": "object",
"properties": {
"after_quantity": {
"type": "number"
},
"before_quantity": {
"type": "number"
},
"change_amount": {
"type": "number"
},
"happened_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"raw_material_id": {
"type": "integer"
},
"raw_material_name": {
"type": "string"
},
"remarks": {
"type": "string"
},
"source_id": {
"type": "integer"
},
"source_type": {
"$ref": "#/definitions/models.StockLogSourceType"
}
}
},
"dto.SubPlanResponse": {
"type": "object",
"properties": {
@@ -9470,6 +9805,10 @@ const docTemplate = `{
"description": "原料名称",
"type": "string",
"maxLength": 100
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -10071,6 +10410,43 @@ const docTemplate = `{
"FatalLevel"
]
},
"models.StockLogSourceType": {
"type": "string",
"enum": [
"采购入库",
"饲喂出库",
"变质出库",
"售卖出库",
"杂用领取",
"手动盘点",
"发酵出库",
"发酵入库"
],
"x-enum-comments": {
"StockLogSourceFermentEnd": "发酵料产出,作为新原料计入库存",
"StockLogSourceFermentStart": "原料投入发酵,从库存中扣除"
},
"x-enum-descriptions": [
"",
"",
"",
"",
"",
"",
"原料投入发酵,从库存中扣除",
"发酵料产出,作为新原料计入库存"
],
"x-enum-varnames": [
"StockLogSourcePurchase",
"StockLogSourceFeeding",
"StockLogSourceDeteriorate",
"StockLogSourceSale",
"StockLogSourceMiscellaneous",
"StockLogSourceManual",
"StockLogSourceFermentStart",
"StockLogSourceFermentEnd"
]
},
"models.TaskType": {
"type": "string",
"enum": [
@@ -10147,6 +10523,7 @@ const docTemplate = `{
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -10156,10 +10533,10 @@ const docTemplate = `{
5,
-1,
5,
6,
7
6
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -10169,8 +10546,7 @@ const docTemplate = `{
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
]
}
},

View File

@@ -2719,6 +2719,18 @@
],
"summary": "获取原料列表",
"parameters": [
{
"type": "number",
"description": "参考价格最大值",
"name": "max_reference_price",
"in": "query"
},
{
"type": "number",
"description": "参考价格最小值",
"name": "min_reference_price",
"in": "query"
},
{
"type": "string",
"description": "按原料名称模糊查询",
@@ -2790,7 +2802,7 @@
"summary": "创建原料",
"parameters": [
{
"description": "原料信息",
"description": "原料信息,包含名称、描述和参考价格",
"name": "rawMaterial",
"in": "body",
"required": true,
@@ -2892,7 +2904,7 @@
"required": true
},
{
"description": "更新后的原料信息",
"description": "更新后的原料信息,包含名称、描述和参考价格",
"name": "rawMaterial",
"in": "body",
"required": true,
@@ -3259,6 +3271,215 @@
}
}
},
"/api/v1/inventory/stock/adjust": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "手动调整指定原料的库存量。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "调整原料库存",
"parameters": [
{
"description": "库存调整请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.StockAdjustmentRequest"
}
}
],
"responses": {
"200": {
"description": "业务码为200代表调整成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.StockLogResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/current": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取所有原料的当前库存列表,支持分页和过滤。",
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "获取当前库存列表",
"parameters": [
{
"type": "string",
"description": "排序字段, 例如 \"stock DESC\"",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "按原料名称模糊查询",
"name": "raw_material_name",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListCurrentStockResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/inventory/stock/logs": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取原料库存变动历史记录,支持分页、过滤和时间范围查询。",
"produces": [
"application/json"
],
"tags": [
"库存管理"
],
"summary": "获取库存变动日志",
"parameters": [
{
"type": "string",
"description": "结束时间 (RFC3339格式)",
"name": "end_time",
"in": "query"
},
{
"type": "string",
"description": "排序字段",
"name": "order_by",
"in": "query"
},
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "每页数量",
"name": "page_size",
"in": "query"
},
{
"type": "integer",
"description": "按原料ID精确查询",
"name": "raw_material_id",
"in": "query"
},
{
"type": "array",
"items": {
"enum": [
"采购入库",
"饲喂出库",
"变质出库",
"售卖出库",
"杂用领取",
"手动盘点",
"发酵出库",
"发酵入库"
],
"type": "string"
},
"collectionFormat": "csv",
"description": "按来源类型查询",
"name": "source_types",
"in": "query"
},
{
"type": "string",
"description": "开始时间 (RFC3339格式, e.g., \"2023-01-01T00:00:00Z\")",
"name": "start_time",
"in": "query"
}
],
"responses": {
"200": {
"description": "业务码为200代表成功获取列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/controller.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/dto.ListStockLogResponse"
}
}
}
]
}
}
}
}
},
"/api/v1/monitor/device-command-logs": {
"get": {
"security": [
@@ -3440,6 +3661,7 @@
},
{
"enum": [
7,
-1,
0,
1,
@@ -3449,12 +3671,12 @@
5,
-1,
5,
6,
7
6
],
"type": "integer",
"format": "int32",
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -3464,8 +3686,7 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
],
"name": "level",
"in": "query"
@@ -6996,6 +7217,10 @@
"description": "原料名称",
"type": "string",
"maxLength": 100
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -7054,6 +7279,27 @@
}
}
},
"dto.CurrentStockResponse": {
"type": "object",
"properties": {
"last_updated": {
"description": "最后更新时间",
"type": "string"
},
"raw_material_id": {
"description": "原料ID",
"type": "integer"
},
"raw_material_name": {
"description": "原料名称",
"type": "string"
},
"stock": {
"description": "当前库存量, 单位: g",
"type": "number"
}
}
},
"dto.DeleteDeviceThresholdAlarmDTO": {
"type": "object",
"required": [
@@ -7251,6 +7497,20 @@
}
}
},
"dto.ListCurrentStockResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.CurrentStockResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListDeviceCommandLogResponse": {
"type": "object",
"properties": {
@@ -7532,6 +7792,20 @@
}
}
},
"dto.ListStockLogResponse": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.StockLogResponse"
}
},
"pagination": {
"$ref": "#/definitions/dto.PaginationDTO"
}
}
},
"dto.ListTaskExecutionLogResponse": {
"type": "object",
"properties": {
@@ -8488,6 +8762,10 @@
"items": {
"$ref": "#/definitions/dto.RawMaterialNutrientDTO"
}
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -8844,6 +9122,63 @@
}
}
},
"dto.StockAdjustmentRequest": {
"type": "object",
"required": [
"change_amount",
"raw_material_id"
],
"properties": {
"change_amount": {
"description": "变动数量, 正数为入库, 负数为出库, 单位: g",
"type": "number"
},
"raw_material_id": {
"description": "要调整的原料ID",
"type": "integer"
},
"remarks": {
"description": "备注",
"type": "string",
"maxLength": 255
}
}
},
"dto.StockLogResponse": {
"type": "object",
"properties": {
"after_quantity": {
"type": "number"
},
"before_quantity": {
"type": "number"
},
"change_amount": {
"type": "number"
},
"happened_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"raw_material_id": {
"type": "integer"
},
"raw_material_name": {
"type": "string"
},
"remarks": {
"type": "string"
},
"source_id": {
"type": "integer"
},
"source_type": {
"$ref": "#/definitions/models.StockLogSourceType"
}
}
},
"dto.SubPlanResponse": {
"type": "object",
"properties": {
@@ -9462,6 +9797,10 @@
"description": "原料名称",
"type": "string",
"maxLength": 100
},
"reference_price": {
"description": "参考价格(kg/元)",
"type": "number"
}
}
},
@@ -10063,6 +10402,43 @@
"FatalLevel"
]
},
"models.StockLogSourceType": {
"type": "string",
"enum": [
"采购入库",
"饲喂出库",
"变质出库",
"售卖出库",
"杂用领取",
"手动盘点",
"发酵出库",
"发酵入库"
],
"x-enum-comments": {
"StockLogSourceFermentEnd": "发酵料产出,作为新原料计入库存",
"StockLogSourceFermentStart": "原料投入发酵,从库存中扣除"
},
"x-enum-descriptions": [
"",
"",
"",
"",
"",
"",
"原料投入发酵,从库存中扣除",
"发酵料产出,作为新原料计入库存"
],
"x-enum-varnames": [
"StockLogSourcePurchase",
"StockLogSourceFeeding",
"StockLogSourceDeteriorate",
"StockLogSourceSale",
"StockLogSourceMiscellaneous",
"StockLogSourceManual",
"StockLogSourceFermentStart",
"StockLogSourceFermentEnd"
]
},
"models.TaskType": {
"type": "string",
"enum": [
@@ -10139,6 +10515,7 @@
"type": "integer",
"format": "int32",
"enum": [
7,
-1,
0,
1,
@@ -10148,10 +10525,10 @@
5,
-1,
5,
6,
7
6
],
"x-enum-varnames": [
"_numLevels",
"DebugLevel",
"InfoLevel",
"WarnLevel",
@@ -10161,8 +10538,7 @@
"FatalLevel",
"_minLevel",
"_maxLevel",
"InvalidLevel",
"_numLevels"
"InvalidLevel"
]
}
},

View File

@@ -420,6 +420,9 @@ definitions:
description: 原料名称
maxLength: 100
type: string
reference_price:
description: 参考价格(kg/元)
type: number
required:
- name
type: object
@@ -462,6 +465,21 @@ definitions:
example: newuser
type: string
type: object
dto.CurrentStockResponse:
properties:
last_updated:
description: 最后更新时间
type: string
raw_material_id:
description: 原料ID
type: integer
raw_material_name:
description: 原料名称
type: string
stock:
description: '当前库存量, 单位: g'
type: number
type: object
dto.DeleteDeviceThresholdAlarmDTO:
properties:
sensor_type:
@@ -590,6 +608,15 @@ definitions:
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListCurrentStockResponse:
properties:
list:
items:
$ref: '#/definitions/dto.CurrentStockResponse'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListDeviceCommandLogResponse:
properties:
list:
@@ -771,6 +798,15 @@ definitions:
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListStockLogResponse:
properties:
list:
items:
$ref: '#/definitions/dto.StockLogResponse'
type: array
pagination:
$ref: '#/definitions/dto.PaginationDTO'
type: object
dto.ListTaskExecutionLogResponse:
properties:
list:
@@ -1405,6 +1441,9 @@ definitions:
items:
$ref: '#/definitions/dto.RawMaterialNutrientDTO'
type: array
reference_price:
description: 参考价格(kg/元)
type: number
type: object
dto.RecipeIngredientDto:
properties:
@@ -1654,6 +1693,45 @@ definitions:
required:
- duration_minutes
type: object
dto.StockAdjustmentRequest:
properties:
change_amount:
description: '变动数量, 正数为入库, 负数为出库, 单位: g'
type: number
raw_material_id:
description: 要调整的原料ID
type: integer
remarks:
description: 备注
maxLength: 255
type: string
required:
- change_amount
- raw_material_id
type: object
dto.StockLogResponse:
properties:
after_quantity:
type: number
before_quantity:
type: number
change_amount:
type: number
happened_at:
type: string
id:
type: integer
raw_material_id:
type: integer
raw_material_name:
type: string
remarks:
type: string
source_id:
type: integer
source_type:
$ref: '#/definitions/models.StockLogSourceType'
type: object
dto.SubPlanResponse:
properties:
child_plan:
@@ -2074,6 +2152,9 @@ definitions:
description: 原料名称
maxLength: 100
type: string
reference_price:
description: 参考价格(kg/元)
type: number
required:
- name
type: object
@@ -2545,6 +2626,38 @@ definitions:
- DPanicLevel
- PanicLevel
- FatalLevel
models.StockLogSourceType:
enum:
- 采购入库
- 饲喂出库
- 变质出库
- 售卖出库
- 杂用领取
- 手动盘点
- 发酵出库
- 发酵入库
type: string
x-enum-comments:
StockLogSourceFermentEnd: 发酵料产出,作为新原料计入库存
StockLogSourceFermentStart: 原料投入发酵,从库存中扣除
x-enum-descriptions:
- ""
- ""
- ""
- ""
- ""
- ""
- 原料投入发酵,从库存中扣除
- 发酵料产出,作为新原料计入库存
x-enum-varnames:
- StockLogSourcePurchase
- StockLogSourceFeeding
- StockLogSourceDeteriorate
- StockLogSourceSale
- StockLogSourceMiscellaneous
- StockLogSourceManual
- StockLogSourceFermentStart
- StockLogSourceFermentEnd
models.TaskType:
enum:
- 计划分析
@@ -2606,6 +2719,7 @@ definitions:
- PlanTypeFilterSystem
zapcore.Level:
enum:
- 7
- -1
- 0
- 1
@@ -2616,10 +2730,10 @@ definitions:
- -1
- 5
- 6
- 7
format: int32
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -2630,7 +2744,6 @@ definitions:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
info:
contact:
email: divano@example.com
@@ -4279,6 +4392,14 @@ paths:
get:
description: 获取所有原料的列表,支持分页和过滤。
parameters:
- description: 参考价格最大值
in: query
name: max_reference_price
type: number
- description: 参考价格最小值
in: query
name: min_reference_price
type: number
- description: 按原料名称模糊查询
in: query
name: name
@@ -4321,7 +4442,7 @@ paths:
- application/json
description: 创建一个新的原料。
parameters:
- description: 原料信息
- description: 原料信息,包含名称、描述和参考价格
in: body
name: rawMaterial
required: true
@@ -4400,7 +4521,7 @@ paths:
name: id
required: true
type: integer
- description: 更新后的原料信息
- description: 更新后的原料信息,包含名称、描述和参考价格
in: body
name: rawMaterial
required: true
@@ -4601,6 +4722,133 @@ paths:
summary: 更新配方
tags:
- 饲料管理-配方
/api/v1/inventory/stock/adjust:
post:
consumes:
- application/json
description: 手动调整指定原料的库存量。
parameters:
- description: 库存调整请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.StockAdjustmentRequest'
produces:
- application/json
responses:
"200":
description: 业务码为200代表调整成功
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.StockLogResponse'
type: object
security:
- BearerAuth: []
summary: 调整原料库存
tags:
- 库存管理
/api/v1/inventory/stock/current:
get:
description: 获取所有原料的当前库存列表,支持分页和过滤。
parameters:
- description: 排序字段, 例如 "stock DESC"
in: query
name: order_by
type: string
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: page_size
type: integer
- description: 按原料名称模糊查询
in: query
name: raw_material_name
type: string
produces:
- application/json
responses:
"200":
description: 业务码为200代表成功获取列表
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ListCurrentStockResponse'
type: object
security:
- BearerAuth: []
summary: 获取当前库存列表
tags:
- 库存管理
/api/v1/inventory/stock/logs:
get:
description: 获取原料库存变动历史记录,支持分页、过滤和时间范围查询。
parameters:
- description: 结束时间 (RFC3339格式)
in: query
name: end_time
type: string
- description: 排序字段
in: query
name: order_by
type: string
- description: 页码
in: query
name: page
type: integer
- description: 每页数量
in: query
name: page_size
type: integer
- description: 按原料ID精确查询
in: query
name: raw_material_id
type: integer
- collectionFormat: csv
description: 按来源类型查询
in: query
items:
enum:
- 采购入库
- 饲喂出库
- 变质出库
- 售卖出库
- 杂用领取
- 手动盘点
- 发酵出库
- 发酵入库
type: string
name: source_types
type: array
- description: 开始时间 (RFC3339格式, e.g., "2023-01-01T00:00:00Z")
in: query
name: start_time
type: string
produces:
- application/json
responses:
"200":
description: 业务码为200代表成功获取列表
schema:
allOf:
- $ref: '#/definitions/controller.Response'
- properties:
data:
$ref: '#/definitions/dto.ListStockLogResponse'
type: object
security:
- BearerAuth: []
summary: 获取库存变动日志
tags:
- 库存管理
/api/v1/monitor/device-command-logs:
get:
description: 根据提供的过滤条件,分页获取设备命令日志
@@ -4699,6 +4947,7 @@ paths:
name: end_time
type: string
- enum:
- 7
- -1
- 0
- 1
@@ -4709,12 +4958,12 @@ paths:
- -1
- 5
- 6
- 7
format: int32
in: query
name: level
type: integer
x-enum-varnames:
- _numLevels
- DebugLevel
- InfoLevel
- WarnLevel
@@ -4725,7 +4974,6 @@ paths:
- _minLevel
- _maxLevel
- InvalidLevel
- _numLevels
- enum:
- 邮件
- 企业微信

View File

@@ -23,6 +23,7 @@ import (
"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"
@@ -62,6 +63,7 @@ type API struct {
pigTypeController *feed.PigTypeController // 猪种类控制器实例
rawMaterialController *feed.RawMaterialController // 原料控制器实例
recipeController *feed.RecipeController // 配方控制器实例
inventoryController *inventory.InventoryController // 库存控制器实例
listenHandler webhook.ListenHandler // 设备上行事件监听器
analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例
}
@@ -85,6 +87,7 @@ func NewAPI(cfg config.ServerConfig,
pigAgeStageService service.PigAgeStageService,
pigTypeService service.PigTypeService,
recipeService service.RecipeService,
inventoryService service.InventoryService,
tokenGenerator token.Generator,
listenHandler webhook.ListenHandler,
) *API {
@@ -122,6 +125,7 @@ func NewAPI(cfg config.ServerConfig,
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

@@ -261,6 +261,15 @@ func (a *API) setupRoutes() {
feedGroup.GET("/recipes", a.recipeController.ListRecipes)
}
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

@@ -34,7 +34,7 @@ func NewRawMaterialController(ctx context.Context, feedManagementService service
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param rawMaterial body dto.CreateRawMaterialRequest true "原料信息"
// @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 {
@@ -67,7 +67,7 @@ func (c *RawMaterialController) CreateRawMaterial(ctx echo.Context) error {
// @Accept json
// @Produce json
// @Param id path int true "原料ID"
// @Param rawMaterial body dto.UpdateRawMaterialRequest true "更新后的原料信息"
// @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 {
@@ -172,7 +172,7 @@ func (c *RawMaterialController) GetRawMaterial(ctx echo.Context) error {
// @Tags 饲料管理-原料
// @Security BearerAuth
// @Produce json
// @Param query query dto.ListRawMaterialRequest false "查询参数"
// @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 {

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

@@ -65,6 +65,7 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse {
ID: rm.ID,
Name: rm.Name,
Description: rm.Description,
ReferencePrice: rm.ReferencePrice,
RawMaterialNutrients: rawMaterialNutrientDTOs,
}
}

View File

@@ -52,14 +52,16 @@ type ListNutrientResponse struct {
// CreateRawMaterialRequest 创建原料的请求体
type CreateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
}
// UpdateRawMaterialRequest 更新原料的请求体
type UpdateRawMaterialRequest struct {
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
Name string `json:"name" validate:"required,max=100"` // 原料名称
Description string `json:"description" validate:"max=255"` // 描述
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
}
// RawMaterialNutrientDTO 原料营养素响应体
@@ -75,16 +77,19 @@ type RawMaterialResponse struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元)
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"` // 按营养名称模糊查询
OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC"
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 是获取原料列表的响应结构

View File

@@ -0,0 +1,66 @@
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) // 默认使用创建时间
if latestLog != nil {
stock = latestLog.AfterQuantity
lastUpdated = latestLog.HappenedAt.Format(time.RFC3339)
}
return &CurrentStockResponse{
RawMaterialID: material.ID,
RawMaterialName: material.Name,
Stock: stock,
LastUpdated: lastUpdated,
}
}
// 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,71 @@
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
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"` // 最后更新时间
}
// 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"
}
// 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

@@ -0,0 +1,157 @@
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, models.StockLogSourceManual, nil, 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, // 注意:这里的排序可能需要调整,比如按原料名排序
}
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

@@ -46,7 +46,7 @@ func NewRawMaterialService(ctx context.Context, recipeSvc recipe.Service) RawMat
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto.CreateRawMaterialRequest) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial")
rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description)
rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description, req.ReferencePrice)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNameConflict) {
return nil, ErrRawMaterialNameConflict
@@ -61,7 +61,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, req *dto.UpdateRawMaterialRequest) (*dto.RawMaterialResponse, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial")
rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description)
rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description, req.ReferencePrice)
if err != nil {
if errors.Is(err, recipe.ErrRawMaterialNotFound) {
return nil, ErrRawMaterialNotFound
@@ -106,9 +106,11 @@ func (s *rawMaterialServiceImpl) ListRawMaterials(ctx context.Context, req *dto.
serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials")
opts := repository.RawMaterialListOptions{
Name: req.Name,
NutrientName: req.NutrientName,
OrderBy: req.OrderBy,
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 {

View File

@@ -69,6 +69,7 @@ func NewApplication(configPath string) (*Application, error) {
appServices.pigAgeStageService,
appServices.pigTypeService,
appServices.recipeService,
appServices.inventoryService,
infra.tokenGenerator,
infra.lora.listenHandler,
)

View File

@@ -9,6 +9,7 @@ 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"
@@ -137,6 +138,7 @@ type DomainServices struct {
notifyService domain_notify.Service
alarmService alarm.AlarmService
recipeService recipe.Service
inventoryService inventory.InventoryCoreService
}
// initDomainServices 初始化所有的领域服务。
@@ -148,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)
@@ -221,7 +226,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
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)
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)
recipeService := recipe.NewRecipeService(
logs.AddCompName(baseCtx, "RecipeService"),
@@ -246,6 +251,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr
notifyService: notifyService,
alarmService: alarmService,
recipeService: recipeService,
inventoryService: inventoryService,
}, nil
}
@@ -265,6 +271,7 @@ type AppServices struct {
pigTypeService service.PigTypeService
rawMaterialService service.RawMaterialService
recipeService service.RecipeService
inventoryService service.InventoryService
}
// initAppServices 初始化所有的应用服务。
@@ -318,6 +325,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
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,
@@ -334,6 +342,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices
pigTypeService: pigTypeService,
rawMaterialService: rawMaterialService,
recipeService: recipeService,
inventoryService: inventoryService,
}
}

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

@@ -12,16 +12,24 @@ import (
"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("原料尚有库存,无法删除")
)
// RawMaterialService 定义了原料领域的核心业务服务接口
type RawMaterialService interface {
CreateRawMaterial(ctx context.Context, name, description string) (*models.RawMaterial, error)
UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*models.RawMaterial, error)
CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error)
UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error)
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)
@@ -33,19 +41,21 @@ 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) 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) (*models.RawMaterial, error) {
func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial")
// 检查名称是否已存在
@@ -58,8 +68,9 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de
}
rawMaterial := &models.RawMaterial{
Name: name,
Description: description,
Name: name,
Description: description,
ReferencePrice: referencePrice,
}
if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil {
@@ -70,7 +81,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de
}
// UpdateRawMaterial 实现了更新原料的核心业务逻辑
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*models.RawMaterial, error) {
func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error) {
serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial")
// 检查要更新的实体是否存在
@@ -95,6 +106,7 @@ func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint3
rawMaterial.Name = name
rawMaterial.Description = description
rawMaterial.ReferencePrice = referencePrice
if err := s.rawMaterialRepo.UpdateRawMaterial(serviceCtx, rawMaterial); err != nil {
return nil, fmt.Errorf("更新原料失败: %w", err)
@@ -116,6 +128,16 @@ func (s *rawMaterialServiceImpl) DeleteRawMaterial(ctx context.Context, id uint3
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
}
if err := s.rawMaterialRepo.DeleteRawMaterial(serviceCtx, id); err != nil {
return fmt.Errorf("删除原料失败: %w", err)
}

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
@@ -13,9 +14,21 @@ import (
// RawMaterialListOptions 定义了查询原料列表时的筛选条件
type RawMaterialListOptions struct {
Name *string
NutrientName *string
OrderBy string
Name *string
NutrientName *string
MinReferencePrice *float32 // 参考价格最小值
MaxReferencePrice *float32 // 参考价格最大值
OrderBy string
}
// StockLogListOptions 定义了查询库存日志列表时的筛选条件
type StockLogListOptions struct {
RawMaterialID *uint32
RawMaterialName *string
SourceTypes []models.StockLogSourceType
StartTime *time.Time
EndTime *time.Time
OrderBy string
}
// RawMaterialRepository 定义了与原料相关的数据库操作接口
@@ -32,6 +45,10 @@ type RawMaterialRepository interface {
// 库存日志相关方法
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 实现
@@ -96,6 +113,14 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R
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 err := db.Count(&total).Error; err != nil {
return nil, 0, err
@@ -118,8 +143,9 @@ func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMa
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial")
// 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段
updateData := map[string]interface{}{
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"name": rawMaterial.Name,
"description": rawMaterial.Description,
"reference_price": rawMaterial.ReferencePrice,
}
result := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}).Where("id = ?", rawMaterial.ID).Updates(updateData)
if result.Error != nil {
@@ -219,3 +245,87 @@ func (r *gormRawMaterialRepository) GetLatestRawMaterialStockLog(ctx context.Con
}
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 {
db = db.Where("raw_material_id = ?", *opts.RawMaterialID)
}
// 新增:按原料名称模糊搜索
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 {
db = db.Where("happened_at >= ?", *opts.StartTime)
}
if opts.EndTime != nil {
db = db.Where("happened_at <= ?", *opts.EndTime)
}
// 首先计算总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("统计库存日志总数失败: %w", err)
}
// 然后应用排序、分页并获取数据
if opts.OrderBy != "" {
db = db.Order(opts.OrderBy)
} else {
// 默认排序
db = db.Order("happened_at DESC, id DESC")
}
offset := (page - 1) * pageSize
if err := db.Preload("RawMaterial").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
return nil, 0, fmt.Errorf("查询库存日志列表失败: %w", err)
}
return logs, total, nil
}

View File

@@ -37,7 +37,7 @@ design/archive/2025-11-05-provide-logger-with-mothed/task-webhook.md
design/archive/2025-11-06-health-check-routing/index.md
design/archive/2025-11-06-system-plan-continuously-triggered/index.md
design/archive/2025-11-10-exceeding-threshold-alarm/index.md
design/archive/recipe-management/index.md
design/recipe-management/index.md
docs/docs.go
docs/swagger.json
docs/swagger.yaml
@@ -55,6 +55,7 @@ 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
@@ -72,6 +73,8 @@ 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
@@ -85,6 +88,7 @@ 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
@@ -108,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