From e1399be5385b9273f61d9244c65f8a9e17b31cb0 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 18 Nov 2025 22:22:31 +0800 Subject: [PATCH 01/59] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8E=9F=E6=9C=89?= =?UTF-8?q?=E9=A3=9F=E7=89=A9=E9=80=BB=E8=BE=91=E5=92=8C=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E5=8E=9F=E6=96=99=E5=92=8C=E8=90=A5?= =?UTF-8?q?=E5=85=BB=E4=BB=B7=E5=80=BC=E8=A1=A8=E5=92=8C=E5=8E=9F=E6=96=99?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E6=97=A5=E5=BF=97=E5=92=8C=E8=90=A5=E5=85=BB?= =?UTF-8?q?=E8=A1=A8=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 47 ++ docs/docs.go | 506 ++---------------- docs/swagger.json | 506 ++---------------- docs/swagger.yaml | 337 ++---------- internal/app/api/router.go | 3 - .../controller/monitor/monitor_controller.go | 102 ---- internal/app/dto/monitor_converter.go | 88 --- internal/app/dto/monitor_dto.go | 114 ---- internal/app/service/monitor_service.go | 68 --- internal/core/component_initializers.go | 3 - internal/infra/database/postgres.go | 17 +- internal/infra/models/feed.go | 111 ---- internal/infra/models/models.go | 6 +- internal/infra/models/raw_material.go | 92 ++++ .../repository/raw_material_repository.go | 187 ------- 15 files changed, 289 insertions(+), 1898 deletions(-) create mode 100644 design/archive/recipe-management/index.md delete mode 100644 internal/infra/models/feed.go create mode 100644 internal/infra/models/raw_material.go delete mode 100644 internal/infra/repository/raw_material_repository.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md new file mode 100644 index 0000000..f0a5cf9 --- /dev/null +++ b/design/archive/recipe-management/index.md @@ -0,0 +1,47 @@ +# 需求 + +饲料配方管理及自动生成配方 + +## issue + +http://git.huangwc.com/pig/pig-farm-controller/issues/66 + +# 开发计划 + +1. 原料营养价值管理 + - 增删改查 + - 内置60+条常用原料(玉米、豆粕43、豆粕46、发酵豆粕、麸皮、次粉、DDGS、乳清粉、鱼粉、膨化大豆、各种氨基酸、预混料、油脂等) + - 每种原料固定营养值(消化能、粗蛋白、赖氨酸、钙、磷等15项左右) + +2. 饲料库存管理(代替批次) + - 字段:饲料名、当前原料种类、当前剩余量(吨)、上次入料日期、保质期剩余天数(手动填)、是否发酵料(勾选) + - 发酵料塔额外字段: + - 发酵状态(未发酵 / 正在发酵 / 已发酵可用) + - 发酵开始日期 + - 发酵几天(默认3~7天) + - 水分增加比例(默认+10~20%) + - 营养折损系数(可调,粗蛋白-5%、能量-3%之类) + +3. 猪只阶段营养需求管理 + - 预设10个常用阶段(教槽、仔猪、小猪、中猪、大猪、后备、怀孕前中后、哺乳) + - 每个阶段维护营养需求上下限(消化能、粗蛋白、赖氨酸、钙、有效磷等12项) + +4. 配方管理 + - 按阶段建配方 + - 支持增删改查 + 复制上个配方快速新建 + - 配方明细:原料 + 配比(%) + +5. 自动生成配方(核心功能) + - 选择阶段 → 点击“自动计算最低成本配方” + - 自动读取当前所有料塔的: + - 剩余量(不够的原料自动降配比) + - 保质期剩余天数(越快过期的优先用,权重×1.5) + - 发酵料塔如果状态是“已发酵可用”则按发酵后营养值参与计算 + - 输出:总成本、营养达标情况、发酵料占比、即将过期原料使用提示 + +6. 配方下发与记录 + - 一键下发到喂料站/料线(生成下料曲线) + - 自动记录今天用了哪个配方 + +7. 简单查看功能 + - 两个配方对比页面(营养+成本对比) diff --git a/docs/docs.go b/docs/docs.go index e956e45..dc7ca45 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -56,13 +56,13 @@ const docTemplate = `{ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -172,13 +172,13 @@ const docTemplate = `{ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -453,13 +453,13 @@ const docTemplate = `{ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -740,13 +740,13 @@ const docTemplate = `{ "parameters": [ { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -1719,85 +1719,6 @@ const docTemplate = `{ } } }, - "/api/v1/monitor/feed-usage-records": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取饲料使用记录", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取饲料使用记录列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "integer", - "name": "feed_formula_id", - "in": "query" - }, - { - "type": "integer", - "name": "operator_id", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "pen_id", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListFeedUsageRecordResponse" - } - } - } - ] - } - } - } - } - }, "/api/v1/monitor/medication-logs": { "get": { "security": [ @@ -1905,6 +1826,7 @@ const docTemplate = `{ }, { "enum": [ + 7, -1, 0, 1, @@ -1914,12 +1836,12 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1929,8 +1851,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -2588,159 +2509,6 @@ const docTemplate = `{ } } }, - "/api/v1/monitor/raw-material-purchases": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取原料采购记录", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取原料采购记录列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "raw_material_id", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - }, - { - "type": "string", - "name": "supplier", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListRawMaterialPurchaseResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/monitor/raw-material-stock-logs": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取原料库存日志", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取原料库存日志列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "raw_material_id", - "in": "query" - }, - { - "type": "integer", - "name": "source_id", - "in": "query" - }, - { - "type": "string", - "name": "source_type", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListRawMaterialStockLogResponse" - } - } - } - ] - } - } - } - } - }, "/api/v1/monitor/sensor-data": { "get": { "security": [ @@ -5645,49 +5413,6 @@ const docTemplate = `{ } } }, - "dto.FeedFormulaDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "dto.FeedUsageRecordDTO": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "feed_formula": { - "$ref": "#/definitions/dto.FeedFormulaDTO" - }, - "feed_formula_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "operator_id": { - "type": "integer" - }, - "pen": { - "$ref": "#/definitions/dto.PenDTO" - }, - "pen_id": { - "type": "integer" - }, - "recorded_at": { - "type": "string" - }, - "remarks": { - "type": "string" - } - } - }, "dto.HistoricalAlarmDTO": { "type": "object", "properties": { @@ -5782,20 +5507,6 @@ const docTemplate = `{ } } }, - "dto.ListFeedUsageRecordResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.FeedUsageRecordDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, "dto.ListHistoricalAlarmResponse": { "type": "object", "properties": { @@ -5951,34 +5662,6 @@ const docTemplate = `{ } } }, - "dto.ListRawMaterialPurchaseResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.RawMaterialPurchaseDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, - "dto.ListRawMaterialStockLogResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.RawMaterialStockLogDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -6216,17 +5899,6 @@ const docTemplate = `{ } } }, - "dto.PenDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, "dto.PenResponse": { "type": "object", "properties": { @@ -6733,75 +6405,6 @@ const docTemplate = `{ } } }, - "dto.RawMaterialDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "dto.RawMaterialPurchaseDTO": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "purchase_date": { - "type": "string" - }, - "raw_material": { - "$ref": "#/definitions/dto.RawMaterialDTO" - }, - "raw_material_id": { - "type": "integer" - }, - "supplier": { - "type": "string" - }, - "total_price": { - "type": "number" - }, - "unit_price": { - "type": "number" - } - } - }, - "dto.RawMaterialStockLogDTO": { - "type": "object", - "properties": { - "change_amount": { - "type": "number" - }, - "happened_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "raw_material_id": { - "type": "integer" - }, - "remarks": { - "type": "string" - }, - "source_id": { - "type": "integer" - }, - "source_type": { - "$ref": "#/definitions/models.StockLogSourceType" - } - } - }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -8140,13 +7743,13 @@ const docTemplate = `{ "models.SeverityLevel": { "type": "string", "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "x-enum-varnames": [ "DebugLevel", @@ -8158,25 +7761,6 @@ const docTemplate = `{ "FatalLevel" ] }, - "models.StockLogSourceType": { - "type": "string", - "enum": [ - "采购入库", - "饲喂出库", - "变质出库", - "售卖出库", - "杂用领取", - "手动盘点" - ], - "x-enum-varnames": [ - "StockLogSourcePurchase", - "StockLogSourceFeeding", - "StockLogSourceDeteriorate", - "StockLogSourceSale", - "StockLogSourceMiscellaneous", - "StockLogSourceManual" - ] - }, "models.TaskType": { "type": "string", "enum": [ @@ -8185,6 +7769,7 @@ const docTemplate = `{ "下料", "全量采集", "告警通知", + "通知刷新", "设备阈值检查", "区域阈值检查" ], @@ -8194,6 +7779,7 @@ const docTemplate = `{ "TaskTypeAreaCollectorThresholdCheck": "区域阈值检查任务", "TaskTypeDeviceThresholdCheck": "设备阈值检查任务", "TaskTypeFullCollection": "新增的全量采集任务", + "TaskTypeNotificationRefresh": "通知刷新任务", "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", "TaskTypeWaiting": "等待任务" }, @@ -8203,6 +7789,7 @@ const docTemplate = `{ "下料口释放指定重量任务", "新增的全量采集任务", "告警通知任务", + "通知刷新任务", "设备阈值检查任务", "区域阈值检查任务" ], @@ -8212,6 +7799,7 @@ const docTemplate = `{ "TaskTypeReleaseFeedWeight", "TaskTypeFullCollection", "TaskTypeAlarmNotification", + "TaskTypeNotificationRefresh", "TaskTypeDeviceThresholdCheck", "TaskTypeAreaCollectorThresholdCheck" ] @@ -8249,6 +7837,7 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -8258,10 +7847,10 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -8271,8 +7860,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 5edc711..e300abb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -48,13 +48,13 @@ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -164,13 +164,13 @@ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -445,13 +445,13 @@ }, { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -732,13 +732,13 @@ "parameters": [ { "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "type": "string", "x-enum-varnames": [ @@ -1711,85 +1711,6 @@ } } }, - "/api/v1/monitor/feed-usage-records": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取饲料使用记录", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取饲料使用记录列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "integer", - "name": "feed_formula_id", - "in": "query" - }, - { - "type": "integer", - "name": "operator_id", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "pen_id", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListFeedUsageRecordResponse" - } - } - } - ] - } - } - } - } - }, "/api/v1/monitor/medication-logs": { "get": { "security": [ @@ -1897,6 +1818,7 @@ }, { "enum": [ + 7, -1, 0, 1, @@ -1906,12 +1828,12 @@ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1921,8 +1843,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -2580,159 +2501,6 @@ } } }, - "/api/v1/monitor/raw-material-purchases": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取原料采购记录", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取原料采购记录列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "raw_material_id", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - }, - { - "type": "string", - "name": "supplier", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListRawMaterialPurchaseResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/monitor/raw-material-stock-logs": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "根据提供的过滤条件,分页获取原料库存日志", - "produces": [ - "application/json" - ], - "tags": [ - "数据监控" - ], - "summary": "获取原料库存日志列表", - "parameters": [ - { - "type": "string", - "name": "end_time", - "in": "query" - }, - { - "type": "string", - "name": "order_by", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "name": "page_size", - "in": "query" - }, - { - "type": "integer", - "name": "raw_material_id", - "in": "query" - }, - { - "type": "integer", - "name": "source_id", - "in": "query" - }, - { - "type": "string", - "name": "source_type", - "in": "query" - }, - { - "type": "string", - "name": "start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/dto.ListRawMaterialStockLogResponse" - } - } - } - ] - } - } - } - } - }, "/api/v1/monitor/sensor-data": { "get": { "security": [ @@ -5637,49 +5405,6 @@ } } }, - "dto.FeedFormulaDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "dto.FeedUsageRecordDTO": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "feed_formula": { - "$ref": "#/definitions/dto.FeedFormulaDTO" - }, - "feed_formula_id": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "operator_id": { - "type": "integer" - }, - "pen": { - "$ref": "#/definitions/dto.PenDTO" - }, - "pen_id": { - "type": "integer" - }, - "recorded_at": { - "type": "string" - }, - "remarks": { - "type": "string" - } - } - }, "dto.HistoricalAlarmDTO": { "type": "object", "properties": { @@ -5774,20 +5499,6 @@ } } }, - "dto.ListFeedUsageRecordResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.FeedUsageRecordDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, "dto.ListHistoricalAlarmResponse": { "type": "object", "properties": { @@ -5943,34 +5654,6 @@ } } }, - "dto.ListRawMaterialPurchaseResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.RawMaterialPurchaseDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, - "dto.ListRawMaterialStockLogResponse": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/dto.RawMaterialStockLogDTO" - } - }, - "pagination": { - "$ref": "#/definitions/dto.PaginationDTO" - } - } - }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -6208,17 +5891,6 @@ } } }, - "dto.PenDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, "dto.PenResponse": { "type": "object", "properties": { @@ -6725,75 +6397,6 @@ } } }, - "dto.RawMaterialDTO": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "dto.RawMaterialPurchaseDTO": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "purchase_date": { - "type": "string" - }, - "raw_material": { - "$ref": "#/definitions/dto.RawMaterialDTO" - }, - "raw_material_id": { - "type": "integer" - }, - "supplier": { - "type": "string" - }, - "total_price": { - "type": "number" - }, - "unit_price": { - "type": "number" - } - } - }, - "dto.RawMaterialStockLogDTO": { - "type": "object", - "properties": { - "change_amount": { - "type": "number" - }, - "happened_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "raw_material_id": { - "type": "integer" - }, - "remarks": { - "type": "string" - }, - "source_id": { - "type": "integer" - }, - "source_type": { - "$ref": "#/definitions/models.StockLogSourceType" - } - } - }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -8132,13 +7735,13 @@ "models.SeverityLevel": { "type": "string", "enum": [ - "Debug", - "Info", - "Warn", - "Error", - "DPanic", - "Panic", - "Fatal" + "debug", + "info", + "warn", + "error", + "dpanic", + "panic", + "fatal" ], "x-enum-varnames": [ "DebugLevel", @@ -8150,25 +7753,6 @@ "FatalLevel" ] }, - "models.StockLogSourceType": { - "type": "string", - "enum": [ - "采购入库", - "饲喂出库", - "变质出库", - "售卖出库", - "杂用领取", - "手动盘点" - ], - "x-enum-varnames": [ - "StockLogSourcePurchase", - "StockLogSourceFeeding", - "StockLogSourceDeteriorate", - "StockLogSourceSale", - "StockLogSourceMiscellaneous", - "StockLogSourceManual" - ] - }, "models.TaskType": { "type": "string", "enum": [ @@ -8177,6 +7761,7 @@ "下料", "全量采集", "告警通知", + "通知刷新", "设备阈值检查", "区域阈值检查" ], @@ -8186,6 +7771,7 @@ "TaskTypeAreaCollectorThresholdCheck": "区域阈值检查任务", "TaskTypeDeviceThresholdCheck": "设备阈值检查任务", "TaskTypeFullCollection": "新增的全量采集任务", + "TaskTypeNotificationRefresh": "通知刷新任务", "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", "TaskTypeWaiting": "等待任务" }, @@ -8195,6 +7781,7 @@ "下料口释放指定重量任务", "新增的全量采集任务", "告警通知任务", + "通知刷新任务", "设备阈值检查任务", "区域阈值检查任务" ], @@ -8204,6 +7791,7 @@ "TaskTypeReleaseFeedWeight", "TaskTypeFullCollection", "TaskTypeAlarmNotification", + "TaskTypeNotificationRefresh", "TaskTypeDeviceThresholdCheck", "TaskTypeAreaCollectorThresholdCheck" ] @@ -8241,6 +7829,7 @@ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -8250,10 +7839,10 @@ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -8263,8 +7852,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 74e893d..50e51df 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -432,34 +432,6 @@ definitions: thresholds: type: number type: object - dto.FeedFormulaDTO: - properties: - id: - type: integer - name: - type: string - type: object - dto.FeedUsageRecordDTO: - properties: - amount: - type: number - feed_formula: - $ref: '#/definitions/dto.FeedFormulaDTO' - feed_formula_id: - type: integer - id: - type: integer - operator_id: - type: integer - pen: - $ref: '#/definitions/dto.PenDTO' - pen_id: - type: integer - recorded_at: - type: string - remarks: - type: string - type: object dto.HistoricalAlarmDTO: properties: alarm_code: @@ -521,15 +493,6 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object - dto.ListFeedUsageRecordResponse: - properties: - list: - items: - $ref: '#/definitions/dto.FeedUsageRecordDTO' - type: array - pagination: - $ref: '#/definitions/dto.PaginationDTO' - type: object dto.ListHistoricalAlarmResponse: properties: list: @@ -630,24 +593,6 @@ definitions: example: 100 type: integer type: object - dto.ListRawMaterialPurchaseResponse: - properties: - list: - items: - $ref: '#/definitions/dto.RawMaterialPurchaseDTO' - type: array - pagination: - $ref: '#/definitions/dto.PaginationDTO' - type: object - dto.ListRawMaterialStockLogResponse: - properties: - list: - items: - $ref: '#/definitions/dto.RawMaterialStockLogDTO' - type: array - pagination: - $ref: '#/definitions/dto.PaginationDTO' - type: object dto.ListSensorDataResponse: properties: list: @@ -806,13 +751,6 @@ definitions: total: type: integer type: object - dto.PenDTO: - properties: - id: - type: integer - name: - type: string - type: object dto.PenResponse: properties: capacity: @@ -1142,51 +1080,6 @@ definitions: $ref: '#/definitions/dto.TaskResponse' type: array type: object - dto.RawMaterialDTO: - properties: - id: - type: integer - name: - type: string - type: object - dto.RawMaterialPurchaseDTO: - properties: - amount: - type: number - created_at: - type: string - id: - type: integer - purchase_date: - type: string - raw_material: - $ref: '#/definitions/dto.RawMaterialDTO' - raw_material_id: - type: integer - supplier: - type: string - total_price: - type: number - unit_price: - type: number - type: object - dto.RawMaterialStockLogDTO: - properties: - change_amount: - type: number - happened_at: - type: string - id: - type: integer - raw_material_id: - type: integer - remarks: - type: string - source_id: - type: integer - source_type: - $ref: '#/definitions/models.StockLogSourceType' - type: object dto.ReclassifyPenToNewBatchRequest: properties: pen_id: @@ -2159,13 +2052,13 @@ definitions: - SensorTypeWeight models.SeverityLevel: enum: - - Debug - - Info - - Warn - - Error - - DPanic - - Panic - - Fatal + - debug + - info + - warn + - error + - dpanic + - panic + - fatal type: string x-enum-varnames: - DebugLevel @@ -2175,22 +2068,6 @@ definitions: - DPanicLevel - PanicLevel - FatalLevel - models.StockLogSourceType: - enum: - - 采购入库 - - 饲喂出库 - - 变质出库 - - 售卖出库 - - 杂用领取 - - 手动盘点 - type: string - x-enum-varnames: - - StockLogSourcePurchase - - StockLogSourceFeeding - - StockLogSourceDeteriorate - - StockLogSourceSale - - StockLogSourceMiscellaneous - - StockLogSourceManual models.TaskType: enum: - 计划分析 @@ -2198,6 +2075,7 @@ definitions: - 下料 - 全量采集 - 告警通知 + - 通知刷新 - 设备阈值检查 - 区域阈值检查 type: string @@ -2207,6 +2085,7 @@ definitions: TaskTypeAreaCollectorThresholdCheck: 区域阈值检查任务 TaskTypeDeviceThresholdCheck: 设备阈值检查任务 TaskTypeFullCollection: 新增的全量采集任务 + TaskTypeNotificationRefresh: 通知刷新任务 TaskTypeReleaseFeedWeight: 下料口释放指定重量任务 TaskTypeWaiting: 等待任务 x-enum-descriptions: @@ -2215,6 +2094,7 @@ definitions: - 下料口释放指定重量任务 - 新增的全量采集任务 - 告警通知任务 + - 通知刷新任务 - 设备阈值检查任务 - 区域阈值检查任务 x-enum-varnames: @@ -2223,6 +2103,7 @@ definitions: - TaskTypeReleaseFeedWeight - TaskTypeFullCollection - TaskTypeAlarmNotification + - TaskTypeNotificationRefresh - TaskTypeDeviceThresholdCheck - TaskTypeAreaCollectorThresholdCheck models.ValueDescriptor: @@ -2248,6 +2129,7 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: + - 7 - -1 - 0 - 1 @@ -2258,10 +2140,10 @@ definitions: - -1 - 5 - 6 - - 7 format: int32 type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2272,7 +2154,6 @@ definitions: - _minLevel - _maxLevel - InvalidLevel - - _numLevels info: contact: email: divano@example.com @@ -2353,13 +2234,13 @@ paths: type: boolean - description: 按告警严重性等级过滤 enum: - - Debug - - Info - - Warn - - Error - - DPanic - - Panic - - Fatal + - debug + - info + - warn + - error + - dpanic + - panic + - fatal in: query name: level type: string @@ -2430,13 +2311,13 @@ paths: type: integer - description: 按告警等级过滤 enum: - - Debug - - Info - - Warn - - Error - - DPanic - - Panic - - Fatal + - debug + - info + - warn + - error + - dpanic + - panic + - fatal in: query name: level type: string @@ -2612,13 +2493,13 @@ paths: type: integer - description: 按告警等级过滤 enum: - - Debug - - Info - - Warn - - Error - - DPanic - - Panic - - Fatal + - debug + - info + - warn + - error + - dpanic + - panic + - fatal in: query name: level type: string @@ -2798,13 +2679,13 @@ paths: parameters: - description: 按告警严重性等级过滤 enum: - - Debug - - Info - - Warn - - Error - - DPanic - - Panic - - Fatal + - debug + - info + - warn + - error + - dpanic + - panic + - fatal in: query name: level type: string @@ -3333,51 +3214,6 @@ paths: summary: 获取设备命令日志列表 tags: - 数据监控 - /api/v1/monitor/feed-usage-records: - get: - description: 根据提供的过滤条件,分页获取饲料使用记录 - parameters: - - in: query - name: end_time - type: string - - in: query - name: feed_formula_id - type: integer - - in: query - name: operator_id - type: integer - - in: query - name: order_by - type: string - - in: query - name: page - type: integer - - in: query - name: page_size - type: integer - - in: query - name: pen_id - type: integer - - in: query - name: start_time - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/dto.ListFeedUsageRecordResponse' - type: object - security: - - BearerAuth: [] - summary: 获取饲料使用记录列表 - tags: - - 数据监控 /api/v1/monitor/medication-logs: get: description: 根据提供的过滤条件,分页获取用药记录 @@ -3434,6 +3270,7 @@ paths: name: end_time type: string - enum: + - 7 - -1 - 0 - 1 @@ -3444,12 +3281,12 @@ paths: - -1 - 5 - 6 - - 7 format: int32 in: query name: level type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -3460,7 +3297,6 @@ paths: - _minLevel - _maxLevel - InvalidLevel - - _numLevels - enum: - 邮件 - 企业微信 @@ -3846,93 +3682,6 @@ paths: summary: 获取计划执行日志列表 tags: - 数据监控 - /api/v1/monitor/raw-material-purchases: - get: - description: 根据提供的过滤条件,分页获取原料采购记录 - parameters: - - in: query - name: end_time - type: string - - in: query - name: order_by - type: string - - in: query - name: page - type: integer - - in: query - name: page_size - type: integer - - in: query - name: raw_material_id - type: integer - - in: query - name: start_time - type: string - - in: query - name: supplier - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/dto.ListRawMaterialPurchaseResponse' - type: object - security: - - BearerAuth: [] - summary: 获取原料采购记录列表 - tags: - - 数据监控 - /api/v1/monitor/raw-material-stock-logs: - get: - description: 根据提供的过滤条件,分页获取原料库存日志 - parameters: - - in: query - name: end_time - type: string - - in: query - name: order_by - type: string - - in: query - name: page - type: integer - - in: query - name: page_size - type: integer - - in: query - name: raw_material_id - type: integer - - in: query - name: source_id - type: integer - - in: query - name: source_type - type: string - - in: query - name: start_time - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/dto.ListRawMaterialStockLogResponse' - type: object - security: - - BearerAuth: [] - summary: 获取原料库存日志列表 - tags: - - 数据监控 /api/v1/monitor/sensor-data: get: description: 根据提供的过滤条件,分页获取传感器数据 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 500f495..6560d5b 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -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) diff --git a/internal/app/controller/monitor/monitor_controller.go b/internal/app/controller/monitor/monitor_controller.go index ae787ad..52e1816 100644 --- a/internal/app/controller/monitor/monitor_controller.go +++ b/internal/app/controller/monitor/monitor_controller.go @@ -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 根据提供的过滤条件,分页获取用药记录 diff --git a/internal/app/dto/monitor_converter.go b/internal/app/dto/monitor_converter.go index 657a4d4..a8d2fd0 100644 --- a/internal/app/dto/monitor_converter.go +++ b/internal/app/dto/monitor_converter.go @@ -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)) diff --git a/internal/app/dto/monitor_dto.go b/internal/app/dto/monitor_dto.go index 6fcf4b6..a711c2b 100644 --- a/internal/app/dto/monitor_dto.go +++ b/internal/app/dto/monitor_dto.go @@ -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 定义了获取用药记录列表的请求参数 diff --git a/internal/app/service/monitor_service.go b/internal/app/service/monitor_service.go index 64be63e..eeee8c0 100644 --- a/internal/app/service/monitor_service.go +++ b/internal/app/service/monitor_service.go @@ -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") diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 73a9c05..d9c6659 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -79,7 +79,6 @@ type Repositories struct { pigTradeRepo repository.PigTradeRepository pigSickPigLogRepo repository.PigSickLogRepository medicationLogRepo repository.MedicationLogRepository - rawMaterialRepo repository.RawMaterialRepository notificationRepo repository.NotificationRepository alarmRepo repository.AlarmRepository unitOfWork repository.UnitOfWork @@ -108,7 +107,6 @@ 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), unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db), @@ -247,7 +245,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, diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index d86d578..3ce4a6b 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -167,9 +167,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 +177,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 +208,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 +218,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 { @@ -258,6 +254,15 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { // 使用 IF NOT EXISTS 保证幂等性 // 如果索引已存在,此命令不会报错 + // 为 raw_material_nutrients 表创建部分唯一索引,以兼容软删除 + logger.Debug("正在为 raw_material_nutrients 表创建部分唯一索引") + partialIndexSQL := "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_material_nutrients_unique_when_not_deleted ON raw_material_nutrients (raw_material_id, nutrient_id) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 raw_material_nutrients 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 raw_material_nutrients 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)") + // 为 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);" diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go deleted file mode 100644 index 472ba2d..0000000 --- a/internal/infra/models/feed.go +++ /dev/null @@ -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" -} diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 152556b..4232d42 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -63,11 +63,9 @@ func GetAllModels() []interface{} { // Feed Models &RawMaterial{}, - &RawMaterialPurchase{}, + &Nutrient{}, + &RawMaterialNutrient{}, &RawMaterialStockLog{}, - &FeedFormula{}, - &FeedFormulaComponent{}, - &FeedUsageRecord{}, // Medication Models &Medication{}, diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go new file mode 100644 index 0000000..d138231 --- /dev/null +++ b/internal/infra/models/raw_material.go @@ -0,0 +1,92 @@ +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 = "发酵入库" // 发酵料产出,作为新原料计入库存 +) + +// NutrientType 定义了营养素的分类,用于配方优化和成本控制。 +type NutrientType string + +const ( + PositiveNutrient NutrientType = "正面营养" // 希望在配方中最大化的营养素,如蛋白质、能量 + NegativeNutrient NutrientType = "负面营养" // 需要控制上限的营养素,如粗纤维、霉菌毒素 +) + +// RawMaterial 代表一种原料的静态定义,是系统中的原料字典。 +type RawMaterial struct { + Model + Name string `gorm:"size:100;unique;not null;comment:原料名称"` + Description string `gorm:"size:255;comment:描述"` + // Quantity 是当前库存的快照值,用于提供高性能的库存查询。 + // 注意:此字段的值必须在数据库事务中与 RawMaterialStockLog 同步更新,以保证数据一致性。 + Quantity float32 `gorm:"not null;default:0;comment:当前库存快照, 单位: g"` +} + +func (RawMaterial) TableName() string { + return "raw_materials" +} + +// Nutrient 代表一种营养素的静态定义,是系统中的营养素字典。 +// 注意:本系统强制统一营养单位,不再单独设置Unit字段。 +// 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。 +type Nutrient struct { + Model + Name string `gorm:"size:100;unique;not null;comment:营养素名称"` + Type NutrientType `gorm:"size:50;not null;comment:营养素类型 (正面营养/负面营养)"` + Description string `gorm:"size:255;comment:描述"` +} + +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"` + // 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" +} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go deleted file mode 100644 index 2795167..0000000 --- a/internal/infra/repository/raw_material_repository.go +++ /dev/null @@ -1,187 +0,0 @@ -package repository - -import ( - "context" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - - "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" -} - -// RawMaterialStockLogListOptions 定义了查询原料库存日志时的可选参数 -type RawMaterialStockLogListOptions 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" -} - -// 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) -} - -// gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现 -type gormRawMaterialRepository struct { - ctx context.Context - db *gorm.DB -} - -// NewGormRawMaterialRepository 创建一个新的 RawMaterialRepository GORM 实现实例 -func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository { - 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 - } - - var results []models.RawMaterialPurchase - var total int64 - - query := r.db.WithContext(repoCtx).Model(&models.RawMaterialPurchase{}) - - if opts.RawMaterialID != nil { - query = query.Where("raw_material_id = ?", *opts.RawMaterialID) - } - if opts.Supplier != nil { - query = query.Where("supplier LIKE ?", "%"+*opts.Supplier+"%") - } - if opts.StartTime != nil { - query = query.Where("purchase_date >= ?", *opts.StartTime) - } - if opts.EndTime != nil { - query = query.Where("purchase_date <= ?", *opts.EndTime) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - orderBy := "purchase_date DESC" - if opts.OrderBy != "" { - orderBy = opts.OrderBy - } - query = query.Order(orderBy).Preload("RawMaterial") - - offset := (page - 1) * pageSize - err := query.Limit(pageSize).Offset(offset).Find(&results).Error - - return results, total, err -} - -// 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 - } - - 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 -} -- 2.49.1 From a1be06854f49dc82689720318f2f4ef9e664139a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 19 Nov 2025 15:06:18 +0800 Subject: [PATCH 02/59] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swaggo | 0 Makefile | 4 ++-- .air.toml => config/.air.toml | 0 .golangci.yml => config/.golangci.yml | 0 config.example.yml => config/config.example.yml | 0 config.yml => config/config.yml | 2 +- main.go | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 .swaggo rename .air.toml => config/.air.toml (100%) rename .golangci.yml => config/.golangci.yml (100%) rename config.example.yml => config/config.example.yml (100%) rename config.yml => config/config.yml (97%) diff --git a/.swaggo b/.swaggo deleted file mode 100644 index e69de29..0000000 diff --git a/Makefile b/Makefile index d919f4e..44ebf4c 100644 --- a/Makefile +++ b/Makefile @@ -50,12 +50,12 @@ 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 diff --git a/.air.toml b/config/.air.toml similarity index 100% rename from .air.toml rename to config/.air.toml diff --git a/.golangci.yml b/config/.golangci.yml similarity index 100% rename from .golangci.yml rename to config/.golangci.yml diff --git a/config.example.yml b/config/config.example.yml similarity index 100% rename from config.example.yml rename to config/config.example.yml diff --git a/config.yml b/config/config.yml similarity index 97% rename from config.yml rename to config/config.yml index 92b0866..ab89f66 100644 --- a/config.yml +++ b/config/config.yml @@ -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" # 日志文件路径 diff --git a/main.go b/main.go index 67e64a2..ac137b6 100644 --- a/main.go +++ b/main.go @@ -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 -- 2.49.1 From a74ab4e5e7c82404a6dcb2c8dc02b4a6a0c1f00a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 19 Nov 2025 19:31:51 +0800 Subject: [PATCH 03/59] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6,=20=E5=AE=9E=E7=8E=B0=E4=BB=8Ejson=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E8=AF=BB=E5=8F=96=E5=8E=9F=E6=9D=90=E6=96=99?= =?UTF-8?q?=E8=90=A5=E5=85=BB=E9=A2=84=E8=AE=BE=E5=80=BC,=20=E5=B9=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=86=99=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 1740 +++++++++++++++++++++ config/presets-data/system_plans.json | 0 design/archive/recipe-management/index.md | 5 + go.mod | 34 +- go.sum | 140 +- internal/core/application.go | 22 +- internal/core/data_initializer.go | 20 +- internal/infra/database/seeder.go | 203 +++ internal/infra/models/raw_material.go | 13 +- 9 files changed, 1991 insertions(+), 186 deletions(-) create mode 100644 config/presets-data/nutrient.json create mode 100644 config/presets-data/system_plans.json create mode 100644 internal/infra/database/seeder.go diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json new file mode 100644 index 0000000..672f3f1 --- /dev/null +++ b/config/presets-data/nutrient.json @@ -0,0 +1,1740 @@ +{ + "type": "nutrient", + "data": { + "DL-蛋氨酸98": { + "可消化蛋氨酸 (SID %)": 98.0, + "干物质 (%)": 100, + "总蛋氨酸 (%)": 98.0, + "水分 (%)": 0 + }, + "L-色氨酸98": { + "可消化色氨酸 (SID %)": 98.0, + "干物质 (%)": 100, + "总色氨酸 (%)": 98.0, + "水分 (%)": 0 + }, + "L-苏氨酸98": { + "可消化苏氨酸 (SID %)": 98.0, + "干物质 (%)": 100, + "总苏氨酸 (%)": 98.0, + "水分 (%)": 0 + }, + "L-赖氨酸HCl 98": { + "可消化赖氨酸 (SID %)": 78.4, + "干物质 (%)": 100, + "总赖氨酸 (%)": 78.4, + "水分 (%)": 0, + "粗蛋白 (%)": 80.0 + }, + "乳清粉": { + "乳糖 (%)": 72, + "代谢能 (kcal/kg)": 3400, + "净能 (kcal/kg)": 2600, + "可消化色氨酸 (SID %)": 0.17, + "可消化苏氨酸 (SID %)": 0.81, + "可消化蛋氨酸 (SID %)": 0.21, + "可消化赖氨酸 (SID %)": 0.90, + "干物质 (%)": 95, + "总磷 (%)": 0.70, + "总色氨酸 (%)": 0.18, + "总苏氨酸 (%)": 0.85, + "总蛋氨酸 (%)": 0.22, + "总赖氨酸 (%)": 0.95, + "有效磷 (%)": 0.60, + "水分 (%)": 5, + "消化能 (kcal/kg)": 3500, + "盐分 (%)": 2.0, + "粗灰分 (%)": 9.0, + "粗纤维 (%)": 0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 12.0, + "蛋+胱氨酸 (%)": 0.70, + "钙 (%)": 0.80 + }, + "兔肉粉": { + "代谢能 (kcal/kg)": 3600, + "净能 (kcal/kg)": 2850, + "可消化色氨酸 (SID %)": 0.62, + "可消化苏氨酸 (SID %)": 2.66, + "可消化蛋氨酸 (SID %)": 1.24, + "可消化赖氨酸 (SID %)": 4.28, + "干物质 (%)": 93, + "总磷 (%)": 1.20, + "总色氨酸 (%)": 0.65, + "总苏氨酸 (%)": 2.80, + "总蛋氨酸 (%)": 1.30, + "总赖氨酸 (%)": 4.50, + "有效磷 (%)": 0.95, + "水分 (%)": 7, + "消化能 (kcal/kg)": 3750, + "粗灰分 (%)": 12.0, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 10.0, + "粗蛋白 (%)": 68.0, + "蛋+胱氨酸 (%)": 2.20, + "钙 (%)": 1.50 + }, + "全株玉米青贮": { + "代谢能 (kcal/kg)": 2650, + "净能 (kcal/kg)": 1950, + "可消化色氨酸 (SID %)": 0.05, + "可消化苏氨酸 (SID %)": 0.26, + "可消化蛋氨酸 (SID %)": 0.11, + "可消化赖氨酸 (SID %)": 0.25, + "干物质 (%)": 32, + "总磷 (%)": 0.23, + "总色氨酸 (%)": 0.06, + "总苏氨酸 (%)": 0.30, + "总蛋氨酸 (%)": 0.13, + "总赖氨酸 (%)": 0.28, + "有效磷 (%)": 0.09, + "水分 (%)": 68, + "消化能 (kcal/kg)": 2750, + "粗灰分 (%)": 4.8, + "粗纤维 (%)": 24.0, + "粗脂肪 (%)": 2.8, + "粗蛋白 (%)": 7.5, + "蛋+胱氨酸 (%)": 0.30, + "钙 (%)": 0.22 + }, + "双低菜籽粕": { + "代谢能 (kcal/kg)": 3100, + "净能 (kcal/kg)": 2100, + "单宁 (mg/kg)": 8000, + "可消化色氨酸 (SID %)": 0.42, + "可消化苏氨酸 (SID %)": 1.46, + "可消化蛋氨酸 (SID %)": 0.67, + "可消化赖氨酸 (SID %)": 1.85, + "噁唑烷硫酮 (μmol/g)": 8, + "干物质 (%)": 91, + "异硫氰酸酯 (μmol/g)": 4, + "总磷 (%)": 1.12, + "总色氨酸 (%)": 0.48, + "总苏氨酸 (%)": 1.65, + "总蛋氨酸 (%)": 0.75, + "总赖氨酸 (%)": 2.10, + "有效磷 (%)": 0.42, + "水分 (%)": 9, + "消化能 (kcal/kg)": 3200, + "硫甙 (μmol/g)": 15, + "粗灰分 (%)": 7.2, + "粗纤维 (%)": 10.5, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 37.5, + "蛋+胱氨酸 (%)": 1.55, + "钙 (%)": 0.72 + }, + "向日葵籽": { + "代谢能 (kcal/kg)": 3300, + "净能 (kcal/kg)": 2500, + "可消化色氨酸 (SID %)": 0.27, + "可消化苏氨酸 (SID %)": 0.71, + "可消化蛋氨酸 (SID %)": 0.45, + "可消化赖氨酸 (SID %)": 0.72, + "干物质 (%)": 92, + "总磷 (%)": 0.80, + "总色氨酸 (%)": 0.30, + "总苏氨酸 (%)": 0.80, + "总蛋氨酸 (%)": 0.50, + "总赖氨酸 (%)": 0.80, + "有效磷 (%)": 0.30, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3400, + "粗灰分 (%)": 4.0, + "粗纤维 (%)": 15.0, + "粗脂肪 (%)": 40.0, + "粗蛋白 (%)": 20.0, + "蛋+胱氨酸 (%)": 1.00, + "钙 (%)": 0.20 + }, + "啤酒糟干": { + "代谢能 (kcal/kg)": 2550, + "净能 (kcal/kg)": 1800, + "可消化色氨酸 (SID %)": 0.25, + "可消化苏氨酸 (SID %)": 0.80, + "可消化蛋氨酸 (SID %)": 0.40, + "可消化赖氨酸 (SID %)": 0.84, + "干物质 (%)": 92, + "总磷 (%)": 0.60, + "总色氨酸 (%)": 0.28, + "总苏氨酸 (%)": 0.90, + "总蛋氨酸 (%)": 0.45, + "总赖氨酸 (%)": 0.95, + "有效磷 (%)": 0.30, + "水分 (%)": 8, + "消化能 (kcal/kg)": 2650, + "粗灰分 (%)": 4.5, + "粗纤维 (%)": 14.0, + "粗脂肪 (%)": 6.5, + "粗蛋白 (%)": 25.0, + "蛋+胱氨酸 (%)": 1.20, + "钙 (%)": 0.30 + }, + "啤酒花渣": { + "代谢能 (kcal/kg)": 2300, + "净能 (kcal/kg)": 1600, + "可消化色氨酸 (SID %)": 0.27, + "可消化苏氨酸 (SID %)": 0.80, + "可消化蛋氨酸 (SID %)": 0.36, + "可消化赖氨酸 (SID %)": 0.88, + "干物质 (%)": 25, + "总磷 (%)": 0.50, + "总色氨酸 (%)": 0.30, + "总苏氨酸 (%)": 0.90, + "总蛋氨酸 (%)": 0.40, + "总赖氨酸 (%)": 1.00, + "有效磷 (%)": 0.20, + "水分 (%)": 75, + "消化能 (kcal/kg)": 2400, + "粗灰分 (%)": 10.0, + "粗纤维 (%)": 20.0, + "粗脂肪 (%)": 3.0, + "粗蛋白 (%)": 20.0, + "蛋+胱氨酸 (%)": 0.80, + "钙 (%)": 0.50 + }, + "国产鱼粉60": { + "代谢能 (kcal/kg)": 3400, + "净能 (kcal/kg)": 2600, + "可消化色氨酸 (SID %)": 0.57, + "可消化苏氨酸 (SID %)": 2.28, + "可消化蛋氨酸 (SID %)": 1.52, + "可消化赖氨酸 (SID %)": 4.18, + "干物质 (%)": 91, + "总磷 (%)": 3.80, + "总色氨酸 (%)": 0.60, + "总苏氨酸 (%)": 2.40, + "总蛋氨酸 (%)": 1.60, + "总赖氨酸 (%)": 4.40, + "挥发性盐基氮 (mg/100g)": 180, + "有效磷 (%)": 2.60, + "水分 (%)": 9, + "消化能 (kcal/kg)": 3600, + "粗灰分 (%)": 22.0, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 8.0, + "粗蛋白 (%)": 60.0, + "组胺 (mg/kg)": 1200, + "蛋+胱氨酸 (%)": 2.20, + "钙 (%)": 6.50 + }, + "土豆蛋白": { + "代谢能 (kcal/kg)": 3000, + "净能 (kcal/kg)": 2200, + "可消化色氨酸 (SID %)": 0.76, + "可消化苏氨酸 (SID %)": 3.33, + "可消化蛋氨酸 (SID %)": 1.71, + "可消化赖氨酸 (SID %)": 5.23, + "干物质 (%)": 92, + "总磷 (%)": 0.50, + "总色氨酸 (%)": 0.80, + "总苏氨酸 (%)": 3.50, + "总蛋氨酸 (%)": 1.80, + "总赖氨酸 (%)": 5.50, + "有效磷 (%)": 0.30, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3100, + "粗灰分 (%)": 5.0, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 80.0, + "蛋+胱氨酸 (%)": 3.00, + "钙 (%)": 0.10 + }, + "大豆油": { + "代谢能 (kcal/kg)": 8600, + "净能 (kcal/kg)": 7400, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 8800, + "粗脂肪 (%)": 99.9 + }, + "大豆粕44": { + "KOH蛋白溶解度 (%)": 78, + "代谢能 (kcal/kg)": 3260, + "净能 (kcal/kg)": 2230, + "可消化色氨酸 (SID %)": 0.53, + "可消化苏氨酸 (SID %)": 1.52, + "可消化蛋氨酸 (SID %)": 0.55, + "可消化赖氨酸 (SID %)": 2.44, + "大豆抗原蛋白-β-伴球蛋白 (mg/kg)": 9200, + "大豆抗原蛋白-球蛋白 (mg/kg)": 18500, + "寡糖-棉子糖+水苏糖 (%)": 5.8, + "干物质 (%)": 89, + "总磷 (%)": 0.65, + "总色氨酸 (%)": 0.60, + "总苏氨酸 (%)": 1.72, + "总蛋氨酸 (%)": 0.62, + "总赖氨酸 (%)": 2.76, + "有效磷 (%)": 0.25, + "植酸磷 (%)": 1.15, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3380, + "粗灰分 (%)": 6.2, + "粗纤维 (%)": 6.0, + "粗脂肪 (%)": 1.8, + "粗蛋白 (%)": 43.8, + "胰蛋白酶抑制因子 (TIU/mg)": 3.4, + "脲酶活性 (ΔpH)": 0.12, + "蛋+胱氨酸 (%)": 1.26, + "钙 (%)": 0.29, + "黄曲霉毒素B1 (μg/kg)": 6 + }, + "大豆粕46": { + "KOH蛋白溶解度 (%)": 82, + "代谢能 (kcal/kg)": 3360, + "净能 (kcal/kg)": 2320, + "可消化色氨酸 (SID %)": 0.57, + "可消化苏氨酸 (SID %)": 1.63, + "可消化蛋氨酸 (SID %)": 0.58, + "可消化赖氨酸 (SID %)": 2.61, + "大豆抗原蛋白-β-伴球蛋白 (mg/kg)": 6800, + "大豆抗原蛋白-球蛋白 (mg/kg)": 13800, + "寡糖-棉子糖+水苏糖 (%)": 5.5, + "干物质 (%)": 89, + "总磷 (%)": 0.68, + "总色氨酸 (%)": 0.64, + "总苏氨酸 (%)": 1.84, + "总蛋氨酸 (%)": 0.66, + "总赖氨酸 (%)": 2.95, + "有效磷 (%)": 0.27, + "植酸磷 (%)": 1.18, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3480, + "粗灰分 (%)": 6.3, + "粗纤维 (%)": 5.5, + "粗脂肪 (%)": 1.5, + "粗蛋白 (%)": 46.8, + "胰蛋白酶抑制因子 (TIU/mg)": 2.7, + "脲酶活性 (ΔpH)": 0.09, + "蛋+胱氨酸 (%)": 1.34, + "钙 (%)": 0.30, + "黄曲霉毒素B1 (μg/kg)": 5 + }, + "大豆粕48": { + "KOH蛋白溶解度 (%)": 86, + "代谢能 (kcal/kg)": 3420, + "净能 (kcal/kg)": 2380, + "可消化色氨酸 (SID %)": 0.58, + "可消化苏氨酸 (SID %)": 1.68, + "可消化蛋氨酸 (SID %)": 0.60, + "可消化赖氨酸 (SID %)": 2.70, + "大豆抗原蛋白-β-伴球蛋白 (mg/kg)": 4200, + "大豆抗原蛋白-球蛋白 (mg/kg)": 8500, + "寡糖-棉子糖+水苏糖 (%)": 5.2, + "干物质 (%)": 89, + "总磷 (%)": 0.70, + "总色氨酸 (%)": 0.66, + "总苏氨酸 (%)": 1.90, + "总蛋氨酸 (%)": 0.68, + "总赖氨酸 (%)": 3.05, + "有效磷 (%)": 0.29, + "植酸磷 (%)": 1.20, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3550, + "粗灰分 (%)": 6.4, + "粗纤维 (%)": 5.2, + "粗脂肪 (%)": 1.2, + "粗蛋白 (%)": 48.5, + "胰蛋白酶抑制因子 (TIU/mg)": 2.1, + "脲酶活性 (ΔpH)": 0.06, + "蛋+胱氨酸 (%)": 1.38, + "钙 (%)": 0.31, + "黄曲霉毒素B1 (μg/kg)": 4 + }, + "大麦": { + "代谢能 (kcal/kg)": 3050, + "净能 (kcal/kg)": 2350, + "可消化色氨酸 (SID %)": 0.12, + "可消化苏氨酸 (SID %)": 0.35, + "可消化蛋氨酸 (SID %)": 0.17, + "可消化赖氨酸 (SID %)": 0.35, + "呕吐毒素DON (μg/kg)": 1000, + "干物质 (%)": 89, + "总磷 (%)": 0.38, + "总色氨酸 (%)": 0.14, + "总苏氨酸 (%)": 0.40, + "总蛋氨酸 (%)": 0.19, + "总赖氨酸 (%)": 0.40, + "有效磷 (%)": 0.13, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3150, + "粗灰分 (%)": 2.5, + "粗纤维 (%)": 5.5, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 11.2, + "蛋+胱氨酸 (%)": 0.42, + "钙 (%)": 0.06, + "黄曲霉毒素B1 (μg/kg)": 15 + }, + "小苏打": { + "Na": 27.0, + "干物质 (%)": 100, + "水分 (%)": 0 + }, + "小麦": { + "代谢能 (kcal/kg)": 3200, + "净能 (kcal/kg)": 2480, + "可消化色氨酸 (SID %)": 0.14, + "可消化苏氨酸 (SID %)": 0.37, + "可消化蛋氨酸 (SID %)": 0.18, + "可消化赖氨酸 (SID %)": 0.31, + "呕吐毒素DON (μg/kg)": 1200, + "干物质 (%)": 89, + "总磷 (%)": 0.38, + "总色氨酸 (%)": 0.16, + "总苏氨酸 (%)": 0.42, + "总蛋氨酸 (%)": 0.20, + "总赖氨酸 (%)": 0.35, + "有效磷 (%)": 0.12, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3310, + "玉米赤霉烯酮ZEN (μg/kg)": 200, + "粗灰分 (%)": 1.8, + "粗纤维 (%)": 2.8, + "粗脂肪 (%)": 1.9, + "粗蛋白 (%)": 12.5, + "蛋+胱氨酸 (%)": 0.48, + "钙 (%)": 0.05, + "黄曲霉毒素B1 (μg/kg)": 10 + }, + "小麦次粉": { + "代谢能 (kcal/kg)": 3050, + "净能 (kcal/kg)": 2300, + "可消化色氨酸 (SID %)": 0.17, + "可消化苏氨酸 (SID %)": 0.46, + "可消化蛋氨酸 (SID %)": 0.22, + "可消化赖氨酸 (SID %)": 0.42, + "呕吐毒素DON (μg/kg)": 2500, + "干物质 (%)": 88, + "总磷 (%)": 0.75, + "总色氨酸 (%)": 0.19, + "总苏氨酸 (%)": 0.52, + "总蛋氨酸 (%)": 0.25, + "总赖氨酸 (%)": 0.48, + "有效磷 (%)": 0.28, + "水分 (%)": 12, + "消化能 (kcal/kg)": 3150, + "玉米赤霉烯酮ZEN (μg/kg)": 400, + "粗灰分 (%)": 3.5, + "粗纤维 (%)": 5.0, + "粗脂肪 (%)": 3.0, + "粗蛋白 (%)": 15.8, + "蛋+胱氨酸 (%)": 0.58, + "钙 (%)": 0.10, + "黄曲霉毒素B1 (μg/kg)": 20 + }, + "小麦麸": { + "代谢能 (kcal/kg)": 2350, + "净能 (kcal/kg)": 1650, + "可消化色氨酸 (SID %)": 0.19, + "可消化苏氨酸 (SID %)": 0.53, + "可消化蛋氨酸 (SID %)": 0.21, + "可消化赖氨酸 (SID %)": 0.51, + "呕吐毒素DON (μg/kg)": 3000, + "干物质 (%)": 89, + "总磷 (%)": 1.15, + "总色氨酸 (%)": 0.22, + "总苏氨酸 (%)": 0.60, + "总蛋氨酸 (%)": 0.24, + "总赖氨酸 (%)": 0.58, + "有效磷 (%)": 0.35, + "水分 (%)": 11, + "消化能 (kcal/kg)": 2450, + "玉米赤霉烯酮ZEN (μg/kg)": 500, + "粗灰分 (%)": 5.8, + "粗纤维 (%)": 10.0, + "粗脂肪 (%)": 3.9, + "粗蛋白 (%)": 15.5, + "蛋+胱氨酸 (%)": 0.55, + "钙 (%)": 0.13, + "黄曲霉毒素B1 (μg/kg)": 25 + }, + "木薯干": { + "代谢能 (kcal/kg)": 3300, + "净能 (kcal/kg)": 2550, + "可消化色氨酸 (SID %)": 0.03, + "可消化苏氨酸 (SID %)": 0.08, + "可消化蛋氨酸 (SID %)": 0.04, + "可消化赖氨酸 (SID %)": 0.09, + "干物质 (%)": 90, + "总磷 (%)": 0.35, + "总色氨酸 (%)": 0.03, + "总苏氨酸 (%)": 0.09, + "总蛋氨酸 (%)": 0.05, + "总赖氨酸 (%)": 0.10, + "有效磷 (%)": 0.12, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3400, + "粗灰分 (%)": 3.0, + "粗纤维 (%)": 4.0, + "粗脂肪 (%)": 0.8, + "粗蛋白 (%)": 2.8, + "蛋+胱氨酸 (%)": 0.12, + "钙 (%)": 0.15 + }, + "杂交构树叶粉": { + "代谢能 (kcal/kg)": 2350, + "净能 (kcal/kg)": 1680, + "单宁 (mg/kg)": 18000, + "可消化色氨酸 (SID %)": 0.25, + "可消化苏氨酸 (SID %)": 0.80, + "可消化蛋氨酸 (SID %)": 0.34, + "可消化赖氨酸 (SID %)": 0.89, + "干物质 (%)": 91.5, + "总磷 (%)": 0.32, + "总色氨酸 (%)": 0.28, + "总苏氨酸 (%)": 0.92, + "总蛋氨酸 (%)": 0.38, + "总赖氨酸 (%)": 1.05, + "总黄酮 (mg/kg)": 12000, + "有效磷 (%)": 0.18, + "氢氰酸 (mg/kg)": 0, + "水分 (%)": 8.5, + "消化能 (kcal/kg)": 2480, + "生物碱 (mg/kg)": 800, + "粗灰分 (%)": 11.2, + "粗纤维 (%)": 16.5, + "粗脂肪 (%)": 4.2, + "粗蛋白 (%)": 21.8, + "绿原酸 (mg/kg)": 4500, + "草酸 (mg/kg)": 6000, + "蛋+胱氨酸 (%)": 0.68, + "钙 (%)": 2.10 + }, + "构树叶粉(老叶高纤维)": { + "代谢能 (kcal/kg)": 2050, + "净能 (kcal/kg)": 1450, + "单宁 (mg/kg)": 25000, + "可消化色氨酸 (SID %)": 0.19, + "可消化苏氨酸 (SID %)": 0.64, + "可消化蛋氨酸 (SID %)": 0.27, + "可消化赖氨酸 (SID %)": 0.70, + "干物质 (%)": 92.0, + "总磷 (%)": 0.28, + "总色氨酸 (%)": 0.22, + "总苏氨酸 (%)": 0.73, + "总蛋氨酸 (%)": 0.30, + "总赖氨酸 (%)": 0.82, + "有效磷 (%)": 0.15, + "水分 (%)": 8.0, + "消化能 (kcal/kg)": 2150, + "粗灰分 (%)": 12.5, + "粗纤维 (%)": 22.0, + "粗脂肪 (%)": 3.5, + "粗蛋白 (%)": 17.5, + "绿原酸 (mg/kg)": 6000, + "草酸 (mg/kg)": 9000, + "蛋+胱氨酸 (%)": 0.55, + "钙 (%)": 2.30 + }, + "柠檬酸渣": { + "代谢能 (kcal/kg)": 2700, + "净能 (kcal/kg)": 1950, + "可消化色氨酸 (SID %)": 0.13, + "可消化苏氨酸 (SID %)": 0.49, + "可消化蛋氨酸 (SID %)": 0.22, + "可消化赖氨酸 (SID %)": 0.53, + "干物质 (%)": 90, + "总磷 (%)": 0.40, + "总色氨酸 (%)": 0.15, + "总苏氨酸 (%)": 0.55, + "总蛋氨酸 (%)": 0.25, + "总赖氨酸 (%)": 0.60, + "有效磷 (%)": 0.15, + "水分 (%)": 10, + "消化能 (kcal/kg)": 2800, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 12.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 10.0, + "蛋+胱氨酸 (%)": 0.50, + "钙 (%)": 0.30 + }, + "棉籽粕": { + "代谢能 (kcal/kg)": 2750, + "净能 (kcal/kg)": 1900, + "可消化色氨酸 (SID %)": 0.37, + "可消化苏氨酸 (SID %)": 1.19, + "可消化蛋氨酸 (SID %)": 0.51, + "可消化赖氨酸 (SID %)": 1.50, + "干物质 (%)": 92, + "总棉酚 (mg/kg)": 9000, + "总磷 (%)": 1.00, + "总色氨酸 (%)": 0.42, + "总苏氨酸 (%)": 1.35, + "总蛋氨酸 (%)": 0.58, + "总赖氨酸 (%)": 1.70, + "有效磷 (%)": 0.30, + "水分 (%)": 8, + "消化能 (kcal/kg)": 2850, + "游离棉酚 (mg/kg)": 800, + "环丙烯酸 (mg/kg)": 300, + "粗灰分 (%)": 6.5, + "粗纤维 (%)": 12.0, + "粗脂肪 (%)": 2.5, + "粗蛋白 (%)": 41.0, + "蛋+胱氨酸 (%)": 1.15, + "钙 (%)": 0.20, + "黄曲霉毒素B1 (μg/kg)": 50 + }, + "棕榈油": { + "代谢能 (kcal/kg)": 8600, + "净能 (kcal/kg)": 7400, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 8800, + "粗脂肪 (%)": 99.9 + }, + "棕榈粕": { + "代谢能 (kcal/kg)": 2550, + "净能 (kcal/kg)": 1800, + "可消化色氨酸 (SID %)": 0.14, + "可消化苏氨酸 (SID %)": 0.49, + "可消化蛋氨酸 (SID %)": 0.22, + "可消化赖氨酸 (SID %)": 0.40, + "干物质 (%)": 92, + "总磷 (%)": 0.60, + "总色氨酸 (%)": 0.16, + "总苏氨酸 (%)": 0.55, + "总蛋氨酸 (%)": 0.25, + "总赖氨酸 (%)": 0.45, + "有效磷 (%)": 0.20, + "水分 (%)": 8, + "消化能 (kcal/kg)": 2650, + "粗灰分 (%)": 6.0, + "粗纤维 (%)": 15.0, + "粗脂肪 (%)": 8.0, + "粗蛋白 (%)": 15.5, + "蛋+胱氨酸 (%)": 0.55, + "钙 (%)": 0.25 + }, + "椰子粕": { + "代谢能 (kcal/kg)": 3100, + "净能 (kcal/kg)": 2300, + "可消化色氨酸 (SID %)": 0.19, + "可消化苏氨酸 (SID %)": 0.62, + "可消化蛋氨酸 (SID %)": 0.31, + "可消化赖氨酸 (SID %)": 0.57, + "干物质 (%)": 92, + "总磷 (%)": 0.65, + "总色氨酸 (%)": 0.22, + "总苏氨酸 (%)": 0.70, + "总蛋氨酸 (%)": 0.35, + "总赖氨酸 (%)": 0.65, + "有效磷 (%)": 0.25, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3200, + "粗灰分 (%)": 6.5, + "粗纤维 (%)": 12.0, + "粗脂肪 (%)": 8.5, + "粗蛋白 (%)": 20.5, + "蛋+胱氨酸 (%)": 0.75, + "钙 (%)": 0.15 + }, + "燕麦": { + "代谢能 (kcal/kg)": 2950, + "净能 (kcal/kg)": 2250, + "可消化色氨酸 (SID %)": 0.15, + "可消化苏氨酸 (SID %)": 0.40, + "可消化蛋氨酸 (SID %)": 0.20, + "可消化赖氨酸 (SID %)": 0.42, + "呕吐毒素DON (μg/kg)": 800, + "干物质 (%)": 90, + "总磷 (%)": 0.40, + "总色氨酸 (%)": 0.17, + "总苏氨酸 (%)": 0.45, + "总蛋氨酸 (%)": 0.22, + "总赖氨酸 (%)": 0.48, + "有效磷 (%)": 0.14, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3050, + "粗灰分 (%)": 3.0, + "粗纤维 (%)": 3.0, + "粗脂肪 (%)": 6.5, + "粗蛋白 (%)": 12.0, + "蛋+胱氨酸 (%)": 0.48, + "钙 (%)": 0.08, + "黄曲霉毒素B1 (μg/kg)": 10 + }, + "燕麦草": { + "代谢能 (kcal/kg)": 2050, + "净能 (kcal/kg)": 1350, + "可消化色氨酸 (SID %)": 0.09, + "可消化苏氨酸 (SID %)": 0.28, + "可消化蛋氨酸 (SID %)": 0.13, + "可消化赖氨酸 (SID %)": 0.31, + "干物质 (%)": 25, + "总磷 (%)": 0.25, + "总色氨酸 (%)": 0.10, + "总苏氨酸 (%)": 0.32, + "总蛋氨酸 (%)": 0.15, + "总赖氨酸 (%)": 0.35, + "有效磷 (%)": 0.08, + "水分 (%)": 75, + "消化能 (kcal/kg)": 2150, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 32.0, + "粗脂肪 (%)": 2.5, + "粗蛋白 (%)": 9.0, + "蛋+胱氨酸 (%)": 0.35, + "钙 (%)": 0.35 + }, + "猪肺粉": { + "代谢能 (kcal/kg)": 3450, + "净能 (kcal/kg)": 2700, + "可消化色氨酸 (SID %)": 0.66, + "可消化苏氨酸 (SID %)": 2.85, + "可消化蛋氨酸 (SID %)": 1.43, + "可消化赖氨酸 (SID %)": 4.75, + "干物质 (%)": 92, + "总磷 (%)": 1.50, + "总色氨酸 (%)": 0.70, + "总苏氨酸 (%)": 3.00, + "总蛋氨酸 (%)": 1.50, + "总赖氨酸 (%)": 5.00, + "有效磷 (%)": 1.20, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3600, + "粗灰分 (%)": 10.0, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 15.0, + "粗蛋白 (%)": 70.0, + "蛋+胱氨酸 (%)": 2.50, + "钙 (%)": 1.00 + }, + "玉米": { + "T-2/HT-2毒素 (μg/kg)": 50, + "代谢能 (kcal/kg)": 3340, + "净能 (kcal/kg)": 2590, + "单宁 (mg/kg)": 800, + "可消化色氨酸 (SID %)": 0.06, + "可消化苏氨酸 (SID %)": 0.27, + "可消化蛋氨酸 (SID %)": 0.16, + "可消化赖氨酸 (SID %)": 0.22, + "呕吐毒素DON (μg/kg)": 800, + "噁唑烷硫酮 (μmol/g)": 0, + "干物质 (%)": 88, + "异硫氰酸酯 (μmol/g)": 0, + "总磷 (%)": 0.28, + "总色氨酸 (%)": 0.07, + "总苏氨酸 (%)": 0.31, + "总蛋氨酸 (%)": 0.18, + "总赖氨酸 (%)": 0.25, + "有效磷 (%)": 0.08, + "水分 (%)": 12, + "消化能 (kcal/kg)": 3450, + "游离棉酚 (mg/kg)": 0, + "玉米赤霉烯酮ZEN (μg/kg)": 120, + "硫甙 (μmol/g)": 0, + "粗灰分 (%)": 1.3, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 3.8, + "粗蛋白 (%)": 8.2, + "胰蛋白酶抑制因子 (TIU/mg)": 0, + "脲酶活性 (ΔpH)": 0, + "蛋+胱氨酸 (%)": 0.38, + "钙 (%)": 0.02, + "黄曲霉毒素B1 (μg/kg)": 15 + }, + "玉米DDGS": { + "代谢能 (kcal/kg)": 3300, + "净能 (kcal/kg)": 2400, + "可消化色氨酸 (SID %)": 0.18, + "可消化苏氨酸 (SID %)": 0.88, + "可消化蛋氨酸 (SID %)": 0.46, + "可消化赖氨酸 (SID %)": 0.72, + "呕吐毒素DON (μg/kg)": 1500, + "干物质 (%)": 91, + "总磷 (%)": 0.82, + "总色氨酸 (%)": 0.20, + "总苏氨酸 (%)": 1.00, + "总蛋氨酸 (%)": 0.52, + "总赖氨酸 (%)": 0.82, + "有效磷 (%)": 0.55, + "水分 (%)": 9, + "消化能 (kcal/kg)": 3450, + "玉米赤霉烯酮ZEN (μg/kg)": 300, + "硫酸盐 (mg/kg)": 6000, + "粗灰分 (%)": 4.8, + "粗纤维 (%)": 8.0, + "粗脂肪 (%)": 9.5, + "粗蛋白 (%)": 26.5, + "蛋+胱氨酸 (%)": 1.12, + "钙 (%)": 0.08, + "黄曲霉毒素B1 (μg/kg)": 30 + }, + "玉米油": { + "代谢能 (kcal/kg)": 8600, + "净能 (kcal/kg)": 7400, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 8800, + "粗脂肪 (%)": 99.9 + }, + "玉米胚芽粕": { + "代谢能 (kcal/kg)": 3200, + "净能 (kcal/kg)": 2350, + "可消化色氨酸 (SID %)": 0.16, + "可消化苏氨酸 (SID %)": 0.71, + "可消化蛋氨酸 (SID %)": 0.40, + "可消化赖氨酸 (SID %)": 0.79, + "干物质 (%)": 90, + "总磷 (%)": 0.75, + "总色氨酸 (%)": 0.18, + "总苏氨酸 (%)": 0.80, + "总蛋氨酸 (%)": 0.45, + "总赖氨酸 (%)": 0.90, + "有效磷 (%)": 0.40, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3350, + "粗灰分 (%)": 4.0, + "粗纤维 (%)": 8.5, + "粗脂肪 (%)": 8.0, + "粗蛋白 (%)": 20.0, + "蛋+胱氨酸 (%)": 0.95, + "钙 (%)": 0.05 + }, + "玉米蛋白粉60": { + "代谢能 (kcal/kg)": 3950, + "净能 (kcal/kg)": 3000, + "可消化色氨酸 (SID %)": 0.27, + "可消化苏氨酸 (SID %)": 1.77, + "可消化蛋氨酸 (SID %)": 1.16, + "可消化赖氨酸 (SID %)": 0.92, + "干物质 (%)": 91, + "总磷 (%)": 0.50, + "总色氨酸 (%)": 0.30, + "总苏氨酸 (%)": 2.00, + "总蛋氨酸 (%)": 1.30, + "总赖氨酸 (%)": 1.05, + "有效磷 (%)": 0.20, + "水分 (%)": 9, + "消化能 (kcal/kg)": 4100, + "粗灰分 (%)": 2.0, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 2.5, + "粗蛋白 (%)": 62.0, + "蛋+胱氨酸 (%)": 2.20, + "钙 (%)": 0.05 + }, + "玉米青贮": { + "代谢能 (kcal/kg)": 2750, + "净能 (kcal/kg)": 2000, + "可消化色氨酸 (SID %)": 0.05, + "可消化苏氨酸 (SID %)": 0.28, + "可消化蛋氨酸 (SID %)": 0.12, + "可消化赖氨酸 (SID %)": 0.26, + "干物质 (%)": 35, + "总磷 (%)": 0.25, + "总色氨酸 (%)": 0.06, + "总苏氨酸 (%)": 0.32, + "总蛋氨酸 (%)": 0.14, + "总赖氨酸 (%)": 0.30, + "有效磷 (%)": 0.10, + "水分 (%)": 65, + "消化能 (kcal/kg)": 2850, + "粗灰分 (%)": 5.0, + "粗纤维 (%)": 22.0, + "粗脂肪 (%)": 3.0, + "粗蛋白 (%)": 8.0, + "蛋+胱氨酸 (%)": 0.32, + "钙 (%)": 0.25 + }, + "瓜子粕": { + "代谢能 (kcal/kg)": 2900, + "净能 (kcal/kg)": 2100, + "可消化色氨酸 (SID %)": 0.36, + "可消化苏氨酸 (SID %)": 1.06, + "可消化蛋氨酸 (SID %)": 0.54, + "可消化赖氨酸 (SID %)": 1.06, + "干物质 (%)": 90, + "总磷 (%)": 0.70, + "总色氨酸 (%)": 0.40, + "总苏氨酸 (%)": 1.20, + "总蛋氨酸 (%)": 0.60, + "总赖氨酸 (%)": 1.20, + "有效磷 (%)": 0.25, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3000, + "粗灰分 (%)": 6.0, + "粗纤维 (%)": 10.0, + "粗脂肪 (%)": 5.0, + "粗蛋白 (%)": 35.0, + "蛋+胱氨酸 (%)": 1.20, + "钙 (%)": 0.25 + }, + "甜菜粕": { + "代谢能 (kcal/kg)": 2750, + "净能 (kcal/kg)": 2000, + "可消化色氨酸 (SID %)": 0.09, + "可消化苏氨酸 (SID %)": 0.40, + "可消化蛋氨酸 (SID %)": 0.13, + "可消化赖氨酸 (SID %)": 0.48, + "干物质 (%)": 91, + "总磷 (%)": 0.12, + "总色氨酸 (%)": 0.10, + "总苏氨酸 (%)": 0.45, + "总蛋氨酸 (%)": 0.15, + "总赖氨酸 (%)": 0.55, + "有效磷 (%)": 0.05, + "水分 (%)": 9, + "消化能 (kcal/kg)": 2850, + "粗灰分 (%)": 7.0, + "粗纤维 (%)": 18.0, + "粗脂肪 (%)": 0.8, + "粗蛋白 (%)": 9.5, + "蛋+胱氨酸 (%)": 0.40, + "钙 (%)": 0.80 + }, + "石粉": { + "干物质 (%)": 100, + "总磷 (%)": 0.02, + "有效磷 (%)": 0.01, + "水分 (%)": 0, + "钙 (%)": 38.0 + }, + "碎米": { + "代谢能 (kcal/kg)": 3380, + "净能 (kcal/kg)": 2650, + "可消化色氨酸 (SID %)": 0.08, + "可消化苏氨酸 (SID %)": 0.26, + "可消化蛋氨酸 (SID %)": 0.18, + "可消化赖氨酸 (SID %)": 0.25, + "呕吐毒素DON (μg/kg)": 600, + "干物质 (%)": 88, + "总磷 (%)": 0.25, + "总色氨酸 (%)": 0.09, + "总苏氨酸 (%)": 0.30, + "总蛋氨酸 (%)": 0.20, + "总赖氨酸 (%)": 0.28, + "有效磷 (%)": 0.07, + "水分 (%)": 12, + "消化能 (kcal/kg)": 3500, + "粗灰分 (%)": 1.2, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 1.5, + "粗蛋白 (%)": 7.8, + "蛋+胱氨酸 (%)": 0.40, + "钙 (%)": 0.03, + "黄曲霉毒素B1 (μg/kg)": 30 + }, + "磷酸氢钙": { + "干物质 (%)": 100, + "总磷 (%)": 18.5, + "有效磷 (%)": 17.0, + "水分 (%)": 0, + "钙 (%)": 21.0 + }, + "稻草粉": { + "代谢能 (kcal/kg)": 1700, + "净能 (kcal/kg)": 1200, + "可消化色氨酸 (SID %)": 0.03, + "可消化苏氨酸 (SID %)": 0.13, + "可消化蛋氨酸 (SID %)": 0.04, + "可消化赖氨酸 (SID %)": 0.13, + "干物质 (%)": 92, + "总磷 (%)": 0.10, + "总色氨酸 (%)": 0.04, + "总苏氨酸 (%)": 0.15, + "总蛋氨酸 (%)": 0.05, + "总赖氨酸 (%)": 0.15, + "有效磷 (%)": 0.04, + "水分 (%)": 8, + "消化能 (kcal/kg)": 1800, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 40.0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 4.0, + "蛋+胱氨酸 (%)": 0.12, + "钙 (%)": 0.30 + }, + "稻谷": { + "代谢能 (kcal/kg)": 3100, + "净能 (kcal/kg)": 2400, + "可消化色氨酸 (SID %)": 0.09, + "可消化苏氨酸 (SID %)": 0.26, + "可消化蛋氨酸 (SID %)": 0.18, + "可消化赖氨酸 (SID %)": 0.27, + "干物质 (%)": 88, + "总磷 (%)": 0.50, + "总色氨酸 (%)": 0.10, + "总苏氨酸 (%)": 0.30, + "总蛋氨酸 (%)": 0.20, + "总赖氨酸 (%)": 0.30, + "有效磷 (%)": 0.15, + "水分 (%)": 12, + "消化能 (kcal/kg)": 3200, + "粗灰分 (%)": 5.0, + "粗纤维 (%)": 5.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 8.0, + "蛋+胱氨酸 (%)": 0.40, + "钙 (%)": 0.05 + }, + "稻谷糠": { + "代谢能 (kcal/kg)": 2800, + "净能 (kcal/kg)": 2050, + "可消化色氨酸 (SID %)": 0.18, + "可消化苏氨酸 (SID %)": 0.44, + "可消化蛋氨酸 (SID %)": 0.27, + "可消化赖氨酸 (SID %)": 0.44, + "干物质 (%)": 91, + "总磷 (%)": 1.50, + "总色氨酸 (%)": 0.20, + "总苏氨酸 (%)": 0.50, + "总蛋氨酸 (%)": 0.30, + "总赖氨酸 (%)": 0.50, + "有效磷 (%)": 0.45, + "水分 (%)": 9, + "消化能 (kcal/kg)": 2900, + "粗灰分 (%)": 10.0, + "粗纤维 (%)": 10.0, + "粗脂肪 (%)": 15.0, + "粗蛋白 (%)": 13.0, + "蛋+胱氨酸 (%)": 0.60, + "钙 (%)": 0.15 + }, + "米糠": { + "代谢能 (kcal/kg)": 2850, + "净能 (kcal/kg)": 2100, + "可消化色氨酸 (SID %)": 0.16, + "可消化苏氨酸 (SID %)": 0.49, + "可消化蛋氨酸 (SID %)": 0.25, + "可消化赖氨酸 (SID %)": 0.46, + "呕吐毒素DON (μg/kg)": 800, + "干物质 (%)": 91, + "总磷 (%)": 1.60, + "总色氨酸 (%)": 0.18, + "总苏氨酸 (%)": 0.55, + "总蛋氨酸 (%)": 0.28, + "总赖氨酸 (%)": 0.52, + "有效磷 (%)": 0.40, + "水分 (%)": 9, + "消化能 (kcal/kg)": 2950, + "玉米赤霉烯酮ZEN (μg/kg)": 150, + "粗灰分 (%)": 9.0, + "粗纤维 (%)": 11.0, + "粗脂肪 (%)": 14.0, + "粗蛋白 (%)": 13.5, + "蛋+胱氨酸 (%)": 0.60, + "钙 (%)": 0.10, + "黄曲霉毒素B1 (μg/kg)": 50 + }, + "米糠粕": { + "代谢能 (kcal/kg)": 2550, + "净能 (kcal/kg)": 1850, + "可消化色氨酸 (SID %)": 0.18, + "可消化苏氨酸 (SID %)": 0.53, + "可消化蛋氨酸 (SID %)": 0.27, + "可消化赖氨酸 (SID %)": 0.51, + "呕吐毒素DON (μg/kg)": 700, + "干物质 (%)": 92, + "总磷 (%)": 1.70, + "总色氨酸 (%)": 0.20, + "总苏氨酸 (%)": 0.60, + "总蛋氨酸 (%)": 0.30, + "总赖氨酸 (%)": 0.58, + "有效磷 (%)": 0.45, + "水分 (%)": 8, + "消化能 (kcal/kg)": 2650, + "粗灰分 (%)": 10.5, + "粗纤维 (%)": 13.0, + "粗脂肪 (%)": 3.0, + "粗蛋白 (%)": 15.0, + "蛋+胱氨酸 (%)": 0.65, + "钙 (%)": 0.12, + "黄曲霉毒素B1 (μg/kg)": 40 + }, + "红薯干": { + "代谢能 (kcal/kg)": 3100, + "净能 (kcal/kg)": 2350, + "可消化色氨酸 (SID %)": 0.04, + "可消化苏氨酸 (SID %)": 0.16, + "可消化蛋氨酸 (SID %)": 0.07, + "可消化赖氨酸 (SID %)": 0.13, + "干物质 (%)": 90, + "总磷 (%)": 0.20, + "总色氨酸 (%)": 0.05, + "总苏氨酸 (%)": 0.18, + "总蛋氨酸 (%)": 0.08, + "总赖氨酸 (%)": 0.15, + "有效磷 (%)": 0.08, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3200, + "粗灰分 (%)": 4.0, + "粗纤维 (%)": 5.0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 4.0, + "蛋+胱氨酸 (%)": 0.18, + "钙 (%)": 0.20 + }, + "肉粉": { + "代谢能 (kcal/kg)": 3200, + "净能 (kcal/kg)": 2400, + "可消化色氨酸 (SID %)": 0.33, + "可消化苏氨酸 (SID %)": 1.71, + "可消化蛋氨酸 (SID %)": 0.76, + "可消化赖氨酸 (SID %)": 2.85, + "干物质 (%)": 92, + "总磷 (%)": 4.50, + "总色氨酸 (%)": 0.35, + "总苏氨酸 (%)": 1.80, + "总蛋氨酸 (%)": 0.80, + "总赖氨酸 (%)": 3.00, + "有效磷 (%)": 3.20, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3400, + "粗灰分 (%)": 25.0, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 12.0, + "粗蛋白 (%)": 55.0, + "蛋+胱氨酸 (%)": 1.40, + "钙 (%)": 8.50 + }, + "肉骨粉50": { + "代谢能 (kcal/kg)": 3000, + "净能 (kcal/kg)": 2200, + "可消化色氨酸 (SID %)": 0.28, + "可消化苏氨酸 (SID %)": 1.57, + "可消化蛋氨酸 (SID %)": 0.71, + "可消化赖氨酸 (SID %)": 2.57, + "干物质 (%)": 93, + "总磷 (%)": 5.20, + "总色氨酸 (%)": 0.30, + "总苏氨酸 (%)": 1.65, + "总蛋氨酸 (%)": 0.75, + "总赖氨酸 (%)": 2.70, + "有效磷 (%)": 3.60, + "水分 (%)": 7, + "消化能 (kcal/kg)": 3200, + "粗灰分 (%)": 30.0, + "粗纤维 (%)": 2.5, + "粗脂肪 (%)": 10.0, + "粗蛋白 (%)": 50.0, + "蛋+胱氨酸 (%)": 1.35, + "钙 (%)": 10.0 + }, + "脱脂奶粉": { + "代谢能 (kcal/kg)": 3800, + "净能 (kcal/kg)": 3100, + "可消化色氨酸 (SID %)": 0.46, + "可消化苏氨酸 (SID %)": 1.38, + "可消化蛋氨酸 (SID %)": 0.81, + "可消化赖氨酸 (SID %)": 2.57, + "干物质 (%)": 94, + "总磷 (%)": 1.00, + "总色氨酸 (%)": 0.48, + "总苏氨酸 (%)": 1.45, + "总蛋氨酸 (%)": 0.85, + "总赖氨酸 (%)": 2.70, + "有效磷 (%)": 0.90, + "水分 (%)": 6, + "消化能 (kcal/kg)": 3980, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 34.0, + "蛋+胱氨酸 (%)": 1.50, + "钙 (%)": 1.25 + }, + "膨化全脂大豆": { + "KOH蛋白溶解度 (%)": 90, + "代谢能 (kcal/kg)": 3950, + "净能 (kcal/kg)": 3050, + "可消化色氨酸 (SID %)": 0.44, + "可消化苏氨酸 (SID %)": 1.33, + "可消化蛋氨酸 (SID %)": 0.46, + "可消化赖氨酸 (SID %)": 2.03, + "大豆抗原蛋白-β-伴球蛋白 (mg/kg)": 1500, + "大豆抗原蛋白-球蛋白 (mg/kg)": 3200, + "干物质 (%)": 90, + "总磷 (%)": 0.60, + "总色氨酸 (%)": 0.50, + "总苏氨酸 (%)": 1.50, + "总蛋氨酸 (%)": 0.52, + "总赖氨酸 (%)": 2.30, + "有效磷 (%)": 0.24, + "水分 (%)": 10, + "消化能 (kcal/kg)": 4150, + "粗灰分 (%)": 5.2, + "粗纤维 (%)": 5.0, + "粗脂肪 (%)": 18.5, + "粗蛋白 (%)": 36.8, + "胰蛋白酶抑制因子 (TIU/mg)": 1.4, + "脲酶活性 (ΔpH)": 0.03, + "蛋+胱氨酸 (%)": 1.05, + "钙 (%)": 0.25, + "黄曲霉毒素B1 (μg/kg)": 8 + }, + "芝麻粕": { + "代谢能 (kcal/kg)": 3200, + "净能 (kcal/kg)": 2250, + "可消化色氨酸 (SID %)": 0.49, + "可消化苏氨酸 (SID %)": 1.33, + "可消化蛋氨酸 (SID %)": 0.98, + "可消化赖氨酸 (SID %)": 1.15, + "干物质 (%)": 92, + "总磷 (%)": 1.30, + "总色氨酸 (%)": 0.55, + "总苏氨酸 (%)": 1.50, + "总蛋氨酸 (%)": 1.10, + "总赖氨酸 (%)": 1.30, + "有效磷 (%)": 0.45, + "植酸磷 (%)": 1.8, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3350, + "粗灰分 (%)": 12.0, + "粗纤维 (%)": 8.0, + "粗脂肪 (%)": 6.0, + "粗蛋白 (%)": 43.0, + "草酸 (mg/kg)": 8000, + "蛋+胱氨酸 (%)": 1.60, + "钙 (%)": 1.20 + }, + "花生秧粉": { + "代谢能 (kcal/kg)": 2350, + "净能 (kcal/kg)": 1650, + "可消化色氨酸 (SID %)": 0.12, + "可消化苏氨酸 (SID %)": 0.44, + "可消化蛋氨酸 (SID %)": 0.20, + "可消化赖氨酸 (SID %)": 0.48, + "干物质 (%)": 90, + "总磷 (%)": 0.30, + "总色氨酸 (%)": 0.14, + "总苏氨酸 (%)": 0.50, + "总蛋氨酸 (%)": 0.22, + "总赖氨酸 (%)": 0.55, + "有效磷 (%)": 0.12, + "水分 (%)": 10, + "消化能 (kcal/kg)": 2450, + "粗灰分 (%)": 7.5, + "粗纤维 (%)": 25.0, + "粗脂肪 (%)": 3.0, + "粗蛋白 (%)": 13.5, + "蛋+胱氨酸 (%)": 0.45, + "钙 (%)": 1.10 + }, + "花生粕": { + "代谢能 (kcal/kg)": 2950, + "净能 (kcal/kg)": 2050, + "可消化色氨酸 (SID %)": 0.42, + "可消化苏氨酸 (SID %)": 1.19, + "可消化蛋氨酸 (SID %)": 0.44, + "可消化赖氨酸 (SID %)": 1.46, + "呕吐毒素DON (μg/kg)": 500, + "干物质 (%)": 92, + "总磷 (%)": 0.60, + "总色氨酸 (%)": 0.48, + "总苏氨酸 (%)": 1.35, + "总蛋氨酸 (%)": 0.50, + "总赖氨酸 (%)": 1.65, + "有效磷 (%)": 0.22, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3050, + "粗灰分 (%)": 5.5, + "粗纤维 (%)": 10.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 46.0, + "蛋+胱氨酸 (%)": 1.10, + "钙 (%)": 0.20, + "黄曲霉毒素B1 (μg/kg)": 80 + }, + "苜蓿草块": { + "代谢能 (kcal/kg)": 2250, + "净能 (kcal/kg)": 1500, + "可消化色氨酸 (SID %)": 0.21, + "可消化苏氨酸 (SID %)": 0.66, + "可消化蛋氨酸 (SID %)": 0.27, + "可消化赖氨酸 (SID %)": 0.77, + "干物质 (%)": 90, + "总磷 (%)": 0.30, + "总色氨酸 (%)": 0.24, + "总苏氨酸 (%)": 0.75, + "总蛋氨酸 (%)": 0.30, + "总赖氨酸 (%)": 0.88, + "有效磷 (%)": 0.14, + "水分 (%)": 10, + "消化能 (kcal/kg)": 2350, + "粗灰分 (%)": 10.0, + "粗纤维 (%)": 26.0, + "粗脂肪 (%)": 2.5, + "粗蛋白 (%)": 18.5, + "蛋+胱氨酸 (%)": 0.58, + "钙 (%)": 1.50 + }, + "苜蓿草粉": { + "代谢能 (kcal/kg)": 2100, + "净能 (kcal/kg)": 1400, + "单宁 (mg/kg)": 5000, + "可消化色氨酸 (SID %)": 0.19, + "可消化苏氨酸 (SID %)": 0.62, + "可消化蛋氨酸 (SID %)": 0.25, + "可消化赖氨酸 (SID %)": 0.70, + "干物质 (%)": 92, + "总磷 (%)": 0.28, + "总色氨酸 (%)": 0.22, + "总苏氨酸 (%)": 0.70, + "总蛋氨酸 (%)": 0.28, + "总赖氨酸 (%)": 0.80, + "有效磷 (%)": 0.12, + "水分 (%)": 8, + "消化能 (kcal/kg)": 2200, + "皂苷 (mg/kg)": 4000, + "粗灰分 (%)": 9.0, + "粗纤维 (%)": 28.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 17.0, + "蛋+胱氨酸 (%)": 0.55, + "钙 (%)": 1.40, + "香豆素 (mg/kg)": 800 + }, + "苹果渣": { + "代谢能 (kcal/kg)": 2500, + "净能 (kcal/kg)": 1800, + "可消化色氨酸 (SID %)": 0.06, + "可消化苏氨酸 (SID %)": 0.20, + "可消化蛋氨酸 (SID %)": 0.09, + "可消化赖氨酸 (SID %)": 0.18, + "干物质 (%)": 25, + "总磷 (%)": 0.10, + "总色氨酸 (%)": 0.07, + "总苏氨酸 (%)": 0.22, + "总蛋氨酸 (%)": 0.10, + "总赖氨酸 (%)": 0.20, + "有效磷 (%)": 0.05, + "水分 (%)": 75, + "消化能 (kcal/kg)": 2600, + "粗灰分 (%)": 3.0, + "粗纤维 (%)": 15.0, + "粗脂肪 (%)": 1.5, + "粗蛋白 (%)": 4.5, + "蛋+胱氨酸 (%)": 0.25, + "钙 (%)": 0.15 + }, + "菜籽粕": { + "代谢能 (kcal/kg)": 2950, + "净能 (kcal/kg)": 2000, + "单宁 (mg/kg)": 12000, + "可消化色氨酸 (SID %)": 0.40, + "可消化苏氨酸 (SID %)": 1.41, + "可消化蛋氨酸 (SID %)": 0.64, + "可消化赖氨酸 (SID %)": 1.75, + "噁唑烷硫酮 (μmol/g)": 25, + "干物质 (%)": 91, + "异硫氰酸酯 (μmol/g)": 18, + "总磷 (%)": 1.10, + "总色氨酸 (%)": 0.45, + "总苏氨酸 (%)": 1.60, + "总蛋氨酸 (%)": 0.72, + "总赖氨酸 (%)": 1.98, + "有效磷 (%)": 0.38, + "水分 (%)": 9, + "消化能 (kcal/kg)": 3050, + "硫甙 (μmol/g)": 45, + "粗灰分 (%)": 7.0, + "粗纤维 (%)": 11.0, + "粗脂肪 (%)": 2.5, + "粗蛋白 (%)": 36.5, + "蛋+胱氨酸 (%)": 1.50, + "钙 (%)": 0.70, + "黄曲霉毒素B1 (μg/kg)": 15 + }, + "葡萄糖": { + "代谢能 (kcal/kg)": 3650, + "净能 (kcal/kg)": 3650, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 3650, + "粗蛋白 (%)": 0 + }, + "葵花籽粕": { + "代谢能 (kcal/kg)": 2450, + "净能 (kcal/kg)": 1700, + "单宁 (mg/kg)": 3000, + "可消化色氨酸 (SID %)": 0.34, + "可消化苏氨酸 (SID %)": 1.06, + "可消化蛋氨酸 (SID %)": 0.67, + "可消化赖氨酸 (SID %)": 1.01, + "干物质 (%)": 91, + "总磷 (%)": 1.00, + "总色氨酸 (%)": 0.38, + "总苏氨酸 (%)": 1.20, + "总蛋氨酸 (%)": 0.75, + "总赖氨酸 (%)": 1.15, + "有效磷 (%)": 0.32, + "水分 (%)": 9, + "消化能 (kcal/kg)": 2550, + "粗灰分 (%)": 7.0, + "粗纤维 (%)": 20.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 32.0, + "绿原酸 (mg/kg)": 1800, + "蛋+胱氨酸 (%)": 1.25, + "钙 (%)": 0.35 + }, + "蔗糖": { + "代谢能 (kcal/kg)": 3700, + "净能 (kcal/kg)": 3700, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 3700, + "粗蛋白 (%)": 0 + }, + "虾粉": { + "代谢能 (kcal/kg)": 3350, + "净能 (kcal/kg)": 2600, + "可消化色氨酸 (SID %)": 0.38, + "可消化苏氨酸 (SID %)": 2.09, + "可消化蛋氨酸 (SID %)": 1.14, + "可消化赖氨酸 (SID %)": 3.61, + "干物质 (%)": 92, + "总磷 (%)": 2.50, + "总色氨酸 (%)": 0.40, + "总苏氨酸 (%)": 2.20, + "总蛋氨酸 (%)": 1.20, + "总赖氨酸 (%)": 3.80, + "有效磷 (%)": 2.00, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3500, + "粗灰分 (%)": 20.0, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 5.0, + "粗蛋白 (%)": 50.0, + "蛋+胱氨酸 (%)": 2.00, + "钙 (%)": 5.00 + }, + "蚕蛹粉": { + "代谢能 (kcal/kg)": 3650, + "净能 (kcal/kg)": 2850, + "可消化色氨酸 (SID %)": 0.48, + "可消化苏氨酸 (SID %)": 1.90, + "可消化蛋氨酸 (SID %)": 0.86, + "可消化赖氨酸 (SID %)": 3.33, + "干物质 (%)": 95, + "总磷 (%)": 1.00, + "总色氨酸 (%)": 0.50, + "总苏氨酸 (%)": 2.00, + "总蛋氨酸 (%)": 0.90, + "总赖氨酸 (%)": 3.50, + "有效磷 (%)": 0.80, + "水分 (%)": 5, + "消化能 (kcal/kg)": 3800, + "粗灰分 (%)": 5.0, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 30.0, + "粗蛋白 (%)": 55.0, + "蛋+胱氨酸 (%)": 1.50, + "钙 (%)": 0.50 + }, + "蚕豆": { + "代谢能 (kcal/kg)": 3280, + "净能 (kcal/kg)": 2500, + "可消化色氨酸 (SID %)": 0.22, + "可消化苏氨酸 (SID %)": 0.84, + "可消化蛋氨酸 (SID %)": 0.18, + "可消化赖氨酸 (SID %)": 1.50, + "干物质 (%)": 89, + "总磷 (%)": 0.45, + "总色氨酸 (%)": 0.25, + "总苏氨酸 (%)": 0.95, + "总蛋氨酸 (%)": 0.20, + "总赖氨酸 (%)": 1.70, + "有效磷 (%)": 0.18, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3400, + "粗灰分 (%)": 3.5, + "粗纤维 (%)": 8.0, + "粗脂肪 (%)": 1.8, + "粗蛋白 (%)": 26.0, + "蛋+胱氨酸 (%)": 0.48, + "钙 (%)": 0.12 + }, + "蟹粉": { + "代谢能 (kcal/kg)": 3250, + "净能 (kcal/kg)": 2550, + "可消化色氨酸 (SID %)": 0.33, + "可消化苏氨酸 (SID %)": 1.90, + "可消化蛋氨酸 (SID %)": 0.95, + "可消化赖氨酸 (SID %)": 3.04, + "干物质 (%)": 93, + "总磷 (%)": 3.00, + "总色氨酸 (%)": 0.35, + "总苏氨酸 (%)": 2.00, + "总蛋氨酸 (%)": 1.00, + "总赖氨酸 (%)": 3.20, + "有效磷 (%)": 2.50, + "水分 (%)": 7, + "消化能 (kcal/kg)": 3400, + "粗灰分 (%)": 25.0, + "粗纤维 (%)": 3.0, + "粗脂肪 (%)": 4.0, + "粗蛋白 (%)": 45.0, + "蛋+胱氨酸 (%)": 1.80, + "钙 (%)": 8.00 + }, + "血浆蛋白粉": { + "代谢能 (kcal/kg)": 4350, + "免疫球蛋白IgG (%)": 18, + "净能 (kcal/kg)": 3400, + "可消化色氨酸 (SID %)": 1.04, + "可消化苏氨酸 (SID %)": 4.94, + "可消化蛋氨酸 (SID %)": 0.76, + "可消化赖氨酸 (SID %)": 6.55, + "干物质 (%)": 92, + "总磷 (%)": 1.30, + "总色氨酸 (%)": 1.10, + "总苏氨酸 (%)": 5.20, + "总蛋氨酸 (%)": 0.80, + "总赖氨酸 (%)": 6.90, + "有效磷 (%)": 1.10, + "水分 (%)": 8, + "消化能 (kcal/kg)": 4550, + "盐分 (%)": 2.5, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 0.5, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 78.0, + "蛋+胱氨酸 (%)": 1.60, + "钙 (%)": 0.20 + }, + "血粉": { + "代谢能 (kcal/kg)": 4000, + "净能 (kcal/kg)": 3100, + "可消化色氨酸 (SID %)": 1.14, + "可消化苏氨酸 (SID %)": 3.61, + "可消化蛋氨酸 (SID %)": 1.04, + "可消化赖氨酸 (SID %)": 7.13, + "干物质 (%)": 91, + "总磷 (%)": 0.40, + "总色氨酸 (%)": 1.20, + "总苏氨酸 (%)": 3.80, + "总蛋氨酸 (%)": 1.10, + "总赖氨酸 (%)": 7.50, + "有效磷 (%)": 0.35, + "水分 (%)": 9, + "消化能 (kcal/kg)": 4200, + "粗灰分 (%)": 4.0, + "粗纤维 (%)": 1.0, + "粗脂肪 (%)": 1.0, + "粗蛋白 (%)": 88.0, + "蛋+胱氨酸 (%)": 1.80, + "钙 (%)": 0.30 + }, + "豆磷脂": { + "代谢能 (kcal/kg)": 6300, + "净能 (kcal/kg)": 5500, + "干物质 (%)": 95, + "水分 (%)": 5, + "消化能 (kcal/kg)": 6500, + "粗脂肪 (%)": 50.0, + "粗蛋白 (%)": 12.0 + }, + "豌豆": { + "代谢能 (kcal/kg)": 3380, + "净能 (kcal/kg)": 2600, + "可消化色氨酸 (SID %)": 0.19, + "可消化苏氨酸 (SID %)": 0.75, + "可消化蛋氨酸 (SID %)": 0.22, + "可消化赖氨酸 (SID %)": 1.46, + "干物质 (%)": 88, + "总磷 (%)": 0.40, + "总色氨酸 (%)": 0.22, + "总苏氨酸 (%)": 0.85, + "总蛋氨酸 (%)": 0.25, + "总赖氨酸 (%)": 1.65, + "有效磷 (%)": 0.15, + "水分 (%)": 12, + "消化能 (kcal/kg)": 3500, + "粗灰分 (%)": 3.0, + "粗纤维 (%)": 6.0, + "粗脂肪 (%)": 1.5, + "粗蛋白 (%)": 22.5, + "蛋+胱氨酸 (%)": 0.55, + "钙 (%)": 0.10 + }, + "豌豆蛋白": { + "代谢能 (kcal/kg)": 3480, + "净能 (kcal/kg)": 2680, + "可消化色氨酸 (SID %)": 0.53, + "可消化苏氨酸 (SID %)": 2.13, + "可消化蛋氨酸 (SID %)": 0.72, + "可消化赖氨酸 (SID %)": 4.00, + "干物质 (%)": 90, + "总磷 (%)": 0.30, + "总色氨酸 (%)": 0.60, + "总苏氨酸 (%)": 2.40, + "总蛋氨酸 (%)": 0.80, + "总赖氨酸 (%)": 4.50, + "有效磷 (%)": 0.12, + "水分 (%)": 10, + "消化能 (kcal/kg)": 3600, + "粗灰分 (%)": 4.0, + "粗纤维 (%)": 4.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 65.0, + "蛋+胱氨酸 (%)": 1.60, + "钙 (%)": 0.10 + }, + "进口鱼粉65": { + "代谢能 (kcal/kg)": 3650, + "净能 (kcal/kg)": 2800, + "可消化色氨酸 (SID %)": 0.66, + "可消化苏氨酸 (SID %)": 2.56, + "可消化蛋氨酸 (SID %)": 1.75, + "可消化赖氨酸 (SID %)": 4.75, + "干物质 (%)": 92, + "总磷 (%)": 3.20, + "总色氨酸 (%)": 0.70, + "总苏氨酸 (%)": 2.70, + "总蛋氨酸 (%)": 1.85, + "总赖氨酸 (%)": 5.00, + "挥发性盐基氮 (mg/100g)": 120, + "有效磷 (%)": 2.40, + "水分 (%)": 8, + "沙门氏菌": 0, + "消化能 (kcal/kg)": 3850, + "粗灰分 (%)": 18.0, + "粗纤维 (%)": 0.5, + "粗脂肪 (%)": 9.0, + "粗蛋白 (%)": 65.0, + "组胺 (mg/kg)": 800, + "蛋+胱氨酸 (%)": 2.55, + "钙 (%)": 5.50 + }, + "食盐": { + "Cl": 60.0, + "Na": 39.0, + "干物质 (%)": 100, + "水分 (%)": 0 + }, + "饲料酵母粉": { + "代谢能 (kcal/kg)": 3450, + "净能 (kcal/kg)": 2650, + "可消化色氨酸 (SID %)": 0.52, + "可消化苏氨酸 (SID %)": 2.00, + "可消化蛋氨酸 (SID %)": 0.66, + "可消化赖氨酸 (SID %)": 3.04, + "干物质 (%)": 93, + "总磷 (%)": 1.40, + "总色氨酸 (%)": 0.55, + "总苏氨酸 (%)": 2.10, + "总蛋氨酸 (%)": 0.70, + "总赖氨酸 (%)": 3.20, + "有效磷 (%)": 1.00, + "水分 (%)": 7, + "消化能 (kcal/kg)": 3600, + "粗灰分 (%)": 8.0, + "粗纤维 (%)": 3.0, + "粗脂肪 (%)": 2.0, + "粗蛋白 (%)": 45.0, + "蛋+胱氨酸 (%)": 1.30, + "钙 (%)": 0.20 + }, + "高粱": { + "代谢能 (kcal/kg)": 3220, + "净能 (kcal/kg)": 2500, + "单宁 (mg/kg)": 8000, + "可消化色氨酸 (SID %)": 0.08, + "可消化苏氨酸 (SID %)": 0.25, + "可消化蛋氨酸 (SID %)": 0.14, + "可消化赖氨酸 (SID %)": 0.18, + "呕吐毒素DON (μg/kg)": 600, + "干物质 (%)": 89, + "总磷 (%)": 0.28, + "总色氨酸 (%)": 0.09, + "总苏氨酸 (%)": 0.29, + "总蛋氨酸 (%)": 0.16, + "总赖氨酸 (%)": 0.21, + "有效磷 (%)": 0.08, + "水分 (%)": 11, + "消化能 (kcal/kg)": 3320, + "游离棉酚 (mg/kg)": 0, + "玉米赤霉烯酮ZEN (μg/kg)": 80, + "粗灰分 (%)": 1.6, + "粗纤维 (%)": 2.3, + "粗脂肪 (%)": 3.2, + "粗蛋白 (%)": 9.0, + "蛋+胱氨酸 (%)": 0.35, + "钙 (%)": 0.03, + "黄曲霉毒素B1 (μg/kg)": 10 + }, + "鱼油": { + "代谢能 (kcal/kg)": 8800, + "净能 (kcal/kg)": 7600, + "干物质 (%)": 100, + "水分 (%)": 0, + "消化能 (kcal/kg)": 9000, + "粗脂肪 (%)": 99.9 + }, + "鸡肉粉": { + "代谢能 (kcal/kg)": 3550, + "净能 (kcal/kg)": 2800, + "可消化色氨酸 (SID %)": 0.57, + "可消化苏氨酸 (SID %)": 2.38, + "可消化蛋氨酸 (SID %)": 1.14, + "可消化赖氨酸 (SID %)": 3.80, + "干物质 (%)": 92, + "总磷 (%)": 2.00, + "总色氨酸 (%)": 0.60, + "总苏氨酸 (%)": 2.50, + "总蛋氨酸 (%)": 1.20, + "总赖氨酸 (%)": 4.00, + "有效磷 (%)": 1.50, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3700, + "粗灰分 (%)": 15.0, + "粗纤维 (%)": 1.5, + "粗脂肪 (%)": 12.0, + "粗蛋白 (%)": 65.0, + "蛋+胱氨酸 (%)": 2.00, + "钙 (%)": 3.00 + }, + "鸭肉粉": { + "代谢能 (kcal/kg)": 3500, + "净能 (kcal/kg)": 2750, + "可消化色氨酸 (SID %)": 0.52, + "可消化苏氨酸 (SID %)": 2.19, + "可消化蛋氨酸 (SID %)": 1.05, + "可消化赖氨酸 (SID %)": 3.52, + "干物质 (%)": 92, + "总磷 (%)": 1.80, + "总色氨酸 (%)": 0.55, + "总苏氨酸 (%)": 2.30, + "总蛋氨酸 (%)": 1.10, + "总赖氨酸 (%)": 3.70, + "有效磷 (%)": 1.40, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3650, + "粗灰分 (%)": 16.0, + "粗纤维 (%)": 2.0, + "粗脂肪 (%)": 14.0, + "粗蛋白 (%)": 60.0, + "蛋+胱氨酸 (%)": 1.90, + "钙 (%)": 2.50 + }, + "鹅肉粉": { + "代谢能 (kcal/kg)": 3470, + "净能 (kcal/kg)": 2720, + "可消化色氨酸 (SID %)": 0.55, + "可消化苏氨酸 (SID %)": 2.28, + "可消化蛋氨酸 (SID %)": 1.09, + "可消化赖氨酸 (SID %)": 3.61, + "干物质 (%)": 92, + "总磷 (%)": 1.90, + "总色氨酸 (%)": 0.58, + "总苏氨酸 (%)": 2.40, + "总蛋氨酸 (%)": 1.15, + "总赖氨酸 (%)": 3.80, + "有效磷 (%)": 1.45, + "水分 (%)": 8, + "消化能 (kcal/kg)": 3620, + "粗灰分 (%)": 14.0, + "粗纤维 (%)": 1.8, + "粗脂肪 (%)": 13.0, + "粗蛋白 (%)": 62.0, + "蛋+胱氨酸 (%)": 1.95, + "钙 (%)": 2.80 + } + } +} \ No newline at end of file diff --git a/config/presets-data/system_plans.json b/config/presets-data/system_plans.json new file mode 100644 index 0000000..e69de29 diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index f0a5cf9..ac0c8a0 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -45,3 +45,8 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 7. 简单查看功能 - 两个配方对比页面(营养+成本对比) + +# 完成事项 + +1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 +2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 \ No newline at end of file diff --git a/go.mod b/go.mod index 2e56398..34117dc 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,10 @@ 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 google.golang.org/protobuf v1.36.9 @@ -23,7 +24,6 @@ require ( 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 +31,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 +50,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 +76,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 +85,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 ) diff --git a/go.sum b/go.sum index 14ee566..a955b19 100644 --- a/go.sum +++ b/go.sum @@ -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,23 @@ 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= 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= diff --git a/internal/core/application.go b/internal/core/application.go index 2dff55d..5270b0a 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -14,9 +14,10 @@ import ( // Application 是整个应用的核心,封装了所有组件和生命周期。 type Application struct { - Config *config.Config - Ctx context.Context - API *api.API + cfgPath string + Config *config.Config + Ctx context.Context + API *api.API Infra *Infrastructure Domain *DomainServices @@ -68,12 +69,13 @@ func NewApplication(configPath string) (*Application, error) { // 4. 组装 Application 对象 app := &Application{ - Config: cfg, - Ctx: selfCtx, - API: apiServer, - Infra: infra, - Domain: domain, - App: appServices, + cfgPath: configPath, + Config: cfg, + Ctx: selfCtx, + API: apiServer, + Infra: infra, + Domain: domain, + App: appServices, } return app, nil @@ -90,7 +92,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) } diff --git a/internal/core/data_initializer.go b/internal/core/data_initializer.go index 9081022..644d411 100644 --- a/internal/core/data_initializer.go +++ b/internal/core/data_initializer.go @@ -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 { diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go new file mode 100644 index 0000000..8025bc9 --- /dev/null +++ b/internal/infra/database/seeder.go @@ -0,0 +1,203 @@ +package database + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + + "github.com/tidwall/gjson" + "gorm.io/gorm" +) + +// SeederFunc 定义了处理一种特定类型预设数据文件的函数签名。 +type SeederFunc func(tx *gorm.DB, jsonData []byte) error + +// SeedFromPreset 是一个通用的数据播种函数。 +// 它会读取指定目录下的所有 .json 文件,并根据文件内容中的 "type" 字段进行分发。 +// 同时,它会校验所有必需的预设类型是否都已成功加载。 +func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { + logger := logs.TraceLogger(ctx, ctx, "SeedFromPreset") + + // 定义必须存在的预设数据类型 + requiredTypes := []string{"nutrient"} + processedTypes := make(map[string]bool) + + // 用于检测重复的 type + typeToFileMap := make(map[string]string) + + files, err := os.ReadDir(presetDir) + if err != nil { + return fmt.Errorf("读取预设数据目录 '%s' 失败: %w", presetDir, err) + } + + return db.Transaction(func(tx *gorm.DB) error { + 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' 字段,已跳过\n", filePath) + continue + } + dataTypeStr := dataType.String() + + if existingFile, found := typeToFileMap[dataTypeStr]; found { + return fmt.Errorf("预设数据校验失败: type '%s' 在文件 '%s' 和 '%s' 中重复定义", dataTypeStr, existingFile, filePath) + } + typeToFileMap[dataTypeStr] = filePath + + var seederFunc SeederFunc + switch dataTypeStr { + case "nutrient": + seederFunc = seedNutrients + default: + logger.Warnf("警告: 文件 '%s' 中存在未知的 type: '%s',已跳过\n", filePath, dataTypeStr) + continue + } + + if err := seederFunc(tx, jsonData); err != nil { + return fmt.Errorf("处理文件 '%s' (type: %s) 时发生错误: %w", filePath, dataTypeStr, 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 // 提交事务 + }) +} + +// seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 +func seedNutrients(tx *gorm.DB, jsonData []byte) error { + // 1. 严格校验JSON文件,检查内部重复键 + parsedData, err := validateAndParseNutrientJSON(jsonData) + if err != nil { + return fmt.Errorf("JSON源文件校验失败: %w", err) + } + + // 2. 将通过校验的、干净的数据写入数据库 + for rawMaterialName, nutrients := range parsedData { + var rawMaterial models.RawMaterial + if err := tx.Where(models.RawMaterial{Name: rawMaterialName}).FirstOrCreate(&rawMaterial).Error; err != nil { + return fmt.Errorf("预设原料 '%s' 失败: %w", rawMaterialName, err) + } + + for nutrientName, value := range nutrients { + var nutrient models.Nutrient + if err := tx.Where(models.Nutrient{Name: nutrientName}).FirstOrCreate(&nutrient).Error; err != nil { + return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) + } + + linkData := models.RawMaterialNutrient{ + RawMaterialID: rawMaterial.ID, + NutrientID: nutrient.ID, + } + // 使用 FirstOrCreate 确保关联的唯一性 + if err := tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ + RawMaterialID: linkData.RawMaterialID, + NutrientID: linkData.NutrientID, + Value: value, + }).Error; err != nil { + return fmt.Errorf("为原料 '%s' 和营养素 '%s' 创建关联失败: %w", rawMaterialName, nutrientName, err) + } + } + } + return nil +} + +// validateAndParseNutrientJSON 使用 json.Decoder 手动解析,以捕获重复的键。 +func validateAndParseNutrientJSON(jsonData []byte) (map[string]map[string]float32, error) { + dataNode := gjson.GetBytes(jsonData, "data") + if !dataNode.Exists() { + return nil, errors.New("JSON文件中缺少 'data' 字段") + } + if !dataNode.IsObject() { + return nil, 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 nil, errors.New("'data' 字段解析起始符失败") + } + + result := make(map[string]map[string]float32) + seenRawMaterials := make(map[string]bool) + + for decoder.More() { + // 1. 解析原料名称 + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("解析原料名称失败: %w", err) + } + rawMaterialName := t.(string) + if seenRawMaterials[rawMaterialName] { + return nil, fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) + } + seenRawMaterials[rawMaterialName] = true + + // 2. 解析该原料的营养成分对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return nil, fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) + } + + nutrients := make(map[string]float32) + seenNutrients := make(map[string]bool) + for decoder.More() { + // 解析营养素名称 + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) + } + nutrientName := t.(string) + if seenNutrients[nutrientName] { + return nil, fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) + } + seenNutrients[nutrientName] = true + + // 解析营养素含量 + t, err = decoder.Token() + if err != nil { + return nil, fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) + } + if value, ok := t.(json.Number); ok { + f64, _ := value.Float64() + nutrients[nutrientName] = float32(f64) + } else { + return nil, fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t) + } + } + // 读取营养成分对象的 "}" + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return nil, fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) + } + result[rawMaterialName] = nutrients + } + return result, nil +} diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go index d138231..b992cb4 100644 --- a/internal/infra/models/raw_material.go +++ b/internal/infra/models/raw_material.go @@ -18,14 +18,6 @@ const ( StockLogSourceFermentEnd StockLogSourceType = "发酵入库" // 发酵料产出,作为新原料计入库存 ) -// NutrientType 定义了营养素的分类,用于配方优化和成本控制。 -type NutrientType string - -const ( - PositiveNutrient NutrientType = "正面营养" // 希望在配方中最大化的营养素,如蛋白质、能量 - NegativeNutrient NutrientType = "负面营养" // 需要控制上限的营养素,如粗纤维、霉菌毒素 -) - // RawMaterial 代表一种原料的静态定义,是系统中的原料字典。 type RawMaterial struct { Model @@ -45,9 +37,8 @@ func (RawMaterial) TableName() string { // 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。 type Nutrient struct { Model - Name string `gorm:"size:100;unique;not null;comment:营养素名称"` - Type NutrientType `gorm:"size:50;not null;comment:营养素类型 (正面营养/负面营养)"` - Description string `gorm:"size:255;comment:描述"` + Name string `gorm:"size:100;unique;not null;comment:营养素名称"` + Description string `gorm:"size:255;comment:描述"` } func (Nutrient) TableName() string { -- 2.49.1 From 365d69e0c63bd396b1a999f327738e43ddb8a4ca Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 19 Nov 2025 19:58:09 +0800 Subject: [PATCH 04/59] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E5=8E=9F=E6=96=99=E5=92=8C=E8=90=A5=E5=85=BB=E7=AE=80=E4=BB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 148 +++++++++++++++++++++++++++++- internal/infra/database/seeder.go | 38 +++++++- 2 files changed, 178 insertions(+), 8 deletions(-) diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index 672f3f1..e589b34 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -368,7 +368,7 @@ "黄曲霉毒素B1 (μg/kg)": 15 }, "小苏打": { - "Na": 27.0, + "钠 (%)": 27.0, "干物质 (%)": 100, "水分 (%)": 0 }, @@ -1603,8 +1603,8 @@ "钙 (%)": 5.50 }, "食盐": { - "Cl": 60.0, - "Na": 39.0, + "氯 (%)": 60.0, + "钠 (%)": 39.0, "干物质 (%)": 100, "水分 (%)": 0 }, @@ -1736,5 +1736,147 @@ "蛋+胱氨酸 (%)": 1.95, "钙 (%)": 2.80 } + }, + "descriptions": { + "raw_materials": { + "DL-蛋氨酸98": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", + "L-色氨酸98": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", + "L-苏氨酸98": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", + "L-赖氨酸HCl 98": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", + "乳清粉": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", + "兔肉粉": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", + "全株玉米青贮": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", + "双低菜籽粕": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", + "向日葵籽": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", + "啤酒糟干": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", + "啤酒花渣": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", + "国产鱼粉60": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", + "土豆蛋白": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", + "大豆油": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", + "大豆粕44": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", + "大豆粕46": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", + "大豆粕48": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", + "大麦": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", + "小苏打": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", + "小麦": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", + "小麦次粉": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", + "小麦麸": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", + "木薯干": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", + "杂交构树叶粉": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", + "构树叶粉(老叶高纤维)": "老叶构树粉,纤维更高,适合母猪粗饲料使用。", + "柠檬酸渣": "湿态副产品,适口性好,可用于母猪料降低成本。", + "棉籽粕": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。", + "棕榈油": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", + "棕榈粕": "高纤维高脂肪副产品,能量一般,多用于母猪料。", + "椰子粕": "蛋白和能量中等,适口性好,可部分替代豆粕。", + "燕麦": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", + "燕麦草": "粗饲料,母猪用以增加饱腹感和肠道健康。", + "猪肺粉": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", + "玉米": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", + "玉米DDGS": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", + "玉米油": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", + "玉米胚芽粕": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", + "玉米蛋白粉60": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", + "玉米青贮": "粗饲料,母猪用以调节肠道,降低饲料成本。", + "瓜子粕": "葵花籽粕的别称,蛋白较高,纤维也高。", + "甜菜粕": "高可溶性纤维,母猪极佳的防便秘原料。", + "石粉": "最常用的钙源,价格低廉,注意粒度影响吸收率。", + "碎米": "能量接近玉米,蛋白稍低,适口性好。", + "磷酸氢钙": "猪最常用磷钙来源,有效磷高。", + "稻草粉": "最廉价粗纤维来源,母猪限量使用防便秘。", + "稻谷": "带壳稻子,能量低于玉米,纤维高。", + "稻谷糠": "米糠的一种,高脂肪高磷,需注意酸败。", + "米糠": "高能量高磷副产品,注意黄曲霉毒素和酸败。", + "米糠粕": "脱脂米糠,蛋白较高,能量降低。", + "红薯干": "高淀粉低蛋白能量原料,类似木薯。", + "肉粉": "普通肉粉,蛋白和灰分波动大,质量不稳定。", + "肉骨粉50": "含骨较高,钙磷比例好,但蛋白较低。", + "脱脂奶粉": "优质乳蛋白源,仔猪料黄金原料。", + "膨化全脂大豆": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", + "芝麻粕": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", + "花生秧粉": "粗饲料,母猪用。", + "花生粕": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", + "苜蓿草块": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", + "苜蓿草粉": "蛋白较高,但皂苷和香豆素可能影响采食。", + "苹果渣": "湿态副产品,适口性好,母猪喜欢。", + "菜籽粕": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", + "葡萄糖": "快速能量源,教槽料常用,缓解应激。", + "葵花籽粕": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", + "蔗糖": "高能量碳水,教槽料诱食用。", + "虾粉": "优质动物蛋白,含虾青素,改善体色。", + "蚕蛹粉": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", + "蚕豆": "蛋白较高,淀粉消化率好,但含抗营养因子。", + "蟹粉": "高蛋白高灰分动物蛋白,钙磷丰富。", + "血浆蛋白粉": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。", + "血粉": "赖氨酸极高,但适口性差,需喷涂使用。", + "豆磷脂": "高能量乳化剂,促进脂肪消化,改善皮毛。", + "豌豆": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", + "豌豆蛋白": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", + "进口鱼粉65": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", + "食盐": "提供钠和氯,调节电解质平衡。", + "饲料酵母粉": "富含核苷酸和小肽,促进肠道健康和免疫。", + "高粱": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", + "鱼油": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", + "鸡肉粉": "优质陆基动物蛋白,消化率高,适口性好。", + "鸭肉粉": "与鸡肉粉类似,脂肪稍高。", + "鹅肉粉": "蛋白和脂肪中等,质量稳定。" + }, + "nutrients": { + "可消化蛋氨酸 (SID %)": "猪第二限制性氨基酸,直接影响瘦肉率和生长速度。", + "可消化色氨酸 (SID %)": "猪第四限制性氨基酸,影响采食量、情绪和免疫力,缺乏时猪只易应激。", + "可消化苏氨酸 (SID %)": "猪第三限制性氨基酸,影响肠道黏膜和免疫器官发育。", + "可消化赖氨酸 (SID %)": "猪第一限制性氨基酸,决定蛋白沉积效率,低蛋白日粮核心指标。", + "干物质 (%)": "原料实际含固体比例,影响贮存和配方计算。", + "总蛋氨酸 (%)": "原料中总的蛋氨酸含量,需结合消化率使用。", + "总色氨酸 (%)": "原料中总的色氨酸含量,猪最易缺乏的氨基酸之一。", + "总苏氨酸 (%)": "原料中总的苏氨酸含量,仔猪阶段需求高。", + "总赖氨酸 (%)": "原料中总的赖氨酸含量,第一限制性氨基酸。", + "水分 (%)": "原料含水量,过高易发霉,影响贮存和营养浓度。", + "粗蛋白 (%)": "饲料蛋白质含量的最基本指标,猪生长核心营养。", + "粗脂肪 (%)": "提供浓缩能量,改善适口性和皮毛光泽。", + "粗纤维 (%)": "影响肠道健康,母猪需要适量以防便秘,生长猪过高降低能量。", + "粗灰分 (%)": "矿物质总含量,过高可能影响消化率。", + "钙 (%)": "骨骼发育、神经肌肉功能必需,母猪缺钙易瘫痪。", + "总磷 (%)": "骨骼和能量代谢必需,但过量排泄污染环境。", + "有效磷 (%)": "猪实际可利用的磷,植物性原料植酸磷利用率低,动物性高。", + "代谢能 (kcal/kg)": "猪实际可利用的能量,最重要的能量指标。", + "净能 (kcal/kg)": "更精确的能量体系,考虑热增耗,现代配方趋势。", + "消化能 (kcal/kg)": "猪消化道可吸收的能量,介于代谢能和净能之间。", + "乳糖 (%)": "仔猪最易利用的糖源,促进乳酸菌生长,断奶料关键。", + "盐分 (%)": "提供钠氯,调节渗透压和胃酸。", + "蛋+胱氨酸 (%)": "含硫氨基酸总量,影响毛发生长和抗氧化。", + "挥发性盐基氮 (mg/100g)": "鱼粉新鲜度指标,超标说明腐败,影响适口性和安全性。", + "组胺 (mg/kg)": "鱼粉腐败产物,高时引起猪过敏和腹泻。", + "胰蛋白酶抑制因子 (TIU/mg)": "豆粕抗营养因子,过高抑制蛋白消化,影响生长。", + "脲酶活性 (ΔpH)": "豆粕加热程度指标,太高说明生,太低说明过熟。", + "KOH蛋白溶解度 (%)": "豆粕加热是否合适的关键指标,78-85%最优。", + "黄曲霉毒素B1 (μg/kg)": "最强致癌霉菌毒素,严重影响肝脏和免疫力,必须严格控制。", + "呕吐毒素DON (μg/kg)": "导致猪呕吐、拒食和免疫抑制,麦类原料常见。", + "玉米赤霉烯酮ZEN (μg/kg)": "雌激素样毒素,导致母猪假发情和繁殖障碍。", + "游离棉酚 (mg/kg)": "影响公猪精子活力和生长,棉粕核心毒素。", + "硫甙 (μmol/g)": "菜粕中导致甲状腺肿大的前体物质,双低菜粕已大幅降低。", + "单宁 (mg/kg)": "影响蛋白消化率和适口性,高粱和构树常见。", + "草酸 (mg/kg)": "与钙结合降低钙吸收,芝麻粕和构树含量高。", + "免疫球蛋白IgG (%)": "血浆蛋白粉核心功能成分,提升仔猪免疫力和肠道成熟。", + "钠 (%)": "钠元素,主要维持猪体内酸碱平衡、渗透压和神经肌肉正常功能。母猪泌乳期和高温季节需求大幅增加,缺钠会导致猪只食欲下降、啃墙、异食癖,甚至生长停滞。日粮钠水平一般控制在0.15%-0.35%,过高易引起盐中毒。", + "氯 (%)": "氯元素,与钠共同维持电解质平衡,是胃酸(盐酸)的主要成分。氯不足会影响胃酸分泌,导致消化不良、食欲减退。猪只呕吐或腹泻时氯流失严重,易出现酸中毒。高氯日粮(>0.5%)会加重热应激和饮水量,湿粪便增多。", + "异硫氰酸酯 (μmol/g)": "菜粕、芥子粕等十字花科原料中的辛辣毒素,刺激性强,高量会导致甲状腺肿大、采食量下降、生长受阻,双低品种已大幅降低,但仍需关注限量。", + "氢氰酸 (mg/kg)": "木薯、苦杏仁等原料中潜在剧毒物质,阻断细胞呼吸,极微量即可引起猪中毒死亡。构树原料标0为已脱毒处理,使用前必须确认氢氰酸含量合格。", + "总黄酮 (mg/kg)": "构树等植物次生代谢物,具有抗氧化、抗菌、改善血管功能的作用,对母猪繁殖性能和仔猪抗应激有一定正面作用,但过量也可能影响适口性。", + "生物碱 (mg/kg)": "构树等原料中潜在有害物质,高量会引起猪神经症状、食欲下降甚至中毒,使用时需严格控制比例。", + "沙门氏菌": "进口鱼粉卫生指标,必须为0(即阴性)。一旦检出阳性,整批原料禁止用于猪饲料,否则极易引发仔猪副伤寒和全身感染。", + "总棉酚 (mg/kg)": "棉粕中主要毒素,包括游离棉酚和结合棉酚。高量引起公猪精子畸形、母猪流产、生长猪肝肾损伤。生长育肥猪建议总棉酚<300mg/kg,种猪<100mg/kg。", + "环丙烯酸 (mg/kg)": "棉粕中另一种毒素,与棉酚协同作用,破坏赖氨酸利用率,严重影响蛋白质沉积。优质脱酚棉粕应<500mg/kg。", + "硫酸盐 (mg/kg)": "玉米DDGS发酵副产物,高硫酸盐(>8000mg/kg常见)易导致猪脑软化症(硫中毒),生长猪日粮硫酸盐建议不超过4000mg/kg。", + "噁唑烷硫酮 (μmol/g)": "老品种菜粕中甲状腺肿诱导物,与异硫氰酸酯协同作用,双低菜粕已降至10μmol/g以下,普通菜粕可高达50-100μmol/g,使用时需大幅限量或添加碘。", + "绿原酸 (mg/kg)": "葵花籽粕、构树等原料中多酚类物质,强抗氧化但也抑制蛋白酶活性,高量降低蛋白消化率,一般控制在2000mg/kg以下。", + "T-2/HT-2毒素 (μg/kg)": "镰刀菌毒素,强细胞毒性,导致猪口腔溃疡、拒食、呕吐、免疫抑制、皮肤坏死。欧盟限量100-250μg/kg,中国建议<500μg/kg。", + "大豆抗原蛋白-球蛋白 (mg/kg)": "大豆中主要抗原蛋白,引起仔猪肠道过敏、腹泻、生长受阻。优质豆粕<2000mg/kg,发酵或膨化大豆可降至检测不出。", + "植酸磷 (%)": "植物性原料中磷的主要存在形式,猪利用率仅20-40%,过高造成磷浪费和环境污染,也是锌、钙等矿物质的螯合剂,影响微量元素吸收。", + "大豆抗原蛋白-β-伴球蛋白 (mg/kg)": "大豆中最强过敏原之一,比球蛋白更难破坏,仔猪断奶料必须严格控制<100mg/kg,否则严重腹泻。", + "寡糖-棉子糖+水苏糖 (%)": "豆粕中难消化寡糖,引起肠道胀气和腹泻。普通豆粕5-6%,低寡糖品种或发酵豆粕可降至1%以下。", + "皂苷 (mg/kg)": "苜蓿等原料中苦味物质,高量严重影响适口性和肠道健康,苜蓿草粉一般2000-5000mg/kg,使用比例不宜超过10%。", + "香豆素 (mg/kg)": "苜蓿、甜菜粕等中抗凝血物质,高量导致出血倾向,苜蓿草粉通常<1000mg/kg,需限量使用。" + } } } \ No newline at end of file diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index 8025bc9..c595d60 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -52,7 +52,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { dataType := gjson.GetBytes(jsonData, "type") if !dataType.Exists() { - logger.Warnf("警告: 文件 '%s' 中缺少 'type' 字段,已跳过\n", filePath) + logger.Warnf("警告: 文件 '%s' 中缺少 'type' 字段,已跳过", filePath) continue } dataTypeStr := dataType.String() @@ -67,7 +67,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { case "nutrient": seederFunc = seedNutrients default: - logger.Warnf("警告: 文件 '%s' 中存在未知的 type: '%s',已跳过\n", filePath, dataTypeStr) + logger.Warnf("警告: 文件 '%s' 中存在未知的 type: '%s',已跳过", filePath, dataTypeStr) continue } @@ -100,16 +100,44 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { return fmt.Errorf("JSON源文件校验失败: %w", err) } - // 2. 将通过校验的、干净的数据写入数据库 + // 2. 解析简介信息 + descriptionsNode := gjson.GetBytes(jsonData, "descriptions") + rawMaterialDescriptions := make(map[string]string) + nutrientDescriptions := make(map[string]string) + + if descriptionsNode.Exists() { + descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool { + rawMaterialDescriptions[key.String()] = value.String() + return true + }) + descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool { + nutrientDescriptions[key.String()] = value.String() + return true + }) + } + + // 3. 将通过校验的、干净的数据写入数据库 for rawMaterialName, nutrients := range parsedData { var rawMaterial models.RawMaterial - if err := tx.Where(models.RawMaterial{Name: rawMaterialName}).FirstOrCreate(&rawMaterial).Error; err != nil { + // 将 Description 放入 Create 对象中 + err := tx.Where(models.RawMaterial{Name: rawMaterialName}). + FirstOrCreate(&rawMaterial, models.RawMaterial{ + Name: rawMaterialName, + Description: rawMaterialDescriptions[rawMaterialName], + }).Error + if err != nil { return fmt.Errorf("预设原料 '%s' 失败: %w", rawMaterialName, err) } for nutrientName, value := range nutrients { var nutrient models.Nutrient - if err := tx.Where(models.Nutrient{Name: nutrientName}).FirstOrCreate(&nutrient).Error; err != nil { + // 将 Description 放入 Create 对象中 + err := tx.Where(models.Nutrient{Name: nutrientName}). + FirstOrCreate(&nutrient, models.Nutrient{ + Name: nutrientName, + Description: nutrientDescriptions[nutrientName], + }).Error + if err != nil { return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) } -- 2.49.1 From fd0939fe0ab6976d9aba2fb158f95696ecbf3b25 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 19 Nov 2025 23:23:48 +0800 Subject: [PATCH 05/59] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E9=A2=86=E5=9F=9F,=20=E5=AE=9E=E7=8E=B0=E8=90=A5=E5=85=BB?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- internal/domain/recipe/recipe_service.go | 144 ++++++++++++++++++ .../infra/repository/nutrient_repository.go | 134 ++++++++++++++++ .../repository/raw_material_repository.go | 22 +++ project_structure.txt | 19 ++- 5 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 internal/domain/recipe/recipe_service.go create mode 100644 internal/infra/repository/nutrient_repository.go create mode 100644 internal/infra/repository/raw_material_repository.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index ac0c8a0..e721802 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -49,4 +49,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 # 完成事项 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 -2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 \ No newline at end of file +2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 +3. 定义配方领域, 实现营养元素的增删改查 \ No newline at end of file diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go new file mode 100644 index 0000000..5eaad96 --- /dev/null +++ b/internal/domain/recipe/recipe_service.go @@ -0,0 +1,144 @@ +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" +) + +// 定义领域特定的错误 +var ( + ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在") + ErrNutrientNotFound = fmt.Errorf("营养种类不存在") + ErrNutrientInUse = fmt.Errorf("营养种类正在被原料使用,无法删除") +) + +// Service 定义了配方与原料领域的核心业务服务接口 +type Service 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, page, pageSize int) ([]models.Nutrient, int64, error) +} + +// recipeServiceImpl 是 RecipeService 的实现 +type recipeServiceImpl struct { + ctx context.Context + nutrientRepo repository.NutrientRepository +} + +// NewRecipeService 创建一个新的 RecipeService 实例 +func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository) Service { + return &recipeServiceImpl{ + ctx: ctx, + nutrientRepo: nutrientRepo, + } +} + +// CreateNutrient 实现了创建营养种类的核心业务逻辑 +func (s *recipeServiceImpl) 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 { + 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 *recipeServiceImpl) 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 { + return nil, fmt.Errorf("获取待更新的营养种类失败: %w", err) + } + if nutrient == nil { + return nil, ErrNutrientNotFound + } + + // 如果名称有变动,检查新名称是否与其它记录冲突 + if nutrient.Name != name { + existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name) + if err != nil { + 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 *recipeServiceImpl) DeleteNutrient(ctx context.Context, id uint32) error { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteNutrient") + + // 检查实体是否存在 + nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) + if err != nil { + return fmt.Errorf("获取待删除的营养种类失败: %w", err) + } + if nutrient == nil { + return ErrNutrientNotFound + } + + if err := s.nutrientRepo.DeleteNutrient(serviceCtx, id); err != nil { + return fmt.Errorf("删除营养种类失败: %w", err) + } + + return nil +} + +// GetNutrient 实现了获取单个营养种类的逻辑 +func (s *recipeServiceImpl) 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 { + return nil, fmt.Errorf("获取营养种类失败: %w", err) + } + if nutrient == nil { + return nil, ErrNutrientNotFound + } + return nutrient, nil +} + +// ListNutrients 实现了列出营养种类的逻辑 +func (s *recipeServiceImpl) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients") + + nutrients, total, err := s.nutrientRepo.ListNutrients(serviceCtx, page, pageSize) + if err != nil { + return nil, 0, fmt.Errorf("获取营养种类列表失败: %w", err) + } + return nutrients, total, nil +} diff --git a/internal/infra/repository/nutrient_repository.go b/internal/infra/repository/nutrient_repository.go new file mode 100644 index 0000000..70b7ef1 --- /dev/null +++ b/internal/infra/repository/nutrient_repository.go @@ -0,0 +1,134 @@ +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" +) + +// 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, 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 + if err := r.db.WithContext(repoCtx).First(&nutrient, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 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 + if err := r.db.WithContext(repoCtx).Where("name = ?", name).First(&nutrient).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // 记录未找到不应视为错误 + } + return nil, err + } + return &nutrient, nil +} + +// ListNutrients 列出所有营养种类(分页) +func (r *gormNutrientRepository) ListNutrients(ctx context.Context, 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 err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 然后应用分页并获取数据 + offset := (page - 1) * pageSize + if err := db.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 + }) + +} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go new file mode 100644 index 0000000..da7d37c --- /dev/null +++ b/internal/infra/repository/raw_material_repository.go @@ -0,0 +1,22 @@ +package repository + +import ( + "context" + + "gorm.io/gorm" +) + +// RawMaterialRepository 定义了与原料相关的数据库操作接口 +type RawMaterialRepository interface { +} + +// gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现 +type gormRawMaterialRepository struct { + ctx context.Context + db *gorm.DB +} + +// NewGormRawMaterialRepository 创建一个新的 RawMaterialRepository GORM 实现实例 +func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository { + return &gormRawMaterialRepository{ctx: ctx, db: db} +} diff --git a/project_structure.txt b/project_structure.txt index c01eab0..73cbd99 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -1,15 +1,16 @@  -.air.toml .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/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 +34,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 docs/docs.go docs/swagger.json docs/swagger.yaml @@ -102,25 +104,28 @@ 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/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/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 @@ -129,6 +134,7 @@ 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/schedule.go internal/infra/models/sensor_data.go internal/infra/models/user.go @@ -145,6 +151,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 -- 2.49.1 From c697e668e332a4c4ee7b0b5005268ef7131172e4 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 13:43:09 +0800 Subject: [PATCH 06/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=8E=9F=E6=9D=90?= =?UTF-8?q?=E6=96=99=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=E5=92=8C?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=B1=82=E7=9A=84=E5=8E=9F=E6=96=99=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E8=AE=B0=E5=BD=95=E8=A1=A8=E5=A2=9E=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- internal/domain/recipe/recipe_service.go | 154 ++++++++++++++++-- internal/infra/models/raw_material.go | 15 +- .../infra/repository/nutrient_repository.go | 20 +-- .../repository/raw_material_repository.go | 140 ++++++++++++++++ 5 files changed, 296 insertions(+), 36 deletions(-) diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index e721802..9cc4f67 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -50,4 +50,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 -3. 定义配方领域, 实现营养元素的增删改查 \ No newline at end of file +3. 定义配方领域, 实现营养元素的增删改查 +4. 实现原材料的增删改查和仓库层的原料库存记录表增查 \ No newline at end of file diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 5eaad96..f6b22e2 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -2,11 +2,14 @@ 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" ) // 定义领域特定的错误 @@ -14,28 +17,41 @@ var ( ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在") ErrNutrientNotFound = fmt.Errorf("营养种类不存在") ErrNutrientInUse = fmt.Errorf("营养种类正在被原料使用,无法删除") + + ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") + ErrRawMaterialNotFound = fmt.Errorf("原料不存在") ) // Service 定义了配方与原料领域的核心业务服务接口 type Service 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, page, pageSize int) ([]models.Nutrient, int64, error) + + // 原料相关接口 + CreateRawMaterial(ctx context.Context, name, description string) (*models.RawMaterial, error) + UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*models.RawMaterial, error) + DeleteRawMaterial(ctx context.Context, id uint32) error + GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error) + ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) } // recipeServiceImpl 是 RecipeService 的实现 type recipeServiceImpl struct { - ctx context.Context - nutrientRepo repository.NutrientRepository + ctx context.Context + nutrientRepo repository.NutrientRepository + rawMaterialRepo repository.RawMaterialRepository } // NewRecipeService 创建一个新的 RecipeService 实例 -func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository) Service { +func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository) Service { return &recipeServiceImpl{ - ctx: ctx, - nutrientRepo: nutrientRepo, + ctx: ctx, + nutrientRepo: nutrientRepo, + rawMaterialRepo: rawMaterialRepo, } } @@ -45,7 +61,7 @@ func (s *recipeServiceImpl) CreateNutrient(ctx context.Context, name, descriptio // 检查名称是否已存在 existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name) - if err != nil { + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // 只有不是记录未找到的错误才返回 return nil, fmt.Errorf("检查营养种类名称失败: %w", err) } if existing != nil { @@ -71,16 +87,16 @@ func (s *recipeServiceImpl) UpdateNutrient(ctx context.Context, id uint32, name, // 检查要更新的实体是否存在 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 == nil { - return nil, ErrNutrientNotFound - } // 如果名称有变动,检查新名称是否与其它记录冲突 if nutrient.Name != name { existing, err := s.nutrientRepo.GetNutrientByName(serviceCtx, name) - if err != nil { + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("检查新的营养种类名称失败: %w", err) } if existing != nil && existing.ID != id { @@ -103,13 +119,13 @@ func (s *recipeServiceImpl) DeleteNutrient(ctx context.Context, id uint32) error serviceCtx := logs.AddFuncName(ctx, s.ctx, "DeleteNutrient") // 检查实体是否存在 - nutrient, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) + _, err := s.nutrientRepo.GetNutrientByID(serviceCtx, id) if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrNutrientNotFound + } return fmt.Errorf("获取待删除的营养种类失败: %w", err) } - if nutrient == nil { - return ErrNutrientNotFound - } if err := s.nutrientRepo.DeleteNutrient(serviceCtx, id); err != nil { return fmt.Errorf("删除营养种类失败: %w", err) @@ -124,11 +140,11 @@ func (s *recipeServiceImpl) GetNutrient(ctx context.Context, id uint32) (*models 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 == nil { - return nil, ErrNutrientNotFound - } return nutrient, nil } @@ -142,3 +158,107 @@ func (s *recipeServiceImpl) ListNutrients(ctx context.Context, page, pageSize in } return nutrients, total, nil } + +// CreateRawMaterial 实现了创建原料的核心业务逻辑 +func (s *recipeServiceImpl) CreateRawMaterial(ctx context.Context, name, description string) (*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, + } + + if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil { + return nil, fmt.Errorf("创建原料失败: %w", err) + } + + return rawMaterial, nil +} + +// UpdateRawMaterial 实现了更新原料的核心业务逻辑 +func (s *recipeServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*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 + + if err := s.rawMaterialRepo.UpdateRawMaterial(serviceCtx, rawMaterial); err != nil { + return nil, fmt.Errorf("更新原料失败: %w", err) + } + + return rawMaterial, nil +} + +// DeleteRawMaterial 实现了删除原料的核心业务逻辑 +func (s *recipeServiceImpl) 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) + } + + if err := s.rawMaterialRepo.DeleteRawMaterial(serviceCtx, id); err != nil { + return fmt.Errorf("删除原料失败: %w", err) + } + + return nil +} + +// GetRawMaterial 实现了获取单个原料的逻辑 +func (s *recipeServiceImpl) 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 *recipeServiceImpl) ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials") + + rawMaterials, total, err := s.rawMaterialRepo.ListRawMaterials(serviceCtx, page, pageSize) + if err != nil { + return nil, 0, fmt.Errorf("获取原料列表失败: %w", err) + } + return rawMaterials, total, nil +} diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go index b992cb4..703f6b2 100644 --- a/internal/infra/models/raw_material.go +++ b/internal/infra/models/raw_material.go @@ -23,9 +23,8 @@ type RawMaterial struct { Model Name string `gorm:"size:100;unique;not null;comment:原料名称"` Description string `gorm:"size:255;comment:描述"` - // Quantity 是当前库存的快照值,用于提供高性能的库存查询。 - // 注意:此字段的值必须在数据库事务中与 RawMaterialStockLog 同步更新,以保证数据一致性。 - Quantity float32 `gorm:"not null;default:0;comment:当前库存快照, 单位: g"` + // RawMaterialNutrients 关联此原料的所有营养素含量信息 + RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"` } func (RawMaterial) TableName() string { @@ -39,6 +38,8 @@ type Nutrient struct { Model Name string `gorm:"size:100;unique;not null;comment:营养素名称"` Description string `gorm:"size:255;comment:描述"` + // RawMaterialNutrients 记录营养在哪些原料中存在且比例是多少 + RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:NutrientID"` } func (Nutrient) TableName() string { @@ -66,9 +67,11 @@ func (RawMaterialNutrient) TableName() string { // 它是保证数据一致性和可审计性的核心。 type RawMaterialStockLog struct { Model - RawMaterialID uint32 `gorm:"not null;index;comment:关联的原料ID"` - RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` - ChangeAmount float32 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库, 单位: g"` + 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)。 diff --git a/internal/infra/repository/nutrient_repository.go b/internal/infra/repository/nutrient_repository.go index 70b7ef1..f2e89c3 100644 --- a/internal/infra/repository/nutrient_repository.go +++ b/internal/infra/repository/nutrient_repository.go @@ -38,33 +38,29 @@ func (r *gormNutrientRepository) CreateNutrient(ctx context.Context, nutrient *m return r.db.WithContext(repoCtx).Create(nutrient).Error } -// GetNutrientByID 根据ID获取单个营养种类 +// 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 - if err := r.db.WithContext(repoCtx).First(&nutrient, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil // 记录未找到不应视为错误 - } + // 如果记录未找到,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 根据名称获取单个营养种类 +// GetNutrientByName 根据名称获取单个营养种类,并预加载关联的原料信息 func (r *gormNutrientRepository) GetNutrientByName(ctx context.Context, name string) (*models.Nutrient, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "GetNutrientByName") var nutrient models.Nutrient - if err := r.db.WithContext(repoCtx).Where("name = ?", name).First(&nutrient).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil // 记录未找到不应视为错误 - } + // 如果记录未找到,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 列出所有营养种类(分页) +// ListNutrients 列出所有营养种类(分页),并预加载关联的原料信息 func (r *gormNutrientRepository) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { repoCtx := logs.AddFuncName(ctx, r.ctx, "ListNutrients") var nutrients []models.Nutrient @@ -79,7 +75,7 @@ func (r *gormNutrientRepository) ListNutrients(ctx context.Context, page, pageSi // 然后应用分页并获取数据 offset := (page - 1) * pageSize - if err := db.Offset(offset).Limit(pageSize).Find(&nutrients).Error; err != nil { + if err := db.Preload("RawMaterialNutrients.RawMaterial").Offset(offset).Limit(pageSize).Find(&nutrients).Error; err != nil { return nil, 0, err } diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index da7d37c..3f40c43 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -2,12 +2,27 @@ 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" ) // RawMaterialRepository 定义了与原料相关的数据库操作接口 type RawMaterialRepository interface { + 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, page, pageSize int) ([]models.RawMaterial, int64, error) + UpdateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error + DeleteRawMaterial(ctx context.Context, id uint32) error + + // 库存日志相关方法 + CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error + GetLatestRawMaterialStockLog(ctx context.Context, rawMaterialID uint32) (*models.RawMaterialStockLog, error) } // gormRawMaterialRepository 是 RawMaterialRepository 的 GORM 实现 @@ -20,3 +35,128 @@ type gormRawMaterialRepository struct { func NewGormRawMaterialRepository(ctx context.Context, db *gorm.DB) RawMaterialRepository { return &gormRawMaterialRepository{ctx: ctx, db: db} } + +// 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 +} + +// 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, page, pageSize int) ([]models.RawMaterial, int64, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListRawMaterials") + var rawMaterials []models.RawMaterial + var total int64 + + db := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}) + + // 首先计算总数 + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 然后应用分页并获取数据 + 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, + } + 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 + }) +} + +// 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 +} -- 2.49.1 From 6ca101727a8a973d973a68d41490922441de4223 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 14:38:36 +0800 Subject: [PATCH 07/59] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=8C=AA=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8C=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- internal/infra/database/postgres.go | 55 ++++++++++++++++++++++- internal/infra/models/models.go | 6 ++- internal/infra/models/pig.go | 43 ++++++++++++++++++ internal/infra/models/pig_nutrient.go | 16 +++++++ project_structure.txt | 2 + 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 internal/infra/models/pig.go create mode 100644 internal/infra/models/pig_nutrient.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 9cc4f67..60bf2fa 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -51,4 +51,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 3. 定义配方领域, 实现营养元素的增删改查 -4. 实现原材料的增删改查和仓库层的原料库存记录表增查 \ No newline at end of file +4. 实现原材料的增删改查和仓库层的原料库存记录表增查 +5. 定义猪的模型和营养需求模型 \ No newline at end of file diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 3ce4a6b..b2a84cb 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -250,9 +250,20 @@ 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 保证幂等性 // 如果索引已存在,此命令不会报错 + if err := ps.creatingUniqueIndex(storageCtx); err != nil { + return err + } + if err := ps.createGinIndexes(storageCtx); err != nil { + return err + } + return nil +} + +func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { + storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex") // 为 raw_material_nutrients 表创建部分唯一索引,以兼容软删除 logger.Debug("正在为 raw_material_nutrients 表创建部分唯一索引") @@ -263,6 +274,47 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { } logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)") + // 为 pig_breeds 表创建部分唯一索引,以兼容软删除 (name 唯一) + logger.Debug("正在为 pig_breeds 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_breeds_unique_name_when_not_deleted ON pig_breeds (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_breeds 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_breeds 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_breeds 创建部分唯一索引 (或已存在)") + + // 为 pig_age_stages 表创建部分唯一索引,以兼容软删除 (name 唯一) + logger.Debug("正在为 pig_age_stages 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_age_stages_unique_name_when_not_deleted ON pig_age_stages (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_age_stages 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_age_stages 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_age_stages 创建部分唯一索引 (或已存在)") + + // 为 pig_types 表创建部分唯一索引,以兼容软删除 (breed_id, age_stage_id 组合唯一) + logger.Debug("正在为 pig_types 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_types_unique_breed_age_stage_when_not_deleted ON pig_types (breed_id, age_stage_id) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_types 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_types 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_types 创建部分唯一索引 (或已存在)") + + // 为 pig_nutrient_requirements 表创建部分唯一索引,以兼容软删除 (pig_type_id, nutrient_id 组合唯一) + logger.Debug("正在为 pig_nutrient_requirements 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted ON pig_nutrient_requirements (pig_type_id, nutrient_id) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_nutrient_requirements 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)") + return nil +} + +func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error { + storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes") + // 为 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);" @@ -280,6 +332,5 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err) } logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)") - return nil } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 4232d42..47324ed 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -56,6 +56,10 @@ func GetAllModels() []interface{} { &WeighingRecord{}, &PigTransferLog{}, &PigSickLog{}, + &PigBreed{}, + &PigAgeStage{}, + &PigType{}, + &PigNutrientRequirement{}, // Pig Buy & Sell &PigPurchase{}, @@ -119,7 +123,7 @@ func (a *UintArray) Scan(src interface{}) error { case string: srcStr = v default: - return errors.New("无法扫描非字符串或字节类型的源到 UintArray") + return errors.New("无法将值 %v (类型 %T) 扫描为 UintArray") } // 去掉花括号 diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go new file mode 100644 index 0000000..132c270 --- /dev/null +++ b/internal/infra/models/pig.go @@ -0,0 +1,43 @@ +package models + +// PigBreed 猪品种模型 +type PigBreed struct { + Model + Name string `gorm:"size:50;not null;comment:品种名称"` + Description string `gorm:"size:255;comment:品种描述"` +} + +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)"` +} + +func (PigType) TableName() string { + return "pig_types" +} diff --git a/internal/infra/models/pig_nutrient.go b/internal/infra/models/pig_nutrient.go new file mode 100644 index 0000000..2552115 --- /dev/null +++ b/internal/infra/models/pig_nutrient.go @@ -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" +} diff --git a/project_structure.txt b/project_structure.txt index 73cbd99..dbd7129 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -129,7 +129,9 @@ internal/infra/models/farm_asset.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 -- 2.49.1 From 1f3d3d8a7c992b2e1beb003faa3c217edb05df29 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 16:47:47 +0800 Subject: [PATCH 08/59] =?UTF-8?q?=E9=85=8D=E7=BD=AEcontinue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .continue/mcpServers/new-mcp-server.yaml | 9 ++++++++ .continue/rules/new-rule.md | 26 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .continue/mcpServers/new-mcp-server.yaml create mode 100644 .continue/rules/new-rule.md diff --git a/.continue/mcpServers/new-mcp-server.yaml b/.continue/mcpServers/new-mcp-server.yaml new file mode 100644 index 0000000..ed60082 --- /dev/null +++ b/.continue/mcpServers/new-mcp-server.yaml @@ -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: {} diff --git a/.continue/rules/new-rule.md b/.continue/rules/new-rule.md new file mode 100644 index 0000000..6f0425b --- /dev/null +++ b/.continue/rules/new-rule.md @@ -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 和网络状态等。 \ No newline at end of file -- 2.49.1 From da934a9bbb2d9c639c08ed2f4c8fbf8a540b9dff Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 17:37:02 +0800 Subject: [PATCH 09/59] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BD=AF=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=92=8C=E5=94=AF=E4=B8=80=E7=B4=A2=E5=BC=95=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E5=AD=98=E5=9C=A8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/postgres.go | 72 ++++++++++++++++++++++++ internal/infra/models/device.go | 4 +- internal/infra/models/device_template.go | 2 +- internal/infra/models/farm_asset.go | 2 +- internal/infra/models/pig_batch.go | 2 +- internal/infra/models/raw_material.go | 4 +- internal/infra/models/user.go | 2 +- 7 files changed, 80 insertions(+), 8 deletions(-) diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index b2a84cb..6babcd7 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -309,6 +309,78 @@ func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err) } logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)") + + // 为 users 表创建部分唯一索引 + logger.Debug("正在为 users 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_unique_username_when_not_deleted ON users (username) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 users 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 users 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 users 创建部分唯一索引 (或已存在)") + + // 为 area_controllers 表创建部分唯一索引 (Name) + logger.Debug("正在为 area_controllers 表创建部分唯一索引 (Name)") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_name_when_not_deleted ON area_controllers (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 area_controllers 创建部分唯一索引 (Name) 失败", "error", err) + return fmt.Errorf("为 area_controllers 创建部分唯一索引 (Name) 失败: %w", err) + } + logger.Debug("成功为 area_controllers 创建部分唯一索引 (Name) (或已存在)") + + // 为 area_controllers 表创建部分唯一索引 (NetworkID) + logger.Debug("正在为 area_controllers 表创建部分唯一索引 (NetworkID)") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_network_id_when_not_deleted ON area_controllers (network_id) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 area_controllers 创建部分唯一索引 (NetworkID) 失败", "error", err) + return fmt.Errorf("为 area_controllers 创建部分唯一索引 (NetworkID) 失败: %w", err) + } + logger.Debug("成功为 area_controllers 创建部分唯一索引 (NetworkID) (或已存在)") + + // 为 device_templates 表创建部分唯一索引 + logger.Debug("正在为 device_templates 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_device_templates_unique_name_when_not_deleted ON device_templates (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 device_templates 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 device_templates 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 device_templates 创建部分唯一索引 (或已存在)") + + // 为 pig_batches 表创建部分唯一索引 + logger.Debug("正在为 pig_batches 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_batches_unique_batch_number_when_not_deleted ON pig_batches (batch_number) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_batches 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_batches 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_batches 创建部分唯一索引 (或已存在)") + + // 为 pig_houses 表创建部分唯一索引 + logger.Debug("正在为 pig_houses 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_houses_unique_name_when_not_deleted ON pig_houses (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 pig_houses 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 pig_houses 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 pig_houses 创建部分唯一索引 (或已存在)") + + // 为 raw_materials 表创建部分唯一索引 + logger.Debug("正在为 raw_materials 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_materials_unique_name_when_not_deleted ON raw_materials (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 raw_materials 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 raw_materials 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 raw_materials 创建部分唯一索引 (或已存在)") + + // 为 nutrients 表创建部分唯一索引 + logger.Debug("正在为 nutrients 表创建部分唯一索引") + partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_nutrients_unique_name_when_not_deleted ON nutrients (name) WHERE deleted_at IS NULL;" + if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { + logger.Errorw("为 nutrients 创建部分唯一索引失败", "error", err) + return fmt.Errorf("为 nutrients 创建部分唯一索引失败: %w", err) + } + logger.Debug("成功为 nutrients 创建部分唯一索引 (或已存在)") return nil } diff --git a/internal/infra/models/device.go b/internal/infra/models/device.go index aa2af91..6801110 100644 --- a/internal/infra/models/device.go +++ b/internal/infra/models/device.go @@ -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"` diff --git a/internal/infra/models/device_template.go b/internal/infra/models/device_template.go index 0f5cb6d..c6fc759 100644 --- a/internal/infra/models/device_template.go +++ b/internal/infra/models/device_template.go @@ -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"` diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index c4723b0..84471d2 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -7,7 +7,7 @@ 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"` // 一个猪舍包含多个猪栏 } diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index ee9ca0d..b08f9d6 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -31,7 +31,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:批次结束日期 (全部淘汰或售出)"` diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go index 703f6b2..18baa54 100644 --- a/internal/infra/models/raw_material.go +++ b/internal/infra/models/raw_material.go @@ -21,7 +21,7 @@ const ( // RawMaterial 代表一种原料的静态定义,是系统中的原料字典。 type RawMaterial struct { Model - Name string `gorm:"size:100;unique;not null;comment:原料名称"` + Name string `gorm:"size:100;not null;comment:原料名称"` Description string `gorm:"size:255;comment:描述"` // RawMaterialNutrients 关联此原料的所有营养素含量信息 RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"` @@ -36,7 +36,7 @@ func (RawMaterial) TableName() string { // 约定:宏量营养素(粗蛋白等)单位为百分比(%),微量元素(氨基酸等)单位为毫克/千克(mg/kg)。 type Nutrient struct { Model - Name string `gorm:"size:100;unique;not null;comment:营养素名称"` + Name string `gorm:"size:100;not null;comment:营养素名称"` Description string `gorm:"size:255;comment:描述"` // RawMaterialNutrients 记录营养在哪些原料中存在且比例是多少 RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:NutrientID"` diff --git a/internal/infra/models/user.go b/internal/infra/models/user.go index 2d66ca7..9d33e40 100644 --- a/internal/infra/models/user.go +++ b/internal/infra/models/user.go @@ -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 时被忽略,防止密码泄露 -- 2.49.1 From 1313140e4526069a390c41ae06165a72f1300ae9 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 17:46:01 +0800 Subject: [PATCH 10/59] =?UTF-8?q?=E9=87=8D=E6=9E=84creatingUniqueIndex?= =?UTF-8?q?=E5=92=8CcreateGinIndexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/postgres.go | 270 +++++++++++++++------------- internal/infra/models/farm_asset.go | 8 + 2 files changed, 156 insertions(+), 122 deletions(-) diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 6babcd7..b2a513b 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -6,6 +6,7 @@ package database import ( "context" "fmt" + "strings" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -265,144 +266,169 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex") - // 为 raw_material_nutrients 表创建部分唯一索引,以兼容软删除 - logger.Debug("正在为 raw_material_nutrients 表创建部分唯一索引") - partialIndexSQL := "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_material_nutrients_unique_when_not_deleted ON raw_material_nutrients (raw_material_id, nutrient_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 raw_material_nutrients 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 raw_material_nutrients 创建部分唯一索引失败: %w", err) + // uniqueIndexDefinition 结构体定义了唯一索引的详细信息 + type uniqueIndexDefinition struct { + tableName string // 索引所属的表名 + columns []string // 构成唯一索引的列名 + indexName string // 唯一索引的名称 + whereClause string // 可选的 WHERE 子句,用于创建部分索引 + description string // 索引的描述,用于日志记录 } - logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)") - // 为 pig_breeds 表创建部分唯一索引,以兼容软删除 (name 唯一) - logger.Debug("正在为 pig_breeds 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_breeds_unique_name_when_not_deleted ON pig_breeds (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_breeds 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_breeds 创建部分唯一索引失败: %w", err) + // 定义所有需要创建的唯一索引 + 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 唯一)", + }, } - logger.Debug("成功为 pig_breeds 创建部分唯一索引 (或已存在)") - // 为 pig_age_stages 表创建部分唯一索引,以兼容软删除 (name 唯一) - logger.Debug("正在为 pig_age_stages 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_age_stages_unique_name_when_not_deleted ON pig_age_stages (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_age_stages 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_age_stages 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_age_stages 创建部分唯一索引 (或已存在)") + for _, indexDef := range uniqueIndexesToCreate { + logger.Debugw("正在为表创建部分唯一索引", "表名", indexDef.tableName, "索引名", indexDef.indexName, "描述", indexDef.description) - // 为 pig_types 表创建部分唯一索引,以兼容软删除 (breed_id, age_stage_id 组合唯一) - logger.Debug("正在为 pig_types 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_types_unique_breed_age_stage_when_not_deleted ON pig_types (breed_id, age_stage_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_types 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_types 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_types 创建部分唯一索引 (或已存在)") + // 拼接列名字符串 + 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) - // 为 pig_nutrient_requirements 表创建部分唯一索引,以兼容软删除 (pig_type_id, nutrient_id 组合唯一) - logger.Debug("正在为 pig_nutrient_requirements 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted ON pig_nutrient_requirements (pig_type_id, nutrient_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_nutrient_requirements 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err) + 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) } - logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)") - // 为 users 表创建部分唯一索引 - logger.Debug("正在为 users 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_unique_username_when_not_deleted ON users (username) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 users 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 users 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 users 创建部分唯一索引 (或已存在)") - - // 为 area_controllers 表创建部分唯一索引 (Name) - logger.Debug("正在为 area_controllers 表创建部分唯一索引 (Name)") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_name_when_not_deleted ON area_controllers (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 area_controllers 创建部分唯一索引 (Name) 失败", "error", err) - return fmt.Errorf("为 area_controllers 创建部分唯一索引 (Name) 失败: %w", err) - } - logger.Debug("成功为 area_controllers 创建部分唯一索引 (Name) (或已存在)") - - // 为 area_controllers 表创建部分唯一索引 (NetworkID) - logger.Debug("正在为 area_controllers 表创建部分唯一索引 (NetworkID)") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_network_id_when_not_deleted ON area_controllers (network_id) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 area_controllers 创建部分唯一索引 (NetworkID) 失败", "error", err) - return fmt.Errorf("为 area_controllers 创建部分唯一索引 (NetworkID) 失败: %w", err) - } - logger.Debug("成功为 area_controllers 创建部分唯一索引 (NetworkID) (或已存在)") - - // 为 device_templates 表创建部分唯一索引 - logger.Debug("正在为 device_templates 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_device_templates_unique_name_when_not_deleted ON device_templates (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 device_templates 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 device_templates 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 device_templates 创建部分唯一索引 (或已存在)") - - // 为 pig_batches 表创建部分唯一索引 - logger.Debug("正在为 pig_batches 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_batches_unique_batch_number_when_not_deleted ON pig_batches (batch_number) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_batches 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_batches 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_batches 创建部分唯一索引 (或已存在)") - - // 为 pig_houses 表创建部分唯一索引 - logger.Debug("正在为 pig_houses 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_houses_unique_name_when_not_deleted ON pig_houses (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 pig_houses 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 pig_houses 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 pig_houses 创建部分唯一索引 (或已存在)") - - // 为 raw_materials 表创建部分唯一索引 - logger.Debug("正在为 raw_materials 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_materials_unique_name_when_not_deleted ON raw_materials (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 raw_materials 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 raw_materials 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 raw_materials 创建部分唯一索引 (或已存在)") - - // 为 nutrients 表创建部分唯一索引 - logger.Debug("正在为 nutrients 表创建部分唯一索引") - partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_nutrients_unique_name_when_not_deleted ON nutrients (name) WHERE deleted_at IS NULL;" - if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { - logger.Errorw("为 nutrients 创建部分唯一索引失败", "error", err) - return fmt.Errorf("为 nutrients 创建部分唯一索引失败: %w", err) - } - logger.Debug("成功为 nutrients 创建部分唯一索引 (或已存在)") return nil } func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error { storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes") - // 为 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) + // ginIndexDefinition 结构体定义了 GIN 索引的详细信息 + type ginIndexDefinition struct { + tableName string // 索引所属的表名 + columnName string // 需要创建 GIN 索引的列名 + indexName string // GIN 索引的名称 + description string // 索引的描述,用于日志记录 } - 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) + // 定义所有需要创建的 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 索引", + }, } - logger.Debug("成功为 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) + } + return nil } diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 84471d2..52de9dd 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -12,6 +12,10 @@ type PigHouse struct { 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" +} -- 2.49.1 From d185f334ac22381ca08a0512632fd71d077bbd9f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 18:58:49 +0800 Subject: [PATCH 11/59] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Taber=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/component_initializers.go | 17 ++++++++++++- internal/infra/database/postgres.go | 34 ++++++++++++------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index d9c6659..9fbc395 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -24,6 +24,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/utils/token" "gorm.io/gorm" + "gorm.io/gorm/schema" ) // Infrastructure 聚合了所有基础设施层的组件。 @@ -421,6 +422,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 { @@ -428,8 +431,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) } diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index b2a513b..4d2647f 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -263,18 +263,26 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error { 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") - // uniqueIndexDefinition 结构体定义了唯一索引的详细信息 - type uniqueIndexDefinition struct { - tableName string // 索引所属的表名 - columns []string // 构成唯一索引的列名 - indexName string // 唯一索引的名称 - whereClause string // 可选的 WHERE 子句,用于创建部分索引 - description string // 索引的描述,用于日志记录 - } - // 定义所有需要创建的唯一索引 uniqueIndexesToCreate := []uniqueIndexDefinition{ { @@ -392,14 +400,6 @@ func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error { storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes") - // ginIndexDefinition 结构体定义了 GIN 索引的详细信息 - type ginIndexDefinition struct { - tableName string // 索引所属的表名 - columnName string // 需要创建 GIN 索引的列名 - indexName string // GIN 索引的名称 - description string // 索引的描述,用于日志记录 - } - // 定义所有需要创建的 GIN 索引 ginIndexesToCreate := []ginIndexDefinition{ { -- 2.49.1 From c4ab53db12a8c428842e09a350c95648cb931814 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 21:00:58 +0800 Subject: [PATCH 12/59] =?UTF-8?q?=E4=BC=98=E5=8C=96PigBatchStatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig_batch.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index b08f9d6..6d94b0a 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -12,12 +12,10 @@ import ( type PigBatchStatus string const ( - BatchStatusWeaning PigBatchStatus = "保育" // 从断奶到保育结束 - BatchStatusGrowing PigBatchStatus = "生长" // 生长育肥阶段 - BatchStatusFinishing PigBatchStatus = "育肥" // 最后的育肥阶段 - BatchStatusForSale PigBatchStatus = "待售" // 达到出栏标准 - BatchStatusSold PigBatchStatus = "已出售" - BatchStatusArchived PigBatchStatus = "已归档" // 批次结束(如全群淘汰等) + BatchStatusActive PigBatchStatus = "生产中" // 饲养中 + BatchStatusForSale PigBatchStatus = "待售" // 达到出栏标准 + BatchStatusSold PigBatchStatus = "已出售" + BatchStatusArchived PigBatchStatus = "已归档" // 批次结束(如全群淘汰等) ) // PigBatchOriginType 定义了猪批次的来源 -- 2.49.1 From aa13239e89b11582fd4fe497cd87c1a14a03571a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 22:55:18 +0800 Subject: [PATCH 13/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BB=8Ejson=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E7=8C=AA=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82=E5=B9=B6?= =?UTF-8?q?=E5=86=99=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 2 +- .../pig_nutrient_requirement.json | 914 ++++++++++++++++++ design/archive/recipe-management/index.md | 3 +- internal/infra/database/seeder.go | 361 ++++++- internal/infra/models/pig.go | 8 +- 5 files changed, 1254 insertions(+), 34 deletions(-) create mode 100644 config/presets-data/pig_nutrient_requirement.json diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index e589b34..9c322a8 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -1864,7 +1864,7 @@ "氢氰酸 (mg/kg)": "木薯、苦杏仁等原料中潜在剧毒物质,阻断细胞呼吸,极微量即可引起猪中毒死亡。构树原料标0为已脱毒处理,使用前必须确认氢氰酸含量合格。", "总黄酮 (mg/kg)": "构树等植物次生代谢物,具有抗氧化、抗菌、改善血管功能的作用,对母猪繁殖性能和仔猪抗应激有一定正面作用,但过量也可能影响适口性。", "生物碱 (mg/kg)": "构树等原料中潜在有害物质,高量会引起猪神经症状、食欲下降甚至中毒,使用时需严格控制比例。", - "沙门氏菌": "进口鱼粉卫生指标,必须为0(即阴性)。一旦检出阳性,整批原料禁止用于猪饲料,否则极易引发仔猪副伤寒和全身感染。", + "沙门氏菌 (mg/kg)": "进口鱼粉卫生指标,必须为0(即阴性)。一旦检出阳性,整批原料禁止用于猪饲料,否则极易引发仔猪副伤寒和全身感染。", "总棉酚 (mg/kg)": "棉粕中主要毒素,包括游离棉酚和结合棉酚。高量引起公猪精子畸形、母猪流产、生长猪肝肾损伤。生长育肥猪建议总棉酚<300mg/kg,种猪<100mg/kg。", "环丙烯酸 (mg/kg)": "棉粕中另一种毒素,与棉酚协同作用,破坏赖氨酸利用率,严重影响蛋白质沉积。优质脱酚棉粕应<500mg/kg。", "硫酸盐 (mg/kg)": "玉米DDGS发酵副产物,高硫酸盐(>8000mg/kg常见)易导致猪脑软化症(硫中毒),生长猪日粮硫酸盐建议不超过4000mg/kg。", diff --git a/config/presets-data/pig_nutrient_requirement.json b/config/presets-data/pig_nutrient_requirement.json new file mode 100644 index 0000000..c6aacf8 --- /dev/null +++ b/config/presets-data/pig_nutrient_requirement.json @@ -0,0 +1,914 @@ +{ + "type": "pig_nutrient_requirements", + "data": { + "杜长大 (DLY)": { + "保育期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.012, + "max_requirement": 0.015 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0072, + "max_requirement": 0.0105 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0078, + "max_requirement": 0.0108 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0022, + "max_requirement": 0.0030 + }, + "粗蛋白 (%)": { + "min_requirement": 0.18, + "max_requirement": 0.22 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.009, + "max_requirement": 0.012 + }, + "总磷 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0045 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3226.5, + "max_requirement": 3585.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥前期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0094, + "max_requirement": 0.0110 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0055, + "max_requirement": 0.0073 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0058, + "max_requirement": 0.0077 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0016, + "max_requirement": 0.0022 + }, + "粗蛋白 (%)": { + "min_requirement": 0.16, + "max_requirement": 0.18 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.007, + "max_requirement": 0.009 + }, + "总磷 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0040 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3107.0, + "max_requirement": 3346.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥后期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0081, + "max_requirement": 0.0090 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.0058 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0048, + "max_requirement": 0.0061 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0013, + "max_requirement": 0.0018 + }, + "粗蛋白 (%)": { + "min_requirement": 0.14, + "max_requirement": 0.16 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "总磷 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.006 + }, + "有效磷 (%)": { + "min_requirement": 0.0018, + "max_requirement": 0.0035 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2987.5, + "max_requirement": 3226.5 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "二次育肥期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0053, + "max_requirement": 0.0065 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0030, + "max_requirement": 0.0041 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0031, + "max_requirement": 0.0043 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0010, + "max_requirement": 0.0013 + }, + "粗蛋白 (%)": { + "min_requirement": 0.12, + "max_requirement": 0.14 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "总磷 (%)": { + "min_requirement": 0.004, + "max_requirement": 0.0055 + }, + "有效磷 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0030 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2868.0, + "max_requirement": 3107.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + } + }, + "杜大长 (DY L)": { + "保育期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.012, + "max_requirement": 0.015 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0072, + "max_requirement": 0.0105 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0078, + "max_requirement": 0.0108 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0022, + "max_requirement": 0.0030 + }, + "粗蛋白 (%)": { + "min_requirement": 0.18, + "max_requirement": 0.22 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.009, + "max_requirement": 0.012 + }, + "总磷 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0045 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3226.5, + "max_requirement": 3585.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥前期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0094, + "max_requirement": 0.0110 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0055, + "max_requirement": 0.0073 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0058, + "max_requirement": 0.0077 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0016, + "max_requirement": 0.0022 + }, + "粗蛋白 (%)": { + "min_requirement": 0.16, + "max_requirement": 0.18 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.007, + "max_requirement": 0.009 + }, + "总磷 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0040 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3107.0, + "max_requirement": 3346.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥后期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0081, + "max_requirement": 0.0090 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.0058 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0048, + "max_requirement": 0.0061 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0013, + "max_requirement": 0.0018 + }, + "粗蛋白 (%)": { + "min_requirement": 0.14, + "max_requirement": 0.16 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "总磷 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.006 + }, + "有效磷 (%)": { + "min_requirement": 0.0018, + "max_requirement": 0.0035 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2987.5, + "max_requirement": 3226.5 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "二次育肥期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0053, + "max_requirement": 0.0065 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0030, + "max_requirement": 0.0041 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0031, + "max_requirement": 0.0043 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0010, + "max_requirement": 0.0013 + }, + "粗蛋白 (%)": { + "min_requirement": 0.12, + "max_requirement": 0.14 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "总磷 (%)": { + "min_requirement": 0.004, + "max_requirement": 0.0055 + }, + "有效磷 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0030 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2868.0, + "max_requirement": 3107.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + } + }, + "皮长大 (PL Y)": { + "保育期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.012, + "max_requirement": 0.015 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0072, + "max_requirement": 0.0105 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0078, + "max_requirement": 0.0108 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0022, + "max_requirement": 0.0030 + }, + "粗蛋白 (%)": { + "min_requirement": 0.18, + "max_requirement": 0.22 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.009, + "max_requirement": 0.012 + }, + "总磷 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0045 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3226.5, + "max_requirement": 3585.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥前期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0094, + "max_requirement": 0.0110 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0055, + "max_requirement": 0.0073 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0058, + "max_requirement": 0.0077 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0016, + "max_requirement": 0.0022 + }, + "粗蛋白 (%)": { + "min_requirement": 0.16, + "max_requirement": 0.18 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.007, + "max_requirement": 0.009 + }, + "总磷 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "有效磷 (%)": { + "min_requirement": 0.002, + "max_requirement": 0.0040 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 3107.0, + "max_requirement": 3346.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "育肥后期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0081, + "max_requirement": 0.0090 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.0058 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0048, + "max_requirement": 0.0061 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0013, + "max_requirement": 0.0018 + }, + "粗蛋白 (%)": { + "min_requirement": 0.14, + "max_requirement": 0.16 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.006, + "max_requirement": 0.008 + }, + "总磷 (%)": { + "min_requirement": 0.0045, + "max_requirement": 0.006 + }, + "有效磷 (%)": { + "min_requirement": 0.0018, + "max_requirement": 0.0035 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2987.5, + "max_requirement": 3226.5 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素B1 (μg/kg)": { + "max_requirement": 10 + }, + "呕吐毒素DON (μg/kg)": { + "max_requirement": 1 + }, + "玉米赤霉烯酮ZEN (μg/kg)": { + "max_requirement": 0.15 + } + }, + "二次育肥期": { + "可消化赖氨酸 (SID %)": { + "min_requirement": 0.0053, + "max_requirement": 0.0065 + }, + "蛋+胱氨酸 (%)": { + "min_requirement": 0.0030, + "max_requirement": 0.0041 + }, + "可消化苏氨酸 (SID %)": { + "min_requirement": 0.0031, + "max_requirement": 0.0043 + }, + "可消化色氨酸 (SID %)": { + "min_requirement": 0.0010, + "max_requirement": 0.0013 + }, + "粗蛋白 (%)": { + "min_requirement": 0.12, + "max_requirement": 0.14 + }, + "粗脂肪 (%)": { + "min_requirement": 0.03, + "max_requirement": 0.06 + }, + "粗纤维 (%)": { + "min_requirement": 0.02, + "max_requirement": 0.06 + }, + "钙 (%)": { + "min_requirement": 0.005, + "max_requirement": 0.007 + }, + "总磷 (%)": { + "min_requirement": 0.004, + "max_requirement": 0.0055 + }, + "有效磷 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0030 + }, + "代谢能 (kcal/kg)": { + "min_requirement": 2868.0, + "max_requirement": 3107.0 + }, + "钠 (%)": { + "min_requirement": 0.0015, + "max_requirement": 0.0025 + }, + "氯 (%)": { + "min_requirement": 0.0025, + "max_requirement": 0.0045 + }, + "黄曲霉毒素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": "抗应激能力中等,对疫病和环境变化相对敏感;无法作为种猪进行自繁。" + }, + "杜大长 (DY L)": { + "description": "杜大长是另一种重要的外三元猪,与杜长大体系相似,但在母本的搭配上有所区别,同样追求高生长速度和高瘦肉率。", + "parent_info": "终端父本:杜洛克 (D);二元母本:大约克 (Y) × 长白 (L)。", + "appearance_features": "全身白色,体型比杜长大略微魁梧,肌肉丰满度高。", + "breed_advantages": "生长性能和瘦肉率与杜长大相当,同时遗传了大约克母本的良好体型和生长潜力,综合性能优秀。", + "breed_disadvantages": "与杜长大相似,无法留作种用,需要依赖稳定的种源体系;对饲养管理要求高。" + }, + "皮长大 (PL Y)": { + "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 + } + }, + "杜大长 (DY L)": { + "保育期": { + "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 + } + } + } + } +} diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 60bf2fa..0348367 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -52,4 +52,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 2. 迁移配置文件, 实现从json文件中读取原材料营养预设值, 并自动写入数据库 3. 定义配方领域, 实现营养元素的增删改查 4. 实现原材料的增删改查和仓库层的原料库存记录表增查 -5. 定义猪的模型和营养需求模型 \ No newline at end of file +5. 定义猪的模型和营养需求模型 +6. 实现从json读取猪营养需求并写入数据库 \ No newline at end of file diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index c595d60..186c03f 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -26,60 +26,87 @@ type SeederFunc func(tx *gorm.DB, jsonData []byte) error func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { logger := logs.TraceLogger(ctx, ctx, "SeedFromPreset") - // 定义必须存在的预设数据类型 - requiredTypes := []string{"nutrient"} - processedTypes := make(map[string]bool) + // 定义必须存在的预设数据类型及其处理顺序 + // 确保 "nutrient" 在 "pig_nutrient_requirements" 之前处理,因为后者依赖于前者。 + processingOrder := []string{"nutrient", "pig_nutrient_requirements"} + requiredTypes := make(map[string]bool) + for _, t := range processingOrder { + requiredTypes[t] = true + } - // 用于检测重复的 type - typeToFileMap := make(map[string]string) + 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 _, file := range files { - if filepath.Ext(file.Name()) != ".json" { - continue + for _, dataTypeStr := range processingOrder { + jsonDatas, ok := groupedFiles[dataTypeStr] + if !ok { + // 如果是必需类型但没有找到文件,则报错 + if requiredTypes[dataTypeStr] { + return fmt.Errorf("预设数据校验失败: 缺少必需的预设文件类型: '%s'", dataTypeStr) + } + 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() - - if existingFile, found := typeToFileMap[dataTypeStr]; found { - return fmt.Errorf("预设数据校验失败: type '%s' 在文件 '%s' 和 '%s' 中重复定义", dataTypeStr, existingFile, filePath) - } - typeToFileMap[dataTypeStr] = filePath - var seederFunc SeederFunc switch dataTypeStr { case "nutrient": seederFunc = seedNutrients + case "pig_nutrient_requirements": + seederFunc = seedPigNutrientRequirements default: - logger.Warnf("警告: 文件 '%s' 中存在未知的 type: '%s',已跳过", filePath, dataTypeStr) + logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr) continue } - if err := seederFunc(tx, jsonData); err != nil { - return fmt.Errorf("处理文件 '%s' (type: %s) 时发生错误: %w", filePath, dataTypeStr, err) + for _, jsonData := range jsonDatas { + // 获取原始文件路径用于错误报告 + originalFilePath := typeToFileMap[dataTypeStr] + if err := seederFunc(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 { + for reqType := range requiredTypes { if !processedTypes[reqType] { missingTypes = append(missingTypes, reqType) } @@ -158,6 +185,280 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { return nil } +// seedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 +func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { + // 1. 严格校验JSON文件,检查内部重复键 + parsedData, err := validateAndParsePigNutrientRequirementJSON(jsonData) + if 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. 将通过校验的、干净的数据写入数据库 + for breedName, ageStagesData := range parsedData { + 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 fmt.Errorf("预设猪品种 '%s' 失败: %w", breedName, err) + } + + for ageStageName, nutrientsData := range ageStagesData { + 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 fmt.Errorf("预设猪年龄阶段 '%s' 失败: %w", ageStageName, err) + } + + 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 fmt.Errorf("预设猪类型 '%s' - '%s' 失败: %w", breedName, ageStageName, err) + } + + for nutrientName, requirement := range nutrientsData { + 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 fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) + } + + linkData := models.PigNutrientRequirement{ + PigTypeID: pigType.ID, + NutrientID: nutrient.ID, + MinRequirement: requirement.MinRequirement, + MaxRequirement: requirement.MaxRequirement, + } + // 使用 FirstOrCreate 确保关联的唯一性 + if err := tx.Where(models.PigNutrientRequirement{ + PigTypeID: pigType.ID, + NutrientID: nutrient.ID, + }).FirstOrCreate(&linkData, linkData).Error; err != nil { + return fmt.Errorf("为猪类型 '%s' - '%s' 和营养素 '%s' 创建营养需求失败: %w", breedName, ageStageName, nutrientName, err) + } + } + } + } + return nil +} + +// validateAndParsePigNutrientRequirementJSON 严格校验并解析猪营养需求JSON文件 +func validateAndParsePigNutrientRequirementJSON(jsonData []byte) (map[string]map[string]map[string]struct { + MinRequirement float32 + MaxRequirement float32 +}, error) { + dataNode := gjson.GetBytes(jsonData, "data") + if !dataNode.Exists() { + return nil, errors.New("JSON文件中缺少 'data' 字段") + } + if !dataNode.IsObject() { + return nil, 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 nil, fmt.Errorf("'data' 字段解析起始符失败: %v", err) + } + + result := make(map[string]map[string]map[string]struct { + MinRequirement float32 + MaxRequirement float32 + }) + seenBreeds := make(map[string]bool) + + for decoder.More() { + // 解析 PigBreed 名称 + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("解析猪品种名称失败: %w", err) + } + breedName := t.(string) + if seenBreeds[breedName] { + return nil, fmt.Errorf("猪品种名称 '%s' 重复", breedName) + } + seenBreeds[breedName] = true + + // 解析该品种的年龄阶段对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return nil, fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName) + } + + ageStages := make(map[string]map[string]struct { + MinRequirement float32 + MaxRequirement float32 + }) + seenAgeStages := make(map[string]bool) + + for decoder.More() { + // 解析 PigAgeStage 名称 + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err) + } + ageStageName := t.(string) + if seenAgeStages[ageStageName] { + return nil, fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName) + } + seenAgeStages[ageStageName] = true + + // 解析该年龄阶段的营养成分对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return nil, fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName) + } + + nutrients := make(map[string]struct { + MinRequirement float32 + MaxRequirement float32 + }) + seenNutrients := make(map[string]bool) + + for decoder.More() { + // 解析 Nutrient 名称 + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err) + } + nutrientName := t.(string) + if seenNutrients[nutrientName] { + return nil, fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName) + } + seenNutrients[nutrientName] = true + + // 解析 min_requirement 和 max_requirement 对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return nil, fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName) + } + + var req struct { + MinRequirement float32 + MaxRequirement float32 + } + for decoder.More() { + t, err := decoder.Token() + if err != nil { + return nil, fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err) + } + key := t.(string) + + t, err = decoder.Token() + if err != nil { + return nil, fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err) + } + if value, ok := t.(json.Number); ok { + f64, _ := value.Float64() + if key == "min_requirement" { + req.MinRequirement = float32(f64) + } else if key == "max_requirement" { + req.MaxRequirement = float32(f64) + } else { + return nil, fmt.Errorf("营养素 '%s' 中存在未知键 '%s'", nutrientName, key) + } + } else { + return nil, fmt.Errorf("期望营养素 '%s' 的 '%s' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, key, t, t) + } + } + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return nil, fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) + } + nutrients[nutrientName] = req + } + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return nil, fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) + } + ageStages[ageStageName] = nutrients + } + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return nil, fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) + } + result[breedName] = ageStages + } + return result, nil +} + // validateAndParseNutrientJSON 使用 json.Decoder 手动解析,以捕获重复的键。 func validateAndParseNutrientJSON(jsonData []byte) (map[string]map[string]float32, error) { dataNode := gjson.GetBytes(jsonData, "data") diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 132c270..788e575 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -3,8 +3,12 @@ package models // PigBreed 猪品种模型 type PigBreed struct { Model - Name string `gorm:"size:50;not null;comment:品种名称"` - Description string `gorm:"size:255;comment:品种描述"` + 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 { -- 2.49.1 From a669bfda6c95b2c99f62205d421d9a9c595cd8e9 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 20 Nov 2025 23:01:08 +0800 Subject: [PATCH 14/59] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/pig_nutrient_requirement.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/presets-data/pig_nutrient_requirement.json b/config/presets-data/pig_nutrient_requirement.json index c6aacf8..bf3adaf 100644 --- a/config/presets-data/pig_nutrient_requirement.json +++ b/config/presets-data/pig_nutrient_requirement.json @@ -255,7 +255,7 @@ } } }, - "杜大长 (DY L)": { + "杜大长 (DYL)": { "保育期": { "可消化赖氨酸 (SID %)": { "min_requirement": 0.012, @@ -509,7 +509,7 @@ } } }, - "皮长大 (PL Y)": { + "皮长大 (PLY)": { "保育期": { "可消化赖氨酸 (SID %)": { "min_requirement": 0.012, @@ -773,14 +773,14 @@ "breed_advantages": "生长速度快、日增重极高、饲料转化率最优、瘦肉率稳定在60%以上、适应性良好、出栏时间最短。", "breed_disadvantages": "抗应激能力中等,对疫病和环境变化相对敏感;无法作为种猪进行自繁。" }, - "杜大长 (DY L)": { + "杜大长 (DYL)": { "description": "杜大长是另一种重要的外三元猪,与杜长大体系相似,但在母本的搭配上有所区别,同样追求高生长速度和高瘦肉率。", "parent_info": "终端父本:杜洛克 (D);二元母本:大约克 (Y) × 长白 (L)。", "appearance_features": "全身白色,体型比杜长大略微魁梧,肌肉丰满度高。", "breed_advantages": "生长性能和瘦肉率与杜长大相当,同时遗传了大约克母本的良好体型和生长潜力,综合性能优秀。", "breed_disadvantages": "与杜长大相似,无法留作种用,需要依赖稳定的种源体系;对饲养管理要求高。" }, - "皮长大 (PL Y)": { + "皮长大 (PLY)": { "description": "皮长大是以皮特兰作为终端父本的杂交体系,专注于生产超高瘦肉率的商品肉猪。", "parent_info": "终端父本:皮特兰 (P);二元母本:长白 (L) × 大约克 (Y)。", "appearance_features": "多数为白色或带有黑色斑点,肌肉极其发达,后臀饱满,体型呈方形。", @@ -833,7 +833,7 @@ "max_weight": 140000.0 } }, - "杜大长 (DY L)": { + "杜大长 (DYL)": { "保育期": { "description": "与杜长大相近,生长潜力强,管理重点为断奶适应与稳定采食。", "daily_feed_intake": 400.0, -- 2.49.1 From 9996fcfd747e5436245da97529dc8796e0270df5 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 21 Nov 2025 15:03:42 +0800 Subject: [PATCH 15/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E9=A2=86=E5=9F=9F=E5=85=B3=E4=BA=8E=E7=8C=AA=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=92=8C=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82=E7=9A=84=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=94=B9=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- docs/docs.go | 34 +-- docs/swagger.json | 34 +-- docs/swagger.yaml | 24 +- internal/domain/recipe/recipe_service.go | 236 ++++++++++++++- internal/infra/models/pig.go | 25 +- .../infra/repository/pig_type_repository.go | 281 ++++++++++++++++++ project_structure.txt | 4 + 8 files changed, 565 insertions(+), 76 deletions(-) create mode 100644 internal/infra/repository/pig_type_repository.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 0348367..9c25944 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -53,4 +53,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 3. 定义配方领域, 实现营养元素的增删改查 4. 实现原材料的增删改查和仓库层的原料库存记录表增查 5. 定义猪的模型和营养需求模型 -6. 实现从json读取猪营养需求并写入数据库 \ No newline at end of file +6. 实现从json读取猪营养需求并写入数据库 +7. 实现配方领域关于猪模型和营养需求的增删改查 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index dc7ca45..a8352fe 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1826,7 +1826,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -1836,12 +1835,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1851,7 +1850,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -7563,32 +7563,24 @@ const docTemplate = `{ "models.PigBatchStatus": { "type": "string", "enum": [ - "保育", - "生长", - "育肥", + "生产中", "待售", "已出售", "已归档" ], "x-enum-comments": { + "BatchStatusActive": "饲养中", "BatchStatusArchived": "批次结束(如全群淘汰等)", - "BatchStatusFinishing": "最后的育肥阶段", - "BatchStatusForSale": "达到出栏标准", - "BatchStatusGrowing": "生长育肥阶段", - "BatchStatusWeaning": "从断奶到保育结束" + "BatchStatusForSale": "达到出栏标准" }, "x-enum-descriptions": [ - "从断奶到保育结束", - "生长育肥阶段", - "最后的育肥阶段", + "饲养中", "达到出栏标准", "", "批次结束(如全群淘汰等)" ], "x-enum-varnames": [ - "BatchStatusWeaning", - "BatchStatusGrowing", - "BatchStatusFinishing", + "BatchStatusActive", "BatchStatusForSale", "BatchStatusSold", "BatchStatusArchived" @@ -7837,7 +7829,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -7847,10 +7838,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -7860,7 +7851,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index e300abb..01c16a5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1818,7 +1818,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -1828,12 +1827,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -1843,7 +1842,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -7555,32 +7555,24 @@ "models.PigBatchStatus": { "type": "string", "enum": [ - "保育", - "生长", - "育肥", + "生产中", "待售", "已出售", "已归档" ], "x-enum-comments": { + "BatchStatusActive": "饲养中", "BatchStatusArchived": "批次结束(如全群淘汰等)", - "BatchStatusFinishing": "最后的育肥阶段", - "BatchStatusForSale": "达到出栏标准", - "BatchStatusGrowing": "生长育肥阶段", - "BatchStatusWeaning": "从断奶到保育结束" + "BatchStatusForSale": "达到出栏标准" }, "x-enum-descriptions": [ - "从断奶到保育结束", - "生长育肥阶段", - "最后的育肥阶段", + "饲养中", "达到出栏标准", "", "批次结束(如全群淘汰等)" ], "x-enum-varnames": [ - "BatchStatusWeaning", - "BatchStatusGrowing", - "BatchStatusFinishing", + "BatchStatusActive", "BatchStatusForSale", "BatchStatusSold", "BatchStatusArchived" @@ -7829,7 +7821,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -7839,10 +7830,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -7852,7 +7843,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 50e51df..d9aff30 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1905,30 +1905,22 @@ definitions: - TreatmentLocationSickBay models.PigBatchStatus: enum: - - 保育 - - 生长 - - 育肥 + - 生产中 - 待售 - 已出售 - 已归档 type: string x-enum-comments: + BatchStatusActive: 饲养中 BatchStatusArchived: 批次结束(如全群淘汰等) - BatchStatusFinishing: 最后的育肥阶段 BatchStatusForSale: 达到出栏标准 - BatchStatusGrowing: 生长育肥阶段 - BatchStatusWeaning: 从断奶到保育结束 x-enum-descriptions: - - 从断奶到保育结束 - - 生长育肥阶段 - - 最后的育肥阶段 + - 饲养中 - 达到出栏标准 - "" - 批次结束(如全群淘汰等) x-enum-varnames: - - BatchStatusWeaning - - BatchStatusGrowing - - BatchStatusFinishing + - BatchStatusActive - BatchStatusForSale - BatchStatusSold - BatchStatusArchived @@ -2129,7 +2121,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2140,10 +2131,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2154,6 +2145,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -3270,7 +3262,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -3281,12 +3272,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -3297,6 +3288,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index f6b22e2..c7d6483 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -14,12 +14,15 @@ import ( // 定义领域特定的错误 var ( - ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在") - ErrNutrientNotFound = fmt.Errorf("营养种类不存在") - ErrNutrientInUse = fmt.Errorf("营养种类正在被原料使用,无法删除") - + ErrNutrientNameConflict = fmt.Errorf("营养种类名称已存在") + ErrNutrientNotFound = fmt.Errorf("营养种类不存在") ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") ErrRawMaterialNotFound = fmt.Errorf("原料不存在") + ErrPigBreedInUse = fmt.Errorf("猪品种正在被猪类型使用,无法删除") + ErrPigBreedNotFound = fmt.Errorf("猪品种不存在") + ErrPigAgeStageInUse = fmt.Errorf("猪年龄阶段正在被猪类型使用,无法删除") + ErrPigAgeStageNotFound = fmt.Errorf("猪年龄阶段不存在") + ErrPigTypeNotFound = fmt.Errorf("猪类型不存在") ) // Service 定义了配方与原料领域的核心业务服务接口 @@ -37,6 +40,27 @@ type Service interface { DeleteRawMaterial(ctx context.Context, id uint32) error GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error) ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) + + // 猪品种相关接口 + 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) + + // 猪年龄阶段相关接口 + 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) + + // 猪类型相关接口 + 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) } // recipeServiceImpl 是 RecipeService 的实现 @@ -44,14 +68,16 @@ type recipeServiceImpl struct { ctx context.Context nutrientRepo repository.NutrientRepository rawMaterialRepo repository.RawMaterialRepository + pigTypeRepo repository.PigTypeRepository } // NewRecipeService 创建一个新的 RecipeService 实例 -func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository) Service { +func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository, pigTypeRepo repository.PigTypeRepository) Service { return &recipeServiceImpl{ ctx: ctx, nutrientRepo: nutrientRepo, rawMaterialRepo: rawMaterialRepo, + pigTypeRepo: pigTypeRepo, } } @@ -262,3 +288,203 @@ func (s *recipeServiceImpl) ListRawMaterials(ctx context.Context, page, pageSize } return rawMaterials, total, nil } + +// CreatePigBreed 实现了创建猪品种的核心业务逻辑 +func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 +} + +// CreatePigAgeStage 实现了创建猪年龄阶段的核心业务逻辑 +func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 +} + +// CreatePigType 实现了创建猪类型的核心业务逻辑 +func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 +} diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 788e575..3796a3c 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -4,7 +4,7 @@ package models type PigBreed struct { Model Name string `gorm:"size:50;not null;comment:品种名称"` - Description string `gorm:"type:text" json:"description"` // 保留描述字段 + 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"` // 品种优点 @@ -29,17 +29,18 @@ func (PigAgeStage) TableName() string { // 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)"` + 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 { diff --git a/internal/infra/repository/pig_type_repository.go b/internal/infra/repository/pig_type_repository.go new file mode 100644 index 0000000..05a9e65 --- /dev/null +++ b/internal/infra/repository/pig_type_repository.go @@ -0,0 +1,281 @@ +package repository + +import ( + "context" + + "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) +} + +// 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").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").Find(&results).Error + + return results, total, err +} diff --git a/project_structure.txt b/project_structure.txt index dbd7129..ab57131 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -1,4 +1,6 @@  +.continue/mcpServers/new-mcp-server.yaml +.continue/rules/new-rule.md .gitignore AGENTS.md Makefile @@ -10,6 +12,7 @@ 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 @@ -163,6 +166,7 @@ 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/repository.go -- 2.49.1 From 534891309c5d314b4a8e619c90f541646784b594 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 21 Nov 2025 16:02:06 +0800 Subject: [PATCH 16/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E9=A2=86=E5=9F=9F=E7=9A=84web=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- docs/docs.go | 1793 +++++++++++++++++ docs/swagger.json | 1793 +++++++++++++++++ docs/swagger.yaml | 1111 ++++++++++ internal/app/api/api.go | 5 + internal/app/api/router.go | 40 + .../app/controller/feed/feed_controller.go | 875 ++++++++ internal/app/dto/feed_converter.go | 202 ++ internal/app/dto/feed_dto.go | 261 +++ .../app/service/feed_management_service.go | 529 +++++ internal/core/application.go | 1 + internal/core/component_initializers.go | 20 + .../infra/repository/pig_type_repository.go | 4 +- project_structure.txt | 4 + 14 files changed, 6638 insertions(+), 3 deletions(-) create mode 100644 internal/app/controller/feed/feed_controller.go create mode 100644 internal/app/dto/feed_converter.go create mode 100644 internal/app/dto/feed_dto.go create mode 100644 internal/app/service/feed_management_service.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 9c25944..379a678 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -54,4 +54,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 4. 实现原材料的增删改查和仓库层的原料库存记录表增查 5. 定义猪的模型和营养需求模型 6. 实现从json读取猪营养需求并写入数据库 -7. 实现配方领域关于猪模型和营养需求的增删改查 \ No newline at end of file +7. 实现配方领域关于猪模型和营养需求的增删改查 +8. 实现配方领域的web接口 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index a8352fe..6493ec4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1645,6 +1645,1254 @@ const docTemplate = `{ } } }, + "/api/v1/feed/nutrients": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有营养种类的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取营养种类列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListNutrientResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的营养种类。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建营养种类", + "parameters": [ + { + "description": "营养种类信息", + "name": "nutrient", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateNutrientRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/nutrients/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个营养种类的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取营养种类详情", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新营养种类信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新营养种类", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的营养种类信息", + "name": "nutrient", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateNutrientRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除营养种类。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除营养种类", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-age-stages": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪年龄阶段的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪年龄阶段列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪年龄阶段。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪年龄阶段", + "parameters": [ + { + "description": "猪年龄阶段信息", + "name": "pigAgeStage", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigAgeStageRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-age-stages/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪年龄阶段的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪年龄阶段详情", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪年龄阶段信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪年龄阶段", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪年龄阶段信息", + "name": "pigAgeStage", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigAgeStageRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪年龄阶段。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪年龄阶段", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-breeds": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪品种的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪品种列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigBreedResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪品种。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪品种", + "parameters": [ + { + "description": "猪品种信息", + "name": "pigBreed", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigBreedRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-breeds/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪品种的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪品种详情", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪品种信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪品种", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪品种信息", + "name": "pigBreed", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigBreedRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪品种。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪品种", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-types": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪类型的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪类型列表", + "parameters": [ + { + "type": "integer", + "description": "关联的猪年龄阶段ID", + "name": "age_stage_id", + "in": "query" + }, + { + "type": "string", + "description": "关联的猪年龄阶段名称 (用于模糊查询)", + "name": "age_stage_name", + "in": "query" + }, + { + "type": "integer", + "description": "关联的猪品种ID", + "name": "breed_id", + "in": "query" + }, + { + "type": "string", + "description": "关联的猪品种名称 (用于模糊查询)", + "name": "breed_name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigTypeResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪类型。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪类型", + "parameters": [ + { + "description": "猪类型信息", + "name": "pigType", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigTypeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-types/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪类型的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪类型详情", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪类型信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪类型", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪类型信息", + "name": "pigType", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigTypeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪类型。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪类型", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/raw-materials": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有原料的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取原料列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListRawMaterialResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的原料。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建原料", + "parameters": [ + { + "description": "原料信息", + "name": "rawMaterial", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateRawMaterialRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/raw-materials/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个原料的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取原料详情", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新原料信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新原料", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的原料信息", + "name": "rawMaterial", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRawMaterialRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除原料。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除原料", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -5173,6 +6421,24 @@ const docTemplate = `{ } } }, + "dto.CreateNutrientRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "营养素名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.CreatePenRequest": { "type": "object", "required": [ @@ -5192,6 +6458,57 @@ const docTemplate = `{ } } }, + "dto.CreatePigAgeStageRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "阶段描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "年龄阶段名称", + "type": "string", + "maxLength": 50 + } + } + }, + "dto.CreatePigBreedRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "appearance_features": { + "description": "外貌特征", + "type": "string" + }, + "breed_advantages": { + "description": "品种优点", + "type": "string" + }, + "breed_disadvantages": { + "description": "品种缺点", + "type": "string" + }, + "description": { + "description": "其他描述", + "type": "string" + }, + "name": { + "description": "品种名称", + "type": "string", + "maxLength": 50 + }, + "parent_info": { + "description": "父母信息", + "type": "string" + } + } + }, "dto.CreatePigHouseRequest": { "type": "object", "required": [ @@ -5206,6 +6523,52 @@ const docTemplate = `{ } } }, + "dto.CreatePigTypeRequest": { + "type": "object", + "required": [ + "age_stage_id", + "breed_id" + ], + "properties": { + "age_stage_id": { + "description": "关联的猪年龄阶段ID", + "type": "integer" + }, + "breed_id": { + "description": "关联的猪品种ID", + "type": "integer" + }, + "daily_feed_intake": { + "description": "理论日均食量 (g/天)", + "type": "number" + }, + "daily_gain_weight": { + "description": "理论日增重 (g/天)", + "type": "number" + }, + "description": { + "description": "该猪类型的描述或特点", + "type": "string", + "maxLength": 255 + }, + "max_days": { + "description": "该猪类型在该年龄阶段的最大日龄", + "type": "integer" + }, + "max_weight": { + "description": "该猪类型在该年龄阶段的最大体重 (g)", + "type": "number" + }, + "min_days": { + "description": "该猪类型在该年龄阶段的最小日龄", + "type": "integer" + }, + "min_weight": { + "description": "该猪类型在该年龄阶段的最小体重 (g)", + "type": "number" + } + } + }, "dto.CreatePlanRequest": { "type": "object", "required": [ @@ -5252,6 +6615,24 @@ const docTemplate = `{ } } }, + "dto.CreateRawMaterialRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "原料名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.CreateUserRequest": { "type": "object", "required": [ @@ -5549,6 +6930,20 @@ const docTemplate = `{ } } }, + "dto.ListNutrientResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.NutrientResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPendingCollectionResponse": { "type": "object", "properties": { @@ -5563,6 +6958,20 @@ const docTemplate = `{ } } }, + "dto.ListPigAgeStageResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPigBatchLogResponse": { "type": "object", "properties": { @@ -5577,6 +6986,20 @@ const docTemplate = `{ } } }, + "dto.ListPigBreedResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPigPurchaseResponse": { "type": "object", "properties": { @@ -5633,6 +7056,20 @@ const docTemplate = `{ } } }, + "dto.ListPigTypeResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPlanExecutionLogResponse": { "type": "object", "properties": { @@ -5662,6 +7099,20 @@ const docTemplate = `{ } } }, + "dto.ListRawMaterialResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -5885,6 +7336,26 @@ const docTemplate = `{ } } }, + "dto.NutrientResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PaginationDTO": { "type": "object", "properties": { @@ -5951,6 +7422,26 @@ const docTemplate = `{ } } }, + "dto.PigAgeStageResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigBatchCreateDTO": { "type": "object", "required": [ @@ -6128,6 +7619,38 @@ const docTemplate = `{ } } }, + "dto.PigBreedResponse": { + "type": "object", + "properties": { + "appearance_features": { + "type": "string" + }, + "breed_advantages": { + "type": "string" + }, + "breed_disadvantages": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_info": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigHouseResponse": { "type": "object", "properties": { @@ -6142,6 +7665,35 @@ const docTemplate = `{ } } }, + "dto.PigNutrientRequirementDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_requirement": { + "description": "最高营养需求量", + "type": "number" + }, + "min_requirement": { + "description": "最低营养需求量", + "type": "number" + }, + "nutrient_id": { + "type": "integer" + }, + "nutrient_name": { + "description": "营养素名称", + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigPurchaseDTO": { "type": "object", "properties": { @@ -6300,6 +7852,62 @@ const docTemplate = `{ } } }, + "dto.PigTypeResponse": { + "type": "object", + "properties": { + "age_stage_id": { + "type": "integer" + }, + "age_stage_name": { + "description": "猪年龄阶段名称", + "type": "string" + }, + "breed_id": { + "type": "integer" + }, + "breed_name": { + "description": "猪品种名称", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "daily_feed_intake": { + "type": "number" + }, + "daily_gain_weight": { + "type": "number" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_days": { + "type": "integer" + }, + "max_weight": { + "type": "number" + }, + "min_days": { + "type": "integer" + }, + "min_weight": { + "type": "number" + }, + "pig_nutrient_requirements": { + "description": "关联的营养需求", + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigNutrientRequirementDTO" + } + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PlanExecutionLogDTO": { "type": "object", "properties": { @@ -6405,6 +8013,58 @@ const docTemplate = `{ } } }, + "dto.RawMaterialNutrientDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nutrient_id": { + "type": "integer" + }, + "nutrient_name": { + "description": "营养素名称", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "value": { + "description": "营养价值含量", + "type": "number" + } + } + }, + "dto.RawMaterialResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "raw_material_nutrients": { + "description": "关联的营养素信息", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialNutrientDTO" + } + }, + "updated_at": { + "type": "string" + } + } + }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -7061,6 +8721,24 @@ const docTemplate = `{ } } }, + "dto.UpdateNutrientRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "营养素名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.UpdatePenRequest": { "type": "object", "required": [ @@ -7121,6 +8799,57 @@ const docTemplate = `{ } } }, + "dto.UpdatePigAgeStageRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "阶段描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "年龄阶段名称", + "type": "string", + "maxLength": 50 + } + } + }, + "dto.UpdatePigBreedRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "appearance_features": { + "description": "外貌特征", + "type": "string" + }, + "breed_advantages": { + "description": "品种优点", + "type": "string" + }, + "breed_disadvantages": { + "description": "品种缺点", + "type": "string" + }, + "description": { + "description": "其他描述", + "type": "string" + }, + "name": { + "description": "品种名称", + "type": "string", + "maxLength": 50 + }, + "parent_info": { + "description": "父母信息", + "type": "string" + } + } + }, "dto.UpdatePigHouseRequest": { "type": "object", "required": [ @@ -7135,6 +8864,52 @@ const docTemplate = `{ } } }, + "dto.UpdatePigTypeRequest": { + "type": "object", + "required": [ + "age_stage_id", + "breed_id" + ], + "properties": { + "age_stage_id": { + "description": "关联的猪年龄阶段ID", + "type": "integer" + }, + "breed_id": { + "description": "关联的猪品种ID", + "type": "integer" + }, + "daily_feed_intake": { + "description": "理论日均食量 (g/天)", + "type": "number" + }, + "daily_gain_weight": { + "description": "理论日增重 (g/天)", + "type": "number" + }, + "description": { + "description": "该猪类型的描述或特点", + "type": "string", + "maxLength": 255 + }, + "max_days": { + "description": "该猪类型在该年龄阶段的最大日龄", + "type": "integer" + }, + "max_weight": { + "description": "该猪类型在该年龄阶段的最大体重 (g)", + "type": "number" + }, + "min_days": { + "description": "该猪类型在该年龄阶段的最小日龄", + "type": "integer" + }, + "min_weight": { + "description": "该猪类型在该年龄阶段的最小体重 (g)", + "type": "number" + } + } + }, "dto.UpdatePlanRequest": { "type": "object", "required": [ @@ -7180,6 +8955,24 @@ const docTemplate = `{ } } }, + "dto.UpdateRawMaterialRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "原料名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.UserActionLogDTO": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 01c16a5..6dd2ea1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1637,6 +1637,1254 @@ } } }, + "/api/v1/feed/nutrients": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有营养种类的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取营养种类列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListNutrientResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的营养种类。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建营养种类", + "parameters": [ + { + "description": "营养种类信息", + "name": "nutrient", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateNutrientRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/nutrients/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个营养种类的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取营养种类详情", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新营养种类信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新营养种类", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的营养种类信息", + "name": "nutrient", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateNutrientRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.NutrientResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除营养种类。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除营养种类", + "parameters": [ + { + "type": "integer", + "description": "营养种类ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-age-stages": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪年龄阶段的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪年龄阶段列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪年龄阶段。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪年龄阶段", + "parameters": [ + { + "description": "猪年龄阶段信息", + "name": "pigAgeStage", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigAgeStageRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-age-stages/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪年龄阶段的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪年龄阶段详情", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪年龄阶段信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪年龄阶段", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪年龄阶段信息", + "name": "pigAgeStage", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigAgeStageRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪年龄阶段。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪年龄阶段", + "parameters": [ + { + "type": "integer", + "description": "猪年龄阶段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-breeds": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪品种的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪品种列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigBreedResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪品种。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪品种", + "parameters": [ + { + "description": "猪品种信息", + "name": "pigBreed", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigBreedRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-breeds/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪品种的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪品种详情", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪品种信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪品种", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪品种信息", + "name": "pigBreed", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigBreedRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪品种。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪品种", + "parameters": [ + { + "type": "integer", + "description": "猪品种ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/pig-types": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有猪类型的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪类型列表", + "parameters": [ + { + "type": "integer", + "description": "关联的猪年龄阶段ID", + "name": "age_stage_id", + "in": "query" + }, + { + "type": "string", + "description": "关联的猪年龄阶段名称 (用于模糊查询)", + "name": "age_stage_name", + "in": "query" + }, + { + "type": "integer", + "description": "关联的猪品种ID", + "name": "breed_id", + "in": "query" + }, + { + "type": "string", + "description": "关联的猪品种名称 (用于模糊查询)", + "name": "breed_name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListPigTypeResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的猪类型。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建猪类型", + "parameters": [ + { + "description": "猪类型信息", + "name": "pigType", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigTypeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/pig-types/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个猪类型的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取猪类型详情", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新猪类型信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新猪类型", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的猪类型信息", + "name": "pigType", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigTypeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除猪类型。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除猪类型", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/feed/raw-materials": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有原料的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取原料列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListRawMaterialResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的原料。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "创建原料", + "parameters": [ + { + "description": "原料信息", + "name": "rawMaterial", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateRawMaterialRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/raw-materials/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个原料的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "获取原料详情", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新原料信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "更新原料", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的原料信息", + "name": "rawMaterial", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRawMaterialRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除原料。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "删除原料", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -5165,6 +6413,24 @@ } } }, + "dto.CreateNutrientRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "营养素名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.CreatePenRequest": { "type": "object", "required": [ @@ -5184,6 +6450,57 @@ } } }, + "dto.CreatePigAgeStageRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "阶段描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "年龄阶段名称", + "type": "string", + "maxLength": 50 + } + } + }, + "dto.CreatePigBreedRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "appearance_features": { + "description": "外貌特征", + "type": "string" + }, + "breed_advantages": { + "description": "品种优点", + "type": "string" + }, + "breed_disadvantages": { + "description": "品种缺点", + "type": "string" + }, + "description": { + "description": "其他描述", + "type": "string" + }, + "name": { + "description": "品种名称", + "type": "string", + "maxLength": 50 + }, + "parent_info": { + "description": "父母信息", + "type": "string" + } + } + }, "dto.CreatePigHouseRequest": { "type": "object", "required": [ @@ -5198,6 +6515,52 @@ } } }, + "dto.CreatePigTypeRequest": { + "type": "object", + "required": [ + "age_stage_id", + "breed_id" + ], + "properties": { + "age_stage_id": { + "description": "关联的猪年龄阶段ID", + "type": "integer" + }, + "breed_id": { + "description": "关联的猪品种ID", + "type": "integer" + }, + "daily_feed_intake": { + "description": "理论日均食量 (g/天)", + "type": "number" + }, + "daily_gain_weight": { + "description": "理论日增重 (g/天)", + "type": "number" + }, + "description": { + "description": "该猪类型的描述或特点", + "type": "string", + "maxLength": 255 + }, + "max_days": { + "description": "该猪类型在该年龄阶段的最大日龄", + "type": "integer" + }, + "max_weight": { + "description": "该猪类型在该年龄阶段的最大体重 (g)", + "type": "number" + }, + "min_days": { + "description": "该猪类型在该年龄阶段的最小日龄", + "type": "integer" + }, + "min_weight": { + "description": "该猪类型在该年龄阶段的最小体重 (g)", + "type": "number" + } + } + }, "dto.CreatePlanRequest": { "type": "object", "required": [ @@ -5244,6 +6607,24 @@ } } }, + "dto.CreateRawMaterialRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "原料名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.CreateUserRequest": { "type": "object", "required": [ @@ -5541,6 +6922,20 @@ } } }, + "dto.ListNutrientResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.NutrientResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPendingCollectionResponse": { "type": "object", "properties": { @@ -5555,6 +6950,20 @@ } } }, + "dto.ListPigAgeStageResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigAgeStageResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPigBatchLogResponse": { "type": "object", "properties": { @@ -5569,6 +6978,20 @@ } } }, + "dto.ListPigBreedResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBreedResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPigPurchaseResponse": { "type": "object", "properties": { @@ -5625,6 +7048,20 @@ } } }, + "dto.ListPigTypeResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListPlanExecutionLogResponse": { "type": "object", "properties": { @@ -5654,6 +7091,20 @@ } } }, + "dto.ListRawMaterialResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -5877,6 +7328,26 @@ } } }, + "dto.NutrientResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PaginationDTO": { "type": "object", "properties": { @@ -5943,6 +7414,26 @@ } } }, + "dto.PigAgeStageResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigBatchCreateDTO": { "type": "object", "required": [ @@ -6120,6 +7611,38 @@ } } }, + "dto.PigBreedResponse": { + "type": "object", + "properties": { + "appearance_features": { + "type": "string" + }, + "breed_advantages": { + "type": "string" + }, + "breed_disadvantages": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent_info": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigHouseResponse": { "type": "object", "properties": { @@ -6134,6 +7657,35 @@ } } }, + "dto.PigNutrientRequirementDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_requirement": { + "description": "最高营养需求量", + "type": "number" + }, + "min_requirement": { + "description": "最低营养需求量", + "type": "number" + }, + "nutrient_id": { + "type": "integer" + }, + "nutrient_name": { + "description": "营养素名称", + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PigPurchaseDTO": { "type": "object", "properties": { @@ -6292,6 +7844,62 @@ } } }, + "dto.PigTypeResponse": { + "type": "object", + "properties": { + "age_stage_id": { + "type": "integer" + }, + "age_stage_name": { + "description": "猪年龄阶段名称", + "type": "string" + }, + "breed_id": { + "type": "integer" + }, + "breed_name": { + "description": "猪品种名称", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "daily_feed_intake": { + "type": "number" + }, + "daily_gain_weight": { + "type": "number" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_days": { + "type": "integer" + }, + "max_weight": { + "type": "number" + }, + "min_days": { + "type": "integer" + }, + "min_weight": { + "type": "number" + }, + "pig_nutrient_requirements": { + "description": "关联的营养需求", + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigNutrientRequirementDTO" + } + }, + "updated_at": { + "type": "string" + } + } + }, "dto.PlanExecutionLogDTO": { "type": "object", "properties": { @@ -6397,6 +8005,58 @@ } } }, + "dto.RawMaterialNutrientDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nutrient_id": { + "type": "integer" + }, + "nutrient_name": { + "description": "营养素名称", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "value": { + "description": "营养价值含量", + "type": "number" + } + } + }, + "dto.RawMaterialResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "raw_material_nutrients": { + "description": "关联的营养素信息", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialNutrientDTO" + } + }, + "updated_at": { + "type": "string" + } + } + }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -7053,6 +8713,24 @@ } } }, + "dto.UpdateNutrientRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "营养素名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.UpdatePenRequest": { "type": "object", "required": [ @@ -7113,6 +8791,57 @@ } } }, + "dto.UpdatePigAgeStageRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "阶段描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "年龄阶段名称", + "type": "string", + "maxLength": 50 + } + } + }, + "dto.UpdatePigBreedRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "appearance_features": { + "description": "外貌特征", + "type": "string" + }, + "breed_advantages": { + "description": "品种优点", + "type": "string" + }, + "breed_disadvantages": { + "description": "品种缺点", + "type": "string" + }, + "description": { + "description": "其他描述", + "type": "string" + }, + "name": { + "description": "品种名称", + "type": "string", + "maxLength": 50 + }, + "parent_info": { + "description": "父母信息", + "type": "string" + } + } + }, "dto.UpdatePigHouseRequest": { "type": "object", "required": [ @@ -7127,6 +8856,52 @@ } } }, + "dto.UpdatePigTypeRequest": { + "type": "object", + "required": [ + "age_stage_id", + "breed_id" + ], + "properties": { + "age_stage_id": { + "description": "关联的猪年龄阶段ID", + "type": "integer" + }, + "breed_id": { + "description": "关联的猪品种ID", + "type": "integer" + }, + "daily_feed_intake": { + "description": "理论日均食量 (g/天)", + "type": "number" + }, + "daily_gain_weight": { + "description": "理论日增重 (g/天)", + "type": "number" + }, + "description": { + "description": "该猪类型的描述或特点", + "type": "string", + "maxLength": 255 + }, + "max_days": { + "description": "该猪类型在该年龄阶段的最大日龄", + "type": "integer" + }, + "max_weight": { + "description": "该猪类型在该年龄阶段的最大体重 (g)", + "type": "number" + }, + "min_days": { + "description": "该猪类型在该年龄阶段的最小日龄", + "type": "integer" + }, + "min_weight": { + "description": "该猪类型在该年龄阶段的最小体重 (g)", + "type": "number" + } + } + }, "dto.UpdatePlanRequest": { "type": "object", "required": [ @@ -7172,6 +8947,24 @@ } } }, + "dto.UpdateRawMaterialRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "原料名称", + "type": "string", + "maxLength": 100 + } + } + }, "dto.UserActionLogDTO": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d9aff30..01fb86b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -273,6 +273,19 @@ definitions: - sensor_type - thresholds type: object + dto.CreateNutrientRequest: + properties: + description: + description: 描述 + maxLength: 255 + type: string + name: + description: 营养素名称 + maxLength: 100 + type: string + required: + - name + type: object dto.CreatePenRequest: properties: capacity: @@ -286,6 +299,43 @@ definitions: - house_id - pen_number type: object + dto.CreatePigAgeStageRequest: + properties: + description: + description: 阶段描述 + maxLength: 255 + type: string + name: + description: 年龄阶段名称 + maxLength: 50 + type: string + required: + - name + type: object + dto.CreatePigBreedRequest: + properties: + appearance_features: + description: 外貌特征 + type: string + breed_advantages: + description: 品种优点 + type: string + breed_disadvantages: + description: 品种缺点 + type: string + description: + description: 其他描述 + type: string + name: + description: 品种名称 + maxLength: 50 + type: string + parent_info: + description: 父母信息 + type: string + required: + - name + type: object dto.CreatePigHouseRequest: properties: description: @@ -295,6 +345,40 @@ definitions: required: - name type: object + dto.CreatePigTypeRequest: + properties: + age_stage_id: + description: 关联的猪年龄阶段ID + type: integer + breed_id: + description: 关联的猪品种ID + type: integer + daily_feed_intake: + description: 理论日均食量 (g/天) + type: number + daily_gain_weight: + description: 理论日增重 (g/天) + type: number + description: + description: 该猪类型的描述或特点 + maxLength: 255 + type: string + max_days: + description: 该猪类型在该年龄阶段的最大日龄 + type: integer + max_weight: + description: 该猪类型在该年龄阶段的最大体重 (g) + type: number + min_days: + description: 该猪类型在该年龄阶段的最小日龄 + type: integer + min_weight: + description: 该猪类型在该年龄阶段的最小体重 (g) + type: number + required: + - age_stage_id + - breed_id + type: object dto.CreatePlanRequest: properties: cron_expression: @@ -326,6 +410,19 @@ definitions: - execution_type - name type: object + dto.CreateRawMaterialRequest: + properties: + description: + description: 描述 + maxLength: 255 + type: string + name: + description: 原料名称 + maxLength: 100 + type: string + required: + - name + type: object dto.CreateUserRequest: properties: password: @@ -520,6 +617,15 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object + dto.ListNutrientResponse: + properties: + list: + items: + $ref: '#/definitions/dto.NutrientResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListPendingCollectionResponse: properties: list: @@ -529,6 +635,15 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object + dto.ListPigAgeStageResponse: + properties: + list: + items: + $ref: '#/definitions/dto.PigAgeStageResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListPigBatchLogResponse: properties: list: @@ -538,6 +653,15 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object + dto.ListPigBreedResponse: + properties: + list: + items: + $ref: '#/definitions/dto.PigBreedResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListPigPurchaseResponse: properties: list: @@ -574,6 +698,15 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object + dto.ListPigTypeResponse: + properties: + list: + items: + $ref: '#/definitions/dto.PigTypeResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListPlanExecutionLogResponse: properties: list: @@ -593,6 +726,15 @@ definitions: example: 100 type: integer type: object + dto.ListRawMaterialResponse: + properties: + list: + items: + $ref: '#/definitions/dto.RawMaterialResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListSensorDataResponse: properties: list: @@ -742,6 +884,19 @@ definitions: user_id: type: integer type: object + dto.NutrientResponse: + properties: + created_at: + type: string + description: + type: string + id: + type: integer + name: + type: string + updated_at: + type: string + type: object dto.PaginationDTO: properties: page: @@ -785,6 +940,19 @@ definitions: status: $ref: '#/definitions/models.PendingCollectionStatus' type: object + dto.PigAgeStageResponse: + properties: + created_at: + type: string + description: + type: string + id: + type: integer + name: + type: string + updated_at: + type: string + type: object dto.PigBatchCreateDTO: properties: batch_number: @@ -901,6 +1069,27 @@ definitions: - $ref: '#/definitions/models.PigBatchStatus' description: 批次状态,可选 type: object + dto.PigBreedResponse: + properties: + appearance_features: + type: string + breed_advantages: + type: string + breed_disadvantages: + type: string + created_at: + type: string + description: + type: string + id: + type: integer + name: + type: string + parent_info: + type: string + updated_at: + type: string + type: object dto.PigHouseResponse: properties: description: @@ -910,6 +1099,26 @@ definitions: name: type: string type: object + dto.PigNutrientRequirementDTO: + properties: + created_at: + type: string + id: + type: integer + max_requirement: + description: 最高营养需求量 + type: number + min_requirement: + description: 最低营养需求量 + type: number + nutrient_id: + type: integer + nutrient_name: + description: 营养素名称 + type: string + updated_at: + type: string + type: object dto.PigPurchaseDTO: properties: created_at: @@ -1014,6 +1223,44 @@ definitions: updated_at: type: string type: object + dto.PigTypeResponse: + properties: + age_stage_id: + type: integer + age_stage_name: + description: 猪年龄阶段名称 + type: string + breed_id: + type: integer + breed_name: + description: 猪品种名称 + type: string + created_at: + type: string + daily_feed_intake: + type: number + daily_gain_weight: + type: number + description: + type: string + id: + type: integer + max_days: + type: integer + max_weight: + type: number + min_days: + type: integer + min_weight: + type: number + pig_nutrient_requirements: + description: 关联的营养需求 + items: + $ref: '#/definitions/dto.PigNutrientRequirementDTO' + type: array + updated_at: + type: string + type: object dto.PlanExecutionLogDTO: properties: created_at: @@ -1080,6 +1327,41 @@ definitions: $ref: '#/definitions/dto.TaskResponse' type: array type: object + dto.RawMaterialNutrientDTO: + properties: + created_at: + type: string + id: + type: integer + nutrient_id: + type: integer + nutrient_name: + description: 营养素名称 + type: string + updated_at: + type: string + value: + description: 营养价值含量 + type: number + type: object + dto.RawMaterialResponse: + properties: + created_at: + type: string + description: + type: string + id: + type: integer + name: + type: string + raw_material_nutrients: + description: 关联的营养素信息 + items: + $ref: '#/definitions/dto.RawMaterialNutrientDTO' + type: array + updated_at: + type: string + type: object dto.ReclassifyPenToNewBatchRequest: properties: pen_id: @@ -1530,6 +1812,19 @@ definitions: - operator - thresholds type: object + dto.UpdateNutrientRequest: + properties: + description: + description: 描述 + maxLength: 255 + type: string + name: + description: 营养素名称 + maxLength: 100 + type: string + required: + - name + type: object dto.UpdatePenRequest: properties: capacity: @@ -1571,6 +1866,43 @@ definitions: required: - status type: object + dto.UpdatePigAgeStageRequest: + properties: + description: + description: 阶段描述 + maxLength: 255 + type: string + name: + description: 年龄阶段名称 + maxLength: 50 + type: string + required: + - name + type: object + dto.UpdatePigBreedRequest: + properties: + appearance_features: + description: 外貌特征 + type: string + breed_advantages: + description: 品种优点 + type: string + breed_disadvantages: + description: 品种缺点 + type: string + description: + description: 其他描述 + type: string + name: + description: 品种名称 + maxLength: 50 + type: string + parent_info: + description: 父母信息 + type: string + required: + - name + type: object dto.UpdatePigHouseRequest: properties: description: @@ -1580,6 +1912,40 @@ definitions: required: - name type: object + dto.UpdatePigTypeRequest: + properties: + age_stage_id: + description: 关联的猪年龄阶段ID + type: integer + breed_id: + description: 关联的猪品种ID + type: integer + daily_feed_intake: + description: 理论日均食量 (g/天) + type: number + daily_gain_weight: + description: 理论日增重 (g/天) + type: number + description: + description: 该猪类型的描述或特点 + maxLength: 255 + type: string + max_days: + description: 该猪类型在该年龄阶段的最大日龄 + type: integer + max_weight: + description: 该猪类型在该年龄阶段的最大体重 (g) + type: number + min_days: + description: 该猪类型在该年龄阶段的最小日龄 + type: integer + min_weight: + description: 该猪类型在该年龄阶段的最小体重 (g) + type: number + required: + - age_stage_id + - breed_id + type: object dto.UpdatePlanRequest: properties: cron_expression: @@ -1610,6 +1976,19 @@ definitions: required: - execution_type type: object + dto.UpdateRawMaterialRequest: + properties: + description: + description: 描述 + maxLength: 255 + type: string + name: + description: 原料名称 + maxLength: 100 + type: string + required: + - name + type: object dto.UserActionLogDTO: properties: action_type: @@ -3164,6 +3543,738 @@ paths: summary: 手动控制设备 tags: - 设备管理 + /api/v1/feed/nutrients: + get: + description: 获取所有营养种类的列表,支持分页和过滤。 + parameters: + - description: 按名称模糊查询 + in: query + name: name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListNutrientResponse' + type: object + security: + - BearerAuth: [] + summary: 获取营养种类列表 + tags: + - 饲料管理 + post: + consumes: + - application/json + description: 创建一个新的营养种类。 + parameters: + - description: 营养种类信息 + in: body + name: nutrient + required: true + schema: + $ref: '#/definitions/dto.CreateNutrientRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.NutrientResponse' + type: object + security: + - BearerAuth: [] + summary: 创建营养种类 + tags: + - 饲料管理 + /api/v1/feed/nutrients/{id}: + delete: + description: 根据ID删除营养种类。 + parameters: + - description: 营养种类ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除营养种类 + tags: + - 饲料管理 + get: + description: 根据ID获取单个营养种类的详细信息。 + parameters: + - description: 营养种类ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.NutrientResponse' + type: object + security: + - BearerAuth: [] + summary: 获取营养种类详情 + tags: + - 饲料管理 + put: + consumes: + - application/json + description: 根据ID更新营养种类信息。 + parameters: + - description: 营养种类ID + in: path + name: id + required: true + type: integer + - description: 更新后的营养种类信息 + in: body + name: nutrient + required: true + schema: + $ref: '#/definitions/dto.UpdateNutrientRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.NutrientResponse' + type: object + security: + - BearerAuth: [] + summary: 更新营养种类 + tags: + - 饲料管理 + /api/v1/feed/pig-age-stages: + get: + description: 获取所有猪年龄阶段的列表,支持分页和过滤。 + parameters: + - description: 按名称模糊查询 + in: query + name: name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListPigAgeStageResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪年龄阶段列表 + tags: + - 饲料管理 + post: + consumes: + - application/json + description: 创建一个新的猪年龄阶段。 + parameters: + - description: 猪年龄阶段信息 + in: body + name: pigAgeStage + required: true + schema: + $ref: '#/definitions/dto.CreatePigAgeStageRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigAgeStageResponse' + type: object + security: + - BearerAuth: [] + summary: 创建猪年龄阶段 + tags: + - 饲料管理 + /api/v1/feed/pig-age-stages/{id}: + delete: + description: 根据ID删除猪年龄阶段。 + parameters: + - description: 猪年龄阶段ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除猪年龄阶段 + tags: + - 饲料管理 + get: + description: 根据ID获取单个猪年龄阶段的详细信息。 + parameters: + - description: 猪年龄阶段ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigAgeStageResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪年龄阶段详情 + tags: + - 饲料管理 + put: + consumes: + - application/json + description: 根据ID更新猪年龄阶段信息。 + parameters: + - description: 猪年龄阶段ID + in: path + name: id + required: true + type: integer + - description: 更新后的猪年龄阶段信息 + in: body + name: pigAgeStage + required: true + schema: + $ref: '#/definitions/dto.UpdatePigAgeStageRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigAgeStageResponse' + type: object + security: + - BearerAuth: [] + summary: 更新猪年龄阶段 + tags: + - 饲料管理 + /api/v1/feed/pig-breeds: + get: + description: 获取所有猪品种的列表,支持分页和过滤。 + parameters: + - description: 按名称模糊查询 + in: query + name: name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListPigBreedResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪品种列表 + tags: + - 饲料管理 + post: + consumes: + - application/json + description: 创建一个新的猪品种。 + parameters: + - description: 猪品种信息 + in: body + name: pigBreed + required: true + schema: + $ref: '#/definitions/dto.CreatePigBreedRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBreedResponse' + type: object + security: + - BearerAuth: [] + summary: 创建猪品种 + tags: + - 饲料管理 + /api/v1/feed/pig-breeds/{id}: + delete: + description: 根据ID删除猪品种。 + parameters: + - description: 猪品种ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除猪品种 + tags: + - 饲料管理 + get: + description: 根据ID获取单个猪品种的详细信息。 + parameters: + - description: 猪品种ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBreedResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪品种详情 + tags: + - 饲料管理 + put: + consumes: + - application/json + description: 根据ID更新猪品种信息。 + parameters: + - description: 猪品种ID + in: path + name: id + required: true + type: integer + - description: 更新后的猪品种信息 + in: body + name: pigBreed + required: true + schema: + $ref: '#/definitions/dto.UpdatePigBreedRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBreedResponse' + type: object + security: + - BearerAuth: [] + summary: 更新猪品种 + tags: + - 饲料管理 + /api/v1/feed/pig-types: + get: + description: 获取所有猪类型的列表,支持分页和过滤。 + parameters: + - description: 关联的猪年龄阶段ID + in: query + name: age_stage_id + type: integer + - description: 关联的猪年龄阶段名称 (用于模糊查询) + in: query + name: age_stage_name + type: string + - description: 关联的猪品种ID + in: query + name: breed_id + type: integer + - description: 关联的猪品种名称 (用于模糊查询) + in: query + name: breed_name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListPigTypeResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪类型列表 + tags: + - 饲料管理 + post: + consumes: + - application/json + description: 创建一个新的猪类型。 + parameters: + - description: 猪类型信息 + in: body + name: pigType + required: true + schema: + $ref: '#/definitions/dto.CreatePigTypeRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigTypeResponse' + type: object + security: + - BearerAuth: [] + summary: 创建猪类型 + tags: + - 饲料管理 + /api/v1/feed/pig-types/{id}: + delete: + description: 根据ID删除猪类型。 + parameters: + - description: 猪类型ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除猪类型 + tags: + - 饲料管理 + get: + description: 根据ID获取单个猪类型的详细信息。 + parameters: + - description: 猪类型ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigTypeResponse' + type: object + security: + - BearerAuth: [] + summary: 获取猪类型详情 + tags: + - 饲料管理 + put: + consumes: + - application/json + description: 根据ID更新猪类型信息。 + parameters: + - description: 猪类型ID + in: path + name: id + required: true + type: integer + - description: 更新后的猪类型信息 + in: body + name: pigType + required: true + schema: + $ref: '#/definitions/dto.UpdatePigTypeRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigTypeResponse' + type: object + security: + - BearerAuth: [] + summary: 更新猪类型 + tags: + - 饲料管理 + /api/v1/feed/raw-materials: + get: + description: 获取所有原料的列表,支持分页和过滤。 + parameters: + - description: 按名称模糊查询 + in: query + name: name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListRawMaterialResponse' + type: object + security: + - BearerAuth: [] + summary: 获取原料列表 + tags: + - 饲料管理 + post: + consumes: + - application/json + description: 创建一个新的原料。 + parameters: + - description: 原料信息 + in: body + name: rawMaterial + required: true + schema: + $ref: '#/definitions/dto.CreateRawMaterialRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RawMaterialResponse' + type: object + security: + - BearerAuth: [] + summary: 创建原料 + tags: + - 饲料管理 + /api/v1/feed/raw-materials/{id}: + delete: + description: 根据ID删除原料。 + parameters: + - description: 原料ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除原料 + tags: + - 饲料管理 + get: + description: 根据ID获取单个原料的详细信息。 + parameters: + - description: 原料ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RawMaterialResponse' + type: object + security: + - BearerAuth: [] + summary: 获取原料详情 + tags: + - 饲料管理 + put: + consumes: + - application/json + description: 根据ID更新原料信息。 + parameters: + - description: 原料ID + in: path + name: id + required: true + type: integer + - description: 更新后的原料信息 + in: body + name: rawMaterial + required: true + schema: + $ref: '#/definitions/dto.UpdateRawMaterialRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RawMaterialResponse' + type: object + security: + - BearerAuth: [] + summary: 更新原料 + tags: + - 饲料管理 /api/v1/monitor/device-command-logs: get: description: 根据提供的过滤条件,分页获取设备命令日志 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 43804b4..627ac5d 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -21,6 +21,7 @@ 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/management" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/monitor" @@ -55,6 +56,7 @@ type API struct { monitorController *monitor.Controller // 数据监控控制器实例 healthController *health.Controller // 健康检查控制器实例 alarmController *alarm.ThresholdAlarmController // 阈值告警控制器 + feedController *feed.Controller // 饲料管理控制器实例 listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -72,6 +74,7 @@ func NewAPI(cfg config.ServerConfig, userService service.UserService, auditService service.AuditService, alarmService service.ThresholdAlarmService, + feedManagementService service.FeedManagementService, tokenGenerator token.Generator, listenHandler webhook.ListenHandler, ) *API { @@ -111,6 +114,8 @@ func NewAPI(cfg config.ServerConfig, healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")), // 在 NewAPI 中初始化阈 alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService), + // 在 NewAPI 中初始化饲料管理控制器 + feedController: feed.NewController(logs.AddCompName(baseCtx, "FeedController"), feedManagementService), } api.setupRoutes() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 6560d5b..800aeaa 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -212,6 +212,46 @@ func (a *API) setupRoutes() { } } logger.Debug("告警相关接口注册成功 (需要认证和审计)") + + // 饲料管理相关路由组 + feedGroup := authGroup.Group("/feed") + { + // 营养种类 (Nutrient) 路由 + feedGroup.POST("/nutrients", a.feedController.CreateNutrient) + feedGroup.PUT("/nutrients/:id", a.feedController.UpdateNutrient) + feedGroup.DELETE("/nutrients/:id", a.feedController.DeleteNutrient) + feedGroup.GET("/nutrients/:id", a.feedController.GetNutrient) + feedGroup.GET("/nutrients", a.feedController.ListNutrients) + + // 原料 (RawMaterial) 路由 + feedGroup.POST("/raw-materials", a.feedController.CreateRawMaterial) + feedGroup.PUT("/raw-materials/:id", a.feedController.UpdateRawMaterial) + feedGroup.DELETE("/raw-materials/:id", a.feedController.DeleteRawMaterial) + feedGroup.GET("/raw-materials/:id", a.feedController.GetRawMaterial) + feedGroup.GET("/raw-materials", a.feedController.ListRawMaterials) + + // 猪品种 (PigBreed) 路由 + feedGroup.POST("/pig-breeds", a.feedController.CreatePigBreed) + feedGroup.PUT("/pig-breeds/:id", a.feedController.UpdatePigBreed) + feedGroup.DELETE("/pig-breeds/:id", a.feedController.DeletePigBreed) + feedGroup.GET("/pig-breeds/:id", a.feedController.GetPigBreed) + feedGroup.GET("/pig-breeds", a.feedController.ListPigBreeds) + + // 猪年龄阶段 (PigAgeStage) 路由 + feedGroup.POST("/pig-age-stages", a.feedController.CreatePigAgeStage) + feedGroup.PUT("/pig-age-stages/:id", a.feedController.UpdatePigAgeStage) + feedGroup.DELETE("/pig-age-stages/:id", a.feedController.DeletePigAgeStage) + feedGroup.GET("/pig-age-stages/:id", a.feedController.GetPigAgeStage) + feedGroup.GET("/pig-age-stages", a.feedController.ListPigAgeStages) + + // 猪类型 (PigType) 路由 + feedGroup.POST("/pig-types", a.feedController.CreatePigType) + feedGroup.PUT("/pig-types/:id", a.feedController.UpdatePigType) + feedGroup.DELETE("/pig-types/:id", a.feedController.DeletePigType) + feedGroup.GET("/pig-types/:id", a.feedController.GetPigType) + feedGroup.GET("/pig-types", a.feedController.ListPigTypes) + } + logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") } logger.Debug("所有接口注册成功") diff --git a/internal/app/controller/feed/feed_controller.go b/internal/app/controller/feed/feed_controller.go new file mode 100644 index 0000000..6a5e4f1 --- /dev/null +++ b/internal/app/controller/feed/feed_controller.go @@ -0,0 +1,875 @@ +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" +) + +// Controller 定义了饲料管理相关的控制器 +type Controller struct { + ctx context.Context + feedManagementService service.FeedManagementService +} + +// NewController 创建一个新的 Controller 实例 +func NewController(ctx context.Context, feedManagementService service.FeedManagementService) *Controller { + return &Controller{ + ctx: ctx, + feedManagementService: feedManagementService, + } +} + +// --- 营养种类 (Nutrient) 接口方法实现 --- + +// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) +} + +// --- 原料 (RawMaterial) 接口方法实现 --- + +// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) +} + +// --- 猪品种 (PigBreed) 接口方法实现 --- + +// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) +} + +// --- 猪年龄阶段 (PigAgeStage) 接口方法实现 --- + +// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) +} + +// --- 猪类型 (PigType) 接口方法实现 --- + +// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) +} diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go new file mode 100644 index 0000000..43eda01 --- /dev/null +++ b/internal/app/dto/feed_converter.go @@ -0,0 +1,202 @@ +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 + } + return &NutrientResponse{ + ID: nutrient.ID, + CreatedAt: nutrient.CreatedAt, + UpdatedAt: nutrient.UpdatedAt, + Name: nutrient.Name, + Description: nutrient.Description, + } +} + +// 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, + CreatedAt: rmn.CreatedAt, + UpdatedAt: rmn.UpdatedAt, + NutrientID: rmn.NutrientID, + Nutrient: rmn.Nutrient.Name, // 假设 Nutrient 已经被预加载 + Value: rmn.Value, + } + } + + return &RawMaterialResponse{ + ID: rm.ID, + CreatedAt: rm.CreatedAt, + UpdatedAt: rm.UpdatedAt, + Name: rm.Name, + Description: rm.Description, + 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, + CreatedAt: breed.CreatedAt, + UpdatedAt: breed.UpdatedAt, + 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, + CreatedAt: ageStage.CreatedAt, + UpdatedAt: ageStage.UpdatedAt, + 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, + CreatedAt: pnr.CreatedAt, + UpdatedAt: pnr.UpdatedAt, + NutrientID: pnr.NutrientID, + NutrientName: pnr.Nutrient.Name, // 假设 Nutrient 已经被预加载 + MinRequirement: pnr.MinRequirement, + MaxRequirement: pnr.MaxRequirement, + } + } + + return &PigTypeResponse{ + ID: pt.ID, + CreatedAt: pt.CreatedAt, + UpdatedAt: pt.UpdatedAt, + 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, + }, + } +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go new file mode 100644 index 0000000..00afdd0 --- /dev/null +++ b/internal/app/dto/feed_dto.go @@ -0,0 +1,261 @@ +package dto + +import ( + "time" +) + +// ============================================================================================================= +// 营养种类 (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"` // 描述 +} + +// NutrientResponse 营养种类响应体 +type NutrientResponse struct { + ID uint32 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Description string `json:"description"` +} + +// ListNutrientRequest 定义了获取营养种类列表的请求参数 +type ListNutrientRequest 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" +} + +// 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"` // 描述 +} + +// UpdateRawMaterialRequest 更新原料的请求体 +type UpdateRawMaterialRequest struct { + Name string `json:"name" validate:"required,max=100"` // 原料名称 + Description string `json:"description" validate:"max=255"` // 描述 +} + +// RawMaterialNutrientDTO 原料营养素响应体 +type RawMaterialNutrientDTO struct { + ID uint32 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + NutrientID uint32 `json:"nutrient_id"` + Nutrient string `json:"nutrient_name"` // 营养素名称 + Value float32 `json:"value"` // 营养价值含量 +} + +// RawMaterialResponse 原料响应体 +type RawMaterialResponse struct { + ID uint32 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Description string `json:"description"` + 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"` // 按名称模糊查询 + OrderBy string `json:"order_by" query:"order_by"` // 排序字段,例如 "id DESC" +} + +// ListRawMaterialResponse 是获取原料列表的响应结构 +type ListRawMaterialResponse struct { + List []RawMaterialResponse `json:"list"` + Pagination PaginationDTO `json:"pagination"` +} + +// ============================================================================================================= +// 猪品种 (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"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + 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"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + 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"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + 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"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + 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"` +} diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go new file mode 100644 index 0000000..2c7fabc --- /dev/null +++ b/internal/app/service/feed_management_service.go @@ -0,0 +1,529 @@ +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 ( + ErrNutrientNameConflict = errors.New("营养种类名称已存在") + ErrNutrientNotFound = errors.New("营养种类不存在") + ErrRawMaterialNameConflict = errors.New("原料名称已存在") + ErrRawMaterialNotFound = errors.New("原料不存在") + ErrPigBreedInUse = errors.New("猪品种正在被猪类型使用,无法删除") + ErrPigBreedNotFound = errors.New("猪品种不存在") + ErrPigAgeStageInUse = errors.New("猪年龄阶段正在被猪类型使用,无法删除") + ErrPigAgeStageNotFound = errors.New("猪年龄阶段不存在") + ErrPigTypeNotFound = errors.New("猪类型不存在") +) + +// FeedManagementService 定义了饲料管理的应用服务接口 +type FeedManagementService 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) + + // 原料相关 + 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) + + // 猪品种相关 + 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) + + // 猪年龄阶段相关 + 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) + + // 猪类型相关 + 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) +} + +// feedManagementServiceImpl 是 FeedManagementService 接口的实现 +type feedManagementServiceImpl struct { + ctx context.Context + recipeSvc recipe.Service +} + +// NewFeedManagementService 创建一个新的 FeedManagementService 实例 +func NewFeedManagementService(ctx context.Context, recipeSvc recipe.Service) FeedManagementService { + return &feedManagementServiceImpl{ + ctx: ctx, + recipeSvc: recipeSvc, + } +} + +// ===================================================================================================================== +// 营养种类 (Nutrient) 实现 +// ===================================================================================================================== + +// CreateNutrient 创建营养种类 +func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) ListNutrients(ctx context.Context, req *dto.ListNutrientRequest) (*dto.ListNutrientResponse, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients") + + nutrients, total, err := s.recipeSvc.ListNutrients(serviceCtx, req.Page, req.PageSize) + if err != nil { + return nil, fmt.Errorf("获取营养种类列表失败: %w", err) + } + + return dto.ConvertNutrientListToDTO(nutrients, total, req.Page, req.PageSize), nil +} + +// ===================================================================================================================== +// 原料 (RawMaterial) 实现 +// ===================================================================================================================== + +// CreateRawMaterial 创建原料 +func (s *feedManagementServiceImpl) 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) + 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 *feedManagementServiceImpl) 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) + 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) ListRawMaterials(ctx context.Context, req *dto.ListRawMaterialRequest) (*dto.ListRawMaterialResponse, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials") + + rawMaterials, total, err := s.recipeSvc.ListRawMaterials(serviceCtx, req.Page, req.PageSize) + if err != nil { + return nil, fmt.Errorf("获取原料列表失败: %w", err) + } + + return dto.ConvertRawMaterialListToDTO(rawMaterials, total, req.Page, req.PageSize), nil +} + +// ===================================================================================================================== +// 猪品种 (PigBreed) 实现 +// ===================================================================================================================== + +// CreatePigBreed 创建猪品种 +func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 +} + +// ===================================================================================================================== +// 猪年龄阶段 (PigAgeStage) 实现 +// ===================================================================================================================== + +// CreatePigAgeStage 创建猪年龄阶段 +func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 +} + +// ===================================================================================================================== +// 猪类型 (PigType) 实现 +// ===================================================================================================================== + +// CreatePigType 创建猪类型 +func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 +} diff --git a/internal/core/application.go b/internal/core/application.go index 5270b0a..377fbff 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -63,6 +63,7 @@ func NewApplication(configPath string) (*Application, error) { appServices.userService, appServices.auditService, appServices.thresholdAlarmService, + appServices.feedManagementService, infra.tokenGenerator, infra.lora.listenHandler, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 9fbc395..10b213d 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -12,6 +12,7 @@ import ( 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" @@ -82,6 +83,9 @@ type Repositories struct { medicationLogRepo repository.MedicationLogRepository notificationRepo repository.NotificationRepository alarmRepo repository.AlarmRepository + pigTypeRepo repository.PigTypeRepository + rawMaterialRepo repository.RawMaterialRepository + nutrientRepo repository.NutrientRepository unitOfWork repository.UnitOfWork } @@ -110,6 +114,9 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories { medicationLogRepo: repository.NewGormMedicationLogRepository(logs.AddCompName(baseCtx, "MedicationLogRepo"), 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), unitOfWork: repository.NewGormUnitOfWork(logs.AddCompName(baseCtx, "UnitOfWork"), db), } } @@ -127,6 +134,7 @@ type DomainServices struct { planService plan.Service notifyService domain_notify.Service alarmService alarm.AlarmService + recipeService recipe.Service } // initDomainServices 初始化所有的领域服务。 @@ -206,6 +214,14 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr taskFactory, ) + // 配方管理服务 + recipeService := recipe.NewRecipeService( + logs.AddCompName(baseCtx, "RecipeService"), + infra.repos.nutrientRepo, + infra.repos.rawMaterialRepo, + infra.repos.pigTypeRepo, + ) + return &DomainServices{ pigPenTransferManager: pigPenTransferManager, pigTradeManager: pigTradeManager, @@ -218,6 +234,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr planService: planService, notifyService: notifyService, alarmService: alarmService, + recipeService: recipeService, }, nil } @@ -231,6 +248,7 @@ type AppServices struct { userService service.UserService auditService service.AuditService thresholdAlarmService service.ThresholdAlarmService + feedManagementService service.FeedManagementService } // initAppServices 初始化所有的应用服务。 @@ -278,6 +296,7 @@ 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) + feedManagementService := service.NewFeedManagementService(logs.AddCompName(baseCtx, "FeedManagementService"), domainServices.recipeService) return &AppServices{ pigFarmService: pigFarmService, @@ -288,6 +307,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices planService: planService, userService: userService, thresholdAlarmService: thresholdAlarmService, + feedManagementService: feedManagementService, } } diff --git a/internal/infra/repository/pig_type_repository.go b/internal/infra/repository/pig_type_repository.go index 05a9e65..a975b9b 100644 --- a/internal/infra/repository/pig_type_repository.go +++ b/internal/infra/repository/pig_type_repository.go @@ -216,7 +216,7 @@ func (r *gormPigTypeRepository) CreatePigType(ctx context.Context, pigType *mode 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").First(&pigType, id).Error + err := r.db.WithContext(repoCtx).Preload("Breed").Preload("AgeStage").Preload("PigNutrientRequirements.Nutrient").First(&pigType, id).Error if err != nil { return nil, err } @@ -275,7 +275,7 @@ func (r *gormPigTypeRepository) ListPigTypes(ctx context.Context, opts PigTypeLi query = query.Order(orderBy) offset := (page - 1) * pageSize - err := query.Limit(pageSize).Offset(offset).Preload("Breed").Preload("AgeStage").Find(&results).Error + err := query.Limit(pageSize).Offset(offset).Preload("Breed").Preload("AgeStage").Preload("PigNutrientRequirements.Nutrient").Find(&results).Error return results, total, err } diff --git a/project_structure.txt b/project_structure.txt index ab57131..d1c4654 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -48,6 +48,7 @@ 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/feed_controller.go internal/app/controller/health/health_controller.go internal/app/controller/management/controller_helpers.go internal/app/controller/management/pig_batch_controller.go @@ -64,6 +65,8 @@ 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/monitor_converter.go internal/app/dto/monitor_dto.go internal/app/dto/notification_converter.go @@ -77,6 +80,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/feed_management_service.go internal/app/service/monitor_service.go internal/app/service/pig_batch_service.go internal/app/service/pig_farm_service.go -- 2.49.1 From 4224be8567a5ee150313c92785a59ce47094705b Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 21 Nov 2025 16:37:09 +0800 Subject: [PATCH 17/59] =?UTF-8?q?=E6=8B=93=E5=B1=95=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 40 ++++++++++++++++++++++++------ docs/swagger.json | 40 ++++++++++++++++++++++++------ docs/swagger.yaml | 25 ++++++++++++++++--- internal/app/dto/feed_converter.go | 22 ++++++++++++---- internal/app/dto/feed_dto.go | 18 ++++++++++---- 5 files changed, 115 insertions(+), 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 6493ec4..23b6121 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3074,6 +3074,7 @@ const docTemplate = `{ }, { "enum": [ + 7, -1, 0, 1, @@ -3083,12 +3084,12 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3098,8 +3099,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7336,6 +7336,23 @@ const docTemplate = `{ } } }, + "dto.NutrientRawMaterialDTO": { + "type": "object", + "properties": { + "id": { + "description": "原料ID", + "type": "integer" + }, + "name": { + "description": "原料名称", + "type": "string" + }, + "value": { + "description": "该原料中此营养素的含量", + "type": "number" + } + } + }, "dto.NutrientResponse": { "type": "object", "properties": { @@ -7351,6 +7368,13 @@ const docTemplate = `{ "name": { "type": "string" }, + "raw_materials": { + "description": "包含此营养的原料列表", + "type": "array", + "items": { + "$ref": "#/definitions/dto.NutrientRawMaterialDTO" + } + }, "updated_at": { "type": "string" } @@ -9622,6 +9646,7 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -9631,10 +9656,10 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9644,8 +9669,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 6dd2ea1..cb6b09e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3066,6 +3066,7 @@ }, { "enum": [ + 7, -1, 0, 1, @@ -3075,12 +3076,12 @@ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3090,8 +3091,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7328,6 +7328,23 @@ } } }, + "dto.NutrientRawMaterialDTO": { + "type": "object", + "properties": { + "id": { + "description": "原料ID", + "type": "integer" + }, + "name": { + "description": "原料名称", + "type": "string" + }, + "value": { + "description": "该原料中此营养素的含量", + "type": "number" + } + } + }, "dto.NutrientResponse": { "type": "object", "properties": { @@ -7343,6 +7360,13 @@ "name": { "type": "string" }, + "raw_materials": { + "description": "包含此营养的原料列表", + "type": "array", + "items": { + "$ref": "#/definitions/dto.NutrientRawMaterialDTO" + } + }, "updated_at": { "type": "string" } @@ -9614,6 +9638,7 @@ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -9623,10 +9648,10 @@ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9636,8 +9661,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 01fb86b..7cd095d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -884,6 +884,18 @@ definitions: user_id: type: integer type: object + dto.NutrientRawMaterialDTO: + properties: + id: + description: 原料ID + type: integer + name: + description: 原料名称 + type: string + value: + description: 该原料中此营养素的含量 + type: number + type: object dto.NutrientResponse: properties: created_at: @@ -894,6 +906,11 @@ definitions: type: integer name: type: string + raw_materials: + description: 包含此营养的原料列表 + items: + $ref: '#/definitions/dto.NutrientRawMaterialDTO' + type: array updated_at: type: string type: object @@ -2500,6 +2517,7 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: + - 7 - -1 - 0 - 1 @@ -2510,10 +2528,10 @@ definitions: - -1 - 5 - 6 - - 7 format: int32 type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2524,7 +2542,6 @@ definitions: - _minLevel - _maxLevel - InvalidLevel - - _numLevels info: contact: email: divano@example.com @@ -4373,6 +4390,7 @@ paths: name: end_time type: string - enum: + - 7 - -1 - 0 - 1 @@ -4383,12 +4401,12 @@ paths: - -1 - 5 - 6 - - 7 format: int32 in: query name: level type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4399,7 +4417,6 @@ paths: - _minLevel - _maxLevel - InvalidLevel - - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index 43eda01..e7aabab 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -9,12 +9,24 @@ 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, - CreatedAt: nutrient.CreatedAt, - UpdatedAt: nutrient.UpdatedAt, - Name: nutrient.Name, - Description: nutrient.Description, + ID: nutrient.ID, + CreatedAt: nutrient.CreatedAt, + UpdatedAt: nutrient.UpdatedAt, + Name: nutrient.Name, + Description: nutrient.Description, + RawMaterials: rawMaterials, } } diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 00afdd0..41bc4a1 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -20,13 +20,21 @@ type UpdateNutrientRequest struct { 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"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Name string `json:"name"` - Description string `json:"description"` + ID uint32 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Description string `json:"description"` + RawMaterials []NutrientRawMaterialDTO `json:"raw_materials"` // 包含此营养的原料列表 } // ListNutrientRequest 定义了获取营养种类列表的请求参数 -- 2.49.1 From 7829ac99319182abdcd9a9cc409f8de528b2a5aa Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 21 Nov 2025 17:23:57 +0800 Subject: [PATCH 18/59] =?UTF-8?q?=E6=8B=93=E5=B1=95=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 32 ++++++++++++------ docs/swagger.json | 32 ++++++++++++------ docs/swagger.yaml | 20 +++++++---- internal/app/dto/feed_dto.go | 18 +++++----- .../app/service/feed_management_service.go | 14 ++++++-- internal/domain/recipe/recipe_service.go | 12 +++---- .../infra/repository/nutrient_repository.go | 30 +++++++++++++++-- .../repository/raw_material_repository.go | 33 ++++++++++++++++--- 8 files changed, 142 insertions(+), 49 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 23b6121..32dfb60 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1663,7 +1663,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "按名称模糊查询", + "description": "按营养名称模糊查询", "name": "name", "in": "query" }, @@ -1684,6 +1684,12 @@ const docTemplate = `{ "description": "每页数量", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "按原料名称模糊查询", + "name": "raw_material_name", + "in": "query" } ], "responses": { @@ -2665,10 +2671,16 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "按名称模糊查询", + "description": "按原料名称模糊查询", "name": "name", "in": "query" }, + { + "type": "string", + "description": "按营养名称模糊查询", + "name": "nutrient_name", + "in": "query" + }, { "type": "string", "description": "排序字段,例如 \"id DESC\"", @@ -3074,7 +3086,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -3084,12 +3095,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3099,7 +3110,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -9646,7 +9658,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -9656,10 +9667,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9669,7 +9680,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index cb6b09e..bf084fd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1655,7 +1655,7 @@ "parameters": [ { "type": "string", - "description": "按名称模糊查询", + "description": "按营养名称模糊查询", "name": "name", "in": "query" }, @@ -1676,6 +1676,12 @@ "description": "每页数量", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "按原料名称模糊查询", + "name": "raw_material_name", + "in": "query" } ], "responses": { @@ -2657,10 +2663,16 @@ "parameters": [ { "type": "string", - "description": "按名称模糊查询", + "description": "按原料名称模糊查询", "name": "name", "in": "query" }, + { + "type": "string", + "description": "按营养名称模糊查询", + "name": "nutrient_name", + "in": "query" + }, { "type": "string", "description": "排序字段,例如 \"id DESC\"", @@ -3066,7 +3078,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -3076,12 +3087,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3091,7 +3102,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -9638,7 +9650,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -9648,10 +9659,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9661,7 +9672,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7cd095d..e5e8ed7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2517,7 +2517,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2528,10 +2527,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2542,6 +2541,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -3564,7 +3564,7 @@ paths: get: description: 获取所有营养种类的列表,支持分页和过滤。 parameters: - - description: 按名称模糊查询 + - description: 按营养名称模糊查询 in: query name: name type: string @@ -3580,6 +3580,10 @@ paths: in: query name: page_size type: integer + - description: 按原料名称模糊查询 + in: query + name: raw_material_name + type: string produces: - application/json responses: @@ -4152,10 +4156,14 @@ paths: get: description: 获取所有原料的列表,支持分页和过滤。 parameters: - - description: 按名称模糊查询 + - description: 按原料名称模糊查询 in: query name: name type: string + - description: 按营养名称模糊查询 + in: query + name: nutrient_name + type: string - description: 排序字段,例如 "id DESC" in: query name: order_by @@ -4390,7 +4398,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -4401,12 +4408,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4417,6 +4424,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 41bc4a1..db30316 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -39,10 +39,11 @@ type NutrientResponse struct { // ListNutrientRequest 定义了获取营养种类列表的请求参数 type ListNutrientRequest 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" + 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 是获取营养种类列表的响应结构 @@ -89,10 +90,11 @@ type RawMaterialResponse struct { // ListRawMaterialRequest 定义了获取原料列表的请求参数 type ListRawMaterialRequest 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" + 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" } // ListRawMaterialResponse 是获取原料列表的响应结构 diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go index 2c7fabc..f00dc46 100644 --- a/internal/app/service/feed_management_service.go +++ b/internal/app/service/feed_management_service.go @@ -143,7 +143,12 @@ func (s *feedManagementServiceImpl) GetNutrient(ctx context.Context, id uint32) func (s *feedManagementServiceImpl) ListNutrients(ctx context.Context, req *dto.ListNutrientRequest) (*dto.ListNutrientResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListNutrients") - nutrients, total, err := s.recipeSvc.ListNutrients(serviceCtx, req.Page, req.PageSize) + 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) } @@ -218,7 +223,12 @@ func (s *feedManagementServiceImpl) GetRawMaterial(ctx context.Context, id uint3 func (s *feedManagementServiceImpl) ListRawMaterials(ctx context.Context, req *dto.ListRawMaterialRequest) (*dto.ListRawMaterialResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "ListRawMaterials") - rawMaterials, total, err := s.recipeSvc.ListRawMaterials(serviceCtx, req.Page, req.PageSize) + opts := repository.RawMaterialListOptions{ + Name: req.Name, + NutrientName: req.NutrientName, + OrderBy: req.OrderBy, + } + rawMaterials, total, err := s.recipeSvc.ListRawMaterials(serviceCtx, opts, req.Page, req.PageSize) if err != nil { return nil, fmt.Errorf("获取原料列表失败: %w", err) } diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index c7d6483..ff0b66f 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -32,14 +32,14 @@ type Service interface { 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, page, pageSize int) ([]models.Nutrient, int64, error) + ListNutrients(ctx context.Context, opts repository.NutrientListOptions, page, pageSize int) ([]models.Nutrient, int64, error) // 原料相关接口 CreateRawMaterial(ctx context.Context, name, description string) (*models.RawMaterial, error) UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*models.RawMaterial, error) DeleteRawMaterial(ctx context.Context, id uint32) error GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error) - ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) + ListRawMaterials(ctx context.Context, opts repository.RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) // 猪品种相关接口 CreatePigBreed(ctx context.Context, breed *models.PigBreed) error @@ -175,10 +175,10 @@ func (s *recipeServiceImpl) GetNutrient(ctx context.Context, id uint32) (*models } // ListNutrients 实现了列出营养种类的逻辑 -func (s *recipeServiceImpl) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { +func (s *recipeServiceImpl) 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, page, pageSize) + nutrients, total, err := s.nutrientRepo.ListNutrients(serviceCtx, opts, page, pageSize) if err != nil { return nil, 0, fmt.Errorf("获取营养种类列表失败: %w", err) } @@ -279,10 +279,10 @@ func (s *recipeServiceImpl) GetRawMaterial(ctx context.Context, id uint32) (*mod } // ListRawMaterials 实现了列出原料的逻辑 -func (s *recipeServiceImpl) ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) { +func (s *recipeServiceImpl) 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, page, pageSize) + rawMaterials, total, err := s.rawMaterialRepo.ListRawMaterials(serviceCtx, opts, page, pageSize) if err != nil { return nil, 0, fmt.Errorf("获取原料列表失败: %w", err) } diff --git a/internal/infra/repository/nutrient_repository.go b/internal/infra/repository/nutrient_repository.go index f2e89c3..57f6eac 100644 --- a/internal/infra/repository/nutrient_repository.go +++ b/internal/infra/repository/nutrient_repository.go @@ -11,12 +11,19 @@ import ( "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, page, pageSize int) ([]models.Nutrient, int64, 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 } @@ -61,19 +68,36 @@ func (r *gormNutrientRepository) GetNutrientByName(ctx context.Context, name str } // ListNutrients 列出所有营养种类(分页),并预加载关联的原料信息 -func (r *gormNutrientRepository) ListNutrients(ctx context.Context, page, pageSize int) ([]models.Nutrient, int64, error) { +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 diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 3f40c43..efff010 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -11,12 +11,19 @@ import ( "gorm.io/gorm" ) +// RawMaterialListOptions 定义了查询原料列表时的筛选条件 +type RawMaterialListOptions struct { + Name *string + NutrientName *string + OrderBy string +} + // RawMaterialRepository 定义了与原料相关的数据库操作接口 type RawMaterialRepository interface { 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, page, pageSize int) ([]models.RawMaterial, int64, 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 @@ -64,20 +71,38 @@ func (r *gormRawMaterialRepository) GetRawMaterialByName(ctx context.Context, na return &rawMaterial, nil } -// ListRawMaterials 列出所有原料(分页),并预加载关联的营养素信息 -func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, page, pageSize int) ([]models.RawMaterial, int64, error) { +// 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 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 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 -- 2.49.1 From f81635f9972a119ff7131d4d1c80eb85a8b1bcf3 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 21 Nov 2025 18:36:02 +0800 Subject: [PATCH 19/59] =?UTF-8?q?seeder=E6=94=AF=E6=8C=81=E6=8C=89?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/seeder.go | 208 ++++++++++++++---------------- 1 file changed, 98 insertions(+), 110 deletions(-) diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index 186c03f..4da7561 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -122,8 +122,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { // seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 func seedNutrients(tx *gorm.DB, jsonData []byte) error { // 1. 严格校验JSON文件,检查内部重复键 - parsedData, err := validateAndParseNutrientJSON(jsonData) - if err != nil { + if err := validateAndParseNutrientJSON(jsonData); err != nil { return fmt.Errorf("JSON源文件校验失败: %w", err) } @@ -144,28 +143,36 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { } // 3. 将通过校验的、干净的数据写入数据库 - for rawMaterialName, nutrients := range parsedData { + dataNode := gjson.GetBytes(jsonData, "data") + var err error // 用于捕获 ForEach 内部的错误 + dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool { + rawMaterialName := rawMaterialKey.String() var rawMaterial models.RawMaterial // 将 Description 放入 Create 对象中 - err := tx.Where(models.RawMaterial{Name: rawMaterialName}). + err = tx.Where(models.RawMaterial{Name: rawMaterialName}). FirstOrCreate(&rawMaterial, models.RawMaterial{ Name: rawMaterialName, Description: rawMaterialDescriptions[rawMaterialName], }).Error if err != nil { - return fmt.Errorf("预设原料 '%s' 失败: %w", rawMaterialName, err) + // 返回 false 停止 ForEach 遍历 + return false } - for nutrientName, value := range nutrients { + 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}). + err = tx.Where(models.Nutrient{Name: nutrientName}). FirstOrCreate(&nutrient, models.Nutrient{ Name: nutrientName, Description: nutrientDescriptions[nutrientName], }).Error if err != nil { - return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) + // 返回 false 停止 ForEach 遍历 + return false } linkData := models.RawMaterialNutrient{ @@ -173,23 +180,27 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { NutrientID: nutrient.ID, } // 使用 FirstOrCreate 确保关联的唯一性 - if err := tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ + err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ RawMaterialID: linkData.RawMaterialID, NutrientID: linkData.NutrientID, Value: value, - }).Error; err != nil { - return fmt.Errorf("为原料 '%s' 和营养素 '%s' 创建关联失败: %w", rawMaterialName, nutrientName, err) + }).Error + if err != nil { + // 返回 false 停止 ForEach 遍历 + return false } - } - } - return nil + return true + }) + return err == nil // 如果内部遍历有错误,则停止外部遍历 + }) + + return err // 返回捕获到的错误 } // seedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { // 1. 严格校验JSON文件,检查内部重复键 - parsedData, err := validateAndParsePigNutrientRequirementJSON(jsonData) - if err != nil { + if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil { return fmt.Errorf("JSON源文件校验失败: %w", err) } @@ -244,11 +255,14 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { } // 3. 将通过校验的、干净的数据写入数据库 - for breedName, ageStagesData := range parsedData { + dataNode := gjson.GetBytes(jsonData, "data") + var err error // 用于捕获 ForEach 内部的错误 + dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool { + breedName := breedKey.String() var pigBreed models.PigBreed // 查找或创建 PigBreed pbDesc := pigBreedDescriptions[breedName] - err := tx.Where(models.PigBreed{Name: breedName}). + err = tx.Where(models.PigBreed{Name: breedName}). FirstOrCreate(&pigBreed, models.PigBreed{ Name: breedName, Description: pbDesc.Description, @@ -258,20 +272,22 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { BreedDisadvantages: pbDesc.BreedDisadvantages, }).Error if err != nil { - return fmt.Errorf("预设猪品种 '%s' 失败: %w", breedName, err) + return false } - for ageStageName, nutrientsData := range ageStagesData { + 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}). + err = tx.Where(models.PigAgeStage{Name: ageStageName}). FirstOrCreate(&pigAgeStage, models.PigAgeStage{ Name: ageStageName, Description: pasDesc.Description, }).Error + if err != nil { - return fmt.Errorf("预设猪年龄阶段 '%s' 失败: %w", ageStageName, err) + return false } var pigType models.PigType @@ -290,243 +306,215 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { MaxWeight: ptDesc.MaxWeight, }).Error if err != nil { - return fmt.Errorf("预设猪类型 '%s' - '%s' 失败: %w", breedName, ageStageName, err) + return false } - for nutrientName, requirement := range nutrientsData { + 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}). + err = tx.Where(models.Nutrient{Name: nutrientName}). FirstOrCreate(&nutrient, models.Nutrient{ Name: nutrientName, // Description 字段在 nutrient seeder 中处理,这里不设置 }).Error if err != nil { - return fmt.Errorf("预设营养素 '%s' 失败: %w", nutrientName, err) + return false } linkData := models.PigNutrientRequirement{ PigTypeID: pigType.ID, NutrientID: nutrient.ID, - MinRequirement: requirement.MinRequirement, - MaxRequirement: requirement.MaxRequirement, + MinRequirement: minReq, + MaxRequirement: maxReq, } // 使用 FirstOrCreate 确保关联的唯一性 - if err := tx.Where(models.PigNutrientRequirement{ + err = tx.Where(models.PigNutrientRequirement{ PigTypeID: pigType.ID, NutrientID: nutrient.ID, - }).FirstOrCreate(&linkData, linkData).Error; err != nil { - return fmt.Errorf("为猪类型 '%s' - '%s' 和营养素 '%s' 创建营养需求失败: %w", breedName, ageStageName, nutrientName, err) + }).FirstOrCreate(&linkData, linkData).Error + if err != nil { + return false } - } - } - } - return nil + return true + }) + return err == nil // 如果内部遍历有错误,则停止外部遍历 + }) + return err == nil // 如果内部遍历有错误,则停止外部遍历 + }) + return err // 返回捕获到的错误 } -// validateAndParsePigNutrientRequirementJSON 严格校验并解析猪营养需求JSON文件 -func validateAndParsePigNutrientRequirementJSON(jsonData []byte) (map[string]map[string]map[string]struct { - MinRequirement float32 - MaxRequirement float32 -}, error) { +// validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件 +func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error { dataNode := gjson.GetBytes(jsonData, "data") if !dataNode.Exists() { - return nil, errors.New("JSON文件中缺少 'data' 字段") + return errors.New("JSON文件中缺少 'data' 字段") } if !dataNode.IsObject() { - return nil, errors.New("'data' 字段必须是一个JSON对象") + 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 nil, fmt.Errorf("'data' 字段解析起始符失败: %v", err) + return fmt.Errorf("'data' 字段解析起始符失败: %v", err) } - result := make(map[string]map[string]map[string]struct { - MinRequirement float32 - MaxRequirement float32 - }) seenBreeds := make(map[string]bool) for decoder.More() { // 解析 PigBreed 名称 t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("解析猪品种名称失败: %w", err) + return fmt.Errorf("解析猪品种名称失败: %w", err) } breedName := t.(string) if seenBreeds[breedName] { - return nil, fmt.Errorf("猪品种名称 '%s' 重复", breedName) + return fmt.Errorf("猪品种名称 '%s' 重复", breedName) } seenBreeds[breedName] = true // 解析该品种的年龄阶段对象 if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return nil, fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName) + return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName) } - ageStages := make(map[string]map[string]struct { - MinRequirement float32 - MaxRequirement float32 - }) seenAgeStages := make(map[string]bool) for decoder.More() { // 解析 PigAgeStage 名称 t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err) + return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err) } ageStageName := t.(string) if seenAgeStages[ageStageName] { - return nil, fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName) + return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName) } seenAgeStages[ageStageName] = true // 解析该年龄阶段的营养成分对象 if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return nil, fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName) + return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName) } - nutrients := make(map[string]struct { - MinRequirement float32 - MaxRequirement float32 - }) seenNutrients := make(map[string]bool) for decoder.More() { // 解析 Nutrient 名称 t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err) + return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err) } nutrientName := t.(string) if seenNutrients[nutrientName] { - return nil, fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, 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 nil, fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName) + return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName) } - var req struct { - MinRequirement float32 - MaxRequirement float32 - } for decoder.More() { t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err) + return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err) } - key := t.(string) + // key := t.(string) // 校验时不需要使用 key 的值 t, err = decoder.Token() if err != nil { - return nil, fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err) + return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err) } - if value, ok := t.(json.Number); ok { - f64, _ := value.Float64() - if key == "min_requirement" { - req.MinRequirement = float32(f64) - } else if key == "max_requirement" { - req.MaxRequirement = float32(f64) - } else { - return nil, fmt.Errorf("营养素 '%s' 中存在未知键 '%s'", nutrientName, key) - } - } else { - return nil, fmt.Errorf("期望营养素 '%s' 的 '%s' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, key, t, t) + 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 nil, fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) + return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) } - nutrients[nutrientName] = req } if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return nil, fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) + return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) } - ageStages[ageStageName] = nutrients } if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return nil, fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) + return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) } - result[breedName] = ageStages } - return result, nil + return nil } -// validateAndParseNutrientJSON 使用 json.Decoder 手动解析,以捕获重复的键。 -func validateAndParseNutrientJSON(jsonData []byte) (map[string]map[string]float32, error) { +// validateAndParseNutrientJSON 严格校验JSON文件 +func validateAndParseNutrientJSON(jsonData []byte) error { dataNode := gjson.GetBytes(jsonData, "data") if !dataNode.Exists() { - return nil, errors.New("JSON文件中缺少 'data' 字段") + return errors.New("JSON文件中缺少 'data' 字段") } if !dataNode.IsObject() { - return nil, errors.New("'data' 字段必须是一个JSON对象") + 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 nil, errors.New("'data' 字段解析起始符失败") + return errors.New("'data' 字段解析起始符失败") } - result := make(map[string]map[string]float32) seenRawMaterials := make(map[string]bool) for decoder.More() { // 1. 解析原料名称 t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("解析原料名称失败: %w", err) + return fmt.Errorf("解析原料名称失败: %w", err) } rawMaterialName := t.(string) if seenRawMaterials[rawMaterialName] { - return nil, fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) + return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) } seenRawMaterials[rawMaterialName] = true // 2. 解析该原料的营养成分对象 if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return nil, fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) + return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) } - nutrients := make(map[string]float32) seenNutrients := make(map[string]bool) for decoder.More() { // 解析营养素名称 t, err := decoder.Token() if err != nil { - return nil, fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) + return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) } nutrientName := t.(string) if seenNutrients[nutrientName] { - return nil, fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) + return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) } seenNutrients[nutrientName] = true // 解析营养素含量 t, err = decoder.Token() if err != nil { - return nil, fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) + return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) } - if value, ok := t.(json.Number); ok { - f64, _ := value.Float64() - nutrients[nutrientName] = float32(f64) - } else { - return nil, fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t) + 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 nil, fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) + return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) } - result[rawMaterialName] = nutrients } - return result, nil + return nil } -- 2.49.1 From 9aea4875371e3ed00a932267403cf3fbd250ad1c Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 16:44:22 +0800 Subject: [PATCH 20/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=8E=9F=E6=96=99=E8=90=A5=E5=85=BB=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- docs/docs.go | 131 ++++++++++++------ docs/swagger.json | 131 ++++++++++++------ docs/swagger.yaml | 83 +++++++---- .../app/controller/feed/feed_controller.go | 42 ++++++ internal/app/dto/feed_converter.go | 14 -- internal/app/dto/feed_dto.go | 67 ++++----- .../app/service/feed_management_service.go | 38 +++++ internal/core/component_initializers.go | 1 + internal/domain/recipe/recipe_service.go | 40 +++++- .../repository/raw_material_repository.go | 26 ++++ 11 files changed, 411 insertions(+), 165 deletions(-) diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 379a678..cc35118 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -55,4 +55,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 5. 定义猪的模型和营养需求模型 6. 实现从json读取猪营养需求并写入数据库 7. 实现配方领域关于猪模型和营养需求的增删改查 -8. 实现配方领域的web接口 \ No newline at end of file +8. 实现配方领域的web接口 +9. 实现修改原料营养信息 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 32dfb60..75329e9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2905,6 +2905,64 @@ const docTemplate = `{ } } }, + "/api/v1/feed/raw-materials/{id}/nutrients": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据原料ID,替换其所有的营养成分信息。这是一个覆盖操作。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "全量更新原料的营养成分", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的营养成分列表", + "name": "nutrients", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRawMaterialNutrientsRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -7368,9 +7426,6 @@ const docTemplate = `{ "dto.NutrientResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7386,9 +7441,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.NutrientRawMaterialDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -7461,9 +7513,6 @@ const docTemplate = `{ "dto.PigAgeStageResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7472,9 +7521,6 @@ const docTemplate = `{ }, "name": { "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7667,9 +7713,6 @@ const docTemplate = `{ "breed_disadvantages": { "type": "string" }, - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7681,9 +7724,6 @@ const docTemplate = `{ }, "parent_info": { "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7704,9 +7744,6 @@ const docTemplate = `{ "dto.PigNutrientRequirementDTO": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "id": { "type": "integer" }, @@ -7724,9 +7761,6 @@ const docTemplate = `{ "nutrient_name": { "description": "营养素名称", "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7905,9 +7939,6 @@ const docTemplate = `{ "description": "猪品种名称", "type": "string" }, - "created_at": { - "type": "string" - }, "daily_feed_intake": { "type": "number" }, @@ -7938,9 +7969,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.PigNutrientRequirementDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -8052,9 +8080,6 @@ const docTemplate = `{ "dto.RawMaterialNutrientDTO": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "id": { "type": "integer" }, @@ -8065,21 +8090,32 @@ const docTemplate = `{ "description": "营养素名称", "type": "string" }, - "updated_at": { - "type": "string" - }, "value": { "description": "营养价值含量", "type": "number" } } }, + "dto.RawMaterialNutrientItem": { + "type": "object", + "required": [ + "nutrient_id" + ], + "properties": { + "nutrient_id": { + "description": "营养素ID", + "type": "integer" + }, + "value": { + "description": "含量值,必须大于等于0", + "type": "number", + "minimum": 0 + } + } + }, "dto.RawMaterialResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -8095,9 +8131,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.RawMaterialNutrientDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -8991,6 +9024,20 @@ const docTemplate = `{ } } }, + "dto.UpdateRawMaterialNutrientsRequest": { + "type": "object", + "required": [ + "nutrients" + ], + "properties": { + "nutrients": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialNutrientItem" + } + } + } + }, "dto.UpdateRawMaterialRequest": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index bf084fd..bdf18ce 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2897,6 +2897,64 @@ } } }, + "/api/v1/feed/raw-materials/{id}/nutrients": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据原料ID,替换其所有的营养成分信息。这是一个覆盖操作。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "全量更新原料的营养成分", + "parameters": [ + { + "type": "integer", + "description": "原料ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的营养成分列表", + "name": "nutrients", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRawMaterialNutrientsRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RawMaterialResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -7360,9 +7418,6 @@ "dto.NutrientResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7378,9 +7433,6 @@ "items": { "$ref": "#/definitions/dto.NutrientRawMaterialDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -7453,9 +7505,6 @@ "dto.PigAgeStageResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7464,9 +7513,6 @@ }, "name": { "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7659,9 +7705,6 @@ "breed_disadvantages": { "type": "string" }, - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -7673,9 +7716,6 @@ }, "parent_info": { "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7696,9 +7736,6 @@ "dto.PigNutrientRequirementDTO": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "id": { "type": "integer" }, @@ -7716,9 +7753,6 @@ "nutrient_name": { "description": "营养素名称", "type": "string" - }, - "updated_at": { - "type": "string" } } }, @@ -7897,9 +7931,6 @@ "description": "猪品种名称", "type": "string" }, - "created_at": { - "type": "string" - }, "daily_feed_intake": { "type": "number" }, @@ -7930,9 +7961,6 @@ "items": { "$ref": "#/definitions/dto.PigNutrientRequirementDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -8044,9 +8072,6 @@ "dto.RawMaterialNutrientDTO": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "id": { "type": "integer" }, @@ -8057,21 +8082,32 @@ "description": "营养素名称", "type": "string" }, - "updated_at": { - "type": "string" - }, "value": { "description": "营养价值含量", "type": "number" } } }, + "dto.RawMaterialNutrientItem": { + "type": "object", + "required": [ + "nutrient_id" + ], + "properties": { + "nutrient_id": { + "description": "营养素ID", + "type": "integer" + }, + "value": { + "description": "含量值,必须大于等于0", + "type": "number", + "minimum": 0 + } + } + }, "dto.RawMaterialResponse": { "type": "object", "properties": { - "created_at": { - "type": "string" - }, "description": { "type": "string" }, @@ -8087,9 +8123,6 @@ "items": { "$ref": "#/definitions/dto.RawMaterialNutrientDTO" } - }, - "updated_at": { - "type": "string" } } }, @@ -8983,6 +9016,20 @@ } } }, + "dto.UpdateRawMaterialNutrientsRequest": { + "type": "object", + "required": [ + "nutrients" + ], + "properties": { + "nutrients": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RawMaterialNutrientItem" + } + } + } + }, "dto.UpdateRawMaterialRequest": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e5e8ed7..0bf0acb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -898,8 +898,6 @@ definitions: type: object dto.NutrientResponse: properties: - created_at: - type: string description: type: string id: @@ -911,8 +909,6 @@ definitions: items: $ref: '#/definitions/dto.NutrientRawMaterialDTO' type: array - updated_at: - type: string type: object dto.PaginationDTO: properties: @@ -959,16 +955,12 @@ definitions: type: object dto.PigAgeStageResponse: properties: - created_at: - type: string description: type: string id: type: integer name: type: string - updated_at: - type: string type: object dto.PigBatchCreateDTO: properties: @@ -1094,8 +1086,6 @@ definitions: type: string breed_disadvantages: type: string - created_at: - type: string description: type: string id: @@ -1104,8 +1094,6 @@ definitions: type: string parent_info: type: string - updated_at: - type: string type: object dto.PigHouseResponse: properties: @@ -1118,8 +1106,6 @@ definitions: type: object dto.PigNutrientRequirementDTO: properties: - created_at: - type: string id: type: integer max_requirement: @@ -1133,8 +1119,6 @@ definitions: nutrient_name: description: 营养素名称 type: string - updated_at: - type: string type: object dto.PigPurchaseDTO: properties: @@ -1252,8 +1236,6 @@ definitions: breed_name: description: 猪品种名称 type: string - created_at: - type: string daily_feed_intake: type: number daily_gain_weight: @@ -1275,8 +1257,6 @@ definitions: items: $ref: '#/definitions/dto.PigNutrientRequirementDTO' type: array - updated_at: - type: string type: object dto.PlanExecutionLogDTO: properties: @@ -1346,8 +1326,6 @@ definitions: type: object dto.RawMaterialNutrientDTO: properties: - created_at: - type: string id: type: integer nutrient_id: @@ -1355,16 +1333,24 @@ definitions: nutrient_name: description: 营养素名称 type: string - updated_at: - type: string value: description: 营养价值含量 type: number type: object + dto.RawMaterialNutrientItem: + properties: + nutrient_id: + description: 营养素ID + type: integer + value: + description: 含量值,必须大于等于0 + minimum: 0 + type: number + required: + - nutrient_id + type: object dto.RawMaterialResponse: properties: - created_at: - type: string description: type: string id: @@ -1376,8 +1362,6 @@ definitions: items: $ref: '#/definitions/dto.RawMaterialNutrientDTO' type: array - updated_at: - type: string type: object dto.ReclassifyPenToNewBatchRequest: properties: @@ -1993,6 +1977,15 @@ definitions: required: - execution_type type: object + dto.UpdateRawMaterialNutrientsRequest: + properties: + nutrients: + items: + $ref: '#/definitions/dto.RawMaterialNutrientItem' + type: array + required: + - nutrients + type: object dto.UpdateRawMaterialRequest: properties: description: @@ -4300,6 +4293,40 @@ paths: summary: 更新原料 tags: - 饲料管理 + /api/v1/feed/raw-materials/{id}/nutrients: + put: + consumes: + - application/json + description: 根据原料ID,替换其所有的营养成分信息。这是一个覆盖操作。 + parameters: + - description: 原料ID + in: path + name: id + required: true + type: integer + - description: 新的营养成分列表 + in: body + name: nutrients + required: true + schema: + $ref: '#/definitions/dto.UpdateRawMaterialNutrientsRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RawMaterialResponse' + type: object + security: + - BearerAuth: [] + summary: 全量更新原料的营养成分 + tags: + - 饲料管理 /api/v1/monitor/device-command-logs: get: description: 根据提供的过滤条件,分页获取设备命令日志 diff --git a/internal/app/controller/feed/feed_controller.go b/internal/app/controller/feed/feed_controller.go index 6a5e4f1..260d63f 100644 --- a/internal/app/controller/feed/feed_controller.go +++ b/internal/app/controller/feed/feed_controller.go @@ -365,6 +365,48 @@ func (c *Controller) ListRawMaterials(ctx echo.Context) error { 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 *Controller) 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.feedManagementService.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) +} + // --- 猪品种 (PigBreed) 接口方法实现 --- // CreatePigBreed godoc diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index e7aabab..bca9492 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -22,8 +22,6 @@ func ConvertNutrientToDTO(nutrient *models.Nutrient) *NutrientResponse { return &NutrientResponse{ ID: nutrient.ID, - CreatedAt: nutrient.CreatedAt, - UpdatedAt: nutrient.UpdatedAt, Name: nutrient.Name, Description: nutrient.Description, RawMaterials: rawMaterials, @@ -57,8 +55,6 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse { for i, rmn := range rm.RawMaterialNutrients { rawMaterialNutrientDTOs[i] = RawMaterialNutrientDTO{ ID: rmn.ID, - CreatedAt: rmn.CreatedAt, - UpdatedAt: rmn.UpdatedAt, NutrientID: rmn.NutrientID, Nutrient: rmn.Nutrient.Name, // 假设 Nutrient 已经被预加载 Value: rmn.Value, @@ -67,8 +63,6 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse { return &RawMaterialResponse{ ID: rm.ID, - CreatedAt: rm.CreatedAt, - UpdatedAt: rm.UpdatedAt, Name: rm.Name, Description: rm.Description, RawMaterialNutrients: rawMaterialNutrientDTOs, @@ -99,8 +93,6 @@ func ConvertPigBreedToDTO(breed *models.PigBreed) *PigBreedResponse { } return &PigBreedResponse{ ID: breed.ID, - CreatedAt: breed.CreatedAt, - UpdatedAt: breed.UpdatedAt, Name: breed.Name, Description: breed.Description, ParentInfo: breed.ParentInfo, @@ -134,8 +126,6 @@ func ConvertPigAgeStageToDTO(ageStage *models.PigAgeStage) *PigAgeStageResponse } return &PigAgeStageResponse{ ID: ageStage.ID, - CreatedAt: ageStage.CreatedAt, - UpdatedAt: ageStage.UpdatedAt, Name: ageStage.Name, Description: ageStage.Description, } @@ -168,8 +158,6 @@ func ConvertPigTypeToDTO(pt *models.PigType) *PigTypeResponse { for i, pnr := range pt.PigNutrientRequirements { pigNutrientRequirementDTOs[i] = PigNutrientRequirementDTO{ ID: pnr.ID, - CreatedAt: pnr.CreatedAt, - UpdatedAt: pnr.UpdatedAt, NutrientID: pnr.NutrientID, NutrientName: pnr.Nutrient.Name, // 假设 Nutrient 已经被预加载 MinRequirement: pnr.MinRequirement, @@ -179,8 +167,6 @@ func ConvertPigTypeToDTO(pt *models.PigType) *PigTypeResponse { return &PigTypeResponse{ ID: pt.ID, - CreatedAt: pt.CreatedAt, - UpdatedAt: pt.UpdatedAt, BreedID: pt.BreedID, BreedName: pt.Breed.Name, // 假设 Breed 已经被预加载 AgeStageID: pt.AgeStageID, diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index db30316..57d6f18 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -1,9 +1,5 @@ package dto -import ( - "time" -) - // ============================================================================================================= // 营养种类 (Nutrient) 相关 DTO // ============================================================================================================= @@ -30,8 +26,6 @@ type NutrientRawMaterialDTO struct { // NutrientResponse 营养种类响应体 type NutrientResponse struct { ID uint32 `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` Name string `json:"name"` Description string `json:"description"` RawMaterials []NutrientRawMaterialDTO `json:"raw_materials"` // 包含此营养的原料列表 @@ -70,19 +64,15 @@ type UpdateRawMaterialRequest struct { // RawMaterialNutrientDTO 原料营养素响应体 type RawMaterialNutrientDTO struct { - ID uint32 `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - NutrientID uint32 `json:"nutrient_id"` - Nutrient string `json:"nutrient_name"` // 营养素名称 - Value float32 `json:"value"` // 营养价值含量 + 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"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` Name string `json:"name"` Description string `json:"description"` RawMaterialNutrients []RawMaterialNutrientDTO `json:"raw_material_nutrients"` // 关联的营养素信息 @@ -103,6 +93,17 @@ type ListRawMaterialResponse struct { 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 // ============================================================================================================= @@ -129,15 +130,13 @@ type UpdatePigBreedRequest struct { // PigBreedResponse 猪品种响应体 type PigBreedResponse struct { - ID uint32 `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - 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"` + 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 定义了获取猪品种列表的请求参数 @@ -172,11 +171,9 @@ type UpdatePigAgeStageRequest struct { // PigAgeStageResponse 猪年龄阶段响应体 type PigAgeStageResponse struct { - ID uint32 `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Name string `json:"name"` - Description string `json:"description"` + ID uint32 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` } // ListPigAgeStageRequest 定义了获取猪年龄阶段列表的请求参数 @@ -225,20 +222,16 @@ type UpdatePigTypeRequest struct { // PigNutrientRequirementDTO 猪营养需求响应体 type PigNutrientRequirementDTO struct { - ID uint32 `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - NutrientID uint32 `json:"nutrient_id"` - NutrientName string `json:"nutrient_name"` // 营养素名称 - MinRequirement float32 `json:"min_requirement"` // 最低营养需求量 - MaxRequirement float32 `json:"max_requirement"` // 最高营养需求量 + 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"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` BreedID uint32 `json:"breed_id"` BreedName string `json:"breed_name"` // 猪品种名称 AgeStageID uint32 `json:"age_stage_id"` diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go index f00dc46..ba2eb62 100644 --- a/internal/app/service/feed_management_service.go +++ b/internal/app/service/feed_management_service.go @@ -40,6 +40,7 @@ type FeedManagementService interface { 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) // 新增 // 猪品种相关 CreatePigBreed(ctx context.Context, req *dto.CreatePigBreedRequest) (*dto.PigBreedResponse, error) @@ -236,6 +237,43 @@ func (s *feedManagementServiceImpl) ListRawMaterials(ctx context.Context, req *d return dto.ConvertRawMaterialListToDTO(rawMaterials, total, req.Page, req.PageSize), nil } +// UpdateRawMaterialNutrients 全量更新原料的营养成分 +func (s *feedManagementServiceImpl) 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{ + 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 +} + // ===================================================================================================================== // 猪品种 (PigBreed) 实现 // ===================================================================================================================== diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 10b213d..b354386 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -217,6 +217,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr // 配方管理服务 recipeService := recipe.NewRecipeService( logs.AddCompName(baseCtx, "RecipeService"), + infra.repos.unitOfWork, infra.repos.nutrientRepo, infra.repos.rawMaterialRepo, infra.repos.pigTypeRepo, diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index ff0b66f..3a210df 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -40,6 +40,7 @@ type Service interface { 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 // 猪品种相关接口 CreatePigBreed(ctx context.Context, breed *models.PigBreed) error @@ -66,15 +67,17 @@ type Service interface { // recipeServiceImpl 是 RecipeService 的实现 type recipeServiceImpl struct { ctx context.Context + uow repository.UnitOfWork nutrientRepo repository.NutrientRepository rawMaterialRepo repository.RawMaterialRepository pigTypeRepo repository.PigTypeRepository } // NewRecipeService 创建一个新的 RecipeService 实例 -func NewRecipeService(ctx context.Context, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository, pigTypeRepo repository.PigTypeRepository) Service { +func NewRecipeService(ctx context.Context, uow repository.UnitOfWork, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository, pigTypeRepo repository.PigTypeRepository) Service { return &recipeServiceImpl{ ctx: ctx, + uow: uow, nutrientRepo: nutrientRepo, rawMaterialRepo: rawMaterialRepo, pigTypeRepo: pigTypeRepo, @@ -289,6 +292,41 @@ func (s *recipeServiceImpl) ListRawMaterials(ctx context.Context, opts repositor return rawMaterials, total, nil } +// UpdateRawMaterialNutrients 实现了全量更新原料营养成分的业务逻辑 +func (s *recipeServiceImpl) 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 +} + // CreatePigBreed 实现了创建猪品种的核心业务逻辑 func (s *recipeServiceImpl) CreatePigBreed(ctx context.Context, breed *models.PigBreed) error { serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreatePigBreed") diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index efff010..3df2102 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -26,6 +26,8 @@ type RawMaterialRepository interface { 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 // 库存日志相关方法 CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error @@ -162,6 +164,30 @@ func (r *gormRawMaterialRepository) DeleteRawMaterial(ctx context.Context, id ui }) } +// 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 { + // 如果没有要创建的记录,直接返回成功,避免执行空的Create语句 + if len(nutrients) == 0 { + return nil + } + repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchRawMaterialNutrientsTx") + tx := db.WithContext(repoCtx) + if err := tx.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") -- 2.49.1 From 4405d1f3f1e16f0d42a9779987a488405ca31a0a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 17:29:25 +0800 Subject: [PATCH 21/59] =?UTF-8?q?=E4=BF=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/router.go | 1 + internal/app/service/feed_management_service.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 800aeaa..98f03b5 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -226,6 +226,7 @@ func (a *API) setupRoutes() { // 原料 (RawMaterial) 路由 feedGroup.POST("/raw-materials", a.feedController.CreateRawMaterial) feedGroup.PUT("/raw-materials/:id", a.feedController.UpdateRawMaterial) + feedGroup.PUT("/raw-materials/:id/nutrients", a.feedController.UpdateRawMaterialNutrients) feedGroup.DELETE("/raw-materials/:id", a.feedController.DeleteRawMaterial) feedGroup.GET("/raw-materials/:id", a.feedController.GetRawMaterial) feedGroup.GET("/raw-materials", a.feedController.ListRawMaterials) diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go index ba2eb62..3fdb4b2 100644 --- a/internal/app/service/feed_management_service.go +++ b/internal/app/service/feed_management_service.go @@ -245,8 +245,9 @@ func (s *feedManagementServiceImpl) UpdateRawMaterialNutrients(ctx context.Conte nutrients := make([]models.RawMaterialNutrient, len(req.Nutrients)) for i, item := range req.Nutrients { nutrients[i] = models.RawMaterialNutrient{ - NutrientID: item.NutrientID, - Value: item.Value, + RawMaterialID: id, + NutrientID: item.NutrientID, + Value: item.Value, } } -- 2.49.1 From 0637f5fb6cb50d9fcbbbcd8961f616df660cf867 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 17:55:52 +0800 Subject: [PATCH 22/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=8C=AA=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 110 ++++++++++++++++-- docs/swagger.json | 110 ++++++++++++++++-- docs/swagger.yaml | 67 ++++++++++- internal/app/api/router.go | 1 + .../app/controller/feed/feed_controller.go | 42 +++++++ internal/app/dto/feed_dto.go | 12 ++ .../app/service/feed_management_service.go | 40 +++++++ internal/domain/recipe/recipe_service.go | 36 ++++++ .../infra/repository/pig_type_repository.go | 29 +++++ 9 files changed, 427 insertions(+), 20 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 75329e9..1698c3e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2653,6 +2653,64 @@ const docTemplate = `{ } } }, + "/api/v1/feed/pig-types/{id}/nutrient-requirements": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据猪类型ID,替换其所有的营养需求信息。这是一个覆盖操作。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "全量更新猪类型的营养需求", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的营养需求列表", + "name": "nutrientRequirements", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigTypeNutrientRequirementsRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/raw-materials": { "get": { "security": [ @@ -3144,6 +3202,7 @@ const docTemplate = `{ }, { "enum": [ + 7, -1, 0, 1, @@ -3153,12 +3212,12 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3168,8 +3227,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7764,6 +7822,28 @@ const docTemplate = `{ } } }, + "dto.PigNutrientRequirementItem": { + "type": "object", + "required": [ + "nutrient_id" + ], + "properties": { + "max_requirement": { + "description": "最高营养需求量", + "type": "number", + "minimum": 0 + }, + "min_requirement": { + "description": "最低营养需求量", + "type": "number", + "minimum": 0 + }, + "nutrient_id": { + "description": "营养素ID", + "type": "integer" + } + } + }, "dto.PigPurchaseDTO": { "type": "object", "properties": { @@ -8933,6 +9013,20 @@ const docTemplate = `{ } } }, + "dto.UpdatePigTypeNutrientRequirementsRequest": { + "type": "object", + "required": [ + "nutrient_requirements" + ], + "properties": { + "nutrient_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigNutrientRequirementItem" + } + } + } + }, "dto.UpdatePigTypeRequest": { "type": "object", "required": [ @@ -9705,6 +9799,7 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -9714,10 +9809,10 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9727,8 +9822,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index bdf18ce..ceb65c1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2645,6 +2645,64 @@ } } }, + "/api/v1/feed/pig-types/{id}/nutrient-requirements": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据猪类型ID,替换其所有的营养需求信息。这是一个覆盖操作。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理" + ], + "summary": "全量更新猪类型的营养需求", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的营养需求列表", + "name": "nutrientRequirements", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigTypeNutrientRequirementsRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigTypeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/raw-materials": { "get": { "security": [ @@ -3136,6 +3194,7 @@ }, { "enum": [ + 7, -1, 0, 1, @@ -3145,12 +3204,12 @@ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3160,8 +3219,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7756,6 +7814,28 @@ } } }, + "dto.PigNutrientRequirementItem": { + "type": "object", + "required": [ + "nutrient_id" + ], + "properties": { + "max_requirement": { + "description": "最高营养需求量", + "type": "number", + "minimum": 0 + }, + "min_requirement": { + "description": "最低营养需求量", + "type": "number", + "minimum": 0 + }, + "nutrient_id": { + "description": "营养素ID", + "type": "integer" + } + } + }, "dto.PigPurchaseDTO": { "type": "object", "properties": { @@ -8925,6 +9005,20 @@ } } }, + "dto.UpdatePigTypeNutrientRequirementsRequest": { + "type": "object", + "required": [ + "nutrient_requirements" + ], + "properties": { + "nutrient_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigNutrientRequirementItem" + } + } + } + }, "dto.UpdatePigTypeRequest": { "type": "object", "required": [ @@ -9697,6 +9791,7 @@ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -9706,10 +9801,10 @@ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9719,8 +9814,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0bf0acb..929eeea 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1120,6 +1120,22 @@ definitions: description: 营养素名称 type: string type: object + dto.PigNutrientRequirementItem: + properties: + max_requirement: + description: 最高营养需求量 + minimum: 0 + type: number + min_requirement: + description: 最低营养需求量 + minimum: 0 + type: number + nutrient_id: + description: 营养素ID + type: integer + required: + - nutrient_id + type: object dto.PigPurchaseDTO: properties: created_at: @@ -1913,6 +1929,15 @@ definitions: required: - name type: object + dto.UpdatePigTypeNutrientRequirementsRequest: + properties: + nutrient_requirements: + items: + $ref: '#/definitions/dto.PigNutrientRequirementItem' + type: array + required: + - nutrient_requirements + type: object dto.UpdatePigTypeRequest: properties: age_stage_id: @@ -2510,6 +2535,7 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: + - 7 - -1 - 0 - 1 @@ -2520,10 +2546,10 @@ definitions: - -1 - 5 - 6 - - 7 format: int32 type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2534,7 +2560,6 @@ definitions: - _minLevel - _maxLevel - InvalidLevel - - _numLevels info: contact: email: divano@example.com @@ -4145,6 +4170,40 @@ paths: summary: 更新猪类型 tags: - 饲料管理 + /api/v1/feed/pig-types/{id}/nutrient-requirements: + put: + consumes: + - application/json + description: 根据猪类型ID,替换其所有的营养需求信息。这是一个覆盖操作。 + parameters: + - description: 猪类型ID + in: path + name: id + required: true + type: integer + - description: 新的营养需求列表 + in: body + name: nutrientRequirements + required: true + schema: + $ref: '#/definitions/dto.UpdatePigTypeNutrientRequirementsRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigTypeResponse' + type: object + security: + - BearerAuth: [] + summary: 全量更新猪类型的营养需求 + tags: + - 饲料管理 /api/v1/feed/raw-materials: get: description: 获取所有原料的列表,支持分页和过滤。 @@ -4425,6 +4484,7 @@ paths: name: end_time type: string - enum: + - 7 - -1 - 0 - 1 @@ -4435,12 +4495,12 @@ paths: - -1 - 5 - 6 - - 7 format: int32 in: query name: level type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4451,7 +4511,6 @@ paths: - _minLevel - _maxLevel - InvalidLevel - - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 98f03b5..c1835ce 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -251,6 +251,7 @@ func (a *API) setupRoutes() { feedGroup.DELETE("/pig-types/:id", a.feedController.DeletePigType) feedGroup.GET("/pig-types/:id", a.feedController.GetPigType) feedGroup.GET("/pig-types", a.feedController.ListPigTypes) + feedGroup.PUT("/pig-types/:id/nutrient-requirements", a.feedController.UpdatePigTypeNutrientRequirements) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") } diff --git a/internal/app/controller/feed/feed_controller.go b/internal/app/controller/feed/feed_controller.go index 260d63f..185e070 100644 --- a/internal/app/controller/feed/feed_controller.go +++ b/internal/app/controller/feed/feed_controller.go @@ -915,3 +915,45 @@ func (c *Controller) ListPigTypes(ctx echo.Context) error { 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 *Controller) 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.feedManagementService.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) +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 57d6f18..1fcb10c 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -262,3 +262,15 @@ 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"` // 最高营养需求量 +} diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go index 3fdb4b2..ef4d2ae 100644 --- a/internal/app/service/feed_management_service.go +++ b/internal/app/service/feed_management_service.go @@ -62,6 +62,7 @@ type FeedManagementService interface { 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) // 新增 } // feedManagementServiceImpl 是 FeedManagementService 接口的实现 @@ -576,3 +577,42 @@ func (s *feedManagementServiceImpl) ListPigTypes(ctx context.Context, req *dto.L return dto.ConvertPigTypeListToDTO(pigTypes, total, req.Page, req.PageSize), nil } + +// UpdatePigTypeNutrientRequirements 全量更新猪类型的营养需求 +func (s *feedManagementServiceImpl) 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 +} diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 3a210df..3641275 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -62,6 +62,7 @@ type Service interface { 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 } // recipeServiceImpl 是 RecipeService 的实现 @@ -526,3 +527,38 @@ func (s *recipeServiceImpl) ListPigTypes(ctx context.Context, opts repository.Pi } return pigTypes, total, nil } + +// UpdatePigTypeNutrientRequirements 实现了全量更新猪类型营养需求的核心业务逻辑 +func (s *recipeServiceImpl) 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 +} diff --git a/internal/infra/repository/pig_type_repository.go b/internal/infra/repository/pig_type_repository.go index a975b9b..0f006e6 100644 --- a/internal/infra/repository/pig_type_repository.go +++ b/internal/infra/repository/pig_type_repository.go @@ -2,6 +2,7 @@ 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" @@ -71,6 +72,10 @@ type PigTypeRepository interface { 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 实现。 @@ -279,3 +284,27 @@ func (r *gormPigTypeRepository) ListPigTypes(ctx context.Context, opts PigTypeLi 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 +} -- 2.49.1 From 3ef2910058c074ddf9062985328a23e21021a386 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 17:55:56 +0800 Subject: [PATCH 23/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=8C=AA=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index cc35118..1886e75 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -56,4 +56,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 6. 实现从json读取猪营养需求并写入数据库 7. 实现配方领域关于猪模型和营养需求的增删改查 8. 实现配方领域的web接口 -9. 实现修改原料营养信息 \ No newline at end of file +9. 实现修改原料营养信息 +10. 实现修改猪营养需求 \ No newline at end of file -- 2.49.1 From b40eb350163ff9ca8bc1c3a70dd0afc53f24bf48 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 20:52:15 +0800 Subject: [PATCH 24/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=8C=AA=E8=90=A5=E5=85=BB=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- internal/infra/database/postgres.go | 14 ++ internal/infra/models/models.go | 2 + internal/infra/models/recipe.go | 29 +++ .../infra/repository/recipe_repository.go | 182 ++++++++++++++++++ project_structure.txt | 2 + 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 internal/infra/models/recipe.go create mode 100644 internal/infra/repository/recipe_repository.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 1886e75..c394a5b 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -57,4 +57,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 7. 实现配方领域关于猪模型和营养需求的增删改查 8. 实现配方领域的web接口 9. 实现修改原料营养信息 -10. 实现修改猪营养需求 \ No newline at end of file +10. 实现修改猪营养需求 +11. 配方模型定义和仓库层增删改查方法 \ No newline at end of file diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 4d2647f..fa23d75 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -376,6 +376,20 @@ func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { 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 { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 47324ed..3caeb56 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -70,6 +70,8 @@ func GetAllModels() []interface{} { &Nutrient{}, &RawMaterialNutrient{}, &RawMaterialStockLog{}, + &Recipe{}, + &RecipeIngredient{}, // Medication Models &Medication{}, diff --git a/internal/infra/models/recipe.go b/internal/infra/models/recipe.go new file mode 100644 index 0000000..a5278b0 --- /dev/null +++ b/internal/infra/models/recipe.go @@ -0,0 +1,29 @@ +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" +} + +// 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" +} diff --git a/internal/infra/repository/recipe_repository.go b/internal/infra/repository/recipe_repository.go new file mode 100644 index 0000000..4d416a6 --- /dev/null +++ b/internal/infra/repository/recipe_repository.go @@ -0,0 +1,182 @@ +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 + UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error + DeleteRecipe(ctx context.Context, id uint32) 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") + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + if err := tx.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 +} + +// UpdateRecipeIngredients 更新配方关联的原料列表 +func (r *gormRecipeRepository) UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error { + repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRecipeIngredients") + + return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { + // 1. 删除所有旧的关联配方原料 + if err := tx.Where("recipe_id = ?", recipeID).Delete(&models.RecipeIngredient{}).Error; err != nil { + return fmt.Errorf("删除旧的配方原料失败: %w", err) + } + + // 2. 批量创建新的关联配方原料 + if len(ingredients) > 0 { + for i := range ingredients { + ingredients[i].RecipeID = recipeID + } + if err := tx.Create(&ingredients).Error; err != nil { + return fmt.Errorf("创建新的配方原料失败: %w", err) + } + } + 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 + }) +} diff --git a/project_structure.txt b/project_structure.txt index d1c4654..c61b69d 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -144,6 +144,7 @@ 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 @@ -173,6 +174,7 @@ 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 -- 2.49.1 From 851682d579a122e687918dc3fd871f144b296fc2 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 22 Nov 2025 21:29:23 +0800 Subject: [PATCH 25/59] =?UTF-8?q?=E9=85=8D=E6=96=B9=E9=A2=86=E5=9F=9F?= =?UTF-8?q?=E5=B1=82=E6=96=B9=E6=B3=95+=E9=87=8D=E6=9E=84=E9=85=8D?= =?UTF-8?q?=E6=96=B9=E9=A2=86=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 4 +- internal/core/component_initializers.go | 20 +- internal/domain/recipe/nutrient_service.go | 146 +++++ .../domain/recipe/pig_age_stage_service.go | 113 ++++ internal/domain/recipe/pig_breed_service.go | 112 ++++ internal/domain/recipe/pig_type_service.go | 139 +++++ .../domain/recipe/raw_material_service.go | 184 ++++++ internal/domain/recipe/recipe_core_service.go | 189 ++++++ internal/domain/recipe/recipe_service.go | 580 +----------------- .../repository/raw_material_repository.go | 14 +- .../infra/repository/recipe_repository.go | 68 +- 11 files changed, 982 insertions(+), 587 deletions(-) create mode 100644 internal/domain/recipe/nutrient_service.go create mode 100644 internal/domain/recipe/pig_age_stage_service.go create mode 100644 internal/domain/recipe/pig_breed_service.go create mode 100644 internal/domain/recipe/pig_type_service.go create mode 100644 internal/domain/recipe/raw_material_service.go create mode 100644 internal/domain/recipe/recipe_core_service.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index c394a5b..612428d 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -58,4 +58,6 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 8. 实现配方领域的web接口 9. 实现修改原料营养信息 10. 实现修改猪营养需求 -11. 配方模型定义和仓库层增删改查方法 \ No newline at end of file +11. 配方模型定义和仓库层增删改查方法 +12. 配方领域层方法 +13. 重构配方领域 \ No newline at end of file diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index b354386..97ed6f0 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -86,6 +86,7 @@ type Repositories struct { pigTypeRepo repository.PigTypeRepository rawMaterialRepo repository.RawMaterialRepository nutrientRepo repository.NutrientRepository + recipeRepo repository.RecipeRepository unitOfWork repository.UnitOfWork } @@ -117,6 +118,7 @@ func initRepositories(ctx context.Context, db *gorm.DB) *Repositories { 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), } } @@ -214,13 +216,21 @@ 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) + recipeCoreService := recipe.NewRecipeCoreService(logs.AddCompName(baseCtx, "RecipeCoreService"), infra.repos.unitOfWork, infra.repos.recipeRepo) recipeService := recipe.NewRecipeService( logs.AddCompName(baseCtx, "RecipeService"), - infra.repos.unitOfWork, - infra.repos.nutrientRepo, - infra.repos.rawMaterialRepo, - infra.repos.pigTypeRepo, + nutrientService, + rawMaterialService, + pigBreedService, + pigAgeStageService, + pigTypeService, + recipeCoreService, ) return &DomainServices{ diff --git a/internal/domain/recipe/nutrient_service.go b/internal/domain/recipe/nutrient_service.go new file mode 100644 index 0000000..48ec8e8 --- /dev/null +++ b/internal/domain/recipe/nutrient_service.go @@ -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 +} diff --git a/internal/domain/recipe/pig_age_stage_service.go b/internal/domain/recipe/pig_age_stage_service.go new file mode 100644 index 0000000..d7b74ec --- /dev/null +++ b/internal/domain/recipe/pig_age_stage_service.go @@ -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 +} diff --git a/internal/domain/recipe/pig_breed_service.go b/internal/domain/recipe/pig_breed_service.go new file mode 100644 index 0000000..8b380fa --- /dev/null +++ b/internal/domain/recipe/pig_breed_service.go @@ -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 +} diff --git a/internal/domain/recipe/pig_type_service.go b/internal/domain/recipe/pig_type_service.go new file mode 100644 index 0000000..82058f2 --- /dev/null +++ b/internal/domain/recipe/pig_type_service.go @@ -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 +} diff --git a/internal/domain/recipe/raw_material_service.go b/internal/domain/recipe/raw_material_service.go new file mode 100644 index 0000000..1ade9ec --- /dev/null +++ b/internal/domain/recipe/raw_material_service.go @@ -0,0 +1,184 @@ +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 ( + ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") + ErrRawMaterialNotFound = 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) + 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 +} + +// NewRawMaterialService 创建一个新的 RawMaterialService 实例 +func NewRawMaterialService(ctx context.Context, uow repository.UnitOfWork, rawMaterialRepo repository.RawMaterialRepository) RawMaterialService { + return &rawMaterialServiceImpl{ + ctx: ctx, + uow: uow, + rawMaterialRepo: rawMaterialRepo, + } +} + +// CreateRawMaterial 实现了创建原料的核心业务逻辑 +func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string) (*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, + } + + 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) (*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 + + 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) + } + + 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 +} diff --git a/internal/domain/recipe/recipe_core_service.go b/internal/domain/recipe/recipe_core_service.go new file mode 100644 index 0000000..523f7a2 --- /dev/null +++ b/internal/domain/recipe/recipe_core_service.go @@ -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 +} diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 3641275..4ca5032 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -2,563 +2,47 @@ 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("营养种类不存在") - ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") - ErrRawMaterialNotFound = fmt.Errorf("原料不存在") - ErrPigBreedInUse = fmt.Errorf("猪品种正在被猪类型使用,无法删除") - ErrPigBreedNotFound = fmt.Errorf("猪品种不存在") - ErrPigAgeStageInUse = fmt.Errorf("猪年龄阶段正在被猪类型使用,无法删除") - ErrPigAgeStageNotFound = fmt.Errorf("猪年龄阶段不存在") - ErrPigTypeNotFound = fmt.Errorf("猪类型不存在") ) // Service 定义了配方与原料领域的核心业务服务接口 +// 该接口聚合了所有子领域的服务接口 type Service 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) - - // 原料相关接口 - CreateRawMaterial(ctx context.Context, name, description string) (*models.RawMaterial, error) - UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*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 - - // 猪品种相关接口 - 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) - - // 猪年龄阶段相关接口 - 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) - - // 猪类型相关接口 - 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 + NutrientService + RawMaterialService + PigBreedService + PigAgeStageService + PigTypeService + RecipeCoreService } -// recipeServiceImpl 是 RecipeService 的实现 +// recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 type recipeServiceImpl struct { - ctx context.Context - uow repository.UnitOfWork - nutrientRepo repository.NutrientRepository - rawMaterialRepo repository.RawMaterialRepository - pigTypeRepo repository.PigTypeRepository + ctx context.Context + NutrientService + RawMaterialService + PigBreedService + PigAgeStageService + PigTypeService + RecipeCoreService } -// NewRecipeService 创建一个新的 RecipeService 实例 -func NewRecipeService(ctx context.Context, uow repository.UnitOfWork, nutrientRepo repository.NutrientRepository, rawMaterialRepo repository.RawMaterialRepository, pigTypeRepo repository.PigTypeRepository) Service { +// NewRecipeService 创建一个新的 Service 实例 +func NewRecipeService( + ctx context.Context, + nutrientService NutrientService, + rawMaterialService RawMaterialService, + pigBreedService PigBreedService, + pigAgeStageService PigAgeStageService, + pigTypeService PigTypeService, + recipeCoreService RecipeCoreService, +) Service { return &recipeServiceImpl{ - ctx: ctx, - uow: uow, - nutrientRepo: nutrientRepo, - rawMaterialRepo: rawMaterialRepo, - pigTypeRepo: pigTypeRepo, + ctx: ctx, + NutrientService: nutrientService, + RawMaterialService: rawMaterialService, + PigBreedService: pigBreedService, + PigAgeStageService: pigAgeStageService, + PigTypeService: pigTypeService, + RecipeCoreService: recipeCoreService, } } - -// CreateNutrient 实现了创建营养种类的核心业务逻辑 -func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 -} - -// CreateRawMaterial 实现了创建原料的核心业务逻辑 -func (s *recipeServiceImpl) CreateRawMaterial(ctx context.Context, name, description string) (*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, - } - - if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil { - return nil, fmt.Errorf("创建原料失败: %w", err) - } - - return rawMaterial, nil -} - -// UpdateRawMaterial 实现了更新原料的核心业务逻辑 -func (s *recipeServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string) (*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 - - if err := s.rawMaterialRepo.UpdateRawMaterial(serviceCtx, rawMaterial); err != nil { - return nil, fmt.Errorf("更新原料失败: %w", err) - } - - return rawMaterial, nil -} - -// DeleteRawMaterial 实现了删除原料的核心业务逻辑 -func (s *recipeServiceImpl) 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) - } - - if err := s.rawMaterialRepo.DeleteRawMaterial(serviceCtx, id); err != nil { - return fmt.Errorf("删除原料失败: %w", err) - } - - return nil -} - -// GetRawMaterial 实现了获取单个原料的逻辑 -func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 -} - -// CreatePigBreed 实现了创建猪品种的核心业务逻辑 -func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 -} - -// CreatePigAgeStage 实现了创建猪年龄阶段的核心业务逻辑 -func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 -} - -// CreatePigType 实现了创建猪类型的核心业务逻辑 -func (s *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 *recipeServiceImpl) 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 -} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 3df2102..1862ba4 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -176,13 +176,21 @@ func (r *gormRawMaterialRepository) DeleteNutrientsByRawMaterialIDTx(ctx context // 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 } - repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateBatchRawMaterialNutrientsTx") - tx := db.WithContext(repoCtx) - if err := tx.Create(&nutrients).Error; err != 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 diff --git a/internal/infra/repository/recipe_repository.go b/internal/infra/repository/recipe_repository.go index 4d416a6..176cc1b 100644 --- a/internal/infra/repository/recipe_repository.go +++ b/internal/infra/repository/recipe_repository.go @@ -25,8 +25,11 @@ type RecipeRepository interface { 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 - UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) 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 实现 @@ -43,12 +46,12 @@ func NewGormRecipeRepository(ctx context.Context, db *gorm.DB) RecipeRepository // CreateRecipe 创建一个新的配方,并处理其关联的配方原料 func (r *gormRecipeRepository) CreateRecipe(ctx context.Context, recipe *models.Recipe) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateRecipe") - return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { - if err := tx.Create(recipe).Error; err != nil { - return fmt.Errorf("创建配方失败: %w", err) - } - return nil - }) + // 注意:这里的事务只针对 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获取单个配方,并预加载其关联的配方原料和原料信息 @@ -130,29 +133,6 @@ func (r *gormRecipeRepository) UpdateRecipe(ctx context.Context, recipe *models. return nil } -// UpdateRecipeIngredients 更新配方关联的原料列表 -func (r *gormRecipeRepository) UpdateRecipeIngredients(ctx context.Context, recipeID uint32, ingredients []models.RecipeIngredient) error { - repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRecipeIngredients") - - return r.db.WithContext(repoCtx).Transaction(func(tx *gorm.DB) error { - // 1. 删除所有旧的关联配方原料 - if err := tx.Where("recipe_id = ?", recipeID).Delete(&models.RecipeIngredient{}).Error; err != nil { - return fmt.Errorf("删除旧的配方原料失败: %w", err) - } - - // 2. 批量创建新的关联配方原料 - if len(ingredients) > 0 { - for i := range ingredients { - ingredients[i].RecipeID = recipeID - } - if err := tx.Create(&ingredients).Error; err != nil { - return fmt.Errorf("创建新的配方原料失败: %w", err) - } - } - return nil - }) -} - // DeleteRecipe 根据ID删除一个配方,并级联软删除关联的 RecipeIngredient 记录 func (r *gormRecipeRepository) DeleteRecipe(ctx context.Context, id uint32) error { repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteRecipe") @@ -180,3 +160,31 @@ func (r *gormRecipeRepository) DeleteRecipe(ctx context.Context, id uint32) erro 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 +} -- 2.49.1 From 1b2e211bfa2d6cd78df1d3a4e68d3395d0e7a582 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 23 Nov 2025 14:49:55 +0800 Subject: [PATCH 26/59] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E7=B1=BB=E6=8E=A7=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 70 +- docs/swagger.json | 70 +- docs/swagger.yaml | 62 +- internal/app/api/api.go | 87 +- internal/app/api/router.go | 54 +- .../app/controller/feed/feed_controller.go | 940 +----------------- .../controller/feed/nutrient_controller.go | 195 ++++ .../feed/pig_age_stage_controller.go | 193 ++++ .../controller/feed/pig_breed_controller.go | 193 ++++ .../controller/feed/pig_type_controller.go | 243 +++++ .../feed/raw_material_controller.go | 237 +++++ project_structure.txt | 11 + 12 files changed, 1245 insertions(+), 1110 deletions(-) create mode 100644 internal/app/controller/feed/nutrient_controller.go create mode 100644 internal/app/controller/feed/pig_age_stage_controller.go create mode 100644 internal/app/controller/feed/pig_breed_controller.go create mode 100644 internal/app/controller/feed/pig_type_controller.go create mode 100644 internal/app/controller/feed/raw_material_controller.go diff --git a/docs/docs.go b/docs/docs.go index 1698c3e..48a8945 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1657,7 +1657,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "获取营养种类列表", "parameters": [ @@ -1727,7 +1727,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "创建营养种类", "parameters": [ @@ -1775,7 +1775,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "获取营养种类详情", "parameters": [ @@ -1822,7 +1822,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "更新营养种类", "parameters": [ @@ -1875,7 +1875,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "删除营养种类", "parameters": [ @@ -1909,7 +1909,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪年龄阶段列表", "parameters": [ @@ -1973,7 +1973,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪年龄阶段", "parameters": [ @@ -2021,7 +2021,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪年龄阶段详情", "parameters": [ @@ -2068,7 +2068,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪年龄阶段", "parameters": [ @@ -2121,7 +2121,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪年龄阶段", "parameters": [ @@ -2155,7 +2155,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪品种列表", "parameters": [ @@ -2219,7 +2219,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪品种", "parameters": [ @@ -2267,7 +2267,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪品种详情", "parameters": [ @@ -2314,7 +2314,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪品种", "parameters": [ @@ -2367,7 +2367,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪品种", "parameters": [ @@ -2401,7 +2401,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪类型列表", "parameters": [ @@ -2483,7 +2483,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪类型", "parameters": [ @@ -2531,7 +2531,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪类型详情", "parameters": [ @@ -2578,7 +2578,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪类型", "parameters": [ @@ -2631,7 +2631,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪类型", "parameters": [ @@ -2668,7 +2668,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "全量更新猪类型的营养需求", "parameters": [ @@ -2723,7 +2723,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "获取原料列表", "parameters": [ @@ -2793,7 +2793,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "创建原料", "parameters": [ @@ -2841,7 +2841,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "获取原料详情", "parameters": [ @@ -2888,7 +2888,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "更新原料", "parameters": [ @@ -2941,7 +2941,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "删除原料", "parameters": [ @@ -2978,7 +2978,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "全量更新原料的营养成分", "parameters": [ @@ -3202,7 +3202,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -3212,12 +3211,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3227,7 +3226,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -9799,7 +9799,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -9809,10 +9808,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9822,7 +9821,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index ceb65c1..ee1aa18 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1649,7 +1649,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "获取营养种类列表", "parameters": [ @@ -1719,7 +1719,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "创建营养种类", "parameters": [ @@ -1767,7 +1767,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "获取营养种类详情", "parameters": [ @@ -1814,7 +1814,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "更新营养种类", "parameters": [ @@ -1867,7 +1867,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-营养" ], "summary": "删除营养种类", "parameters": [ @@ -1901,7 +1901,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪年龄阶段列表", "parameters": [ @@ -1965,7 +1965,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪年龄阶段", "parameters": [ @@ -2013,7 +2013,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪年龄阶段详情", "parameters": [ @@ -2060,7 +2060,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪年龄阶段", "parameters": [ @@ -2113,7 +2113,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪年龄阶段", "parameters": [ @@ -2147,7 +2147,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪品种列表", "parameters": [ @@ -2211,7 +2211,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪品种", "parameters": [ @@ -2259,7 +2259,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪品种详情", "parameters": [ @@ -2306,7 +2306,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪品种", "parameters": [ @@ -2359,7 +2359,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪品种", "parameters": [ @@ -2393,7 +2393,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪类型列表", "parameters": [ @@ -2475,7 +2475,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "创建猪类型", "parameters": [ @@ -2523,7 +2523,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "获取猪类型详情", "parameters": [ @@ -2570,7 +2570,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "更新猪类型", "parameters": [ @@ -2623,7 +2623,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "删除猪类型", "parameters": [ @@ -2660,7 +2660,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-猪" ], "summary": "全量更新猪类型的营养需求", "parameters": [ @@ -2715,7 +2715,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "获取原料列表", "parameters": [ @@ -2785,7 +2785,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "创建原料", "parameters": [ @@ -2833,7 +2833,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "获取原料详情", "parameters": [ @@ -2880,7 +2880,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "更新原料", "parameters": [ @@ -2933,7 +2933,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "删除原料", "parameters": [ @@ -2970,7 +2970,7 @@ "application/json" ], "tags": [ - "饲料管理" + "饲料管理-原料" ], "summary": "全量更新原料的营养成分", "parameters": [ @@ -3194,7 +3194,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -3204,12 +3203,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3219,7 +3218,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -9791,7 +9791,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -9801,10 +9800,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -9814,7 +9813,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 929eeea..31b7882 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2535,7 +2535,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2546,10 +2545,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2560,6 +2559,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -3618,7 +3618,7 @@ paths: - BearerAuth: [] summary: 获取营养种类列表 tags: - - 饲料管理 + - 饲料管理-营养 post: consumes: - application/json @@ -3646,7 +3646,7 @@ paths: - BearerAuth: [] summary: 创建营养种类 tags: - - 饲料管理 + - 饲料管理-营养 /api/v1/feed/nutrients/{id}: delete: description: 根据ID删除营养种类。 @@ -3667,7 +3667,7 @@ paths: - BearerAuth: [] summary: 删除营养种类 tags: - - 饲料管理 + - 饲料管理-营养 get: description: 根据ID获取单个营养种类的详细信息。 parameters: @@ -3692,7 +3692,7 @@ paths: - BearerAuth: [] summary: 获取营养种类详情 tags: - - 饲料管理 + - 饲料管理-营养 put: consumes: - application/json @@ -3725,7 +3725,7 @@ paths: - BearerAuth: [] summary: 更新营养种类 tags: - - 饲料管理 + - 饲料管理-营养 /api/v1/feed/pig-age-stages: get: description: 获取所有猪年龄阶段的列表,支持分页和过滤。 @@ -3762,7 +3762,7 @@ paths: - BearerAuth: [] summary: 获取猪年龄阶段列表 tags: - - 饲料管理 + - 饲料管理-猪 post: consumes: - application/json @@ -3790,7 +3790,7 @@ paths: - BearerAuth: [] summary: 创建猪年龄阶段 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-age-stages/{id}: delete: description: 根据ID删除猪年龄阶段。 @@ -3811,7 +3811,7 @@ paths: - BearerAuth: [] summary: 删除猪年龄阶段 tags: - - 饲料管理 + - 饲料管理-猪 get: description: 根据ID获取单个猪年龄阶段的详细信息。 parameters: @@ -3836,7 +3836,7 @@ paths: - BearerAuth: [] summary: 获取猪年龄阶段详情 tags: - - 饲料管理 + - 饲料管理-猪 put: consumes: - application/json @@ -3869,7 +3869,7 @@ paths: - BearerAuth: [] summary: 更新猪年龄阶段 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-breeds: get: description: 获取所有猪品种的列表,支持分页和过滤。 @@ -3906,7 +3906,7 @@ paths: - BearerAuth: [] summary: 获取猪品种列表 tags: - - 饲料管理 + - 饲料管理-猪 post: consumes: - application/json @@ -3934,7 +3934,7 @@ paths: - BearerAuth: [] summary: 创建猪品种 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-breeds/{id}: delete: description: 根据ID删除猪品种。 @@ -3955,7 +3955,7 @@ paths: - BearerAuth: [] summary: 删除猪品种 tags: - - 饲料管理 + - 饲料管理-猪 get: description: 根据ID获取单个猪品种的详细信息。 parameters: @@ -3980,7 +3980,7 @@ paths: - BearerAuth: [] summary: 获取猪品种详情 tags: - - 饲料管理 + - 饲料管理-猪 put: consumes: - application/json @@ -4013,7 +4013,7 @@ paths: - BearerAuth: [] summary: 更新猪品种 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-types: get: description: 获取所有猪类型的列表,支持分页和过滤。 @@ -4062,7 +4062,7 @@ paths: - BearerAuth: [] summary: 获取猪类型列表 tags: - - 饲料管理 + - 饲料管理-猪 post: consumes: - application/json @@ -4090,7 +4090,7 @@ paths: - BearerAuth: [] summary: 创建猪类型 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-types/{id}: delete: description: 根据ID删除猪类型。 @@ -4111,7 +4111,7 @@ paths: - BearerAuth: [] summary: 删除猪类型 tags: - - 饲料管理 + - 饲料管理-猪 get: description: 根据ID获取单个猪类型的详细信息。 parameters: @@ -4136,7 +4136,7 @@ paths: - BearerAuth: [] summary: 获取猪类型详情 tags: - - 饲料管理 + - 饲料管理-猪 put: consumes: - application/json @@ -4169,7 +4169,7 @@ paths: - BearerAuth: [] summary: 更新猪类型 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/pig-types/{id}/nutrient-requirements: put: consumes: @@ -4203,7 +4203,7 @@ paths: - BearerAuth: [] summary: 全量更新猪类型的营养需求 tags: - - 饲料管理 + - 饲料管理-猪 /api/v1/feed/raw-materials: get: description: 获取所有原料的列表,支持分页和过滤。 @@ -4244,7 +4244,7 @@ paths: - BearerAuth: [] summary: 获取原料列表 tags: - - 饲料管理 + - 饲料管理-原料 post: consumes: - application/json @@ -4272,7 +4272,7 @@ paths: - BearerAuth: [] summary: 创建原料 tags: - - 饲料管理 + - 饲料管理-原料 /api/v1/feed/raw-materials/{id}: delete: description: 根据ID删除原料。 @@ -4293,7 +4293,7 @@ paths: - BearerAuth: [] summary: 删除原料 tags: - - 饲料管理 + - 饲料管理-原料 get: description: 根据ID获取单个原料的详细信息。 parameters: @@ -4318,7 +4318,7 @@ paths: - BearerAuth: [] summary: 获取原料详情 tags: - - 饲料管理 + - 饲料管理-原料 put: consumes: - application/json @@ -4351,7 +4351,7 @@ paths: - BearerAuth: [] summary: 更新原料 tags: - - 饲料管理 + - 饲料管理-原料 /api/v1/feed/raw-materials/{id}/nutrients: put: consumes: @@ -4385,7 +4385,7 @@ paths: - BearerAuth: [] summary: 全量更新原料的营养成分 tags: - - 饲料管理 + - 饲料管理-原料 /api/v1/monitor/device-command-logs: get: description: 根据提供的过滤条件,分页获取设备命令日志 @@ -4484,7 +4484,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -4495,12 +4494,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4511,6 +4510,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 627ac5d..405e944 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -41,24 +41,29 @@ import ( // API 结构体定义了 HTTP 服务器及其依赖 type API struct { - echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求 - Ctx context.Context // API 组件的上下文,包含日志记录器 - userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 - tokenGenerator token.Generator // Token 服务接口,用于 JWT token 的生成和解析 - auditService service.AuditService // 审计服务,用于记录用户操作 - httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 - config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig - userController *user.Controller // 用户控制器实例 - deviceController *device.Controller // 设备控制器实例 - planController *plan.Controller // 计划控制器实例 - pigFarmController *management.PigFarmController // 猪场管理控制器实例 - pigBatchController *management.PigBatchController // 猪群控制器实例 - monitorController *monitor.Controller // 数据监控控制器实例 - healthController *health.Controller // 健康检查控制器实例 - alarmController *alarm.ThresholdAlarmController // 阈值告警控制器 - feedController *feed.Controller // 饲料管理控制器实例 - listenHandler webhook.ListenHandler // 设备上行事件监听器 - analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例 + echo *echo.Echo // Echo 引擎实例,用于处理 HTTP 请求 + Ctx context.Context // API 组件的上下文,包含日志记录器 + userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 + tokenGenerator token.Generator // Token 服务接口,用于 JWT token 的生成和解析 + auditService service.AuditService // 审计服务,用于记录用户操作 + httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 + config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig + userController *user.Controller // 用户控制器实例 + deviceController *device.Controller // 设备控制器实例 + planController *plan.Controller // 计划控制器实例 + pigFarmController *management.PigFarmController // 猪场管理控制器实例 + pigBatchController *management.PigBatchController // 猪群控制器实例 + monitorController *monitor.Controller // 数据监控控制器实例 + healthController *health.Controller // 健康检查控制器实例 + alarmController *alarm.ThresholdAlarmController // 阈值告警控制器 + feedController *feed.Controller // 饲料管理控制器实例 + nutrientController *feed.NutrientController // 营养控制器实例 + pigAgeStageController *feed.PigAgeStageController // 猪龄阶段控制器实例 + pigBreedController *feed.PigBreedController // 猪品种控制器实例 + pigTypeController *feed.PigTypeController // 猪种类控制器实例 + rawMaterialController *feed.RawMaterialController // 原料控制器实例 + listenHandler webhook.ListenHandler // 设备上行事件监听器 + analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例 } // NewAPI 创建并返回一个新的 API 实例 @@ -91,31 +96,27 @@ func NewAPI(cfg config.ServerConfig, // 初始化 API 结构体 baseCtx := context.Background() api := &API{ - echo: e, - Ctx: ctx, - userRepo: userRepo, - tokenGenerator: tokenGenerator, - 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), - // 在 NewAPI 中初始化饲料管理控制器 - feedController: feed.NewController(logs.AddCompName(baseCtx, "FeedController"), feedManagementService), + echo: e, + Ctx: ctx, + userRepo: userRepo, + tokenGenerator: tokenGenerator, + auditService: auditService, + config: cfg, + listenHandler: listenHandler, + userController: user.NewController(logs.AddCompName(baseCtx, "UserController"), userService), + deviceController: device.NewController(logs.AddCompName(baseCtx, "DeviceController"), deviceService), + planController: plan.NewController(logs.AddCompName(baseCtx, "PlanController"), planService), + pigFarmController: management.NewPigFarmController(logs.AddCompName(baseCtx, "PigFarmController"), pigFarmService), + pigBatchController: management.NewPigBatchController(logs.AddCompName(baseCtx, "PigBatchController"), pigBatchService), + monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService), + healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")), + alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService), + feedController: feed.NewController(logs.AddCompName(baseCtx, "FeedController"), feedManagementService), + nutrientController: feed.NewNutrientController(logs.AddCompName(baseCtx, "NutrientController"), feedManagementService), + pigAgeStageController: feed.NewPigAgeStageController(logs.AddCompName(baseCtx, "PigAgeStageController"), feedManagementService), + pigBreedController: feed.NewPigBreedController(logs.AddCompName(baseCtx, "PigBreedController"), feedManagementService), + pigTypeController: feed.NewPigTypeController(logs.AddCompName(baseCtx, "PigTypeController"), feedManagementService), + rawMaterialController: feed.NewRawMaterialController(logs.AddCompName(baseCtx, "RawMaterialController"), feedManagementService), } api.setupRoutes() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index c1835ce..e5d04bd 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -217,41 +217,41 @@ func (a *API) setupRoutes() { feedGroup := authGroup.Group("/feed") { // 营养种类 (Nutrient) 路由 - feedGroup.POST("/nutrients", a.feedController.CreateNutrient) - feedGroup.PUT("/nutrients/:id", a.feedController.UpdateNutrient) - feedGroup.DELETE("/nutrients/:id", a.feedController.DeleteNutrient) - feedGroup.GET("/nutrients/:id", a.feedController.GetNutrient) - feedGroup.GET("/nutrients", a.feedController.ListNutrients) + 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.feedController.CreateRawMaterial) - feedGroup.PUT("/raw-materials/:id", a.feedController.UpdateRawMaterial) - feedGroup.PUT("/raw-materials/:id/nutrients", a.feedController.UpdateRawMaterialNutrients) - feedGroup.DELETE("/raw-materials/:id", a.feedController.DeleteRawMaterial) - feedGroup.GET("/raw-materials/:id", a.feedController.GetRawMaterial) - feedGroup.GET("/raw-materials", a.feedController.ListRawMaterials) + 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.feedController.CreatePigBreed) - feedGroup.PUT("/pig-breeds/:id", a.feedController.UpdatePigBreed) - feedGroup.DELETE("/pig-breeds/:id", a.feedController.DeletePigBreed) - feedGroup.GET("/pig-breeds/:id", a.feedController.GetPigBreed) - feedGroup.GET("/pig-breeds", a.feedController.ListPigBreeds) + 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.feedController.CreatePigAgeStage) - feedGroup.PUT("/pig-age-stages/:id", a.feedController.UpdatePigAgeStage) - feedGroup.DELETE("/pig-age-stages/:id", a.feedController.DeletePigAgeStage) - feedGroup.GET("/pig-age-stages/:id", a.feedController.GetPigAgeStage) - feedGroup.GET("/pig-age-stages", a.feedController.ListPigAgeStages) + 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.feedController.CreatePigType) - feedGroup.PUT("/pig-types/:id", a.feedController.UpdatePigType) - feedGroup.DELETE("/pig-types/:id", a.feedController.DeletePigType) - feedGroup.GET("/pig-types/:id", a.feedController.GetPigType) - feedGroup.GET("/pig-types", a.feedController.ListPigTypes) - feedGroup.PUT("/pig-types/:id/nutrient-requirements", a.feedController.UpdatePigTypeNutrientRequirements) + 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) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") } diff --git a/internal/app/controller/feed/feed_controller.go b/internal/app/controller/feed/feed_controller.go index 185e070..559f853 100644 --- a/internal/app/controller/feed/feed_controller.go +++ b/internal/app/controller/feed/feed_controller.go @@ -2,18 +2,11 @@ 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" ) -// Controller 定义了饲料管理相关的控制器 +// Controller 定义了饲料管理相关的控制器,作为各个子控制器的入口 type Controller struct { ctx context.Context feedManagementService service.FeedManagementService @@ -26,934 +19,3 @@ func NewController(ctx context.Context, feedManagementService service.FeedManage feedManagementService: feedManagementService, } } - -// --- 营养种类 (Nutrient) 接口方法实现 --- - -// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) -} - -// --- 原料 (RawMaterial) 接口方法实现 --- - -// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) -} - -// --- 猪品种 (PigBreed) 接口方法实现 --- - -// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) -} - -// --- 猪年龄阶段 (PigAgeStage) 接口方法实现 --- - -// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) -} - -// --- 猪类型 (PigType) 接口方法实现 --- - -// 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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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 *Controller) 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.feedManagementService.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) -} diff --git a/internal/app/controller/feed/nutrient_controller.go b/internal/app/controller/feed/nutrient_controller.go new file mode 100644 index 0000000..97d16eb --- /dev/null +++ b/internal/app/controller/feed/nutrient_controller.go @@ -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 + feedManagementService service.FeedManagementService +} + +// NewNutrientController 创建一个新的 NutrientController 实例 +func NewNutrientController(ctx context.Context, feedManagementService service.FeedManagementService) *NutrientController { + return &NutrientController{ + ctx: ctx, + feedManagementService: 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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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) +} diff --git a/internal/app/controller/feed/pig_age_stage_controller.go b/internal/app/controller/feed/pig_age_stage_controller.go new file mode 100644 index 0000000..dffc4e1 --- /dev/null +++ b/internal/app/controller/feed/pig_age_stage_controller.go @@ -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 + feedManagementService service.FeedManagementService +} + +// NewPigAgeStageController 创建一个新的 PigAgeStageController 实例 +func NewPigAgeStageController(ctx context.Context, feedManagementService service.FeedManagementService) *PigAgeStageController { + return &PigAgeStageController{ + ctx: ctx, + feedManagementService: 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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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) +} diff --git a/internal/app/controller/feed/pig_breed_controller.go b/internal/app/controller/feed/pig_breed_controller.go new file mode 100644 index 0000000..26b54ea --- /dev/null +++ b/internal/app/controller/feed/pig_breed_controller.go @@ -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 + feedManagementService service.FeedManagementService +} + +// NewPigBreedController 创建一个新的 PigBreedController 实例 +func NewPigBreedController(ctx context.Context, feedManagementService service.FeedManagementService) *PigBreedController { + return &PigBreedController{ + ctx: ctx, + feedManagementService: 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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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) +} diff --git a/internal/app/controller/feed/pig_type_controller.go b/internal/app/controller/feed/pig_type_controller.go new file mode 100644 index 0000000..f9d0147 --- /dev/null +++ b/internal/app/controller/feed/pig_type_controller.go @@ -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 + feedManagementService service.FeedManagementService +} + +// NewPigTypeController 创建一个新的 PigTypeController 实例 +func NewPigTypeController(ctx context.Context, feedManagementService service.FeedManagementService) *PigTypeController { + return &PigTypeController{ + ctx: ctx, + feedManagementService: 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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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) +} diff --git a/internal/app/controller/feed/raw_material_controller.go b/internal/app/controller/feed/raw_material_controller.go new file mode 100644 index 0000000..dc776b7 --- /dev/null +++ b/internal/app/controller/feed/raw_material_controller.go @@ -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 + feedManagementService service.FeedManagementService +} + +// NewRawMaterialController 创建一个新的 RawMaterialController 实例 +func NewRawMaterialController(ctx context.Context, feedManagementService service.FeedManagementService) *RawMaterialController { + return &RawMaterialController{ + ctx: ctx, + feedManagementService: 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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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.feedManagementService.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) +} diff --git a/project_structure.txt b/project_structure.txt index c61b69d..57dc0a0 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -49,6 +49,11 @@ 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/feed_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/health/health_controller.go internal/app/controller/management/controller_helpers.go internal/app/controller/management/pig_batch_controller.go @@ -111,6 +116,12 @@ 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_service.go internal/domain/task/alarm_notification_task.go internal/domain/task/area_threshold_check_task.go -- 2.49.1 From 1200f36d14f4b07c88b2a25ca5a0ff4712b5aa7d Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 23 Nov 2025 15:16:45 +0800 Subject: [PATCH 27/59] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E7=B1=BB=E6=9C=8D=E5=8A=A1=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/api.go | 18 +- .../app/controller/feed/feed_controller.go | 21 - .../controller/feed/nutrient_controller.go | 20 +- .../feed/pig_age_stage_controller.go | 20 +- .../controller/feed/pig_breed_controller.go | 20 +- .../controller/feed/pig_type_controller.go | 22 +- .../feed/raw_material_controller.go | 22 +- .../app/service/feed_management_service.go | 618 ------------------ internal/app/service/nutrient_service.go | 116 ++++ internal/app/service/pig_age_stage_service.go | 122 ++++ internal/app/service/pig_breed_service.go | 130 ++++ internal/app/service/pig_type_service.go | 203 ++++++ internal/app/service/raw_material_service.go | 157 +++++ internal/core/application.go | 6 +- internal/core/component_initializers.go | 18 +- project_structure.txt | 7 +- 16 files changed, 815 insertions(+), 705 deletions(-) delete mode 100644 internal/app/controller/feed/feed_controller.go delete mode 100644 internal/app/service/feed_management_service.go create mode 100644 internal/app/service/nutrient_service.go create mode 100644 internal/app/service/pig_age_stage_service.go create mode 100644 internal/app/service/pig_breed_service.go create mode 100644 internal/app/service/pig_type_service.go create mode 100644 internal/app/service/raw_material_service.go diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 405e944..1a2cea4 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -56,7 +56,6 @@ type API struct { monitorController *monitor.Controller // 数据监控控制器实例 healthController *health.Controller // 健康检查控制器实例 alarmController *alarm.ThresholdAlarmController // 阈值告警控制器 - feedController *feed.Controller // 饲料管理控制器实例 nutrientController *feed.NutrientController // 营养控制器实例 pigAgeStageController *feed.PigAgeStageController // 猪龄阶段控制器实例 pigBreedController *feed.PigBreedController // 猪品种控制器实例 @@ -79,7 +78,11 @@ func NewAPI(cfg config.ServerConfig, userService service.UserService, auditService service.AuditService, alarmService service.ThresholdAlarmService, - feedManagementService service.FeedManagementService, + nutrientService service.NutrientService, + rawMaterialService service.RawMaterialService, + pigBreedService service.PigBreedService, + pigAgeStageService service.PigAgeStageService, + pigTypeService service.PigTypeService, tokenGenerator token.Generator, listenHandler webhook.ListenHandler, ) *API { @@ -111,12 +114,11 @@ func NewAPI(cfg config.ServerConfig, monitorController: monitor.NewController(logs.AddCompName(baseCtx, "MonitorController"), monitorService), healthController: health.NewController(logs.AddCompName(baseCtx, "HealthController")), alarmController: alarm.NewThresholdAlarmController(logs.AddCompName(baseCtx, "ThresholdAlarmController"), alarmService), - feedController: feed.NewController(logs.AddCompName(baseCtx, "FeedController"), feedManagementService), - nutrientController: feed.NewNutrientController(logs.AddCompName(baseCtx, "NutrientController"), feedManagementService), - pigAgeStageController: feed.NewPigAgeStageController(logs.AddCompName(baseCtx, "PigAgeStageController"), feedManagementService), - pigBreedController: feed.NewPigBreedController(logs.AddCompName(baseCtx, "PigBreedController"), feedManagementService), - pigTypeController: feed.NewPigTypeController(logs.AddCompName(baseCtx, "PigTypeController"), feedManagementService), - rawMaterialController: feed.NewRawMaterialController(logs.AddCompName(baseCtx, "RawMaterialController"), feedManagementService), + 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), } api.setupRoutes() // 设置所有路由 diff --git a/internal/app/controller/feed/feed_controller.go b/internal/app/controller/feed/feed_controller.go deleted file mode 100644 index 559f853..0000000 --- a/internal/app/controller/feed/feed_controller.go +++ /dev/null @@ -1,21 +0,0 @@ -package feed - -import ( - "context" - - "git.huangwc.com/pig/pig-farm-controller/internal/app/service" -) - -// Controller 定义了饲料管理相关的控制器,作为各个子控制器的入口 -type Controller struct { - ctx context.Context - feedManagementService service.FeedManagementService -} - -// NewController 创建一个新的 Controller 实例 -func NewController(ctx context.Context, feedManagementService service.FeedManagementService) *Controller { - return &Controller{ - ctx: ctx, - feedManagementService: feedManagementService, - } -} diff --git a/internal/app/controller/feed/nutrient_controller.go b/internal/app/controller/feed/nutrient_controller.go index 97d16eb..f7469c0 100644 --- a/internal/app/controller/feed/nutrient_controller.go +++ b/internal/app/controller/feed/nutrient_controller.go @@ -15,15 +15,15 @@ import ( // NutrientController 定义了营养种类相关的控制器 type NutrientController struct { - ctx context.Context - feedManagementService service.FeedManagementService + ctx context.Context + nutrientService service.NutrientService } // NewNutrientController 创建一个新的 NutrientController 实例 -func NewNutrientController(ctx context.Context, feedManagementService service.FeedManagementService) *NutrientController { +func NewNutrientController(ctx context.Context, feedManagementService service.NutrientService) *NutrientController { return &NutrientController{ - ctx: ctx, - feedManagementService: feedManagementService, + ctx: ctx, + nutrientService: feedManagementService, } } @@ -46,7 +46,7 @@ func (c *NutrientController) CreateNutrient(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.CreateNutrient(reqCtx, &req) + resp, err := c.nutrientService.CreateNutrient(reqCtx, &req) if err != nil { logger.Errorf("%s: 服务层创建营养种类失败: %v", actionType, err) if errors.Is(err, service.ErrNutrientNameConflict) { @@ -86,7 +86,7 @@ func (c *NutrientController) UpdateNutrient(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdateNutrient(reqCtx, uint32(id), &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) { @@ -121,7 +121,7 @@ func (c *NutrientController) DeleteNutrient(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的营养种类ID格式", actionType, "营养种类ID格式错误", idStr) } - err = c.feedManagementService.DeleteNutrient(reqCtx, uint32(id)) + 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) { @@ -153,7 +153,7 @@ func (c *NutrientController) GetNutrient(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的营养种类ID格式", actionType, "营养种类ID格式错误", idStr) } - resp, err := c.feedManagementService.GetNutrient(reqCtx, uint32(id)) + 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) { @@ -184,7 +184,7 @@ func (c *NutrientController) ListNutrients(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req) } - resp, err := c.feedManagementService.ListNutrients(reqCtx, &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) diff --git a/internal/app/controller/feed/pig_age_stage_controller.go b/internal/app/controller/feed/pig_age_stage_controller.go index dffc4e1..d8261a7 100644 --- a/internal/app/controller/feed/pig_age_stage_controller.go +++ b/internal/app/controller/feed/pig_age_stage_controller.go @@ -15,15 +15,15 @@ import ( // PigAgeStageController 定义了猪年龄阶段相关的控制器 type PigAgeStageController struct { - ctx context.Context - feedManagementService service.FeedManagementService + ctx context.Context + pigAgeStageService service.PigAgeStageService } // NewPigAgeStageController 创建一个新的 PigAgeStageController 实例 -func NewPigAgeStageController(ctx context.Context, feedManagementService service.FeedManagementService) *PigAgeStageController { +func NewPigAgeStageController(ctx context.Context, feedManagementService service.PigAgeStageService) *PigAgeStageController { return &PigAgeStageController{ - ctx: ctx, - feedManagementService: feedManagementService, + ctx: ctx, + pigAgeStageService: feedManagementService, } } @@ -46,7 +46,7 @@ func (c *PigAgeStageController) CreatePigAgeStage(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.CreatePigAgeStage(reqCtx, &req) + resp, err := c.pigAgeStageService.CreatePigAgeStage(reqCtx, &req) if err != nil { logger.Errorf("%s: 服务层创建猪年龄阶段失败: %v", actionType, err) // 猪年龄阶段没有名称冲突的领域错误,这里直接返回内部错误 @@ -84,7 +84,7 @@ func (c *PigAgeStageController) UpdatePigAgeStage(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdatePigAgeStage(reqCtx, uint32(id), &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) { @@ -116,7 +116,7 @@ func (c *PigAgeStageController) DeletePigAgeStage(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪年龄阶段ID格式", actionType, "猪年龄阶段ID格式错误", idStr) } - err = c.feedManagementService.DeletePigAgeStage(reqCtx, uint32(id)) + 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) { @@ -151,7 +151,7 @@ func (c *PigAgeStageController) GetPigAgeStage(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪年龄阶段ID格式", actionType, "猪年龄阶段ID格式错误", idStr) } - resp, err := c.feedManagementService.GetPigAgeStage(reqCtx, uint32(id)) + 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) { @@ -182,7 +182,7 @@ func (c *PigAgeStageController) ListPigAgeStages(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req) } - resp, err := c.feedManagementService.ListPigAgeStages(reqCtx, &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) diff --git a/internal/app/controller/feed/pig_breed_controller.go b/internal/app/controller/feed/pig_breed_controller.go index 26b54ea..a0867d8 100644 --- a/internal/app/controller/feed/pig_breed_controller.go +++ b/internal/app/controller/feed/pig_breed_controller.go @@ -15,15 +15,15 @@ import ( // PigBreedController 定义了猪品种相关的控制器 type PigBreedController struct { - ctx context.Context - feedManagementService service.FeedManagementService + ctx context.Context + pigBreedService service.PigBreedService } // NewPigBreedController 创建一个新的 PigBreedController 实例 -func NewPigBreedController(ctx context.Context, feedManagementService service.FeedManagementService) *PigBreedController { +func NewPigBreedController(ctx context.Context, feedManagementService service.PigBreedService) *PigBreedController { return &PigBreedController{ - ctx: ctx, - feedManagementService: feedManagementService, + ctx: ctx, + pigBreedService: feedManagementService, } } @@ -46,7 +46,7 @@ func (c *PigBreedController) CreatePigBreed(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.CreatePigBreed(reqCtx, &req) + resp, err := c.pigBreedService.CreatePigBreed(reqCtx, &req) if err != nil { logger.Errorf("%s: 服务层创建猪品种失败: %v", actionType, err) // 猪品种没有名称冲突的领域错误,这里直接返回内部错误 @@ -84,7 +84,7 @@ func (c *PigBreedController) UpdatePigBreed(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdatePigBreed(reqCtx, uint32(id), &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) { @@ -116,7 +116,7 @@ func (c *PigBreedController) DeletePigBreed(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪品种ID格式", actionType, "猪品种ID格式错误", idStr) } - err = c.feedManagementService.DeletePigBreed(reqCtx, uint32(id)) + 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) { @@ -151,7 +151,7 @@ func (c *PigBreedController) GetPigBreed(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪品种ID格式", actionType, "猪品种ID格式错误", idStr) } - resp, err := c.feedManagementService.GetPigBreed(reqCtx, uint32(id)) + 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) { @@ -182,7 +182,7 @@ func (c *PigBreedController) ListPigBreeds(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req) } - resp, err := c.feedManagementService.ListPigBreeds(reqCtx, &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) diff --git a/internal/app/controller/feed/pig_type_controller.go b/internal/app/controller/feed/pig_type_controller.go index f9d0147..e3c3e06 100644 --- a/internal/app/controller/feed/pig_type_controller.go +++ b/internal/app/controller/feed/pig_type_controller.go @@ -15,15 +15,15 @@ import ( // PigTypeController 定义了猪类型相关的控制器 type PigTypeController struct { - ctx context.Context - feedManagementService service.FeedManagementService + ctx context.Context + typeService service.PigTypeService } // NewPigTypeController 创建一个新的 PigTypeController 实例 -func NewPigTypeController(ctx context.Context, feedManagementService service.FeedManagementService) *PigTypeController { +func NewPigTypeController(ctx context.Context, feedManagementService service.PigTypeService) *PigTypeController { return &PigTypeController{ - ctx: ctx, - feedManagementService: feedManagementService, + ctx: ctx, + typeService: feedManagementService, } } @@ -46,7 +46,7 @@ func (c *PigTypeController) CreatePigType(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.CreatePigType(reqCtx, &req) + resp, err := c.typeService.CreatePigType(reqCtx, &req) if err != nil { logger.Errorf("%s: 服务层创建猪类型失败: %v", actionType, err) if errors.Is(err, service.ErrPigBreedNotFound) { @@ -89,7 +89,7 @@ func (c *PigTypeController) UpdatePigType(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdatePigType(reqCtx, uint32(id), &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) { @@ -127,7 +127,7 @@ func (c *PigTypeController) DeletePigType(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr) } - err = c.feedManagementService.DeletePigType(reqCtx, uint32(id)) + 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) { @@ -159,7 +159,7 @@ func (c *PigTypeController) GetPigType(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr) } - resp, err := c.feedManagementService.GetPigType(reqCtx, uint32(id)) + 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) { @@ -190,7 +190,7 @@ func (c *PigTypeController) ListPigTypes(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req) } - resp, err := c.feedManagementService.ListPigTypes(reqCtx, &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) @@ -228,7 +228,7 @@ func (c *PigTypeController) UpdatePigTypeNutrientRequirements(ctx echo.Context) return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdatePigTypeNutrientRequirements(reqCtx, uint32(id), &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) { diff --git a/internal/app/controller/feed/raw_material_controller.go b/internal/app/controller/feed/raw_material_controller.go index dc776b7..66f67e9 100644 --- a/internal/app/controller/feed/raw_material_controller.go +++ b/internal/app/controller/feed/raw_material_controller.go @@ -15,15 +15,15 @@ import ( // RawMaterialController 定义了原料相关的控制器 type RawMaterialController struct { - ctx context.Context - feedManagementService service.FeedManagementService + ctx context.Context + rawMaterialService service.RawMaterialService } // NewRawMaterialController 创建一个新的 RawMaterialController 实例 -func NewRawMaterialController(ctx context.Context, feedManagementService service.FeedManagementService) *RawMaterialController { +func NewRawMaterialController(ctx context.Context, feedManagementService service.RawMaterialService) *RawMaterialController { return &RawMaterialController{ - ctx: ctx, - feedManagementService: feedManagementService, + ctx: ctx, + rawMaterialService: feedManagementService, } } @@ -46,7 +46,7 @@ func (c *RawMaterialController) CreateRawMaterial(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.CreateRawMaterial(reqCtx, &req) + resp, err := c.rawMaterialService.CreateRawMaterial(reqCtx, &req) if err != nil { logger.Errorf("%s: 服务层创建原料失败: %v", actionType, err) if errors.Is(err, service.ErrRawMaterialNameConflict) { @@ -86,7 +86,7 @@ func (c *RawMaterialController) UpdateRawMaterial(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdateRawMaterial(reqCtx, uint32(id), &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) { @@ -121,7 +121,7 @@ func (c *RawMaterialController) DeleteRawMaterial(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr) } - err = c.feedManagementService.DeleteRawMaterial(reqCtx, uint32(id)) + 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) { @@ -153,7 +153,7 @@ func (c *RawMaterialController) GetRawMaterial(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的原料ID格式", actionType, "原料ID格式错误", idStr) } - resp, err := c.feedManagementService.GetRawMaterial(reqCtx, uint32(id)) + 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) { @@ -184,7 +184,7 @@ func (c *RawMaterialController) ListRawMaterials(ctx echo.Context) error { return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", req) } - resp, err := c.feedManagementService.ListRawMaterials(reqCtx, &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) @@ -222,7 +222,7 @@ func (c *RawMaterialController) UpdateRawMaterialNutrients(ctx echo.Context) err return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) } - resp, err := c.feedManagementService.UpdateRawMaterialNutrients(reqCtx, uint32(id), &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) { diff --git a/internal/app/service/feed_management_service.go b/internal/app/service/feed_management_service.go deleted file mode 100644 index ef4d2ae..0000000 --- a/internal/app/service/feed_management_service.go +++ /dev/null @@ -1,618 +0,0 @@ -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 ( - ErrNutrientNameConflict = errors.New("营养种类名称已存在") - ErrNutrientNotFound = errors.New("营养种类不存在") - ErrRawMaterialNameConflict = errors.New("原料名称已存在") - ErrRawMaterialNotFound = errors.New("原料不存在") - ErrPigBreedInUse = errors.New("猪品种正在被猪类型使用,无法删除") - ErrPigBreedNotFound = errors.New("猪品种不存在") - ErrPigAgeStageInUse = errors.New("猪年龄阶段正在被猪类型使用,无法删除") - ErrPigAgeStageNotFound = errors.New("猪年龄阶段不存在") - ErrPigTypeNotFound = errors.New("猪类型不存在") -) - -// FeedManagementService 定义了饲料管理的应用服务接口 -type FeedManagementService 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) - - // 原料相关 - 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) // 新增 - - // 猪品种相关 - 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) - - // 猪年龄阶段相关 - 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) - - // 猪类型相关 - 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) // 新增 -} - -// feedManagementServiceImpl 是 FeedManagementService 接口的实现 -type feedManagementServiceImpl struct { - ctx context.Context - recipeSvc recipe.Service -} - -// NewFeedManagementService 创建一个新的 FeedManagementService 实例 -func NewFeedManagementService(ctx context.Context, recipeSvc recipe.Service) FeedManagementService { - return &feedManagementServiceImpl{ - ctx: ctx, - recipeSvc: recipeSvc, - } -} - -// ===================================================================================================================== -// 营养种类 (Nutrient) 实现 -// ===================================================================================================================== - -// CreateNutrient 创建营养种类 -func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 -} - -// ===================================================================================================================== -// 原料 (RawMaterial) 实现 -// ===================================================================================================================== - -// CreateRawMaterial 创建原料 -func (s *feedManagementServiceImpl) 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) - 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 *feedManagementServiceImpl) 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) - 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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, - 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 *feedManagementServiceImpl) 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 -} - -// ===================================================================================================================== -// 猪品种 (PigBreed) 实现 -// ===================================================================================================================== - -// CreatePigBreed 创建猪品种 -func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 -} - -// ===================================================================================================================== -// 猪年龄阶段 (PigAgeStage) 实现 -// ===================================================================================================================== - -// CreatePigAgeStage 创建猪年龄阶段 -func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 -} - -// ===================================================================================================================== -// 猪类型 (PigType) 实现 -// ===================================================================================================================== - -// CreatePigType 创建猪类型 -func (s *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 *feedManagementServiceImpl) 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 -} diff --git a/internal/app/service/nutrient_service.go b/internal/app/service/nutrient_service.go new file mode 100644 index 0000000..744925e --- /dev/null +++ b/internal/app/service/nutrient_service.go @@ -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 +} diff --git a/internal/app/service/pig_age_stage_service.go b/internal/app/service/pig_age_stage_service.go new file mode 100644 index 0000000..d416ddd --- /dev/null +++ b/internal/app/service/pig_age_stage_service.go @@ -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 +} diff --git a/internal/app/service/pig_breed_service.go b/internal/app/service/pig_breed_service.go new file mode 100644 index 0000000..b1b7180 --- /dev/null +++ b/internal/app/service/pig_breed_service.go @@ -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 +} diff --git a/internal/app/service/pig_type_service.go b/internal/app/service/pig_type_service.go new file mode 100644 index 0000000..f1d9c11 --- /dev/null +++ b/internal/app/service/pig_type_service.go @@ -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 +} diff --git a/internal/app/service/raw_material_service.go b/internal/app/service/raw_material_service.go new file mode 100644 index 0000000..3cd5298 --- /dev/null +++ b/internal/app/service/raw_material_service.go @@ -0,0 +1,157 @@ +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) + 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) + 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, + 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 +} diff --git a/internal/core/application.go b/internal/core/application.go index 377fbff..2511771 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -63,7 +63,11 @@ func NewApplication(configPath string) (*Application, error) { appServices.userService, appServices.auditService, appServices.thresholdAlarmService, - appServices.feedManagementService, + appServices.nutrientService, + appServices.rawMaterialService, + appServices.pigBreedService, + appServices.pigAgeStageService, + appServices.pigTypeService, infra.tokenGenerator, infra.lora.listenHandler, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 97ed6f0..bc5339d 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -259,7 +259,11 @@ type AppServices struct { userService service.UserService auditService service.AuditService thresholdAlarmService service.ThresholdAlarmService - feedManagementService service.FeedManagementService + nutrientService service.NutrientService + pigAgeStageService service.PigAgeStageService + pigBreedService service.PigBreedService + pigTypeService service.PigTypeService + rawMaterialService service.RawMaterialService } // initAppServices 初始化所有的应用服务。 @@ -307,7 +311,11 @@ 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) - feedManagementService := service.NewFeedManagementService(logs.AddCompName(baseCtx, "FeedManagementService"), domainServices.recipeService) + 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) return &AppServices{ pigFarmService: pigFarmService, @@ -318,7 +326,11 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices planService: planService, userService: userService, thresholdAlarmService: thresholdAlarmService, - feedManagementService: feedManagementService, + nutrientService: nutrientService, + pigAgeStageService: pigAgeStageService, + pigBreedService: pigBreedService, + pigTypeService: pigTypeService, + rawMaterialService: rawMaterialService, } } diff --git a/project_structure.txt b/project_structure.txt index 57dc0a0..ed9a36d 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -48,7 +48,6 @@ 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/feed_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 @@ -85,12 +84,16 @@ 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/feed_management_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/threshold_alarm_service.go internal/app/service/user_service.go internal/app/webhook/chirp_stack.go -- 2.49.1 From d7deaa346b7034c28f52d973e0097e4de8662796 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 24 Nov 2025 13:25:15 +0800 Subject: [PATCH 28/59] =?UTF-8?q?=E9=85=8D=E6=96=B9=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E6=94=B9=E6=9F=A5=E6=9C=8D=E5=8A=A1=E5=B1=82=E5=92=8C=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/archive/recipe-management/index.md | 3 +- docs/docs.go | 348 ++++++++++++++++++ docs/swagger.json | 348 ++++++++++++++++++ docs/swagger.yaml | 215 +++++++++++ internal/app/api/api.go | 3 + internal/app/api/router.go | 7 + .../app/controller/feed/recipe_controller.go | 196 ++++++++++ internal/app/dto/feed_converter.go | 81 ++++ internal/app/dto/feed_dto.go | 46 +++ internal/app/service/recipe_service.go | 159 ++++++++ internal/core/application.go | 1 + internal/core/component_initializers.go | 3 + project_structure.txt | 2 + 13 files changed, 1411 insertions(+), 1 deletion(-) create mode 100644 internal/app/controller/feed/recipe_controller.go create mode 100644 internal/app/service/recipe_service.go diff --git a/design/archive/recipe-management/index.md b/design/archive/recipe-management/index.md index 612428d..e5bbe80 100644 --- a/design/archive/recipe-management/index.md +++ b/design/archive/recipe-management/index.md @@ -60,4 +60,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 10. 实现修改猪营养需求 11. 配方模型定义和仓库层增删改查方法 12. 配方领域层方法 -13. 重构配方领域 \ No newline at end of file +13. 重构配方领域 +14. 配方增删改查服务层和控制器 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 48a8945..e645141 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3021,6 +3021,252 @@ const docTemplate = `{ } } }, + "/api/v1/feed/recipes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有配方的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "获取配方列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListRecipeResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的配方,包含其原料组成。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "创建配方", + "parameters": [ + { + "description": "配方信息", + "name": "recipe", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateRecipeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/recipes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个配方的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "获取配方详情", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新配方信息及其原料组成。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "更新配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的配方信息", + "name": "recipe", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRecipeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "删除配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -6761,6 +7007,31 @@ const docTemplate = `{ } } }, + "dto.CreateRecipeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "配方描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "配方名称", + "type": "string", + "maxLength": 100 + }, + "recipe_ingredients": { + "description": "配方原料组成", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.CreateUserRequest": { "type": "object", "required": [ @@ -7241,6 +7512,20 @@ const docTemplate = `{ } } }, + "dto.ListRecipeResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -8214,6 +8499,44 @@ const docTemplate = `{ } } }, + "dto.RecipeIngredientDto": { + "type": "object", + "required": [ + "raw_material_id" + ], + "properties": { + "percentage": { + "description": "原料在配方中的百分比 (0-1之间)", + "type": "number", + "maximum": 1, + "minimum": 0 + }, + "raw_material_id": { + "description": "原料ID", + "type": "integer" + } + } + }, + "dto.RecipeResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "recipe_ingredients": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -9150,6 +9473,31 @@ const docTemplate = `{ } } }, + "dto.UpdateRecipeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "配方描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "配方名称", + "type": "string", + "maxLength": 100 + }, + "recipe_ingredients": { + "description": "配方原料组成", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.UserActionLogDTO": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index ee1aa18..a01c4f3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3013,6 +3013,252 @@ } } }, + "/api/v1/feed/recipes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有配方的列表,支持分页和过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "获取配方列表", + "parameters": [ + { + "type": "string", + "description": "按名称模糊查询", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "排序字段,例如 \"id DESC\"", + "name": "order_by", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListRecipeResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的配方,包含其原料组成。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "创建配方", + "parameters": [ + { + "description": "配方信息", + "name": "recipe", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateRecipeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/feed/recipes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取单个配方的详细信息。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "获取配方详情", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID更新配方信息及其原料组成。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "更新配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的配方信息", + "name": "recipe", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateRecipeRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.RecipeResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID删除配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "删除配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/monitor/device-command-logs": { "get": { "security": [ @@ -6753,6 +6999,31 @@ } } }, + "dto.CreateRecipeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "配方描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "配方名称", + "type": "string", + "maxLength": 100 + }, + "recipe_ingredients": { + "description": "配方原料组成", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.CreateUserRequest": { "type": "object", "required": [ @@ -7233,6 +7504,20 @@ } } }, + "dto.ListRecipeResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListSensorDataResponse": { "type": "object", "properties": { @@ -8206,6 +8491,44 @@ } } }, + "dto.RecipeIngredientDto": { + "type": "object", + "required": [ + "raw_material_id" + ], + "properties": { + "percentage": { + "description": "原料在配方中的百分比 (0-1之间)", + "type": "number", + "maximum": 1, + "minimum": 0 + }, + "raw_material_id": { + "description": "原料ID", + "type": "integer" + } + } + }, + "dto.RecipeResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "recipe_ingredients": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.ReclassifyPenToNewBatchRequest": { "type": "object", "required": [ @@ -9142,6 +9465,31 @@ } } }, + "dto.UpdateRecipeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "description": "配方描述", + "type": "string", + "maxLength": 255 + }, + "name": { + "description": "配方名称", + "type": "string", + "maxLength": 100 + }, + "recipe_ingredients": { + "description": "配方原料组成", + "type": "array", + "items": { + "$ref": "#/definitions/dto.RecipeIngredientDto" + } + } + } + }, "dto.UserActionLogDTO": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 31b7882..e52e437 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -423,6 +423,24 @@ definitions: required: - name type: object + dto.CreateRecipeRequest: + properties: + description: + description: 配方描述 + maxLength: 255 + type: string + name: + description: 配方名称 + maxLength: 100 + type: string + recipe_ingredients: + description: 配方原料组成 + items: + $ref: '#/definitions/dto.RecipeIngredientDto' + type: array + required: + - name + type: object dto.CreateUserRequest: properties: password: @@ -735,6 +753,15 @@ definitions: pagination: $ref: '#/definitions/dto.PaginationDTO' type: object + dto.ListRecipeResponse: + properties: + list: + items: + $ref: '#/definitions/dto.RecipeResponse' + type: array + pagination: + $ref: '#/definitions/dto.PaginationDTO' + type: object dto.ListSensorDataResponse: properties: list: @@ -1379,6 +1406,32 @@ definitions: $ref: '#/definitions/dto.RawMaterialNutrientDTO' type: array type: object + dto.RecipeIngredientDto: + properties: + percentage: + description: 原料在配方中的百分比 (0-1之间) + maximum: 1 + minimum: 0 + type: number + raw_material_id: + description: 原料ID + type: integer + required: + - raw_material_id + type: object + dto.RecipeResponse: + properties: + description: + type: string + id: + type: integer + name: + type: string + recipe_ingredients: + items: + $ref: '#/definitions/dto.RecipeIngredientDto' + type: array + type: object dto.ReclassifyPenToNewBatchRequest: properties: pen_id: @@ -2024,6 +2077,24 @@ definitions: required: - name type: object + dto.UpdateRecipeRequest: + properties: + description: + description: 配方描述 + maxLength: 255 + type: string + name: + description: 配方名称 + maxLength: 100 + type: string + recipe_ingredients: + description: 配方原料组成 + items: + $ref: '#/definitions/dto.RecipeIngredientDto' + type: array + required: + - name + type: object dto.UserActionLogDTO: properties: action_type: @@ -4386,6 +4457,150 @@ paths: summary: 全量更新原料的营养成分 tags: - 饲料管理-原料 + /api/v1/feed/recipes: + get: + description: 获取所有配方的列表,支持分页和过滤。 + parameters: + - description: 按名称模糊查询 + in: query + name: name + type: string + - description: 排序字段,例如 "id DESC" + in: query + name: order_by + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListRecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 获取配方列表 + tags: + - 饲料管理-配方 + post: + consumes: + - application/json + description: 创建一个新的配方,包含其原料组成。 + parameters: + - description: 配方信息 + in: body + name: recipe + required: true + schema: + $ref: '#/definitions/dto.CreateRecipeRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 创建配方 + tags: + - 饲料管理-配方 + /api/v1/feed/recipes/{id}: + delete: + description: 根据ID删除配方。 + parameters: + - description: 配方ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表删除成功 + schema: + $ref: '#/definitions/controller.Response' + security: + - BearerAuth: [] + summary: 删除配方 + tags: + - 饲料管理-配方 + get: + description: 根据ID获取单个配方的详细信息。 + parameters: + - description: 配方ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 获取配方详情 + tags: + - 饲料管理-配方 + put: + consumes: + - application/json + description: 根据ID更新配方信息及其原料组成。 + parameters: + - description: 配方ID + in: path + name: id + required: true + type: integer + - description: 更新后的配方信息 + in: body + name: recipe + required: true + schema: + $ref: '#/definitions/dto.UpdateRecipeRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.RecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 更新配方 + tags: + - 饲料管理-配方 /api/v1/monitor/device-command-logs: get: description: 根据提供的过滤条件,分页获取设备命令日志 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 1a2cea4..830c322 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -61,6 +61,7 @@ type API struct { pigBreedController *feed.PigBreedController // 猪品种控制器实例 pigTypeController *feed.PigTypeController // 猪种类控制器实例 rawMaterialController *feed.RawMaterialController // 原料控制器实例 + recipeController *feed.RecipeController // 配方控制器实例 listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *domain_plan.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -83,6 +84,7 @@ func NewAPI(cfg config.ServerConfig, pigBreedService service.PigBreedService, pigAgeStageService service.PigAgeStageService, pigTypeService service.PigTypeService, + recipeService service.RecipeService, tokenGenerator token.Generator, listenHandler webhook.ListenHandler, ) *API { @@ -119,6 +121,7 @@ func NewAPI(cfg config.ServerConfig, 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), } api.setupRoutes() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index e5d04bd..ce940d0 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -252,6 +252,13 @@ func (a *API) setupRoutes() { 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) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") } diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go new file mode 100644 index 0000000..ebe371b --- /dev/null +++ b/internal/app/controller/feed/recipe_controller.go @@ -0,0 +1,196 @@ +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) +} diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index bca9492..f255b8b 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -198,3 +198,84 @@ func ConvertPigTypeListToDTO(pigTypes []models.PigType, total int64, page, pageS }, } } + +// 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, + } +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 1fcb10c..395eeef 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -274,3 +274,49 @@ type PigNutrientRequirementItem struct { 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"` +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go new file mode 100644 index 0000000..01265e1 --- /dev/null +++ b/internal/app/service/recipe_service.go @@ -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 ( + 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) +} + +// recipeServiceImpl 是 RecipeService 接口的实现 +type recipeServiceImpl struct { + ctx context.Context + recipeSvc recipe.RecipeCoreService +} + +// NewRecipeService 创建一个新的 RecipeService 实例 +func NewRecipeService(ctx context.Context, recipeSvc recipe.RecipeCoreService) RecipeService { + return &recipeServiceImpl{ + ctx: ctx, + recipeSvc: recipeSvc, + } +} + +// 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 +} diff --git a/internal/core/application.go b/internal/core/application.go index 2511771..b21e53f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -68,6 +68,7 @@ func NewApplication(configPath string) (*Application, error) { appServices.pigBreedService, appServices.pigAgeStageService, appServices.pigTypeService, + appServices.recipeService, infra.tokenGenerator, infra.lora.listenHandler, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index bc5339d..6878729 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -264,6 +264,7 @@ type AppServices struct { pigBreedService service.PigBreedService pigTypeService service.PigTypeService rawMaterialService service.RawMaterialService + recipeService service.RecipeService } // initAppServices 初始化所有的应用服务。 @@ -316,6 +317,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices 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) return &AppServices{ pigFarmService: pigFarmService, @@ -331,6 +333,7 @@ func initAppServices(ctx context.Context, infra *Infrastructure, domainServices pigBreedService: pigBreedService, pigTypeService: pigTypeService, rawMaterialService: rawMaterialService, + recipeService: recipeService, } } diff --git a/project_structure.txt b/project_structure.txt index ed9a36d..c1e1dee 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -53,6 +53,7 @@ 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/management/controller_helpers.go internal/app/controller/management/pig_batch_controller.go @@ -94,6 +95,7 @@ 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 -- 2.49.1 From ae27eb142de9eb4bf93e738689728f72b9e63ff4 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 15:25:52 +0800 Subject: [PATCH 29/59] agents.md --- .gitignore | 1 + AGENTS.md | 20 +++---------------- Makefile | 8 +++++++- .../{archive => }/recipe-management/index.md | 0 4 files changed, 11 insertions(+), 18 deletions(-) rename design/{archive => }/recipe-management/index.md (100%) diff --git a/.gitignore b/.gitignore index 0dcf0d7..7809001 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ vendor/ # IDE-specific files .idea/ +.vscode/ *.swp *.swo diff --git a/AGENTS.md b/AGENTS.md index 0669699..c095f59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,18 +1,4 @@ - -# OpenSpec Instructions +# 资源地址 -These instructions are for AI assistants working in this project. - -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. - - \ No newline at end of file +1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang +2. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构 \ No newline at end of file diff --git a/Makefile b/Makefile index 44ebf4c..46a503b 100644 --- a/Makefile +++ b/Makefile @@ -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" \ No newline at end of file + gemini -m "gemini-2.5-flash" + diff --git a/design/archive/recipe-management/index.md b/design/recipe-management/index.md similarity index 100% rename from design/archive/recipe-management/index.md rename to design/recipe-management/index.md -- 2.49.1 From 44ff3b19d6e1908afb27607e40fa4e8d3d22e6cf Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 18:10:28 +0800 Subject: [PATCH 30/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=BA=93=E5=AD=98?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/recipe-management/index.md | 3 +- docs/docs.go | 321 +++++++++++++++++- docs/swagger.json | 321 +++++++++++++++++- docs/swagger.yaml | 198 ++++++++++- internal/app/api/api.go | 4 + internal/app/api/router.go | 9 + .../inventory/inventory_controller.go | 121 +++++++ internal/app/dto/inventory_converter.go | 66 ++++ internal/app/dto/inventory_dto.go | 67 ++++ internal/app/service/inventory_service.go | 157 +++++++++ internal/core/application.go | 1 + internal/core/component_initializers.go | 9 + .../domain/inventory/inventory_service.go | 170 ++++++++++ .../repository/raw_material_repository.go | 99 ++++++ project_structure.txt | 7 +- 15 files changed, 1531 insertions(+), 22 deletions(-) create mode 100644 internal/app/controller/inventory/inventory_controller.go create mode 100644 internal/app/dto/inventory_converter.go create mode 100644 internal/app/dto/inventory_dto.go create mode 100644 internal/app/service/inventory_service.go create mode 100644 internal/domain/inventory/inventory_service.go diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index e5bbe80..a1cbae1 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -61,4 +61,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 11. 配方模型定义和仓库层增删改查方法 12. 配方领域层方法 13. 重构配方领域 -14. 配方增删改查服务层和控制器 \ No newline at end of file +14. 配方增删改查服务层和控制器 +15. 实现库存管理相关逻辑 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index e645141..fb30255 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3267,6 +3267,205 @@ 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": { + "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 +3647,7 @@ const docTemplate = `{ }, { "enum": [ + 7, -1, 0, 1, @@ -3457,12 +3657,12 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3472,8 +3672,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7062,6 +7261,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 +7479,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 +7774,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": { @@ -8852,6 +9100,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": { + "type": "string" + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -10147,6 +10452,7 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -10156,10 +10462,10 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10169,8 +10475,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index a01c4f3..64d21ae 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3259,6 +3259,205 @@ } } }, + "/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": { + "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 +3639,7 @@ }, { "enum": [ + 7, -1, 0, 1, @@ -3449,12 +3649,12 @@ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3464,8 +3664,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7054,6 +7253,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 +7471,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 +7766,20 @@ } } }, + "dto.ListStockLogResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.StockLogResponse" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationDTO" + } + } + }, "dto.ListTaskExecutionLogResponse": { "type": "object", "properties": { @@ -8844,6 +9092,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": { + "type": "string" + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -10139,6 +10444,7 @@ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -10148,10 +10454,10 @@ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10161,8 +10467,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e52e437..db58d7f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -462,6 +462,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 +605,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 +795,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: @@ -1654,6 +1687,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: + type: string + type: object dto.SubPlanResponse: properties: child_plan: @@ -2606,6 +2678,7 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: + - 7 - -1 - 0 - 1 @@ -2616,10 +2689,10 @@ definitions: - -1 - 5 - 6 - - 7 format: int32 type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2630,7 +2703,6 @@ definitions: - _minLevel - _maxLevel - InvalidLevel - - _numLevels info: contact: email: divano@example.com @@ -4601,6 +4673,124 @@ 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: + 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 +4889,7 @@ paths: name: end_time type: string - enum: + - 7 - -1 - 0 - 1 @@ -4709,12 +4900,12 @@ paths: - -1 - 5 - 6 - - 7 format: int32 in: query name: level type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4725,7 +4916,6 @@ paths: - _minLevel - _maxLevel - InvalidLevel - - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 830c322..2e3b94e 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -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() // 设置所有路由 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index ce940d0..4918118 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -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("所有接口注册成功") diff --git a/internal/app/controller/inventory/inventory_controller.go b/internal/app/controller/inventory/inventory_controller.go new file mode 100644 index 0000000..296d7ab --- /dev/null +++ b/internal/app/controller/inventory/inventory_controller.go @@ -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) +} diff --git a/internal/app/dto/inventory_converter.go b/internal/app/dto/inventory_converter.go new file mode 100644 index 0000000..6399b43 --- /dev/null +++ b/internal/app/dto/inventory_converter.go @@ -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: string(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, + }, + } +} diff --git a/internal/app/dto/inventory_dto.go b/internal/app/dto/inventory_dto.go new file mode 100644 index 0000000..0a306b0 --- /dev/null +++ b/internal/app/dto/inventory_dto.go @@ -0,0 +1,67 @@ +package dto + +import "time" + +// ============================================================================================================= +// 库存 (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 string `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 []string `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"` +} diff --git a/internal/app/service/inventory_service.go b/internal/app/service/inventory_service.go new file mode 100644 index 0000000..69cf231 --- /dev/null +++ b/internal/app/service/inventory_service.go @@ -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 +} diff --git a/internal/core/application.go b/internal/core/application.go index b21e53f..38c0bfd 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -69,6 +69,7 @@ func NewApplication(configPath string) (*Application, error) { appServices.pigAgeStageService, appServices.pigTypeService, appServices.recipeService, + appServices.inventoryService, infra.tokenGenerator, infra.lora.listenHandler, ) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 6878729..70ed680 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -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 初始化所有的领域服务。 @@ -233,6 +235,9 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr recipeCoreService, ) + // 库存管理 + inventoryService := inventory.NewInventoryCoreService(logs.AddCompName(baseCtx, "InventoryCoreService"), infra.repos.unitOfWork, infra.repos.rawMaterialRepo) + return &DomainServices{ pigPenTransferManager: pigPenTransferManager, pigTradeManager: pigTradeManager, @@ -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, } } diff --git a/internal/domain/inventory/inventory_service.go b/internal/domain/inventory/inventory_service.go new file mode 100644 index 0000000..fb0de26 --- /dev/null +++ b/internal/domain/inventory/inventory_service.go @@ -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 +} diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 1862ba4..43df119 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -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" @@ -18,6 +19,16 @@ type RawMaterialListOptions struct { OrderBy string } +// StockLogListOptions 定义了查询库存日志列表时的筛选条件 +type StockLogListOptions struct { + RawMaterialID *uint32 + RawMaterialName *string + SourceTypes []models.StockLogSourceType + StartTime *time.Time + EndTime *time.Time + OrderBy string +} + // RawMaterialRepository 定义了与原料相关的数据库操作接口 type RawMaterialRepository interface { CreateRawMaterial(ctx context.Context, rawMaterial *models.RawMaterial) error @@ -32,6 +43,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 实现 @@ -219,3 +234,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 +} diff --git a/project_structure.txt b/project_structure.txt index c1e1dee..01895d2 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -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 -- 2.49.1 From c66671bf5f1ed8bb27fac8f535d29872e3a5d280 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 18:54:11 +0800 Subject: [PATCH 31/59] =?UTF-8?q?=E5=8E=9F=E6=96=99=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/raw_material_service.go | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/domain/recipe/raw_material_service.go b/internal/domain/recipe/raw_material_service.go index 1ade9ec..17befb8 100644 --- a/internal/domain/recipe/raw_material_service.go +++ b/internal/domain/recipe/raw_material_service.go @@ -12,10 +12,18 @@ 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 定义了原料领域的核心业务服务接口 @@ -33,14 +41,16 @@ 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, } } @@ -116,6 +126,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) } -- 2.49.1 From c01ce6d1e604f3b6cb5f54eeea7b03651c68d9fa Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 20:03:36 +0800 Subject: [PATCH 32/59] =?UTF-8?q?=E5=8E=9F=E6=96=99=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=82=E8=80=83=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 8 ++++++- .../feed/raw_material_controller.go | 6 ++--- internal/app/dto/feed_converter.go | 1 + internal/app/dto/feed_dto.go | 23 +++++++++++-------- internal/app/service/raw_material_service.go | 12 ++++++---- .../domain/recipe/raw_material_service.go | 14 ++++++----- internal/infra/models/raw_material.go | 5 ++-- .../repository/raw_material_repository.go | 21 +++++++++++++---- 8 files changed, 59 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c095f59..d2de6ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,10 @@ # 资源地址 1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang -2. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构 \ No newline at end of file +2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html +3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构 + +# 权限管理 + +1. 我授权你执行数据库的所有查询类sql +2. 我授权你操作浏览器访问我的项目swagger文档地址和前端项目, 并允许你进行任何操作 \ No newline at end of file diff --git a/internal/app/controller/feed/raw_material_controller.go b/internal/app/controller/feed/raw_material_controller.go index 66f67e9..6355a31 100644 --- a/internal/app/controller/feed/raw_material_controller.go +++ b/internal/app/controller/feed/raw_material_controller.go @@ -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 { diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index f255b8b..0a8a970 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -65,6 +65,7 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse { ID: rm.ID, Name: rm.Name, Description: rm.Description, + ReferencePrice: rm.ReferencePrice, RawMaterialNutrients: rawMaterialNutrientDTOs, } } diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index 395eeef..d03b8e6 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -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 是获取原料列表的响应结构 diff --git a/internal/app/service/raw_material_service.go b/internal/app/service/raw_material_service.go index 3cd5298..6622abe 100644 --- a/internal/app/service/raw_material_service.go +++ b/internal/app/service/raw_material_service.go @@ -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 { diff --git a/internal/domain/recipe/raw_material_service.go b/internal/domain/recipe/raw_material_service.go index 17befb8..a06e529 100644 --- a/internal/domain/recipe/raw_material_service.go +++ b/internal/domain/recipe/raw_material_service.go @@ -28,8 +28,8 @@ var ( // 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) @@ -55,7 +55,7 @@ func NewRawMaterialService(ctx context.Context, uow repository.UnitOfWork, rawMa } // 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") // 检查名称是否已存在 @@ -68,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 { @@ -80,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") // 检查要更新的实体是否存在 @@ -105,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) diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go index 18baa54..ebbee55 100644 --- a/internal/infra/models/raw_material.go +++ b/internal/infra/models/raw_material.go @@ -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"` } diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 43df119..a1f9e2c 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -14,9 +14,11 @@ import ( // RawMaterialListOptions 定义了查询原料列表时的筛选条件 type RawMaterialListOptions struct { - Name *string - NutrientName *string - OrderBy string + Name *string + NutrientName *string + MinReferencePrice *float32 // 参考价格最小值 + MaxReferencePrice *float32 // 参考价格最大值 + OrderBy string } // StockLogListOptions 定义了查询库存日志列表时的筛选条件 @@ -111,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 @@ -133,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 { -- 2.49.1 From 566f2d9a1500570106fc5fbc02659e41063d4308 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 20:05:52 +0800 Subject: [PATCH 33/59] =?UTF-8?q?=E6=B3=A8=E5=85=A5=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/component_initializers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index 70ed680..b811295 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -150,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) @@ -223,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"), @@ -235,9 +238,6 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr recipeCoreService, ) - // 库存管理 - inventoryService := inventory.NewInventoryCoreService(logs.AddCompName(baseCtx, "InventoryCoreService"), infra.repos.unitOfWork, infra.repos.rawMaterialRepo) - return &DomainServices{ pigPenTransferManager: pigPenTransferManager, pigTradeManager: pigTradeManager, -- 2.49.1 From d7e2777c133fdca8ff8bb098f52228f77448556d Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 25 Nov 2025 20:22:38 +0800 Subject: [PATCH 34/59] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 77 ++++++++++++++++++++++++- docs/swagger.json | 77 ++++++++++++++++++++++++- docs/swagger.yaml | 64 +++++++++++++++++++- internal/app/dto/inventory_converter.go | 2 +- internal/app/dto/inventory_dto.go | 40 +++++++------ 5 files changed, 232 insertions(+), 28 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index fb30255..1f1cd3e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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, @@ -3430,6 +3442,16 @@ const docTemplate = `{ { "type": "array", "items": { + "enum": [ + "采购入库", + "饲喂出库", + "变质出库", + "售卖出库", + "杂用领取", + "手动盘点", + "发酵出库", + "发酵入库" + ], "type": "string" }, "collectionFormat": "csv", @@ -7203,6 +7225,10 @@ const docTemplate = `{ "description": "原料名称", "type": "string", "maxLength": 100 + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -8744,6 +8770,10 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/dto.RawMaterialNutrientDTO" } + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -9153,7 +9183,7 @@ const docTemplate = `{ "type": "integer" }, "source_type": { - "type": "string" + "$ref": "#/definitions/models.StockLogSourceType" } } }, @@ -9775,6 +9805,10 @@ const docTemplate = `{ "description": "原料名称", "type": "string", "maxLength": 100 + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -10376,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": [ diff --git a/docs/swagger.json b/docs/swagger.json index 64d21ae..9e9a8a8 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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, @@ -3422,6 +3434,16 @@ { "type": "array", "items": { + "enum": [ + "采购入库", + "饲喂出库", + "变质出库", + "售卖出库", + "杂用领取", + "手动盘点", + "发酵出库", + "发酵入库" + ], "type": "string" }, "collectionFormat": "csv", @@ -7195,6 +7217,10 @@ "description": "原料名称", "type": "string", "maxLength": 100 + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -8736,6 +8762,10 @@ "items": { "$ref": "#/definitions/dto.RawMaterialNutrientDTO" } + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -9145,7 +9175,7 @@ "type": "integer" }, "source_type": { - "type": "string" + "$ref": "#/definitions/models.StockLogSourceType" } } }, @@ -9767,6 +9797,10 @@ "description": "原料名称", "type": "string", "maxLength": 100 + }, + "reference_price": { + "description": "参考价格(kg/元)", + "type": "number" } } }, @@ -10368,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": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index db58d7f..1c65e1e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -420,6 +420,9 @@ definitions: description: 原料名称 maxLength: 100 type: string + reference_price: + description: 参考价格(kg/元) + type: number required: - name type: object @@ -1438,6 +1441,9 @@ definitions: items: $ref: '#/definitions/dto.RawMaterialNutrientDTO' type: array + reference_price: + description: 参考价格(kg/元) + type: number type: object dto.RecipeIngredientDto: properties: @@ -1724,7 +1730,7 @@ definitions: source_id: type: integer source_type: - type: string + $ref: '#/definitions/models.StockLogSourceType' type: object dto.SubPlanResponse: properties: @@ -2146,6 +2152,9 @@ definitions: description: 原料名称 maxLength: 100 type: string + reference_price: + description: 参考价格(kg/元) + type: number required: - name type: object @@ -2617,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: - 计划分析 @@ -4351,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 @@ -4393,7 +4442,7 @@ paths: - application/json description: 创建一个新的原料。 parameters: - - description: 原料信息 + - description: 原料信息,包含名称、描述和参考价格 in: body name: rawMaterial required: true @@ -4472,7 +4521,7 @@ paths: name: id required: true type: integer - - description: 更新后的原料信息 + - description: 更新后的原料信息,包含名称、描述和参考价格 in: body name: rawMaterial required: true @@ -4767,6 +4816,15 @@ paths: description: 按来源类型查询 in: query items: + enum: + - 采购入库 + - 饲喂出库 + - 变质出库 + - 售卖出库 + - 杂用领取 + - 手动盘点 + - 发酵出库 + - 发酵入库 type: string name: source_types type: array diff --git a/internal/app/dto/inventory_converter.go b/internal/app/dto/inventory_converter.go index 6399b43..91ad693 100644 --- a/internal/app/dto/inventory_converter.go +++ b/internal/app/dto/inventory_converter.go @@ -41,7 +41,7 @@ func ConvertStockLogToDTO(log *models.RawMaterialStockLog) *StockLogResponse { ChangeAmount: log.ChangeAmount, BeforeQuantity: log.BeforeQuantity, AfterQuantity: log.AfterQuantity, - SourceType: string(log.SourceType), + SourceType: log.SourceType, SourceID: log.SourceID, HappenedAt: log.HappenedAt, Remarks: log.Remarks, diff --git a/internal/app/dto/inventory_dto.go b/internal/app/dto/inventory_dto.go index 0a306b0..27ec39d 100644 --- a/internal/app/dto/inventory_dto.go +++ b/internal/app/dto/inventory_dto.go @@ -1,6 +1,10 @@ package dto -import "time" +import ( + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) // ============================================================================================================= // 库存 (Inventory) 相关 DTO @@ -37,27 +41,27 @@ type ListCurrentStockResponse struct { // 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 string `json:"source_type"` - SourceID *uint32 `json:"source_id,omitempty"` - HappenedAt time.Time `json:"happened_at"` - Remarks string `json:"remarks"` + 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 []string `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"` // 排序字段 + 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 是获取库存变动历史列表的响应结构 -- 2.49.1 From ca85671a4c927fd3502f7abfdae0ca8e82867bc8 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 14:35:58 +0800 Subject: [PATCH 35/59] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yml | 2 +- config/presets-data/nutrient.json | 405 ++++++++++++++++++++++++------ internal/infra/database/seeder.go | 113 ++++++++- 3 files changed, 430 insertions(+), 90 deletions(-) diff --git a/config/config.yml b/config/config.yml index ab89f66..92b0866 100644 --- a/config/config.yml +++ b/config/config.yml @@ -12,7 +12,7 @@ server: # 日志配置 log: - level: "info" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal" + level: "debug" # 日志级别: "debug", "info", "warn", "error", "dpanic", "panic", "fatal" format: "console" # 日志格式: "console" 或 "json" enable_file: true # 是否启用文件日志 file_path: "./app_logs/app.log" # 日志文件路径 diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index 9c322a8..ff53a59 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -1739,87 +1739,330 @@ }, "descriptions": { "raw_materials": { - "DL-蛋氨酸98": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", - "L-色氨酸98": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", - "L-苏氨酸98": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", - "L-赖氨酸HCl 98": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", - "乳清粉": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", - "兔肉粉": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", - "全株玉米青贮": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", - "双低菜籽粕": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", - "向日葵籽": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", - "啤酒糟干": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", - "啤酒花渣": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", - "国产鱼粉60": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", - "土豆蛋白": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", - "大豆油": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", - "大豆粕44": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", - "大豆粕46": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", - "大豆粕48": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", - "大麦": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", - "小苏打": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", - "小麦": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", - "小麦次粉": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", - "小麦麸": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", - "木薯干": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", - "杂交构树叶粉": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", - "构树叶粉(老叶高纤维)": "老叶构树粉,纤维更高,适合母猪粗饲料使用。", - "柠檬酸渣": "湿态副产品,适口性好,可用于母猪料降低成本。", - "棉籽粕": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。", - "棕榈油": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", - "棕榈粕": "高纤维高脂肪副产品,能量一般,多用于母猪料。", - "椰子粕": "蛋白和能量中等,适口性好,可部分替代豆粕。", - "燕麦": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", - "燕麦草": "粗饲料,母猪用以增加饱腹感和肠道健康。", - "猪肺粉": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", - "玉米": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", - "玉米DDGS": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", - "玉米油": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", - "玉米胚芽粕": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", - "玉米蛋白粉60": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", - "玉米青贮": "粗饲料,母猪用以调节肠道,降低饲料成本。", - "瓜子粕": "葵花籽粕的别称,蛋白较高,纤维也高。", - "甜菜粕": "高可溶性纤维,母猪极佳的防便秘原料。", - "石粉": "最常用的钙源,价格低廉,注意粒度影响吸收率。", - "碎米": "能量接近玉米,蛋白稍低,适口性好。", - "磷酸氢钙": "猪最常用磷钙来源,有效磷高。", - "稻草粉": "最廉价粗纤维来源,母猪限量使用防便秘。", - "稻谷": "带壳稻子,能量低于玉米,纤维高。", - "稻谷糠": "米糠的一种,高脂肪高磷,需注意酸败。", - "米糠": "高能量高磷副产品,注意黄曲霉毒素和酸败。", - "米糠粕": "脱脂米糠,蛋白较高,能量降低。", - "红薯干": "高淀粉低蛋白能量原料,类似木薯。", - "肉粉": "普通肉粉,蛋白和灰分波动大,质量不稳定。", - "肉骨粉50": "含骨较高,钙磷比例好,但蛋白较低。", - "脱脂奶粉": "优质乳蛋白源,仔猪料黄金原料。", - "膨化全脂大豆": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", - "芝麻粕": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", - "花生秧粉": "粗饲料,母猪用。", - "花生粕": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", - "苜蓿草块": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", - "苜蓿草粉": "蛋白较高,但皂苷和香豆素可能影响采食。", - "苹果渣": "湿态副产品,适口性好,母猪喜欢。", - "菜籽粕": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", - "葡萄糖": "快速能量源,教槽料常用,缓解应激。", - "葵花籽粕": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", - "蔗糖": "高能量碳水,教槽料诱食用。", - "虾粉": "优质动物蛋白,含虾青素,改善体色。", - "蚕蛹粉": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", - "蚕豆": "蛋白较高,淀粉消化率好,但含抗营养因子。", - "蟹粉": "高蛋白高灰分动物蛋白,钙磷丰富。", - "血浆蛋白粉": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。", - "血粉": "赖氨酸极高,但适口性差,需喷涂使用。", - "豆磷脂": "高能量乳化剂,促进脂肪消化,改善皮毛。", - "豌豆": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", - "豌豆蛋白": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", - "进口鱼粉65": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", - "食盐": "提供钠和氯,调节电解质平衡。", - "饲料酵母粉": "富含核苷酸和小肽,促进肠道健康和免疫。", - "高粱": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", - "鱼油": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", - "鸡肉粉": "优质陆基动物蛋白,消化率高,适口性好。", - "鸭肉粉": "与鸡肉粉类似,脂肪稍高。", - "鹅肉粉": "蛋白和脂肪中等,质量稳定。" + "DL-蛋氨酸98": { + "descriptions": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", + "unit_price": 21.50 + }, + "L-色氨酸98": { + "descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", + "unit_price": 68.00 + }, + "L-苏氨酸98": { + "descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", + "unit_price": 10.80 + }, + "L-赖氨酸HCl 98": { + "descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", + "unit_price": 11.20 + }, + "乳清粉": { + "descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", + "unit_price": 6.50 + }, + "兔肉粉": { + "descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", + "unit_price": 11.50 + }, + "全株玉米青贮": { + "descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", + "unit_price": 0.45 + }, + "双低菜籽粕": { + "descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", + "unit_price": 2.40 + }, + "向日葵籽": { + "descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", + "unit_price": 5.80 + }, + "啤酒糟干": { + "descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", + "unit_price": 1.90 + }, + "啤酒花渣": { + "descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", + "unit_price": 0.60 + }, + "国产鱼粉60": { + "descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", + "unit_price": 9.50 + }, + "土豆蛋白": { + "descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", + "unit_price": 8.50 + }, + "大豆油": { + "descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", + "unit_price": 8.20 + }, + "大豆粕44": { + "descriptions": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", + "unit_price": 3.05 + }, + "大豆粕46": { + "descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", + "unit_price": 3.25 + }, + "大豆粕48": { + "descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", + "unit_price": 3.60 + }, + "大麦": { + "descriptions": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", + "unit_price": 2.10 + }, + "小苏打": { + "descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", + "unit_price": 1.60 + }, + "小麦": { + "descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", + "unit_price": 2.55 + }, + "小麦次粉": { + "descriptions": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", + "unit_price": 2.20 + }, + "小麦麸": { + "descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", + "unit_price": 1.75 + }, + "木薯干": { + "descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", + "unit_price": 2.05 + }, + "杂交构树叶粉": { + "descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", + "unit_price": 2.20 + }, + "构树叶粉(老叶高纤维)": { + "descriptions": "老叶构树粉,纤维更高,适合母猪粗饲料使用。", + "unit_price": 1.50 + }, + "柠檬酸渣": { + "descriptions": "湿态副产品,适口性好,可用于母猪料降低成本。", + "unit_price": 0.50 + }, + "棉籽粕": { + "descriptions": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。", + "unit_price": 2.80 + }, + "棕榈油": { + "descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", + "unit_price": 8.50 + }, + "棕榈粕": { + "descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。", + "unit_price": 1.60 + }, + "椰子粕": { + "descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。", + "unit_price": 2.30 + }, + "燕麦": { + "descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", + "unit_price": 3.20 + }, + "燕麦草": { + "descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。", + "unit_price": 2.60 + }, + "猪肺粉": { + "descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", + "unit_price": 9.00 + }, + "玉米": { + "descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", + "unit_price": 2.30 + }, + "玉米DDGS": { + "descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", + "unit_price": 2.15 + }, + "玉米油": { + "descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", + "unit_price": 9.50 + }, + "玉米胚芽粕": { + "descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", + "unit_price": 2.05 + }, + "玉米蛋白粉60": { + "descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", + "unit_price": 4.80 + }, + "玉米青贮": { + "descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。", + "unit_price": 0.40 + }, + "瓜子粕": { + "descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。", + "unit_price": 2.10 + }, + "甜菜粕": { + "descriptions": "高可溶性纤维,母猪极佳的防便秘原料。", + "unit_price": 1.95 + }, + "石粉": { + "descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。", + "unit_price": 0.18 + }, + "碎米": { + "descriptions": "能量接近玉米,蛋白稍低,适口性好。", + "unit_price": 2.80 + }, + "磷酸氢钙": { + "descriptions": "猪最常用磷钙来源,有效磷高。", + "unit_price": 3.20 + }, + "稻草粉": { + "descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。", + "unit_price": 0.60 + }, + "稻谷": { + "descriptions": "带壳稻子,能量低于玉米,纤维高。", + "unit_price": 1.90 + }, + "稻谷糠": { + "descriptions": "米糠的一种,高脂肪高磷,需注意酸败。", + "unit_price": 1.60 + }, + "米糠": { + "descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。", + "unit_price": 1.85 + }, + "米糠粕": { + "descriptions": "脱脂米糠,蛋白较高,能量降低。", + "unit_price": 1.95 + }, + "红薯干": { + "descriptions": "高淀粉低蛋白能量原料,类似木薯。", + "unit_price": 2.20 + }, + "肉粉": { + "descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。", + "unit_price": 4.50 + }, + "肉骨粉50": { + "descriptions": "含骨较高,钙磷比例好,但蛋白较低。", + "unit_price": 4.20 + }, + "脱脂奶粉": { + "descriptions": "优质乳蛋白源,仔猪料黄金原料。", + "unit_price": 22.00 + }, + "膨化全脂大豆": { + "descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", + "unit_price": 4.10 + }, + "芝麻粕": { + "descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", + "unit_price": 2.90 + }, + "花生秧粉": { + "descriptions": "粗饲料,母猪用。", + "unit_price": 0.85 + }, + "花生粕": { + "descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", + "unit_price": 3.70 + }, + "苜蓿草块": { + "descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", + "unit_price": 2.40 + }, + "苜蓿草粉": { + "descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。", + "unit_price": 2.50 + }, + "苹果渣": { + "descriptions": "湿态副产品,适口性好,母猪喜欢。", + "unit_price": 0.55 + }, + "菜籽粕": { + "descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", + "unit_price": 2.30 + }, + "葡萄糖": { + "descriptions": "快速能量源,教槽料常用,缓解应激。", + "unit_price": 3.80 + }, + "葵花籽粕": { + "descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", + "unit_price": 2.10 + }, + "蔗糖": { + "descriptions": "高能量碳水,教槽料诱食用。", + "unit_price": 6.50 + }, + "虾粉": { + "descriptions": "优质动物蛋白,含虾青素,改善体色。", + "unit_price": 6.00 + }, + "蚕蛹粉": { + "descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", + "unit_price": 8.00 + }, + "蚕豆": { + "descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。", + "unit_price": 3.40 + }, + "蟹粉": { + "descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。", + "unit_price": 4.50 + }, + "血浆蛋白粉": { + "descriptions": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。", + "unit_price": 45.00 + }, + "血粉": { + "descriptions": "赖氨酸极高,但适口性差,需喷涂使用。", + "unit_price": 6.50 + }, + "豆磷脂": { + "descriptions": "高能量乳化剂,促进脂肪消化,改善皮毛。", + "unit_price": 6.80 + }, + "豌豆": { + "descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", + "unit_price": 3.50 + }, + "豌豆蛋白": { + "descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", + "unit_price": 9.50 + }, + "进口鱼粉65": { + "descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", + "unit_price": 12.80 + }, + "食盐": { + "descriptions": "提供钠和氯,调节电解质平衡。", + "unit_price": 0.50 + }, + "饲料酵母粉": { + "descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。", + "unit_price": 6.50 + }, + "高粱": { + "descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", + "unit_price": 2.20 + }, + "鱼油": { + "descriptions": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", + "unit_price": 18.00 + }, + "鸡肉粉": { + "descriptions": "优质陆基动物蛋白,消化率高,适口性好。", + "unit_price": 7.50 + }, + "鸭肉粉": { + "descriptions": "与鸡肉粉类似,脂肪稍高。", + "unit_price": 7.20 + }, + "鹅肉粉": { + "descriptions": "蛋白和脂肪中等,质量稳定。", + "unit_price": 7.00 + } }, "nutrients": { "可消化蛋氨酸 (SID %)": "猪第二限制性氨基酸,直接影响瘦肉率和生长速度。", diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index 4da7561..f992bb2 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -119,6 +119,12 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { }) } +// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。 +type rawMaterialInfo struct { + Description string + UnitPrice float32 +} + // seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 func seedNutrients(tx *gorm.DB, jsonData []byte) error { // 1. 严格校验JSON文件,检查内部重复键 @@ -128,12 +134,16 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { // 2. 解析简介信息 descriptionsNode := gjson.GetBytes(jsonData, "descriptions") - rawMaterialDescriptions := make(map[string]string) + 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 { - rawMaterialDescriptions[key.String()] = value.String() + rawMaterialInfos[key.String()] = rawMaterialInfo{ + Description: value.Get("descriptions").String(), + UnitPrice: float32(value.Get("unit_price").Float()), + } return true }) descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool { @@ -148,11 +158,16 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool { rawMaterialName := rawMaterialKey.String() var rawMaterial models.RawMaterial - // 将 Description 放入 Create 对象中 + + // 获取原料的描述和价格信息 + info := rawMaterialInfos[rawMaterialName] + + // 将 Description 和 ReferencePrice 放入 Create 对象中 err = tx.Where(models.RawMaterial{Name: rawMaterialName}). FirstOrCreate(&rawMaterial, models.RawMaterial{ - Name: rawMaterialName, - Description: rawMaterialDescriptions[rawMaterialName], + Name: rawMaterialName, + Description: info.Description, + ReferencePrice: info.UnitPrice, }).Error if err != nil { // 返回 false 停止 ForEach 遍历 @@ -454,6 +469,88 @@ func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error { // validateAndParseNutrientJSON 严格校验JSON文件 func validateAndParseNutrientJSON(jsonData []byte) error { + descriptionsNode := gjson.GetBytes(jsonData, "descriptions") + if !descriptionsNode.Exists() { + return errors.New("JSON文件中缺少 'descriptions' 字段") + } + if !descriptionsNode.IsObject() { + return errors.New("'descriptions' 字段必须是一个JSON对象") + } + + rawMaterialsNode := descriptionsNode.Get("raw_materials") + if !rawMaterialsNode.Exists() { + return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段") + } + if !rawMaterialsNode.IsObject() { + return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象") + } + + // 使用 json.Decoder 严格校验 raw_materials 的结构 + decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw))) + decoder.UseNumber() + + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err) + } + + seenRawMaterials := make(map[string]bool) + + for decoder.More() { + // 1. 解析原料名称 + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("解析原料名称失败: %w", err) + } + rawMaterialName := t.(string) + if seenRawMaterials[rawMaterialName] { + return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) + } + seenRawMaterials[rawMaterialName] = true + + // 2. 解析该原料的描述和价格对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) + } + + for decoder.More() { + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err) + } + key := t.(string) + + switch key { + case "descriptions": + t, err = decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err) + } + if _, ok := t.(string); !ok { + return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) + } + case "unit_price": + t, err = decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err) + } + if _, ok := t.(json.Number); !ok { + return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) + } + default: + // 忽略其他未知字段,但仍需读取其值以继续解析 + if _, err := decoder.Token(); err != nil { + return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err) + } + } + } + + // 读取原料描述和价格对象的 "}" + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) + } + } + + // 校验 data 节点 dataNode := gjson.GetBytes(jsonData, "data") if !dataNode.Exists() { return errors.New("JSON文件中缺少 'data' 字段") @@ -462,14 +559,14 @@ func validateAndParseNutrientJSON(jsonData []byte) error { return errors.New("'data' 字段必须是一个JSON对象") } - decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) + // 重新初始化 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 = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验 for decoder.More() { // 1. 解析原料名称 -- 2.49.1 From 35eae7b3ecafce7fd103eb7a26f75dd7f8d9977e Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 15:05:34 +0800 Subject: [PATCH 36/59] =?UTF-8?q?=E5=8F=AA=E5=9C=A8=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E5=90=AF=E5=8A=A8=E5=B9=B3=E5=8F=B0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E9=A2=84=E8=AE=BE=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/seeder.go | 45 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index f992bb2..fd895ed 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -18,13 +18,22 @@ import ( ) // SeederFunc 定义了处理一种特定类型预设数据文件的函数签名。 -type SeederFunc func(tx *gorm.DB, jsonData []byte) error +type SeederFunc func(ctx context.Context, tx *gorm.DB, jsonData []byte) error + +// isTableEmpty 检查给定模型对应的数据库表是否为空。 +func isTableEmpty(tx *gorm.DB, model interface{}) (bool, error) { + var count int64 + if err := tx.Model(model).Count(&count).Error; err != nil { + return false, fmt.Errorf("查询表记录数失败: %w", err) + } + return count == 0, nil +} // SeedFromPreset 是一个通用的数据播种函数。 // 它会读取指定目录下的所有 .json 文件,并根据文件内容中的 "type" 字段进行分发。 // 同时,它会校验所有必需的预设类型是否都已成功加载。 func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { - logger := logs.TraceLogger(ctx, ctx, "SeedFromPreset") + seedCtx, logger := logs.Trace(ctx, ctx, "SeedFromPreset") // 定义必须存在的预设数据类型及其处理顺序 // 确保 "nutrient" 在 "pig_nutrient_requirements" 之前处理,因为后者依赖于前者。 @@ -97,7 +106,7 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { for _, jsonData := range jsonDatas { // 获取原始文件路径用于错误报告 originalFilePath := typeToFileMap[dataTypeStr] - if err := seederFunc(tx, jsonData); err != nil { + if err := seederFunc(seedCtx, tx, jsonData); err != nil { return fmt.Errorf("处理文件 (type: %s, path: %s) 时发生错误: %w", dataTypeStr, originalFilePath, err) } } @@ -126,7 +135,19 @@ type rawMaterialInfo struct { } // seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 -func seedNutrients(tx *gorm.DB, jsonData []byte) error { +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) @@ -154,7 +175,6 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { // 3. 将通过校验的、干净的数据写入数据库 dataNode := gjson.GetBytes(jsonData, "data") - var err error // 用于捕获 ForEach 内部的错误 dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool { rawMaterialName := rawMaterialKey.String() var rawMaterial models.RawMaterial @@ -213,7 +233,19 @@ func seedNutrients(tx *gorm.DB, jsonData []byte) error { } // seedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 -func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { +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) @@ -271,7 +303,6 @@ func seedPigNutrientRequirements(tx *gorm.DB, jsonData []byte) error { // 3. 将通过校验的、干净的数据写入数据库 dataNode := gjson.GetBytes(jsonData, "data") - var err error // 用于捕获 ForEach 内部的错误 dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool { breedName := breedKey.String() var pigBreed models.PigBreed -- 2.49.1 From ba60ed541cb28a80ce23ad4def60ae409647dd82 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 20:23:29 +0800 Subject: [PATCH 37/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yml | 2 +- design/recipe-management/index.md | 3 +- go.mod | 1 + go.sum | 2 + .../domain/recipe/recipe_generate_manager.go | 213 ++++++++++++++++++ project_structure.txt | 1 + 6 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 internal/domain/recipe/recipe_generate_manager.go diff --git a/config/config.yml b/config/config.yml index 92b0866..ab89f66 100644 --- a/config/config.yml +++ b/config/config.yml @@ -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" # 日志文件路径 diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index a1cbae1..ce7e430 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -62,4 +62,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 12. 配方领域层方法 13. 重构配方领域 14. 配方增删改查服务层和控制器 -15. 实现库存管理相关逻辑 \ No newline at end of file +15. 实现库存管理相关逻辑 +16. 实现配方生成器 \ No newline at end of file diff --git a/go.mod b/go.mod index 34117dc..f2a4bd9 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/tidwall/gjson v1.18.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.43.0 + gonum.org/v1/gonum v0.16.0 google.golang.org/protobuf v1.36.9 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index a955b19..d2b2334 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go new file mode 100644 index 0000000..c5fa384 --- /dev/null +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -0,0 +1,213 @@ +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, + } +} + +// 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("cannot generate recipe: no raw materials provided") + } + if len(pigType.PigNutrientRequirements) == 0 { + return nil, errors.New("cannot generate recipe: pig type has no nutrient requirements") + } + + // --------------------------------------------------------- + // 2. 准备数据结构 + // --------------------------------------------------------- + + // 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value + materialNutrients := make(map[uint32]map[uint32]float64) + // 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料) + materialIndex := make(map[uint32]int) + // 列表: 记录原料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) + } + } + + // 识别约束数量 + // 约束 1: 总重量 = 1 (100%) + // 约束 2..N: 营养素下限 (Min) + // 约束 N..M: 营养素上限 (Max, 仅当 Max > 0 时) + type constraintInfo struct { + isMax bool // true=上限约束(<=), false=下限约束(>=) + nutrientID uint32 + limit float64 + } + var constraints []constraintInfo + + // 添加营养约束 + for _, req := range pigType.PigNutrientRequirements { + // 下限约束 (Value >= Min) + // 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min + constraints = append(constraints, constraintInfo{ + 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("invalid requirement for nutrient %d: min > max", req.NutrientID) + } + constraints = append(constraints, constraintInfo{ + isMax: true, + nutrientID: req.NutrientID, + limit: float64(req.MaxRequirement), + }) + } + } + + // --------------------------------------------------------- + // 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c) + // --------------------------------------------------------- + + // 变量总数 = 原料数量 + 松弛变量数量 + // 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量) + // 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b + numMaterials := len(materials) + numSlack := len(constraints) + numCols := numMaterials + numSlack + + // 行数 = 1 (总量约束) + 营养约束数量 + numRows := 1 + len(constraints) + + // A: 约束系数矩阵 + A := mat.NewDense(numRows, numCols, nil) + // b: 约束值向量 + b := make([]float64, numRows) + // c: 成本向量 (目标函数系数) + c := make([]float64, numCols) + + // --- 填充 c (成本) --- + for i, m := range materials { + c[i] = float64(m.ReferencePrice) + } + // 松弛变量的成本为 0,Go 默认初始化为 0,无需操作 + + // --- 填充 Row 0: 总量约束 (Sum(x) = 1) --- + // 系数: 所有原料对应列为 1,松弛变量列为 0 + for j := 0; j < numMaterials; j++ { + A.Set(0, j, 1.0) + } + b[0] = 1.0 + + // --- 填充营养约束行 --- + for i, cons := range constraints { + rowIndex := i + 1 // 0行被总量约束占用,所以从1开始 + slackColIndex := numMaterials + i // 松弛变量列紧跟在原料列之后 + + b[rowIndex] = cons.limit + + // 1. 设置原料系数 + for j, m := range materials { + // 获取该原料这种营养素的含量,如果没有则为0 + val := materialNutrients[m.ID][cons.nutrientID] + A.Set(rowIndex, j, val) + } + + // 2. 设置松弛变量系数 + // 如果是下限 (>=): 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) + } + } + + // --------------------------------------------------------- + // 4. 执行单纯形法求解 + // --------------------------------------------------------- + + // lp.Simplex 求解: minimize c^T * x subject to A * x = b, x >= 0 + optVal, x, err := lp.Simplex(c, A, b, 1e-8, nil) + + if err != nil { + if errors.Is(err, lp.ErrInfeasible) { + return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解)") + } + if errors.Is(err, lp.ErrUnbounded) { + return nil, errors.New("计算错误:解无界 (可能数据配置有误)") + } + return nil, fmt.Errorf("配方计算失败: %w", err) + } + + // --------------------------------------------------------- + // 5. 结果解析与构建 + // --------------------------------------------------------- + + recipe := &models.Recipe{ + Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), + Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f", len(materials), optVal), + RecipeIngredients: []models.RecipeIngredient{}, + } + + // 遍历原料部分的解 (前 numMaterials 个变量) + totalPercentage := 0.0 + for i := 0; i < numMaterials; i++ { + proportion := x[i] + + // 忽略极小值 (浮点数误差) + if proportion < 1e-6 { + continue + } + + // 记录总和用于最后的校验 + totalPercentage += proportion + + recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{ + RawMaterialID: materialIDs[i], + // 数据库可能需要 RawMaterial 对象,这里只填ID,由调用方或ORM处理加载 + // 比例: float64 -> float32 + Percentage: float32(proportion), + }) + } + + // 二次校验: 确保总量约为 1 + if math.Abs(totalPercentage-1.0) > 1e-3 { + return nil, fmt.Errorf("计算结果异常:原料总量不为 100%% (计算值: %.4f)", totalPercentage) + } + + return recipe, nil +} diff --git a/project_structure.txt b/project_structure.txt index 01895d2..5ef3c45 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -132,6 +132,7 @@ internal/domain/recipe/pig_breed_service.go internal/domain/recipe/pig_type_service.go internal/domain/recipe/raw_material_service.go internal/domain/recipe/recipe_core_service.go +internal/domain/recipe/recipe_generate_manager.go internal/domain/recipe/recipe_service.go internal/domain/task/alarm_notification_task.go internal/domain/task/area_threshold_check_task.go -- 2.49.1 From 34311889e81d18bf81c7bbefa3a2e05860eebf3f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 20:44:41 +0800 Subject: [PATCH 38/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E4=B8=AD=E6=89=80=E6=9C=89=E5=8F=AF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E5=8E=9F=E6=96=99=E4=B8=80=E9=94=AE=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=85=8D=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/recipe-management/index.md | 3 +- docs/docs.go | 79 +++++++++++++++++-- docs/swagger.json | 79 +++++++++++++++++-- docs/swagger.yaml | 46 ++++++++++- internal/app/api/router.go | 1 + .../app/controller/feed/recipe_controller.go | 31 ++++++++ internal/app/dto/feed_converter.go | 12 +++ internal/app/dto/feed_dto.go | 7 ++ internal/app/service/recipe_service.go | 13 ++- internal/core/component_initializers.go | 2 + .../domain/recipe/recipe_generate_manager.go | 1 + internal/domain/recipe/recipe_service.go | 57 +++++++++++-- 12 files changed, 301 insertions(+), 30 deletions(-) diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index ce7e430..165ec05 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -63,4 +63,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 13. 重构配方领域 14. 配方增删改查服务层和控制器 15. 实现库存管理相关逻辑 -16. 实现配方生成器 \ No newline at end of file +16. 实现配方生成器 +17. 实现使用系统中所有可用的原料一键生成配方 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 1f1cd3e..9155de7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3145,6 +3145,52 @@ const docTemplate = `{ } } }, + "/api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,使用系统中所有可用的原料,自动计算并创建一个成本最优的配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用系统中所有可用的原料一键生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3669,7 +3715,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -3679,12 +3724,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3694,7 +3739,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -7439,6 +7485,23 @@ const docTemplate = `{ } } }, + "dto.GenerateRecipeResponse": { + "type": "object", + "properties": { + "description": { + "description": "新生成的配方描述", + "type": "string" + }, + "id": { + "description": "新生成的配方ID", + "type": "integer" + }, + "name": { + "description": "新生成的配方名称", + "type": "string" + } + } + }, "dto.HistoricalAlarmDTO": { "type": "object", "properties": { @@ -10523,7 +10586,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10533,10 +10595,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10546,7 +10608,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 9e9a8a8..dd534bf 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3137,6 +3137,52 @@ } } }, + "/api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,使用系统中所有可用的原料,自动计算并创建一个成本最优的配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用系统中所有可用的原料一键生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3661,7 +3707,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -3671,12 +3716,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3686,7 +3731,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -7431,6 +7477,23 @@ } } }, + "dto.GenerateRecipeResponse": { + "type": "object", + "properties": { + "description": { + "description": "新生成的配方描述", + "type": "string" + }, + "id": { + "description": "新生成的配方ID", + "type": "integer" + }, + "name": { + "description": "新生成的配方名称", + "type": "string" + } + } + }, "dto.HistoricalAlarmDTO": { "type": "object", "properties": { @@ -10515,7 +10578,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10525,10 +10587,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10538,7 +10600,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1c65e1e..4f31241 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -565,6 +565,18 @@ definitions: thresholds: type: number type: object + dto.GenerateRecipeResponse: + properties: + description: + description: 新生成的配方描述 + type: string + id: + description: 新生成的配方ID + type: integer + name: + description: 新生成的配方名称 + type: string + type: object dto.HistoricalAlarmDTO: properties: alarm_code: @@ -2719,7 +2731,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2730,10 +2741,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2744,6 +2755,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -4722,6 +4734,32 @@ paths: summary: 更新配方 tags: - 饲料管理-配方 + /api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}: + post: + description: 根据指定的猪类型ID,使用系统中所有可用的原料,自动计算并创建一个成本最优的配方。 + parameters: + - description: 猪类型ID + in: path + name: pig_type_id + required: true + type: integer + produces: + - application/json + responses: + "201": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.GenerateRecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 使用系统中所有可用的原料一键生成配方 + tags: + - 饲料管理-配方 /api/v1/inventory/stock/adjust: post: consumes: @@ -4947,7 +4985,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -4958,12 +4995,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -4974,6 +5011,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 4918118..0ad8681 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -259,6 +259,7 @@ func (a *API) setupRoutes() { feedGroup.DELETE("/recipes/:id", a.recipeController.DeleteRecipe) feedGroup.GET("/recipes/:id", a.recipeController.GetRecipe) feedGroup.GET("/recipes", a.recipeController.ListRecipes) + feedGroup.POST("/recipes/generate-from-all-materials/:pig_type_id", a.recipeController.GenerateFromAllMaterials) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go index ebe371b..1d44d00 100644 --- a/internal/app/controller/feed/recipe_controller.go +++ b/internal/app/controller/feed/recipe_controller.go @@ -194,3 +194,34 @@ func (c *RecipeController) ListRecipes(ctx echo.Context) error { logger.Infof("%s: 获取配方列表成功, 数量: %d", actionType, len(resp.List)) return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取配方列表成功", resp, actionType, "获取配方列表成功", resp) } + +// GenerateFromAllMaterials godoc +// @Summary 使用系统中所有可用的原料一键生成配方 +// @Description 根据指定的猪类型ID,使用系统中所有可用的原料,自动计算并创建一个成本最优的配方。 +// @Tags 饲料管理-配方 +// @Security BearerAuth +// @Produce json +// @Param pig_type_id path int true "猪类型ID" +// @Success 201 {object} controller.Response{data=dto.GenerateRecipeResponse} "业务码为201代表创建成功" +// @Router /api/v1/feed/recipes/generate-from-all-materials/{pig_type_id} [post] +func (c *RecipeController) GenerateFromAllMaterials(ctx echo.Context) error { + reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "GenerateFromAllMaterials") + const actionType = "使用系统中所有可用的原料一键生成配方" + + idStr := ctx.Param("pig_type_id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, idStr) + return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", idStr) + } + + recipe, err := c.recipeService.GenerateRecipeWithAllRawMaterials(reqCtx, uint32(id)) + if err != nil { + logger.Errorf("%s: 服务层生成配方失败: %v, PigTypeID: %d", actionType, err, id) + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "生成配方失败: "+err.Error(), actionType, "服务层生成配方失败", id) + } + + resp := dto.ToGenerateRecipeResponse(recipe) + logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID) + return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp) +} diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index 0a8a970..afe931f 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -280,3 +280,15 @@ func ConvertUpdateRecipeRequestToModel(req *UpdateRecipeRequest) *models.Recipe RecipeIngredients: ingredients, } } + +// ToGenerateRecipeResponse 将 models.Recipe 转换为 GenerateRecipeResponse DTO +func ToGenerateRecipeResponse(recipe *models.Recipe) *GenerateRecipeResponse { + if recipe == nil { + return nil + } + return &GenerateRecipeResponse{ + ID: recipe.ID, + Name: recipe.Name, + Description: recipe.Description, + } +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index d03b8e6..fd8b8a1 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -325,3 +325,10 @@ type ListRecipeResponse struct { List []RecipeResponse `json:"list"` Pagination PaginationDTO `json:"pagination"` } + +// GenerateRecipeResponse 是一键生成配方的响应体 +type GenerateRecipeResponse struct { + ID uint32 `json:"id"` // 新生成的配方ID + Name string `json:"name"` // 新生成的配方名称 + Description string `json:"description"` // 新生成的配方描述 +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 01265e1..0e1cdfe 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -25,22 +25,31 @@ type RecipeService interface { DeleteRecipe(ctx context.Context, id uint32) error GetRecipeByID(ctx context.Context, id uint32) (*dto.RecipeResponse, error) ListRecipes(ctx context.Context, req *dto.ListRecipeRequest) (*dto.ListRecipeResponse, error) + // GenerateRecipeWithAllRawMaterials 添加新方法 + GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) } // recipeServiceImpl 是 RecipeService 接口的实现 type recipeServiceImpl struct { ctx context.Context - recipeSvc recipe.RecipeCoreService + recipeSvc recipe.Service } // NewRecipeService 创建一个新的 RecipeService 实例 -func NewRecipeService(ctx context.Context, recipeSvc recipe.RecipeCoreService) RecipeService { +func NewRecipeService(ctx context.Context, recipeSvc recipe.Service) RecipeService { return &recipeServiceImpl{ ctx: ctx, recipeSvc: recipeSvc, } } +// GenerateRecipeWithAllRawMaterials 实现新方法 +func (s *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "GenerateRecipeWithAllRawMaterials") + // 直接调用领域服务的方法 + return s.recipeSvc.GenerateRecipeWithAllRawMaterials(serviceCtx, pigTypeID) +} + // CreateRecipe 创建配方 func (s *recipeServiceImpl) CreateRecipe(ctx context.Context, req *dto.CreateRecipeRequest) (*dto.RecipeResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRecipe") diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index b811295..bb29f09 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -228,6 +228,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr pigTypeService := recipe.NewPigTypeService(logs.AddCompName(baseCtx, "PigTypeService"), infra.repos.unitOfWork, infra.repos.pigTypeRepo) rawMaterialService := recipe.NewRawMaterialService(logs.AddCompName(baseCtx, "RawMaterialService"), infra.repos.unitOfWork, infra.repos.rawMaterialRepo, inventoryService) recipeCoreService := recipe.NewRecipeCoreService(logs.AddCompName(baseCtx, "RecipeCoreService"), infra.repos.unitOfWork, infra.repos.recipeRepo) + recipeGenerateManager := recipe.NewRecipeGenerateManager(logs.AddCompName(baseCtx, "RecipeGenerateManager")) recipeService := recipe.NewRecipeService( logs.AddCompName(baseCtx, "RecipeService"), nutrientService, @@ -236,6 +237,7 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr pigAgeStageService, pigTypeService, recipeCoreService, + recipeGenerateManager, ) return &DomainServices{ diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index c5fa384..745e1ba 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -7,6 +7,7 @@ import ( "math" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gonum.org/v1/gonum/mat" "gonum.org/v1/gonum/optimize/convex/lp" ) diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 4ca5032..d8db69b 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -2,6 +2,10 @@ package recipe import ( "context" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) // Service 定义了配方与原料领域的核心业务服务接口 @@ -13,6 +17,8 @@ type Service interface { PigAgeStageService PigTypeService RecipeCoreService + RecipeGenerateManager + GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) } // recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 @@ -24,6 +30,7 @@ type recipeServiceImpl struct { PigAgeStageService PigTypeService RecipeCoreService + RecipeGenerateManager } // NewRecipeService 创建一个新的 Service 实例 @@ -35,14 +42,50 @@ func NewRecipeService( pigAgeStageService PigAgeStageService, pigTypeService PigTypeService, recipeCoreService RecipeCoreService, + recipeGenerateManager RecipeGenerateManager, ) Service { return &recipeServiceImpl{ - ctx: ctx, - NutrientService: nutrientService, - RawMaterialService: rawMaterialService, - PigBreedService: pigBreedService, - PigAgeStageService: pigAgeStageService, - PigTypeService: pigTypeService, - RecipeCoreService: recipeCoreService, + ctx: ctx, + NutrientService: nutrientService, + RawMaterialService: rawMaterialService, + PigBreedService: pigBreedService, + PigAgeStageService: pigAgeStageService, + PigTypeService: pigTypeService, + RecipeCoreService: recipeCoreService, + RecipeGenerateManager: recipeGenerateManager, } } + +// GenerateRecipeWithAllRawMaterials 使用所有已知原料为特定猪类型生成一个新配方。 +// pigTypeID: 目标猪类型的ID。 +// 返回: 生成的配方对象指针和可能的错误。 +func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) { + // 1. 获取猪只类型信息,确保包含了营养需求 + pigType, err := r.GetPigTypeByID(ctx, pigTypeID) + if err != nil { + return nil, fmt.Errorf("获取猪类型信息失败: %w", err) + } + + // 2. 获取所有原料 + // 我们通过传递一个非常大的 pageSize 来获取所有原料,这在大多数情况下是可行的。 + // 对于超大规模系统,可能需要考虑分页迭代,但目前这是一个简单有效的策略。 + materials, _, err := r.ListRawMaterials(ctx, repository.RawMaterialListOptions{}, 1, 9999) + if err != nil { + return nil, fmt.Errorf("获取所有原料列表失败: %w", err) + } + + // 3. 调用生成器生成配方 + recipe, err := r.GenerateRecipe(ctx, *pigType, materials) + if err != nil { + return nil, fmt.Errorf("生成配方失败: %w", err) + } + + // 4. 保存新生成的配方到数据库 + // CreateRecipe 会处理配方及其成分的保存 + if recipe, err = r.CreateRecipe(ctx, recipe); err != nil { + return nil, fmt.Errorf("保存生成的配方失败: %w", err) + } + + // 5. 返回创建的配方 (现在它应该已经有了ID) + return recipe, nil +} -- 2.49.1 From 29b820b846b768c71da85ac80c274be2c5780394 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 21:08:34 +0800 Subject: [PATCH 39/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/recipe/recipe_generate_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 745e1ba..c79a5a1 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -180,7 +180,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType recipe := &models.Recipe{ Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), - Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f", len(materials), optVal), + Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", len(materials), optVal), RecipeIngredients: []models.RecipeIngredient{}, } -- 2.49.1 From 6c0f655d0a6afd06e177215ff2927273ce01b46a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 21:14:32 +0800 Subject: [PATCH 40/59] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=8E=9F=E6=96=99=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/recipe/raw_material_service.go | 16 +++++++++++++--- .../infra/repository/raw_material_repository.go | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/internal/domain/recipe/raw_material_service.go b/internal/domain/recipe/raw_material_service.go index a06e529..0dd5916 100644 --- a/internal/domain/recipe/raw_material_service.go +++ b/internal/domain/recipe/raw_material_service.go @@ -21,9 +21,10 @@ type StockQuerier interface { // 定义领域特定的错误 var ( - ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") - ErrRawMaterialNotFound = fmt.Errorf("原料不存在") - ErrStockNotEmpty = fmt.Errorf("原料尚有库存,无法删除") + ErrRawMaterialNameConflict = fmt.Errorf("原料名称已存在") + ErrRawMaterialNotFound = fmt.Errorf("原料不存在") + ErrStockNotEmpty = fmt.Errorf("原料尚有库存,无法删除") + ErrRawMaterialInUseByRecipe = fmt.Errorf("原料已被配方使用,无法删除") ) // RawMaterialService 定义了原料领域的核心业务服务接口 @@ -138,6 +139,15 @@ func (s *rawMaterialServiceImpl) DeleteRawMaterial(ctx context.Context, id uint3 return ErrStockNotEmpty } + // 检查原料是否被配方使用 + isUsed, err := s.rawMaterialRepo.IsRawMaterialUsedInRecipes(serviceCtx, id) + if err != nil { + return fmt.Errorf("检查原料是否被配方使用失败: %w", err) + } + if isUsed { + return ErrRawMaterialInUseByRecipe + } + if err := s.rawMaterialRepo.DeleteRawMaterial(serviceCtx, id); err != nil { return fmt.Errorf("删除原料失败: %w", err) } diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index a1f9e2c..8490fd6 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -41,6 +41,7 @@ type RawMaterialRepository interface { DeleteRawMaterial(ctx context.Context, id uint32) error DeleteNutrientsByRawMaterialIDTx(ctx context.Context, db *gorm.DB, rawMaterialID uint32) error CreateBatchRawMaterialNutrientsTx(ctx context.Context, db *gorm.DB, nutrients []models.RawMaterialNutrient) error + IsRawMaterialUsedInRecipes(ctx context.Context, rawMaterialID uint32) (bool, error) // 库存日志相关方法 CreateRawMaterialStockLog(ctx context.Context, log *models.RawMaterialStockLog) error @@ -329,3 +330,16 @@ func (r *gormRawMaterialRepository) ListStockLogs(ctx context.Context, opts Stoc return logs, total, nil } + +// IsRawMaterialUsedInRecipes 检查原料是否被任何配方使用 +func (r *gormRawMaterialRepository) IsRawMaterialUsedInRecipes(ctx context.Context, rawMaterialID uint32) (bool, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "IsRawMaterialUsedInRecipes") + var count int64 + err := r.db.WithContext(repoCtx).Model(&models.RecipeIngredient{}). + Where("raw_material_id = ?", rawMaterialID). + Count(&count).Error + if err != nil { + return false, fmt.Errorf("查询原料是否被配方使用失败: %w", err) + } + return count > 0, nil +} -- 2.49.1 From ce3844957f16338d63159b48c5bcc5597fcd65fa Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 22:13:51 +0800 Subject: [PATCH 41/59] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/recipe_generate_manager.go | 99 ++++++++++++++++--- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index c79a5a1..405e4fe 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -32,16 +32,70 @@ func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager { } } +const ( + // 内部虚拟填充料的名称,用于线性规划计算,不应出现在最终配方中。 + internalFillerRawMaterialName = "内部填充料_InternalFiller" + // 内部虚拟填充营养素的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("cannot generate recipe: no raw materials provided") + return nil, errors.New("无法生成配方:未提供任何原料") } if len(pigType.PigNutrientRequirements) == 0 { - return nil, errors.New("cannot generate recipe: pig type has no nutrient requirements") + 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. 准备数据结构 // --------------------------------------------------------- @@ -76,6 +130,11 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 添加营养约束 for _, req := range pigType.PigNutrientRequirements { + // 排除内部虚拟填充营养素的约束,因为它不应有实际需求 + if req.NutrientID == internalFillerNutrientID { + continue + } + // 下限约束 (Value >= Min) // 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min constraints = append(constraints, constraintInfo{ @@ -89,7 +148,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType if req.MaxRequirement > 0 { // 简单的校验,如果 Min > Max 则是逻辑矛盾,直接报错 if req.MinRequirement > req.MaxRequirement { - return nil, fmt.Errorf("invalid requirement for nutrient %d: min > max", req.NutrientID) + return nil, fmt.Errorf("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement) } constraints = append(constraints, constraintInfo{ isMax: true, @@ -106,7 +165,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 变量总数 = 原料数量 + 松弛变量数量 // 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量) // 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b - numMaterials := len(materials) + numMaterials := len(materials) // 此时已包含填充料 numSlack := len(constraints) numCols := numMaterials + numSlack @@ -166,10 +225,10 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType if err != nil { if errors.Is(err, lp.ErrInfeasible) { - return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解)") + return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解),请检查原料或营养需求配置") } if errors.Is(err, lp.ErrUnbounded) { - return nil, errors.New("计算错误:解无界 (可能数据配置有误)") + return nil, errors.New("计算错误:解无界 (可能数据配置有误,例如某个营养素没有上限约束且成本为负)") } return nil, fmt.Errorf("配方计算失败: %w", err) } @@ -178,19 +237,32 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 5. 结果解析与构建 // --------------------------------------------------------- + // 统计实际原料数量(排除填充料) + actualMaterialCount := 0 + for _, m := range materials { + if m.ID != fillerRawMaterial.ID { + actualMaterialCount++ + } + } + recipe := &models.Recipe{ Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), - Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", len(materials), optVal), + Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", actualMaterialCount, optVal), RecipeIngredients: []models.RecipeIngredient{}, } // 遍历原料部分的解 (前 numMaterials 个变量) totalPercentage := 0.0 for i := 0; i < numMaterials; i++ { + // 排除内部虚拟填充料 --- + if materialIDs[i] == fillerRawMaterial.ID { + continue // 跳过填充料,不将其加入最终配方 + } + proportion := x[i] // 忽略极小值 (浮点数误差) - if proportion < 1e-6 { + if proportion < 1e-4 { // 万分之一,即0.01% continue } @@ -199,16 +271,15 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{ RawMaterialID: materialIDs[i], - // 数据库可能需要 RawMaterial 对象,这里只填ID,由调用方或ORM处理加载 - // 比例: float64 -> float32 - Percentage: float32(proportion), + Percentage: float32(proportion), }) } - // 二次校验: 确保总量约为 1 - if math.Abs(totalPercentage-1.0) > 1e-3 { - return nil, fmt.Errorf("计算结果异常:原料总量不为 100%% (计算值: %.4f)", totalPercentage) + // 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除) + if totalPercentage > 1.0+1e-3 { // 允许略微超过100%的浮点误差,但不能显著超过 + return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage) } + // 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。 return recipe, nil } -- 2.49.1 From 5ad403bf86e0d4e40776c16861ae4a704f7eb108 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 22:35:52 +0800 Subject: [PATCH 42/59] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8E=9F=E6=96=99?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=87=8F=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/recipe_generate_manager.go | 129 ++++++++++++------ internal/infra/models/raw_material.go | 7 +- 2 files changed, 93 insertions(+), 43 deletions(-) diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 405e4fe..6e16a67 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -33,10 +33,11 @@ func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager { } const ( - // 内部虚拟填充料的名称,用于线性规划计算,不应出现在最终配方中。 + // internalFillerRawMaterialName 是内部虚拟填充料的名称。 + // 该填充料用于线性规划计算,确保总比例为100%,但不会出现在最终配方中。 internalFillerRawMaterialName = "内部填充料_InternalFiller" - // 内部虚拟填充营养素的ID,用于关联填充料,确保其不与实际营养素冲突。 - // 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值。 + // internalFillerNutrientID 是内部虚拟填充营养素的ID。 + // 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值,用于关联填充料。 internalFillerNutrientID = math.MaxUint32 ) @@ -50,13 +51,14 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType return nil, errors.New("无法生成配方:猪类型未设置营养需求") } - // 剔除无用原料 - // 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的) + // 收集猪类型所有有需求的营养素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 @@ -100,11 +102,11 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 2. 准备数据结构 // --------------------------------------------------------- - // 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value + // materialNutrients 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value materialNutrients := make(map[uint32]map[uint32]float64) - // 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料) + // materialIndex 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料) materialIndex := make(map[uint32]int) - // 列表: 记录原料ID以便结果回溯 + // materialIDs 列表: 记录原料ID以便结果回溯 materialIDs := make([]uint32, len(materials)) for i, m := range materials { @@ -117,16 +119,13 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType } } - // 识别约束数量 - // 约束 1: 总重量 = 1 (100%) - // 约束 2..N: 营养素下限 (Min) - // 约束 N..M: 营养素上限 (Max, 仅当 Max > 0 时) - type constraintInfo struct { + // nutrientConstraints 存储营养素的下限和上限约束信息。 + type nutrientConstraintInfo struct { isMax bool // true=上限约束(<=), false=下限约束(>=) nutrientID uint32 limit float64 } - var constraints []constraintInfo + var nutrientConstraints []nutrientConstraintInfo // 添加营养约束 for _, req := range pigType.PigNutrientRequirements { @@ -135,22 +134,22 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType continue } - // 下限约束 (Value >= Min) + // 添加下限约束 (Value >= Min) // 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min - constraints = append(constraints, constraintInfo{ + nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{ isMax: false, nutrientID: req.NutrientID, limit: float64(req.MinRequirement), }) - // 上限约束 (Value <= Max) + // 添加上限约束 (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) } - constraints = append(constraints, constraintInfo{ + nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{ isMax: true, nutrientID: req.NutrientID, limit: float64(req.MaxRequirement), @@ -158,19 +157,49 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType } } + // 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,表示该原料最大添加比例为 0%,即不能添加。 + // 如果 MaxAdditionRatio 为正数,则为实际限制。 + 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), + }) + } + } + // --------------------------------------------------------- // 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c) // --------------------------------------------------------- - // 变量总数 = 原料数量 + 松弛变量数量 - // 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量) - // 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b numMaterials := len(materials) // 此时已包含填充料 - numSlack := len(constraints) + numNutrientConstraints := len(nutrientConstraints) + numMaxAdditionConstraints := len(maxAdditionConstraints) + + // 松弛变量数量 = 营养约束数量 + 最大添加比例约束数量 + numSlack := numNutrientConstraints + numMaxAdditionConstraints numCols := numMaterials + numSlack - // 行数 = 1 (总量约束) + 营养约束数量 - numRows := 1 + len(constraints) + // 行数 = 1 (总量约束) + 营养约束数量 + 最大添加比例约束数量 + numRows := 1 + numNutrientConstraints + numMaxAdditionConstraints // A: 约束系数矩阵 A := mat.NewDense(numRows, numCols, nil) @@ -179,34 +208,38 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // c: 成本向量 (目标函数系数) c := make([]float64, numCols) - // --- 填充 c (成本) --- + // 填充 c (成本) for i, m := range materials { c[i] = float64(m.ReferencePrice) } // 松弛变量的成本为 0,Go 默认初始化为 0,无需操作 - // --- 填充 Row 0: 总量约束 (Sum(x) = 1) --- + // 填充 Row 0: 总量约束 (Sum(x) = 1) // 系数: 所有原料对应列为 1,松弛变量列为 0 for j := 0; j < numMaterials; j++ { A.Set(0, j, 1.0) } b[0] = 1.0 - // --- 填充营养约束行 --- - for i, cons := range constraints { - rowIndex := i + 1 // 0行被总量约束占用,所以从1开始 - slackColIndex := numMaterials + i // 松弛变量列紧跟在原料列之后 + // currentConstraintRowIndex 记录当前正在填充的约束行索引,从1开始(0行被总量约束占用)。 + currentConstraintRowIndex := 1 + + // 填充营养约束行 + for i, cons := range nutrientConstraints { + rowIndex := currentConstraintRowIndex + i + // 营养约束的松弛变量列紧跟在原料列之后 + slackColIndex := numMaterials + i b[rowIndex] = cons.limit - // 1. 设置原料系数 + // 设置原料系数 for j, m := range materials { // 获取该原料这种营养素的含量,如果没有则为0 val := materialNutrients[m.ID][cons.nutrientID] A.Set(rowIndex, j, val) } - // 2. 设置松弛变量系数 + // 设置松弛变量系数 // 如果是下限 (>=): Sum - s = Limit => s系数为 -1 // 如果是上限 (<=): Sum + s = Limit => s系数为 +1 if cons.isMax { @@ -215,6 +248,19 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType 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. 执行单纯形法求解 @@ -225,7 +271,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType if err != nil { if errors.Is(err, lp.ErrInfeasible) { - return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解),请检查原料或营养需求配置") + return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求或最大添加比例限制 (无可行解),请检查原料配置、营养需求或最大添加比例") } if errors.Is(err, lp.ErrUnbounded) { return nil, errors.New("计算错误:解无界 (可能数据配置有误,例如某个营养素没有上限约束且成本为负)") @@ -254,15 +300,16 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 遍历原料部分的解 (前 numMaterials 个变量) totalPercentage := 0.0 for i := 0; i < numMaterials; i++ { - // 排除内部虚拟填充料 --- + // 排除内部虚拟填充料,不将其加入最终配方 if materialIDs[i] == fillerRawMaterial.ID { - continue // 跳过填充料,不将其加入最终配方 + continue } proportion := x[i] - // 忽略极小值 (浮点数误差) - if proportion < 1e-4 { // 万分之一,即0.01% + // 忽略极小值 (浮点数误差)。 + // 调整过滤阈值到万分之一 (0.01%),即小于0.0001的比例将被忽略。 + if proportion < 1e-4 { continue } @@ -271,12 +318,14 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{ RawMaterialID: materialIDs[i], - Percentage: float32(proportion), + // 比例: float64 -> float32 + Percentage: float32(proportion), }) } - // 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除) - if totalPercentage > 1.0+1e-3 { // 允许略微超过100%的浮点误差,但不能显著超过 + // 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)。 + // 允许略微超过100%的浮点误差,但不能显著超过。 + if totalPercentage > 1.0+1e-3 { return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage) } // 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。 diff --git a/internal/infra/models/raw_material.go b/internal/infra/models/raw_material.go index ebbee55..5ef9869 100644 --- a/internal/infra/models/raw_material.go +++ b/internal/infra/models/raw_material.go @@ -21,9 +21,10 @@ const ( // RawMaterial 代表一种原料的静态定义,是系统中的原料字典。 type RawMaterial struct { Model - Name string `gorm:"size:100;not null;comment:原料名称"` - Description string `gorm:"size:255;comment:描述"` - ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"` + Name string `gorm:"size:100;not null;comment:原料名称"` + Description string `gorm:"size:255;comment:描述"` + ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"` + MaxAdditionRatio float32 `gorm:"comment:该物质最大添加比例"` // RawMaterialNutrients 关联此原料的所有营养素含量信息 RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"` } -- 2.49.1 From 5bd52df2405536916e7c1d799d6a0736e2aec148 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 22:41:38 +0800 Subject: [PATCH 43/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/recipe/recipe_generate_manager.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 6e16a67..3efa18c 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -171,10 +171,9 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType continue } - // 只有当 MaxAdditionRatio >= 0 时才添加约束。 - // 如果 MaxAdditionRatio 为 0,表示该原料最大添加比例为 0%,即不能添加。 - // 如果 MaxAdditionRatio 为正数,则为实际限制。 - if mat.MaxAdditionRatio >= 0 { + // 只有当 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) -- 2.49.1 From 0283c250e4406635d46086610bf0e15dddb57f0f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 22:51:58 +0800 Subject: [PATCH 44/59] =?UTF-8?q?=E9=87=8D=E6=9E=84seeder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/seeder.go | 528 +----------------- .../infra/database/seeder/nutrient_seeder.go | 268 +++++++++ .../seeder/pig_nutrient_requirement_seeder.go | 281 ++++++++++ internal/infra/database/seeder/utils.go | 18 + 4 files changed, 570 insertions(+), 525 deletions(-) create mode 100644 internal/infra/database/seeder/nutrient_seeder.go create mode 100644 internal/infra/database/seeder/pig_nutrient_requirement_seeder.go create mode 100644 internal/infra/database/seeder/utils.go diff --git a/internal/infra/database/seeder.go b/internal/infra/database/seeder.go index fd895ed..82357d1 100644 --- a/internal/infra/database/seeder.go +++ b/internal/infra/database/seeder.go @@ -1,17 +1,14 @@ package database import ( - "bytes" "context" - "encoding/json" - "errors" "fmt" "os" "path/filepath" "strings" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/database/seeder" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "github.com/tidwall/gjson" "gorm.io/gorm" @@ -95,9 +92,9 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { var seederFunc SeederFunc switch dataTypeStr { case "nutrient": - seederFunc = seedNutrients + seederFunc = seeder.SeedNutrients case "pig_nutrient_requirements": - seederFunc = seedPigNutrientRequirements + seederFunc = seeder.SeedPigNutrientRequirements default: logger.Warnf("警告: 存在未知的 type: '%s',已跳过", dataTypeStr) continue @@ -127,522 +124,3 @@ func SeedFromPreset(ctx context.Context, db *gorm.DB, presetDir string) error { return nil // 提交事务 }) } - -// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。 -type rawMaterialInfo struct { - Description string - UnitPrice float32 -} - -// seedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 -func seedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error { - logger := logs.GetLogger(ctx) - - // 检查 Nutrient 表是否为空,如果非空则跳过播种 - isEmpty, err := isTableEmpty(tx, &models.Nutrient{}) - if err != nil { - return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err) - } - if !isEmpty { - logger.Info("已存在原料数据, 跳过数据播种") - return nil - } - - // 1. 严格校验JSON文件,检查内部重复键 - if err := validateAndParseNutrientJSON(jsonData); err != nil { - return fmt.Errorf("JSON源文件校验失败: %w", err) - } - - // 2. 解析简介信息 - descriptionsNode := gjson.GetBytes(jsonData, "descriptions") - rawMaterialInfos := make(map[string]rawMaterialInfo) - nutrientDescriptions := make(map[string]string) - - if descriptionsNode.Exists() { - // 解析 raw_materials 描述和价格 - descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool { - rawMaterialInfos[key.String()] = rawMaterialInfo{ - Description: value.Get("descriptions").String(), - UnitPrice: float32(value.Get("unit_price").Float()), - } - return true - }) - descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool { - nutrientDescriptions[key.String()] = value.String() - return true - }) - } - - // 3. 将通过校验的、干净的数据写入数据库 - dataNode := gjson.GetBytes(jsonData, "data") - dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool { - rawMaterialName := rawMaterialKey.String() - var rawMaterial models.RawMaterial - - // 获取原料的描述和价格信息 - info := rawMaterialInfos[rawMaterialName] - - // 将 Description 和 ReferencePrice 放入 Create 对象中 - err = tx.Where(models.RawMaterial{Name: rawMaterialName}). - FirstOrCreate(&rawMaterial, models.RawMaterial{ - Name: rawMaterialName, - Description: info.Description, - ReferencePrice: info.UnitPrice, - }).Error - if err != nil { - // 返回 false 停止 ForEach 遍历 - return false - } - - rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool { - nutrientName := nutrientKey.String() - value := float32(nutrientValue.Float()) - - var nutrient models.Nutrient - // 将 Description 放入 Create 对象中 - err = tx.Where(models.Nutrient{Name: nutrientName}). - FirstOrCreate(&nutrient, models.Nutrient{ - Name: nutrientName, - Description: nutrientDescriptions[nutrientName], - }).Error - if err != nil { - // 返回 false 停止 ForEach 遍历 - return false - } - - linkData := models.RawMaterialNutrient{ - RawMaterialID: rawMaterial.ID, - NutrientID: nutrient.ID, - } - // 使用 FirstOrCreate 确保关联的唯一性 - err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ - RawMaterialID: linkData.RawMaterialID, - NutrientID: linkData.NutrientID, - Value: value, - }).Error - if err != nil { - // 返回 false 停止 ForEach 遍历 - return false - } - return true - }) - return err == nil // 如果内部遍历有错误,则停止外部遍历 - }) - - return err // 返回捕获到的错误 -} - -// seedPigNutrientRequirements 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 -func seedPigNutrientRequirements(ctx context.Context, tx *gorm.DB, jsonData []byte) error { - logger := logs.GetLogger(ctx) - - // 检查 PigBreed 表是否为空,如果非空则跳过播种 - isEmpty, err := isTableEmpty(tx, &models.PigBreed{}) - if err != nil { - return fmt.Errorf("检查 PigBreed 表是否为空失败: %w", err) - } - if !isEmpty { - logger.Info("已存在猪种数据, 跳过数据播种") - return nil - } - - // 1. 严格校验JSON文件,检查内部重复键 - if err := validateAndParsePigNutrientRequirementJSON(jsonData); err != nil { - return fmt.Errorf("JSON源文件校验失败: %w", err) - } - - // 2. 解析简介信息 - descriptionsNode := gjson.GetBytes(jsonData, "descriptions") - pigBreedDescriptions := make(map[string]models.PigBreed) - pigAgeStageDescriptions := make(map[string]models.PigAgeStage) - pigTypeDescriptions := make(map[string]map[string]models.PigType) - - if descriptionsNode.Exists() { - // 解析 pig_breeds 描述 - descriptionsNode.Get("pig_breeds").ForEach(func(key, value gjson.Result) bool { - var pb models.PigBreed - pb.Name = key.String() - pb.Description = value.Get("description").String() - pb.ParentInfo = value.Get("parent_info").String() - pb.AppearanceFeatures = value.Get("appearance_features").String() - pb.BreedAdvantages = value.Get("breed_advantages").String() - pb.BreedDisadvantages = value.Get("breed_disadvantages").String() - pigBreedDescriptions[key.String()] = pb - return true - }) - - // 解析 pig_age_stages 描述 - descriptionsNode.Get("pig_age_stages").ForEach(func(key, value gjson.Result) bool { - var pas models.PigAgeStage - pas.Name = key.String() - pas.Description = value.String() - pigAgeStageDescriptions[key.String()] = pas - return true - }) - - // 解析 pig_breed_age_stages (PigType) 描述 - descriptionsNode.Get("pig_breed_age_stages").ForEach(func(breedKey, breedValue gjson.Result) bool { - if _, ok := pigTypeDescriptions[breedKey.String()]; !ok { - pigTypeDescriptions[breedKey.String()] = make(map[string]models.PigType) - } - breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool { - var pt models.PigType - pt.Description = ageStageValue.Get("description").String() - pt.DailyFeedIntake = float32(ageStageValue.Get("daily_feed_intake").Float()) - pt.DailyGainWeight = float32(ageStageValue.Get("daily_gain_weight").Float()) - pt.MinDays = uint32(ageStageValue.Get("min_days").Uint()) - pt.MaxDays = uint32(ageStageValue.Get("max_days").Uint()) - pt.MinWeight = float32(ageStageValue.Get("min_weight").Float()) - pt.MaxWeight = float32(ageStageValue.Get("max_weight").Float()) - pigTypeDescriptions[breedKey.String()][ageStageKey.String()] = pt - return true - }) - return true - }) - } - - // 3. 将通过校验的、干净的数据写入数据库 - dataNode := gjson.GetBytes(jsonData, "data") - dataNode.ForEach(func(breedKey, breedValue gjson.Result) bool { - breedName := breedKey.String() - var pigBreed models.PigBreed - // 查找或创建 PigBreed - pbDesc := pigBreedDescriptions[breedName] - err = tx.Where(models.PigBreed{Name: breedName}). - FirstOrCreate(&pigBreed, models.PigBreed{ - Name: breedName, - Description: pbDesc.Description, - ParentInfo: pbDesc.ParentInfo, - AppearanceFeatures: pbDesc.AppearanceFeatures, - BreedAdvantages: pbDesc.BreedAdvantages, - BreedDisadvantages: pbDesc.BreedDisadvantages, - }).Error - if err != nil { - return false - } - - breedValue.ForEach(func(ageStageKey, ageStageValue gjson.Result) bool { - ageStageName := ageStageKey.String() - var pigAgeStage models.PigAgeStage - // 查找或创建 PigAgeStage - pasDesc := pigAgeStageDescriptions[ageStageName] - err = tx.Where(models.PigAgeStage{Name: ageStageName}). - FirstOrCreate(&pigAgeStage, models.PigAgeStage{ - Name: ageStageName, - Description: pasDesc.Description, - }).Error - - if err != nil { - return false - } - - var pigType models.PigType - // 查找或创建 PigType - ptDesc := pigTypeDescriptions[breedName][ageStageName] - err = tx.Where(models.PigType{BreedID: pigBreed.ID, AgeStageID: pigAgeStage.ID}). - FirstOrCreate(&pigType, models.PigType{ - BreedID: pigBreed.ID, - AgeStageID: pigAgeStage.ID, - Description: ptDesc.Description, - DailyFeedIntake: ptDesc.DailyFeedIntake, - DailyGainWeight: ptDesc.DailyGainWeight, - MinDays: ptDesc.MinDays, - MaxDays: ptDesc.MaxDays, - MinWeight: ptDesc.MinWeight, - MaxWeight: ptDesc.MaxWeight, - }).Error - if err != nil { - return false - } - - ageStageValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool { - nutrientName := nutrientKey.String() - minReq := float32(nutrientValue.Get("min_requirement").Float()) - maxReq := float32(nutrientValue.Get("max_requirement").Float()) - - var nutrient models.Nutrient - // 查找或创建 Nutrient (这里假设 Nutrient 已经在 seedNutrients 中处理,但为了健壮性,再次 FirstOrCreate) - err = tx.Where(models.Nutrient{Name: nutrientName}). - FirstOrCreate(&nutrient, models.Nutrient{ - Name: nutrientName, - // Description 字段在 nutrient seeder 中处理,这里不设置 - }).Error - if err != nil { - return false - } - - linkData := models.PigNutrientRequirement{ - PigTypeID: pigType.ID, - NutrientID: nutrient.ID, - MinRequirement: minReq, - MaxRequirement: maxReq, - } - // 使用 FirstOrCreate 确保关联的唯一性 - err = tx.Where(models.PigNutrientRequirement{ - PigTypeID: pigType.ID, - NutrientID: nutrient.ID, - }).FirstOrCreate(&linkData, linkData).Error - if err != nil { - return false - } - return true - }) - return err == nil // 如果内部遍历有错误,则停止外部遍历 - }) - return err == nil // 如果内部遍历有错误,则停止外部遍历 - }) - return err // 返回捕获到的错误 -} - -// validateAndParsePigNutrientRequirementJSON 严格校验猪营养需求JSON文件 -func validateAndParsePigNutrientRequirementJSON(jsonData []byte) error { - dataNode := gjson.GetBytes(jsonData, "data") - if !dataNode.Exists() { - return errors.New("JSON文件中缺少 'data' 字段") - } - if !dataNode.IsObject() { - return errors.New("'data' 字段必须是一个JSON对象") - } - - decoder := json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) - decoder.UseNumber() - - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("'data' 字段解析起始符失败: %v", err) - } - - seenBreeds := make(map[string]bool) - - for decoder.More() { - // 解析 PigBreed 名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("解析猪品种名称失败: %w", err) - } - breedName := t.(string) - if seenBreeds[breedName] { - return fmt.Errorf("猪品种名称 '%s' 重复", breedName) - } - seenBreeds[breedName] = true - - // 解析该品种的年龄阶段对象 - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("期望猪品种 '%s' 的值是一个JSON对象", breedName) - } - - seenAgeStages := make(map[string]bool) - - for decoder.More() { - // 解析 PigAgeStage 名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("在猪品种 '%s' 中解析年龄阶段名称失败: %w", breedName, err) - } - ageStageName := t.(string) - if seenAgeStages[ageStageName] { - return fmt.Errorf("在猪品种 '%s' 中, 年龄阶段名称 '%s' 重复", breedName, ageStageName) - } - seenAgeStages[ageStageName] = true - - // 解析该年龄阶段的营养成分对象 - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("期望年龄阶段 '%s' 的值是一个JSON对象", ageStageName) - } - - seenNutrients := make(map[string]bool) - - for decoder.More() { - // 解析 Nutrient 名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("在年龄阶段 '%s' 中解析营养素名称失败: %w", ageStageName, err) - } - nutrientName := t.(string) - if seenNutrients[nutrientName] { - return fmt.Errorf("在年龄阶段 '%s' 中, 营养素名称 '%s' 重复", ageStageName, nutrientName) - } - seenNutrients[nutrientName] = true - - // 解析 min_requirement 和 max_requirement 对象 - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("期望营养素 '%s' 的值是一个JSON对象", nutrientName) - } - - for decoder.More() { - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("解析营养素 '%s' 的需求键失败: %w", nutrientName, err) - } - // key := t.(string) // 校验时不需要使用 key 的值 - - t, err = decoder.Token() - if err != nil { - return fmt.Errorf("解析营养素 '%s' 的需求值失败: %w", nutrientName, err) - } - if _, ok := t.(json.Number); !ok { - return fmt.Errorf("期望营养素 '%s' 的需求值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t) - } - } - if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return fmt.Errorf("解析营养素 '%s' 的值结束符 '}' 失败", nutrientName) - } - } - if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return fmt.Errorf("解析年龄阶段 '%s' 的值结束符 '}' 失败", ageStageName) - } - } - if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return fmt.Errorf("解析猪品种 '%s' 的值结束符 '}' 失败", breedName) - } - } - return nil -} - -// validateAndParseNutrientJSON 严格校验JSON文件 -func validateAndParseNutrientJSON(jsonData []byte) error { - descriptionsNode := gjson.GetBytes(jsonData, "descriptions") - if !descriptionsNode.Exists() { - return errors.New("JSON文件中缺少 'descriptions' 字段") - } - if !descriptionsNode.IsObject() { - return errors.New("'descriptions' 字段必须是一个JSON对象") - } - - rawMaterialsNode := descriptionsNode.Get("raw_materials") - if !rawMaterialsNode.Exists() { - return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段") - } - if !rawMaterialsNode.IsObject() { - return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象") - } - - // 使用 json.Decoder 严格校验 raw_materials 的结构 - decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw))) - decoder.UseNumber() - - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err) - } - - seenRawMaterials := make(map[string]bool) - - for decoder.More() { - // 1. 解析原料名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("解析原料名称失败: %w", err) - } - rawMaterialName := t.(string) - if seenRawMaterials[rawMaterialName] { - return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) - } - seenRawMaterials[rawMaterialName] = true - - // 2. 解析该原料的描述和价格对象 - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) - } - - for decoder.More() { - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err) - } - key := t.(string) - - switch key { - case "descriptions": - t, err = decoder.Token() - if err != nil { - return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err) - } - if _, ok := t.(string); !ok { - return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) - } - case "unit_price": - t, err = decoder.Token() - if err != nil { - return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err) - } - if _, ok := t.(json.Number); !ok { - return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) - } - default: - // 忽略其他未知字段,但仍需读取其值以继续解析 - if _, err := decoder.Token(); err != nil { - return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err) - } - } - } - - // 读取原料描述和价格对象的 "}" - if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) - } - } - - // 校验 data 节点 - dataNode := gjson.GetBytes(jsonData, "data") - if !dataNode.Exists() { - return errors.New("JSON文件中缺少 'data' 字段") - } - if !dataNode.IsObject() { - return errors.New("'data' 字段必须是一个JSON对象") - } - - // 重新初始化 decoder 用于 data 节点的校验 - decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) - decoder.UseNumber() - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return errors.New("'data' 字段解析起始符失败") - } - - seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验 - - for decoder.More() { - // 1. 解析原料名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("解析原料名称失败: %w", err) - } - rawMaterialName := t.(string) - if seenRawMaterials[rawMaterialName] { - return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) - } - seenRawMaterials[rawMaterialName] = true - - // 2. 解析该原料的营养成分对象 - if t, err := decoder.Token(); err != nil || t != json.Delim('{') { - return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) - } - - seenNutrients := make(map[string]bool) - for decoder.More() { - // 解析营养素名称 - t, err := decoder.Token() - if err != nil { - return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) - } - nutrientName := t.(string) - if seenNutrients[nutrientName] { - return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) - } - seenNutrients[nutrientName] = true - - // 解析营养素含量 - t, err = decoder.Token() - if err != nil { - return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) - } - if _, ok := t.(json.Number); !ok { - return fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t) - } - } - - // 读取营养成分对象的 "}" - if t, err := decoder.Token(); err != nil || t != json.Delim('}') { - return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) - } - } - return nil -} diff --git a/internal/infra/database/seeder/nutrient_seeder.go b/internal/infra/database/seeder/nutrient_seeder.go new file mode 100644 index 0000000..8eb9f3b --- /dev/null +++ b/internal/infra/database/seeder/nutrient_seeder.go @@ -0,0 +1,268 @@ +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 +} + +// SeedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 +func SeedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error { + logger := logs.GetLogger(ctx) + + // 检查 Nutrient 表是否为空,如果非空则跳过播种 + isEmpty, err := isTableEmpty(tx, &models.Nutrient{}) + if err != nil { + return fmt.Errorf("检查 Nutrient 表是否为空失败: %w", err) + } + if !isEmpty { + logger.Info("已存在原料数据, 跳过数据播种") + return nil + } + + // 1. 严格校验JSON文件,检查内部重复键 + if err := validateAndParseNutrientJSON(jsonData); err != nil { + return fmt.Errorf("JSON源文件校验失败: %w", err) + } + + // 2. 解析简介信息 + descriptionsNode := gjson.GetBytes(jsonData, "descriptions") + rawMaterialInfos := make(map[string]rawMaterialInfo) + nutrientDescriptions := make(map[string]string) + + if descriptionsNode.Exists() { + // 解析 raw_materials 描述和价格 + descriptionsNode.Get("raw_materials").ForEach(func(key, value gjson.Result) bool { + rawMaterialInfos[key.String()] = rawMaterialInfo{ + Description: value.Get("descriptions").String(), + UnitPrice: float32(value.Get("unit_price").Float()), + } + return true + }) + descriptionsNode.Get("nutrients").ForEach(func(key, value gjson.Result) bool { + nutrientDescriptions[key.String()] = value.String() + return true + }) + } + + // 3. 将通过校验的、干净的数据写入数据库 + dataNode := gjson.GetBytes(jsonData, "data") + dataNode.ForEach(func(rawMaterialKey, rawMaterialValue gjson.Result) bool { + rawMaterialName := rawMaterialKey.String() + var rawMaterial models.RawMaterial + + // 获取原料的描述和价格信息 + info := rawMaterialInfos[rawMaterialName] + + // 将 Description 和 ReferencePrice 放入 Create 对象中 + err = tx.Where(models.RawMaterial{Name: rawMaterialName}). + FirstOrCreate(&rawMaterial, models.RawMaterial{ + Name: rawMaterialName, + Description: info.Description, + ReferencePrice: info.UnitPrice, + }).Error + if err != nil { + // 返回 false 停止 ForEach 遍历 + return false + } + + rawMaterialValue.ForEach(func(nutrientKey, nutrientValue gjson.Result) bool { + nutrientName := nutrientKey.String() + value := float32(nutrientValue.Float()) + + var nutrient models.Nutrient + // 将 Description 放入 Create 对象中 + err = tx.Where(models.Nutrient{Name: nutrientName}). + FirstOrCreate(&nutrient, models.Nutrient{ + Name: nutrientName, + Description: nutrientDescriptions[nutrientName], + }).Error + if err != nil { + // 返回 false 停止 ForEach 遍历 + return false + } + + linkData := models.RawMaterialNutrient{ + RawMaterialID: rawMaterial.ID, + NutrientID: nutrient.ID, + } + // 使用 FirstOrCreate 确保关联的唯一性 + err = tx.Where(linkData).FirstOrCreate(&linkData, models.RawMaterialNutrient{ + RawMaterialID: linkData.RawMaterialID, + NutrientID: linkData.NutrientID, + Value: value, + }).Error + if err != nil { + // 返回 false 停止 ForEach 遍历 + return false + } + return true + }) + return err == nil // 如果内部遍历有错误,则停止外部遍历 + }) + + return err // 返回捕获到的错误 +} + +// validateAndParseNutrientJSON 严格校验JSON文件 +func validateAndParseNutrientJSON(jsonData []byte) error { + descriptionsNode := gjson.GetBytes(jsonData, "descriptions") + if !descriptionsNode.Exists() { + return errors.New("JSON文件中缺少 'descriptions' 字段") + } + if !descriptionsNode.IsObject() { + return errors.New("'descriptions' 字段必须是一个JSON对象") + } + + rawMaterialsNode := descriptionsNode.Get("raw_materials") + if !rawMaterialsNode.Exists() { + return errors.New("JSON文件中缺少 'descriptions.raw_materials' 字段") + } + if !rawMaterialsNode.IsObject() { + return errors.New("'descriptions.raw_materials' 字段必须是一个JSON对象") + } + + // 使用 json.Decoder 严格校验 raw_materials 的结构 + decoder := json.NewDecoder(bytes.NewReader([]byte(rawMaterialsNode.Raw))) + decoder.UseNumber() + + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return fmt.Errorf("'descriptions.raw_materials' 字段解析起始符失败: %v", err) + } + + seenRawMaterials := make(map[string]bool) + + for decoder.More() { + // 1. 解析原料名称 + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("解析原料名称失败: %w", err) + } + rawMaterialName := t.(string) + if seenRawMaterials[rawMaterialName] { + return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) + } + seenRawMaterials[rawMaterialName] = true + + // 2. 解析该原料的描述和价格对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) + } + + for decoder.More() { + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 内部键失败: %w", rawMaterialName, err) + } + key := t.(string) + + switch key { + case "descriptions": + t, err = decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 的 'descriptions' 值失败: %w", rawMaterialName, err) + } + if _, ok := t.(string); !ok { + return fmt.Errorf("期望原料 '%s' 的 'descriptions' 值是字符串, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) + } + case "unit_price": + t, err = decoder.Token() + if err != nil { + return fmt.Errorf("解析原料 '%s' 的 'unit_price' 值失败: %w", rawMaterialName, err) + } + if _, ok := t.(json.Number); !ok { + return fmt.Errorf("期望原料 '%s' 的 'unit_price' 值是数字, 但实际得到的类型是 %T, 值为 '%v'", rawMaterialName, t, t) + } + default: + // 忽略其他未知字段,但仍需读取其值以继续解析 + if _, err := decoder.Token(); err != nil { + return fmt.Errorf("解析原料 '%s' 的未知键 '%s' 的值失败: %w", rawMaterialName, key, err) + } + } + } + + // 读取原料描述和价格对象的 "}" + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) + } + } + + // 校验 data 节点 + dataNode := gjson.GetBytes(jsonData, "data") + if !dataNode.Exists() { + return errors.New("JSON文件中缺少 'data' 字段") + } + if !dataNode.IsObject() { + return errors.New("'data' 字段必须是一个JSON对象") + } + + // 重新初始化 decoder 用于 data 节点的校验 + decoder = json.NewDecoder(bytes.NewReader([]byte(dataNode.Raw))) + decoder.UseNumber() + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return errors.New("'data' 字段解析起始符失败") + } + + seenRawMaterials = make(map[string]bool) // 重置 seenRawMaterials 用于 data 节点校验 + + for decoder.More() { + // 1. 解析原料名称 + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("解析原料名称失败: %w", err) + } + rawMaterialName := t.(string) + if seenRawMaterials[rawMaterialName] { + return fmt.Errorf("原料名称 '%s' 重复", rawMaterialName) + } + seenRawMaterials[rawMaterialName] = true + + // 2. 解析该原料的营养成分对象 + if t, err := decoder.Token(); err != nil || t != json.Delim('{') { + return fmt.Errorf("期望原料 '%s' 的值是一个JSON对象", rawMaterialName) + } + + seenNutrients := make(map[string]bool) + for decoder.More() { + // 解析营养素名称 + t, err := decoder.Token() + if err != nil { + return fmt.Errorf("在原料 '%s' 中解析营养素名称失败: %w", rawMaterialName, err) + } + nutrientName := t.(string) + if seenNutrients[nutrientName] { + return fmt.Errorf("在原料 '%s' 中, 营养素名称 '%s' 重复", rawMaterialName, nutrientName) + } + seenNutrients[nutrientName] = true + + // 解析营养素含量 + t, err = decoder.Token() + if err != nil { + return fmt.Errorf("在原料 '%s' 中解析营养素 '%s' 的含量值失败: %w", rawMaterialName, nutrientName, err) + } + if _, ok := t.(json.Number); !ok { + return fmt.Errorf("期望营养素 '%s' 的含量值是数字, 但实际得到的类型是 %T, 值为 '%v'", nutrientName, t, t) + } + } + + // 读取营养成分对象的 "}" + if t, err := decoder.Token(); err != nil || t != json.Delim('}') { + return fmt.Errorf("解析原料 '%s' 的值结束符 '}' 失败", rawMaterialName) + } + } + return nil +} diff --git a/internal/infra/database/seeder/pig_nutrient_requirement_seeder.go b/internal/infra/database/seeder/pig_nutrient_requirement_seeder.go new file mode 100644 index 0000000..fbd361b --- /dev/null +++ b/internal/infra/database/seeder/pig_nutrient_requirement_seeder.go @@ -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 +} diff --git a/internal/infra/database/seeder/utils.go b/internal/infra/database/seeder/utils.go new file mode 100644 index 0000000..9281580 --- /dev/null +++ b/internal/infra/database/seeder/utils.go @@ -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 +} -- 2.49.1 From 5c99ff747583a0b927cd0a56a9d70ed1ffda9a97 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 26 Nov 2025 22:56:24 +0800 Subject: [PATCH 45/59] =?UTF-8?q?=E5=8E=9F=E6=96=99=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=9C=80=E5=A4=A7=E6=B7=BB=E5=8A=A0=E9=87=8F=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 243 ++++++++++++------ .../infra/database/seeder/nutrient_seeder.go | 25 +- 2 files changed, 180 insertions(+), 88 deletions(-) diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index ff53a59..fb53fbb 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -1741,327 +1741,408 @@ "raw_materials": { "DL-蛋氨酸98": { "descriptions": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", - "unit_price": 21.50 + "unit_price": 21.50, + "max_ratio": 0.008 }, "L-色氨酸98": { "descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", - "unit_price": 68.00 + "unit_price": 68.00, + "max_ratio": 0.003 }, "L-苏氨酸98": { "descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", - "unit_price": 10.80 + "unit_price": 10.80, + "max_ratio": 0.010 }, "L-赖氨酸HCl 98": { "descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", - "unit_price": 11.20 + "unit_price": 11.20, + "max_ratio": 0.020 }, "乳清粉": { "descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", - "unit_price": 6.50 + "unit_price": 6.50, + "max_ratio": 0.30 }, "兔肉粉": { "descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", - "unit_price": 11.50 + "unit_price": 11.50, + "max_ratio": 0.15 }, "全株玉米青贮": { "descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", - "unit_price": 0.45 + "unit_price": 0.45, + "max_ratio": 0.15 }, "双低菜籽粕": { "descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", - "unit_price": 2.40 + "unit_price": 2.40, + "max_ratio": 0.20 }, "向日葵籽": { "descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", - "unit_price": 5.80 + "unit_price": 5.80, + "max_ratio": 0.10 }, "啤酒糟干": { "descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", - "unit_price": 1.90 + "unit_price": 1.90, + "max_ratio": 0.20 }, "啤酒花渣": { "descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", - "unit_price": 0.60 + "unit_price": 0.60, + "max_ratio": 0.10 }, "国产鱼粉60": { "descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", - "unit_price": 9.50 + "unit_price": 9.50, + "max_ratio": 0.10 }, "土豆蛋白": { "descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", - "unit_price": 8.50 + "unit_price": 8.50, + "max_ratio": 0.10 }, "大豆油": { "descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", - "unit_price": 8.20 + "unit_price": 8.20, + "max_ratio": 0.08 }, "大豆粕44": { "descriptions": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", - "unit_price": 3.05 + "unit_price": 3.05, + "max_ratio": 0.50 }, "大豆粕46": { "descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", - "unit_price": 3.25 + "unit_price": 3.25, + "max_ratio": 0.50 }, "大豆粕48": { "descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", - "unit_price": 3.60 + "unit_price": 3.60, + "max_ratio": 0.50 }, "大麦": { "descriptions": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", - "unit_price": 2.10 + "unit_price": 2.10, + "max_ratio": 0.40 }, "小苏打": { "descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", - "unit_price": 1.60 + "unit_price": 1.60, + "max_ratio": 0.02 }, "小麦": { "descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", - "unit_price": 2.55 + "unit_price": 2.55, + "max_ratio": 0.40 }, "小麦次粉": { "descriptions": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", - "unit_price": 2.20 + "unit_price": 2.20, + "max_ratio": 0.15 }, "小麦麸": { "descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", - "unit_price": 1.75 + "unit_price": 1.75, + "max_ratio": 0.20 }, "木薯干": { "descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", - "unit_price": 2.05 + "unit_price": 2.05, + "max_ratio": 0.30 }, "杂交构树叶粉": { "descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", - "unit_price": 2.20 + "unit_price": 2.20, + "max_ratio": 0.05 }, "构树叶粉(老叶高纤维)": { "descriptions": "老叶构树粉,纤维更高,适合母猪粗饲料使用。", - "unit_price": 1.50 + "unit_price": 1.50, + "max_ratio": 0.10 }, "柠檬酸渣": { "descriptions": "湿态副产品,适口性好,可用于母猪料降低成本。", - "unit_price": 0.50 + "unit_price": 0.50, + "max_ratio": 0.05 }, "棉籽粕": { "descriptions": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。", - "unit_price": 2.80 + "unit_price": 2.80, + "max_ratio": 0.03 }, "棕榈油": { "descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", - "unit_price": 8.50 + "unit_price": 8.50, + "max_ratio": 0.08 }, "棕榈粕": { "descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。", - "unit_price": 1.60 + "unit_price": 1.60, + "max_ratio": 0.10 }, "椰子粕": { "descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。", - "unit_price": 2.30 + "unit_price": 2.30, + "max_ratio": 0.15 }, "燕麦": { "descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", - "unit_price": 3.20 + "unit_price": 3.20, + "max_ratio": 0.10 }, "燕麦草": { "descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。", - "unit_price": 2.60 + "unit_price": 2.60, + "max_ratio": 0.10 }, "猪肺粉": { "descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", - "unit_price": 9.00 + "unit_price": 9.00, + "max_ratio": 0.10 }, "玉米": { "descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", - "unit_price": 2.30 + "unit_price": 2.30, + "max_ratio": 0.85 }, "玉米DDGS": { "descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", - "unit_price": 2.15 + "unit_price": 2.15, + "max_ratio": 0.30 }, "玉米油": { "descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", - "unit_price": 9.50 + "unit_price": 9.50, + "max_ratio": 0.08 }, "玉米胚芽粕": { "descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", - "unit_price": 2.05 + "unit_price": 2.05, + "max_ratio": 0.15 }, "玉米蛋白粉60": { "descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", - "unit_price": 4.80 + "unit_price": 4.80, + "max_ratio": 0.10 }, "玉米青贮": { "descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。", - "unit_price": 0.40 + "unit_price": 0.40, + "max_ratio": 0.15 }, "瓜子粕": { "descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。", - "unit_price": 2.10 + "unit_price": 2.10, + "max_ratio": 0.15 }, "甜菜粕": { "descriptions": "高可溶性纤维,母猪极佳的防便秘原料。", - "unit_price": 1.95 + "unit_price": 1.95, + "max_ratio": 0.15 }, "石粉": { "descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。", - "unit_price": 0.18 + "unit_price": 0.18, + "max_ratio": 0.05 }, "碎米": { "descriptions": "能量接近玉米,蛋白稍低,适口性好。", - "unit_price": 2.80 + "unit_price": 2.80, + "max_ratio": 0.40 }, "磷酸氢钙": { "descriptions": "猪最常用磷钙来源,有效磷高。", - "unit_price": 3.20 + "unit_price": 3.20, + "max_ratio": 0.05 }, "稻草粉": { "descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。", - "unit_price": 0.60 + "unit_price": 0.60, + "max_ratio": 0.10 }, "稻谷": { "descriptions": "带壳稻子,能量低于玉米,纤维高。", - "unit_price": 1.90 + "unit_price": 1.90, + "max_ratio": 0.20 }, "稻谷糠": { "descriptions": "米糠的一种,高脂肪高磷,需注意酸败。", - "unit_price": 1.60 + "unit_price": 1.60, + "max_ratio": 0.10 }, "米糠": { "descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。", - "unit_price": 1.85 + "unit_price": 1.85, + "max_ratio": 0.15 }, "米糠粕": { "descriptions": "脱脂米糠,蛋白较高,能量降低。", - "unit_price": 1.95 + "unit_price": 1.95, + "max_ratio": 0.15 }, "红薯干": { "descriptions": "高淀粉低蛋白能量原料,类似木薯。", - "unit_price": 2.20 + "unit_price": 2.20, + "max_ratio": 0.30 }, "肉粉": { "descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。", - "unit_price": 4.50 + "unit_price": 4.50, + "max_ratio": 0.10 }, "肉骨粉50": { "descriptions": "含骨较高,钙磷比例好,但蛋白较低。", - "unit_price": 4.20 + "unit_price": 4.20, + "max_ratio": 0.10 }, "脱脂奶粉": { "descriptions": "优质乳蛋白源,仔猪料黄金原料。", - "unit_price": 22.00 + "unit_price": 22.00, + "max_ratio": 0.15 }, "膨化全脂大豆": { "descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", - "unit_price": 4.10 + "unit_price": 4.10, + "max_ratio": 0.20 }, "芝麻粕": { "descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", - "unit_price": 2.90 + "unit_price": 2.90, + "max_ratio": 0.10 }, "花生秧粉": { "descriptions": "粗饲料,母猪用。", - "unit_price": 0.85 + "unit_price": 0.85, + "max_ratio": 0.10 }, "花生粕": { "descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", - "unit_price": 3.70 + "unit_price": 3.70, + "max_ratio": 0.05 }, "苜蓿草块": { "descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", - "unit_price": 2.40 + "unit_price": 2.40, + "max_ratio": 0.10 }, "苜蓿草粉": { "descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。", - "unit_price": 2.50 + "unit_price": 2.50, + "max_ratio": 0.10 }, "苹果渣": { "descriptions": "湿态副产品,适口性好,母猪喜欢。", - "unit_price": 0.55 + "unit_price": 0.55, + "max_ratio": 0.10 }, "菜籽粕": { "descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", - "unit_price": 2.30 + "unit_price": 2.30, + "max_ratio": 0.08 }, "葡萄糖": { "descriptions": "快速能量源,教槽料常用,缓解应激。", - "unit_price": 3.80 + "unit_price": 3.80, + "max_ratio": 0.15 }, "葵花籽粕": { "descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", - "unit_price": 2.10 + "unit_price": 2.10, + "max_ratio": 0.15 }, "蔗糖": { "descriptions": "高能量碳水,教槽料诱食用。", - "unit_price": 6.50 + "unit_price": 6.50, + "max_ratio": 0.10 }, "虾粉": { "descriptions": "优质动物蛋白,含虾青素,改善体色。", - "unit_price": 6.00 + "unit_price": 6.00, + "max_ratio": 0.10 }, "蚕蛹粉": { "descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", - "unit_price": 8.00 + "unit_price": 8.00, + "max_ratio": 0.10 }, "蚕豆": { "descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。", - "unit_price": 3.40 + "unit_price": 3.40, + "max_ratio": 0.20 }, "蟹粉": { "descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。", - "unit_price": 4.50 + "unit_price": 4.50, + "max_ratio": 0.10 }, "血浆蛋白粉": { "descriptions": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。", - "unit_price": 45.00 + "unit_price": 45.00, + "max_ratio": 0.05 }, "血粉": { "descriptions": "赖氨酸极高,但适口性差,需喷涂使用。", - "unit_price": 6.50 + "unit_price": 6.50, + "max_ratio": 0.05 }, "豆磷脂": { "descriptions": "高能量乳化剂,促进脂肪消化,改善皮毛。", - "unit_price": 6.80 + "unit_price": 6.80, + "max_ratio": 0.01 }, "豌豆": { "descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", - "unit_price": 3.50 + "unit_price": 3.50, + "max_ratio": 0.30 }, "豌豆蛋白": { "descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", - "unit_price": 9.50 + "unit_price": 9.50, + "max_ratio": 0.15 }, "进口鱼粉65": { "descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", - "unit_price": 12.80 + "unit_price": 12.80, + "max_ratio": 0.15 }, "食盐": { "descriptions": "提供钠和氯,调节电解质平衡。", - "unit_price": 0.50 + "unit_price": 0.50, + "max_ratio": 0.01 }, "饲料酵母粉": { "descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。", - "unit_price": 6.50 + "unit_price": 6.50, + "max_ratio": 0.10 }, "高粱": { "descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", - "unit_price": 2.20 + "unit_price": 2.20, + "max_ratio": 0.30 }, "鱼油": { "descriptions": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", - "unit_price": 18.00 + "unit_price": 18.00, + "max_ratio": 0.03 }, "鸡肉粉": { "descriptions": "优质陆基动物蛋白,消化率高,适口性好。", - "unit_price": 7.50 + "unit_price": 7.50, + "max_ratio": 0.15 }, "鸭肉粉": { "descriptions": "与鸡肉粉类似,脂肪稍高。", - "unit_price": 7.20 + "unit_price": 7.20, + "max_ratio": 0.12 }, "鹅肉粉": { "descriptions": "蛋白和脂肪中等,质量稳定。", - "unit_price": 7.00 + "unit_price": 7.00, + "max_ratio": 0.12 } }, "nutrients": { diff --git a/internal/infra/database/seeder/nutrient_seeder.go b/internal/infra/database/seeder/nutrient_seeder.go index 8eb9f3b..db337be 100644 --- a/internal/infra/database/seeder/nutrient_seeder.go +++ b/internal/infra/database/seeder/nutrient_seeder.go @@ -14,10 +14,11 @@ import ( "gorm.io/gorm" ) -// rawMaterialInfo 用于临时存储解析后的原料描述和价格信息。 +// rawMaterialInfo 用于临时存储解析后的原料描述、价格和最大添加量信息。 type rawMaterialInfo struct { Description string UnitPrice float32 + MaxRatio float32 } // SeedNutrients 先严格校验JSON源文件,然后以“有则跳过”的模式播种数据。 @@ -45,11 +46,12 @@ func SeedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error { nutrientDescriptions := make(map[string]string) if descriptionsNode.Exists() { - // 解析 raw_materials 描述和价格 + // 解析 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 }) @@ -65,15 +67,16 @@ func SeedNutrients(ctx context.Context, tx *gorm.DB, jsonData []byte) error { rawMaterialName := rawMaterialKey.String() var rawMaterial models.RawMaterial - // 获取原料的描述和价格信息 + // 获取原料的描述、价格和最大添加量信息 info := rawMaterialInfos[rawMaterialName] - // 将 Description 和 ReferencePrice 放入 Create 对象中 + // 将 Description, ReferencePrice 和 MaxAdditionRatio 放入 Create 对象中 err = tx.Where(models.RawMaterial{Name: rawMaterialName}). FirstOrCreate(&rawMaterial, models.RawMaterial{ - Name: rawMaterialName, - Description: info.Description, - ReferencePrice: info.UnitPrice, + Name: rawMaterialName, + Description: info.Description, + ReferencePrice: info.UnitPrice, + MaxAdditionRatio: info.MaxRatio, }).Error if err != nil { // 返回 false 停止 ForEach 遍历 @@ -187,6 +190,14 @@ func validateAndParseNutrientJSON(jsonData []byte) error { 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 { -- 2.49.1 From dca6cc5dd44cac0bc558aae31abf4d830aeff864 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 00:39:01 +0800 Subject: [PATCH 46/59] =?UTF-8?q?=E5=8E=9F=E6=96=99=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=9C=80=E5=A4=A7=E6=B7=BB=E5=8A=A0=E9=87=8F=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/dto/feed_dto.go | 15 +++++++++------ internal/domain/recipe/raw_material_service.go | 18 +++++++++++------- .../repository/raw_material_repository.go | 7 ++++--- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index fd8b8a1..ef1a777 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -52,16 +52,18 @@ type ListNutrientResponse struct { // CreateRawMaterialRequest 创建原料的请求体 type CreateRawMaterialRequest struct { - Name string `json:"name" validate:"required,max=100"` // 原料名称 - Description string `json:"description" validate:"max=255"` // 描述 - ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元) + Name string `json:"name" validate:"required,max=100"` // 原料名称 + Description string `json:"description" validate:"max=255"` // 描述 + ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元) + MaxAdditionRatio float32 `json:"max_addition_ratio"` // 最大添加比例 } // UpdateRawMaterialRequest 更新原料的请求体 type UpdateRawMaterialRequest struct { - Name string `json:"name" validate:"required,max=100"` // 原料名称 - Description string `json:"description" validate:"max=255"` // 描述 - ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元) + Name string `json:"name" validate:"required,max=100"` // 原料名称 + Description string `json:"description" validate:"max=255"` // 描述 + ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元) + MaxAdditionRatio *float32 `json:"max_addition_ratio"` // 最大添加比例 } // RawMaterialNutrientDTO 原料营养素响应体 @@ -78,6 +80,7 @@ type RawMaterialResponse struct { Name string `json:"name"` Description string `json:"description"` ReferencePrice float32 `json:"reference_price"` // 参考价格(kg/元) + MaxAdditionRatio float32 `json:"max_addition_ratio"` // 最大添加比例 RawMaterialNutrients []RawMaterialNutrientDTO `json:"raw_material_nutrients"` // 关联的营养素信息 } diff --git a/internal/domain/recipe/raw_material_service.go b/internal/domain/recipe/raw_material_service.go index 0dd5916..a16a967 100644 --- a/internal/domain/recipe/raw_material_service.go +++ b/internal/domain/recipe/raw_material_service.go @@ -29,8 +29,8 @@ var ( // RawMaterialService 定义了原料领域的核心业务服务接口 type RawMaterialService interface { - CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error) - UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error) + CreateRawMaterial(ctx context.Context, name, description string, referencePrice, maxAdditionRatio float32) (*models.RawMaterial, error) + UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32, maxAdditionRatio *float32) (*models.RawMaterial, error) DeleteRawMaterial(ctx context.Context, id uint32) error GetRawMaterial(ctx context.Context, id uint32) (*models.RawMaterial, error) ListRawMaterials(ctx context.Context, opts repository.RawMaterialListOptions, page, pageSize int) ([]models.RawMaterial, int64, error) @@ -56,7 +56,7 @@ func NewRawMaterialService(ctx context.Context, uow repository.UnitOfWork, rawMa } // CreateRawMaterial 实现了创建原料的核心业务逻辑 -func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice float32) (*models.RawMaterial, error) { +func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, description string, referencePrice, maxAdditionRatio float32) (*models.RawMaterial, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial") // 检查名称是否已存在 @@ -69,9 +69,10 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de } rawMaterial := &models.RawMaterial{ - Name: name, - Description: description, - ReferencePrice: referencePrice, + Name: name, + Description: description, + ReferencePrice: referencePrice, + MaxAdditionRatio: maxAdditionRatio, } if err := s.rawMaterialRepo.CreateRawMaterial(serviceCtx, rawMaterial); err != nil { @@ -82,7 +83,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, name, de } // UpdateRawMaterial 实现了更新原料的核心业务逻辑 -func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32) (*models.RawMaterial, error) { +func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, name, description string, referencePrice float32, maxAdditionRatio *float32) (*models.RawMaterial, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial") // 检查要更新的实体是否存在 @@ -108,6 +109,9 @@ func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint3 rawMaterial.Name = name rawMaterial.Description = description rawMaterial.ReferencePrice = referencePrice + if maxAdditionRatio != nil { + rawMaterial.MaxAdditionRatio = *maxAdditionRatio + } if err := s.rawMaterialRepo.UpdateRawMaterial(serviceCtx, rawMaterial); err != nil { return nil, fmt.Errorf("更新原料失败: %w", err) diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 8490fd6..3e63cbc 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -144,9 +144,10 @@ func (r *gormRawMaterialRepository) UpdateRawMaterial(ctx context.Context, rawMa repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateRawMaterial") // 使用 map 更新以避免 GORM 的零值问题,并确保只更新指定字段 updateData := map[string]interface{}{ - "name": rawMaterial.Name, - "description": rawMaterial.Description, - "reference_price": rawMaterial.ReferencePrice, + "name": rawMaterial.Name, + "description": rawMaterial.Description, + "reference_price": rawMaterial.ReferencePrice, + "max_addition_ratio": rawMaterial.MaxAdditionRatio, } result := r.db.WithContext(repoCtx).Model(&models.RawMaterial{}).Where("id = ?", rawMaterial.ID).Updates(updateData) if result.Error != nil { -- 2.49.1 From e2da441a6d6aa09a7aae1777684382ba037ebd54 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 15:52:38 +0800 Subject: [PATCH 47/59] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 128 ++-- .../pig_nutrient_requirement.json | 578 +++++++++--------- internal/app/service/raw_material_service.go | 4 +- .../domain/recipe/recipe_generate_manager.go | 7 +- 4 files changed, 361 insertions(+), 356 deletions(-) diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index fb53fbb..3ceffd1 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -1742,52 +1742,52 @@ "DL-蛋氨酸98": { "descriptions": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", "unit_price": 21.50, - "max_ratio": 0.008 + "max_ratio": 1.00 }, "L-色氨酸98": { "descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", "unit_price": 68.00, - "max_ratio": 0.003 + "max_ratio": 1.00 }, "L-苏氨酸98": { "descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", "unit_price": 10.80, - "max_ratio": 0.010 + "max_ratio": 1.00 }, "L-赖氨酸HCl 98": { "descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", "unit_price": 11.20, - "max_ratio": 0.020 + "max_ratio": 1.00 }, "乳清粉": { "descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", "unit_price": 6.50, - "max_ratio": 0.30 + "max_ratio": 0.40 }, "兔肉粉": { "descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", "unit_price": 11.50, - "max_ratio": 0.15 + "max_ratio": 0.20 }, "全株玉米青贮": { "descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", "unit_price": 0.45, - "max_ratio": 0.15 + "max_ratio": 0.30 }, "双低菜籽粕": { "descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", "unit_price": 2.40, - "max_ratio": 0.20 + "max_ratio": 0.35 }, "向日葵籽": { "descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", "unit_price": 5.80, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "啤酒糟干": { "descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", "unit_price": 1.90, - "max_ratio": 0.20 + "max_ratio": 0.30 }, "啤酒花渣": { "descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", @@ -1797,37 +1797,37 @@ "国产鱼粉60": { "descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", "unit_price": 9.50, - "max_ratio": 0.10 + "max_ratio": 0.15 }, "土豆蛋白": { "descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", "unit_price": 8.50, - "max_ratio": 0.10 + "max_ratio": 1.00 }, "大豆油": { "descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", "unit_price": 8.20, - "max_ratio": 0.08 + "max_ratio": 0.10 }, "大豆粕44": { "descriptions": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", "unit_price": 3.05, - "max_ratio": 0.50 + "max_ratio": 0.65 }, "大豆粕46": { "descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", "unit_price": 3.25, - "max_ratio": 0.50 + "max_ratio": 0.65 }, "大豆粕48": { "descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", "unit_price": 3.60, - "max_ratio": 0.50 + "max_ratio": 0.65 }, "大麦": { "descriptions": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", "unit_price": 2.10, - "max_ratio": 0.40 + "max_ratio": 0.60 }, "小苏打": { "descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", @@ -1837,22 +1837,22 @@ "小麦": { "descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", "unit_price": 2.55, - "max_ratio": 0.40 + "max_ratio": 0.60 }, "小麦次粉": { "descriptions": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", "unit_price": 2.20, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "小麦麸": { "descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", "unit_price": 1.75, - "max_ratio": 0.20 + "max_ratio": 0.40 }, "木薯干": { "descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", "unit_price": 2.05, - "max_ratio": 0.30 + "max_ratio": 0.50 }, "杂交构树叶粉": { "descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", @@ -1877,147 +1877,147 @@ "棕榈油": { "descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", "unit_price": 8.50, - "max_ratio": 0.08 + "max_ratio": 0.10 }, "棕榈粕": { "descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。", "unit_price": 1.60, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "椰子粕": { "descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。", "unit_price": 2.30, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "燕麦": { "descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", "unit_price": 3.20, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "燕麦草": { "descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。", "unit_price": 2.60, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "猪肺粉": { "descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", "unit_price": 9.00, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "玉米": { "descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", "unit_price": 2.30, - "max_ratio": 0.85 + "max_ratio": 1.00 }, "玉米DDGS": { "descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", "unit_price": 2.15, - "max_ratio": 0.30 + "max_ratio": 0.40 }, "玉米油": { "descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", "unit_price": 9.50, - "max_ratio": 0.08 + "max_ratio": 0.10 }, "玉米胚芽粕": { "descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", "unit_price": 2.05, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "玉米蛋白粉60": { "descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", "unit_price": 4.80, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "玉米青贮": { "descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。", "unit_price": 0.40, - "max_ratio": 0.15 + "max_ratio": 0.30 }, "瓜子粕": { "descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。", "unit_price": 2.10, - "max_ratio": 0.15 + "max_ratio": 0.35 }, "甜菜粕": { "descriptions": "高可溶性纤维,母猪极佳的防便秘原料。", "unit_price": 1.95, - "max_ratio": 0.15 + "max_ratio": 0.30 }, "石粉": { "descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。", "unit_price": 0.18, - "max_ratio": 0.05 + "max_ratio": 0.08 }, "碎米": { "descriptions": "能量接近玉米,蛋白稍低,适口性好。", "unit_price": 2.80, - "max_ratio": 0.40 + "max_ratio": 0.60 }, "磷酸氢钙": { "descriptions": "猪最常用磷钙来源,有效磷高。", "unit_price": 3.20, - "max_ratio": 0.05 + "max_ratio": 0.08 }, "稻草粉": { "descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。", "unit_price": 0.60, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "稻谷": { "descriptions": "带壳稻子,能量低于玉米,纤维高。", "unit_price": 1.90, - "max_ratio": 0.20 + "max_ratio": 0.30 }, "稻谷糠": { "descriptions": "米糠的一种,高脂肪高磷,需注意酸败。", "unit_price": 1.60, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "米糠": { "descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。", "unit_price": 1.85, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "米糠粕": { "descriptions": "脱脂米糠,蛋白较高,能量降低。", "unit_price": 1.95, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "红薯干": { "descriptions": "高淀粉低蛋白能量原料,类似木薯。", "unit_price": 2.20, - "max_ratio": 0.30 + "max_ratio": 0.50 }, "肉粉": { "descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。", "unit_price": 4.50, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "肉骨粉50": { "descriptions": "含骨较高,钙磷比例好,但蛋白较低。", "unit_price": 4.20, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "脱脂奶粉": { "descriptions": "优质乳蛋白源,仔猪料黄金原料。", "unit_price": 22.00, - "max_ratio": 0.15 + "max_ratio": 0.30 }, "膨化全脂大豆": { "descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", "unit_price": 4.10, - "max_ratio": 0.20 + "max_ratio": 0.30 }, "芝麻粕": { "descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", "unit_price": 2.90, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "花生秧粉": { "descriptions": "粗饲料,母猪用。", "unit_price": 0.85, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "花生粕": { "descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", @@ -2027,12 +2027,12 @@ "苜蓿草块": { "descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", "unit_price": 2.40, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "苜蓿草粉": { "descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。", "unit_price": 2.50, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "苹果渣": { "descriptions": "湿态副产品,适口性好,母猪喜欢。", @@ -2042,7 +2042,7 @@ "菜籽粕": { "descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", "unit_price": 2.30, - "max_ratio": 0.08 + "max_ratio": 0.15 }, "葡萄糖": { "descriptions": "快速能量源,教槽料常用,缓解应激。", @@ -2052,7 +2052,7 @@ "葵花籽粕": { "descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", "unit_price": 2.10, - "max_ratio": 0.15 + "max_ratio": 0.35 }, "蔗糖": { "descriptions": "高能量碳水,教槽料诱食用。", @@ -2067,12 +2067,12 @@ "蚕蛹粉": { "descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", "unit_price": 8.00, - "max_ratio": 0.10 + "max_ratio": 0.15 }, "蚕豆": { "descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。", "unit_price": 3.40, - "max_ratio": 0.20 + "max_ratio": 0.50 }, "蟹粉": { "descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。", @@ -2097,17 +2097,17 @@ "豌豆": { "descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", "unit_price": 3.50, - "max_ratio": 0.30 + "max_ratio": 0.50 }, "豌豆蛋白": { "descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", "unit_price": 9.50, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "进口鱼粉65": { "descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", "unit_price": 12.80, - "max_ratio": 0.15 + "max_ratio": 0.30 }, "食盐": { "descriptions": "提供钠和氯,调节电解质平衡。", @@ -2117,12 +2117,12 @@ "饲料酵母粉": { "descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。", "unit_price": 6.50, - "max_ratio": 0.10 + "max_ratio": 0.20 }, "高粱": { "descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", "unit_price": 2.20, - "max_ratio": 0.30 + "max_ratio": 0.50 }, "鱼油": { "descriptions": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", @@ -2132,17 +2132,17 @@ "鸡肉粉": { "descriptions": "优质陆基动物蛋白,消化率高,适口性好。", "unit_price": 7.50, - "max_ratio": 0.15 + "max_ratio": 0.25 }, "鸭肉粉": { "descriptions": "与鸡肉粉类似,脂肪稍高。", "unit_price": 7.20, - "max_ratio": 0.12 + "max_ratio": 0.20 }, "鹅肉粉": { "descriptions": "蛋白和脂肪中等,质量稳定。", "unit_price": 7.00, - "max_ratio": 0.12 + "max_ratio": 0.20 } }, "nutrients": { diff --git a/config/presets-data/pig_nutrient_requirement.json b/config/presets-data/pig_nutrient_requirement.json index bf3adaf..8f6d72f 100644 --- a/config/presets-data/pig_nutrient_requirement.json +++ b/config/presets-data/pig_nutrient_requirement.json @@ -4,56 +4,56 @@ "杜长大 (DLY)": { "保育期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.012, - "max_requirement": 0.015 + "min_requirement": 1.2, + "max_requirement": 1.5 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0072, - "max_requirement": 0.0105 + "min_requirement": 0.72, + "max_requirement": 1.05 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0078, - "max_requirement": 0.0108 + "min_requirement": 0.78, + "max_requirement": 1.08 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0022, - "max_requirement": 0.0030 + "min_requirement": 0.22, + "max_requirement": 0.30 }, "粗蛋白 (%)": { - "min_requirement": 0.18, - "max_requirement": 0.22 + "min_requirement": 18.0, + "max_requirement": 22.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.009, - "max_requirement": 0.012 + "min_requirement": 0.9, + "max_requirement": 1.2 }, "总磷 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0045 + "min_requirement": 0.2, + "max_requirement": 0.45 }, "代谢能 (kcal/kg)": { "min_requirement": 3226.5, "max_requirement": 3585.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -67,56 +67,56 @@ }, "育肥前期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0094, - "max_requirement": 0.0110 + "min_requirement": 0.94, + "max_requirement": 1.10 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0055, - "max_requirement": 0.0073 + "min_requirement": 0.55, + "max_requirement": 0.73 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0058, - "max_requirement": 0.0077 + "min_requirement": 0.58, + "max_requirement": 0.77 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0016, - "max_requirement": 0.0022 + "min_requirement": 0.16, + "max_requirement": 0.22 }, "粗蛋白 (%)": { - "min_requirement": 0.16, - "max_requirement": 0.18 + "min_requirement": 16.0, + "max_requirement": 18.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.007, - "max_requirement": 0.009 + "min_requirement": 0.7, + "max_requirement": 0.9 }, "总磷 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0040 + "min_requirement": 0.2, + "max_requirement": 0.40 }, "代谢能 (kcal/kg)": { "min_requirement": 3107.0, "max_requirement": 3346.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -130,56 +130,56 @@ }, "育肥后期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0081, - "max_requirement": 0.0090 + "min_requirement": 0.81, + "max_requirement": 0.90 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.0058 + "min_requirement": 0.45, + "max_requirement": 0.58 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0048, - "max_requirement": 0.0061 + "min_requirement": 0.48, + "max_requirement": 0.61 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0013, - "max_requirement": 0.0018 + "min_requirement": 0.13, + "max_requirement": 0.18 }, "粗蛋白 (%)": { - "min_requirement": 0.14, - "max_requirement": 0.16 + "min_requirement": 14.0, + "max_requirement": 16.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "总磷 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.006 + "min_requirement": 0.45, + "max_requirement": 0.6 }, "有效磷 (%)": { - "min_requirement": 0.0018, - "max_requirement": 0.0035 + "min_requirement": 0.18, + "max_requirement": 0.35 }, "代谢能 (kcal/kg)": { "min_requirement": 2987.5, "max_requirement": 3226.5 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -193,56 +193,56 @@ }, "二次育肥期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0053, - "max_requirement": 0.0065 + "min_requirement": 0.53, + "max_requirement": 0.65 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0030, - "max_requirement": 0.0041 + "min_requirement": 0.30, + "max_requirement": 0.41 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0031, - "max_requirement": 0.0043 + "min_requirement": 0.31, + "max_requirement": 0.43 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0010, - "max_requirement": 0.0013 + "min_requirement": 0.10, + "max_requirement": 0.13 }, "粗蛋白 (%)": { - "min_requirement": 0.12, - "max_requirement": 0.14 + "min_requirement": 12.0, + "max_requirement": 14.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "总磷 (%)": { - "min_requirement": 0.004, - "max_requirement": 0.0055 + "min_requirement": 0.4, + "max_requirement": 0.55 }, "有效磷 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0030 + "min_requirement": 0.15, + "max_requirement": 0.30 }, "代谢能 (kcal/kg)": { "min_requirement": 2868.0, "max_requirement": 3107.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -258,56 +258,56 @@ "杜大长 (DYL)": { "保育期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.012, - "max_requirement": 0.015 + "min_requirement": 1.2, + "max_requirement": 1.5 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0072, - "max_requirement": 0.0105 + "min_requirement": 0.72, + "max_requirement": 1.05 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0078, - "max_requirement": 0.0108 + "min_requirement": 0.78, + "max_requirement": 1.08 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0022, - "max_requirement": 0.0030 + "min_requirement": 0.22, + "max_requirement": 0.30 }, "粗蛋白 (%)": { - "min_requirement": 0.18, - "max_requirement": 0.22 + "min_requirement": 18.0, + "max_requirement": 22.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.009, - "max_requirement": 0.012 + "min_requirement": 0.9, + "max_requirement": 1.2 }, "总磷 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0045 + "min_requirement": 0.2, + "max_requirement": 0.45 }, "代谢能 (kcal/kg)": { "min_requirement": 3226.5, "max_requirement": 3585.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -321,56 +321,56 @@ }, "育肥前期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0094, - "max_requirement": 0.0110 + "min_requirement": 0.94, + "max_requirement": 1.10 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0055, - "max_requirement": 0.0073 + "min_requirement": 0.55, + "max_requirement": 0.73 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0058, - "max_requirement": 0.0077 + "min_requirement": 0.58, + "max_requirement": 0.77 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0016, - "max_requirement": 0.0022 + "min_requirement": 0.16, + "max_requirement": 0.22 }, "粗蛋白 (%)": { - "min_requirement": 0.16, - "max_requirement": 0.18 + "min_requirement": 16.0, + "max_requirement": 18.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.007, - "max_requirement": 0.009 + "min_requirement": 0.7, + "max_requirement": 0.9 }, "总磷 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0040 + "min_requirement": 0.2, + "max_requirement": 0.40 }, "代谢能 (kcal/kg)": { "min_requirement": 3107.0, "max_requirement": 3346.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -384,56 +384,56 @@ }, "育肥后期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0081, - "max_requirement": 0.0090 + "min_requirement": 0.81, + "max_requirement": 0.90 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.0058 + "min_requirement": 0.45, + "max_requirement": 0.58 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0048, - "max_requirement": 0.0061 + "min_requirement": 0.48, + "max_requirement": 0.61 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0013, - "max_requirement": 0.0018 + "min_requirement": 0.13, + "max_requirement": 0.18 }, "粗蛋白 (%)": { - "min_requirement": 0.14, - "max_requirement": 0.16 + "min_requirement": 14.0, + "max_requirement": 16.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "总磷 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.006 + "min_requirement": 0.45, + "max_requirement": 0.6 }, "有效磷 (%)": { - "min_requirement": 0.0018, - "max_requirement": 0.0035 + "min_requirement": 0.18, + "max_requirement": 0.35 }, "代谢能 (kcal/kg)": { "min_requirement": 2987.5, "max_requirement": 3226.5 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -447,56 +447,56 @@ }, "二次育肥期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0053, - "max_requirement": 0.0065 + "min_requirement": 0.53, + "max_requirement": 0.65 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0030, - "max_requirement": 0.0041 + "min_requirement": 0.30, + "max_requirement": 0.41 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0031, - "max_requirement": 0.0043 + "min_requirement": 0.31, + "max_requirement": 0.43 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0010, - "max_requirement": 0.0013 + "min_requirement": 0.10, + "max_requirement": 0.13 }, "粗蛋白 (%)": { - "min_requirement": 0.12, - "max_requirement": 0.14 + "min_requirement": 12.0, + "max_requirement": 14.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "总磷 (%)": { - "min_requirement": 0.004, - "max_requirement": 0.0055 + "min_requirement": 0.4, + "max_requirement": 0.55 }, "有效磷 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0030 + "min_requirement": 0.15, + "max_requirement": 0.30 }, "代谢能 (kcal/kg)": { "min_requirement": 2868.0, "max_requirement": 3107.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -512,56 +512,56 @@ "皮长大 (PLY)": { "保育期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.012, - "max_requirement": 0.015 + "min_requirement": 1.2, + "max_requirement": 1.5 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0072, - "max_requirement": 0.0105 + "min_requirement": 0.72, + "max_requirement": 1.05 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0078, - "max_requirement": 0.0108 + "min_requirement": 0.78, + "max_requirement": 1.08 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0022, - "max_requirement": 0.0030 + "min_requirement": 0.22, + "max_requirement": 0.30 }, "粗蛋白 (%)": { - "min_requirement": 0.18, - "max_requirement": 0.22 + "min_requirement": 18.0, + "max_requirement": 22.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.009, - "max_requirement": 0.012 + "min_requirement": 0.9, + "max_requirement": 1.2 }, "总磷 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0045 + "min_requirement": 0.2, + "max_requirement": 0.45 }, "代谢能 (kcal/kg)": { "min_requirement": 3226.5, "max_requirement": 3585.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -575,56 +575,56 @@ }, "育肥前期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0094, - "max_requirement": 0.0110 + "min_requirement": 0.94, + "max_requirement": 1.10 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0055, - "max_requirement": 0.0073 + "min_requirement": 0.55, + "max_requirement": 0.73 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0058, - "max_requirement": 0.0077 + "min_requirement": 0.58, + "max_requirement": 0.77 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0016, - "max_requirement": 0.0022 + "min_requirement": 0.16, + "max_requirement": 0.22 }, "粗蛋白 (%)": { - "min_requirement": 0.16, - "max_requirement": 0.18 + "min_requirement": 16.0, + "max_requirement": 18.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.007, - "max_requirement": 0.009 + "min_requirement": 0.7, + "max_requirement": 0.9 }, "总磷 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "有效磷 (%)": { - "min_requirement": 0.002, - "max_requirement": 0.0040 + "min_requirement": 0.2, + "max_requirement": 0.40 }, "代谢能 (kcal/kg)": { "min_requirement": 3107.0, "max_requirement": 3346.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -638,56 +638,56 @@ }, "育肥后期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0081, - "max_requirement": 0.0090 + "min_requirement": 0.81, + "max_requirement": 0.90 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.0058 + "min_requirement": 0.45, + "max_requirement": 0.58 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0048, - "max_requirement": 0.0061 + "min_requirement": 0.48, + "max_requirement": 0.61 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0013, - "max_requirement": 0.0018 + "min_requirement": 0.13, + "max_requirement": 0.18 }, "粗蛋白 (%)": { - "min_requirement": 0.14, - "max_requirement": 0.16 + "min_requirement": 14.0, + "max_requirement": 16.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.006, - "max_requirement": 0.008 + "min_requirement": 0.6, + "max_requirement": 0.8 }, "总磷 (%)": { - "min_requirement": 0.0045, - "max_requirement": 0.006 + "min_requirement": 0.45, + "max_requirement": 0.6 }, "有效磷 (%)": { - "min_requirement": 0.0018, - "max_requirement": 0.0035 + "min_requirement": 0.18, + "max_requirement": 0.35 }, "代谢能 (kcal/kg)": { "min_requirement": 2987.5, "max_requirement": 3226.5 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -701,56 +701,56 @@ }, "二次育肥期": { "可消化赖氨酸 (SID %)": { - "min_requirement": 0.0053, - "max_requirement": 0.0065 + "min_requirement": 0.53, + "max_requirement": 0.65 }, "蛋+胱氨酸 (%)": { - "min_requirement": 0.0030, - "max_requirement": 0.0041 + "min_requirement": 0.30, + "max_requirement": 0.41 }, "可消化苏氨酸 (SID %)": { - "min_requirement": 0.0031, - "max_requirement": 0.0043 + "min_requirement": 0.31, + "max_requirement": 0.43 }, "可消化色氨酸 (SID %)": { - "min_requirement": 0.0010, - "max_requirement": 0.0013 + "min_requirement": 0.10, + "max_requirement": 0.13 }, "粗蛋白 (%)": { - "min_requirement": 0.12, - "max_requirement": 0.14 + "min_requirement": 12.0, + "max_requirement": 14.0 }, "粗脂肪 (%)": { - "min_requirement": 0.03, - "max_requirement": 0.06 + "min_requirement": 3.0, + "max_requirement": 6.0 }, "粗纤维 (%)": { - "min_requirement": 0.02, - "max_requirement": 0.06 + "min_requirement": 2.0, + "max_requirement": 6.0 }, "钙 (%)": { - "min_requirement": 0.005, - "max_requirement": 0.007 + "min_requirement": 0.5, + "max_requirement": 0.7 }, "总磷 (%)": { - "min_requirement": 0.004, - "max_requirement": 0.0055 + "min_requirement": 0.4, + "max_requirement": 0.55 }, "有效磷 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0030 + "min_requirement": 0.15, + "max_requirement": 0.30 }, "代谢能 (kcal/kg)": { "min_requirement": 2868.0, "max_requirement": 3107.0 }, "钠 (%)": { - "min_requirement": 0.0015, - "max_requirement": 0.0025 + "min_requirement": 0.15, + "max_requirement": 0.25 }, "氯 (%)": { - "min_requirement": 0.0025, - "max_requirement": 0.0045 + "min_requirement": 0.25, + "max_requirement": 0.45 }, "黄曲霉毒素B1 (μg/kg)": { "max_requirement": 10 @@ -911,4 +911,4 @@ } } } -} +} \ No newline at end of file diff --git a/internal/app/service/raw_material_service.go b/internal/app/service/raw_material_service.go index 6622abe..3b57cd5 100644 --- a/internal/app/service/raw_material_service.go +++ b/internal/app/service/raw_material_service.go @@ -46,7 +46,7 @@ func NewRawMaterialService(ctx context.Context, recipeSvc recipe.Service) RawMat func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto.CreateRawMaterialRequest) (*dto.RawMaterialResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "CreateRawMaterial") - rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description, req.ReferencePrice) + rawMaterial, err := s.recipeSvc.CreateRawMaterial(serviceCtx, req.Name, req.Description, req.ReferencePrice, req.MaxAdditionRatio) if err != nil { if errors.Is(err, recipe.ErrRawMaterialNameConflict) { return nil, ErrRawMaterialNameConflict @@ -61,7 +61,7 @@ func (s *rawMaterialServiceImpl) CreateRawMaterial(ctx context.Context, req *dto func (s *rawMaterialServiceImpl) UpdateRawMaterial(ctx context.Context, id uint32, req *dto.UpdateRawMaterialRequest) (*dto.RawMaterialResponse, error) { serviceCtx := logs.AddFuncName(ctx, s.ctx, "UpdateRawMaterial") - rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description, req.ReferencePrice) + rawMaterial, err := s.recipeSvc.UpdateRawMaterial(serviceCtx, id, req.Name, req.Description, req.ReferencePrice, req.MaxAdditionRatio) if err != nil { if errors.Is(err, recipe.ErrRawMaterialNotFound) { return nil, ErrRawMaterialNotFound diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 3efa18c..676bb02 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -325,9 +325,14 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)。 // 允许略微超过100%的浮点误差,但不能显著超过。 if totalPercentage > 1.0+1e-3 { - return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage) + return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.2f),请检查算法或数据配置", totalPercentage) } // 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。 + // 此时需要在描述中说明需要添加的廉价填充料的百分比。 + if totalPercentage < 1.0-1e-4 { // 允许微小的浮点误差 + fillerPercentage := (1.0 - totalPercentage) * 100.0 + recipe.Description = fmt.Sprintf("%s。注意:配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage*100.0, fillerPercentage) + } return recipe, nil } -- 2.49.1 From b8e0301175e8003c940d2114bdadce048aa8f836 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 16:27:49 +0800 Subject: [PATCH 48/59] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/presets-data/nutrient.json | 162 +++++++++--------- .../domain/recipe/recipe_generate_manager.go | 4 +- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/config/presets-data/nutrient.json b/config/presets-data/nutrient.json index 3ceffd1..a60fb3a 100644 --- a/config/presets-data/nutrient.json +++ b/config/presets-data/nutrient.json @@ -1742,407 +1742,407 @@ "DL-蛋氨酸98": { "descriptions": "饲料级合成蛋氨酸,几乎100%可利用,是猪限制性氨基酸补充的首选来源,可显著提高生长速度和饲料转化率。", "unit_price": 21.50, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "L-色氨酸98": { "descriptions": "饲料级合成色氨酸,猪的第四限制性氨基酸,缺乏时严重影响采食量和生长,补充可提升猪只食欲和免疫力。", "unit_price": 68.00, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "L-苏氨酸98": { "descriptions": "饲料级合成苏氨酸,猪的第三限制性氨基酸,主要影响蛋白沉积和免疫器官发育,仔猪阶段尤为重要。", "unit_price": 10.80, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "L-赖氨酸HCl 98": { "descriptions": "饲料级赖氨酸盐酸盐,猪的第一限制性氨基酸,低蛋白日粮配方核心,降低氮排放的同时维持生长性能。", "unit_price": 11.20, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "乳清粉": { "descriptions": "仔猪最优质的乳源蛋白和乳糖来源,提高采食量、促进肠道发育、缓解断奶应激,是教槽料和保育料黄金原料。", "unit_price": 6.50, - "max_ratio": 0.40 + "max_ratio": 40.00 }, "兔肉粉": { "descriptions": "高蛋白高消化率动物蛋白源,氨基酸平衡好,适口性佳,适合高档仔猪料和母猪料使用。", "unit_price": 11.50, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "全株玉米青贮": { "descriptions": "粗饲料来源,提供有效纤维,调节成年母猪肠道健康,降低便秘,价格低廉。", "unit_price": 0.45, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "双低菜籽粕": { "descriptions": "双低菜粕,硫甙和异硫氰酸酯含量低,可部分替代豆粕使用,但仍需注意赖氨酸利用率和甲状腺影响。", "unit_price": 2.40, - "max_ratio": 0.35 + "max_ratio": 35.00 }, "向日葵籽": { "descriptions": "高油分能量原料,富含亚油酸,但纤维高,猪的利用率一般,多用于母猪料。", "unit_price": 5.80, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "啤酒糟干": { "descriptions": "高蛋白高纤维副产品,适口性好,可用于生长肥育猪和母猪料,注意霉菌毒素风险。", "unit_price": 1.90, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "啤酒花渣": { "descriptions": "啤酒副产物,湿态使用时适口性好,可降低母猪便秘,但干物质低、易发霉。", "unit_price": 0.60, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "国产鱼粉60": { "descriptions": "中等品质鱼粉,蛋白高但新鲜度一般,挥发性盐基氮和组胺需关注,仔猪料谨慎使用。", "unit_price": 9.50, - "max_ratio": 0.15 + "max_ratio": 15.00 }, "土豆蛋白": { "descriptions": "高消化率植物浓缩蛋白,氨基酸平衡好,是优质替代血浆和鱼粉的原料之一。", "unit_price": 8.50, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "大豆油": { "descriptions": "高能量油脂,猪利用率极高,用于提高日粮能量浓度,改善皮毛光亮度。", "unit_price": 8.20, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "大豆粕44": { "descriptions": "普通豆粕,蛋白43.8%左右,抗营养因子较高,需关注脲酶和KOH溶解度。", "unit_price": 3.05, - "max_ratio": 0.65 + "max_ratio": 65.00 }, "大豆粕46": { "descriptions": "优质豆粕,蛋白更高,抗营养因子更低,是猪料最常用蛋白原料。", "unit_price": 3.25, - "max_ratio": 0.65 + "max_ratio": 65.00 }, "大豆粕48": { "descriptions": "高蛋白豆粕,抗营养因子最低,低蛋白日粮配方的理想蛋白源。", "unit_price": 3.60, - "max_ratio": 0.65 + "max_ratio": 65.00 }, "大麦": { "descriptions": "能量稍低于玉米,纤维较高,可部分替代玉米,注意DON毒素风险。", "unit_price": 2.10, - "max_ratio": 0.60 + "max_ratio": 60.00 }, "小苏打": { "descriptions": "缓冲剂和钠源,缓解热应激、改善母猪泌乳期酸中毒。", "unit_price": 1.60, - "max_ratio": 0.02 + "max_ratio": 2.00 }, "小麦": { "descriptions": "能量与玉米接近,但黏性大,易导致肠道问题,仔猪料慎用。", "unit_price": 2.55, - "max_ratio": 0.60 + "max_ratio": 60.00 }, "小麦次粉": { "descriptions": "小麦加工副产品,蛋白和磷较高,但DON和ZEN风险高,限量使用。", "unit_price": 2.20, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "小麦麸": { "descriptions": "高纤维原料,用于母猪料促进肠道蠕动,降低便秘。", "unit_price": 1.75, - "max_ratio": 0.40 + "max_ratio": 40.00 }, "木薯干": { "descriptions": "高能量淀粉源,几乎不含蛋白,价格低廉,但需搭配优质蛋白。", "unit_price": 2.05, - "max_ratio": 0.50 + "max_ratio": 50.00 }, "杂交构树叶粉": { "descriptions": "新型蛋白饲料资源,蛋白中等,富含黄酮,但单宁和草酸高,需限量并配合脱毒处理。", "unit_price": 2.20, - "max_ratio": 0.05 + "max_ratio": 5.00 }, "构树叶粉(老叶高纤维)": { "descriptions": "老叶构树粉,纤维更高,适合母猪粗饲料使用。", "unit_price": 1.50, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "柠檬酸渣": { "descriptions": "湿态副产品,适口性好,可用于母猪料降低成本。", "unit_price": 0.50, - "max_ratio": 0.05 + "max_ratio": 5.00 }, "棉籽粕": { "descriptions": "蛋白较高,但游离棉酚严重影响公猪生育力和生长,需严格限量或脱毒。", "unit_price": 2.80, - "max_ratio": 0.03 + "max_ratio": 3.00 }, "棕榈油": { "descriptions": "饱和脂肪酸高,能量高,但熔点高,冬季易凝固,仔猪利用率稍差。", "unit_price": 8.50, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "棕榈粕": { "descriptions": "高纤维高脂肪副产品,能量一般,多用于母猪料。", "unit_price": 1.60, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "椰子粕": { "descriptions": "蛋白和能量中等,适口性好,可部分替代豆粕。", "unit_price": 2.30, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "燕麦": { "descriptions": "能量和脂肪较高,适口性佳,但价格贵,一般少用。", "unit_price": 3.20, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "燕麦草": { "descriptions": "粗饲料,母猪用以增加饱腹感和肠道健康。", "unit_price": 2.60, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "猪肺粉": { "descriptions": "优质动物蛋白,消化率高,适口性极佳,适合高档仔猪料。", "unit_price": 9.00, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "玉米": { "descriptions": "猪最主要的能量原料,淀粉消化率高,毒素风险需关注。", "unit_price": 2.30, - "max_ratio": 1.00 + "max_ratio": 100.00 }, "玉米DDGS": { "descriptions": "高蛋白高脂肪玉米副产品,磷利用率高,适合生长肥育猪和母猪。", "unit_price": 2.15, - "max_ratio": 0.40 + "max_ratio": 40.00 }, "玉米油": { "descriptions": "优质植物油,富含不饱和脂肪酸,能量最高油脂之一。", "unit_price": 9.50, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "玉米胚芽粕": { "descriptions": "蛋白和脂肪较高,磷利用率好,可部分替代豆粕和油。", "unit_price": 2.05, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "玉米蛋白粉60": { "descriptions": "高蛋白高蛋氨酸,色素来源,用于改善猪皮红毛亮。", "unit_price": 4.80, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "玉米青贮": { "descriptions": "粗饲料,母猪用以调节肠道,降低饲料成本。", "unit_price": 0.40, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "瓜子粕": { "descriptions": "葵花籽粕的别称,蛋白较高,纤维也高。", "unit_price": 2.10, - "max_ratio": 0.35 + "max_ratio": 35.00 }, "甜菜粕": { "descriptions": "高可溶性纤维,母猪极佳的防便秘原料。", "unit_price": 1.95, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "石粉": { "descriptions": "最常用的钙源,价格低廉,注意粒度影响吸收率。", "unit_price": 0.18, - "max_ratio": 0.08 + "max_ratio": 8.00 }, "碎米": { "descriptions": "能量接近玉米,蛋白稍低,适口性好。", "unit_price": 2.80, - "max_ratio": 0.60 + "max_ratio": 60.00 }, "磷酸氢钙": { "descriptions": "猪最常用磷钙来源,有效磷高。", "unit_price": 3.20, - "max_ratio": 0.08 + "max_ratio": 8.00 }, "稻草粉": { "descriptions": "最廉价粗纤维来源,母猪限量使用防便秘。", "unit_price": 0.60, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "稻谷": { "descriptions": "带壳稻子,能量低于玉米,纤维高。", "unit_price": 1.90, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "稻谷糠": { "descriptions": "米糠的一种,高脂肪高磷,需注意酸败。", "unit_price": 1.60, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "米糠": { "descriptions": "高能量高磷副产品,注意黄曲霉毒素和酸败。", "unit_price": 1.85, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "米糠粕": { "descriptions": "脱脂米糠,蛋白较高,能量降低。", "unit_price": 1.95, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "红薯干": { "descriptions": "高淀粉低蛋白能量原料,类似木薯。", "unit_price": 2.20, - "max_ratio": 0.50 + "max_ratio": 50.00 }, "肉粉": { "descriptions": "普通肉粉,蛋白和灰分波动大,质量不稳定。", "unit_price": 4.50, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "肉骨粉50": { "descriptions": "含骨较高,钙磷比例好,但蛋白较低。", "unit_price": 4.20, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "脱脂奶粉": { "descriptions": "优质乳蛋白源,仔猪料黄金原料。", "unit_price": 22.00, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "膨化全脂大豆": { "descriptions": "经过高温膨化的全脂大豆,抗营养因子破坏彻底,仔猪可用。", "unit_price": 4.10, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "芝麻粕": { "descriptions": "蛋白高,蛋氨酸丰富,但草酸高,需限量。", "unit_price": 2.90, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "花生秧粉": { "descriptions": "粗饲料,母猪用。", "unit_price": 0.85, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "花生粕": { "descriptions": "蛋白高,但黄曲霉毒素风险极高,猪场慎用。", "unit_price": 3.70, - "max_ratio": 0.05 + "max_ratio": 5.00 }, "苜蓿草块": { "descriptions": "优质粗饲料,富含维生素和未知生长因子,母猪和仔猪都适用。", "unit_price": 2.40, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "苜蓿草粉": { "descriptions": "蛋白较高,但皂苷和香豆素可能影响采食。", "unit_price": 2.50, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "苹果渣": { "descriptions": "湿态副产品,适口性好,母猪喜欢。", "unit_price": 0.55, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "菜籽粕": { "descriptions": "普通菜粕,硫甙高,对甲状腺影响大,猪限量使用。", "unit_price": 2.30, - "max_ratio": 0.15 + "max_ratio": 15.00 }, "葡萄糖": { "descriptions": "快速能量源,教槽料常用,缓解应激。", "unit_price": 3.80, - "max_ratio": 0.15 + "max_ratio": 15.00 }, "葵花籽粕": { "descriptions": "高纤维蛋白源,赖氨酸低,需补充赖氨酸。", "unit_price": 2.10, - "max_ratio": 0.35 + "max_ratio": 35.00 }, "蔗糖": { "descriptions": "高能量碳水,教槽料诱食用。", "unit_price": 6.50, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "虾粉": { "descriptions": "优质动物蛋白,含虾青素,改善体色。", "unit_price": 6.00, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "蚕蛹粉": { "descriptions": "高蛋白高脂肪,氨基酸平衡好,但脂肪易氧化。", "unit_price": 8.00, - "max_ratio": 0.15 + "max_ratio": 15.00 }, "蚕豆": { "descriptions": "蛋白较高,淀粉消化率好,但含抗营养因子。", "unit_price": 3.40, - "max_ratio": 0.50 + "max_ratio": 50.00 }, "蟹粉": { "descriptions": "高蛋白高灰分动物蛋白,钙磷丰富。", "unit_price": 4.50, - "max_ratio": 0.10 + "max_ratio": 10.00 }, "血浆蛋白粉": { "descriptions": "仔猪断奶料黄金功能性蛋白,IgG高,促进肠道发育和免疫。", "unit_price": 45.00, - "max_ratio": 0.05 + "max_ratio": 5.00 }, "血粉": { "descriptions": "赖氨酸极高,但适口性差,需喷涂使用。", "unit_price": 6.50, - "max_ratio": 0.05 + "max_ratio": 5.00 }, "豆磷脂": { "descriptions": "高能量乳化剂,促进脂肪消化,改善皮毛。", "unit_price": 6.80, - "max_ratio": 0.01 + "max_ratio": 1.00 }, "豌豆": { "descriptions": "蛋白中等,淀粉消化好,可部分替代玉米和豆粕。", "unit_price": 3.50, - "max_ratio": 0.50 + "max_ratio": 50.00 }, "豌豆蛋白": { "descriptions": "豌豆浓缩蛋白,蛋白高,抗营养因子低。", "unit_price": 9.50, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "进口鱼粉65": { "descriptions": "高品质鱼粉,新鲜度好,仔猪和母猪料优质蛋白源。", "unit_price": 12.80, - "max_ratio": 0.30 + "max_ratio": 30.00 }, "食盐": { "descriptions": "提供钠和氯,调节电解质平衡。", "unit_price": 0.50, - "max_ratio": 0.01 + "max_ratio": 1.00 }, "饲料酵母粉": { "descriptions": "富含核苷酸和小肽,促进肠道健康和免疫。", "unit_price": 6.50, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "高粱": { "descriptions": "能量接近玉米,但单宁高影响消化率,需选低单宁品种。", "unit_price": 2.20, - "max_ratio": 0.50 + "max_ratio": 50.00 }, "鱼油": { "descriptions": "富含DHA和EPA,促进脑发育和抗炎,母猪和仔猪推荐。", "unit_price": 18.00, - "max_ratio": 0.03 + "max_ratio": 3.00 }, "鸡肉粉": { "descriptions": "优质陆基动物蛋白,消化率高,适口性好。", "unit_price": 7.50, - "max_ratio": 0.25 + "max_ratio": 25.00 }, "鸭肉粉": { "descriptions": "与鸡肉粉类似,脂肪稍高。", "unit_price": 7.20, - "max_ratio": 0.20 + "max_ratio": 20.00 }, "鹅肉粉": { "descriptions": "蛋白和脂肪中等,质量稳定。", "unit_price": 7.00, - "max_ratio": 0.20 + "max_ratio": 20.00 } }, "nutrients": { diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 676bb02..6296cea 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -180,7 +180,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType } maxAdditionConstraints = append(maxAdditionConstraints, maxAdditionConstraintInfo{ materialColIndex: materialColIndex, - limit: float64(mat.MaxAdditionRatio), + limit: float64(mat.MaxAdditionRatio) / 100.0, }) } } @@ -318,7 +318,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{ RawMaterialID: materialIDs[i], // 比例: float64 -> float32 - Percentage: float32(proportion), + Percentage: float32(proportion * 100.0), }) } -- 2.49.1 From e6b307b0dcb9f2273cc660f5b38fd959f96c9f5f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 16:42:32 +0800 Subject: [PATCH 49/59] =?UTF-8?q?=E4=BF=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/dto/feed_converter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/app/dto/feed_converter.go b/internal/app/dto/feed_converter.go index afe931f..7bb3983 100644 --- a/internal/app/dto/feed_converter.go +++ b/internal/app/dto/feed_converter.go @@ -66,6 +66,7 @@ func ConvertRawMaterialToDTO(rm *models.RawMaterial) *RawMaterialResponse { Name: rm.Name, Description: rm.Description, ReferencePrice: rm.ReferencePrice, + MaxAdditionRatio: rm.MaxAdditionRatio, RawMaterialNutrients: rawMaterialNutrientDTOs, } } -- 2.49.1 From 3b12802900f1b39385a13cbf21642956159d1bdf Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 17:33:28 +0800 Subject: [PATCH 50/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8C=89=E5=8E=9F?= =?UTF-8?q?=E6=96=99=E6=98=AF=E5=90=A6=E6=9C=89=E5=BA=93=E5=AD=98=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 34 ++++++++++++++----- docs/swagger.json | 34 ++++++++++++++----- docs/swagger.yaml | 21 +++++++++--- internal/app/dto/inventory_dto.go | 1 + internal/app/service/inventory_service.go | 5 +-- .../repository/raw_material_repository.go | 17 ++++++++++ project_structure.txt | 3 ++ 7 files changed, 93 insertions(+), 22 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 9155de7..d08f2c4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3392,6 +3392,12 @@ const docTemplate = `{ ], "summary": "获取当前库存列表", "parameters": [ + { + "type": "boolean", + "description": "只查询有库存的原料", + "name": "has_stock", + "in": "query" + }, { "type": "string", "description": "排序字段, 例如 \"stock DESC\"", @@ -3715,6 +3721,7 @@ const docTemplate = `{ }, { "enum": [ + 7, -1, 0, 1, @@ -3724,12 +3731,12 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3739,8 +3746,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7267,6 +7273,10 @@ const docTemplate = `{ "type": "string", "maxLength": 255 }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "description": "原料名称", "type": "string", @@ -8824,6 +8834,10 @@ const docTemplate = `{ "id": { "type": "integer" }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "type": "string" }, @@ -9864,6 +9878,10 @@ const docTemplate = `{ "type": "string", "maxLength": 255 }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "description": "原料名称", "type": "string", @@ -10586,6 +10604,7 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -10595,10 +10614,10 @@ const docTemplate = `{ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10608,8 +10627,7 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index dd534bf..7687226 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3384,6 +3384,12 @@ ], "summary": "获取当前库存列表", "parameters": [ + { + "type": "boolean", + "description": "只查询有库存的原料", + "name": "has_stock", + "in": "query" + }, { "type": "string", "description": "排序字段, 例如 \"stock DESC\"", @@ -3707,6 +3713,7 @@ }, { "enum": [ + 7, -1, 0, 1, @@ -3716,12 +3723,12 @@ 5, -1, 5, - 6, - 7 + 6 ], "type": "integer", "format": "int32", "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3731,8 +3738,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ], "name": "level", "in": "query" @@ -7259,6 +7265,10 @@ "type": "string", "maxLength": 255 }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "description": "原料名称", "type": "string", @@ -8816,6 +8826,10 @@ "id": { "type": "integer" }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "type": "string" }, @@ -9856,6 +9870,10 @@ "type": "string", "maxLength": 255 }, + "max_addition_ratio": { + "description": "最大添加比例", + "type": "number" + }, "name": { "description": "原料名称", "type": "string", @@ -10578,6 +10596,7 @@ "type": "integer", "format": "int32", "enum": [ + 7, -1, 0, 1, @@ -10587,10 +10606,10 @@ 5, -1, 5, - 6, - 7 + 6 ], "x-enum-varnames": [ + "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10600,8 +10619,7 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel", - "_numLevels" + "InvalidLevel" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4f31241..50b7304 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -416,6 +416,9 @@ definitions: description: 描述 maxLength: 255 type: string + max_addition_ratio: + description: 最大添加比例 + type: number name: description: 原料名称 maxLength: 100 @@ -1446,6 +1449,9 @@ definitions: type: string id: type: integer + max_addition_ratio: + description: 最大添加比例 + type: number name: type: string raw_material_nutrients: @@ -2160,6 +2166,9 @@ definitions: description: 描述 maxLength: 255 type: string + max_addition_ratio: + description: 最大添加比例 + type: number name: description: 原料名称 maxLength: 100 @@ -2731,6 +2740,7 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: + - 7 - -1 - 0 - 1 @@ -2741,10 +2751,10 @@ definitions: - -1 - 5 - 6 - - 7 format: int32 type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2755,7 +2765,6 @@ definitions: - _minLevel - _maxLevel - InvalidLevel - - _numLevels info: contact: email: divano@example.com @@ -4793,6 +4802,10 @@ paths: get: description: 获取所有原料的当前库存列表,支持分页和过滤。 parameters: + - description: 只查询有库存的原料 + in: query + name: has_stock + type: boolean - description: 排序字段, 例如 "stock DESC" in: query name: order_by @@ -4985,6 +4998,7 @@ paths: name: end_time type: string - enum: + - 7 - -1 - 0 - 1 @@ -4995,12 +5009,12 @@ paths: - -1 - 5 - 6 - - 7 format: int32 in: query name: level type: integer x-enum-varnames: + - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -5011,7 +5025,6 @@ paths: - _minLevel - _maxLevel - InvalidLevel - - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/dto/inventory_dto.go b/internal/app/dto/inventory_dto.go index 27ec39d..a695a91 100644 --- a/internal/app/dto/inventory_dto.go +++ b/internal/app/dto/inventory_dto.go @@ -31,6 +31,7 @@ type ListCurrentStockRequest struct { PageSize int `json:"page_size" query:"page_size"` // 每页数量 RawMaterialName *string `json:"raw_material_name" query:"raw_material_name"` // 按原料名称模糊查询 OrderBy string `json:"order_by" query:"order_by"` // 排序字段, 例如 "stock DESC" + HasStock *bool `json:"has_stock" query:"has_stock"` // 只查询有库存的原料 } // ListCurrentStockResponse 是获取当前库存列表的响应结构 diff --git a/internal/app/service/inventory_service.go b/internal/app/service/inventory_service.go index 69cf231..93226c2 100644 --- a/internal/app/service/inventory_service.go +++ b/internal/app/service/inventory_service.go @@ -75,8 +75,9 @@ func (s *inventoryServiceImpl) ListCurrentStock(ctx context.Context, req *dto.Li // 1. 获取分页的原料列表 rawMatOpts := repository.RawMaterialListOptions{ - Name: req.RawMaterialName, - OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序 + Name: req.RawMaterialName, + OrderBy: req.OrderBy, // 注意:这里的排序可能需要调整,比如按原料名排序 + HasStock: req.HasStock, } rawMaterials, total, err := s.rawMatRepo.ListRawMaterials(serviceCtx, rawMatOpts, req.Page, req.PageSize) if err != nil { diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 3e63cbc..78d6d71 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -18,6 +18,7 @@ type RawMaterialListOptions struct { NutrientName *string MinReferencePrice *float32 // 参考价格最小值 MaxReferencePrice *float32 // 参考价格最大值 + HasStock *bool // 是否只查询有库存的原料 OrderBy string } @@ -122,6 +123,22 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R db = db.Where("reference_price <= ?", *opts.MaxReferencePrice) } + // 筛选有库存的原料 + if opts.HasStock != nil && *opts.HasStock { + // 内部子查询:生成带有 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 且 after_quantity > 0 的 raw_material_id + // GORM 会将 rankedLogsQuery 作为一个子查询嵌入到 FROM 子句中 + latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery). + Select("raw_material_id"). + Where("rn = 1 AND after_quantity > 0") + + // 将这个子查询直接应用到主查询的 WHERE id IN (?) 条件中 + db = db.Where("id IN (?)", latestStockLogSubQuery) + } + // 首先计算总数 if err := db.Count(&total).Error; err != nil { return nil, 0, err diff --git a/project_structure.txt b/project_structure.txt index 5ef3c45..0dd433a 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -145,6 +145,9 @@ internal/domain/task/task.go internal/infra/config/config.go internal/infra/database/postgres.go internal/infra/database/seeder.go +internal/infra/database/seeder/nutrient_seeder.go +internal/infra/database/seeder/pig_nutrient_requirement_seeder.go +internal/infra/database/seeder/utils.go internal/infra/database/storage.go internal/infra/logs/context.go internal/infra/logs/encoder.go -- 2.49.1 From 33cdf7278ec1f90c88dde1d34aac1870681a2d24 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 18:32:22 +0800 Subject: [PATCH 51/59] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=93=8D=E4=BD=9C=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 23 ++++++++++++++++++++++- docs/swagger.json | 23 ++++++++++++++++++++++- docs/swagger.yaml | 12 ++++++++++++ internal/app/dto/inventory_converter.go | 11 +++++++---- internal/app/dto/inventory_dto.go | 17 ++++++++++------- internal/app/service/inventory_service.go | 2 +- 6 files changed, 74 insertions(+), 14 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index d08f2c4..97adb7f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -7346,6 +7346,14 @@ const docTemplate = `{ "dto.CurrentStockResponse": { "type": "object", "properties": { + "last_operation_source_type": { + "description": "上次库存变动的来源类型", + "allOf": [ + { + "$ref": "#/definitions/models.StockLogSourceType" + } + ] + }, "last_updated": { "description": "最后更新时间", "type": "string" @@ -9211,7 +9219,8 @@ const docTemplate = `{ "type": "object", "required": [ "change_amount", - "raw_material_id" + "raw_material_id", + "source_type" ], "properties": { "change_amount": { @@ -9226,6 +9235,18 @@ const docTemplate = `{ "description": "备注", "type": "string", "maxLength": 255 + }, + "source_id": { + "description": "来源ID, 例如: 配方ID, 采购单ID等", + "type": "integer" + }, + "source_type": { + "description": "库存变动来源类型", + "allOf": [ + { + "$ref": "#/definitions/models.StockLogSourceType" + } + ] } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 7687226..450e877 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -7338,6 +7338,14 @@ "dto.CurrentStockResponse": { "type": "object", "properties": { + "last_operation_source_type": { + "description": "上次库存变动的来源类型", + "allOf": [ + { + "$ref": "#/definitions/models.StockLogSourceType" + } + ] + }, "last_updated": { "description": "最后更新时间", "type": "string" @@ -9203,7 +9211,8 @@ "type": "object", "required": [ "change_amount", - "raw_material_id" + "raw_material_id", + "source_type" ], "properties": { "change_amount": { @@ -9218,6 +9227,18 @@ "description": "备注", "type": "string", "maxLength": 255 + }, + "source_id": { + "description": "来源ID, 例如: 配方ID, 采购单ID等", + "type": "integer" + }, + "source_type": { + "description": "库存变动来源类型", + "allOf": [ + { + "$ref": "#/definitions/models.StockLogSourceType" + } + ] } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 50b7304..08f9ea2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -470,6 +470,10 @@ definitions: type: object dto.CurrentStockResponse: properties: + last_operation_source_type: + allOf: + - $ref: '#/definitions/models.StockLogSourceType' + description: 上次库存变动的来源类型 last_updated: description: 最后更新时间 type: string @@ -1723,9 +1727,17 @@ definitions: description: 备注 maxLength: 255 type: string + source_id: + description: '来源ID, 例如: 配方ID, 采购单ID等' + type: integer + source_type: + allOf: + - $ref: '#/definitions/models.StockLogSourceType' + description: 库存变动来源类型 required: - change_amount - raw_material_id + - source_type type: object dto.StockLogResponse: properties: diff --git a/internal/app/dto/inventory_converter.go b/internal/app/dto/inventory_converter.go index 91ad693..02d4256 100644 --- a/internal/app/dto/inventory_converter.go +++ b/internal/app/dto/inventory_converter.go @@ -14,17 +14,20 @@ func ConvertCurrentStockToDTO(material *models.RawMaterial, latestLog *models.Ra stock := float32(0) lastUpdated := material.CreatedAt.Format(time.RFC3339) // 默认使用创建时间 + var lastOperationSourceType models.StockLogSourceType if latestLog != nil { stock = latestLog.AfterQuantity lastUpdated = latestLog.HappenedAt.Format(time.RFC3339) + lastOperationSourceType = latestLog.SourceType } return &CurrentStockResponse{ - RawMaterialID: material.ID, - RawMaterialName: material.Name, - Stock: stock, - LastUpdated: lastUpdated, + RawMaterialID: material.ID, + RawMaterialName: material.Name, + Stock: stock, + LastUpdated: lastUpdated, + LastOperationSourceType: lastOperationSourceType, } } diff --git a/internal/app/dto/inventory_dto.go b/internal/app/dto/inventory_dto.go index a695a91..5844099 100644 --- a/internal/app/dto/inventory_dto.go +++ b/internal/app/dto/inventory_dto.go @@ -12,17 +12,20 @@ import ( // StockAdjustmentRequest 手动调整库存的请求体 type StockAdjustmentRequest struct { - RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 要调整的原料ID - ChangeAmount float32 `json:"change_amount" validate:"required,ne=0"` // 变动数量, 正数为入库, 负数为出库, 单位: g - Remarks string `json:"remarks" validate:"max=255"` // 备注 + RawMaterialID uint32 `json:"raw_material_id" validate:"required"` // 要调整的原料ID + ChangeAmount float32 `json:"change_amount" validate:"required,ne=0"` // 变动数量, 正数为入库, 负数为出库, 单位: g + SourceType models.StockLogSourceType `json:"source_type" validate:"required"` // 库存变动来源类型 + SourceID *uint32 `json:"source_id,omitempty"` // 来源ID, 例如: 配方ID, 采购单ID等 + Remarks string `json:"remarks" validate:"max=255"` // 备注 } // CurrentStockResponse 单个原料及其当前库存的响应体 type CurrentStockResponse struct { - RawMaterialID uint32 `json:"raw_material_id"` // 原料ID - RawMaterialName string `json:"raw_material_name"` // 原料名称 - Stock float32 `json:"stock"` // 当前库存量, 单位: g - LastUpdated string `json:"last_updated"` // 最后更新时间 + RawMaterialID uint32 `json:"raw_material_id"` // 原料ID + RawMaterialName string `json:"raw_material_name"` // 原料名称 + Stock float32 `json:"stock"` // 当前库存量, 单位: g + LastUpdated string `json:"last_updated"` // 最后更新时间 + LastOperationSourceType models.StockLogSourceType `json:"last_operation_source_type"` // 上次库存变动的来源类型 } // ListCurrentStockRequest 定义了获取当前库存列表的请求参数 diff --git a/internal/app/service/inventory_service.go b/internal/app/service/inventory_service.go index 93226c2..0acef7c 100644 --- a/internal/app/service/inventory_service.go +++ b/internal/app/service/inventory_service.go @@ -47,7 +47,7 @@ func (s *inventoryServiceImpl) AdjustStock(ctx context.Context, req *dto.StockAd serviceCtx := logs.AddFuncName(ctx, s.ctx, "AdjustStock") // 调用领域服务执行核心业务逻辑 - log, err := s.invSvc.AdjustStock(serviceCtx, req.RawMaterialID, req.ChangeAmount, models.StockLogSourceManual, nil, req.Remarks) + log, err := s.invSvc.AdjustStock(serviceCtx, req.RawMaterialID, req.ChangeAmount, req.SourceType, req.SourceID, req.Remarks) if err != nil { if errors.Is(err, inventory.ErrRawMaterialNotFound) { return nil, ErrInventoryRawMaterialNotFound -- 2.49.1 From da8e1d01911b14bf40041dc455f7d01611aa5ef9 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 20:03:14 +0800 Subject: [PATCH 52/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/recipe_generate_manager.go | 12 ++--- internal/domain/recipe/recipe_service.go | 50 ++++++++++++++++--- internal/infra/models/recipe.go | 22 ++++++++ 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/internal/domain/recipe/recipe_generate_manager.go b/internal/domain/recipe/recipe_generate_manager.go index 6296cea..cb833f8 100644 --- a/internal/domain/recipe/recipe_generate_manager.go +++ b/internal/domain/recipe/recipe_generate_manager.go @@ -266,7 +266,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType // --------------------------------------------------------- // lp.Simplex 求解: minimize c^T * x subject to A * x = b, x >= 0 - optVal, x, err := lp.Simplex(c, A, b, 1e-8, nil) + _, x, err := lp.Simplex(c, A, b, 1e-8, nil) if err != nil { if errors.Is(err, lp.ErrInfeasible) { @@ -291,8 +291,8 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType } recipe := &models.Recipe{ - Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), - Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", actualMaterialCount, optVal), + Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), // 提供一个默认的名称 + Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。", actualMaterialCount), // 提供一个默认的描述 RecipeIngredients: []models.RecipeIngredient{}, } @@ -327,12 +327,6 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType if totalPercentage > 1.0+1e-3 { return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.2f),请检查算法或数据配置", totalPercentage) } - // 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。 - // 此时需要在描述中说明需要添加的廉价填充料的百分比。 - if totalPercentage < 1.0-1e-4 { // 允许微小的浮点误差 - fillerPercentage := (1.0 - totalPercentage) * 100.0 - recipe.Description = fmt.Sprintf("%s。注意:配方中实际原料占比 %.2f%%,需额外补充 %.2f%% 廉价填充料", recipe.Description, totalPercentage*100.0, fillerPercentage) - } return recipe, nil } diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index d8db69b..f1a39b3 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -4,6 +4,7 @@ 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" ) @@ -60,8 +61,10 @@ func NewRecipeService( // 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(ctx, pigTypeID) + pigType, err := r.GetPigTypeByID(serviceCtx, pigTypeID) if err != nil { return nil, fmt.Errorf("获取猪类型信息失败: %w", err) } @@ -69,23 +72,54 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex // 2. 获取所有原料 // 我们通过传递一个非常大的 pageSize 来获取所有原料,这在大多数情况下是可行的。 // 对于超大规模系统,可能需要考虑分页迭代,但目前这是一个简单有效的策略。 - materials, _, err := r.ListRawMaterials(ctx, repository.RawMaterialListOptions{}, 1, 9999) + materials, _, err := r.ListRawMaterials(serviceCtx, repository.RawMaterialListOptions{}, 1, 9999) if err != nil { return nil, fmt.Errorf("获取所有原料列表失败: %w", err) } // 3. 调用生成器生成配方 - recipe, err := r.GenerateRecipe(ctx, *pigType, materials) + recipe, err := r.GenerateRecipe(serviceCtx, *pigType, materials) if err != nil { return nil, fmt.Errorf("生成配方失败: %w", err) } - // 4. 保存新生成的配方到数据库 - // CreateRecipe 会处理配方及其成分的保存 - if recipe, err = r.CreateRecipe(ctx, recipe); err != nil { - return nil, fmt.Errorf("保存生成的配方失败: %w", err) + // 4. 丰富配方描述:计算并添加参考价格信息 + + // 填充 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) + } } - // 5. 返回创建的配方 (现在它应该已经有了ID) + 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", recipe) + + // 6. 返回创建的配方 (现在它应该已经有了ID) return recipe, nil } diff --git a/internal/infra/models/recipe.go b/internal/infra/models/recipe.go index a5278b0..4602af1 100644 --- a/internal/infra/models/recipe.go +++ b/internal/infra/models/recipe.go @@ -13,6 +13,28 @@ func (Recipe) TableName() string { return "recipes" } +// CalculateTotalRawMaterialProportion 计算配方中所有原料的总比例 +func (r Recipe) CalculateTotalRawMaterialProportion() float32 { + var totalPercentage float32 + for _, ingredient := range r.RecipeIngredients { + totalPercentage += ingredient.Percentage + } + return totalPercentage +} + +// CalculateReferencePricePerKilogram 根据原料参考价计算配方每公斤的成本 +func (r Recipe) CalculateReferencePricePerKilogram() float32 { + var totalCost float32 + for _, ingredient := range r.RecipeIngredients { + // 确保 RawMaterial 已经被加载 + if ingredient.RawMaterial.ID == 0 { + return 0.0 + } + totalCost += ingredient.RawMaterial.ReferencePrice * ingredient.Percentage + } + return totalCost +} + // RecipeIngredient 配方原料组成模型 type RecipeIngredient struct { Model -- 2.49.1 From 1b5f715dec43b0694d13d419e69c6c67e3224ae1 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 21:06:15 +0800 Subject: [PATCH 53/59] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BC=98=E5=85=88?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=BA=93=E5=AD=98=E7=9A=84=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/recipe-management/index.md | 3 +- docs/docs.go | 62 ++++++++++-- docs/swagger.json | 62 ++++++++++-- docs/swagger.yaml | 34 ++++++- internal/app/api/router.go | 1 + .../app/controller/feed/recipe_controller.go | 31 ++++++ internal/app/service/recipe_service.go | 9 ++ internal/domain/recipe/recipe_service.go | 98 +++++++++++++++++++ 8 files changed, 279 insertions(+), 21 deletions(-) diff --git a/design/recipe-management/index.md b/design/recipe-management/index.md index 165ec05..7e4caa8 100644 --- a/design/recipe-management/index.md +++ b/design/recipe-management/index.md @@ -64,4 +64,5 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 14. 配方增删改查服务层和控制器 15. 实现库存管理相关逻辑 16. 实现配方生成器 -17. 实现使用系统中所有可用的原料一键生成配方 \ No newline at end of file +17. 实现使用系统中所有可用的原料一键生成配方 +18. 实现优先使用库存的配方一键生成 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 97adb7f..0e42018 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3191,6 +3191,52 @@ const docTemplate = `{ } } }, + "/api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用优先有库存原料的策略生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3721,7 +3767,6 @@ const docTemplate = `{ }, { "enum": [ - 7, -1, 0, 1, @@ -3731,12 +3776,12 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3746,7 +3791,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -10625,7 +10671,6 @@ const docTemplate = `{ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10635,10 +10680,10 @@ const docTemplate = `{ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10648,7 +10693,8 @@ const docTemplate = `{ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.json b/docs/swagger.json index 450e877..f6e3d7b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3183,6 +3183,52 @@ } } }, + "/api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "使用优先有库存原料的策略生成配方", + "parameters": [ + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.GenerateRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/feed/recipes/{id}": { "get": { "security": [ @@ -3713,7 +3759,6 @@ }, { "enum": [ - 7, -1, 0, 1, @@ -3723,12 +3768,12 @@ 5, -1, 5, - 6 + 6, + 7 ], "type": "integer", "format": "int32", "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -3738,7 +3783,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ], "name": "level", "in": "query" @@ -10617,7 +10663,6 @@ "type": "integer", "format": "int32", "enum": [ - 7, -1, 0, 1, @@ -10627,10 +10672,10 @@ 5, -1, 5, - 6 + 6, + 7 ], "x-enum-varnames": [ - "_numLevels", "DebugLevel", "InfoLevel", "WarnLevel", @@ -10640,7 +10685,8 @@ "FatalLevel", "_minLevel", "_maxLevel", - "InvalidLevel" + "InvalidLevel", + "_numLevels" ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 08f9ea2..8fd239e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2752,7 +2752,6 @@ definitions: - PlanTypeFilterSystem zapcore.Level: enum: - - 7 - -1 - 0 - 1 @@ -2763,10 +2762,10 @@ definitions: - -1 - 5 - 6 + - 7 format: int32 type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -2777,6 +2776,7 @@ definitions: - _minLevel - _maxLevel - InvalidLevel + - _numLevels info: contact: email: divano@example.com @@ -4781,6 +4781,32 @@ paths: summary: 使用系统中所有可用的原料一键生成配方 tags: - 饲料管理-配方 + /api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}: + post: + description: 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。 + parameters: + - description: 猪类型ID + in: path + name: pig_type_id + required: true + type: integer + produces: + - application/json + responses: + "201": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.GenerateRecipeResponse' + type: object + security: + - BearerAuth: [] + summary: 使用优先有库存原料的策略生成配方 + tags: + - 饲料管理-配方 /api/v1/inventory/stock/adjust: post: consumes: @@ -5010,7 +5036,6 @@ paths: name: end_time type: string - enum: - - 7 - -1 - 0 - 1 @@ -5021,12 +5046,12 @@ paths: - -1 - 5 - 6 + - 7 format: int32 in: query name: level type: integer x-enum-varnames: - - _numLevels - DebugLevel - InfoLevel - WarnLevel @@ -5037,6 +5062,7 @@ paths: - _minLevel - _maxLevel - InvalidLevel + - _numLevels - enum: - 邮件 - 企业微信 diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 0ad8681..4189c06 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -260,6 +260,7 @@ func (a *API) setupRoutes() { 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.GenerateFromAllMaterials) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go index 1d44d00..6374454 100644 --- a/internal/app/controller/feed/recipe_controller.go +++ b/internal/app/controller/feed/recipe_controller.go @@ -225,3 +225,34 @@ func (c *RecipeController) GenerateFromAllMaterials(ctx echo.Context) error { 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) +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 0e1cdfe..1e86f46 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -27,6 +27,8 @@ type RecipeService interface { 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 接口的实现 @@ -50,6 +52,13 @@ func (s *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex 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") diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index f1a39b3..190f278 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -20,6 +20,8 @@ type Service interface { RecipeCoreService RecipeGenerateManager GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) + // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 + GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) } // recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 @@ -123,3 +125,99 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex // 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 // 复制一份 + // 大幅调低有库存原料的参考价格,诱导生成器优先使用 + adjustedMat.ReferencePrice = 0.01 // 设置一个非常小的价格 + 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. 丰富配方描述:计算并添加参考价格信息 + // 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格 + // 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("%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) + } + + // 6. 保存新生成的配方到数据库 + if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil { + + return nil, fmt.Errorf("保存生成的配方失败: %w", err) + } + logger.Infof("成功生成优先使用库存原料的配方: %+v", recipe) + + // 7. 返回创建的配方 + return recipe, nil +} -- 2.49.1 From d6e5d897680389013e47ba73167384c7754bb2ee Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 21:39:09 +0800 Subject: [PATCH 54/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/router.go | 2 +- internal/domain/recipe/recipe_service.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/app/api/router.go b/internal/app/api/router.go index 4189c06..b741782 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -260,7 +260,7 @@ func (a *API) setupRoutes() { 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.GenerateFromAllMaterials) + feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateRecipeWithPrioritizedStockRawMaterials) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 190f278..86e82a5 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -86,6 +86,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex } // 4. 丰富配方描述:计算并添加参考价格信息 + recipe.Name = fmt.Sprintf("%s - 使用所有已知原料", recipe.Name) // 填充 RecipeIngredients 中的 RawMaterial 字段,以便后续计算成本 rawMaterialMap := make(map[uint32]models.RawMaterial) @@ -120,7 +121,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithAllRawMaterials(ctx context.Contex if recipe, err = r.CreateRecipe(serviceCtx, recipe); err != nil { return nil, fmt.Errorf("保存生成的配方失败: %w", err) } - logger.Infof("成功生成配方: %+v", recipe) + logger.Infof("成功生成配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description) // 6. 返回创建的配方 (现在它应该已经有了ID) return recipe, nil @@ -184,6 +185,8 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c } // 5. 丰富配方描述:计算并添加参考价格信息 + recipe.Name = fmt.Sprintf("%s - 优先使用库存已有原料", recipe.Name) + // 注意:这里需要使用原始的、未调整价格的原料信息来计算最终的参考价格 // rawMaterialMap 从 allOriginalMaterials 构建,确保使用原始价格 rawMaterialMap := make(map[uint32]models.RawMaterial) @@ -201,7 +204,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c } referencePrice := recipe.CalculateReferencePricePerKilogram() / 100 - recipe.Description = fmt.Sprintf("%s 计算时预估成本: %.2f元/kg。", recipe.Description, referencePrice) + recipe.Description = fmt.Sprintf("使用有库存的 %v 种原料和 %v 种无库存原料计算的库存原料优先使用的配方。 计算时预估成本: %.2f元/kg。", len(stockMaterials), len(noStockMaterials), referencePrice) // 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。 // 此时需要在描述中说明需要添加的廉价填充料的百分比。 @@ -216,7 +219,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c return nil, fmt.Errorf("保存生成的配方失败: %w", err) } - logger.Infof("成功生成优先使用库存原料的配方: %+v", recipe) + logger.Infof("成功生成优先使用库存原料的配方: 配方名称: %v | 配方简介: %v", recipe.Name, recipe.Description) // 7. 返回创建的配方 return recipe, nil -- 2.49.1 From 968d996a9b0aadf64288e110ba5a2dd5fe31d2bf Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 21:47:07 +0800 Subject: [PATCH 55/59] =?UTF-8?q?=E4=BF=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/recipe/recipe_service.go | 2 +- .../repository/raw_material_repository.go | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 86e82a5..c7e6871 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -204,7 +204,7 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c } referencePrice := recipe.CalculateReferencePricePerKilogram() / 100 - recipe.Description = fmt.Sprintf("使用有库存的 %v 种原料和 %v 种无库存原料计算的库存原料优先使用的配方。 计算时预估成本: %.2f元/kg。", len(stockMaterials), len(noStockMaterials), referencePrice) + recipe.Description = fmt.Sprintf("使用 %v 种有库存原料和 %v 种无库存原料计算的库存原料优先使用的配方。 计算时预估成本: %.2f元/kg。", len(stockMaterials), len(noStockMaterials), referencePrice) // 如果 totalPercentage 小于 100%,说明填充料被使用,这是符合预期的。 // 此时需要在描述中说明需要添加的廉价填充料的百分比。 diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 78d6d71..96da7b0 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -18,7 +18,7 @@ type RawMaterialListOptions struct { NutrientName *string MinReferencePrice *float32 // 参考价格最小值 MaxReferencePrice *float32 // 参考价格最大值 - HasStock *bool // 是否只查询有库存的原料 + HasStock *bool OrderBy string } @@ -123,17 +123,24 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R db = db.Where("reference_price <= ?", *opts.MaxReferencePrice) } - // 筛选有库存的原料 - if opts.HasStock != nil && *opts.HasStock { + // 筛选有/无库存的原料 + 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 且 after_quantity > 0 的 raw_material_id - // GORM 会将 rankedLogsQuery 作为一个子查询嵌入到 FROM 子句中 + // 外部子查询:从 ranked_logs 中筛选 rn=1 的 raw_material_id latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery). Select("raw_material_id"). - Where("rn = 1 AND after_quantity > 0") + Where("rn = 1") + + if *opts.HasStock { + // 筛选有库存的原料 (after_quantity > 0) + latestStockLogSubQuery = latestStockLogSubQuery.Where("after_quantity > 0") + } else { + // 筛选没有库存的原料 (after_quantity = 0) + latestStockLogSubQuery = latestStockLogSubQuery.Where("after_quantity = 0") + } // 将这个子查询直接应用到主查询的 WHERE id IN (?) 条件中 db = db.Where("id IN (?)", latestStockLogSubQuery) -- 2.49.1 From bc4355cad51d11f33f011546a65c77957ee91559 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 27 Nov 2025 22:01:37 +0800 Subject: [PATCH 56/59] =?UTF-8?q?=E4=BF=AEbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/repository/raw_material_repository.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/infra/repository/raw_material_repository.go b/internal/infra/repository/raw_material_repository.go index 96da7b0..2c00b5e 100644 --- a/internal/infra/repository/raw_material_repository.go +++ b/internal/infra/repository/raw_material_repository.go @@ -132,18 +132,18 @@ func (r *gormRawMaterialRepository) ListRawMaterials(ctx context.Context, opts R // 外部子查询:从 ranked_logs 中筛选 rn=1 的 raw_material_id latestStockLogSubQuery := r.db.Table("(?) as ranked_logs", rankedLogsQuery). Select("raw_material_id"). - Where("rn = 1") + Where("rn = 1"). + Where("after_quantity > 0") if *opts.HasStock { - // 筛选有库存的原料 (after_quantity > 0) - latestStockLogSubQuery = latestStockLogSubQuery.Where("after_quantity > 0") + // 筛选有库存的原料 (ID 在有正库存的集合中) + db = db.Where("id IN (?)", latestStockLogSubQuery) } else { - // 筛选没有库存的原料 (after_quantity = 0) - latestStockLogSubQuery = latestStockLogSubQuery.Where("after_quantity = 0") + // 筛选无库存的原料 (ID 不在有正库存的集合中) + // 包含了最新库存为0 和 没有库存日志的原料。 + db = db.Where("id NOT IN (?)", latestStockLogSubQuery) } - // 将这个子查询直接应用到主查询的 WHERE id IN (?) 条件中 - db = db.Where("id IN (?)", latestStockLogSubQuery) } // 首先计算总数 -- 2.49.1 From 04b46d80259aeb557839f673928b766638dad4d3 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 28 Nov 2025 14:37:53 +0800 Subject: [PATCH 57/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/device/general_device_service.go | 6 +++--- .../transport/lora/lora_mesh_uart_passthrough_transport.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/domain/device/general_device_service.go b/internal/domain/device/general_device_service.go index 4364735..78bd2b8 100644 --- a/internal/domain/device/general_device_service.go +++ b/internal/domain/device/general_device_service.go @@ -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 } diff --git a/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go b/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go index a302e48..063fe5e 100644 --- a/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go +++ b/internal/infra/transport/lora/lora_mesh_uart_passthrough_transport.go @@ -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) -- 2.49.1 From de68151539c10cf4aa9ca9039fd665b10ee9db0e Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 28 Nov 2025 22:22:39 +0800 Subject: [PATCH 58/59] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=A5=E7=AE=97?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + internal/domain/recipe/recipe_service.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d2de6ff..883e2b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ 1. 你可以访问 http://localhost:8080/ 进入我的前端界面, 前端项目是另一个项目, 但接入的是当前项目对应的后端平台, 如果需要登录账号密码都是huang 2. 你可以阅读 config/config.yml 了解我的配置信息, 包括数据库的连接地址和账号密码, 本平台监听的端口等, 后端的swagger界面在 http://localhost:8086/swagger/index.html 3. 项目根目录有project_structure.txt, 你需要先阅读此文件了解项目目录结构 +4. 项目中有config/presets-data目录, 里面有一些预设数据, 可以间接看作数据库内的数据, 在测试环境中他们一般会和数据库的数据保持一致 # 权限管理 diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index c7e6871..04056a9 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -169,7 +169,8 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c for _, mat := range stockMaterials { adjustedMat := mat // 复制一份 // 大幅调低有库存原料的参考价格,诱导生成器优先使用 - adjustedMat.ReferencePrice = 0.01 // 设置一个非常小的价格 + // TODO 按理说应该尽量优先使用已有原料, 但如果搭配后购买缺失原料花的钱还不如不用已有原料的另一个组合钱少怎么办 + adjustedMat.ReferencePrice = adjustedMat.ReferencePrice * 0.1 materialsForGeneration = append(materialsForGeneration, adjustedMat) logger.Debugf("原料 '%s' (ID: %d) 有库存,生成配方时参考价格调整为 %.2f", mat.Name, mat.ID, adjustedMat.ReferencePrice) } -- 2.49.1 From 4aa56441ceb7e1e6f2ce6381aedad3170c8bddc6 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 29 Nov 2025 15:38:52 +0800 Subject: [PATCH 59/59] =?UTF-8?q?=E5=BD=92=E6=A1=A3=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-11-29-recipe-management}/index.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) rename design/{recipe-management => archive/2025-11-29-recipe-management}/index.md (79%) diff --git a/design/recipe-management/index.md b/design/archive/2025-11-29-recipe-management/index.md similarity index 79% rename from design/recipe-management/index.md rename to design/archive/2025-11-29-recipe-management/index.md index 7e4caa8..4b929db 100644 --- a/design/recipe-management/index.md +++ b/design/archive/2025-11-29-recipe-management/index.md @@ -46,6 +46,20 @@ http://git.huangwc.com/pig/pig-farm-controller/issues/66 7. 简单查看功能 - 两个配方对比页面(营养+成本对比) +# 实现总结 + +## 实现内容 + +实现库存和原料和营养和猪营养需求的管理, 支持根据库存和已录入原料和猪营养需求生成配方 + +## TODO + +1. 发酵料管理考虑到发酵目前没有自动化流程, 不好追踪, 遂暂时不做 +2. 目前的价格是根据原料的参考价设置的, 后续应当实现一个在服务供平台采集参考价, 以及使用原料采购价计算 +3. 原料应该加上膨润土等, 比如膨润土的黄曲霉素含量应该是负数以表示减少饲料里的含量 +4. 饲料保质期考虑到批次间管理暂时不方便, 等可以实现同一原料先进先出后再实现 +5. 暂时不支持指定原料列表然后自动生成, 也不支持告诉用户当前生成不出是为什么, 等以后再做 + # 完成事项 1. 定义原料表, 营养表, 原料营养表, 原料库存变更表 -- 2.49.1