From 3cc88a52486cf0060a14ffc2ef71955e1483d886 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 1 Oct 2025 20:39:59 +0800 Subject: [PATCH 01/65] =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E6=94=B9=E6=88=90?= =?UTF-8?q?=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 57 +++++++++---------- docs/swagger.json | 57 +++++++++---------- docs/swagger.yaml | 57 +++++++++---------- .../app/controller/plan/plan_controller.go | 14 ++--- internal/app/service/task/scheduler.go | 2 +- internal/core/application.go | 2 +- internal/infra/models/device_template.go | 4 +- internal/infra/models/execution.go | 20 +++---- internal/infra/models/feed.go | 5 ++ internal/infra/models/pig.go | 5 ++ internal/infra/models/plan.go | 31 +++++----- internal/infra/models/sensor_data.go | 10 ++-- 12 files changed, 136 insertions(+), 128 deletions(-) create mode 100644 internal/infra/models/feed.go create mode 100644 internal/infra/models/pig.go diff --git a/docs/docs.go b/docs/docs.go index f03b544..fb7fc98 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1345,8 +1345,8 @@ const docTemplate = `{ "models.DeviceCategory": { "type": "string", "enum": [ - "actuator", - "sensor" + "执行器", + "传感器" ], "x-enum-varnames": [ "CategoryActuator", @@ -1356,8 +1356,8 @@ const docTemplate = `{ "models.PlanContentType": { "type": "string", "enum": [ - "sub_plans", - "tasks" + "子计划", + "任务" ], "x-enum-comments": { "PlanContentTypeSubPlans": "计划包含子计划", @@ -1375,8 +1375,8 @@ const docTemplate = `{ "models.PlanExecutionType": { "type": "string", "enum": [ - "automatic", - "manual" + "自动", + "手动" ], "x-enum-comments": { "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", @@ -1392,19 +1392,18 @@ const docTemplate = `{ ] }, "models.PlanStatus": { - "type": "integer", - "format": "int32", + "type": "string", "enum": [ - 0, - 1, - 2, - 3 + "已禁用", + "已启用", + "执行完毕", + "执行失败" ], "x-enum-comments": { "PlanStatusDisabled": "禁用计划", "PlanStatusEnabled": "启用计划", "PlanStatusFailed": "执行失败", - "PlanStatusStopeed": "执行完毕" + "PlanStatusStopped": "执行完毕" }, "x-enum-descriptions": [ "禁用计划", @@ -1415,18 +1414,18 @@ const docTemplate = `{ "x-enum-varnames": [ "PlanStatusDisabled", "PlanStatusEnabled", - "PlanStatusStopeed", + "PlanStatusStopped", "PlanStatusFailed" ] }, "models.SensorType": { "type": "string", "enum": [ - "signal_metrics", - "battery_level", - "temperature", - "humidity", - "weight" + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" ], "x-enum-comments": { "SensorTypeBatteryLevel": "电池电量", @@ -1453,9 +1452,9 @@ const docTemplate = `{ "models.TaskType": { "type": "string", "enum": [ - "plan_analysis", - "waiting", - "release_feed_weight" + "计划分析", + "等待", + "下料" ], "x-enum-comments": { "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", @@ -1514,7 +1513,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", @@ -1558,7 +1557,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanContentType" } ], - "example": "tasks" + "example": "任务" }, "cron_expression": { "type": "string", @@ -1582,7 +1581,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "id": { "type": "integer", @@ -1598,7 +1597,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanStatus" } ], - "example": 0 + "example": "已启用" }, "sub_plans": { "type": "array", @@ -1663,7 +1662,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, @@ -1700,7 +1699,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, @@ -1725,7 +1724,7 @@ const docTemplate = `{ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", diff --git a/docs/swagger.json b/docs/swagger.json index 080e250..60e6dec 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1334,8 +1334,8 @@ "models.DeviceCategory": { "type": "string", "enum": [ - "actuator", - "sensor" + "执行器", + "传感器" ], "x-enum-varnames": [ "CategoryActuator", @@ -1345,8 +1345,8 @@ "models.PlanContentType": { "type": "string", "enum": [ - "sub_plans", - "tasks" + "子计划", + "任务" ], "x-enum-comments": { "PlanContentTypeSubPlans": "计划包含子计划", @@ -1364,8 +1364,8 @@ "models.PlanExecutionType": { "type": "string", "enum": [ - "automatic", - "manual" + "自动", + "手动" ], "x-enum-comments": { "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", @@ -1381,19 +1381,18 @@ ] }, "models.PlanStatus": { - "type": "integer", - "format": "int32", + "type": "string", "enum": [ - 0, - 1, - 2, - 3 + "已禁用", + "已启用", + "执行完毕", + "执行失败" ], "x-enum-comments": { "PlanStatusDisabled": "禁用计划", "PlanStatusEnabled": "启用计划", "PlanStatusFailed": "执行失败", - "PlanStatusStopeed": "执行完毕" + "PlanStatusStopped": "执行完毕" }, "x-enum-descriptions": [ "禁用计划", @@ -1404,18 +1403,18 @@ "x-enum-varnames": [ "PlanStatusDisabled", "PlanStatusEnabled", - "PlanStatusStopeed", + "PlanStatusStopped", "PlanStatusFailed" ] }, "models.SensorType": { "type": "string", "enum": [ - "signal_metrics", - "battery_level", - "temperature", - "humidity", - "weight" + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" ], "x-enum-comments": { "SensorTypeBatteryLevel": "电池电量", @@ -1442,9 +1441,9 @@ "models.TaskType": { "type": "string", "enum": [ - "plan_analysis", - "waiting", - "release_feed_weight" + "计划分析", + "等待", + "下料" ], "x-enum-comments": { "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", @@ -1503,7 +1502,7 @@ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", @@ -1547,7 +1546,7 @@ "$ref": "#/definitions/models.PlanContentType" } ], - "example": "tasks" + "example": "任务" }, "cron_expression": { "type": "string", @@ -1571,7 +1570,7 @@ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "id": { "type": "integer", @@ -1587,7 +1586,7 @@ "$ref": "#/definitions/models.PlanStatus" } ], - "example": 0 + "example": "已启用" }, "sub_plans": { "type": "array", @@ -1652,7 +1651,7 @@ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, @@ -1689,7 +1688,7 @@ "$ref": "#/definitions/models.TaskType" } ], - "example": "waiting" + "example": "等待" } } }, @@ -1714,7 +1713,7 @@ "$ref": "#/definitions/models.PlanExecutionType" } ], - "example": "automatic" + "example": "自动" }, "name": { "type": "string", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a17b732..2e66b9f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -229,16 +229,16 @@ definitions: type: object models.DeviceCategory: enum: - - actuator - - sensor + - 执行器 + - 传感器 type: string x-enum-varnames: - CategoryActuator - CategorySensor models.PlanContentType: enum: - - sub_plans - - tasks + - 子计划 + - 任务 type: string x-enum-comments: PlanContentTypeSubPlans: 计划包含子计划 @@ -251,8 +251,8 @@ definitions: - PlanContentTypeTasks models.PlanExecutionType: enum: - - automatic - - manual + - 自动 + - 手动 type: string x-enum-comments: PlanExecutionTypeAutomatic: 自动执行 (包含定时和循环) @@ -265,17 +265,16 @@ definitions: - PlanExecutionTypeManual models.PlanStatus: enum: - - 0 - - 1 - - 2 - - 3 - format: int32 - type: integer + - 已禁用 + - 已启用 + - 执行完毕 + - 执行失败 + type: string x-enum-comments: PlanStatusDisabled: 禁用计划 PlanStatusEnabled: 启用计划 PlanStatusFailed: 执行失败 - PlanStatusStopeed: 执行完毕 + PlanStatusStopped: 执行完毕 x-enum-descriptions: - 禁用计划 - 启用计划 @@ -284,15 +283,15 @@ definitions: x-enum-varnames: - PlanStatusDisabled - PlanStatusEnabled - - PlanStatusStopeed + - PlanStatusStopped - PlanStatusFailed models.SensorType: enum: - - signal_metrics - - battery_level - - temperature - - humidity - - weight + - 信号强度 + - 电池电量 + - 温度 + - 湿度 + - 重量 type: string x-enum-comments: SensorTypeBatteryLevel: 电池电量 @@ -314,9 +313,9 @@ definitions: - SensorTypeWeight models.TaskType: enum: - - plan_analysis - - waiting - - release_feed_weight + - 计划分析 + - 等待 + - 下料 type: string x-enum-comments: TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 @@ -355,7 +354,7 @@ definitions: execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' - example: automatic + example: 自动 name: example: 猪舍温度控制计划 type: string @@ -386,7 +385,7 @@ definitions: content_type: allOf: - $ref: '#/definitions/models.PlanContentType' - example: tasks + example: 任务 cron_expression: example: 0 0 6 * * * type: string @@ -402,7 +401,7 @@ definitions: execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' - example: automatic + example: 自动 id: example: 1 type: integer @@ -412,7 +411,7 @@ definitions: status: allOf: - $ref: '#/definitions/models.PlanStatus' - example: 0 + example: 已启用 sub_plans: items: $ref: '#/definitions/plan.SubPlanResponse' @@ -456,7 +455,7 @@ definitions: type: allOf: - $ref: '#/definitions/models.TaskType' - example: waiting + example: 等待 type: object plan.TaskResponse: properties: @@ -481,7 +480,7 @@ definitions: type: allOf: - $ref: '#/definitions/models.TaskType' - example: waiting + example: 等待 type: object plan.UpdatePlanRequest: properties: @@ -497,7 +496,7 @@ definitions: execution_type: allOf: - $ref: '#/definitions/models.PlanExecutionType' - example: automatic + example: 自动 name: example: 猪舍温度控制计划V2 type: string diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 6956e6a..f834f52 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -19,7 +19,7 @@ import ( type CreatePlanRequest struct { Name string `json:"name" binding:"required" example:"猪舍温度控制计划"` Description string `json:"description" example:"根据温度自动调节风扇和加热器"` - ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"automatic"` + ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"` ExecuteNum uint `json:"execute_num,omitempty" example:"10"` CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` @@ -31,12 +31,12 @@ type PlanResponse struct { ID uint `json:"id" example:"1"` Name string `json:"name" example:"猪舍温度控制计划"` Description string `json:"description" example:"根据温度自动调节风扇和加热器"` - ExecutionType models.PlanExecutionType `json:"execution_type" example:"automatic"` - Status models.PlanStatus `json:"status" example:"0"` + ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` + Status models.PlanStatus `json:"status" example:"已启用"` ExecuteNum uint `json:"execute_num" example:"10"` ExecuteCount uint `json:"execute_count" example:"0"` CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` - ContentType models.PlanContentType `json:"content_type" example:"tasks"` + ContentType models.PlanContentType `json:"content_type" example:"任务"` SubPlans []SubPlanResponse `json:"sub_plans,omitempty"` Tasks []TaskResponse `json:"tasks,omitempty"` } @@ -51,7 +51,7 @@ type ListPlansResponse struct { type UpdatePlanRequest struct { Name string `json:"name" example:"猪舍温度控制计划V2"` Description string `json:"description" example:"更新后的描述"` - ExecutionType models.PlanExecutionType `json:"execution_type" example:"automatic"` + ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` ExecuteNum uint `json:"execute_num,omitempty" example:"10"` CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` @@ -72,7 +72,7 @@ type TaskRequest struct { Name string `json:"name" example:"打开风扇"` Description string `json:"description" example:"打开1号风扇"` ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"waiting"` + Type models.TaskType `json:"type" example:"等待"` Parameters map[string]interface{} `json:"parameters,omitempty"` } @@ -83,7 +83,7 @@ type TaskResponse struct { Name string `json:"name" example:"打开风扇"` Description string `json:"description" example:"打开1号风扇"` ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"waiting"` + Type models.TaskType `json:"type" example:"等待"` Parameters map[string]interface{} `json:"parameters,omitempty"` } diff --git a/internal/app/service/task/scheduler.go b/internal/app/service/task/scheduler.go index d60d6c4..6b190f3 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/app/service/task/scheduler.go @@ -435,7 +435,7 @@ func (s *Scheduler) handlePlanCompletion(planLogID uint) { // 如果是自动计划且达到执行次数上限,或计划是手动类型,则更新计划状态为已停止 if (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteNum > 0 && newExecuteCount >= plan.ExecuteNum) || plan.ExecutionType == models.PlanExecutionTypeManual { - newStatus = models.PlanStatusStopeed + newStatus = models.PlanStatusStopped s.logger.Infof("计划 %d 已完成执行,状态更新为 '执行完毕'。", topLevelPlanID) } diff --git a/internal/core/application.go b/internal/core/application.go index a68c4f2..0c40657 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -263,7 +263,7 @@ func (app *Application) initializePendingTasks( if plan.ExecutionType == models.PlanExecutionTypeManual || (plan.ExecutionType == models.PlanExecutionTypeAutomatic && plan.ExecuteCount >= plan.ExecuteNum) { // 更新计划状态为已停止 - plan.Status = models.PlanStatusStopeed + plan.Status = models.PlanStatusStopped logger.Infof("计划 #%d 状态已更新为 '执行完毕'。", plan.ID) } diff --git a/internal/infra/models/device_template.go b/internal/infra/models/device_template.go index 5eae4f5..ceb49e4 100644 --- a/internal/infra/models/device_template.go +++ b/internal/infra/models/device_template.go @@ -15,9 +15,9 @@ type DeviceCategory string const ( // CategoryActuator 代表一个执行器,可以被控制(例如:风机、阀门) - CategoryActuator DeviceCategory = "actuator" + CategoryActuator DeviceCategory = "执行器" // CategorySensor 代表一个传感器,用于报告测量值(例如:温度计) - CategorySensor DeviceCategory = "sensor" + CategorySensor DeviceCategory = "传感器" ) // ValueDescriptor 描述了传感器可以报告的单个数值。 diff --git a/internal/infra/models/execution.go b/internal/infra/models/execution.go index 9a8a58d..721441d 100644 --- a/internal/infra/models/execution.go +++ b/internal/infra/models/execution.go @@ -17,11 +17,11 @@ const ( type ExecutionStatus string const ( - ExecutionStatusStarted ExecutionStatus = "started" // 开始执行 - ExecutionStatusCompleted ExecutionStatus = "completed" // 执行完成 - ExecutionStatusFailed ExecutionStatus = "failed" // 执行失败 - ExecutionStatusCancelled ExecutionStatus = "cancelled" // 执行取消 - ExecutionStatusWaiting ExecutionStatus = "waiting" // 等待执行 (用于预写日志) + ExecutionStatusStarted ExecutionStatus = "已开始" // 开始执行 + ExecutionStatusCompleted ExecutionStatus = "已完成" // 执行完成 + ExecutionStatusFailed ExecutionStatus = "失败" // 执行失败 + ExecutionStatusCancelled ExecutionStatus = "已取消" // 执行取消 + ExecutionStatusWaiting ExecutionStatus = "等待中" // 等待执行 (用于预写日志) ) // PlanExecutionLog 记录整个计划的一次执行历史 @@ -92,9 +92,9 @@ func (log *TaskExecutionLog) AfterFind(tx *gorm.DB) (err error) { type PendingCollectionStatus string const ( - PendingStatusPending PendingCollectionStatus = "pending" // 请求已发送,等待设备响应 - PendingStatusFulfilled PendingCollectionStatus = "fulfilled" // 已收到设备响应并成功处理 - PendingStatusTimedOut PendingCollectionStatus = "timed_out" // 请求超时,未收到设备响应 + PendingStatusPending PendingCollectionStatus = "等待中" // 请求已发送,等待设备响应 + PendingStatusFulfilled PendingCollectionStatus = "已完成" // 已收到设备响应并成功处理 + PendingStatusTimedOut PendingCollectionStatus = "已超时" // 请求超时,未收到设备响应 ) // DeviceCommandLog 记录所有“发后即忘”的下行指令日志。 @@ -160,8 +160,8 @@ func (PendingCollection) TableName() string { type AuditStatus string const ( - AuditStatusSuccess AuditStatus = "success" - AuditStatusFailed AuditStatus = "failed" + AuditStatusSuccess AuditStatus = "成功" + AuditStatusFailed AuditStatus = "失败" ) // --- 审计日志相关上下文键 --- diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go new file mode 100644 index 0000000..f0a5807 --- /dev/null +++ b/internal/infra/models/feed.go @@ -0,0 +1,5 @@ +package models + +/* + 饲料和饲喂相关的模型 +*/ diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go new file mode 100644 index 0000000..1579859 --- /dev/null +++ b/internal/infra/models/pig.go @@ -0,0 +1,5 @@ +package models + +/* + 和猪只本身相关的模型 +*/ diff --git a/internal/infra/models/plan.go b/internal/infra/models/plan.go index bf2ce42..15189fc 100644 --- a/internal/infra/models/plan.go +++ b/internal/infra/models/plan.go @@ -15,25 +15,25 @@ import ( type PlanExecutionType string const ( - PlanExecutionTypeAutomatic PlanExecutionType = "automatic" // 自动执行 (包含定时和循环) - PlanExecutionTypeManual PlanExecutionType = "manual" // 手动执行 + PlanExecutionTypeAutomatic PlanExecutionType = "自动" // 自动执行 (包含定时和循环) + PlanExecutionTypeManual PlanExecutionType = "手动" // 手动执行 ) // PlanContentType 定义了计划包含的内容类型 type PlanContentType string const ( - PlanContentTypeSubPlans PlanContentType = "sub_plans" // 计划包含子计划 - PlanContentTypeTasks PlanContentType = "tasks" // 计划包含任务 + PlanContentTypeSubPlans PlanContentType = "子计划" // 计划包含子计划 + PlanContentTypeTasks PlanContentType = "任务" // 计划包含任务 ) // TaskType 定义了任务的类型,每个类型可以对应 task 包中的一个具体动作 type TaskType string const ( - TaskPlanAnalysis TaskType = "plan_analysis" // 解析Plan的Task列表并添加到待执行队列的特殊任务 - TaskTypeWaiting TaskType = "waiting" // 等待任务 - TaskTypeReleaseFeedWeight TaskType = "release_feed_weight" // 下料口释放指定重量任务 + TaskPlanAnalysis TaskType = "计划分析" // 解析Plan的Task列表并添加到待执行队列的特殊任务 + TaskTypeWaiting TaskType = "等待" // 等待任务 + TaskTypeReleaseFeedWeight TaskType = "下料" // 下料口释放指定重量任务 ) // -- Task Parameters -- @@ -42,13 +42,14 @@ const ( ParamsPlanID = "plan_id" ) -type PlanStatus uint8 +// PlanStatus 定义了计划的状态 +type PlanStatus string const ( - PlanStatusDisabled PlanStatus = 0 // 禁用计划 - PlanStatusEnabled PlanStatus = 1 // 启用计划 - PlanStatusStopeed PlanStatus = 2 // 执行完毕 - PlanStatusFailed PlanStatus = 3 // 执行失败 + PlanStatusDisabled PlanStatus = "已禁用" // 禁用计划 + PlanStatusEnabled PlanStatus = "已启用" // 启用计划 + PlanStatusStopped PlanStatus = "执行完毕" // 执行完毕 + PlanStatusFailed PlanStatus = "执行失败" // 执行失败 ) // Plan 代表系统中的一个计划,可以包含子计划或任务 @@ -58,9 +59,9 @@ type Plan struct { Name string `gorm:"not null" json:"name"` Description string `json:"description"` ExecutionType PlanExecutionType `gorm:"not null;index" json:"execution_type"` - Status PlanStatus `gorm:"default:0;index" json:"status"` // 计划是否被启动 - ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 - ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 + Status PlanStatus `gorm:"default:'已禁用';index" json:"status"` // 计划是否被启动 + ExecuteNum uint `gorm:"default:0" json:"execute_num"` // 计划预期执行次数 + ExecuteCount uint `gorm:"default:0" json:"execute_count"` // 执行计数器 // 针对 PlanExecutionTypeAutomatic,使用 Cron 表达式定义调度规则 CronExpression string `json:"cron_expression"` diff --git a/internal/infra/models/sensor_data.go b/internal/infra/models/sensor_data.go index 48be4f5..4c50221 100644 --- a/internal/infra/models/sensor_data.go +++ b/internal/infra/models/sensor_data.go @@ -10,11 +10,11 @@ import ( type SensorType string const ( - SensorTypeSignalMetrics SensorType = "signal_metrics" // 信号强度 - SensorTypeBatteryLevel SensorType = "battery_level" // 电池电量 - SensorTypeTemperature SensorType = "temperature" // 温度 - SensorTypeHumidity SensorType = "humidity" // 湿度 - SensorTypeWeight SensorType = "weight" // 重量 + SensorTypeSignalMetrics SensorType = "信号强度" // 信号强度 + SensorTypeBatteryLevel SensorType = "电池电量" // 电池电量 + SensorTypeTemperature SensorType = "温度" // 温度 + SensorTypeHumidity SensorType = "湿度" // 湿度 + SensorTypeWeight SensorType = "重量" // 重量 ) // SignalMetrics 存储信号强度数据 From 0b8b37511e368228375d3be2a37c25e4978bfd00 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Wed, 1 Oct 2025 20:40:35 +0800 Subject: [PATCH 02/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=85=BD=E8=8D=AF?= =?UTF-8?q?=E6=A8=A1=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/medication.go | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 internal/infra/models/medication.go diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go new file mode 100644 index 0000000..05dcbce --- /dev/null +++ b/internal/infra/models/medication.go @@ -0,0 +1,68 @@ +package models + +import ( + "time" + + "gorm.io/datatypes" + "gorm.io/gorm" +) + +/* + 所有与药品、疫苗和用药记录相关的模型 +*/ + +// MedicationType 定义了兽药的类型 +type MedicationType string + +const ( + Powder MedicationType = "粉剂" + Injection MedicationType = "针剂" + Vaccine MedicationType = "疫苗" +) + +// MedicationCategory 定义了兽药的种类 +type MedicationCategory string + +const ( + Tetracycline MedicationCategory = "四环素类" + Sulfonamide MedicationCategory = "磺胺类" + Penicillin MedicationCategory = "青霉素类" + Macrolide MedicationCategory = "大环内酯类" + Quinolone MedicationCategory = "喹诺酮类" + Anthelmintic MedicationCategory = "驱虫药" + Disinfectant MedicationCategory = "消毒药" + BiologicalProduct MedicationCategory = "生物制品" +) + +// MixType 定义了粉剂药物该如何混合 +type MixType string + +const ( + MixFeed = "饲料加药" + MixWater = "水中加药" +) + +// PowderInstructions 定义了粉剂使用说明. +// 在程序中, 可以将 Medication.Instructions 字段反序列化为此结构进行操作. +type PowderInstructions struct { + // 出栏前停药期 + WithdrawalPeriod time.Duration `json:"withdrawal_period"` + // 拌料使用计量, 每千克体重用多少克药, 单位: g/kg + BodyWeightDosageUsed float64 `json:"body_weight_dosage_used"` + // 拌料使用剂量, 每升水加多少克药或每千克饲料干重加多少克药, 单位: g/kg(L) + MixDosageUsed float64 `json:"mix_dosage_used"` + // 拌料使用方式, 兑水/拌料 + MixType MixType `json:"mix_type"` +} + +// Medication 定义了兽药/疫苗的基本信息模型 +type Medication struct { + gorm.Model + Name string `gorm:"size:100;not null;comment:药品名称" json:"name"` + Type MedicationType `gorm:"size:20;not null;comment:兽药类型 (粉剂, 针剂, 疫苗)" json:"type"` + Category MedicationCategory `gorm:"size:30;not null;comment:兽药种类 (四环素类, 磺胺类等)" json:"category"` + DosagePerUnit float64 `gorm:"size:50;comment:一份药物的计量 (针剂计量单位为毫升, 粉剂为克)" json:"dosage_per_unit"` + ActiveIngredientConcentration float64 `gorm:"size:50;comment:有效成分含量百分比" json:"active_ingredient_concentration"` + Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"` + Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"` +} From 829f0a625354f6f0bd01cc5005d911e5d365fcef Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Thu, 2 Oct 2025 00:18:13 +0800 Subject: [PATCH 03/65] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yml | 52 +++++++++++++++++++ Makefile | 8 ++- internal/app/api/api.go | 17 +++--- .../app/controller/plan/plan_controller.go | 2 +- .../app/controller/user/user_controller.go | 2 +- internal/app/middleware/audit.go | 2 +- internal/app/middleware/auth.go | 2 +- .../transport => webhook}/chirp_stack.go | 4 +- .../chirp_stack_types.go | 2 +- .../transport => webhook}/transport.go | 2 +- internal/core/application.go | 12 ++--- .../{app/service => domain}/audit/service.go | 0 .../device/device_service.go | 0 .../device/general_device_service.go | 2 +- .../device/proto/device.pb.go | 2 +- .../device/proto/device.proto | 2 +- .../task/analysis_plan_task_manager.go | 0 .../service => domain}/task/delay_task.go | 0 .../task/delay_task_test.go | 0 .../task/release_feed_weight_task.go | 2 +- .../{app/service => domain}/task/scheduler.go | 2 +- internal/{app/service => domain}/task/task.go | 0 .../service => domain}/token/token_service.go | 0 .../token/token_service_test.go | 0 24 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 .golangci.yml rename internal/app/{service/transport => webhook}/chirp_stack.go (99%) rename internal/app/{service/transport => webhook}/chirp_stack_types.go (99%) rename internal/app/{service/transport => webhook}/transport.go (89%) rename internal/{app/service => domain}/audit/service.go (100%) rename internal/{app/service => domain}/device/device_service.go (100%) rename internal/{app/service => domain}/device/general_device_service.go (99%) rename internal/{app/service => domain}/device/proto/device.pb.go (99%) rename internal/{app/service => domain}/device/proto/device.proto (96%) rename internal/{app/service => domain}/task/analysis_plan_task_manager.go (100%) rename internal/{app/service => domain}/task/delay_task.go (100%) rename internal/{app/service => domain}/task/delay_task_test.go (100%) rename internal/{app/service => domain}/task/release_feed_weight_task.go (98%) rename internal/{app/service => domain}/task/scheduler.go (99%) rename internal/{app/service => domain}/task/task.go (100%) rename internal/{app/service => domain}/token/token_service.go (100%) rename internal/{app/service => domain}/token/token_service_test.go (100%) diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..badb4a6 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,52 @@ +# .golangci.yml - 为你的项目量身定制的 linter 配置 + +linters-settings: + # 这里可以对特定的 linter 进行微调 + errcheck: + # 检查未处理的错误,但可以排除一些常见的、我们确认无需处理的函数 + exclude-functions: + - io/ioutil.ReadFile + - io.Copy + - io.WriteString + - os.Create + +linters: + # 明确我们想要禁用的 linter + disable: + # --- 暂时禁用的“干扰项” --- + - godox # 禁用对 TODO, FIXME 注释的检查,让我们能专注于代码 + + # --- 暂时禁用的“风格/复杂度”检查器 --- + - gocyclo # 暂时不检查圈复杂度 + - funlen # 暂时不检查函数长度 + - lll # 暂时不检查行长度 + - wsl # 检查多余的空格和换行,可以后期再处理 + - gocritic # 这个检查器包含很多子项,有些可能过于严格,可以先禁用,或在下面精细配置 + + # 排除路径:分析这些文件但不报告问题(使用 regex 匹配) + exclusions: + paths: + # 排除 docs/ 目录(匹配路径以 docs/ 开头) + - '^docs/' + + # 精细排除规则:用于特定文件/文本的 linter 排除 + rules: + # 排除对 main.go 中 log.Fatalf 的抱怨(仅针对 goconst linter) + - path: '^main\.go$' + text: "log.Fatalf" + linters: + - goconst + + # 你也可以明确启用你认为最重要的检查器,形成一个“白名单” + # enable: + # - govet + # - errcheck + # - staticcheck + # - unused + # - gosimple + # - ineffassign + # - typecheck + +run: + # 完全跳过测试文件分析(不解析、不报告任何问题) + tests: false \ No newline at end of file diff --git a/Makefile b/Makefile index 4ed8fd6..a050764 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ help: @echo " swag Generate swagger docs" @echo " help Show this help message" @echo " proto Generate protobuf files" + @echo " lint Lint the code" # 运行应用 .PHONY: run @@ -44,4 +45,9 @@ swag: # 生成protobuf文件 .PHONY: proto proto: - protoc --go_out=internal/app/service/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/app/service/device/proto --go-grpc_opt=paths=source_relative -Iinternal/app/service/device/proto internal/app/service/device/proto/device.proto + protoc --go_out=internal/domain/device/proto --go_opt=paths=source_relative --go-grpc_out=internal/domain/device/proto --go-grpc_opt=paths=source_relative -Iinternal/domain/device/proto internal/domain/device/proto/device.proto + +# 运行代码检查 +.PHONY: lint +lint: + golangci-lint run ./... \ No newline at end of file diff --git a/internal/app/api/api.go b/internal/app/api/api.go index f53a5ed..57106bd 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -20,10 +20,10 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" "git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport" + "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" @@ -45,7 +45,7 @@ type API struct { userController *user.Controller // 用户控制器实例 deviceController *device.Controller // 设备控制器实例 planController *plan.Controller // 计划控制器实例 - listenHandler transport.ListenHandler // 设备上行事件监听器 + listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -61,7 +61,7 @@ func NewAPI(cfg config.ServerConfig, userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 - listenHandler transport.ListenHandler, + listenHandler webhook.ListenHandler, analysisTaskManager *task.AnalysisPlanTaskManager) *API { // 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) // 从配置中获取 Gin 模式 @@ -127,10 +127,7 @@ func (a *API) setupRoutes() { a.logger.Info("pprof 接口注册成功") // 上行事件监听路由 - a.engine.POST("/upstream", func(c *gin.Context) { - h := a.listenHandler.Handler() - h.ServeHTTP(c.Writer, c.Request) - }) + a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) a.logger.Info("上行事件监听接口注册成功") // 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到 diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index f834f52..284df63 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -5,7 +5,7 @@ import ( "strconv" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" - task "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "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" diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 1f84e68..7d6d1e8 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -5,7 +5,7 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "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" diff --git a/internal/app/middleware/audit.go b/internal/app/middleware/audit.go index db2a8b2..44ee93b 100644 --- a/internal/app/middleware/audit.go +++ b/internal/app/middleware/audit.go @@ -6,7 +6,7 @@ import ( "io" "strconv" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "github.com/gin-gonic/gin" ) diff --git a/internal/app/middleware/auth.go b/internal/app/middleware/auth.go index 8684acb..2755313 100644 --- a/internal/app/middleware/auth.go +++ b/internal/app/middleware/auth.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "github.com/gin-gonic/gin" diff --git a/internal/app/service/transport/chirp_stack.go b/internal/app/webhook/chirp_stack.go similarity index 99% rename from internal/app/service/transport/chirp_stack.go rename to internal/app/webhook/chirp_stack.go index bf5779e..de07597 100644 --- a/internal/app/service/transport/chirp_stack.go +++ b/internal/app/webhook/chirp_stack.go @@ -1,4 +1,4 @@ -package transport +package webhook import ( "encoding/base64" @@ -7,7 +7,7 @@ import ( "net/http" "time" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto" "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" diff --git a/internal/app/service/transport/chirp_stack_types.go b/internal/app/webhook/chirp_stack_types.go similarity index 99% rename from internal/app/service/transport/chirp_stack_types.go rename to internal/app/webhook/chirp_stack_types.go index a8b5bdd..55d7de5 100644 --- a/internal/app/service/transport/chirp_stack_types.go +++ b/internal/app/webhook/chirp_stack_types.go @@ -1,4 +1,4 @@ -package transport +package webhook import ( "encoding/json" diff --git a/internal/app/service/transport/transport.go b/internal/app/webhook/transport.go similarity index 89% rename from internal/app/service/transport/transport.go rename to internal/app/webhook/transport.go index b60fd29..7583c06 100644 --- a/internal/app/service/transport/transport.go +++ b/internal/app/webhook/transport.go @@ -1,4 +1,4 @@ -package transport +package webhook import "net/http" diff --git a/internal/core/application.go b/internal/core/application.go index 0c40657..37dad89 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -8,11 +8,11 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/app/api" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/audit" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/device" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/transport" + "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/database" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -95,7 +95,7 @@ func NewApplication(configPath string) (*Application, error) { auditService := audit.NewService(userActionLogRepo, logger) // 初始化设备上行监听器 - listenHandler := transport.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo) + listenHandler := webhook.NewChirpStackListener(logger, sensorDataRepo, deviceRepo, areaControllerRepo, deviceCommandLogRepo, pendingCollectionRepo) // 初始化计划触发器管理器 analysisPlanTaskManager := task.NewAnalysisPlanTaskManager(planRepo, pendingTaskRepo, executionLogRepo, logger) diff --git a/internal/app/service/audit/service.go b/internal/domain/audit/service.go similarity index 100% rename from internal/app/service/audit/service.go rename to internal/domain/audit/service.go diff --git a/internal/app/service/device/device_service.go b/internal/domain/device/device_service.go similarity index 100% rename from internal/app/service/device/device_service.go rename to internal/domain/device/device_service.go diff --git a/internal/app/service/device/general_device_service.go b/internal/domain/device/general_device_service.go similarity index 99% rename from internal/app/service/device/general_device_service.go rename to internal/domain/device/general_device_service.go index 3ea8069..d838476 100644 --- a/internal/app/service/device/general_device_service.go +++ b/internal/domain/device/general_device_service.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/device/proto" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/device/proto" "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" diff --git a/internal/app/service/device/proto/device.pb.go b/internal/domain/device/proto/device.pb.go similarity index 99% rename from internal/app/service/device/proto/device.pb.go rename to internal/domain/device/proto/device.pb.go index 573d4ff..712f749 100644 --- a/internal/app/service/device/proto/device.pb.go +++ b/internal/domain/device/proto/device.pb.go @@ -354,7 +354,7 @@ const file_device_proto_rawDesc = "" + "\n" + "MethodType\x12\x0f\n" + "\vINSTRUCTION\x10\x00\x12\v\n" + - "\aCOLLECT\x10\x01B#Z!internal/app/service/device/protob\x06proto3" + "\aCOLLECT\x10\x01B\x1eZ\x1cinternal/domain/device/protob\x06proto3" var ( file_device_proto_rawDescOnce sync.Once diff --git a/internal/app/service/device/proto/device.proto b/internal/domain/device/proto/device.proto similarity index 96% rename from internal/app/service/device/proto/device.proto rename to internal/domain/device/proto/device.proto index af23a51..a906255 100644 --- a/internal/app/service/device/proto/device.proto +++ b/internal/domain/device/proto/device.proto @@ -4,7 +4,7 @@ package device; import "google/protobuf/any.proto"; -option go_package = "internal/app/service/device/proto"; +option go_package = "internal/domain/device/proto"; // --- 通用指令结构 --- diff --git a/internal/app/service/task/analysis_plan_task_manager.go b/internal/domain/task/analysis_plan_task_manager.go similarity index 100% rename from internal/app/service/task/analysis_plan_task_manager.go rename to internal/domain/task/analysis_plan_task_manager.go diff --git a/internal/app/service/task/delay_task.go b/internal/domain/task/delay_task.go similarity index 100% rename from internal/app/service/task/delay_task.go rename to internal/domain/task/delay_task.go diff --git a/internal/app/service/task/delay_task_test.go b/internal/domain/task/delay_task_test.go similarity index 100% rename from internal/app/service/task/delay_task_test.go rename to internal/domain/task/delay_task_test.go diff --git a/internal/app/service/task/release_feed_weight_task.go b/internal/domain/task/release_feed_weight_task.go similarity index 98% rename from internal/app/service/task/release_feed_weight_task.go rename to internal/domain/task/release_feed_weight_task.go index f0c2fd4..a8c7d15 100644 --- a/internal/app/service/task/release_feed_weight_task.go +++ b/internal/domain/task/release_feed_weight_task.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/device" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" "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" diff --git a/internal/app/service/task/scheduler.go b/internal/domain/task/scheduler.go similarity index 99% rename from internal/app/service/task/scheduler.go rename to internal/domain/task/scheduler.go index 6b190f3..ebb8ca6 100644 --- a/internal/app/service/task/scheduler.go +++ b/internal/domain/task/scheduler.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "git.huangwc.com/pig/pig-farm-controller/internal/app/service/device" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" "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" diff --git a/internal/app/service/task/task.go b/internal/domain/task/task.go similarity index 100% rename from internal/app/service/task/task.go rename to internal/domain/task/task.go diff --git a/internal/app/service/token/token_service.go b/internal/domain/token/token_service.go similarity index 100% rename from internal/app/service/token/token_service.go rename to internal/domain/token/token_service.go diff --git a/internal/app/service/token/token_service_test.go b/internal/domain/token/token_service_test.go similarity index 100% rename from internal/app/service/token/token_service_test.go rename to internal/domain/token/token_service_test.go From 609aee25137a2cef20ad8a4e819a90df460a66d6 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 16:56:03 +0800 Subject: [PATCH 04/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=AF=B9=E5=BA=94model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/farm_asset.go | 39 ++++++++++++++++ internal/infra/models/medication.go | 23 ++++++++++ internal/infra/models/pig.go | 71 ++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 internal/infra/models/farm_asset.go diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go new file mode 100644 index 0000000..90266ed --- /dev/null +++ b/internal/infra/models/farm_asset.go @@ -0,0 +1,39 @@ +package models + +import ( + "gorm.io/gorm" +) + +/* + 猪场固定资产相关模型 +*/ + +// PigHouse 定义了猪舍,是猪栏的集合 +type PigHouse struct { + gorm.Model + Name string `gorm:"size:100;not null;unique;comment:猪舍名称, 如 '育肥舍A栋'"` + Description string `gorm:"size:255;comment:描述信息"` + Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏 +} + +// PenStatus 定义了猪栏的当前状态 +type PenStatus string + +const ( + PenStatusEmpty PenStatus = "空闲" + PenStatusOccupied PenStatus = "占用" + PenStatusSickPen PenStatus = "病猪栏" + PenStatusRecovering PenStatus = "康复栏" + PenStatusCleaning PenStatus = "清洗消毒" + PenStatusUnderMaint PenStatus = "维修中" +) + +// Pen 是猪栏的物理实体模型, 是所有空间相关数据的“锚点” +type Pen struct { + gorm.Model + PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"` + HouseID uint `gorm:"index;comment:所属猪舍ID"` + PigBatchID uint `gorm:"index;comment:关联的猪批次ID"` + Capacity int `gorm:"not null;comment:设计容量 (头)"` + Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` +} diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index 05dcbce..aa914b6 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -66,3 +66,26 @@ type Medication struct { Manufacturer string `gorm:"size:100;comment:生产厂家" json:"manufacturer"` Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"` } + +// MedicationReasonType 定义了用药原因 +type MedicationReasonType string + +const ( + ReasonTypePreventive MedicationReasonType = "预防" + ReasonTypeTreatment MedicationReasonType = "治疗" + ReasonTypeHealthCare MedicationReasonType = "保健" +) + +// GroupMedicationLog 记录了对整个猪批次的用药情况 +type GroupMedicationLog struct { + gorm.Model + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` + MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` + Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息 + DosageUsed float64 `gorm:"not null;comment:使用的总剂量 (单位由药品决定,如g或ml)"` + TargetCount int `gorm:"not null;comment:用药对象数量"` + Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"` + Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` + Operator string `gorm:"size:50;comment:操作员"` + HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:用药时间"` +} diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 1579859..caf51a0 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -1,5 +1,74 @@ package models +import ( + "time" + + "gorm.io/gorm" +) + /* - 和猪只本身相关的模型 + 和猪只、猪群本身相关的模型 */ + +// PigBatchStatus 定义了猪批次所处的不同阶段或状态 +type PigBatchStatus string + +const ( + BatchStatusWeaning PigBatchStatus = "保育" // 从断奶到保育结束 + BatchStatusGrowing PigBatchStatus = "生长" // 生长育肥阶段 + BatchStatusFinishing PigBatchStatus = "育肥" // 最后的育肥阶段 + BatchStatusForSale PigBatchStatus = "待售" // 达到出栏标准 + BatchStatusSold PigBatchStatus = "已出售" + BatchStatusArchived PigBatchStatus = "已归档" // 批次结束(如全群淘汰等) +) + +// PigBatchOriginType 定义了猪批次的来源 +type PigBatchOriginType string + +const ( + OriginTypeSelfFarrowed PigBatchOriginType = "自繁" + OriginTypePurchased PigBatchOriginType = "外购" +) + +// PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪 +type PigBatch struct { + gorm.Model + BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"` + OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"` + StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"` + InitialCount int `gorm:"not null;comment:初始数量"` + CurrentCount int `gorm:"not null;comment:当前存栏数量"` + CurrentSickCount int `gorm:"not null;default:0;comment:当前病猪数量"` + AverageWeight float64 `gorm:"comment:平均体重 (kg)"` + Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"` + Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"` +} + +// LogChangeType 定义了猪批次数量变更的类型 +type LogChangeType string + +const ( + ChangeTypeDeath LogChangeType = "死亡" + ChangeTypeCull LogChangeType = "淘汰" + ChangeTypeSale LogChangeType = "销售" + ChangeTypeTransferIn LogChangeType = "转入" + ChangeTypeTransferOut LogChangeType = "转出" + ChangeTypeTreatment LogChangeType = "治疗" // 仅改变健康状态,不改变总数 + ChangeTypeRecovering LogChangeType = "康复" // 仅改变健康状态,不改变总数 + ChangeTypeCorrection LogChangeType = "盘点校正" +) + +// PigBatchLog 记录了猪批次数量或状态的每一次变更 +type PigBatchLog struct { + gorm.Model + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` + ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` + ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` + Reason string `gorm:"size:255;comment:变更原因描述"` + BeforeCount int `gorm:"not null;comment:变更前总数"` + AfterCount int `gorm:"not null;comment:变更后总数"` + BeforeSickCount int `gorm:"not null;comment:变更前病猪数"` + AfterSickCount int `gorm:"not null;comment:变更后病猪数"` + Operator string `gorm:"size:50;comment:操作员"` + HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:事件发生时间"` +} From 5754a1d94c90c82daa02ce50ade46916d56c9356 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 17:23:44 +0800 Subject: [PATCH 05/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=AF=B9=E5=BA=94model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/feed.go | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go index f0a5807..cf0a188 100644 --- a/internal/infra/models/feed.go +++ b/internal/infra/models/feed.go @@ -1,5 +1,88 @@ package models +import ( + "time" + + "gorm.io/gorm" +) + /* 饲料和饲喂相关的模型 */ + +// RawMaterial 代表饲料的原料。 +// 建议:所有重量单位统一存储 (例如, 全部使用 'g'),便于计算和避免转换错误。 +type RawMaterial struct { + gorm.Model + Name string `gorm:"size:100;unique;not null;comment:原料名称"` + Description string `gorm:"size:255;comment:描述"` + Quantity float64 `gorm:"not null;comment:库存总量, 单位: g"` +} + +// RawMaterialPurchase 记录了原料的每一次采购。 +type RawMaterialPurchase struct { + gorm.Model + RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` + RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` + Supplier string `gorm:"size:100;comment:供应商"` + Amount float64 `gorm:"not null;comment:采购数量, 单位: g"` + UnitPrice float64 `gorm:"comment:单价"` + TotalPrice float64 `gorm:"comment:总价"` + PurchaseDate time.Time `gorm:"not null;comment:采购日期"` +} + +// StockLogSourceType 定义了库存日志来源的类型 +type StockLogSourceType string + +const ( + StockLogSourcePurchase StockLogSourceType = "采购入库" + StockLogSourceFeeding StockLogSourceType = "饲喂出库" + StockLogSourceDeteriorate StockLogSourceType = "变质出库" + StockLogSourceSale StockLogSourceType = "售卖出库" + StockLogSourceMiscellaneous StockLogSourceType = "杂用领取" + StockLogSourceManual StockLogSourceType = "手动盘点" +) + +// RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。 +type RawMaterialStockLog struct { + gorm.Model + RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` + ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"` + SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"` + SourceID uint `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"` + HappenedAt time.Time `gorm:"not null;comment:业务发生时间"` + Remarks string `gorm:"comment:备注, 如主动领取的理由等"` +} + +// FeedFormula 代表饲料配方。 +// 对于没有配方的外购饲料,可以将其视为一种特殊的 RawMaterial, 并为其创建一个仅包含它自己的 FeedFormula。 +type FeedFormula struct { + gorm.Model + Name string `gorm:"size:100;unique;not null;comment:配方名称"` + Description string `gorm:"size:255;comment:描述"` + Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"` +} + +// FeedFormulaComponent 代表配方中的一种原料及其占比。 +type FeedFormulaComponent struct { + gorm.Model + FeedFormulaID uint `gorm:"not null;index;comment:外键到 FeedFormula"` + RawMaterialID uint `gorm:"not null;index;comment:外键到 RawMaterial"` + RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` + Percentage float64 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"` +} + +// FeedUsageRecord 代表饲料使用记录。 +// 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula, +// 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。 +type FeedUsageRecord struct { + gorm.Model + PenID uint `gorm:"not null;index;comment:关联的猪栏ID"` + Pen Pen `gorm:"foreignKey:PenID"` + FeedFormulaID uint `gorm:"not null;index;comment:使用的饲料配方ID"` + FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"` + Amount float64 `gorm:"not null;comment:使用数量, 单位: g"` + RecordedAt time.Time `gorm:"not null;comment:记录时间"` + OperatorID uint `gorm:"not null;comment:操作员"` + Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"` +} From 8cbe313c89a3e6f4406ae98690fbdf234c1dcf33 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 18:27:53 +0800 Subject: [PATCH 06/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20=E7=8C=AA=E8=88=8D?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=E7=BB=84=20=E5=92=8C=20?= =?UTF-8?q?=E7=8C=AA=E5=9C=88=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/api.go | 29 ++ .../controller/management/feed_controller.go | 1 + .../management/medication_controller.go | 1 + .../controller/management/pig_controller.go | 1 + .../management/pig_farm_controller.go | 426 ++++++++++++++++++ internal/app/service/pig_farm_service.go | 116 +++++ internal/core/application.go | 28 +- internal/infra/database/postgres.go | 18 +- internal/infra/models/feed.go | 45 +- internal/infra/models/medication.go | 15 +- internal/infra/models/models.go | 35 +- internal/infra/models/pig.go | 15 +- .../infra/repository/pig_farm_repository.go | 157 +++++++ 13 files changed, 840 insertions(+), 47 deletions(-) create mode 100644 internal/app/controller/management/feed_controller.go create mode 100644 internal/app/controller/management/medication_controller.go create mode 100644 internal/app/controller/management/pig_controller.go create mode 100644 internal/app/controller/management/pig_farm_controller.go create mode 100644 internal/app/service/pig_farm_service.go create mode 100644 internal/infra/repository/pig_farm_repository.go diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 57106bd..8af8009 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -17,9 +17,11 @@ import ( _ "git.huangwc.com/pig/pig-farm-controller/docs" // 引入 swag 生成的 docs "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device" + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/management" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" "git.huangwc.com/pig/pig-farm-controller/internal/app/middleware" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" @@ -45,6 +47,7 @@ type API struct { userController *user.Controller // 用户控制器实例 deviceController *device.Controller // 设备控制器实例 planController *plan.Controller // 计划控制器实例 + pigFarmController *management.PigFarmController // 猪场管理控制器实例 listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -58,6 +61,7 @@ func NewAPI(cfg config.ServerConfig, areaControllerRepository repository.AreaControllerRepository, deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 planRepository repository.PlanRepository, + pigFarmService service.PigFarmService, userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 @@ -90,6 +94,8 @@ func NewAPI(cfg config.ServerConfig, deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 planController: plan.NewController(logger, planRepository, analysisTaskManager), + // 在 NewAPI 中初始化猪场管理控制器 + pigFarmController: management.NewPigFarmController(logger, pigFarmService), } api.setupRoutes() // 设置所有路由 @@ -192,6 +198,29 @@ func (a *API) setupRoutes() { planGroup.POST("/:id/stop", a.planController.StopPlan) } a.logger.Info("计划相关接口注册成功 (需要认证和审计)") + + // 猪舍相关路由组 + pigHouseGroup := authGroup.Group("/pighouses") + { + pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse) + pigHouseGroup.GET("", a.pigFarmController.ListPigHouses) + pigHouseGroup.GET("/:id", a.pigFarmController.GetPigHouse) + pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) + pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) + } + a.logger.Info("猪舍相关接口注册成功 (需要认证和审计)") + + // 猪圈相关路由组 + penGroup := authGroup.Group("/pens") + { + penGroup.POST("", a.pigFarmController.CreatePen) + penGroup.GET("", a.pigFarmController.ListPens) + penGroup.GET("/:id", a.pigFarmController.GetPen) + penGroup.PUT("/:id", a.pigFarmController.UpdatePen) + penGroup.DELETE("/:id", a.pigFarmController.DeletePen) + } + a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") + } } diff --git a/internal/app/controller/management/feed_controller.go b/internal/app/controller/management/feed_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/feed_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/medication_controller.go b/internal/app/controller/management/medication_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/medication_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_controller.go b/internal/app/controller/management/pig_controller.go new file mode 100644 index 0000000..bd32643 --- /dev/null +++ b/internal/app/controller/management/pig_controller.go @@ -0,0 +1 @@ +package management diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go new file mode 100644 index 0000000..e08c08c --- /dev/null +++ b/internal/app/controller/management/pig_farm_controller.go @@ -0,0 +1,426 @@ +package management + +import ( + "errors" + "strconv" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// --- 数据传输对象 (DTOs) --- + +// PigHouseResponse 定义了猪舍信息的响应结构 +type PigHouseResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// PenResponse 定义了猪栏信息的响应结构 +type PenResponse struct { + ID uint `json:"id"` + PenNumber string `json:"pen_number"` + HouseID uint `json:"house_id"` + Capacity int `json:"capacity"` + Status models.PenStatus `json:"status"` + PigBatchID uint `json:"pig_batch_id"` +} + +// CreatePigHouseRequest 定义了创建猪舍的请求结构 +type CreatePigHouseRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +// UpdatePigHouseRequest 定义了更新猪舍的请求结构 +type UpdatePigHouseRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +// CreatePenRequest 定义了创建猪栏的请求结构 +type CreatePenRequest struct { + PenNumber string `json:"pen_number" binding:"required"` + HouseID uint `json:"house_id" binding:"required"` + Capacity int `json:"capacity" binding:"required"` + Status models.PenStatus `json:"status" binding:"required"` +} + +// UpdatePenRequest 定义了更新猪栏的请求结构 +type UpdatePenRequest struct { + PenNumber string `json:"pen_number" binding:"required"` + HouseID uint `json:"house_id" binding:"required"` + Capacity int `json:"capacity" binding:"required"` + Status models.PenStatus `json:"status" binding:"required"` +} + +// --- 控制器定义 --- + +// PigFarmController 负责处理猪舍和猪栏相关的API请求 +type PigFarmController struct { + logger *logs.Logger + service service.PigFarmService +} + +// NewPigFarmController 创建一个新的 PigFarmController 实例 +func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) *PigFarmController { + return &PigFarmController{ + logger: logger, + service: service, + } +} + +// --- 猪舍 (PigHouse) API 实现 --- + +// CreatePigHouse godoc +// @Summary 创建猪舍 +// @Description 创建一个新的猪舍 +// @Tags 猪场管理 +// @Accept json +// @Produce json +// @Param body body CreatePigHouseRequest true "猪舍信息" +// @Success 201 {object} controller.Response{data=PigHouseResponse} "创建成功" +// @Router /api/v1/pighouses [post] +func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { + const action = "创建猪舍" + var req CreatePigHouseRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + house, err := c.service.CreatePigHouse(req.Name, req.Description) + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req) + return + } + + resp := PigHouseResponse{ + ID: house.ID, + Name: house.Name, + Description: house.Description, + } + controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) +} + +// GetPigHouse godoc +// @Summary 获取单个猪舍 +// @Description 根据ID获取单个猪舍信息 +// @Tags 猪场管理 +// @Produce json +// @Param id path int true "猪舍ID" +// @Success 200 {object} controller.Response{data=PigHouseResponse} "获取成功" +// @Router /api/v1/pighouses/{id} [get] +func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { + const action = "获取猪舍" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + house, err := c.service.GetPigHouseByID(uint(id)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id) + return + } + + resp := PigHouseResponse{ + ID: house.ID, + Name: house.Name, + Description: house.Description, + } + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) +} + +// ListPigHouses godoc +// @Summary 获取猪舍列表 +// @Description 获取所有猪舍的列表 +// @Tags 猪场管理 +// @Produce json +// @Success 200 {object} controller.Response{data=[]PigHouseResponse} "获取成功" +// @Router /api/v1/pighouses [get] +func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { + const action = "获取猪舍列表" + houses, err := c.service.ListPigHouses() + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil) + return + } + + var resp []PigHouseResponse + for _, house := range houses { + resp = append(resp, PigHouseResponse{ + ID: house.ID, + Name: house.Name, + Description: house.Description, + }) + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) +} + +// UpdatePigHouse godoc +// @Summary 更新猪舍 +// @Description 更新一个已存在的猪舍信息 +// @Tags 猪场管理 +// @Accept json +// @Produce json +// @Param id path int true "猪舍ID" +// @Param body body UpdatePigHouseRequest true "猪舍信息" +// @Success 200 {object} controller.Response{data=PigHouseResponse} "更新成功" +// @Router /api/v1/pighouses/{id} [put] +func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { + const action = "更新猪舍" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req UpdatePigHouseRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) + return + } + + resp := PigHouseResponse{ + ID: house.ID, + Name: house.Name, + Description: house.Description, + } + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) +} + +// DeletePigHouse godoc +// @Summary 删除猪舍 +// @Description 根据ID删除一个猪舍 +// @Tags 猪场管理 +// @Produce json +// @Param id path int true "猪舍ID" +// @Success 200 {object} controller.Response "删除成功" +// @Router /api/v1/pighouses/{id} [delete] +func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) { + const action = "删除猪舍" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + if err := c.service.DeletePigHouse(uint(id)); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id) +} + +// --- 猪栏 (Pen) API 实现 --- + +// CreatePen godoc +// @Summary 创建猪栏 +// @Description 创建一个新的猪栏 +// @Tags 猪场管理 +// @Accept json +// @Produce json +// @Param body body CreatePenRequest true "猪栏信息" +// @Success 201 {object} controller.Response{data=PenResponse} "创建成功" +// @Router /api/v1/pens [post] +func (c *PigFarmController) CreatePen(ctx *gin.Context) { + const action = "创建猪栏" + var req CreatePenRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity, req.Status) + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req) + return + } + + resp := PenResponse{ + ID: pen.ID, + PenNumber: pen.PenNumber, + HouseID: pen.HouseID, + Capacity: pen.Capacity, + Status: pen.Status, + PigBatchID: pen.PigBatchID, + } + controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) +} + +// GetPen godoc +// @Summary 获取单个猪栏 +// @Description 根据ID获取单个猪栏信息 +// @Tags 猪场管理 +// @Produce json +// @Param id path int true "猪栏ID" +// @Success 200 {object} controller.Response{data=PenResponse} "获取成功" +// @Router /api/v1/pens/{id} [get] +func (c *PigFarmController) GetPen(ctx *gin.Context) { + const action = "获取猪栏" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + pen, err := c.service.GetPenByID(uint(id)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪栏失败", action, "业务逻辑失败", id) + return + } + + resp := PenResponse{ + ID: pen.ID, + PenNumber: pen.PenNumber, + HouseID: pen.HouseID, + Capacity: pen.Capacity, + Status: pen.Status, + PigBatchID: pen.PigBatchID, + } + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) +} + +// ListPens godoc +// @Summary 获取猪栏列表 +// @Description 获取所有猪栏的列表 +// @Tags 猪场管理 +// @Produce json +// @Success 200 {object} controller.Response{data=[]PenResponse} "获取成功" +// @Router /api/v1/pens [get] +func (c *PigFarmController) ListPens(ctx *gin.Context) { + const action = "获取猪栏列表" + pens, err := c.service.ListPens() + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil) + return + } + + var resp []PenResponse + for _, pen := range pens { + resp = append(resp, PenResponse{ + ID: pen.ID, + PenNumber: pen.PenNumber, + HouseID: pen.HouseID, + Capacity: pen.Capacity, + Status: pen.Status, + PigBatchID: pen.PigBatchID, + }) + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) +} + +// UpdatePen godoc +// @Summary 更新猪栏 +// @Description 更新一个已存在的猪栏信息 +// @Tags 猪场管理 +// @Accept json +// @Produce json +// @Param id path int true "猪栏ID" +// @Param body body UpdatePenRequest true "猪栏信息" +// @Success 200 {object} controller.Response{data=PenResponse} "更新成功" +// @Router /api/v1/pens/{id} [put] +func (c *PigFarmController) UpdatePen(ctx *gin.Context) { + const action = "更新猪栏" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req UpdatePenRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) + return + } + + resp := PenResponse{ + ID: pen.ID, + PenNumber: pen.PenNumber, + HouseID: pen.HouseID, + Capacity: pen.Capacity, + Status: pen.Status, + PigBatchID: pen.PigBatchID, + } + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) +} + +// DeletePen godoc +// @Summary 删除猪栏 +// @Description 根据ID删除一个猪栏 +// @Tags 猪场管理 +// @Produce json +// @Param id path int true "猪栏ID" +// @Success 200 {object} controller.Response "删除成功" +// @Router /api/v1/pens/{id} [delete] +func (c *PigFarmController) DeletePen(ctx *gin.Context) { + const action = "删除猪栏" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + if err := c.service.DeletePen(uint(id)); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id) +} diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go new file mode 100644 index 0000000..5912360 --- /dev/null +++ b/internal/app/service/pig_farm_service.go @@ -0,0 +1,116 @@ +package service + +import ( + "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" +) + +// PigFarmService 提供了猪场资产管理的业务逻辑 +type PigFarmService interface { + // PigHouse methods + CreatePigHouse(name, description string) (*models.PigHouse, error) + GetPigHouseByID(id uint) (*models.PigHouse, error) + ListPigHouses() ([]models.PigHouse, error) + UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) + DeletePigHouse(id uint) error + + // Pen methods + CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) + GetPenByID(id uint) (*models.Pen, error) + ListPens() ([]models.Pen, error) + UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) + DeletePen(id uint) error +} + +type pigFarmService struct { + logger *logs.Logger + repo repository.PigFarmRepository +} + +// NewPigFarmService 创建一个新的 PigFarmService 实例 +func NewPigFarmService(repo repository.PigFarmRepository, logger *logs.Logger) PigFarmService { + return &pigFarmService{ + logger: logger, + repo: repo, + } +} + +// --- PigHouse Implementation --- + +func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) { + house := &models.PigHouse{ + Name: name, + Description: description, + } + err := s.repo.CreatePigHouse(house) + return house, err +} + +func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) { + return s.repo.GetPigHouseByID(id) +} + +func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) { + return s.repo.ListPigHouses() +} + +func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) { + house := &models.PigHouse{ + Model: gorm.Model{ID: id}, + Name: name, + Description: description, + } + err := s.repo.UpdatePigHouse(house) + if err != nil { + return nil, err + } + // 返回更新后的完整信息 + return s.repo.GetPigHouseByID(id) +} + +func (s *pigFarmService) DeletePigHouse(id uint) error { + return s.repo.DeletePigHouse(id) +} + +// --- Pen Implementation --- + +func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + pen := &models.Pen{ + PenNumber: penNumber, + HouseID: houseID, + Capacity: capacity, + Status: status, + } + err := s.repo.CreatePen(pen) + return pen, err +} + +func (s *pigFarmService) GetPenByID(id uint) (*models.Pen, error) { + return s.repo.GetPenByID(id) +} + +func (s *pigFarmService) ListPens() ([]models.Pen, error) { + return s.repo.ListPens() +} + +func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + pen := &models.Pen{ + Model: gorm.Model{ID: id}, + PenNumber: penNumber, + HouseID: houseID, + Capacity: capacity, + Status: status, + } + err := s.repo.UpdatePen(pen) + if err != nil { + return nil, err + } + // 返回更新后的完整信息 + return s.repo.GetPenByID(id) +} + +func (s *pigFarmService) DeletePen(id uint) error { + return s.repo.DeletePen(id) +} diff --git a/internal/core/application.go b/internal/core/application.go index 37dad89..427279f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -8,6 +8,7 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/app/api" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" @@ -58,39 +59,23 @@ func NewApplication(configPath string) (*Application, error) { // 初始化 Token 服务 tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret)) - // 初始化用户仓库 + // --- 仓库对象初始化 --- userRepo := repository.NewGormUserRepository(storage.GetDB()) - - // 初始化设备仓库 deviceRepo := repository.NewGormDeviceRepository(storage.GetDB()) - - // 初始化区域主控仓库 areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB()) - - // 初始化设备模板仓库 deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB()) - - // 初始化计划仓库 planRepo := repository.NewGormPlanRepository(storage.GetDB()) - - // 初始化待执行任务仓库 + pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB()) - - // 初始化执行日志仓库 executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB()) - - // 初始化传感器数据仓库 sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB()) - - // 初始化命令下发历史仓库 deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB()) - - // 初始化待采集请求仓库 pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) - - // 初始化审计日志仓库 userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) + // --- 业务逻辑处理器初始化 --- + pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) + // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) @@ -135,6 +120,7 @@ func NewApplication(configPath string) (*Application, error) { areaControllerRepo, deviceTemplateRepo, planRepo, + pigFarmService, userActionLogRepo, tokenService, auditService, diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 06ad895..5cb9ef6 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -160,11 +160,16 @@ func (ps *PostgresStorage) creatingHyperTable() 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.GroupMedicationLog{}, "happened_at"}, + {models.PigBatchLog{}, "happened_at"}, } for _, table := range tablesToConvert { tableName := table.model.TableName() - chunkInterval := "1 day" // 统一设置为1天 + chunkInterval := "7 days" // 统一设置为7天 ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval) sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval) if err := ps.db.Exec(sql).Error; err != nil { @@ -193,7 +198,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { for _, policy := range policies { tableName := policy.model.TableName() - compressAfter := "3 days" // 统一设置为2天后(即进入第3天)开始压缩 + compressAfter := "15 days" // 统一设置为15天后开始压缩 // 1. 开启表的压缩设置,并指定分段列 ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn) @@ -239,14 +244,5 @@ func (ps *PostgresStorage) creatingIndex() error { } ps.logger.Info("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)") - // 为 devices 表的 properties 字段创建 GIN 索引 - //ps.logger.Info("正在为 devices 表的 properties 字段创建 GIN 索引") - //ginDevicePropertiesIndexSQL := "CREATE INDEX IF NOT EXISTS idx_devices_properties_gin ON devices USING GIN (properties);" - //if err := ps.db.Exec(ginDevicePropertiesIndexSQL).Error; err != nil { - // ps.logger.Errorw("为 devices 的 properties 字段创建 GIN 索引失败", "error", err) - // return fmt.Errorf("为 devices 的 properties 字段创建 GIN 索引失败: %w", err) - //} - //ps.logger.Info("成功为 devices 的 properties 字段创建 GIN 索引 (或已存在)") - return nil } diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go index cf0a188..5a51699 100644 --- a/internal/infra/models/feed.go +++ b/internal/infra/models/feed.go @@ -19,16 +19,27 @@ type RawMaterial struct { Quantity float64 `gorm:"not null;comment:库存总量, 单位: g"` } +func (RawMaterial) TableName() string { + return "raw_materials" +} + // RawMaterialPurchase 记录了原料的每一次采购。 type RawMaterialPurchase struct { - gorm.Model + ID uint `gorm:"primaryKey"` RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` Supplier string `gorm:"size:100;comment:供应商"` Amount float64 `gorm:"not null;comment:采购数量, 单位: g"` UnitPrice float64 `gorm:"comment:单价"` TotalPrice float64 `gorm:"comment:总价"` - PurchaseDate time.Time `gorm:"not null;comment:采购日期"` + PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (RawMaterialPurchase) TableName() string { + return "raw_material_purchases" } // StockLogSourceType 定义了库存日志来源的类型 @@ -45,13 +56,20 @@ const ( // RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。 type RawMaterialStockLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"` SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"` SourceID uint `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"` - HappenedAt time.Time `gorm:"not null;comment:业务发生时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"` Remarks string `gorm:"comment:备注, 如主动领取的理由等"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (RawMaterialStockLog) TableName() string { + return "raw_material_stock_logs" } // FeedFormula 代表饲料配方。 @@ -63,6 +81,10 @@ type FeedFormula struct { Components []FeedFormulaComponent `gorm:"foreignKey:FeedFormulaID"` } +func (FeedFormula) TableName() string { + return "feed_formulas" +} + // FeedFormulaComponent 代表配方中的一种原料及其占比。 type FeedFormulaComponent struct { gorm.Model @@ -72,17 +94,28 @@ type FeedFormulaComponent struct { Percentage float64 `gorm:"not null;comment:该原料在配方中的百分比 (0-1.0)"` } +func (FeedFormulaComponent) TableName() string { + return "feed_formula_components" +} + // FeedUsageRecord 代表饲料使用记录。 // 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula, // 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。 type FeedUsageRecord struct { - gorm.Model + ID uint `gorm:"primaryKey"` PenID uint `gorm:"not null;index;comment:关联的猪栏ID"` Pen Pen `gorm:"foreignKey:PenID"` FeedFormulaID uint `gorm:"not null;index;comment:使用的饲料配方ID"` FeedFormula FeedFormula `gorm:"foreignKey:FeedFormulaID"` Amount float64 `gorm:"not null;comment:使用数量, 单位: g"` - RecordedAt time.Time `gorm:"not null;comment:记录时间"` + RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"` OperatorID uint `gorm:"not null;comment:操作员"` Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (FeedUsageRecord) TableName() string { + return "feed_usage_records" } diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index aa914b6..395ecdf 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -67,6 +67,10 @@ type Medication struct { Instructions datatypes.JSON `gorm:"type:jsonb;comment:使用说明" json:"instructions"` } +func (Medication) TableName() string { + return "medications" +} + // MedicationReasonType 定义了用药原因 type MedicationReasonType string @@ -78,7 +82,7 @@ const ( // GroupMedicationLog 记录了对整个猪批次的用药情况 type GroupMedicationLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息 @@ -87,5 +91,12 @@ type GroupMedicationLog struct { Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"` Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` Operator string `gorm:"size:50;comment:操作员"` - HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:用药时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (GroupMedicationLog) TableName() string { + return "group_medication_logs" } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 8f21ca5..a2f9b07 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -12,20 +12,45 @@ import ( // 这个函数用于在数据库初始化时自动迁移所有的表结构。 func GetAllModels() []interface{} { return []interface{}{ + // Core Models &User{}, + &UserActionLog{}, + + // Device Models &Device{}, + &AreaController{}, + &DeviceTemplate{}, + &SensorData{}, + &DeviceCommandLog{}, + + // Plan & Task Models &Plan{}, &SubPlan{}, &Task{}, &PlanExecutionLog{}, &TaskExecutionLog{}, &PendingTask{}, - &SensorData{}, - &DeviceCommandLog{}, &PendingCollection{}, - &AreaController{}, - &DeviceTemplate{}, - &UserActionLog{}, + + // Farm Asset Models + &PigHouse{}, + &Pen{}, + + // Pig & Batch Models + &PigBatch{}, + &PigBatchLog{}, + + // Feed Models + &RawMaterial{}, + &RawMaterialPurchase{}, + &RawMaterialStockLog{}, + &FeedFormula{}, + &FeedFormulaComponent{}, + &FeedUsageRecord{}, + + // Medication Models + &Medication{}, + &GroupMedicationLog{}, } } diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index caf51a0..0ee4e76 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -44,6 +44,10 @@ type PigBatch struct { Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"` } +func (PigBatch) TableName() string { + return "pig_batches" +} + // LogChangeType 定义了猪批次数量变更的类型 type LogChangeType string @@ -60,7 +64,7 @@ const ( // PigBatchLog 记录了猪批次数量或状态的每一次变更 type PigBatchLog struct { - gorm.Model + ID uint `gorm:"primaryKey"` PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` @@ -70,5 +74,12 @@ type PigBatchLog struct { BeforeSickCount int `gorm:"not null;comment:变更前病猪数"` AfterSickCount int `gorm:"not null;comment:变更后病猪数"` Operator string `gorm:"size:50;comment:操作员"` - HappenedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP;comment:事件发生时间"` + HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (PigBatchLog) TableName() string { + return "pig_batch_logs" } diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go new file mode 100644 index 0000000..5341594 --- /dev/null +++ b/internal/infra/repository/pig_farm_repository.go @@ -0,0 +1,157 @@ +package repository + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +var ( + ErrHouseContainsPens = errors.New("cannot delete a pig house that still contains pens") + ErrHouseNotFound = errors.New("the specified pig house does not exist") +) + +// PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口 +type PigFarmRepository interface { + // PigHouse methods + CreatePigHouse(house *models.PigHouse) error + GetPigHouseByID(id uint) (*models.PigHouse, error) + ListPigHouses() ([]models.PigHouse, error) + UpdatePigHouse(house *models.PigHouse) error + DeletePigHouse(id uint) error + + // Pen methods + CreatePen(pen *models.Pen) error + GetPenByID(id uint) (*models.Pen, error) + ListPens() ([]models.Pen, error) + UpdatePen(pen *models.Pen) error + DeletePen(id uint) error +} + +// gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 +type gormPigFarmRepository struct { + db *gorm.DB +} + +// NewGormPigFarmRepository 创建一个新的 PigFarmRepository GORM 实现实例 +func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository { + return &gormPigFarmRepository{db: db} +} + +// --- PigHouse Implementation --- + +func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error { + return r.db.Create(house).Error +} + +func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) { + var house models.PigHouse + if err := r.db.First(&house, id).Error; err != nil { + return nil, err + } + return &house, nil +} + +func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { + var houses []models.PigHouse + if err := r.db.Find(&houses).Error; err != nil { + return nil, err + } + return houses, nil +} + +func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error { + result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var penCount int64 + if err := tx.Model(&models.Pen{}).Where("house_id = ?", id).Count(&penCount).Error; err != nil { + return err + } + if penCount > 0 { + return ErrHouseContainsPens + } + + result := tx.Delete(&models.PigHouse{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) +} + +// --- Pen Implementation --- + +func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // 验证所属猪舍是否存在 + if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrHouseNotFound + } + return err + } + return tx.Create(pen).Error + }) +} + +func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { + var pen models.Pen + if err := r.db.First(&pen, id).Error; err != nil { + return nil, err + } + return &pen, nil +} + +func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { + var pens []models.Pen + if err := r.db.Find(&pens).Error; err != nil { + return nil, err + } + return pens, nil +} + +func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // 验证所属猪舍是否存在 + if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrHouseNotFound + } + return err + } + + result := tx.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) +} + +func (r *gormPigFarmRepository) DeletePen(id uint) error { + result := r.db.Delete(&models.Pen{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} From 6cc6d719e1c3b4b36cf9c6b7a46e05f4889aa0f1 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 20:32:34 +0800 Subject: [PATCH 07/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig.go | 1 + internal/infra/database/postgres.go | 13 ++++++++++-- internal/infra/models/feed.go | 14 +++--------- internal/infra/models/medication.go | 5 +---- internal/infra/models/models.go | 2 ++ internal/infra/models/pig.go | 33 ++++++++++++++++++++++++----- 6 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 internal/app/service/pig.go diff --git a/internal/app/service/pig.go b/internal/app/service/pig.go new file mode 100644 index 0000000..6d43c33 --- /dev/null +++ b/internal/app/service/pig.go @@ -0,0 +1 @@ +package service diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 5cb9ef6..04058ff 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -165,11 +165,13 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.FeedUsageRecord{}, "recorded_at"}, {models.GroupMedicationLog{}, "happened_at"}, {models.PigBatchLog{}, "happened_at"}, + {models.WeighingBatch{}, "weighing_time"}, + {models.WeighingRecord{}, "weighing_time"}, } for _, table := range tablesToConvert { tableName := table.model.TableName() - chunkInterval := "7 days" // 统一设置为7天 + chunkInterval := "1 days" // 统一设置为1天 ps.logger.Infow("准备将表转换为超表", "table", tableName, "chunk_interval", chunkInterval) sql := fmt.Sprintf("SELECT create_hypertable('%s', '%s', chunk_time_interval => INTERVAL '%s', if_not_exists => TRUE);", tableName, table.timeColumn, chunkInterval) if err := ps.db.Exec(sql).Error; err != nil { @@ -194,11 +196,18 @@ func (ps *PostgresStorage) applyCompressionPolicies() 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.GroupMedicationLog{}, "pig_batch_id"}, + {models.PigBatchLog{}, "pig_batch_id"}, + {models.WeighingBatch{}, "pig_batch_id"}, + {models.WeighingRecord{}, "weighing_batch_id"}, } for _, policy := range policies { tableName := policy.model.TableName() - compressAfter := "15 days" // 统一设置为15天后开始压缩 + compressAfter := "3 days" // 统一设置为2天后(即进入第3天)开始压缩 // 1. 开启表的压缩设置,并指定分段列 ps.logger.Infow("为表启用压缩设置", "table", tableName, "segment_by", policy.segmentColumn) diff --git a/internal/infra/models/feed.go b/internal/infra/models/feed.go index 5a51699..a16378f 100644 --- a/internal/infra/models/feed.go +++ b/internal/infra/models/feed.go @@ -25,7 +25,7 @@ func (RawMaterial) TableName() string { // RawMaterialPurchase 记录了原料的每一次采购。 type RawMaterialPurchase struct { - ID uint `gorm:"primaryKey"` + gorm.Model RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` RawMaterial RawMaterial `gorm:"foreignKey:RawMaterialID"` Supplier string `gorm:"size:100;comment:供应商"` @@ -34,8 +34,6 @@ type RawMaterialPurchase struct { TotalPrice float64 `gorm:"comment:总价"` PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` } func (RawMaterialPurchase) TableName() string { @@ -56,16 +54,13 @@ const ( // RawMaterialStockLog 记录了原料库存的所有变动,提供了完整的追溯链。 type RawMaterialStockLog struct { - ID uint `gorm:"primaryKey"` + gorm.Model RawMaterialID uint `gorm:"not null;index;comment:关联的原料ID"` ChangeAmount float64 `gorm:"not null;comment:变动数量, 正数为入库, 负数为出库"` SourceType StockLogSourceType `gorm:"size:50;not null;index;comment:库存变动来源类型"` SourceID uint `gorm:"not null;index;comment:来源记录的ID (如 RawMaterialPurchase.ID 或 FeedUsageRecord.ID)"` HappenedAt time.Time `gorm:"primaryKey;comment:业务发生时间"` Remarks string `gorm:"comment:备注, 如主动领取的理由等"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` } func (RawMaterialStockLog) TableName() string { @@ -102,7 +97,7 @@ func (FeedFormulaComponent) TableName() string { // 应用层逻辑:当一条使用记录被创建时,应根据其使用的 FeedFormula, // 计算出每种 RawMaterial 的消耗量,并在 RawMaterialStockLog 中创建对应的出库记录。 type FeedUsageRecord struct { - ID uint `gorm:"primaryKey"` + gorm.Model PenID uint `gorm:"not null;index;comment:关联的猪栏ID"` Pen Pen `gorm:"foreignKey:PenID"` FeedFormulaID uint `gorm:"not null;index;comment:使用的饲料配方ID"` @@ -111,9 +106,6 @@ type FeedUsageRecord struct { RecordedAt time.Time `gorm:"primaryKey;comment:记录时间"` OperatorID uint `gorm:"not null;comment:操作员"` Remarks string `gorm:"comment:备注, 如 '例行喂料, 弱猪补料' 等"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` } func (FeedUsageRecord) TableName() string { diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index 395ecdf..1f0b752 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -82,7 +82,7 @@ const ( // GroupMedicationLog 记录了对整个猪批次的用药情况 type GroupMedicationLog struct { - ID uint `gorm:"primaryKey"` + gorm.Model PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` Medication Medication `gorm:"foreignKey:MedicationID"` // 预加载药品信息 @@ -92,9 +92,6 @@ type GroupMedicationLog struct { Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` Operator string `gorm:"size:50;comment:操作员"` HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` } func (GroupMedicationLog) TableName() string { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index a2f9b07..1e6c116 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -39,6 +39,8 @@ func GetAllModels() []interface{} { // Pig & Batch Models &PigBatch{}, &PigBatchLog{}, + &WeighingBatch{}, + &WeighingRecord{}, // Feed Models &RawMaterial{}, diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 0ee4e76..4c5586f 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -39,7 +39,6 @@ type PigBatch struct { InitialCount int `gorm:"not null;comment:初始数量"` CurrentCount int `gorm:"not null;comment:当前存栏数量"` CurrentSickCount int `gorm:"not null;default:0;comment:当前病猪数量"` - AverageWeight float64 `gorm:"comment:平均体重 (kg)"` Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"` Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"` } @@ -64,7 +63,7 @@ const ( // PigBatchLog 记录了猪批次数量或状态的每一次变更 type PigBatchLog struct { - ID uint `gorm:"primaryKey"` + gorm.Model PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` @@ -75,11 +74,35 @@ type PigBatchLog struct { AfterSickCount int `gorm:"not null;comment:变更后病猪数"` Operator string `gorm:"size:50;comment:操作员"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` } func (PigBatchLog) TableName() string { return "pig_batch_logs" } + +// WeighingBatch 记录了一次批次称重的信息 +type WeighingBatch struct { + gorm.Model + WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"` + Description string `gorm:"size:255;comment:批次称重描述"` + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` +} + +func (WeighingBatch) TableName() string { + return "weighing_batches" +} + +// WeighingRecord 记录了单次称重信息 +type WeighingRecord struct { + gorm.Model + Weight float64 `gorm:"not null;comment:单只猪重量 (kg)"` + WeighingBatchID uint `gorm:"not null;index;comment:关联的批次称重ID"` + PenID uint `gorm:"not null;index;comment:所在猪圈ID"` + OperatorID uint `gorm:"not null;comment:操作员ID"` + Remark string `gorm:"size:255;comment:备注"` + WeighingTime time.Time `gorm:"primaryKey;comment:称重时间"` +} + +func (WeighingRecord) TableName() string { + return "weighing_records" +} From 25e9e07cc8cfa61d23f837309a487ab28053c22f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 20:46:04 +0800 Subject: [PATCH 08/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 4c5586f..a954799 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -40,7 +40,6 @@ type PigBatch struct { CurrentCount int `gorm:"not null;comment:当前存栏数量"` CurrentSickCount int `gorm:"not null;default:0;comment:当前病猪数量"` Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"` - Pens []Pen `gorm:"foreignKey:PigBatchID;comment:所在圈舍ID"` } func (PigBatch) TableName() string { From aced495cd66cb680be7c924ddd95a0d5b3d806f4 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 20:58:41 +0800 Subject: [PATCH 09/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index a954799..485fff7 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -33,13 +33,12 @@ const ( // PigBatch 是猪批次的核心模型,代表了一群被共同管理的猪 type PigBatch struct { gorm.Model - BatchNumber string `gorm:"size:50;not null;uniqueIndex;comment:批次编号,如 2024-W25-A01"` - OriginType PigBatchOriginType `gorm:"size:20;not null;comment:批次来源 (自繁, 外购)"` - StartDate time.Time `gorm:"not null;comment:批次开始日期 (如转入日或购买日)"` - InitialCount int `gorm:"not null;comment:初始数量"` - CurrentCount int `gorm:"not null;comment:当前存栏数量"` - CurrentSickCount int `gorm:"not null;default:0;comment:当前病猪数量"` - Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"` + BatchNumber string `gorm:"size:50;not null;uniqueIndex;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:批次结束日期 (全部淘汰或售出)"` + InitialCount int `gorm:"not null;comment:初始数量"` + Status PigBatchStatus `gorm:"size:20;not null;index;comment:批次状态"` } func (PigBatch) TableName() string { From 258e350c353a2d552610d797e5dd6fac457bcff7 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 22:00:44 +0800 Subject: [PATCH 10/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/medication.go | 2 +- internal/infra/models/pig.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index 1f0b752..c6ead4a 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -90,7 +90,7 @@ type GroupMedicationLog struct { TargetCount int `gorm:"not null;comment:用药对象数量"` Reason MedicationReasonType `gorm:"size:20;not null;comment:用药原因"` Description string `gorm:"size:255;comment:具体描述,如'治疗呼吸道病'"` - Operator string `gorm:"size:50;comment:操作员"` + OperatorID uint `gorm:"comment:操作员ID"` HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` } diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 485fff7..037b507 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -70,7 +70,7 @@ type PigBatchLog struct { AfterCount int `gorm:"not null;comment:变更后总数"` BeforeSickCount int `gorm:"not null;comment:变更前病猪数"` AfterSickCount int `gorm:"not null;comment:变更后病猪数"` - Operator string `gorm:"size:50;comment:操作员"` + OperatorID uint `gorm:"comment:操作员ID"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` } From c50366f67000083576407f07d010f44552fb3839 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 22:17:28 +0800 Subject: [PATCH 11/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 037b507..8042528 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -45,6 +45,11 @@ func (PigBatch) TableName() string { return "pig_batches" } +// IsActive 判断猪批次是否处于活跃状态 +func (pb PigBatch) IsActive() bool { + return pb.Status != BatchStatusSold && pb.Status != BatchStatusArchived +} + // LogChangeType 定义了猪批次数量变更的类型 type LogChangeType string From 645c92978bc2d6b4f5aad884c1caff3efad2db36 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 22:23:31 +0800 Subject: [PATCH 12/65] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/repository/pig_farm_repository.go | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index 5341594..f6099f7 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -8,8 +8,9 @@ import ( ) var ( - ErrHouseContainsPens = errors.New("cannot delete a pig house that still contains pens") - ErrHouseNotFound = errors.New("the specified pig house does not exist") + ErrHouseContainsPens = errors.New("请在移除所有猪圈后移除当前猪舍") + ErrHouseNotFound = errors.New("猪舍不存在") + ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") ) // PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口 @@ -146,12 +147,34 @@ func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { } func (r *gormPigFarmRepository) DeletePen(id uint) error { - result := r.db.Delete(&models.Pen{}, id) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil + return r.db.Transaction(func(tx *gorm.DB) error { + var pen models.Pen + if err := tx.First(&pen, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return gorm.ErrRecordNotFound + } + return err + } + + // 检查猪栏是否被活跃批次使用 + if pen.PigBatchID != 0 { + var pigBatch models.PigBatch + err := tx.First(&pigBatch, pen.PigBatchID).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if pigBatch.IsActive() { + return ErrPenInUse + } + } + + result := tx.Delete(&models.Pen{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil + }) } From c4fb237604178e3e5c6ccad7d4088dd34ec1f706 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 22:33:43 +0800 Subject: [PATCH 13/65] =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E5=88=B0=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/service/pig_farm_service.go | 75 ++++++++++- .../infra/repository/pig_farm_repository.go | 127 ++++++------------ 2 files changed, 115 insertions(+), 87 deletions(-) diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 5912360..0461e01 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -1,12 +1,21 @@ package service import ( + "errors" + "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 ( + ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍") + ErrHouseNotFound = errors.New("指定的猪舍不存在") + ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") +) + // PigFarmService 提供了猪场资产管理的业务逻辑 type PigFarmService interface { // PigHouse methods @@ -71,19 +80,42 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod } func (s *pigFarmService) DeletePigHouse(id uint) error { - return s.repo.DeletePigHouse(id) + // 业务逻辑:检查猪舍是否包含猪栏 + penCount, err := s.repo.CountPensInHouse(id) + if err != nil { + return err + } + if penCount > 0 { + return ErrHouseContainsPens + } + + // 调用仓库层进行删除 + err = s.repo.DeletePigHouse(id) + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrHouseNotFound // 或者直接返回 gorm.ErrRecordNotFound,取决于业务需求 + } + return err } // --- Pen Implementation --- func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + // 业务逻辑:验证所属猪舍是否存在 + _, err := s.repo.GetPigHouseByID(houseID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrHouseNotFound + } + return nil, err + } + pen := &models.Pen{ PenNumber: penNumber, HouseID: houseID, Capacity: capacity, Status: status, } - err := s.repo.CreatePen(pen) + err = s.repo.CreatePen(pen) return pen, err } @@ -96,6 +128,15 @@ func (s *pigFarmService) ListPens() ([]models.Pen, error) { } func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { + // 业务逻辑:验证所属猪舍是否存在 + _, err := s.repo.GetPigHouseByID(houseID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrHouseNotFound + } + return nil, err + } + pen := &models.Pen{ Model: gorm.Model{ID: id}, PenNumber: penNumber, @@ -103,7 +144,7 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa Capacity: capacity, Status: status, } - err := s.repo.UpdatePen(pen) + err = s.repo.UpdatePen(pen) if err != nil { return nil, err } @@ -112,5 +153,31 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa } func (s *pigFarmService) DeletePen(id uint) error { - return s.repo.DeletePen(id) + // 业务逻辑:检查猪栏是否被活跃批次使用 + pen, err := s.repo.GetPenByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return gorm.ErrRecordNotFound // 猪栏不存在 + } + return err + } + + // 检查猪栏是否关联了活跃批次 + if pen.PigBatchID != 0 { + pigBatch, err := s.repo.GetPigBatchByID(pen.PigBatchID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + // 如果批次活跃,则不能删除猪栏 + if pigBatch.IsActive() { + return ErrPenInUse + } + } + + // 调用仓库层进行删除 + err = s.repo.DeletePen(id) + if errors.Is(err, gorm.ErrRecordNotFound) { + return gorm.ErrRecordNotFound // 猪栏不存在 + } + return err } diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index f6099f7..366bd33 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -1,18 +1,10 @@ package repository import ( - "errors" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) -var ( - ErrHouseContainsPens = errors.New("请在移除所有猪圈后移除当前猪舍") - ErrHouseNotFound = errors.New("猪舍不存在") - ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") -) - // PigFarmRepository 定义了与猪场资产(猪舍、猪栏)相关的数据库操作接口 type PigFarmRepository interface { // PigHouse methods @@ -21,6 +13,7 @@ type PigFarmRepository interface { ListPigHouses() ([]models.PigHouse, error) UpdatePigHouse(house *models.PigHouse) error DeletePigHouse(id uint) error + CountPensInHouse(houseID uint) (int64, error) // Pen methods CreatePen(pen *models.Pen) error @@ -28,6 +21,9 @@ type PigFarmRepository interface { ListPens() ([]models.Pen, error) UpdatePen(pen *models.Pen) error DeletePen(id uint) error + + // PigBatch methods + GetPigBatchByID(id uint) (*models.PigBatch, error) } // gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 @@ -74,39 +70,26 @@ func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error { } func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var penCount int64 - if err := tx.Model(&models.Pen{}).Where("house_id = ?", id).Count(&penCount).Error; err != nil { - return err - } - if penCount > 0 { - return ErrHouseContainsPens - } + result := r.db.Delete(&models.PigHouse{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} - result := tx.Delete(&models.PigHouse{}, id) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil - }) +func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { + var count int64 + err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error + return count, err } // --- Pen Implementation --- func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // 验证所属猪舍是否存在 - if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrHouseNotFound - } - return err - } - return tx.Create(pen).Error - }) + return r.db.Create(pen).Error } func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { @@ -126,55 +109,33 @@ func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { } func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // 验证所属猪舍是否存在 - if err := tx.First(&models.PigHouse{}, pen.HouseID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrHouseNotFound - } - return err - } - - result := tx.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil - }) + result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil } func (r *gormPigFarmRepository) DeletePen(id uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var pen models.Pen - if err := tx.First(&pen, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return gorm.ErrRecordNotFound - } - return err - } - - // 检查猪栏是否被活跃批次使用 - if pen.PigBatchID != 0 { - var pigBatch models.PigBatch - err := tx.First(&pigBatch, pen.PigBatchID).Error - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - if pigBatch.IsActive() { - return ErrPenInUse - } - } - - result := tx.Delete(&models.Pen{}, id) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil - }) + result := r.db.Delete(&models.Pen{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +// --- PigBatch Implementation --- + +func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { + var batch models.PigBatch + if err := r.db.First(&batch, id).Error; err != nil { + return nil, err + } + return &batch, nil } From fadc1e2535ec1c1d5c89810ddc54aaac7f1d2e43 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 22:34:24 +0800 Subject: [PATCH 14/65] =?UTF-8?q?=E6=94=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/{pig.go => pig_service.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/app/service/{pig.go => pig_service.go} (100%) diff --git a/internal/app/service/pig.go b/internal/app/service/pig_service.go similarity index 100% rename from internal/app/service/pig.go rename to internal/app/service/pig_service.go From d273932693cf41149d5482786fbdf73f7fa9f224 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 23:02:43 +0800 Subject: [PATCH 15/65] =?UTF-8?q?=E9=87=8D=E6=9E=84dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/device/device_controller.go | 311 +++--------------- .../management/pig_farm_controller.go | 101 ++---- .../app/controller/plan/plan_controller.go | 111 ++----- .../app/controller/user/user_controller.go | 69 +--- internal/app/dto/device_converter.go | 142 ++++++++ internal/app/dto/device_dto.go | 96 ++++++ internal/app/dto/pig_farm_dto.go | 48 +++ internal/app/dto/plan_converter.go | 145 ++++++++ internal/app/dto/plan_dto.go | 75 +++++ internal/app/dto/user_dto.go | 43 +++ 10 files changed, 647 insertions(+), 494 deletions(-) create mode 100644 internal/app/dto/device_converter.go create mode 100644 internal/app/dto/device_dto.go create mode 100644 internal/app/dto/pig_farm_dto.go create mode 100644 internal/app/dto/plan_converter.go create mode 100644 internal/app/dto/plan_dto.go create mode 100644 internal/app/dto/user_dto.go diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index ddbbc25..7d49124 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -3,12 +3,11 @@ package device import ( "encoding/json" "errors" - "fmt" "strconv" "strings" - "time" "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/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" @@ -34,243 +33,11 @@ func NewController( return &Controller{ deviceRepo: deviceRepo, areaControllerRepo: areaControllerRepo, - deviceTemplateRepo: deviceTemplateRepo, // 初始化设备模板仓库 + deviceTemplateRepo: deviceTemplateRepo, logger: logger, } } -// --- Request DTOs --- - -// CreateDeviceRequest 定义了创建设备时需要传入的参数 -type CreateDeviceRequest struct { - Name string `json:"name" binding:"required"` - DeviceTemplateID uint `json:"device_template_id" binding:"required"` - AreaControllerID uint `json:"area_controller_id" binding:"required"` - Location string `json:"location,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` -} - -// UpdateDeviceRequest 定义了更新设备时需要传入的参数 -type UpdateDeviceRequest struct { - Name string `json:"name" binding:"required"` - DeviceTemplateID uint `json:"device_template_id" binding:"required"` - AreaControllerID uint `json:"area_controller_id" binding:"required"` - Location string `json:"location,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` -} - -// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数 -type CreateAreaControllerRequest struct { - Name string `json:"name" binding:"required"` - NetworkID string `json:"network_id" binding:"required"` - Location string `json:"location,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` -} - -// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数 -type UpdateAreaControllerRequest struct { - Name string `json:"name" binding:"required"` - NetworkID string `json:"network_id" binding:"required"` - Location string `json:"location,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` -} - -// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数 -type CreateDeviceTemplateRequest struct { - Name string `json:"name" binding:"required"` - Manufacturer string `json:"manufacturer,omitempty"` - Description string `json:"description,omitempty"` - Category models.DeviceCategory `json:"category" binding:"required"` - Commands map[string]interface{} `json:"commands" binding:"required"` - Values []models.ValueDescriptor `json:"values,omitempty"` -} - -// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数 -type UpdateDeviceTemplateRequest struct { - Name string `json:"name" binding:"required"` - Manufacturer string `json:"manufacturer,omitempty"` - Description string `json:"description,omitempty"` - Category models.DeviceCategory `json:"category" binding:"required"` - Commands map[string]interface{} `json:"commands" binding:"required"` - Values []models.ValueDescriptor `json:"values,omitempty"` -} - -// --- Response DTOs --- - -// DeviceResponse 定义了返回给客户端的单个设备信息的结构 -type DeviceResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - DeviceTemplateID uint `json:"device_template_id"` - DeviceTemplateName string `json:"device_template_name"` - AreaControllerID uint `json:"area_controller_id"` - AreaControllerName string `json:"area_controller_name"` - Location string `json:"location"` - Properties map[string]interface{} `json:"properties"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构 -type AreaControllerResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - NetworkID string `json:"network_id"` - Location string `json:"location"` - Status string `json:"status"` - Properties map[string]interface{} `json:"properties"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构 -type DeviceTemplateResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Manufacturer string `json:"manufacturer"` - Description string `json:"description"` - Category models.DeviceCategory `json:"category"` - Commands map[string]interface{} `json:"commands"` - Values []models.ValueDescriptor `json:"values"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -// --- DTO 转换函数 --- - -// newDeviceResponse 从数据库模型创建一个新的设备响应 DTO -func newDeviceResponse(device *models.Device) (*DeviceResponse, error) { - if device == nil { - return nil, nil - } - - var props map[string]interface{} - if len(device.Properties) > 0 && string(device.Properties) != "null" { - if err := device.ParseProperties(&props); err != nil { - return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err) - } - } - - // 确保 DeviceTemplate 和 AreaController 已预加载 - deviceTemplateName := "" - if device.DeviceTemplate.ID != 0 { - deviceTemplateName = device.DeviceTemplate.Name - } - - areaControllerName := "" - if device.AreaController.ID != 0 { - areaControllerName = device.AreaController.Name - } - - return &DeviceResponse{ - ID: device.ID, - Name: device.Name, - DeviceTemplateID: device.DeviceTemplateID, - DeviceTemplateName: deviceTemplateName, - AreaControllerID: device.AreaControllerID, - AreaControllerName: areaControllerName, - Location: device.Location, - Properties: props, - CreatedAt: device.CreatedAt.Format(time.RFC3339), - UpdatedAt: device.UpdatedAt.Format(time.RFC3339), - }, nil -} - -// newListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片 -func newListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) { - list := make([]*DeviceResponse, 0, len(devices)) - for _, device := range devices { - resp, err := newDeviceResponse(device) - if err != nil { - return nil, err - } - list = append(list, resp) - } - return list, nil -} - -// newAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO -func newAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) { - if ac == nil { - return nil, nil - } - - var props map[string]interface{} - if len(ac.Properties) > 0 && string(ac.Properties) != "null" { - if err := json.Unmarshal(ac.Properties, &props); err != nil { - return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err) - } - } - - return &AreaControllerResponse{ - ID: ac.ID, - Name: ac.Name, - NetworkID: ac.NetworkID, - Location: ac.Location, - Status: ac.Status, - Properties: props, - CreatedAt: ac.CreatedAt.Format(time.RFC3339), - UpdatedAt: ac.UpdatedAt.Format(time.RFC3339), - }, nil -} - -// newListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片 -func newListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) { - list := make([]*AreaControllerResponse, 0, len(acs)) - for _, ac := range acs { - resp, err := newAreaControllerResponse(ac) - if err != nil { - return nil, err - } - list = append(list, resp) - } - return list, nil -} - -// newDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO -func newDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) { - if dt == nil { - return nil, nil - } - - var commands map[string]interface{} - if err := dt.ParseCommands(&commands); err != nil { - return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err) - } - - var values []models.ValueDescriptor - if dt.Category == models.CategorySensor { - if err := dt.ParseValues(&values); err != nil { - return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err) - } - } - - return &DeviceTemplateResponse{ - ID: dt.ID, - Name: dt.Name, - Manufacturer: dt.Manufacturer, - Description: dt.Description, - Category: dt.Category, - Commands: commands, - Values: values, - CreatedAt: dt.CreatedAt.Format(time.RFC3339), - UpdatedAt: dt.UpdatedAt.Format(time.RFC3339), - }, nil -} - -// newListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片 -func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) { - list := make([]*DeviceTemplateResponse, 0, len(dts)) - for _, dt := range dts { - resp, err := newDeviceTemplateResponse(dt) - if err != nil { - return nil, err - } - list = append(list, resp) - } - return list, nil -} - // --- Controller Methods: Devices --- // CreateDevice godoc @@ -279,12 +46,12 @@ func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTempl // @Tags 设备管理 // @Accept json // @Produce json -// @Param device body CreateDeviceRequest true "设备信息" -// @Success 200 {object} controller.Response{data=DeviceResponse} +// @Param device body dto.CreateDeviceRequest true "设备信息" +// @Success 200 {object} controller.Response{data=dto.DeviceResponse} // @Router /api/v1/devices [post] func (c *Controller) CreateDevice(ctx *gin.Context) { const actionType = "创建设备" - var req CreateDeviceRequest + var req dto.CreateDeviceRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -325,9 +92,9 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { return } - resp, err := newDeviceResponse(createdDevice) + resp, err := dto.NewDeviceResponse(createdDevice) if err != nil { - c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) + c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice) return } @@ -342,7 +109,7 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { // @Tags 设备管理 // @Produce json // @Param id path string true "设备ID" -// @Success 200 {object} controller.Response{data=DeviceResponse} +// @Success 200 {object} controller.Response{data=dto.DeviceResponse} // @Router /api/v1/devices/{id} [get] func (c *Controller) GetDevice(ctx *gin.Context) { const actionType = "获取设备" @@ -371,7 +138,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) { return } - resp, err := newDeviceResponse(device) + resp, err := dto.NewDeviceResponse(device) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device) @@ -387,7 +154,7 @@ func (c *Controller) GetDevice(ctx *gin.Context) { // @Description 获取系统中所有设备的列表 // @Tags 设备管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]DeviceResponse} +// @Success 200 {object} controller.Response{data=[]dto.DeviceResponse} // @Router /api/v1/devices [get] func (c *Controller) ListDevices(ctx *gin.Context) { const actionType = "获取设备列表" @@ -398,7 +165,7 @@ func (c *Controller) ListDevices(ctx *gin.Context) { return } - resp, err := newListDeviceResponse(devices) + resp, err := dto.NewListDeviceResponse(devices) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices) @@ -416,8 +183,8 @@ func (c *Controller) ListDevices(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path string true "设备ID" -// @Param device body UpdateDeviceRequest true "要更新的设备信息" -// @Success 200 {object} controller.Response{data=DeviceResponse} +// @Param device body dto.UpdateDeviceRequest true "要更新的设备信息" +// @Success 200 {object} controller.Response{data=dto.DeviceResponse} // @Router /api/v1/devices/{id} [put] func (c *Controller) UpdateDevice(ctx *gin.Context) { const actionType = "更新设备" @@ -440,7 +207,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } - var req UpdateDeviceRequest + var req dto.UpdateDeviceRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -479,7 +246,7 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } - resp, err := newDeviceResponse(updatedDevice) + resp, err := dto.NewDeviceResponse(updatedDevice) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice) @@ -539,12 +306,12 @@ func (c *Controller) DeleteDevice(ctx *gin.Context) { // @Tags 区域主控管理 // @Accept json // @Produce json -// @Param areaController body CreateAreaControllerRequest true "区域主控信息" -// @Success 200 {object} controller.Response{data=AreaControllerResponse} +// @Param areaController body dto.CreateAreaControllerRequest true "区域主控信息" +// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse} // @Router /api/v1/area-controllers [post] func (c *Controller) CreateAreaController(ctx *gin.Context) { const actionType = "创建区域主控" - var req CreateAreaControllerRequest + var req dto.CreateAreaControllerRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -577,7 +344,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) { return } - resp, err := newAreaControllerResponse(ac) + resp, err := dto.NewAreaControllerResponse(ac) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac) @@ -594,7 +361,7 @@ func (c *Controller) CreateAreaController(ctx *gin.Context) { // @Tags 区域主控管理 // @Produce json // @Param id path string true "区域主控ID" -// @Success 200 {object} controller.Response{data=AreaControllerResponse} +// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse} // @Router /api/v1/area-controllers/{id} [get] func (c *Controller) GetAreaController(ctx *gin.Context) { const actionType = "获取区域主控" @@ -619,7 +386,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) { return } - resp, err := newAreaControllerResponse(ac) + resp, err := dto.NewAreaControllerResponse(ac) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac) @@ -635,7 +402,7 @@ func (c *Controller) GetAreaController(ctx *gin.Context) { // @Description 获取系统中所有区域主控的列表 // @Tags 区域主控管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]AreaControllerResponse} +// @Success 200 {object} controller.Response{data=[]dto.AreaControllerResponse} // @Router /api/v1/area-controllers [get] func (c *Controller) ListAreaControllers(ctx *gin.Context) { const actionType = "获取区域主控列表" @@ -646,7 +413,7 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) { return } - resp, err := newListAreaControllerResponse(acs) + resp, err := dto.NewListAreaControllerResponse(acs) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs) @@ -664,8 +431,8 @@ func (c *Controller) ListAreaControllers(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path string true "区域主控ID" -// @Param areaController body UpdateAreaControllerRequest true "要更新的区域主控信息" -// @Success 200 {object} controller.Response{data=AreaControllerResponse} +// @Param areaController body dto.UpdateAreaControllerRequest true "要更新的区域主控信息" +// @Success 200 {object} controller.Response{data=dto.AreaControllerResponse} // @Router /api/v1/area-controllers/{id} [put] func (c *Controller) UpdateAreaController(ctx *gin.Context) { const actionType = "更新区域主控" @@ -690,7 +457,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) { return } - var req UpdateAreaControllerRequest + var req dto.UpdateAreaControllerRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -721,7 +488,7 @@ func (c *Controller) UpdateAreaController(ctx *gin.Context) { return } - resp, err := newAreaControllerResponse(existingAC) + resp, err := dto.NewAreaControllerResponse(existingAC) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC) @@ -781,12 +548,12 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) { // @Tags 设备模板管理 // @Accept json // @Produce json -// @Param deviceTemplate body CreateDeviceTemplateRequest true "设备模板信息" -// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Param deviceTemplate body dto.CreateDeviceTemplateRequest true "设备模板信息" +// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse} // @Router /api/v1/device-templates [post] func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) { const actionType = "创建设备模板" - var req CreateDeviceTemplateRequest + var req dto.CreateDeviceTemplateRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -828,7 +595,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) { return } - resp, err := newDeviceTemplateResponse(deviceTemplate) + resp, err := dto.NewDeviceTemplateResponse(deviceTemplate) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate) @@ -845,7 +612,7 @@ func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) { // @Tags 设备模板管理 // @Produce json // @Param id path string true "设备模板ID" -// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse} // @Router /api/v1/device-templates/{id} [get] func (c *Controller) GetDeviceTemplate(ctx *gin.Context) { const actionType = "获取设备模板" @@ -870,7 +637,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) { return } - resp, err := newDeviceTemplateResponse(deviceTemplate) + resp, err := dto.NewDeviceTemplateResponse(deviceTemplate) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate) @@ -886,7 +653,7 @@ func (c *Controller) GetDeviceTemplate(ctx *gin.Context) { // @Description 获取系统中所有设备模板的列表 // @Tags 设备模板管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]DeviceTemplateResponse} +// @Success 200 {object} controller.Response{data=[]dto.DeviceTemplateResponse} // @Router /api/v1/device-templates [get] func (c *Controller) ListDeviceTemplates(ctx *gin.Context) { const actionType = "获取设备模板列表" @@ -897,7 +664,7 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) { return } - resp, err := newListDeviceTemplateResponse(deviceTemplates) + resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates) @@ -915,8 +682,8 @@ func (c *Controller) ListDeviceTemplates(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path string true "设备模板ID" -// @Param deviceTemplate body UpdateDeviceTemplateRequest true "要更新的设备模板信息" -// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Param deviceTemplate body dto.UpdateDeviceTemplateRequest true "要更新的设备模板信息" +// @Success 200 {object} controller.Response{data=dto.DeviceTemplateResponse} // @Router /api/v1/device-templates/{id} [put] func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) { const actionType = "更新设备模板" @@ -941,7 +708,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) { return } - var req UpdateDeviceTemplateRequest + var req dto.UpdateDeviceTemplateRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -981,7 +748,7 @@ func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) { return } - resp, err := newDeviceTemplateResponse(existingDeviceTemplate) + resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate) diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index e08c08c..1dcc7b1 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -5,60 +5,13 @@ import ( "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" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) -// --- 数据传输对象 (DTOs) --- - -// PigHouseResponse 定义了猪舍信息的响应结构 -type PigHouseResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Description string `json:"description"` -} - -// PenResponse 定义了猪栏信息的响应结构 -type PenResponse struct { - ID uint `json:"id"` - PenNumber string `json:"pen_number"` - HouseID uint `json:"house_id"` - Capacity int `json:"capacity"` - Status models.PenStatus `json:"status"` - PigBatchID uint `json:"pig_batch_id"` -} - -// CreatePigHouseRequest 定义了创建猪舍的请求结构 -type CreatePigHouseRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` -} - -// UpdatePigHouseRequest 定义了更新猪舍的请求结构 -type UpdatePigHouseRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` -} - -// CreatePenRequest 定义了创建猪栏的请求结构 -type CreatePenRequest struct { - PenNumber string `json:"pen_number" binding:"required"` - HouseID uint `json:"house_id" binding:"required"` - Capacity int `json:"capacity" binding:"required"` - Status models.PenStatus `json:"status" binding:"required"` -} - -// UpdatePenRequest 定义了更新猪栏的请求结构 -type UpdatePenRequest struct { - PenNumber string `json:"pen_number" binding:"required"` - HouseID uint `json:"house_id" binding:"required"` - Capacity int `json:"capacity" binding:"required"` - Status models.PenStatus `json:"status" binding:"required"` -} - // --- 控制器定义 --- // PigFarmController 负责处理猪舍和猪栏相关的API请求 @@ -83,12 +36,12 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) * // @Tags 猪场管理 // @Accept json // @Produce json -// @Param body body CreatePigHouseRequest true "猪舍信息" -// @Success 201 {object} controller.Response{data=PigHouseResponse} "创建成功" +// @Param body body dto.CreatePigHouseRequest true "猪舍信息" +// @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功" // @Router /api/v1/pighouses [post] func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { const action = "创建猪舍" - var req CreatePigHouseRequest + var req dto.CreatePigHouseRequest if err := ctx.ShouldBindJSON(&req); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) return @@ -101,7 +54,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { return } - resp := PigHouseResponse{ + resp := dto.PigHouseResponse{ ID: house.ID, Name: house.Name, Description: house.Description, @@ -115,7 +68,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { // @Tags 猪场管理 // @Produce json // @Param id path int true "猪舍ID" -// @Success 200 {object} controller.Response{data=PigHouseResponse} "获取成功" +// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功" // @Router /api/v1/pighouses/{id} [get] func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { const action = "获取猪舍" @@ -136,7 +89,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { return } - resp := PigHouseResponse{ + resp := dto.PigHouseResponse{ ID: house.ID, Name: house.Name, Description: house.Description, @@ -149,7 +102,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { // @Description 获取所有猪舍的列表 // @Tags 猪场管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]PigHouseResponse} "获取成功" +// @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功" // @Router /api/v1/pighouses [get] func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { const action = "获取猪舍列表" @@ -160,9 +113,9 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { return } - var resp []PigHouseResponse + var resp []dto.PigHouseResponse for _, house := range houses { - resp = append(resp, PigHouseResponse{ + resp = append(resp, dto.PigHouseResponse{ ID: house.ID, Name: house.Name, Description: house.Description, @@ -179,8 +132,8 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path int true "猪舍ID" -// @Param body body UpdatePigHouseRequest true "猪舍信息" -// @Success 200 {object} controller.Response{data=PigHouseResponse} "更新成功" +// @Param body body dto.UpdatePigHouseRequest true "猪舍信息" +// @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功" // @Router /api/v1/pighouses/{id} [put] func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { const action = "更新猪舍" @@ -190,7 +143,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { return } - var req UpdatePigHouseRequest + var req dto.UpdatePigHouseRequest if err := ctx.ShouldBindJSON(&req); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) return @@ -207,7 +160,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { return } - resp := PigHouseResponse{ + resp := dto.PigHouseResponse{ ID: house.ID, Name: house.Name, Description: house.Description, @@ -252,12 +205,12 @@ func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) { // @Tags 猪场管理 // @Accept json // @Produce json -// @Param body body CreatePenRequest true "猪栏信息" -// @Success 201 {object} controller.Response{data=PenResponse} "创建成功" +// @Param body body dto.CreatePenRequest true "猪栏信息" +// @Success 201 {object} controller.Response{data=dto.PenResponse} "创建成功" // @Router /api/v1/pens [post] func (c *PigFarmController) CreatePen(ctx *gin.Context) { const action = "创建猪栏" - var req CreatePenRequest + var req dto.CreatePenRequest if err := ctx.ShouldBindJSON(&req); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) return @@ -270,7 +223,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { return } - resp := PenResponse{ + resp := dto.PenResponse{ ID: pen.ID, PenNumber: pen.PenNumber, HouseID: pen.HouseID, @@ -287,7 +240,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { // @Tags 猪场管理 // @Produce json // @Param id path int true "猪栏ID" -// @Success 200 {object} controller.Response{data=PenResponse} "获取成功" +// @Success 200 {object} controller.Response{data=dto.PenResponse} "获取成功" // @Router /api/v1/pens/{id} [get] func (c *PigFarmController) GetPen(ctx *gin.Context) { const action = "获取猪栏" @@ -308,7 +261,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) { return } - resp := PenResponse{ + resp := dto.PenResponse{ ID: pen.ID, PenNumber: pen.PenNumber, HouseID: pen.HouseID, @@ -324,7 +277,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) { // @Description 获取所有猪栏的列表 // @Tags 猪场管理 // @Produce json -// @Success 200 {object} controller.Response{data=[]PenResponse} "获取成功" +// @Success 200 {object} controller.Response{data=[]dto.PenResponse} "获取成功" // @Router /api/v1/pens [get] func (c *PigFarmController) ListPens(ctx *gin.Context) { const action = "获取猪栏列表" @@ -335,9 +288,9 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) { return } - var resp []PenResponse + var resp []dto.PenResponse for _, pen := range pens { - resp = append(resp, PenResponse{ + resp = append(resp, dto.PenResponse{ ID: pen.ID, PenNumber: pen.PenNumber, HouseID: pen.HouseID, @@ -357,8 +310,8 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path int true "猪栏ID" -// @Param body body UpdatePenRequest true "猪栏信息" -// @Success 200 {object} controller.Response{data=PenResponse} "更新成功" +// @Param body body dto.UpdatePenRequest true "猪栏信息" +// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功" // @Router /api/v1/pens/{id} [put] func (c *PigFarmController) UpdatePen(ctx *gin.Context) { const action = "更新猪栏" @@ -368,7 +321,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) { return } - var req UpdatePenRequest + var req dto.UpdatePenRequest if err := ctx.ShouldBindJSON(&req); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) return @@ -385,7 +338,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) { return } - resp := PenResponse{ + resp := dto.PenResponse{ ID: pen.ID, PenNumber: pen.PenNumber, HouseID: pen.HouseID, diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 284df63..e7a8862 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -5,6 +5,7 @@ import ( "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/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -13,80 +14,6 @@ import ( "gorm.io/gorm" ) -// --- 请求和响应 DTO 定义 --- - -// CreatePlanRequest 定义创建计划请求的结构体 -type CreatePlanRequest struct { - Name string `json:"name" binding:"required" example:"猪舍温度控制计划"` - Description string `json:"description" example:"根据温度自动调节风扇和加热器"` - ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"` - ExecuteNum uint `json:"execute_num,omitempty" example:"10"` - CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` - SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` - Tasks []TaskRequest `json:"tasks,omitempty"` -} - -// PlanResponse 定义计划详情响应的结构体 -type PlanResponse struct { - ID uint `json:"id" example:"1"` - Name string `json:"name" example:"猪舍温度控制计划"` - Description string `json:"description" example:"根据温度自动调节风扇和加热器"` - ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` - Status models.PlanStatus `json:"status" example:"已启用"` - ExecuteNum uint `json:"execute_num" example:"10"` - ExecuteCount uint `json:"execute_count" example:"0"` - CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` - ContentType models.PlanContentType `json:"content_type" example:"任务"` - SubPlans []SubPlanResponse `json:"sub_plans,omitempty"` - Tasks []TaskResponse `json:"tasks,omitempty"` -} - -// ListPlansResponse 定义获取计划列表响应的结构体 -type ListPlansResponse struct { - Plans []PlanResponse `json:"plans"` - Total int `json:"total" example:"100"` -} - -// UpdatePlanRequest 定义更新计划请求的结构体 -type UpdatePlanRequest struct { - Name string `json:"name" example:"猪舍温度控制计划V2"` - Description string `json:"description" example:"更新后的描述"` - ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` - ExecuteNum uint `json:"execute_num,omitempty" example:"10"` - CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` - SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` - Tasks []TaskRequest `json:"tasks,omitempty"` -} - -// SubPlanResponse 定义子计划响应结构体 -type SubPlanResponse struct { - ID uint `json:"id" example:"1"` - ParentPlanID uint `json:"parent_plan_id" example:"1"` - ChildPlanID uint `json:"child_plan_id" example:"2"` - ExecutionOrder int `json:"execution_order" example:"1"` - ChildPlan *PlanResponse `json:"child_plan,omitempty"` -} - -// TaskRequest 定义任务请求结构体 -type TaskRequest struct { - Name string `json:"name" example:"打开风扇"` - Description string `json:"description" example:"打开1号风扇"` - ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"等待"` - Parameters map[string]interface{} `json:"parameters,omitempty"` -} - -// TaskResponse 定义任务响应结构体 -type TaskResponse struct { - ID int `json:"id" example:"1"` - PlanID uint `json:"plan_id" example:"1"` - Name string `json:"name" example:"打开风扇"` - Description string `json:"description" example:"打开1号风扇"` - ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"等待"` - Parameters map[string]interface{} `json:"parameters,omitempty"` -} - // --- Controller 定义 --- // Controller 定义了计划相关的控制器 @@ -113,11 +40,11 @@ func NewController(logger *logs.Logger, planRepo repository.PlanRepository, anal // @Tags 计划管理 // @Accept json // @Produce json -// @Param plan body CreatePlanRequest true "计划信息" -// @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为201代表创建成功" +// @Param plan body dto.CreatePlanRequest true "计划信息" +// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为201代表创建成功" // @Router /api/v1/plans [post] func (c *Controller) CreatePlan(ctx *gin.Context) { - var req CreatePlanRequest + var req dto.CreatePlanRequest const actionType = "创建计划" if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) @@ -126,7 +53,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { } // 使用已有的转换函数,它已经包含了验证和重排逻辑 - planToCreate, err := PlanFromCreateRequest(&req) + planToCreate, err := dto.NewPlanFromCreateRequest(&req) if err != nil { c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req) @@ -155,7 +82,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { } // 使用已有的转换函数将创建后的模型转换为响应对象 - resp, err := PlanToResponse(planToCreate) + resp, err := dto.NewPlanToResponse(planToCreate) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate) @@ -173,7 +100,7 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { // @Tags 计划管理 // @Produce json // @Param id path int true "计划ID" -// @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表成功获取" +// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表成功获取" // @Router /api/v1/plans/{id} [get] func (c *Controller) GetPlan(ctx *gin.Context) { const actionType = "获取计划详情" @@ -202,7 +129,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) { } // 3. 将模型转换为响应 DTO - resp, err := PlanToResponse(plan) + resp, err := dto.NewPlanToResponse(plan) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan) @@ -219,7 +146,7 @@ func (c *Controller) GetPlan(ctx *gin.Context) { // @Description 获取所有计划的列表 // @Tags 计划管理 // @Produce json -// @Success 200 {object} controller.Response{data=plan.ListPlansResponse} "业务码为200代表成功获取列表" +// @Success 200 {object} controller.Response{data=[]dto.PlanResponse} "业务码为200代表成功获取列表" // @Router /api/v1/plans [get] func (c *Controller) ListPlans(ctx *gin.Context) { const actionType = "获取计划列表" @@ -232,9 +159,9 @@ func (c *Controller) ListPlans(ctx *gin.Context) { } // 2. 将模型转换为响应 DTO - planResponses := make([]PlanResponse, 0, len(plans)) + planResponses := make([]dto.PlanResponse, 0, len(plans)) for _, p := range plans { - resp, err := PlanToResponse(&p) + resp, err := dto.NewPlanToResponse(&p) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p) @@ -244,7 +171,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) { } // 3. 构造并发送成功响应 - resp := ListPlansResponse{ + resp := dto.ListPlansResponse{ Plans: planResponses, Total: len(planResponses), } @@ -259,8 +186,8 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // @Accept json // @Produce json // @Param id path int true "计划ID" -// @Param plan body UpdatePlanRequest true "更新后的计划信息" -// @Success 200 {object} controller.Response{data=plan.PlanResponse} "业务码为200代表更新成功" +// @Param plan body dto.UpdatePlanRequest true "更新后的计划信息" +// @Success 200 {object} controller.Response{data=dto.PlanResponse} "业务码为200代表更新成功" // @Router /api/v1/plans/{id} [put] func (c *Controller) UpdatePlan(ctx *gin.Context) { const actionType = "更新计划" @@ -274,7 +201,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { } // 2. 绑定请求体 - var req UpdatePlanRequest + var req dto.UpdatePlanRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) @@ -282,7 +209,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { } // 3. 将请求转换为模型(转换函数带校验) - planToUpdate, err := PlanFromUpdateRequest(&req) + planToUpdate, err := dto.NewPlanFromUpdateRequest(&req) if err != nil { c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req) @@ -306,8 +233,8 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) return } - c.logger.Errorf("%s: 获取计划详情失败: %v, ID: %d", actionType, err, id) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id) + c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id) return } @@ -337,7 +264,7 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { } // 7. 将模型转换为响应 DTO - resp, err := PlanToResponse(updatedPlan) + resp, err := dto.NewPlanToResponse(updatedPlan) if err != nil { c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan) diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 7d6d1e8..27689d4 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -5,6 +5,7 @@ import ( "time" "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/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -31,50 +32,6 @@ func NewController(userRepo repository.UserRepository, auditRepo repository.User } } -// --- DTOs --- - -// CreateUserRequest 定义创建用户请求的结构体 -type CreateUserRequest struct { - Username string `json:"username" binding:"required" example:"newuser"` - Password string `json:"password" binding:"required" example:"password123"` -} - -// LoginRequest 定义登录请求的结构体 -type LoginRequest struct { - // Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 - Identifier string `json:"identifier" binding:"required" example:"testuser"` - Password string `json:"password" binding:"required" example:"password123"` -} - -// CreateUserResponse 定义创建用户成功响应的结构体 -type CreateUserResponse struct { - Username string `json:"username" example:"newuser"` - ID uint `json:"id" example:"1"` -} - -// LoginResponse 定义登录成功响应的结构体 -type LoginResponse struct { - Username string `json:"username" example:"testuser"` - ID uint `json:"id" example:"1"` - Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` -} - -// HistoryResponse 定义单条操作历史的响应结构体 -type HistoryResponse struct { - UserID uint `json:"user_id" example:"101"` - Username string `json:"username" example:"testuser"` - ActionType string `json:"action_type" example:"更新设备"` - Description string `json:"description" example:"设备更新成功"` - TargetResource interface{} `json:"target_resource"` - Time string `json:"time"` -} - -// ListHistoryResponse 定义操作历史列表的响应结构体 -type ListHistoryResponse struct { - History []HistoryResponse `json:"history"` - Total int64 `json:"total" example:"100"` -} - // --- Controller Methods --- // CreateUser godoc @@ -83,11 +40,11 @@ type ListHistoryResponse struct { // @Tags 用户管理 // @Accept json // @Produce json -// @Param user body CreateUserRequest true "用户信息" -// @Success 200 {object} controller.Response{data=user.CreateUserResponse} "业务码为201代表创建成功" +// @Param user body dto.CreateUserRequest true "用户信息" +// @Success 200 {object} controller.Response{data=dto.CreateUserResponse} "业务码为201代表创建成功" // @Router /api/v1/users [post] func (c *Controller) CreateUser(ctx *gin.Context) { - var req CreateUserRequest + var req dto.CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("创建用户: 参数绑定失败: %v", err) controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error()) @@ -114,7 +71,7 @@ func (c *Controller) CreateUser(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", CreateUserResponse{ + controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{ Username: user.Username, ID: user.ID, }) @@ -126,11 +83,11 @@ func (c *Controller) CreateUser(ctx *gin.Context) { // @Tags 用户管理 // @Accept json // @Produce json -// @Param credentials body LoginRequest true "登录凭证" -// @Success 200 {object} controller.Response{data=user.LoginResponse} "业务码为200代表登录成功" +// @Param credentials body dto.LoginRequest true "登录凭证" +// @Success 200 {object} controller.Response{data=dto.LoginResponse} "业务码为200代表登录成功" // @Router /api/v1/users/login [post] func (c *Controller) Login(ctx *gin.Context) { - var req LoginRequest + var req dto.LoginRequest if err := ctx.ShouldBindJSON(&req); err != nil { c.logger.Errorf("登录: 参数绑定失败: %v", err) controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error()) @@ -162,7 +119,7 @@ func (c *Controller) Login(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", LoginResponse{ + controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{ Username: user.Username, ID: user.ID, Token: tokenString, @@ -178,7 +135,7 @@ func (c *Controller) Login(ctx *gin.Context) { // @Param page query int false "页码" default(1) // @Param page_size query int false "每页大小" default(10) // @Param action_type query string false "按操作类型过滤" -// @Success 200 {object} controller.Response{data=user.ListHistoryResponse} "业务码为200代表成功获取" +// @Success 200 {object} controller.Response{data=dto.ListHistoryResponse} "业务码为200代表成功获取" // @Router /api/v1/users/{id}/history [get] func (c *Controller) ListUserHistory(ctx *gin.Context) { const actionType = "获取用户操作历史" @@ -221,9 +178,9 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) { } // 4. 将数据库模型转换为响应 DTO - historyResponses := make([]HistoryResponse, 0, len(logs)) + historyResponses := make([]dto.HistoryResponse, 0, len(logs)) for _, log := range logs { - historyResponses = append(historyResponses, HistoryResponse{ + historyResponses = append(historyResponses, dto.HistoryResponse{ UserID: log.UserID, Username: log.Username, ActionType: log.ActionType, @@ -234,7 +191,7 @@ func (c *Controller) ListUserHistory(ctx *gin.Context) { } // 5. 发送成功响应 - resp := ListHistoryResponse{ + resp := dto.ListHistoryResponse{ History: historyResponses, Total: total, } diff --git a/internal/app/dto/device_converter.go b/internal/app/dto/device_converter.go new file mode 100644 index 0000000..20a5ce3 --- /dev/null +++ b/internal/app/dto/device_converter.go @@ -0,0 +1,142 @@ +package dto + +import ( + "encoding/json" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// NewDeviceResponse 从数据库模型创建一个新的设备响应 DTO +func NewDeviceResponse(device *models.Device) (*DeviceResponse, error) { + if device == nil { + return nil, nil + } + + var props map[string]interface{} + if len(device.Properties) > 0 && string(device.Properties) != "null" { + if err := device.ParseProperties(&props); err != nil { + return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err) + } + } + + // 确保 DeviceTemplate 和 AreaController 已预加载 + deviceTemplateName := "" + if device.DeviceTemplate.ID != 0 { + deviceTemplateName = device.DeviceTemplate.Name + } + + areaControllerName := "" + if device.AreaController.ID != 0 { + areaControllerName = device.AreaController.Name + } + + return &DeviceResponse{ + ID: device.ID, + Name: device.Name, + DeviceTemplateID: device.DeviceTemplateID, + DeviceTemplateName: deviceTemplateName, + AreaControllerID: device.AreaControllerID, + AreaControllerName: areaControllerName, + Location: device.Location, + Properties: props, + CreatedAt: device.CreatedAt.Format(time.RFC3339), + UpdatedAt: device.UpdatedAt.Format(time.RFC3339), + }, nil +} + +// NewListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片 +func NewListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) { + list := make([]*DeviceResponse, 0, len(devices)) + for _, device := range devices { + resp, err := NewDeviceResponse(device) + if err != nil { + return nil, err + } + list = append(list, resp) + } + return list, nil +} + +// NewAreaControllerResponse 从数据库模型创建一个新的区域主控响应 DTO +func NewAreaControllerResponse(ac *models.AreaController) (*AreaControllerResponse, error) { + if ac == nil { + return nil, nil + } + + var props map[string]interface{} + if len(ac.Properties) > 0 && string(ac.Properties) != "null" { + if err := json.Unmarshal(ac.Properties, &props); err != nil { + return nil, fmt.Errorf("解析区域主控属性失败 (ID: %d): %w", ac.ID, err) + } + } + + return &AreaControllerResponse{ + ID: ac.ID, + Name: ac.Name, + NetworkID: ac.NetworkID, + Location: ac.Location, + Status: ac.Status, + Properties: props, + CreatedAt: ac.CreatedAt.Format(time.RFC3339), + UpdatedAt: ac.UpdatedAt.Format(time.RFC3339), + }, nil +} + +// NewListAreaControllerResponse 从数据库模型切片创建一个新的区域主控列表响应 DTO 切片 +func NewListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControllerResponse, error) { + list := make([]*AreaControllerResponse, 0, len(acs)) + for _, ac := range acs { + resp, err := NewAreaControllerResponse(ac) + if err != nil { + return nil, err + } + list = append(list, resp) + } + return list, nil +} + +// NewDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO +func NewDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) { + if dt == nil { + return nil, nil + } + + var commands map[string]interface{} + if err := dt.ParseCommands(&commands); err != nil { + return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err) + } + + var values []models.ValueDescriptor + if dt.Category == models.CategorySensor { + if err := dt.ParseValues(&values); err != nil { + return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err) + } + } + + return &DeviceTemplateResponse{ + ID: dt.ID, + Name: dt.Name, + Manufacturer: dt.Manufacturer, + Description: dt.Description, + Category: dt.Category, + Commands: commands, + Values: values, + CreatedAt: dt.CreatedAt.Format(time.RFC3339), + UpdatedAt: dt.UpdatedAt.Format(time.RFC3339), + }, nil +} + +// NewListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片 +func NewListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) { + list := make([]*DeviceTemplateResponse, 0, len(dts)) + for _, dt := range dts { + resp, err := NewDeviceTemplateResponse(dt) + if err != nil { + return nil, err + } + list = append(list, resp) + } + return list, nil +} diff --git a/internal/app/dto/device_dto.go b/internal/app/dto/device_dto.go new file mode 100644 index 0000000..d37f3f6 --- /dev/null +++ b/internal/app/dto/device_dto.go @@ -0,0 +1,96 @@ +package dto + +import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + +// CreateDeviceRequest 定义了创建设备时需要传入的参数 +type CreateDeviceRequest struct { + Name string `json:"name" binding:"required"` + DeviceTemplateID uint `json:"device_template_id" binding:"required"` + AreaControllerID uint `json:"area_controller_id" binding:"required"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +} + +// UpdateDeviceRequest 定义了更新设备时需要传入的参数 +type UpdateDeviceRequest struct { + Name string `json:"name" binding:"required"` + DeviceTemplateID uint `json:"device_template_id" binding:"required"` + AreaControllerID uint `json:"area_controller_id" binding:"required"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +} + +// CreateAreaControllerRequest 定义了创建区域主控时需要传入的参数 +type CreateAreaControllerRequest struct { + Name string `json:"name" binding:"required"` + NetworkID string `json:"network_id" binding:"required"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +} + +// UpdateAreaControllerRequest 定义了更新区域主控时需要传入的参数 +type UpdateAreaControllerRequest struct { + Name string `json:"name" binding:"required"` + NetworkID string `json:"network_id" binding:"required"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +} + +// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数 +type CreateDeviceTemplateRequest struct { + Name string `json:"name" binding:"required"` + Manufacturer string `json:"manufacturer,omitempty"` + Description string `json:"description,omitempty"` + Category models.DeviceCategory `json:"category" binding:"required"` + Commands map[string]interface{} `json:"commands" binding:"required"` + Values []models.ValueDescriptor `json:"values,omitempty"` +} + +// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数 +type UpdateDeviceTemplateRequest struct { + Name string `json:"name" binding:"required"` + Manufacturer string `json:"manufacturer,omitempty"` + Description string `json:"description,omitempty"` + Category models.DeviceCategory `json:"category" binding:"required"` + Commands map[string]interface{} `json:"commands" binding:"required"` + Values []models.ValueDescriptor `json:"values,omitempty"` +} + +// DeviceResponse 定义了返回给客户端的单个设备信息的结构 +type DeviceResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + DeviceTemplateID uint `json:"device_template_id"` + DeviceTemplateName string `json:"device_template_name"` + AreaControllerID uint `json:"area_controller_id"` + AreaControllerName string `json:"area_controller_name"` + Location string `json:"location"` + Properties map[string]interface{} `json:"properties"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// AreaControllerResponse 定义了返回给客户端的单个区域主控信息的结构 +type AreaControllerResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + NetworkID string `json:"network_id"` + Location string `json:"location"` + Status string `json:"status"` + Properties map[string]interface{} `json:"properties"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构 +type DeviceTemplateResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Manufacturer string `json:"manufacturer"` + Description string `json:"description"` + Category models.DeviceCategory `json:"category"` + Commands map[string]interface{} `json:"commands"` + Values []models.ValueDescriptor `json:"values"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/app/dto/pig_farm_dto.go b/internal/app/dto/pig_farm_dto.go new file mode 100644 index 0000000..2b5b29b --- /dev/null +++ b/internal/app/dto/pig_farm_dto.go @@ -0,0 +1,48 @@ +package dto + +import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + +// PigHouseResponse 定义了猪舍信息的响应结构 +type PigHouseResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// PenResponse 定义了猪栏信息的响应结构 +type PenResponse struct { + ID uint `json:"id"` + PenNumber string `json:"pen_number"` + HouseID uint `json:"house_id"` + Capacity int `json:"capacity"` + Status models.PenStatus `json:"status"` + PigBatchID uint `json:"pig_batch_id"` +} + +// CreatePigHouseRequest 定义了创建猪舍的请求结构 +type CreatePigHouseRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +// UpdatePigHouseRequest 定义了更新猪舍的请求结构 +type UpdatePigHouseRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` +} + +// CreatePenRequest 定义了创建猪栏的请求结构 +type CreatePenRequest struct { + PenNumber string `json:"pen_number" binding:"required"` + HouseID uint `json:"house_id" binding:"required"` + Capacity int `json:"capacity" binding:"required"` + Status models.PenStatus `json:"status" binding:"required"` +} + +// UpdatePenRequest 定义了更新猪栏的请求结构 +type UpdatePenRequest struct { + PenNumber string `json:"pen_number" binding:"required"` + HouseID uint `json:"house_id" binding:"required"` + Capacity int `json:"capacity" binding:"required"` + Status models.PenStatus `json:"status" binding:"required"` +} diff --git a/internal/app/dto/plan_converter.go b/internal/app/dto/plan_converter.go new file mode 100644 index 0000000..ed0d5fd --- /dev/null +++ b/internal/app/dto/plan_converter.go @@ -0,0 +1,145 @@ +package dto + +import ( + "encoding/json" + "errors" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// NewPlanFromCreateRequest 将 CreatePlanRequest 转换为 models.Plan +func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { + plan := &models.Plan{ + Name: req.Name, + Description: req.Description, + ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, + CronExpression: req.CronExpression, + Status: models.PlanStatusDisabled, // 默认创建时为禁用状态 + } + + if len(req.SubPlanIDs) > 0 { + plan.ContentType = models.PlanContentTypeSubPlans + for _, subPlanID := range req.SubPlanIDs { + plan.SubPlans = append(plan.SubPlans, models.SubPlan{ + ChildPlanID: subPlanID, + }) + } + } else if len(req.Tasks) > 0 { + plan.ContentType = models.PlanContentTypeTasks + for _, taskReq := range req.Tasks { + parametersJSON, err := json.Marshal(taskReq.Parameters) + if err != nil { + return nil, fmt.Errorf("序列化任务参数失败: %w", err) + } + plan.Tasks = append(plan.Tasks, models.Task{ + Name: taskReq.Name, + Description: taskReq.Description, + ExecutionOrder: taskReq.ExecutionOrder, + Type: taskReq.Type, + Parameters: parametersJSON, + }) + } + } else { + return nil, errors.New("计划必须包含子计划或任务") + } + + return plan, nil +} + +// NewPlanFromUpdateRequest 将 UpdatePlanRequest 转换为 models.Plan +func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { + plan := &models.Plan{ + Name: req.Name, + Description: req.Description, + ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, + CronExpression: req.CronExpression, + } + + if len(req.SubPlanIDs) > 0 { + plan.ContentType = models.PlanContentTypeSubPlans + for _, subPlanID := range req.SubPlanIDs { + plan.SubPlans = append(plan.SubPlans, models.SubPlan{ + ChildPlanID: subPlanID, + }) + } + } else if len(req.Tasks) > 0 { + plan.ContentType = models.PlanContentTypeTasks + for _, taskReq := range req.Tasks { + parametersJSON, err := json.Marshal(taskReq.Parameters) + if err != nil { + return nil, fmt.Errorf("序列化任务参数失败: %w", err) + } + plan.Tasks = append(plan.Tasks, models.Task{ + Name: taskReq.Name, + Description: taskReq.Description, + ExecutionOrder: taskReq.ExecutionOrder, + Type: taskReq.Type, + Parameters: parametersJSON, + }) + } + } else { + return nil, errors.New("计划必须包含子计划或任务") + } + + return plan, nil +} + +// NewPlanToResponse 将 models.Plan 转换为 PlanResponse +func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) { + if plan == nil { + return nil, nil + } + + resp := &PlanResponse{ + ID: plan.ID, + Name: plan.Name, + Description: plan.Description, + ExecutionType: plan.ExecutionType, + Status: plan.Status, + ExecuteNum: plan.ExecuteNum, + ExecuteCount: plan.ExecuteCount, + CronExpression: plan.CronExpression, + ContentType: plan.ContentType, + } + + if plan.ContentType == models.PlanContentTypeSubPlans && len(plan.SubPlans) > 0 { + resp.SubPlans = make([]SubPlanResponse, 0, len(plan.SubPlans)) + for _, sp := range plan.SubPlans { + childPlanResp, err := NewPlanToResponse(sp.ChildPlan) // 递归调用 + if err != nil { + return nil, err + } + resp.SubPlans = append(resp.SubPlans, SubPlanResponse{ + ID: sp.ID, + ParentPlanID: sp.ParentPlanID, + ChildPlanID: sp.ChildPlanID, + ExecutionOrder: sp.ExecutionOrder, + ChildPlan: childPlanResp, + }) + } + } else if plan.ContentType == models.PlanContentTypeTasks && len(plan.Tasks) > 0 { + resp.Tasks = make([]TaskResponse, 0, len(plan.Tasks)) + for _, task := range plan.Tasks { + var parameters map[string]interface{} + if len(task.Parameters) > 0 && string(task.Parameters) != "null" { + if err := json.Unmarshal(task.Parameters, ¶meters); err != nil { + return nil, fmt.Errorf("解析任务参数失败 (ID: %d): %w", task.ID, err) + } + } + resp.Tasks = append(resp.Tasks, TaskResponse{ + ID: task.ID, + PlanID: task.PlanID, + Name: task.Name, + Description: task.Description, + ExecutionOrder: task.ExecutionOrder, + Type: task.Type, + Parameters: parameters, + }) + } + } + + return resp, nil +} diff --git a/internal/app/dto/plan_dto.go b/internal/app/dto/plan_dto.go new file mode 100644 index 0000000..37935a2 --- /dev/null +++ b/internal/app/dto/plan_dto.go @@ -0,0 +1,75 @@ +package dto + +import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + +// CreatePlanRequest 定义创建计划请求的结构体 +type CreatePlanRequest struct { + Name string `json:"name" binding:"required" example:"猪舍温度控制计划"` + Description string `json:"description" example:"根据温度自动调节风扇和加热器"` + ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"` + ExecuteNum uint `json:"execute_num,omitempty" example:"10"` + CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` + SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` + Tasks []TaskRequest `json:"tasks,omitempty"` +} + +// PlanResponse 定义计划详情响应的结构体 +type PlanResponse struct { + ID uint `json:"id" example:"1"` + Name string `json:"name" example:"猪舍温度控制计划"` + Description string `json:"description" example:"根据温度自动调节风扇和加热器"` + ExecutionType models.PlanExecutionType `json:"execution_type" example:"自动"` + Status models.PlanStatus `json:"status" example:"已启用"` + ExecuteNum uint `json:"execute_num" example:"10"` + ExecuteCount uint `json:"execute_count" example:"0"` + CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` + ContentType models.PlanContentType `json:"content_type" example:"任务"` + SubPlans []SubPlanResponse `json:"sub_plans,omitempty"` + Tasks []TaskResponse `json:"tasks,omitempty"` +} + +// ListPlansResponse 定义获取计划列表响应的结构体 +type ListPlansResponse struct { + Plans []PlanResponse `json:"plans"` + Total int `json:"total" example:"100"` +} + +// UpdatePlanRequest 定义更新计划请求的结构体 +type UpdatePlanRequest struct { + Name string `json:"name" example:"猪舍温度控制计划V2"` + Description string `json:"description" example:"更新后的描述"` + ExecutionType models.PlanExecutionType `json:"execution_type" binding:"required" example:"自动"` + ExecuteNum uint `json:"execute_num,omitempty" example:"10"` + CronExpression string `json:"cron_expression" example:"0 0 6 * * *"` + SubPlanIDs []uint `json:"sub_plan_ids,omitempty"` + Tasks []TaskRequest `json:"tasks,omitempty"` +} + +// SubPlanResponse 定义子计划响应结构体 +type SubPlanResponse struct { + ID uint `json:"id" example:"1"` + ParentPlanID uint `json:"parent_plan_id" example:"1"` + ChildPlanID uint `json:"child_plan_id" example:"2"` + ExecutionOrder int `json:"execution_order" example:"1"` + ChildPlan *PlanResponse `json:"child_plan,omitempty"` +} + +// TaskRequest 定义任务请求结构体 +type TaskRequest struct { + Name string `json:"name" example:"打开风扇"` + Description string `json:"description" example:"打开1号风扇"` + ExecutionOrder int `json:"execution_order" example:"1"` + Type models.TaskType `json:"type" example:"等待"` + Parameters map[string]interface{} `json:"parameters,omitempty"` +} + +// TaskResponse 定义任务响应结构体 +type TaskResponse struct { + ID int `json:"id" example:"1"` + PlanID uint `json:"plan_id" example:"1"` + Name string `json:"name" example:"打开风扇"` + Description string `json:"description" example:"打开1号风扇"` + ExecutionOrder int `json:"execution_order" example:"1"` + Type models.TaskType `json:"type" example:"等待"` + Parameters map[string]interface{} `json:"parameters,omitempty"` +} diff --git a/internal/app/dto/user_dto.go b/internal/app/dto/user_dto.go new file mode 100644 index 0000000..20d5fe7 --- /dev/null +++ b/internal/app/dto/user_dto.go @@ -0,0 +1,43 @@ +package dto + +// CreateUserRequest 定义创建用户请求的结构体 +type CreateUserRequest struct { + Username string `json:"username" binding:"required" example:"newuser"` + Password string `json:"password" binding:"required" example:"password123"` +} + +// LoginRequest 定义登录请求的结构体 +type LoginRequest struct { + // Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 + Identifier string `json:"identifier" binding:"required" example:"testuser"` + Password string `json:"password" binding:"required" example:"password123"` +} + +// CreateUserResponse 定义创建用户成功响应的结构体 +type CreateUserResponse struct { + Username string `json:"username" example:"newuser"` + ID uint `json:"id" example:"1"` +} + +// LoginResponse 定义登录成功响应的结构体 +type LoginResponse struct { + Username string `json:"username" example:"testuser"` + ID uint `json:"id" example:"1"` + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` +} + +// HistoryResponse 定义单条操作历史的响应结构体 +type HistoryResponse struct { + UserID uint `json:"user_id" example:"101"` + Username string `json:"username" example:"testuser"` + ActionType string `json:"action_type" example:"更新设备"` + Description string `json:"description" example:"设备更新成功"` + TargetResource interface{} `json:"target_resource"` + Time string `json:"time"` +} + +// ListHistoryResponse 定义操作历史列表的响应结构体 +type ListHistoryResponse struct { + History []HistoryResponse `json:"history"` + Total int64 `json:"total" example:"100"` +} From 4e17ddf6384ed546818585f121c8a9c9ecab4171 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 23:42:14 +0800 Subject: [PATCH 16/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=8C=AA=E6=89=B9?= =?UTF-8?q?=E6=AC=A1=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 --- internal/app/api/api.go | 41 ++-- .../management/pig_batch_controller.go | 190 ++++++++++++++++++ internal/app/dto/pig_batch_dto.go | 45 +++++ internal/app/service/pig_batch_service.go | 176 ++++++++++++++++ internal/core/application.go | 3 + .../infra/repository/pig_batch_repository.go | 89 ++++++++ 6 files changed, 531 insertions(+), 13 deletions(-) create mode 100644 internal/app/controller/management/pig_batch_controller.go create mode 100644 internal/app/dto/pig_batch_dto.go create mode 100644 internal/app/service/pig_batch_service.go create mode 100644 internal/infra/repository/pig_batch_repository.go diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 8af8009..5758ab9 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -37,19 +37,20 @@ import ( // API 结构体定义了 HTTP 服务器及其依赖 type API struct { - engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 - logger *logs.Logger // 日志记录器,用于输出日志信息 - userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 - tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 - auditService audit.Service // 审计服务,用于记录用户操作 - httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 - config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig - userController *user.Controller // 用户控制器实例 - deviceController *device.Controller // 设备控制器实例 - planController *plan.Controller // 计划控制器实例 - pigFarmController *management.PigFarmController // 猪场管理控制器实例 - listenHandler webhook.ListenHandler // 设备上行事件监听器 - analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 + engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 + logger *logs.Logger // 日志记录器,用于输出日志信息 + userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 + tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 + auditService audit.Service // 审计服务,用于记录用户操作 + 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 // 猪批次控制器实例 + listenHandler webhook.ListenHandler // 设备上行事件监听器 + analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } // NewAPI 创建并返回一个新的 API 实例 @@ -62,6 +63,7 @@ func NewAPI(cfg config.ServerConfig, deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 planRepository repository.PlanRepository, pigFarmService service.PigFarmService, + pigBatchService service.PigBatchService, // 添加猪批次服务 userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 @@ -96,6 +98,8 @@ func NewAPI(cfg config.ServerConfig, planController: plan.NewController(logger, planRepository, analysisTaskManager), // 在 NewAPI 中初始化猪场管理控制器 pigFarmController: management.NewPigFarmController(logger, pigFarmService), + // 在 NewAPI 中初始化猪批次控制器 + pigBatchController: management.NewPigBatchController(logger, pigBatchService), } api.setupRoutes() // 设置所有路由 @@ -221,6 +225,17 @@ func (a *API) setupRoutes() { } a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") + // 猪批次相关路由组 + pigBatchGroup := authGroup.Group("/pig-batches") + { + pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch) + pigBatchGroup.GET("", a.pigBatchController.ListPigBatches) + pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) + pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) + pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) + } + a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") + } } diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go new file mode 100644 index 0000000..538947f --- /dev/null +++ b/internal/app/controller/management/pig_batch_controller.go @@ -0,0 +1,190 @@ +package management + +import ( + "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/gin-gonic/gin" +) + +// PigBatchController 负责处理猪批次相关的API请求 +type PigBatchController struct { + logger *logs.Logger + service service.PigBatchService +} + +// NewPigBatchController 创建一个新的 PigBatchController 实例 +func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) *PigBatchController { + return &PigBatchController{ + logger: logger, + service: service, + } +} + +// CreatePigBatch godoc +// @Summary 创建猪批次 +// @Description 创建一个新的猪批次 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param body body dto.PigBatchCreateDTO true "猪批次信息" +// @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功" +// @Failure 400 {object} controller.Response "请求参数错误" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches [post] +func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { + const action = "创建猪批次" + var req dto.PigBatchCreateDTO + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + respDTO, err := c.service.CreatePigBatch(&req) + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪批次失败", action, "业务逻辑失败", req) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", respDTO, action, "创建成功", respDTO) +} + +// GetPigBatch godoc +// @Summary 获取单个猪批次 +// @Description 根据ID获取单个猪批次信息 +// @Tags 猪批次管理 +// @Produce json +// @Param id path int true "猪批次ID" +// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功" +// @Failure 400 {object} controller.Response "无效的ID格式" +// @Failure 404 {object} controller.Response "猪批次不存在" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches/{id} [get] +func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { + const action = "获取猪批次" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + respDTO, err := c.service.GetPigBatch(uint(id)) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次失败", action, "业务逻辑失败", id) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTO, action, "获取成功", respDTO) +} + +// UpdatePigBatch godoc +// @Summary 更新猪批次 +// @Description 更新一个已存在的猪批次信息 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.PigBatchUpdateDTO true "猪批次信息" +// @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功" +// @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" +// @Failure 404 {object} controller.Response "猪批次不存在" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches/{id} [put] +func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { + const action = "更新猪批次" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.PigBatchUpdateDTO + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + respDTO, err := c.service.UpdatePigBatch(uint(id), &req) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次失败", action, "业务逻辑失败", req) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", respDTO, action, "更新成功", respDTO) +} + +// DeletePigBatch godoc +// @Summary 删除猪批次 +// @Description 根据ID删除一个猪批次 +// @Tags 猪批次管理 +// @Produce json +// @Param id path int true "猪批次ID" +// @Success 200 {object} controller.Response "删除成功" +// @Failure 400 {object} controller.Response "无效的ID格式" +// @Failure 404 {object} controller.Response "猪批次不存在" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches/{id} [delete] +func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { + const action = "删除猪批次" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + if err := c.service.DeletePigBatch(uint(id)); err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪批次失败", action, "业务逻辑失败", id) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id) +} + +// ListPigBatches godoc +// @Summary 获取猪批次列表 +// @Description 获取所有猪批次的列表,支持按活跃状态筛选 +// @Tags 猪批次管理 +// @Produce json +// @Param is_active query bool false "是否活跃 (true/false)" +// @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches [get] +func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { + const action = "获取猪批次列表" + var query dto.PigBatchQueryDTO + // ShouldBindQuery 会自动处理 URL 查询参数,例如 ?is_active=true + if err := ctx.ShouldBindQuery(&query); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", nil) + return + } + + respDTOs, err := c.service.ListPigBatches(query.IsActive) + if err != nil { + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次列表失败", action, "业务逻辑失败", nil) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs) +} diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go new file mode 100644 index 0000000..c90bedd --- /dev/null +++ b/internal/app/dto/pig_batch_dto.go @@ -0,0 +1,45 @@ +package dto + +import ( + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" // 导入 models 包以使用 PigBatchOriginType 和 PigBatchStatus +) + +// PigBatchCreateDTO 定义了创建猪批次的请求结构 +type PigBatchCreateDTO struct { + BatchNumber string `json:"batch_number" binding:"required"` // 批次编号,必填 + OriginType models.PigBatchOriginType `json:"origin_type" binding:"required"` // 批次来源,必填 + StartDate time.Time `json:"start_date" binding:"required"` // 批次开始日期,必填 + InitialCount int `json:"initial_count" binding:"required,min=1"` // 初始数量,必填,最小为1 + Status models.PigBatchStatus `json:"status" binding:"required"` // 批次状态,必填 +} + +// PigBatchUpdateDTO 定义了更新猪批次的请求结构 +type PigBatchUpdateDTO struct { + BatchNumber *string `json:"batch_number"` // 批次编号,可选 + OriginType *models.PigBatchOriginType `json:"origin_type"` // 批次来源,可选 + StartDate *time.Time `json:"start_date"` // 批次开始日期,可选 + EndDate *time.Time `json:"end_date"` // 批次结束日期,可选 + InitialCount *int `json:"initial_count"` // 初始数量,可选 + Status *models.PigBatchStatus `json:"status"` // 批次状态,可选 +} + +// PigBatchQueryDTO 定义了查询猪批次的请求结构 +type PigBatchQueryDTO struct { + IsActive *bool `json:"is_active" form:"is_active"` // 是否活跃,可选,用于URL查询参数 +} + +// PigBatchResponseDTO 定义了猪批次信息的响应结构 +type PigBatchResponseDTO struct { + ID uint `json:"id"` // 批次ID + BatchNumber string `json:"batch_number"` // 批次编号 + OriginType models.PigBatchOriginType `json:"origin_type"` // 批次来源 + StartDate time.Time `json:"start_date"` // 批次开始日期 + EndDate time.Time `json:"end_date"` // 批次结束日期 + InitialCount int `json:"initial_count"` // 初始数量 + Status models.PigBatchStatus `json:"status"` // 批次状态 + IsActive bool `json:"is_active"` // 是否活跃 + CreateTime time.Time `json:"create_time"` // 创建时间 + UpdateTime time.Time `json:"update_time"` // 更新时间 +} diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go new file mode 100644 index 0000000..5b6eb13 --- /dev/null +++ b/internal/app/service/pig_batch_service.go @@ -0,0 +1,176 @@ +package service + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" + "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 ( + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") // 新增错误:活跃的猪批次不能被删除 +) + +// PigBatchService 提供了猪批次管理的业务逻辑 +type PigBatchService interface { + CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) + GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) + UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) + DeletePigBatch(id uint) error + ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) +} + +type pigBatchService struct { + logger *logs.Logger + repo repository.PigBatchRepository +} + +// NewPigBatchService 创建一个新的 PigBatchService 实例 +func NewPigBatchService(repo repository.PigBatchRepository, logger *logs.Logger) PigBatchService { + return &pigBatchService{ + logger: logger, + repo: repo, + } +} + +// toPigBatchResponseDTO 将 models.PigBatch 转换为 dto.PigBatchResponseDTO +func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.PigBatchResponseDTO { + if batch == nil { + return nil + } + return &dto.PigBatchResponseDTO{ + ID: batch.ID, + BatchNumber: batch.BatchNumber, + OriginType: batch.OriginType, + StartDate: batch.StartDate, + EndDate: batch.EndDate, + InitialCount: batch.InitialCount, + Status: batch.Status, + IsActive: batch.IsActive(), // 使用模型自带的 IsActive 方法 + CreateTime: batch.CreatedAt, + UpdateTime: batch.UpdatedAt, + } +} + +// CreatePigBatch 处理创建猪批次的业务逻辑 +func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) { + batch := &models.PigBatch{ + BatchNumber: dto.BatchNumber, + OriginType: dto.OriginType, + StartDate: dto.StartDate, + InitialCount: dto.InitialCount, + Status: dto.Status, + } + + createdBatch, err := s.repo.CreatePigBatch(batch) + if err != nil { + s.logger.Errorf("创建猪批次失败: %v", err) + return nil, err + } + + return s.toPigBatchResponseDTO(createdBatch), nil +} + +// GetPigBatch 处理获取单个猪批次的业务逻辑 +func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) { + batch, err := s.repo.GetPigBatchByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPigBatchNotFound + } + s.logger.Errorf("获取猪批次失败,ID: %d, 错误: %v", id, err) + return nil, err + } + + return s.toPigBatchResponseDTO(batch), nil +} + +// UpdatePigBatch 处理更新猪批次的业务逻辑 +func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { + existingBatch, err := s.repo.GetPigBatchByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPigBatchNotFound + } + s.logger.Errorf("更新猪批次失败,获取原批次信息错误,ID: %d, 错误: %v", id, err) + return nil, err + } + + // 根据 DTO 中的非空字段更新模型 + if dto.BatchNumber != nil { + existingBatch.BatchNumber = *dto.BatchNumber + } + if dto.OriginType != nil { + existingBatch.OriginType = *dto.OriginType + } + if dto.StartDate != nil { + existingBatch.StartDate = *dto.StartDate + } + if dto.EndDate != nil { + existingBatch.EndDate = *dto.EndDate + } + if dto.InitialCount != nil { + existingBatch.InitialCount = *dto.InitialCount + } + if dto.Status != nil { + existingBatch.Status = *dto.Status + } + + updatedBatch, err := s.repo.UpdatePigBatch(existingBatch) + if err != nil { + s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err) + return nil, err + } + + return s.toPigBatchResponseDTO(updatedBatch), nil +} + +// DeletePigBatch 处理删除猪批次的业务逻辑 +func (s *pigBatchService) DeletePigBatch(id uint) error { + // 1. 获取猪批次信息 + batch, err := s.repo.GetPigBatchByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + s.logger.Errorf("删除猪批次失败,获取批次信息错误,ID: %d, 错误: %v", id, err) + return err + } + + // 2. 检查猪批次是否活跃 + if batch.IsActive() { + return ErrPigBatchActive // 如果活跃,则不允许删除 + } + + // 3. 执行删除操作 + err = s.repo.DeletePigBatch(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) || errors.New("未找到要删除的猪批次").Error() == err.Error() { + return ErrPigBatchNotFound + } + s.logger.Errorf("删除猪批次失败,ID: %d, 错误: %v", id, err) + return err + } + return nil +} + +// ListPigBatches 处理批量查询猪批次的业务逻辑 +func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) { + batches, err := s.repo.ListPigBatches(isActive) + if err != nil { + s.logger.Errorf("批量查询猪批次失败,错误: %v", err) + return nil, err + } + + var responseDTOs []*dto.PigBatchResponseDTO + for _, batch := range batches { + responseDTOs = append(responseDTOs, s.toPigBatchResponseDTO(batch)) + } + + return responseDTOs, nil +} diff --git a/internal/core/application.go b/internal/core/application.go index 427279f..02039e2 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -72,9 +72,11 @@ func NewApplication(configPath string) (*Application, error) { deviceCommandLogRepo := repository.NewGormDeviceCommandLogRepository(storage.GetDB()) pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) + pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) + pigBatchService := service.NewPigBatchService(pigBatchRepo, logger) // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) @@ -121,6 +123,7 @@ func NewApplication(configPath string) (*Application, error) { deviceTemplateRepo, planRepo, pigFarmService, + pigBatchService, userActionLogRepo, tokenService, auditService, diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go new file mode 100644 index 0000000..eb3bdf3 --- /dev/null +++ b/internal/infra/repository/pig_batch_repository.go @@ -0,0 +1,89 @@ +package repository + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigBatchRepository 定义了与猪批次相关的数据库操作接口 +type PigBatchRepository interface { + CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + GetPigBatchByID(id uint) (*models.PigBatch, error) + UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + DeletePigBatch(id uint) error + ListPigBatches(isActive *bool) ([]*models.PigBatch, error) +} + +// gormPigBatchRepository 是 PigBatchRepository 的 GORM 实现 +type gormPigBatchRepository struct { + db *gorm.DB +} + +// NewGormPigBatchRepository 创建一个新的 PigBatchRepository GORM 实现实例 +func NewGormPigBatchRepository(db *gorm.DB) PigBatchRepository { + return &gormPigBatchRepository{db: db} +} + +// CreatePigBatch 创建一个新的猪批次 +func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { + if err := r.db.Create(batch).Error; err != nil { + return nil, err + } + return batch, nil +} + +// GetPigBatchByID 根据ID获取单个猪批次 +func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { + var batch models.PigBatch + if err := r.db.First(&batch, id).Error; err != nil { + return nil, err + } + return &batch, nil +} + +// UpdatePigBatch 更新一个猪批次 +func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { + result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, errors.New("未找到要更新的猪批次或数据未改变") // 明确返回错误,而不是 gorm.ErrRecordNotFound + } + return batch, nil +} + +// DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除) +func (r *gormPigBatchRepository) DeletePigBatch(id uint) error { + result := r.db.Delete(&models.PigBatch{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return errors.New("未找到要删除的猪批次") // 明确返回错误 + } + return nil +} + +// ListPigBatches 批量查询猪批次,支持根据 IsActive 筛选 +func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) { + var batches []*models.PigBatch + query := r.db.Model(&models.PigBatch{}) + + if isActive != nil { + if *isActive { + // 查询活跃的批次:状态不是已出售或已归档 + query = query.Where("status NOT IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived}) + } else { + // 查询非活跃的批次:状态是已出售或已归档 + query = query.Where("status IN (?) ", []models.PigBatchStatus{models.BatchStatusSold, models.BatchStatusArchived}) + } + } + + if err := query.Find(&batches).Error; err != nil { + return nil, err + } + return batches, nil +} From c27b5bd708870b465fe2fdda2b45258aa3702116 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 23:52:25 +0800 Subject: [PATCH 17/65] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=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 --- internal/app/controller/plan/converter.go | 226 --------- .../app/controller/plan/converter_test.go | 459 ------------------ internal/app/dto/plan_converter.go | 307 +++++++----- 3 files changed, 194 insertions(+), 798 deletions(-) delete mode 100644 internal/app/controller/plan/converter.go delete mode 100644 internal/app/controller/plan/converter_test.go diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go deleted file mode 100644 index 57dbf96..0000000 --- a/internal/app/controller/plan/converter.go +++ /dev/null @@ -1,226 +0,0 @@ -package plan - -import ( - "encoding/json" - "fmt" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" -) - -// PlanToResponse 将Plan模型转换为PlanResponse -func PlanToResponse(plan *models.Plan) (*PlanResponse, error) { - if plan == nil { - return nil, nil - } - - response := &PlanResponse{ - ID: plan.ID, - Name: plan.Name, - Description: plan.Description, - ExecutionType: plan.ExecutionType, - Status: plan.Status, - ExecuteNum: plan.ExecuteNum, - ExecuteCount: plan.ExecuteCount, - CronExpression: plan.CronExpression, - ContentType: plan.ContentType, - } - - // 转换子计划 - if plan.ContentType == models.PlanContentTypeSubPlans { - response.SubPlans = make([]SubPlanResponse, len(plan.SubPlans)) - for i, subPlan := range plan.SubPlans { - subPlanResp, err := SubPlanToResponse(&subPlan) - if err != nil { - return nil, err - } - response.SubPlans[i] = subPlanResp - } - } - - // 转换任务 - if plan.ContentType == models.PlanContentTypeTasks { - response.Tasks = make([]TaskResponse, len(plan.Tasks)) - for i, task := range plan.Tasks { - taskResp, err := TaskToResponse(&task) - if err != nil { - return nil, err - } - response.Tasks[i] = taskResp - } - } - - return response, nil -} - -// PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 -func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { - if req == nil { - return nil, nil - } - - plan := &models.Plan{ - Name: req.Name, - Description: req.Description, - ExecutionType: req.ExecutionType, - ExecuteNum: req.ExecuteNum, - CronExpression: req.CronExpression, - // ContentType 在控制器中设置,此处不再处理 - } - - // 处理子计划 (通过ID引用) - if req.SubPlanIDs != nil { - subPlanSlice := req.SubPlanIDs - plan.SubPlans = make([]models.SubPlan, len(subPlanSlice)) - for i, childPlanID := range subPlanSlice { - plan.SubPlans[i] = models.SubPlan{ - ChildPlanID: childPlanID, - ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 - } - } - } - - // 处理任务 - if req.Tasks != nil { - taskSlice := req.Tasks - plan.Tasks = make([]models.Task, len(taskSlice)) - for i, taskReq := range taskSlice { - task, err := TaskFromRequest(&taskReq) - if err != nil { - return nil, err - } - plan.Tasks[i] = task - } - } - - // 1. 首先,执行重复性验证 - if err := plan.ValidateExecutionOrder(); err != nil { - // 如果检测到重复,立即返回错误 - return nil, err - } - - // 2. 然后,调用方法来修复顺序断层 - plan.ReorderSteps() - - return plan, nil -} - -// PlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证 -func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { - if req == nil { - return nil, nil - } - - plan := &models.Plan{ - Name: req.Name, - Description: req.Description, - ExecutionType: req.ExecutionType, - ExecuteNum: req.ExecuteNum, - CronExpression: req.CronExpression, - // ContentType 在控制器中设置,此处不再处理 - } - - // 处理子计划 (通过ID引用) - if req.SubPlanIDs != nil { - subPlanSlice := req.SubPlanIDs - plan.SubPlans = make([]models.SubPlan, len(subPlanSlice)) - for i, childPlanID := range subPlanSlice { - plan.SubPlans[i] = models.SubPlan{ - ChildPlanID: childPlanID, - ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 - } - } - } - - // 处理任务 - if req.Tasks != nil { - taskSlice := req.Tasks - plan.Tasks = make([]models.Task, len(taskSlice)) - for i, taskReq := range taskSlice { - task, err := TaskFromRequest(&taskReq) - if err != nil { - return nil, err - } - plan.Tasks[i] = task - } - } - - // 1. 首先,执行重复性验证 - if err := plan.ValidateExecutionOrder(); err != nil { - // 如果检测到重复,立即返回错误 - return nil, err - } - - // 2. 然后,调用方法来修复顺序断层 - plan.ReorderSteps() - - return plan, nil -} - -// SubPlanToResponse 将SubPlan模型转换为SubPlanResponse -func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) { - if subPlan == nil { - return SubPlanResponse{}, nil - } - - response := SubPlanResponse{ - ID: subPlan.ID, - ParentPlanID: subPlan.ParentPlanID, - ChildPlanID: subPlan.ChildPlanID, - ExecutionOrder: subPlan.ExecutionOrder, - } - - // 如果有完整的子计划数据,也进行转换 - if subPlan.ChildPlan != nil { - childPlanResp, err := PlanToResponse(subPlan.ChildPlan) - if err != nil { - return SubPlanResponse{}, err - } - response.ChildPlan = childPlanResp - } - - return response, nil -} - -// TaskToResponse 将Task模型转换为TaskResponse -func TaskToResponse(task *models.Task) (TaskResponse, error) { - if task == nil { - return TaskResponse{}, nil - } - - var params map[string]interface{} - if len(task.Parameters) > 0 && string(task.Parameters) != "null" { - if err := task.ParseParameters(¶ms); err != nil { - return TaskResponse{}, fmt.Errorf("parsing task parameters failed (ID: %d): %w", task.ID, err) - } - } - - return TaskResponse{ - ID: task.ID, - PlanID: task.PlanID, - Name: task.Name, - Description: task.Description, - ExecutionOrder: task.ExecutionOrder, - Type: task.Type, - Parameters: params, - }, nil -} - -// TaskFromRequest 将TaskRequest转换为Task模型 -func TaskFromRequest(req *TaskRequest) (models.Task, error) { - if req == nil { - return models.Task{}, nil - } - - paramsJSON, err := json.Marshal(req.Parameters) - if err != nil { - return models.Task{}, fmt.Errorf("serializing task parameters failed: %w", err) - } - - return models.Task{ - Name: req.Name, - Description: req.Description, - ExecutionOrder: req.ExecutionOrder, - Type: req.Type, - Parameters: paramsJSON, - }, nil -} diff --git a/internal/app/controller/plan/converter_test.go b/internal/app/controller/plan/converter_test.go deleted file mode 100644 index 2626fe8..0000000 --- a/internal/app/controller/plan/converter_test.go +++ /dev/null @@ -1,459 +0,0 @@ -package plan_test - -import ( - "testing" - - "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" - "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/plan" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "github.com/stretchr/testify/assert" - "gorm.io/datatypes" - "gorm.io/gorm" -) - -func TestPlanToResponse(t *testing.T) { - t.Run("nil plan", func(t *testing.T) { - response := plan.PlanToResponse(nil) - assert.Nil(t, response) - }) - - t.Run("basic plan without associations", func(t *testing.T) { - planModel := &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Test Plan", - Description: "A test plan", - ExecutionType: models.PlanExecutionTypeAutomatic, - CronExpression: "0 0 * * *", - ContentType: models.PlanContentTypeTasks, - } - - response := plan.PlanToResponse(planModel) - assert.NotNil(t, response) - assert.Equal(t, uint(1), response.ID) - assert.Equal(t, "Test Plan", response.Name) - assert.Equal(t, "A test plan", response.Description) - assert.Equal(t, models.PlanExecutionTypeAutomatic, response.ExecutionType) - assert.Equal(t, "0 0 * * *", response.CronExpression) - assert.Equal(t, models.PlanContentTypeTasks, response.ContentType) - assert.Empty(t, response.SubPlans) - assert.Empty(t, response.Tasks) - }) - - t.Run("plan with sub plans", func(t *testing.T) { - childPlan := &models.Plan{ - Model: gorm.Model{ID: 2}, - Name: "Child Plan", - ContentType: models.PlanContentTypeTasks, - } - - planModel := &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - Model: gorm.Model{ID: 10}, - ParentPlanID: 1, - ChildPlanID: 2, - ExecutionOrder: 1, - ChildPlan: childPlan, - }, - }, - } - - response := plan.PlanToResponse(planModel) - assert.NotNil(t, response) - assert.Equal(t, uint(1), response.ID) - assert.Equal(t, "Parent Plan", response.Name) - assert.Equal(t, models.PlanContentTypeSubPlans, response.ContentType) - assert.Len(t, response.SubPlans, 1) - assert.Empty(t, response.Tasks) - - subPlanResp := response.SubPlans[0] - assert.Equal(t, uint(10), subPlanResp.ID) - assert.Equal(t, uint(1), subPlanResp.ParentPlanID) - assert.Equal(t, uint(2), subPlanResp.ChildPlanID) - assert.Equal(t, 1, subPlanResp.ExecutionOrder) - assert.NotNil(t, subPlanResp.ChildPlan) - assert.Equal(t, "Child Plan", subPlanResp.ChildPlan.Name) - }) - - t.Run("plan with tasks", func(t *testing.T) { - params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`)) - - planModel := &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Task Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - { - Model: gorm.Model{ID: 10}, - PlanID: 1, - Name: "Task 1", - Description: "First task", - ExecutionOrder: 1, - Type: models.TaskTypeWaiting, - Parameters: params, - }, - }, - } - - response := plan.PlanToResponse(planModel) - assert.NotNil(t, response) - assert.Equal(t, uint(1), response.ID) - assert.Equal(t, "Task Plan", response.Name) - assert.Equal(t, models.PlanContentTypeTasks, response.ContentType) - assert.Len(t, response.Tasks, 1) - assert.Empty(t, response.SubPlans) - - taskResp := response.Tasks[0] - assert.Equal(t, uint(10), taskResp.ID) - assert.Equal(t, uint(1), taskResp.PlanID) - assert.Equal(t, "Task 1", taskResp.Name) - assert.Equal(t, "First task", taskResp.Description) - assert.Equal(t, 1, taskResp.ExecutionOrder) - assert.Equal(t, models.TaskTypeWaiting, taskResp.Type) - assert.Equal(t, controller.Properties(params), taskResp.Parameters) - }) -} - -func TestPlanFromCreateRequest(t *testing.T) { - t.Run("nil request", func(t *testing.T) { - planModel, err := plan.PlanFromCreateRequest(nil) - assert.NoError(t, err) - assert.Nil(t, planModel) - }) - - t.Run("basic plan without associations", func(t *testing.T) { - req := &plan.CreatePlanRequest{ - Name: "Test Plan", - Description: "A test plan", - ExecutionType: models.PlanExecutionTypeAutomatic, - CronExpression: "0 0 * * *", - ContentType: models.PlanContentTypeTasks, - } - - planModel, err := plan.PlanFromCreateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Equal(t, "Test Plan", planModel.Name) - assert.Equal(t, "A test plan", planModel.Description) - assert.Equal(t, models.PlanExecutionTypeAutomatic, planModel.ExecutionType) - assert.Equal(t, "0 0 * * *", planModel.CronExpression) - assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) - assert.Empty(t, planModel.SubPlans) - assert.Empty(t, planModel.Tasks) - }) - - t.Run("plan with sub plan IDs", func(t *testing.T) { - req := &plan.CreatePlanRequest{ - Name: "Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlanIDs: []uint{2, 3}, - } - - planModel, err := plan.PlanFromCreateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Equal(t, "Parent Plan", planModel.Name) - assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType) - assert.Len(t, planModel.SubPlans, 2) - assert.Empty(t, planModel.Tasks) - - assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID) - assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder) - assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID) - assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder) - }) - - t.Run("plan with tasks", func(t *testing.T) { - params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) - - req := &plan.CreatePlanRequest{ - Name: "Task Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - { - Name: "Task 1", - Description: "First task", - ExecutionOrder: 1, - Type: models.TaskTypeWaiting, - Parameters: params, - }, - }, - } - - planModel, err := plan.PlanFromCreateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Equal(t, "Task Plan", planModel.Name) - assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) - assert.Len(t, planModel.Tasks, 1) - assert.Empty(t, planModel.SubPlans) - - task := planModel.Tasks[0] - assert.Equal(t, "Task 1", task.Name) - assert.Equal(t, "First task", task.Description) - assert.Equal(t, 1, task.ExecutionOrder) - assert.Equal(t, models.TaskTypeWaiting, task.Type) - assert.Equal(t, datatypes.JSON(params), task.Parameters) - }) - - t.Run("plan with tasks with gapped execution order", func(t *testing.T) { - req := &plan.CreatePlanRequest{ - Name: "Task Plan with Gaps", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - {Name: "Task 3", ExecutionOrder: 5}, - {Name: "Task 1", ExecutionOrder: 2}, - }, - } - - planModel, err := plan.PlanFromCreateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Len(t, planModel.Tasks, 2) - - // After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered. - assert.Equal(t, "Task 1", planModel.Tasks[0].Name) - assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder) - assert.Equal(t, "Task 3", planModel.Tasks[1].Name) - assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder) - }) - - t.Run("plan with duplicate task execution order", func(t *testing.T) { - req := &plan.CreatePlanRequest{ - Name: "Invalid Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // Duplicate order - }, - } - - planModel, err := plan.PlanFromCreateRequest(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "任务执行顺序重复") - assert.Nil(t, planModel) - }) -} - -func TestPlanFromUpdateRequest(t *testing.T) { - t.Run("nil request", func(t *testing.T) { - planModel, err := plan.PlanFromUpdateRequest(nil) - assert.NoError(t, err) - assert.Nil(t, planModel) - }) - - t.Run("basic plan without associations", func(t *testing.T) { - req := &plan.UpdatePlanRequest{ - Name: "Updated Plan", - Description: "An updated plan", - ExecutionType: models.PlanExecutionTypeManual, - CronExpression: "0 30 * * *", - ContentType: models.PlanContentTypeTasks, - } - - planModel, err := plan.PlanFromUpdateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Equal(t, "Updated Plan", planModel.Name) - assert.Equal(t, "An updated plan", planModel.Description) - assert.Equal(t, models.PlanExecutionTypeManual, planModel.ExecutionType) - assert.Equal(t, "0 30 * * *", planModel.CronExpression) - assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) - assert.Empty(t, planModel.SubPlans) - assert.Empty(t, planModel.Tasks) - }) - - t.Run("plan with sub plan IDs", func(t *testing.T) { - req := &plan.UpdatePlanRequest{ - Name: "Updated Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlanIDs: []uint{2, 3}, - } - - planModel, err := plan.PlanFromUpdateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - - assert.Equal(t, "Updated Parent Plan", planModel.Name) - assert.Equal(t, models.PlanContentTypeSubPlans, planModel.ContentType) - assert.Len(t, planModel.SubPlans, 2) - assert.Empty(t, planModel.Tasks) - - assert.Equal(t, uint(2), planModel.SubPlans[0].ChildPlanID) - assert.Equal(t, 1, planModel.SubPlans[0].ExecutionOrder) - assert.Equal(t, uint(3), planModel.SubPlans[1].ChildPlanID) - assert.Equal(t, 2, planModel.SubPlans[1].ExecutionOrder) - }) - - t.Run("plan with tasks", func(t *testing.T) { - params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) - - req := &plan.UpdatePlanRequest{ - Name: "Updated Task Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - { - Name: "Task 1", - Description: "First task", - ExecutionOrder: 1, - Type: models.TaskTypeWaiting, - Parameters: params, - }, - }, - } - - planModel, err := plan.PlanFromUpdateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Equal(t, "Updated Task Plan", planModel.Name) - assert.Equal(t, models.PlanContentTypeTasks, planModel.ContentType) - assert.Len(t, planModel.Tasks, 1) - assert.Empty(t, planModel.SubPlans) - - task := planModel.Tasks[0] - assert.Equal(t, "Task 1", task.Name) - assert.Equal(t, 1, task.ExecutionOrder) - assert.Equal(t, datatypes.JSON(params), task.Parameters) - }) - - t.Run("plan with duplicate task execution order", func(t *testing.T) { - req := &plan.UpdatePlanRequest{ - Name: "Invalid Updated Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // Duplicate order - }, - } - - planModel, err := plan.PlanFromUpdateRequest(req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "任务执行顺序重复") - assert.Nil(t, planModel) - }) - - t.Run("plan with tasks with gapped execution order", func(t *testing.T) { - req := &plan.UpdatePlanRequest{ - Name: "Updated Task Plan with Gaps", - ContentType: models.PlanContentTypeTasks, - Tasks: []plan.TaskRequest{ - {Name: "Task 3", ExecutionOrder: 5}, - {Name: "Task 1", ExecutionOrder: 2}, - }, - } - - planModel, err := plan.PlanFromUpdateRequest(req) - assert.NoError(t, err) - assert.NotNil(t, planModel) - assert.Len(t, planModel.Tasks, 2) - - // After ReorderSteps, tasks are sorted by their original ExecutionOrder and then re-numbered. - assert.Equal(t, "Task 1", planModel.Tasks[0].Name) - assert.Equal(t, 1, planModel.Tasks[0].ExecutionOrder) - assert.Equal(t, "Task 3", planModel.Tasks[1].Name) - assert.Equal(t, 2, planModel.Tasks[1].ExecutionOrder) - }) -} - -func TestSubPlanToResponse(t *testing.T) { - t.Run("nil sub plan", func(t *testing.T) { - response := plan.SubPlanToResponse(nil) - assert.Equal(t, plan.SubPlanResponse{}, response) - }) - - t.Run("sub plan without child plan", func(t *testing.T) { - subPlan := &models.SubPlan{ - Model: gorm.Model{ID: 10}, - ParentPlanID: 1, - ChildPlanID: 2, - ExecutionOrder: 1, - } - - response := plan.SubPlanToResponse(subPlan) - assert.Equal(t, uint(10), response.ID) - assert.Equal(t, uint(1), response.ParentPlanID) - assert.Equal(t, uint(2), response.ChildPlanID) - assert.Equal(t, 1, response.ExecutionOrder) - assert.Nil(t, response.ChildPlan) - }) - - t.Run("sub plan with child plan", func(t *testing.T) { - childPlan := &models.Plan{ - Model: gorm.Model{ID: 2}, - Name: "Child Plan", - } - - subPlan := &models.SubPlan{ - Model: gorm.Model{ID: 10}, - ParentPlanID: 1, - ChildPlanID: 2, - ExecutionOrder: 1, - ChildPlan: childPlan, - } - - response := plan.SubPlanToResponse(subPlan) - assert.Equal(t, uint(10), response.ID) - assert.Equal(t, uint(1), response.ParentPlanID) - assert.Equal(t, uint(2), response.ChildPlanID) - assert.Equal(t, 1, response.ExecutionOrder) - assert.NotNil(t, response.ChildPlan) - assert.Equal(t, "Child Plan", response.ChildPlan.Name) - }) -} - -func TestTaskToResponse(t *testing.T) { - t.Run("nil task", func(t *testing.T) { - response := plan.TaskToResponse(nil) - assert.Equal(t, plan.TaskResponse{}, response) - }) - - t.Run("task with parameters", func(t *testing.T) { - params := datatypes.JSON([]byte(`{"device_id": 1, "value": 25}`)) - task := &models.Task{ - Model: gorm.Model{ID: 10}, - PlanID: 1, - Name: "Test Task", - Description: "A test task", - ExecutionOrder: 1, - Type: models.TaskTypeWaiting, - Parameters: params, - } - - response := plan.TaskToResponse(task) - assert.Equal(t, uint(10), response.ID) - assert.Equal(t, uint(1), response.PlanID) - assert.Equal(t, "Test Task", response.Name) - assert.Equal(t, "A test task", response.Description) - assert.Equal(t, 1, response.ExecutionOrder) - assert.Equal(t, models.TaskTypeWaiting, response.Type) - assert.Equal(t, controller.Properties(params), response.Parameters) - }) -} - -func TestTaskFromRequest(t *testing.T) { - t.Run("nil request", func(t *testing.T) { - task := plan.TaskFromRequest(nil) - assert.Equal(t, models.Task{}, task) - }) - - t.Run("task with parameters", func(t *testing.T) { - params := controller.Properties([]byte(`{"device_id": 1, "value": 25}`)) - req := &plan.TaskRequest{ - Name: "Test Task", - Description: "A test task", - ExecutionOrder: 1, - Type: models.TaskTypeWaiting, - Parameters: params, - } - - task := plan.TaskFromRequest(req) - assert.Equal(t, "Test Task", task.Name) - assert.Equal(t, "A test task", task.Description) - assert.Equal(t, 1, task.ExecutionOrder) - assert.Equal(t, models.TaskTypeWaiting, task.Type) - assert.Equal(t, datatypes.JSON(params), task.Parameters) - }) -} diff --git a/internal/app/dto/plan_converter.go b/internal/app/dto/plan_converter.go index ed0d5fd..2ead166 100644 --- a/internal/app/dto/plan_converter.go +++ b/internal/app/dto/plan_converter.go @@ -2,98 +2,18 @@ package dto import ( "encoding/json" - "errors" "fmt" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) -// NewPlanFromCreateRequest 将 CreatePlanRequest 转换为 models.Plan -func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { - plan := &models.Plan{ - Name: req.Name, - Description: req.Description, - ExecutionType: req.ExecutionType, - ExecuteNum: req.ExecuteNum, - CronExpression: req.CronExpression, - Status: models.PlanStatusDisabled, // 默认创建时为禁用状态 - } - - if len(req.SubPlanIDs) > 0 { - plan.ContentType = models.PlanContentTypeSubPlans - for _, subPlanID := range req.SubPlanIDs { - plan.SubPlans = append(plan.SubPlans, models.SubPlan{ - ChildPlanID: subPlanID, - }) - } - } else if len(req.Tasks) > 0 { - plan.ContentType = models.PlanContentTypeTasks - for _, taskReq := range req.Tasks { - parametersJSON, err := json.Marshal(taskReq.Parameters) - if err != nil { - return nil, fmt.Errorf("序列化任务参数失败: %w", err) - } - plan.Tasks = append(plan.Tasks, models.Task{ - Name: taskReq.Name, - Description: taskReq.Description, - ExecutionOrder: taskReq.ExecutionOrder, - Type: taskReq.Type, - Parameters: parametersJSON, - }) - } - } else { - return nil, errors.New("计划必须包含子计划或任务") - } - - return plan, nil -} - -// NewPlanFromUpdateRequest 将 UpdatePlanRequest 转换为 models.Plan -func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { - plan := &models.Plan{ - Name: req.Name, - Description: req.Description, - ExecutionType: req.ExecutionType, - ExecuteNum: req.ExecuteNum, - CronExpression: req.CronExpression, - } - - if len(req.SubPlanIDs) > 0 { - plan.ContentType = models.PlanContentTypeSubPlans - for _, subPlanID := range req.SubPlanIDs { - plan.SubPlans = append(plan.SubPlans, models.SubPlan{ - ChildPlanID: subPlanID, - }) - } - } else if len(req.Tasks) > 0 { - plan.ContentType = models.PlanContentTypeTasks - for _, taskReq := range req.Tasks { - parametersJSON, err := json.Marshal(taskReq.Parameters) - if err != nil { - return nil, fmt.Errorf("序列化任务参数失败: %w", err) - } - plan.Tasks = append(plan.Tasks, models.Task{ - Name: taskReq.Name, - Description: taskReq.Description, - ExecutionOrder: taskReq.ExecutionOrder, - Type: taskReq.Type, - Parameters: parametersJSON, - }) - } - } else { - return nil, errors.New("计划必须包含子计划或任务") - } - - return plan, nil -} - -// NewPlanToResponse 将 models.Plan 转换为 PlanResponse +// NewPlanToResponse 将Plan模型转换为PlanResponse func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) { if plan == nil { return nil, nil } - resp := &PlanResponse{ + response := &PlanResponse{ ID: plan.ID, Name: plan.Name, Description: plan.Description, @@ -105,41 +25,202 @@ func NewPlanToResponse(plan *models.Plan) (*PlanResponse, error) { ContentType: plan.ContentType, } - if plan.ContentType == models.PlanContentTypeSubPlans && len(plan.SubPlans) > 0 { - resp.SubPlans = make([]SubPlanResponse, 0, len(plan.SubPlans)) - for _, sp := range plan.SubPlans { - childPlanResp, err := NewPlanToResponse(sp.ChildPlan) // 递归调用 + // 转换子计划 + if plan.ContentType == models.PlanContentTypeSubPlans { + response.SubPlans = make([]SubPlanResponse, len(plan.SubPlans)) + for i, subPlan := range plan.SubPlans { + subPlanResp, err := SubPlanToResponse(&subPlan) if err != nil { return nil, err } - resp.SubPlans = append(resp.SubPlans, SubPlanResponse{ - ID: sp.ID, - ParentPlanID: sp.ParentPlanID, - ChildPlanID: sp.ChildPlanID, - ExecutionOrder: sp.ExecutionOrder, - ChildPlan: childPlanResp, - }) - } - } else if plan.ContentType == models.PlanContentTypeTasks && len(plan.Tasks) > 0 { - resp.Tasks = make([]TaskResponse, 0, len(plan.Tasks)) - for _, task := range plan.Tasks { - var parameters map[string]interface{} - if len(task.Parameters) > 0 && string(task.Parameters) != "null" { - if err := json.Unmarshal(task.Parameters, ¶meters); err != nil { - return nil, fmt.Errorf("解析任务参数失败 (ID: %d): %w", task.ID, err) - } - } - resp.Tasks = append(resp.Tasks, TaskResponse{ - ID: task.ID, - PlanID: task.PlanID, - Name: task.Name, - Description: task.Description, - ExecutionOrder: task.ExecutionOrder, - Type: task.Type, - Parameters: parameters, - }) + response.SubPlans[i] = subPlanResp } } - return resp, nil + // 转换任务 + if plan.ContentType == models.PlanContentTypeTasks { + response.Tasks = make([]TaskResponse, len(plan.Tasks)) + for i, task := range plan.Tasks { + taskResp, err := TaskToResponse(&task) + if err != nil { + return nil, err + } + response.Tasks[i] = taskResp + } + } + + return response, nil +} + +// NewPlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 +func NewPlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { + if req == nil { + return nil, nil + } + + plan := &models.Plan{ + Name: req.Name, + Description: req.Description, + ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, + CronExpression: req.CronExpression, + // ContentType 在控制器中设置,此处不再处理 + } + + // 处理子计划 (通过ID引用) + if req.SubPlanIDs != nil { + subPlanSlice := req.SubPlanIDs + plan.SubPlans = make([]models.SubPlan, len(subPlanSlice)) + for i, childPlanID := range subPlanSlice { + plan.SubPlans[i] = models.SubPlan{ + ChildPlanID: childPlanID, + ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 + } + } + } + + // 处理任务 + if req.Tasks != nil { + taskSlice := req.Tasks + plan.Tasks = make([]models.Task, len(taskSlice)) + for i, taskReq := range taskSlice { + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task + } + } + + // 1. 首先,执行重复性验证 + if err := plan.ValidateExecutionOrder(); err != nil { + // 如果检测到重复,立即返回错误 + return nil, err + } + + // 2. 然后,调用方法来修复顺序断层 + plan.ReorderSteps() + + return plan, nil +} + +// NewPlanFromUpdateRequest 将UpdatePlanRequest转换为Plan模型,并进行业务规则验证 +func NewPlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { + if req == nil { + return nil, nil + } + + plan := &models.Plan{ + Name: req.Name, + Description: req.Description, + ExecutionType: req.ExecutionType, + ExecuteNum: req.ExecuteNum, + CronExpression: req.CronExpression, + // ContentType 在控制器中设置,此处不再处理 + } + + // 处理子计划 (通过ID引用) + if req.SubPlanIDs != nil { + subPlanSlice := req.SubPlanIDs + plan.SubPlans = make([]models.SubPlan, len(subPlanSlice)) + for i, childPlanID := range subPlanSlice { + plan.SubPlans[i] = models.SubPlan{ + ChildPlanID: childPlanID, + ExecutionOrder: i, // 默认执行顺序, ReorderSteps会再次确认 + } + } + } + + // 处理任务 + if req.Tasks != nil { + taskSlice := req.Tasks + plan.Tasks = make([]models.Task, len(taskSlice)) + for i, taskReq := range taskSlice { + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task + } + } + + // 1. 首先,执行重复性验证 + if err := plan.ValidateExecutionOrder(); err != nil { + // 如果检测到重复,立即返回错误 + return nil, err + } + + // 2. 然后,调用方法来修复顺序断层 + plan.ReorderSteps() + + return plan, nil +} + +// SubPlanToResponse 将SubPlan模型转换为SubPlanResponse +func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) { + if subPlan == nil { + return SubPlanResponse{}, nil + } + + response := SubPlanResponse{ + ID: subPlan.ID, + ParentPlanID: subPlan.ParentPlanID, + ChildPlanID: subPlan.ChildPlanID, + ExecutionOrder: subPlan.ExecutionOrder, + } + + // 如果有完整的子计划数据,也进行转换 + if subPlan.ChildPlan != nil { + childPlanResp, err := NewPlanToResponse(subPlan.ChildPlan) + if err != nil { + return SubPlanResponse{}, err + } + response.ChildPlan = childPlanResp + } + + return response, nil +} + +// TaskToResponse 将Task模型转换为TaskResponse +func TaskToResponse(task *models.Task) (TaskResponse, error) { + if task == nil { + return TaskResponse{}, nil + } + + var params map[string]interface{} + if len(task.Parameters) > 0 && string(task.Parameters) != "null" { + if err := task.ParseParameters(¶ms); err != nil { + return TaskResponse{}, fmt.Errorf("parsing task parameters failed (ID: %d): %w", task.ID, err) + } + } + + return TaskResponse{ + ID: task.ID, + PlanID: task.PlanID, + Name: task.Name, + Description: task.Description, + ExecutionOrder: task.ExecutionOrder, + Type: task.Type, + Parameters: params, + }, nil +} + +// TaskFromRequest 将TaskRequest转换为Task模型 +func TaskFromRequest(req *TaskRequest) (models.Task, error) { + if req == nil { + return models.Task{}, nil + } + + paramsJSON, err := json.Marshal(req.Parameters) + if err != nil { + return models.Task{}, fmt.Errorf("serializing task parameters failed: %w", err) + } + + return models.Task{ + Name: req.Name, + Description: req.Description, + ExecutionOrder: req.ExecutionOrder, + Type: req.Type, + Parameters: paramsJSON, + }, nil } From 9875994df838aaa7ae59f38987fc5a936f333afa Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 23:58:37 +0800 Subject: [PATCH 18/65] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 1871 ----------------- docs/swagger.json | 1842 ---------------- docs/swagger.yaml | 1193 ----------- internal/app/api/api.go | 2 +- .../management/pig_farm_controller.go | 12 +- internal/app/service/pig_farm_service.go | 6 +- 6 files changed, 10 insertions(+), 4916 deletions(-) delete mode 100644 docs/docs.go delete mode 100644 docs/swagger.json delete mode 100644 docs/swagger.yaml diff --git a/docs/docs.go b/docs/docs.go deleted file mode 100644 index fb7fc98..0000000 --- a/docs/docs.go +++ /dev/null @@ -1,1871 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/api/v1/area-controllers": { - "get": { - "description": "获取系统中所有区域主控的列表", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "获取所有区域主控列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新区域主控", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "创建新区域主控", - "parameters": [ - { - "description": "区域主控信息", - "name": "areaController", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateAreaControllerRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/area-controllers/{id}": { - "get": { - "description": "根据ID获取单个区域主控的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "获取区域主控信息", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据ID更新一个已存在的区域主控信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "更新区域主控信息", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的区域主控信息", - "name": "areaController", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateAreaControllerRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据ID删除一个区域主控(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "删除区域主控", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/device-templates": { - "get": { - "description": "获取系统中所有设备模板的列表", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "获取设备模板列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新设备模板", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "创建新设备模板", - "parameters": [ - { - "description": "设备模板信息", - "name": "deviceTemplate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateDeviceTemplateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/device-templates/{id}": { - "get": { - "description": "根据设备模板ID获取单个设备模板的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "获取设备模板信息", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据设备模板ID更新一个已存在的设备模板信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "更新设备模板信息", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的设备模板信息", - "name": "deviceTemplate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据设备模板ID删除一个设备模板(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "删除设备模板", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/devices": { - "get": { - "description": "获取系统中所有设备的列表", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "获取设备列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新设备", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "创建新设备", - "parameters": [ - { - "description": "设备信息", - "name": "device", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateDeviceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/devices/{id}": { - "get": { - "description": "根据设备ID获取单个设备的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "获取设备信息", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据设备ID更新一个已存在的设备信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "更新设备信息", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的设备信息", - "name": "device", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateDeviceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据设备ID删除一个设备(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "删除设备", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/plans": { - "get": { - "description": "获取所有计划的列表", - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "获取计划列表", - "responses": { - "200": { - "description": "业务码为200代表成功获取列表", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.ListPlansResponse" - } - } - } - ] - } - } - } - }, - "post": { - "description": "创建一个新的计划,包括其基本信息和所有关联的子计划/任务。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "创建计划", - "parameters": [ - { - "description": "计划信息", - "name": "plan", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/plan.CreatePlanRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为201代表创建成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.PlanResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/plans/{id}": { - "get": { - "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/plan.PlanResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据计划ID更新计划的详细信息。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "更新计划", - "parameters": [ - { - "type": "integer", - "description": "计划ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "更新后的计划信息", - "name": "plan", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/plan.UpdatePlanRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为200代表更新成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.PlanResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "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/plans/{id}/start": { - "post": { - "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/plans/{id}/stop": { - "post": { - "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/users": { - "post": { - "description": "根据用户名和密码创建一个新的系统用户。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "创建新用户", - "parameters": [ - { - "description": "用户信息", - "name": "user", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.CreateUserRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为201代表创建成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.CreateUserResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/users/login": { - "post": { - "description": "用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "用户登录", - "parameters": [ - { - "description": "登录凭证", - "name": "credentials", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为200代表登录成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.LoginResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/users/{id}/history": { - "get": { - "description": "根据用户ID,分页获取该用户的操作审计日志。", - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "获取指定用户的操作历史", - "parameters": [ - { - "type": "integer", - "description": "用户ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 10, - "description": "每页大小", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "按操作类型过滤", - "name": "action_type", - "in": "query" - } - ], - "responses": { - "200": { - "description": "业务码为200代表成功获取", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.ListHistoryResponse" - } - } - } - ] - } - } - } - } - } - }, - "definitions": { - "controller.Response": { - "type": "object", - "properties": { - "code": { - "description": "业务状态码", - "allOf": [ - { - "$ref": "#/definitions/controller.ResponseCode" - } - ] - }, - "data": { - "description": "业务数据" - }, - "message": { - "description": "提示信息", - "type": "string" - } - } - }, - "controller.ResponseCode": { - "type": "integer", - "enum": [ - 2000, - 2001, - 4000, - 4001, - 4004, - 4009, - 5000, - 5003 - ], - "x-enum-comments": { - "CodeBadRequest": "请求参数错误", - "CodeConflict": "资源冲突", - "CodeCreated": "创建成功", - "CodeInternalError": "服务器内部错误", - "CodeNotFound": "资源未找到", - "CodeServiceUnavailable": "服务不可用", - "CodeSuccess": "操作成功", - "CodeUnauthorized": "未授权" - }, - "x-enum-descriptions": [ - "操作成功", - "创建成功", - "请求参数错误", - "未授权", - "资源未找到", - "资源冲突", - "服务器内部错误", - "服务不可用" - ], - "x-enum-varnames": [ - "CodeSuccess", - "CodeCreated", - "CodeBadRequest", - "CodeUnauthorized", - "CodeNotFound", - "CodeConflict", - "CodeInternalError", - "CodeServiceUnavailable" - ] - }, - "device.AreaControllerResponse": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "device.CreateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.CreateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.CreateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "device.DeviceTemplateResponse": { - "type": "object", - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "device.UpdateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { - "type": "object", - "properties": { - "area_controller_id": { - "type": "integer" - }, - "area_controller_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "device_template_id": { - "type": "integer" - }, - "device_template_name": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "updated_at": { - "type": "string" - } - } - }, - "models.DeviceCategory": { - "type": "string", - "enum": [ - "执行器", - "传感器" - ], - "x-enum-varnames": [ - "CategoryActuator", - "CategorySensor" - ] - }, - "models.PlanContentType": { - "type": "string", - "enum": [ - "子计划", - "任务" - ], - "x-enum-comments": { - "PlanContentTypeSubPlans": "计划包含子计划", - "PlanContentTypeTasks": "计划包含任务" - }, - "x-enum-descriptions": [ - "计划包含子计划", - "计划包含任务" - ], - "x-enum-varnames": [ - "PlanContentTypeSubPlans", - "PlanContentTypeTasks" - ] - }, - "models.PlanExecutionType": { - "type": "string", - "enum": [ - "自动", - "手动" - ], - "x-enum-comments": { - "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", - "PlanExecutionTypeManual": "手动执行" - }, - "x-enum-descriptions": [ - "自动执行 (包含定时和循环)", - "手动执行" - ], - "x-enum-varnames": [ - "PlanExecutionTypeAutomatic", - "PlanExecutionTypeManual" - ] - }, - "models.PlanStatus": { - "type": "string", - "enum": [ - "已禁用", - "已启用", - "执行完毕", - "执行失败" - ], - "x-enum-comments": { - "PlanStatusDisabled": "禁用计划", - "PlanStatusEnabled": "启用计划", - "PlanStatusFailed": "执行失败", - "PlanStatusStopped": "执行完毕" - }, - "x-enum-descriptions": [ - "禁用计划", - "启用计划", - "执行完毕", - "执行失败" - ], - "x-enum-varnames": [ - "PlanStatusDisabled", - "PlanStatusEnabled", - "PlanStatusStopped", - "PlanStatusFailed" - ] - }, - "models.SensorType": { - "type": "string", - "enum": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-comments": { - "SensorTypeBatteryLevel": "电池电量", - "SensorTypeHumidity": "湿度", - "SensorTypeSignalMetrics": "信号强度", - "SensorTypeTemperature": "温度", - "SensorTypeWeight": "重量" - }, - "x-enum-descriptions": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-varnames": [ - "SensorTypeSignalMetrics", - "SensorTypeBatteryLevel", - "SensorTypeTemperature", - "SensorTypeHumidity", - "SensorTypeWeight" - ] - }, - "models.TaskType": { - "type": "string", - "enum": [ - "计划分析", - "等待", - "下料" - ], - "x-enum-comments": { - "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", - "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", - "TaskTypeWaiting": "等待任务" - }, - "x-enum-descriptions": [ - "解析Plan的Task列表并添加到待执行队列的特殊任务", - "等待任务", - "下料口释放指定重量任务" - ], - "x-enum-varnames": [ - "TaskPlanAnalysis", - "TaskTypeWaiting", - "TaskTypeReleaseFeedWeight" - ] - }, - "models.ValueDescriptor": { - "type": "object", - "properties": { - "multiplier": { - "description": "乘数,用于原始数据转换", - "type": "number" - }, - "offset": { - "description": "偏移量,用于原始数据转换", - "type": "number" - }, - "type": { - "$ref": "#/definitions/models.SensorType" - } - } - }, - "plan.CreatePlanRequest": { - "type": "object", - "required": [ - "execution_type", - "name" - ], - "properties": { - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "根据温度自动调节风扇和加热器" - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } - } - } - }, - "plan.ListPlansResponse": { - "type": "object", - "properties": { - "plans": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.PlanResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "plan.PlanResponse": { - "type": "object", - "properties": { - "content_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanContentType" - } - ], - "example": "任务" - }, - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "根据温度自动调节风扇和加热器" - }, - "execute_count": { - "type": "integer", - "example": 0 - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanStatus" - } - ], - "example": "已启用" - }, - "sub_plans": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.SubPlanResponse" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskResponse" - } - } - } - }, - "plan.SubPlanResponse": { - "type": "object", - "properties": { - "child_plan": { - "$ref": "#/definitions/plan.PlanResponse" - }, - "child_plan_id": { - "type": "integer", - "example": 2 - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "id": { - "type": "integer", - "example": 1 - }, - "parent_plan_id": { - "type": "integer", - "example": 1 - } - } - }, - "plan.TaskRequest": { - "type": "object", - "properties": { - "description": { - "type": "string", - "example": "打开1号风扇" - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "打开风扇" - }, - "parameters": { - "type": "object", - "additionalProperties": true - }, - "type": { - "allOf": [ - { - "$ref": "#/definitions/models.TaskType" - } - ], - "example": "等待" - } - } - }, - "plan.TaskResponse": { - "type": "object", - "properties": { - "description": { - "type": "string", - "example": "打开1号风扇" - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "打开风扇" - }, - "parameters": { - "type": "object", - "additionalProperties": true - }, - "plan_id": { - "type": "integer", - "example": 1 - }, - "type": { - "allOf": [ - { - "$ref": "#/definitions/models.TaskType" - } - ], - "example": "等待" - } - } - }, - "plan.UpdatePlanRequest": { - "type": "object", - "properties": { - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "更新后的描述" - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划V2" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } - } - } - }, - "user.CreateUserRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string", - "example": "password123" - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.CreateUserResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.HistoryResponse": { - "type": "object", - "properties": { - "action_type": { - "type": "string", - "example": "更新设备" - }, - "description": { - "type": "string", - "example": "设备更新成功" - }, - "target_resource": {}, - "time": { - "type": "string" - }, - "user_id": { - "type": "integer", - "example": 101 - }, - "username": { - "type": "string", - "example": "testuser" - } - } - }, - "user.ListHistoryResponse": { - "type": "object", - "properties": { - "history": { - "type": "array", - "items": { - "$ref": "#/definitions/user.HistoryResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "user.LoginRequest": { - "type": "object", - "required": [ - "identifier", - "password" - ], - "properties": { - "identifier": { - "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", - "type": "string", - "example": "testuser" - }, - "password": { - "type": "string", - "example": "password123" - } - } - }, - "user.LoginResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "username": { - "type": "string", - "example": "testuser" - } - } - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "", - Host: "", - BasePath: "", - Schemes: []string{}, - Title: "", - Description: "", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/docs/swagger.json b/docs/swagger.json deleted file mode 100644 index 60e6dec..0000000 --- a/docs/swagger.json +++ /dev/null @@ -1,1842 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "contact": {} - }, - "paths": { - "/api/v1/area-controllers": { - "get": { - "description": "获取系统中所有区域主控的列表", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "获取所有区域主控列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新区域主控", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "创建新区域主控", - "parameters": [ - { - "description": "区域主控信息", - "name": "areaController", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateAreaControllerRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/area-controllers/{id}": { - "get": { - "description": "根据ID获取单个区域主控的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "获取区域主控信息", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据ID更新一个已存在的区域主控信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "更新区域主控信息", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的区域主控信息", - "name": "areaController", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateAreaControllerRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.AreaControllerResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据ID删除一个区域主控(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "区域主控管理" - ], - "summary": "删除区域主控", - "parameters": [ - { - "type": "string", - "description": "区域主控ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/device-templates": { - "get": { - "description": "获取系统中所有设备模板的列表", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "获取设备模板列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新设备模板", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "创建新设备模板", - "parameters": [ - { - "description": "设备模板信息", - "name": "deviceTemplate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateDeviceTemplateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/device-templates/{id}": { - "get": { - "description": "根据设备模板ID获取单个设备模板的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "获取设备模板信息", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据设备模板ID更新一个已存在的设备模板信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "更新设备模板信息", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的设备模板信息", - "name": "deviceTemplate", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/device.DeviceTemplateResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据设备模板ID删除一个设备模板(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "设备模板管理" - ], - "summary": "删除设备模板", - "parameters": [ - { - "type": "string", - "description": "设备模板ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/devices": { - "get": { - "description": "获取系统中所有设备的列表", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "获取设备列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - } - ] - } - } - } - }, - "post": { - "description": "根据提供的信息创建一个新设备", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "创建新设备", - "parameters": [ - { - "description": "设备信息", - "name": "device", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.CreateDeviceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/devices/{id}": { - "get": { - "description": "根据设备ID获取单个设备的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "获取设备信息", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据设备ID更新一个已存在的设备信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "更新设备信息", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "要更新的设备信息", - "name": "device", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/device.UpdateDeviceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "description": "根据设备ID删除一个设备(软删除)", - "produces": [ - "application/json" - ], - "tags": [ - "设备管理" - ], - "summary": "删除设备", - "parameters": [ - { - "type": "string", - "description": "设备ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controller.Response" - } - } - } - } - }, - "/api/v1/plans": { - "get": { - "description": "获取所有计划的列表", - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "获取计划列表", - "responses": { - "200": { - "description": "业务码为200代表成功获取列表", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.ListPlansResponse" - } - } - } - ] - } - } - } - }, - "post": { - "description": "创建一个新的计划,包括其基本信息和所有关联的子计划/任务。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "创建计划", - "parameters": [ - { - "description": "计划信息", - "name": "plan", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/plan.CreatePlanRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为201代表创建成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.PlanResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/plans/{id}": { - "get": { - "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/plan.PlanResponse" - } - } - } - ] - } - } - } - }, - "put": { - "description": "根据计划ID更新计划的详细信息。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "计划管理" - ], - "summary": "更新计划", - "parameters": [ - { - "type": "integer", - "description": "计划ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "更新后的计划信息", - "name": "plan", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/plan.UpdatePlanRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为200代表更新成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/plan.PlanResponse" - } - } - } - ] - } - } - } - }, - "delete": { - "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/plans/{id}/start": { - "post": { - "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/plans/{id}/stop": { - "post": { - "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/users": { - "post": { - "description": "根据用户名和密码创建一个新的系统用户。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "创建新用户", - "parameters": [ - { - "description": "用户信息", - "name": "user", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.CreateUserRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为201代表创建成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.CreateUserResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/users/login": { - "post": { - "description": "用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "用户登录", - "parameters": [ - { - "description": "登录凭证", - "name": "credentials", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "业务码为200代表登录成功", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.LoginResponse" - } - } - } - ] - } - } - } - } - }, - "/api/v1/users/{id}/history": { - "get": { - "description": "根据用户ID,分页获取该用户的操作审计日志。", - "produces": [ - "application/json" - ], - "tags": [ - "用户管理" - ], - "summary": "获取指定用户的操作历史", - "parameters": [ - { - "type": "integer", - "description": "用户ID", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 10, - "description": "每页大小", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "按操作类型过滤", - "name": "action_type", - "in": "query" - } - ], - "responses": { - "200": { - "description": "业务码为200代表成功获取", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controller.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/user.ListHistoryResponse" - } - } - } - ] - } - } - } - } - } - }, - "definitions": { - "controller.Response": { - "type": "object", - "properties": { - "code": { - "description": "业务状态码", - "allOf": [ - { - "$ref": "#/definitions/controller.ResponseCode" - } - ] - }, - "data": { - "description": "业务数据" - }, - "message": { - "description": "提示信息", - "type": "string" - } - } - }, - "controller.ResponseCode": { - "type": "integer", - "enum": [ - 2000, - 2001, - 4000, - 4001, - 4004, - 4009, - 5000, - 5003 - ], - "x-enum-comments": { - "CodeBadRequest": "请求参数错误", - "CodeConflict": "资源冲突", - "CodeCreated": "创建成功", - "CodeInternalError": "服务器内部错误", - "CodeNotFound": "资源未找到", - "CodeServiceUnavailable": "服务不可用", - "CodeSuccess": "操作成功", - "CodeUnauthorized": "未授权" - }, - "x-enum-descriptions": [ - "操作成功", - "创建成功", - "请求参数错误", - "未授权", - "资源未找到", - "资源冲突", - "服务器内部错误", - "服务不可用" - ], - "x-enum-varnames": [ - "CodeSuccess", - "CodeCreated", - "CodeBadRequest", - "CodeUnauthorized", - "CodeNotFound", - "CodeConflict", - "CodeInternalError", - "CodeServiceUnavailable" - ] - }, - "device.AreaControllerResponse": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "device.CreateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.CreateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.CreateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "device.DeviceTemplateResponse": { - "type": "object", - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "device.UpdateAreaControllerRequest": { - "type": "object", - "required": [ - "name", - "network_id" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "network_id": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceRequest": { - "type": "object", - "required": [ - "area_controller_id", - "device_template_id", - "name" - ], - "properties": { - "area_controller_id": { - "type": "integer" - }, - "device_template_id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - } - } - }, - "device.UpdateDeviceTemplateRequest": { - "type": "object", - "required": [ - "category", - "commands", - "name" - ], - "properties": { - "category": { - "$ref": "#/definitions/models.DeviceCategory" - }, - "commands": { - "type": "object", - "additionalProperties": true - }, - "description": { - "type": "string" - }, - "manufacturer": { - "type": "string" - }, - "name": { - "type": "string" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ValueDescriptor" - } - } - } - }, - "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { - "type": "object", - "properties": { - "area_controller_id": { - "type": "integer" - }, - "area_controller_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "device_template_id": { - "type": "integer" - }, - "device_template_name": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": true - }, - "updated_at": { - "type": "string" - } - } - }, - "models.DeviceCategory": { - "type": "string", - "enum": [ - "执行器", - "传感器" - ], - "x-enum-varnames": [ - "CategoryActuator", - "CategorySensor" - ] - }, - "models.PlanContentType": { - "type": "string", - "enum": [ - "子计划", - "任务" - ], - "x-enum-comments": { - "PlanContentTypeSubPlans": "计划包含子计划", - "PlanContentTypeTasks": "计划包含任务" - }, - "x-enum-descriptions": [ - "计划包含子计划", - "计划包含任务" - ], - "x-enum-varnames": [ - "PlanContentTypeSubPlans", - "PlanContentTypeTasks" - ] - }, - "models.PlanExecutionType": { - "type": "string", - "enum": [ - "自动", - "手动" - ], - "x-enum-comments": { - "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", - "PlanExecutionTypeManual": "手动执行" - }, - "x-enum-descriptions": [ - "自动执行 (包含定时和循环)", - "手动执行" - ], - "x-enum-varnames": [ - "PlanExecutionTypeAutomatic", - "PlanExecutionTypeManual" - ] - }, - "models.PlanStatus": { - "type": "string", - "enum": [ - "已禁用", - "已启用", - "执行完毕", - "执行失败" - ], - "x-enum-comments": { - "PlanStatusDisabled": "禁用计划", - "PlanStatusEnabled": "启用计划", - "PlanStatusFailed": "执行失败", - "PlanStatusStopped": "执行完毕" - }, - "x-enum-descriptions": [ - "禁用计划", - "启用计划", - "执行完毕", - "执行失败" - ], - "x-enum-varnames": [ - "PlanStatusDisabled", - "PlanStatusEnabled", - "PlanStatusStopped", - "PlanStatusFailed" - ] - }, - "models.SensorType": { - "type": "string", - "enum": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-comments": { - "SensorTypeBatteryLevel": "电池电量", - "SensorTypeHumidity": "湿度", - "SensorTypeSignalMetrics": "信号强度", - "SensorTypeTemperature": "温度", - "SensorTypeWeight": "重量" - }, - "x-enum-descriptions": [ - "信号强度", - "电池电量", - "温度", - "湿度", - "重量" - ], - "x-enum-varnames": [ - "SensorTypeSignalMetrics", - "SensorTypeBatteryLevel", - "SensorTypeTemperature", - "SensorTypeHumidity", - "SensorTypeWeight" - ] - }, - "models.TaskType": { - "type": "string", - "enum": [ - "计划分析", - "等待", - "下料" - ], - "x-enum-comments": { - "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", - "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", - "TaskTypeWaiting": "等待任务" - }, - "x-enum-descriptions": [ - "解析Plan的Task列表并添加到待执行队列的特殊任务", - "等待任务", - "下料口释放指定重量任务" - ], - "x-enum-varnames": [ - "TaskPlanAnalysis", - "TaskTypeWaiting", - "TaskTypeReleaseFeedWeight" - ] - }, - "models.ValueDescriptor": { - "type": "object", - "properties": { - "multiplier": { - "description": "乘数,用于原始数据转换", - "type": "number" - }, - "offset": { - "description": "偏移量,用于原始数据转换", - "type": "number" - }, - "type": { - "$ref": "#/definitions/models.SensorType" - } - } - }, - "plan.CreatePlanRequest": { - "type": "object", - "required": [ - "execution_type", - "name" - ], - "properties": { - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "根据温度自动调节风扇和加热器" - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } - } - } - }, - "plan.ListPlansResponse": { - "type": "object", - "properties": { - "plans": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.PlanResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "plan.PlanResponse": { - "type": "object", - "properties": { - "content_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanContentType" - } - ], - "example": "任务" - }, - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "根据温度自动调节风扇和加热器" - }, - "execute_count": { - "type": "integer", - "example": 0 - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanStatus" - } - ], - "example": "已启用" - }, - "sub_plans": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.SubPlanResponse" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskResponse" - } - } - } - }, - "plan.SubPlanResponse": { - "type": "object", - "properties": { - "child_plan": { - "$ref": "#/definitions/plan.PlanResponse" - }, - "child_plan_id": { - "type": "integer", - "example": 2 - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "id": { - "type": "integer", - "example": 1 - }, - "parent_plan_id": { - "type": "integer", - "example": 1 - } - } - }, - "plan.TaskRequest": { - "type": "object", - "properties": { - "description": { - "type": "string", - "example": "打开1号风扇" - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "打开风扇" - }, - "parameters": { - "type": "object", - "additionalProperties": true - }, - "type": { - "allOf": [ - { - "$ref": "#/definitions/models.TaskType" - } - ], - "example": "等待" - } - } - }, - "plan.TaskResponse": { - "type": "object", - "properties": { - "description": { - "type": "string", - "example": "打开1号风扇" - }, - "execution_order": { - "type": "integer", - "example": 1 - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "打开风扇" - }, - "parameters": { - "type": "object", - "additionalProperties": true - }, - "plan_id": { - "type": "integer", - "example": 1 - }, - "type": { - "allOf": [ - { - "$ref": "#/definitions/models.TaskType" - } - ], - "example": "等待" - } - } - }, - "plan.UpdatePlanRequest": { - "type": "object", - "properties": { - "cron_expression": { - "type": "string", - "example": "0 0 6 * * *" - }, - "description": { - "type": "string", - "example": "更新后的描述" - }, - "execute_num": { - "type": "integer", - "example": 10 - }, - "execution_type": { - "allOf": [ - { - "$ref": "#/definitions/models.PlanExecutionType" - } - ], - "example": "自动" - }, - "name": { - "type": "string", - "example": "猪舍温度控制计划V2" - }, - "sub_plan_ids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "tasks": { - "type": "array", - "items": { - "$ref": "#/definitions/plan.TaskRequest" - } - } - } - }, - "user.CreateUserRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string", - "example": "password123" - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.CreateUserResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "username": { - "type": "string", - "example": "newuser" - } - } - }, - "user.HistoryResponse": { - "type": "object", - "properties": { - "action_type": { - "type": "string", - "example": "更新设备" - }, - "description": { - "type": "string", - "example": "设备更新成功" - }, - "target_resource": {}, - "time": { - "type": "string" - }, - "user_id": { - "type": "integer", - "example": 101 - }, - "username": { - "type": "string", - "example": "testuser" - } - } - }, - "user.ListHistoryResponse": { - "type": "object", - "properties": { - "history": { - "type": "array", - "items": { - "$ref": "#/definitions/user.HistoryResponse" - } - }, - "total": { - "type": "integer", - "example": 100 - } - } - }, - "user.LoginRequest": { - "type": "object", - "required": [ - "identifier", - "password" - ], - "properties": { - "identifier": { - "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", - "type": "string", - "example": "testuser" - }, - "password": { - "type": "string", - "example": "password123" - } - } - }, - "user.LoginResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "username": { - "type": "string", - "example": "testuser" - } - } - } - } -} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 2e66b9f..0000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,1193 +0,0 @@ -definitions: - controller.Response: - properties: - code: - allOf: - - $ref: '#/definitions/controller.ResponseCode' - description: 业务状态码 - data: - description: 业务数据 - message: - description: 提示信息 - type: string - type: object - controller.ResponseCode: - enum: - - 2000 - - 2001 - - 4000 - - 4001 - - 4004 - - 4009 - - 5000 - - 5003 - type: integer - x-enum-comments: - CodeBadRequest: 请求参数错误 - CodeConflict: 资源冲突 - CodeCreated: 创建成功 - CodeInternalError: 服务器内部错误 - CodeNotFound: 资源未找到 - CodeServiceUnavailable: 服务不可用 - CodeSuccess: 操作成功 - CodeUnauthorized: 未授权 - x-enum-descriptions: - - 操作成功 - - 创建成功 - - 请求参数错误 - - 未授权 - - 资源未找到 - - 资源冲突 - - 服务器内部错误 - - 服务不可用 - x-enum-varnames: - - CodeSuccess - - CodeCreated - - CodeBadRequest - - CodeUnauthorized - - CodeNotFound - - CodeConflict - - CodeInternalError - - CodeServiceUnavailable - device.AreaControllerResponse: - properties: - created_at: - type: string - id: - type: integer - location: - type: string - name: - type: string - network_id: - type: string - properties: - additionalProperties: true - type: object - status: - type: string - updated_at: - type: string - type: object - device.CreateAreaControllerRequest: - properties: - location: - type: string - name: - type: string - network_id: - type: string - properties: - additionalProperties: true - type: object - required: - - name - - network_id - type: object - device.CreateDeviceRequest: - properties: - area_controller_id: - type: integer - device_template_id: - type: integer - location: - type: string - name: - type: string - properties: - additionalProperties: true - type: object - required: - - area_controller_id - - device_template_id - - name - type: object - device.CreateDeviceTemplateRequest: - properties: - category: - $ref: '#/definitions/models.DeviceCategory' - commands: - additionalProperties: true - type: object - description: - type: string - manufacturer: - type: string - name: - type: string - values: - items: - $ref: '#/definitions/models.ValueDescriptor' - type: array - required: - - category - - commands - - name - type: object - device.DeviceTemplateResponse: - properties: - category: - $ref: '#/definitions/models.DeviceCategory' - commands: - additionalProperties: true - type: object - created_at: - type: string - description: - type: string - id: - type: integer - manufacturer: - type: string - name: - type: string - updated_at: - type: string - values: - items: - $ref: '#/definitions/models.ValueDescriptor' - type: array - type: object - device.UpdateAreaControllerRequest: - properties: - location: - type: string - name: - type: string - network_id: - type: string - properties: - additionalProperties: true - type: object - required: - - name - - network_id - type: object - device.UpdateDeviceRequest: - properties: - area_controller_id: - type: integer - device_template_id: - type: integer - location: - type: string - name: - type: string - properties: - additionalProperties: true - type: object - required: - - area_controller_id - - device_template_id - - name - type: object - device.UpdateDeviceTemplateRequest: - properties: - category: - $ref: '#/definitions/models.DeviceCategory' - commands: - additionalProperties: true - type: object - description: - type: string - manufacturer: - type: string - name: - type: string - values: - items: - $ref: '#/definitions/models.ValueDescriptor' - type: array - required: - - category - - commands - - name - type: object - git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse: - properties: - area_controller_id: - type: integer - area_controller_name: - type: string - created_at: - type: string - device_template_id: - type: integer - device_template_name: - type: string - id: - type: integer - location: - type: string - name: - type: string - properties: - additionalProperties: true - type: object - updated_at: - type: string - type: object - models.DeviceCategory: - enum: - - 执行器 - - 传感器 - type: string - x-enum-varnames: - - CategoryActuator - - CategorySensor - models.PlanContentType: - enum: - - 子计划 - - 任务 - type: string - x-enum-comments: - PlanContentTypeSubPlans: 计划包含子计划 - PlanContentTypeTasks: 计划包含任务 - x-enum-descriptions: - - 计划包含子计划 - - 计划包含任务 - x-enum-varnames: - - PlanContentTypeSubPlans - - PlanContentTypeTasks - models.PlanExecutionType: - enum: - - 自动 - - 手动 - type: string - x-enum-comments: - PlanExecutionTypeAutomatic: 自动执行 (包含定时和循环) - PlanExecutionTypeManual: 手动执行 - x-enum-descriptions: - - 自动执行 (包含定时和循环) - - 手动执行 - x-enum-varnames: - - PlanExecutionTypeAutomatic - - PlanExecutionTypeManual - models.PlanStatus: - enum: - - 已禁用 - - 已启用 - - 执行完毕 - - 执行失败 - type: string - x-enum-comments: - PlanStatusDisabled: 禁用计划 - PlanStatusEnabled: 启用计划 - PlanStatusFailed: 执行失败 - PlanStatusStopped: 执行完毕 - x-enum-descriptions: - - 禁用计划 - - 启用计划 - - 执行完毕 - - 执行失败 - x-enum-varnames: - - PlanStatusDisabled - - PlanStatusEnabled - - PlanStatusStopped - - PlanStatusFailed - models.SensorType: - enum: - - 信号强度 - - 电池电量 - - 温度 - - 湿度 - - 重量 - type: string - x-enum-comments: - SensorTypeBatteryLevel: 电池电量 - SensorTypeHumidity: 湿度 - SensorTypeSignalMetrics: 信号强度 - SensorTypeTemperature: 温度 - SensorTypeWeight: 重量 - x-enum-descriptions: - - 信号强度 - - 电池电量 - - 温度 - - 湿度 - - 重量 - x-enum-varnames: - - SensorTypeSignalMetrics - - SensorTypeBatteryLevel - - SensorTypeTemperature - - SensorTypeHumidity - - SensorTypeWeight - models.TaskType: - enum: - - 计划分析 - - 等待 - - 下料 - type: string - x-enum-comments: - TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 - TaskTypeReleaseFeedWeight: 下料口释放指定重量任务 - TaskTypeWaiting: 等待任务 - x-enum-descriptions: - - 解析Plan的Task列表并添加到待执行队列的特殊任务 - - 等待任务 - - 下料口释放指定重量任务 - x-enum-varnames: - - TaskPlanAnalysis - - TaskTypeWaiting - - TaskTypeReleaseFeedWeight - models.ValueDescriptor: - properties: - multiplier: - description: 乘数,用于原始数据转换 - type: number - offset: - description: 偏移量,用于原始数据转换 - type: number - type: - $ref: '#/definitions/models.SensorType' - type: object - plan.CreatePlanRequest: - properties: - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 根据温度自动调节风扇和加热器 - type: string - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: 自动 - name: - example: 猪舍温度控制计划 - type: string - sub_plan_ids: - items: - type: integer - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskRequest' - type: array - required: - - execution_type - - name - type: object - plan.ListPlansResponse: - properties: - plans: - items: - $ref: '#/definitions/plan.PlanResponse' - type: array - total: - example: 100 - type: integer - type: object - plan.PlanResponse: - properties: - content_type: - allOf: - - $ref: '#/definitions/models.PlanContentType' - example: 任务 - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 根据温度自动调节风扇和加热器 - type: string - execute_count: - example: 0 - type: integer - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: 自动 - id: - example: 1 - type: integer - name: - example: 猪舍温度控制计划 - type: string - status: - allOf: - - $ref: '#/definitions/models.PlanStatus' - example: 已启用 - sub_plans: - items: - $ref: '#/definitions/plan.SubPlanResponse' - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskResponse' - type: array - type: object - plan.SubPlanResponse: - properties: - child_plan: - $ref: '#/definitions/plan.PlanResponse' - child_plan_id: - example: 2 - type: integer - execution_order: - example: 1 - type: integer - id: - example: 1 - type: integer - parent_plan_id: - example: 1 - type: integer - type: object - plan.TaskRequest: - properties: - description: - example: 打开1号风扇 - type: string - execution_order: - example: 1 - type: integer - name: - example: 打开风扇 - type: string - parameters: - additionalProperties: true - type: object - type: - allOf: - - $ref: '#/definitions/models.TaskType' - example: 等待 - type: object - plan.TaskResponse: - properties: - description: - example: 打开1号风扇 - type: string - execution_order: - example: 1 - type: integer - id: - example: 1 - type: integer - name: - example: 打开风扇 - type: string - parameters: - additionalProperties: true - type: object - plan_id: - example: 1 - type: integer - type: - allOf: - - $ref: '#/definitions/models.TaskType' - example: 等待 - type: object - plan.UpdatePlanRequest: - properties: - cron_expression: - example: 0 0 6 * * * - type: string - description: - example: 更新后的描述 - type: string - execute_num: - example: 10 - type: integer - execution_type: - allOf: - - $ref: '#/definitions/models.PlanExecutionType' - example: 自动 - name: - example: 猪舍温度控制计划V2 - type: string - sub_plan_ids: - items: - type: integer - type: array - tasks: - items: - $ref: '#/definitions/plan.TaskRequest' - type: array - type: object - user.CreateUserRequest: - properties: - password: - example: password123 - type: string - username: - example: newuser - type: string - required: - - password - - username - type: object - user.CreateUserResponse: - properties: - id: - example: 1 - type: integer - username: - example: newuser - type: string - type: object - user.HistoryResponse: - properties: - action_type: - example: 更新设备 - type: string - description: - example: 设备更新成功 - type: string - target_resource: {} - time: - type: string - user_id: - example: 101 - type: integer - username: - example: testuser - type: string - type: object - user.ListHistoryResponse: - properties: - history: - items: - $ref: '#/definitions/user.HistoryResponse' - type: array - total: - example: 100 - type: integer - type: object - user.LoginRequest: - properties: - identifier: - description: Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 - example: testuser - type: string - password: - example: password123 - type: string - required: - - identifier - - password - type: object - user.LoginResponse: - properties: - id: - example: 1 - type: integer - token: - example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - type: string - username: - example: testuser - type: string - type: object -info: - contact: {} -paths: - /api/v1/area-controllers: - get: - description: 获取系统中所有区域主控的列表 - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - items: - $ref: '#/definitions/device.AreaControllerResponse' - type: array - type: object - summary: 获取所有区域主控列表 - tags: - - 区域主控管理 - post: - consumes: - - application/json - description: 根据提供的信息创建一个新区域主控 - parameters: - - description: 区域主控信息 - in: body - name: areaController - required: true - schema: - $ref: '#/definitions/device.CreateAreaControllerRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.AreaControllerResponse' - type: object - summary: 创建新区域主控 - tags: - - 区域主控管理 - /api/v1/area-controllers/{id}: - delete: - description: 根据ID删除一个区域主控(软删除) - parameters: - - description: 区域主控ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controller.Response' - summary: 删除区域主控 - tags: - - 区域主控管理 - get: - description: 根据ID获取单个区域主控的详细信息 - parameters: - - description: 区域主控ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.AreaControllerResponse' - type: object - summary: 获取区域主控信息 - tags: - - 区域主控管理 - put: - consumes: - - application/json - description: 根据ID更新一个已存在的区域主控信息 - parameters: - - description: 区域主控ID - in: path - name: id - required: true - type: string - - description: 要更新的区域主控信息 - in: body - name: areaController - required: true - schema: - $ref: '#/definitions/device.UpdateAreaControllerRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.AreaControllerResponse' - type: object - summary: 更新区域主控信息 - tags: - - 区域主控管理 - /api/v1/device-templates: - get: - description: 获取系统中所有设备模板的列表 - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - items: - $ref: '#/definitions/device.DeviceTemplateResponse' - type: array - type: object - summary: 获取设备模板列表 - tags: - - 设备模板管理 - post: - consumes: - - application/json - description: 根据提供的信息创建一个新设备模板 - parameters: - - description: 设备模板信息 - in: body - name: deviceTemplate - required: true - schema: - $ref: '#/definitions/device.CreateDeviceTemplateRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.DeviceTemplateResponse' - type: object - summary: 创建新设备模板 - tags: - - 设备模板管理 - /api/v1/device-templates/{id}: - delete: - description: 根据设备模板ID删除一个设备模板(软删除) - parameters: - - description: 设备模板ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controller.Response' - summary: 删除设备模板 - tags: - - 设备模板管理 - get: - description: 根据设备模板ID获取单个设备模板的详细信息 - parameters: - - description: 设备模板ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.DeviceTemplateResponse' - type: object - summary: 获取设备模板信息 - tags: - - 设备模板管理 - put: - consumes: - - application/json - description: 根据设备模板ID更新一个已存在的设备模板信息 - parameters: - - description: 设备模板ID - in: path - name: id - required: true - type: string - - description: 要更新的设备模板信息 - in: body - name: deviceTemplate - required: true - schema: - $ref: '#/definitions/device.UpdateDeviceTemplateRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/device.DeviceTemplateResponse' - type: object - summary: 更新设备模板信息 - tags: - - 设备模板管理 - /api/v1/devices: - get: - description: 获取系统中所有设备的列表 - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - items: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' - type: array - type: object - summary: 获取设备列表 - tags: - - 设备管理 - post: - consumes: - - application/json - description: 根据提供的信息创建一个新设备 - parameters: - - description: 设备信息 - in: body - name: device - required: true - schema: - $ref: '#/definitions/device.CreateDeviceRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' - type: object - summary: 创建新设备 - tags: - - 设备管理 - /api/v1/devices/{id}: - delete: - description: 根据设备ID删除一个设备(软删除) - parameters: - - description: 设备ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controller.Response' - summary: 删除设备 - tags: - - 设备管理 - get: - description: 根据设备ID获取单个设备的详细信息 - parameters: - - description: 设备ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' - type: object - summary: 获取设备信息 - tags: - - 设备管理 - put: - consumes: - - application/json - description: 根据设备ID更新一个已存在的设备信息 - parameters: - - description: 设备ID - in: path - name: id - required: true - type: string - - description: 要更新的设备信息 - in: body - name: device - required: true - schema: - $ref: '#/definitions/device.UpdateDeviceRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse' - type: object - summary: 更新设备信息 - tags: - - 设备管理 - /api/v1/plans: - get: - description: 获取所有计划的列表 - produces: - - application/json - responses: - "200": - description: 业务码为200代表成功获取列表 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/plan.ListPlansResponse' - type: object - summary: 获取计划列表 - tags: - - 计划管理 - post: - consumes: - - application/json - description: 创建一个新的计划,包括其基本信息和所有关联的子计划/任务。 - parameters: - - description: 计划信息 - in: body - name: plan - required: true - schema: - $ref: '#/definitions/plan.CreatePlanRequest' - produces: - - application/json - responses: - "200": - description: 业务码为201代表创建成功 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/plan.PlanResponse' - type: object - summary: 创建计划 - tags: - - 计划管理 - /api/v1/plans/{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' - 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/plan.PlanResponse' - type: object - summary: 获取计划详情 - tags: - - 计划管理 - put: - consumes: - - application/json - description: 根据计划ID更新计划的详细信息。 - parameters: - - description: 计划ID - in: path - name: id - required: true - type: integer - - description: 更新后的计划信息 - in: body - name: plan - required: true - schema: - $ref: '#/definitions/plan.UpdatePlanRequest' - produces: - - application/json - responses: - "200": - description: 业务码为200代表更新成功 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/plan.PlanResponse' - type: object - summary: 更新计划 - tags: - - 计划管理 - /api/v1/plans/{id}/start: - post: - 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' - summary: 启动计划 - tags: - - 计划管理 - /api/v1/plans/{id}/stop: - post: - 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' - summary: 停止计划 - tags: - - 计划管理 - /api/v1/users: - post: - consumes: - - application/json - description: 根据用户名和密码创建一个新的系统用户。 - parameters: - - description: 用户信息 - in: body - name: user - required: true - schema: - $ref: '#/definitions/user.CreateUserRequest' - produces: - - application/json - responses: - "200": - description: 业务码为201代表创建成功 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/user.CreateUserResponse' - type: object - summary: 创建新用户 - tags: - - 用户管理 - /api/v1/users/{id}/history: - get: - description: 根据用户ID,分页获取该用户的操作审计日志。 - parameters: - - description: 用户ID - in: path - name: id - required: true - type: integer - - default: 1 - description: 页码 - in: query - name: page - type: integer - - default: 10 - description: 每页大小 - in: query - name: page_size - type: integer - - description: 按操作类型过滤 - in: query - name: action_type - type: string - produces: - - application/json - responses: - "200": - description: 业务码为200代表成功获取 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/user.ListHistoryResponse' - type: object - summary: 获取指定用户的操作历史 - tags: - - 用户管理 - /api/v1/users/login: - post: - consumes: - - application/json - description: 用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。 - parameters: - - description: 登录凭证 - in: body - name: credentials - required: true - schema: - $ref: '#/definitions/user.LoginRequest' - produces: - - application/json - responses: - "200": - description: 业务码为200代表登录成功 - schema: - allOf: - - $ref: '#/definitions/controller.Response' - - properties: - data: - $ref: '#/definitions/user.LoginResponse' - type: object - summary: 用户登录 - tags: - - 用户管理 -swagger: "2.0" diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 5758ab9..6c00efd 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -204,7 +204,7 @@ func (a *API) setupRoutes() { a.logger.Info("计划相关接口注册成功 (需要认证和审计)") // 猪舍相关路由组 - pigHouseGroup := authGroup.Group("/pighouses") + pigHouseGroup := authGroup.Group("/pig-houses") { pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse) pigHouseGroup.GET("", a.pigFarmController.ListPigHouses) diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index 1dcc7b1..9f37005 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -38,7 +38,7 @@ func NewPigFarmController(logger *logs.Logger, service service.PigFarmService) * // @Produce json // @Param body body dto.CreatePigHouseRequest true "猪舍信息" // @Success 201 {object} controller.Response{data=dto.PigHouseResponse} "创建成功" -// @Router /api/v1/pighouses [post] +// @Router /api/v1/pig-houses [post] func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { const action = "创建猪舍" var req dto.CreatePigHouseRequest @@ -69,7 +69,7 @@ func (c *PigFarmController) CreatePigHouse(ctx *gin.Context) { // @Produce json // @Param id path int true "猪舍ID" // @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "获取成功" -// @Router /api/v1/pighouses/{id} [get] +// @Router /api/v1/pig-houses/{id} [get] func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { const action = "获取猪舍" id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) @@ -103,7 +103,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { // @Tags 猪场管理 // @Produce json // @Success 200 {object} controller.Response{data=[]dto.PigHouseResponse} "获取成功" -// @Router /api/v1/pighouses [get] +// @Router /api/v1/pig-houses [get] func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { const action = "获取猪舍列表" houses, err := c.service.ListPigHouses() @@ -134,7 +134,7 @@ func (c *PigFarmController) ListPigHouses(ctx *gin.Context) { // @Param id path int true "猪舍ID" // @Param body body dto.UpdatePigHouseRequest true "猪舍信息" // @Success 200 {object} controller.Response{data=dto.PigHouseResponse} "更新成功" -// @Router /api/v1/pighouses/{id} [put] +// @Router /api/v1/pig-houses/{id} [put] func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { const action = "更新猪舍" id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) @@ -175,7 +175,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { // @Produce json // @Param id path int true "猪舍ID" // @Success 200 {object} controller.Response "删除成功" -// @Router /api/v1/pighouses/{id} [delete] +// @Router /api/v1/pig-houses/{id} [delete] func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) { const action = "删除猪舍" id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) @@ -216,7 +216,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { return } - pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity, req.Status) + pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity) if err != nil { c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req) diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 0461e01..433e5e3 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -26,7 +26,7 @@ type PigFarmService interface { DeletePigHouse(id uint) error // Pen methods - CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) + CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) GetPenByID(id uint) (*models.Pen, error) ListPens() ([]models.Pen, error) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) @@ -99,7 +99,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error { // --- Pen Implementation --- -func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { +func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) { // 业务逻辑:验证所属猪舍是否存在 _, err := s.repo.GetPigHouseByID(houseID) if err != nil { @@ -113,7 +113,7 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int, PenNumber: penNumber, HouseID: houseID, Capacity: capacity, - Status: status, + Status: models.PenStatusEmpty, } err = s.repo.CreatePen(pen) return pen, err From d03163a189cfad3b76b2ce68060c7b73d11b592c Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Fri, 3 Oct 2025 23:59:25 +0800 Subject: [PATCH 19/65] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 2842 +++++++++++++++++++++++++++++++++++++++++++++ docs/swagger.json | 2813 ++++++++++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 1809 +++++++++++++++++++++++++++++ 3 files changed, 7464 insertions(+) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..2f528e9 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,2842 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/area-controllers": { + "get": { + "description": "获取系统中所有区域主控的列表", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "获取所有区域主控列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新区域主控", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "创建新区域主控", + "parameters": [ + { + "description": "区域主控信息", + "name": "areaController", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateAreaControllerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/area-controllers/{id}": { + "get": { + "description": "根据ID获取单个区域主控的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "获取区域主控信息", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据ID更新一个已存在的区域主控信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "更新区域主控信息", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的区域主控信息", + "name": "areaController", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateAreaControllerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个区域主控(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "删除区域主控", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/device-templates": { + "get": { + "description": "获取系统中所有设备模板的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备模板", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "创建新设备模板", + "parameters": [ + { + "description": "设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/device-templates/{id}": { + "get": { + "description": "根据设备模板ID获取单个设备模板的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备模板ID更新一个已存在的设备模板信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "更新设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备模板ID删除一个设备模板(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "删除设备模板", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/devices": { + "get": { + "description": "获取系统中所有设备的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "创建新设备", + "parameters": [ + { + "description": "设备信息", + "name": "device", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/devices/{id}": { + "get": { + "description": "根据设备ID获取单个设备的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备信息", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备ID更新一个已存在的设备信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备信息", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备信息", + "name": "device", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备ID删除一个设备(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "删除设备", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pens": { + "get": { + "description": "获取所有猪栏的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪栏列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪栏", + "parameters": [ + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePenRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pens/{id}": { + "get": { + "description": "根据ID获取单个猪栏信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪栏信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪栏", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches": { + "get": { + "description": "获取所有猪批次的列表,支持按活跃状态筛选", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "获取猪批次列表", + "parameters": [ + { + "type": "boolean", + "description": "是否活跃 (true/false)", + "name": "is_active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "post": { + "description": "创建一个新的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "创建猪批次", + "parameters": [ + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchCreateDTO" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}": { + "get": { + "description": "根据ID获取单个猪批次信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "获取单个猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "put": { + "description": "更新一个已存在的猪批次信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdateDTO" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪批次", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "删除猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-houses": { + "get": { + "description": "获取所有猪舍的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪舍列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪舍", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪舍", + "parameters": [ + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigHouseRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-houses/{id}": { + "get": { + "description": "根据ID获取单个猪舍信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪舍信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigHouseRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪舍", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/plans": { + "get": { + "description": "获取所有计划的列表", + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "获取计划列表", + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的计划,包括其基本信息和所有关联的子计划/任务。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "创建计划", + "parameters": [ + { + "description": "计划信息", + "name": "plan", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePlanRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/plans/{id}": { + "get": { + "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.PlanResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据计划ID更新计划的详细信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "更新计划", + "parameters": [ + { + "type": "integer", + "description": "计划ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的计划信息", + "name": "plan", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePlanRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "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/plans/{id}/start": { + "post": { + "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/plans/{id}/stop": { + "post": { + "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/users": { + "post": { + "description": "根据用户名和密码创建一个新的系统用户。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建新用户", + "parameters": [ + { + "description": "用户信息", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.CreateUserResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/users/login": { + "post": { + "description": "用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录凭证", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.LoginResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/users/{id}/history": { + "get": { + "description": "根据用户ID,分页获取该用户的操作审计日志。", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取指定用户的操作历史", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "按操作类型过滤", + "name": "action_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListHistoryResponse" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "controller.Response": { + "type": "object", + "properties": { + "code": { + "description": "业务状态码", + "allOf": [ + { + "$ref": "#/definitions/controller.ResponseCode" + } + ] + }, + "data": { + "description": "业务数据" + }, + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "controller.ResponseCode": { + "type": "integer", + "enum": [ + 2000, + 2001, + 4000, + 4001, + 4004, + 4009, + 5000, + 5003 + ], + "x-enum-comments": { + "CodeBadRequest": "请求参数错误", + "CodeConflict": "资源冲突", + "CodeCreated": "创建成功", + "CodeInternalError": "服务器内部错误", + "CodeNotFound": "资源未找到", + "CodeServiceUnavailable": "服务不可用", + "CodeSuccess": "操作成功", + "CodeUnauthorized": "未授权" + }, + "x-enum-descriptions": [ + "操作成功", + "创建成功", + "请求参数错误", + "未授权", + "资源未找到", + "资源冲突", + "服务器内部错误", + "服务不可用" + ], + "x-enum-varnames": [ + "CodeSuccess", + "CodeCreated", + "CodeBadRequest", + "CodeUnauthorized", + "CodeNotFound", + "CodeConflict", + "CodeInternalError", + "CodeServiceUnavailable" + ] + }, + "dto.AreaControllerResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "dto.CreateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.CreateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.CreateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.CreatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.CreatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CreatePlanRequest": { + "type": "object", + "required": [ + "execution_type", + "name" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.CreateUserResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.DeviceResponse": { + "type": "object", + "properties": { + "area_controller_id": { + "type": "integer" + }, + "area_controller_name": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "device_template_id": { + "type": "integer" + }, + "device_template_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "updated_at": { + "type": "string" + } + } + }, + "dto.DeviceTemplateResponse": { + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.HistoryResponse": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "更新设备" + }, + "description": { + "type": "string", + "example": "设备更新成功" + }, + "target_resource": {}, + "time": { + "type": "string" + }, + "user_id": { + "type": "integer", + "example": 101 + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.ListHistoryResponse": { + "type": "object", + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HistoryResponse" + } + }, + "total": { + "type": "integer", + "example": 100 + } + } + }, + "dto.LoginRequest": { + "type": "object", + "required": [ + "identifier", + "password" + ], + "properties": { + "identifier": { + "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", + "type": "string", + "example": "testuser" + }, + "password": { + "type": "string", + "example": "password123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.PenResponse": { + "type": "object", + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "pig_batch_id": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.PigBatchCreateDTO": { + "type": "object", + "required": [ + "batch_number", + "initial_count", + "origin_type", + "start_date", + "status" + ], + "properties": { + "batch_number": { + "description": "批次编号,必填", + "type": "string" + }, + "initial_count": { + "description": "初始数量,必填,最小为1", + "type": "integer", + "minimum": 1 + }, + "origin_type": { + "description": "批次来源,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,必填", + "type": "string" + }, + "status": { + "description": "批次状态,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigBatchResponseDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号", + "type": "string" + }, + "create_time": { + "description": "创建时间", + "type": "string" + }, + "end_date": { + "description": "批次结束日期", + "type": "string" + }, + "id": { + "description": "批次ID", + "type": "integer" + }, + "initial_count": { + "description": "初始数量", + "type": "integer" + }, + "is_active": { + "description": "是否活跃", + "type": "boolean" + }, + "origin_type": { + "description": "批次来源", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期", + "type": "string" + }, + "status": { + "description": "批次状态", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + }, + "update_time": { + "description": "更新时间", + "type": "string" + } + } + }, + "dto.PigBatchUpdateDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号,可选", + "type": "string" + }, + "end_date": { + "description": "批次结束日期,可选", + "type": "string" + }, + "initial_count": { + "description": "初始数量,可选", + "type": "integer" + }, + "origin_type": { + "description": "批次来源,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,可选", + "type": "string" + }, + "status": { + "description": "批次状态,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigHouseResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.PlanResponse": { + "type": "object", + "properties": { + "content_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanContentType" + } + ], + "example": "任务" + }, + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_count": { + "type": "integer", + "example": 0 + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanStatus" + } + ], + "example": "已启用" + }, + "sub_plans": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SubPlanResponse" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskResponse" + } + } + } + }, + "dto.SubPlanResponse": { + "type": "object", + "properties": { + "child_plan": { + "$ref": "#/definitions/dto.PlanResponse" + }, + "child_plan_id": { + "type": "integer", + "example": 2 + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "parent_plan_id": { + "type": "integer", + "example": 1 + } + } + }, + "dto.TaskRequest": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "打开1号风扇" + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "打开风扇" + }, + "parameters": { + "type": "object", + "additionalProperties": true + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/models.TaskType" + } + ], + "example": "等待" + } + } + }, + "dto.TaskResponse": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "打开1号风扇" + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "打开风扇" + }, + "parameters": { + "type": "object", + "additionalProperties": true + }, + "plan_id": { + "type": "integer", + "example": 1 + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/models.TaskType" + } + ], + "example": "等待" + } + } + }, + "dto.UpdateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.UpdatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.UpdatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdatePlanRequest": { + "type": "object", + "required": [ + "execution_type" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "更新后的描述" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划V2" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "models.DeviceCategory": { + "type": "string", + "enum": [ + "执行器", + "传感器" + ], + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] + }, + "models.PenStatus": { + "type": "string", + "enum": [ + "空闲", + "占用", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "x-enum-varnames": [ + "PenStatusEmpty", + "PenStatusOccupied", + "PenStatusSickPen", + "PenStatusRecovering", + "PenStatusCleaning", + "PenStatusUnderMaint" + ] + }, + "models.PigBatchOriginType": { + "type": "string", + "enum": [ + "自繁", + "外购" + ], + "x-enum-varnames": [ + "OriginTypeSelfFarrowed", + "OriginTypePurchased" + ] + }, + "models.PigBatchStatus": { + "type": "string", + "enum": [ + "保育", + "生长", + "育肥", + "待售", + "已出售", + "已归档" + ], + "x-enum-comments": { + "BatchStatusArchived": "批次结束(如全群淘汰等)", + "BatchStatusFinishing": "最后的育肥阶段", + "BatchStatusForSale": "达到出栏标准", + "BatchStatusGrowing": "生长育肥阶段", + "BatchStatusWeaning": "从断奶到保育结束" + }, + "x-enum-descriptions": [ + "从断奶到保育结束", + "生长育肥阶段", + "最后的育肥阶段", + "达到出栏标准", + "", + "批次结束(如全群淘汰等)" + ], + "x-enum-varnames": [ + "BatchStatusWeaning", + "BatchStatusGrowing", + "BatchStatusFinishing", + "BatchStatusForSale", + "BatchStatusSold", + "BatchStatusArchived" + ] + }, + "models.PlanContentType": { + "type": "string", + "enum": [ + "子计划", + "任务" + ], + "x-enum-comments": { + "PlanContentTypeSubPlans": "计划包含子计划", + "PlanContentTypeTasks": "计划包含任务" + }, + "x-enum-descriptions": [ + "计划包含子计划", + "计划包含任务" + ], + "x-enum-varnames": [ + "PlanContentTypeSubPlans", + "PlanContentTypeTasks" + ] + }, + "models.PlanExecutionType": { + "type": "string", + "enum": [ + "自动", + "手动" + ], + "x-enum-comments": { + "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", + "PlanExecutionTypeManual": "手动执行" + }, + "x-enum-descriptions": [ + "自动执行 (包含定时和循环)", + "手动执行" + ], + "x-enum-varnames": [ + "PlanExecutionTypeAutomatic", + "PlanExecutionTypeManual" + ] + }, + "models.PlanStatus": { + "type": "string", + "enum": [ + "已禁用", + "已启用", + "执行完毕", + "执行失败" + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusFailed": "执行失败", + "PlanStatusStopped": "执行完毕" + }, + "x-enum-descriptions": [ + "禁用计划", + "启用计划", + "执行完毕", + "执行失败" + ], + "x-enum-varnames": [ + "PlanStatusDisabled", + "PlanStatusEnabled", + "PlanStatusStopped", + "PlanStatusFailed" + ] + }, + "models.SensorType": { + "type": "string", + "enum": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, + "models.TaskType": { + "type": "string", + "enum": [ + "计划分析", + "等待", + "下料" + ], + "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", + "TaskTypeWaiting": "等待任务" + }, + "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", + "等待任务", + "下料口释放指定重量任务" + ], + "x-enum-varnames": [ + "TaskPlanAnalysis", + "TaskTypeWaiting", + "TaskTypeReleaseFeedWeight" + ] + }, + "models.ValueDescriptor": { + "type": "object", + "properties": { + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" + }, + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" + }, + "type": { + "$ref": "#/definitions/models.SensorType" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..7ce156b --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,2813 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/area-controllers": { + "get": { + "description": "获取系统中所有区域主控的列表", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "获取所有区域主控列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新区域主控", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "创建新区域主控", + "parameters": [ + { + "description": "区域主控信息", + "name": "areaController", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateAreaControllerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/area-controllers/{id}": { + "get": { + "description": "根据ID获取单个区域主控的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "获取区域主控信息", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据ID更新一个已存在的区域主控信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "更新区域主控信息", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的区域主控信息", + "name": "areaController", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateAreaControllerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.AreaControllerResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个区域主控(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "区域主控管理" + ], + "summary": "删除区域主控", + "parameters": [ + { + "type": "string", + "description": "区域主控ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/device-templates": { + "get": { + "description": "获取系统中所有设备模板的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备模板", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "创建新设备模板", + "parameters": [ + { + "description": "设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/device-templates/{id}": { + "get": { + "description": "根据设备模板ID获取单个设备模板的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备模板ID更新一个已存在的设备模板信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "更新设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备模板ID删除一个设备模板(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "删除设备模板", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/devices": { + "get": { + "description": "获取系统中所有设备的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "创建新设备", + "parameters": [ + { + "description": "设备信息", + "name": "device", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/devices/{id}": { + "get": { + "description": "根据设备ID获取单个设备的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备信息", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备ID更新一个已存在的设备信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备信息", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备信息", + "name": "device", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.DeviceResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备ID删除一个设备(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "删除设备", + "parameters": [ + { + "type": "string", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pens": { + "get": { + "description": "获取所有猪栏的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪栏列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪栏", + "parameters": [ + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePenRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pens/{id}": { + "get": { + "description": "根据ID获取单个猪栏信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪栏信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪栏信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪栏", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches": { + "get": { + "description": "获取所有猪批次的列表,支持按活跃状态筛选", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "获取猪批次列表", + "parameters": [ + { + "type": "boolean", + "description": "是否活跃 (true/false)", + "name": "is_active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + } + ] + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "post": { + "description": "创建一个新的猪批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "创建猪批次", + "parameters": [ + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchCreateDTO" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}": { + "get": { + "description": "根据ID获取单个猪批次信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "获取单个猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "put": { + "description": "更新一个已存在的猪批次信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdateDTO" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigBatchResponseDTO" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪批次", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "删除猪批次", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-houses": { + "get": { + "description": "获取所有猪舍的列表", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取猪舍列表", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的猪舍", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "创建猪舍", + "parameters": [ + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePigHouseRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/pig-houses/{id}": { + "get": { + "description": "根据ID获取单个猪舍信息", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "获取单个猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "更新一个已存在的猪舍信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪舍信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePigHouseRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PigHouseResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据ID删除一个猪舍", + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "删除猪舍", + "parameters": [ + { + "type": "integer", + "description": "猪舍ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/plans": { + "get": { + "description": "获取所有计划的列表", + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "获取计划列表", + "responses": { + "200": { + "description": "业务码为200代表成功获取列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建一个新的计划,包括其基本信息和所有关联的子计划/任务。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "创建计划", + "parameters": [ + { + "description": "计划信息", + "name": "plan", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreatePlanRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/plans/{id}": { + "get": { + "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.PlanResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据计划ID更新计划的详细信息。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "计划管理" + ], + "summary": "更新计划", + "parameters": [ + { + "type": "integer", + "description": "计划ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新后的计划信息", + "name": "plan", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePlanRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PlanResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "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/plans/{id}/start": { + "post": { + "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/plans/{id}/stop": { + "post": { + "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/users": { + "post": { + "description": "根据用户名和密码创建一个新的系统用户。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建新用户", + "parameters": [ + { + "description": "用户信息", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为201代表创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.CreateUserResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/users/login": { + "post": { + "description": "用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录凭证", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "业务码为200代表登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.LoginResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/users/{id}/history": { + "get": { + "description": "根据用户ID,分页获取该用户的操作审计日志。", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取指定用户的操作历史", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页大小", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "按操作类型过滤", + "name": "action_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "业务码为200代表成功获取", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ListHistoryResponse" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "controller.Response": { + "type": "object", + "properties": { + "code": { + "description": "业务状态码", + "allOf": [ + { + "$ref": "#/definitions/controller.ResponseCode" + } + ] + }, + "data": { + "description": "业务数据" + }, + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "controller.ResponseCode": { + "type": "integer", + "enum": [ + 2000, + 2001, + 4000, + 4001, + 4004, + 4009, + 5000, + 5003 + ], + "x-enum-comments": { + "CodeBadRequest": "请求参数错误", + "CodeConflict": "资源冲突", + "CodeCreated": "创建成功", + "CodeInternalError": "服务器内部错误", + "CodeNotFound": "资源未找到", + "CodeServiceUnavailable": "服务不可用", + "CodeSuccess": "操作成功", + "CodeUnauthorized": "未授权" + }, + "x-enum-descriptions": [ + "操作成功", + "创建成功", + "请求参数错误", + "未授权", + "资源未找到", + "资源冲突", + "服务器内部错误", + "服务不可用" + ], + "x-enum-varnames": [ + "CodeSuccess", + "CodeCreated", + "CodeBadRequest", + "CodeUnauthorized", + "CodeNotFound", + "CodeConflict", + "CodeInternalError", + "CodeServiceUnavailable" + ] + }, + "dto.AreaControllerResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "dto.CreateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.CreateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.CreateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.CreatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.CreatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CreatePlanRequest": { + "type": "object", + "required": [ + "execution_type", + "name" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.CreateUserResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "newuser" + } + } + }, + "dto.DeviceResponse": { + "type": "object", + "properties": { + "area_controller_id": { + "type": "integer" + }, + "area_controller_name": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "device_template_id": { + "type": "integer" + }, + "device_template_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + }, + "updated_at": { + "type": "string" + } + } + }, + "dto.DeviceTemplateResponse": { + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.HistoryResponse": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "更新设备" + }, + "description": { + "type": "string", + "example": "设备更新成功" + }, + "target_resource": {}, + "time": { + "type": "string" + }, + "user_id": { + "type": "integer", + "example": 101 + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.ListHistoryResponse": { + "type": "object", + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HistoryResponse" + } + }, + "total": { + "type": "integer", + "example": 100 + } + } + }, + "dto.LoginRequest": { + "type": "object", + "required": [ + "identifier", + "password" + ], + "properties": { + "identifier": { + "description": "Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号", + "type": "string", + "example": "testuser" + }, + "password": { + "type": "string", + "example": "password123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "username": { + "type": "string", + "example": "testuser" + } + } + }, + "dto.PenResponse": { + "type": "object", + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "pig_batch_id": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.PigBatchCreateDTO": { + "type": "object", + "required": [ + "batch_number", + "initial_count", + "origin_type", + "start_date", + "status" + ], + "properties": { + "batch_number": { + "description": "批次编号,必填", + "type": "string" + }, + "initial_count": { + "description": "初始数量,必填,最小为1", + "type": "integer", + "minimum": 1 + }, + "origin_type": { + "description": "批次来源,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,必填", + "type": "string" + }, + "status": { + "description": "批次状态,必填", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigBatchResponseDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号", + "type": "string" + }, + "create_time": { + "description": "创建时间", + "type": "string" + }, + "end_date": { + "description": "批次结束日期", + "type": "string" + }, + "id": { + "description": "批次ID", + "type": "integer" + }, + "initial_count": { + "description": "初始数量", + "type": "integer" + }, + "is_active": { + "description": "是否活跃", + "type": "boolean" + }, + "origin_type": { + "description": "批次来源", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期", + "type": "string" + }, + "status": { + "description": "批次状态", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + }, + "update_time": { + "description": "更新时间", + "type": "string" + } + } + }, + "dto.PigBatchUpdateDTO": { + "type": "object", + "properties": { + "batch_number": { + "description": "批次编号,可选", + "type": "string" + }, + "end_date": { + "description": "批次结束日期,可选", + "type": "string" + }, + "initial_count": { + "description": "初始数量,可选", + "type": "integer" + }, + "origin_type": { + "description": "批次来源,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchOriginType" + } + ] + }, + "start_date": { + "description": "批次开始日期,可选", + "type": "string" + }, + "status": { + "description": "批次状态,可选", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchStatus" + } + ] + } + } + }, + "dto.PigHouseResponse": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "dto.PlanResponse": { + "type": "object", + "properties": { + "content_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanContentType" + } + ], + "example": "任务" + }, + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "根据温度自动调节风扇和加热器" + }, + "execute_count": { + "type": "integer", + "example": 0 + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanStatus" + } + ], + "example": "已启用" + }, + "sub_plans": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SubPlanResponse" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskResponse" + } + } + } + }, + "dto.SubPlanResponse": { + "type": "object", + "properties": { + "child_plan": { + "$ref": "#/definitions/dto.PlanResponse" + }, + "child_plan_id": { + "type": "integer", + "example": 2 + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "parent_plan_id": { + "type": "integer", + "example": 1 + } + } + }, + "dto.TaskRequest": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "打开1号风扇" + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "打开风扇" + }, + "parameters": { + "type": "object", + "additionalProperties": true + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/models.TaskType" + } + ], + "example": "等待" + } + } + }, + "dto.TaskResponse": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "打开1号风扇" + }, + "execution_order": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "打开风扇" + }, + "parameters": { + "type": "object", + "additionalProperties": true + }, + "plan_id": { + "type": "integer", + "example": 1 + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/models.TaskType" + } + ], + "example": "等待" + } + } + }, + "dto.UpdateAreaControllerRequest": { + "type": "object", + "required": [ + "name", + "network_id" + ], + "properties": { + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "network_id": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceRequest": { + "type": "object", + "required": [ + "area_controller_id", + "device_template_id", + "name" + ], + "properties": { + "area_controller_id": { + "type": "integer" + }, + "device_template_id": { + "type": "integer" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true + } + } + }, + "dto.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "dto.UpdatePenRequest": { + "type": "object", + "required": [ + "capacity", + "house_id", + "pen_number", + "status" + ], + "properties": { + "capacity": { + "type": "integer" + }, + "house_id": { + "type": "integer" + }, + "pen_number": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/models.PenStatus" + } + } + }, + "dto.UpdatePigHouseRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.UpdatePlanRequest": { + "type": "object", + "required": [ + "execution_type" + ], + "properties": { + "cron_expression": { + "type": "string", + "example": "0 0 6 * * *" + }, + "description": { + "type": "string", + "example": "更新后的描述" + }, + "execute_num": { + "type": "integer", + "example": 10 + }, + "execution_type": { + "allOf": [ + { + "$ref": "#/definitions/models.PlanExecutionType" + } + ], + "example": "自动" + }, + "name": { + "type": "string", + "example": "猪舍温度控制计划V2" + }, + "sub_plan_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TaskRequest" + } + } + } + }, + "models.DeviceCategory": { + "type": "string", + "enum": [ + "执行器", + "传感器" + ], + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] + }, + "models.PenStatus": { + "type": "string", + "enum": [ + "空闲", + "占用", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "x-enum-varnames": [ + "PenStatusEmpty", + "PenStatusOccupied", + "PenStatusSickPen", + "PenStatusRecovering", + "PenStatusCleaning", + "PenStatusUnderMaint" + ] + }, + "models.PigBatchOriginType": { + "type": "string", + "enum": [ + "自繁", + "外购" + ], + "x-enum-varnames": [ + "OriginTypeSelfFarrowed", + "OriginTypePurchased" + ] + }, + "models.PigBatchStatus": { + "type": "string", + "enum": [ + "保育", + "生长", + "育肥", + "待售", + "已出售", + "已归档" + ], + "x-enum-comments": { + "BatchStatusArchived": "批次结束(如全群淘汰等)", + "BatchStatusFinishing": "最后的育肥阶段", + "BatchStatusForSale": "达到出栏标准", + "BatchStatusGrowing": "生长育肥阶段", + "BatchStatusWeaning": "从断奶到保育结束" + }, + "x-enum-descriptions": [ + "从断奶到保育结束", + "生长育肥阶段", + "最后的育肥阶段", + "达到出栏标准", + "", + "批次结束(如全群淘汰等)" + ], + "x-enum-varnames": [ + "BatchStatusWeaning", + "BatchStatusGrowing", + "BatchStatusFinishing", + "BatchStatusForSale", + "BatchStatusSold", + "BatchStatusArchived" + ] + }, + "models.PlanContentType": { + "type": "string", + "enum": [ + "子计划", + "任务" + ], + "x-enum-comments": { + "PlanContentTypeSubPlans": "计划包含子计划", + "PlanContentTypeTasks": "计划包含任务" + }, + "x-enum-descriptions": [ + "计划包含子计划", + "计划包含任务" + ], + "x-enum-varnames": [ + "PlanContentTypeSubPlans", + "PlanContentTypeTasks" + ] + }, + "models.PlanExecutionType": { + "type": "string", + "enum": [ + "自动", + "手动" + ], + "x-enum-comments": { + "PlanExecutionTypeAutomatic": "自动执行 (包含定时和循环)", + "PlanExecutionTypeManual": "手动执行" + }, + "x-enum-descriptions": [ + "自动执行 (包含定时和循环)", + "手动执行" + ], + "x-enum-varnames": [ + "PlanExecutionTypeAutomatic", + "PlanExecutionTypeManual" + ] + }, + "models.PlanStatus": { + "type": "string", + "enum": [ + "已禁用", + "已启用", + "执行完毕", + "执行失败" + ], + "x-enum-comments": { + "PlanStatusDisabled": "禁用计划", + "PlanStatusEnabled": "启用计划", + "PlanStatusFailed": "执行失败", + "PlanStatusStopped": "执行完毕" + }, + "x-enum-descriptions": [ + "禁用计划", + "启用计划", + "执行完毕", + "执行失败" + ], + "x-enum-varnames": [ + "PlanStatusDisabled", + "PlanStatusEnabled", + "PlanStatusStopped", + "PlanStatusFailed" + ] + }, + "models.SensorType": { + "type": "string", + "enum": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, + "models.TaskType": { + "type": "string", + "enum": [ + "计划分析", + "等待", + "下料" + ], + "x-enum-comments": { + "TaskPlanAnalysis": "解析Plan的Task列表并添加到待执行队列的特殊任务", + "TaskTypeReleaseFeedWeight": "下料口释放指定重量任务", + "TaskTypeWaiting": "等待任务" + }, + "x-enum-descriptions": [ + "解析Plan的Task列表并添加到待执行队列的特殊任务", + "等待任务", + "下料口释放指定重量任务" + ], + "x-enum-varnames": [ + "TaskPlanAnalysis", + "TaskTypeWaiting", + "TaskTypeReleaseFeedWeight" + ] + }, + "models.ValueDescriptor": { + "type": "object", + "properties": { + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" + }, + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" + }, + "type": { + "$ref": "#/definitions/models.SensorType" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..4bf7ca4 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,1809 @@ +definitions: + controller.Response: + properties: + code: + allOf: + - $ref: '#/definitions/controller.ResponseCode' + description: 业务状态码 + data: + description: 业务数据 + message: + description: 提示信息 + type: string + type: object + controller.ResponseCode: + enum: + - 2000 + - 2001 + - 4000 + - 4001 + - 4004 + - 4009 + - 5000 + - 5003 + type: integer + x-enum-comments: + CodeBadRequest: 请求参数错误 + CodeConflict: 资源冲突 + CodeCreated: 创建成功 + CodeInternalError: 服务器内部错误 + CodeNotFound: 资源未找到 + CodeServiceUnavailable: 服务不可用 + CodeSuccess: 操作成功 + CodeUnauthorized: 未授权 + x-enum-descriptions: + - 操作成功 + - 创建成功 + - 请求参数错误 + - 未授权 + - 资源未找到 + - 资源冲突 + - 服务器内部错误 + - 服务不可用 + x-enum-varnames: + - CodeSuccess + - CodeCreated + - CodeBadRequest + - CodeUnauthorized + - CodeNotFound + - CodeConflict + - CodeInternalError + - CodeServiceUnavailable + dto.AreaControllerResponse: + properties: + created_at: + type: string + id: + type: integer + location: + type: string + name: + type: string + network_id: + type: string + properties: + additionalProperties: true + type: object + status: + type: string + updated_at: + type: string + type: object + dto.CreateAreaControllerRequest: + properties: + location: + type: string + name: + type: string + network_id: + type: string + properties: + additionalProperties: true + type: object + required: + - name + - network_id + type: object + dto.CreateDeviceRequest: + properties: + area_controller_id: + type: integer + device_template_id: + type: integer + location: + type: string + name: + type: string + properties: + additionalProperties: true + type: object + required: + - area_controller_id + - device_template_id + - name + type: object + dto.CreateDeviceTemplateRequest: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + description: + type: string + manufacturer: + type: string + name: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + required: + - category + - commands + - name + type: object + dto.CreatePenRequest: + properties: + capacity: + type: integer + house_id: + type: integer + pen_number: + type: string + status: + $ref: '#/definitions/models.PenStatus' + required: + - capacity + - house_id + - pen_number + - status + type: object + dto.CreatePigHouseRequest: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + dto.CreatePlanRequest: + properties: + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 根据温度自动调节风扇和加热器 + type: string + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + name: + example: 猪舍温度控制计划 + type: string + sub_plan_ids: + items: + type: integer + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskRequest' + type: array + required: + - execution_type + - name + type: object + dto.CreateUserRequest: + properties: + password: + example: password123 + type: string + username: + example: newuser + type: string + required: + - password + - username + type: object + dto.CreateUserResponse: + properties: + id: + example: 1 + type: integer + username: + example: newuser + type: string + type: object + dto.DeviceResponse: + properties: + area_controller_id: + type: integer + area_controller_name: + type: string + created_at: + type: string + device_template_id: + type: integer + device_template_name: + type: string + id: + type: integer + location: + type: string + name: + type: string + properties: + additionalProperties: true + type: object + updated_at: + type: string + type: object + dto.DeviceTemplateResponse: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + created_at: + type: string + description: + type: string + id: + type: integer + manufacturer: + type: string + name: + type: string + updated_at: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + type: object + dto.HistoryResponse: + properties: + action_type: + example: 更新设备 + type: string + description: + example: 设备更新成功 + type: string + target_resource: {} + time: + type: string + user_id: + example: 101 + type: integer + username: + example: testuser + type: string + type: object + dto.ListHistoryResponse: + properties: + history: + items: + $ref: '#/definitions/dto.HistoryResponse' + type: array + total: + example: 100 + type: integer + type: object + dto.LoginRequest: + properties: + identifier: + description: Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 + example: testuser + type: string + password: + example: password123 + type: string + required: + - identifier + - password + type: object + dto.LoginResponse: + properties: + id: + example: 1 + type: integer + token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + type: string + username: + example: testuser + type: string + type: object + dto.PenResponse: + properties: + capacity: + type: integer + house_id: + type: integer + id: + type: integer + pen_number: + type: string + pig_batch_id: + type: integer + status: + $ref: '#/definitions/models.PenStatus' + type: object + dto.PigBatchCreateDTO: + properties: + batch_number: + description: 批次编号,必填 + type: string + initial_count: + description: 初始数量,必填,最小为1 + minimum: 1 + type: integer + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源,必填 + start_date: + description: 批次开始日期,必填 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态,必填 + required: + - batch_number + - initial_count + - origin_type + - start_date + - status + type: object + dto.PigBatchResponseDTO: + properties: + batch_number: + description: 批次编号 + type: string + create_time: + description: 创建时间 + type: string + end_date: + description: 批次结束日期 + type: string + id: + description: 批次ID + type: integer + initial_count: + description: 初始数量 + type: integer + is_active: + description: 是否活跃 + type: boolean + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源 + start_date: + description: 批次开始日期 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态 + update_time: + description: 更新时间 + type: string + type: object + dto.PigBatchUpdateDTO: + properties: + batch_number: + description: 批次编号,可选 + type: string + end_date: + description: 批次结束日期,可选 + type: string + initial_count: + description: 初始数量,可选 + type: integer + origin_type: + allOf: + - $ref: '#/definitions/models.PigBatchOriginType' + description: 批次来源,可选 + start_date: + description: 批次开始日期,可选 + type: string + status: + allOf: + - $ref: '#/definitions/models.PigBatchStatus' + description: 批次状态,可选 + type: object + dto.PigHouseResponse: + properties: + description: + type: string + id: + type: integer + name: + type: string + type: object + dto.PlanResponse: + properties: + content_type: + allOf: + - $ref: '#/definitions/models.PlanContentType' + example: 任务 + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 根据温度自动调节风扇和加热器 + type: string + execute_count: + example: 0 + type: integer + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + id: + example: 1 + type: integer + name: + example: 猪舍温度控制计划 + type: string + status: + allOf: + - $ref: '#/definitions/models.PlanStatus' + example: 已启用 + sub_plans: + items: + $ref: '#/definitions/dto.SubPlanResponse' + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskResponse' + type: array + type: object + dto.SubPlanResponse: + properties: + child_plan: + $ref: '#/definitions/dto.PlanResponse' + child_plan_id: + example: 2 + type: integer + execution_order: + example: 1 + type: integer + id: + example: 1 + type: integer + parent_plan_id: + example: 1 + type: integer + type: object + dto.TaskRequest: + properties: + description: + example: 打开1号风扇 + type: string + execution_order: + example: 1 + type: integer + name: + example: 打开风扇 + type: string + parameters: + additionalProperties: true + type: object + type: + allOf: + - $ref: '#/definitions/models.TaskType' + example: 等待 + type: object + dto.TaskResponse: + properties: + description: + example: 打开1号风扇 + type: string + execution_order: + example: 1 + type: integer + id: + example: 1 + type: integer + name: + example: 打开风扇 + type: string + parameters: + additionalProperties: true + type: object + plan_id: + example: 1 + type: integer + type: + allOf: + - $ref: '#/definitions/models.TaskType' + example: 等待 + type: object + dto.UpdateAreaControllerRequest: + properties: + location: + type: string + name: + type: string + network_id: + type: string + properties: + additionalProperties: true + type: object + required: + - name + - network_id + type: object + dto.UpdateDeviceRequest: + properties: + area_controller_id: + type: integer + device_template_id: + type: integer + location: + type: string + name: + type: string + properties: + additionalProperties: true + type: object + required: + - area_controller_id + - device_template_id + - name + type: object + dto.UpdateDeviceTemplateRequest: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + description: + type: string + manufacturer: + type: string + name: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + required: + - category + - commands + - name + type: object + dto.UpdatePenRequest: + properties: + capacity: + type: integer + house_id: + type: integer + pen_number: + type: string + status: + $ref: '#/definitions/models.PenStatus' + required: + - capacity + - house_id + - pen_number + - status + type: object + dto.UpdatePigHouseRequest: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + dto.UpdatePlanRequest: + properties: + cron_expression: + example: 0 0 6 * * * + type: string + description: + example: 更新后的描述 + type: string + execute_num: + example: 10 + type: integer + execution_type: + allOf: + - $ref: '#/definitions/models.PlanExecutionType' + example: 自动 + name: + example: 猪舍温度控制计划V2 + type: string + sub_plan_ids: + items: + type: integer + type: array + tasks: + items: + $ref: '#/definitions/dto.TaskRequest' + type: array + required: + - execution_type + type: object + models.DeviceCategory: + enum: + - 执行器 + - 传感器 + type: string + x-enum-varnames: + - CategoryActuator + - CategorySensor + models.PenStatus: + enum: + - 空闲 + - 占用 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 + type: string + x-enum-varnames: + - PenStatusEmpty + - PenStatusOccupied + - PenStatusSickPen + - PenStatusRecovering + - PenStatusCleaning + - PenStatusUnderMaint + models.PigBatchOriginType: + enum: + - 自繁 + - 外购 + type: string + x-enum-varnames: + - OriginTypeSelfFarrowed + - OriginTypePurchased + models.PigBatchStatus: + enum: + - 保育 + - 生长 + - 育肥 + - 待售 + - 已出售 + - 已归档 + type: string + x-enum-comments: + BatchStatusArchived: 批次结束(如全群淘汰等) + BatchStatusFinishing: 最后的育肥阶段 + BatchStatusForSale: 达到出栏标准 + BatchStatusGrowing: 生长育肥阶段 + BatchStatusWeaning: 从断奶到保育结束 + x-enum-descriptions: + - 从断奶到保育结束 + - 生长育肥阶段 + - 最后的育肥阶段 + - 达到出栏标准 + - "" + - 批次结束(如全群淘汰等) + x-enum-varnames: + - BatchStatusWeaning + - BatchStatusGrowing + - BatchStatusFinishing + - BatchStatusForSale + - BatchStatusSold + - BatchStatusArchived + models.PlanContentType: + enum: + - 子计划 + - 任务 + type: string + x-enum-comments: + PlanContentTypeSubPlans: 计划包含子计划 + PlanContentTypeTasks: 计划包含任务 + x-enum-descriptions: + - 计划包含子计划 + - 计划包含任务 + x-enum-varnames: + - PlanContentTypeSubPlans + - PlanContentTypeTasks + models.PlanExecutionType: + enum: + - 自动 + - 手动 + type: string + x-enum-comments: + PlanExecutionTypeAutomatic: 自动执行 (包含定时和循环) + PlanExecutionTypeManual: 手动执行 + x-enum-descriptions: + - 自动执行 (包含定时和循环) + - 手动执行 + x-enum-varnames: + - PlanExecutionTypeAutomatic + - PlanExecutionTypeManual + models.PlanStatus: + enum: + - 已禁用 + - 已启用 + - 执行完毕 + - 执行失败 + type: string + x-enum-comments: + PlanStatusDisabled: 禁用计划 + PlanStatusEnabled: 启用计划 + PlanStatusFailed: 执行失败 + PlanStatusStopped: 执行完毕 + x-enum-descriptions: + - 禁用计划 + - 启用计划 + - 执行完毕 + - 执行失败 + x-enum-varnames: + - PlanStatusDisabled + - PlanStatusEnabled + - PlanStatusStopped + - PlanStatusFailed + models.SensorType: + enum: + - 信号强度 + - 电池电量 + - 温度 + - 湿度 + - 重量 + type: string + x-enum-comments: + SensorTypeBatteryLevel: 电池电量 + SensorTypeHumidity: 湿度 + SensorTypeSignalMetrics: 信号强度 + SensorTypeTemperature: 温度 + SensorTypeWeight: 重量 + x-enum-descriptions: + - 信号强度 + - 电池电量 + - 温度 + - 湿度 + - 重量 + x-enum-varnames: + - SensorTypeSignalMetrics + - SensorTypeBatteryLevel + - SensorTypeTemperature + - SensorTypeHumidity + - SensorTypeWeight + models.TaskType: + enum: + - 计划分析 + - 等待 + - 下料 + type: string + x-enum-comments: + TaskPlanAnalysis: 解析Plan的Task列表并添加到待执行队列的特殊任务 + TaskTypeReleaseFeedWeight: 下料口释放指定重量任务 + TaskTypeWaiting: 等待任务 + x-enum-descriptions: + - 解析Plan的Task列表并添加到待执行队列的特殊任务 + - 等待任务 + - 下料口释放指定重量任务 + x-enum-varnames: + - TaskPlanAnalysis + - TaskTypeWaiting + - TaskTypeReleaseFeedWeight + models.ValueDescriptor: + properties: + multiplier: + description: 乘数,用于原始数据转换 + type: number + offset: + description: 偏移量,用于原始数据转换 + type: number + type: + $ref: '#/definitions/models.SensorType' + type: object +info: + contact: {} +paths: + /api/v1/area-controllers: + get: + description: 获取系统中所有区域主控的列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.AreaControllerResponse' + type: array + type: object + summary: 获取所有区域主控列表 + tags: + - 区域主控管理 + post: + consumes: + - application/json + description: 根据提供的信息创建一个新区域主控 + parameters: + - description: 区域主控信息 + in: body + name: areaController + required: true + schema: + $ref: '#/definitions/dto.CreateAreaControllerRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.AreaControllerResponse' + type: object + summary: 创建新区域主控 + tags: + - 区域主控管理 + /api/v1/area-controllers/{id}: + delete: + description: 根据ID删除一个区域主控(软删除) + parameters: + - description: 区域主控ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.Response' + summary: 删除区域主控 + tags: + - 区域主控管理 + get: + description: 根据ID获取单个区域主控的详细信息 + parameters: + - description: 区域主控ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.AreaControllerResponse' + type: object + summary: 获取区域主控信息 + tags: + - 区域主控管理 + put: + consumes: + - application/json + description: 根据ID更新一个已存在的区域主控信息 + parameters: + - description: 区域主控ID + in: path + name: id + required: true + type: string + - description: 要更新的区域主控信息 + in: body + name: areaController + required: true + schema: + $ref: '#/definitions/dto.UpdateAreaControllerRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.AreaControllerResponse' + type: object + summary: 更新区域主控信息 + tags: + - 区域主控管理 + /api/v1/device-templates: + get: + description: 获取系统中所有设备模板的列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.DeviceTemplateResponse' + type: array + type: object + summary: 获取设备模板列表 + tags: + - 设备模板管理 + post: + consumes: + - application/json + description: 根据提供的信息创建一个新设备模板 + parameters: + - description: 设备模板信息 + in: body + name: deviceTemplate + required: true + schema: + $ref: '#/definitions/dto.CreateDeviceTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceTemplateResponse' + type: object + summary: 创建新设备模板 + tags: + - 设备模板管理 + /api/v1/device-templates/{id}: + delete: + description: 根据设备模板ID删除一个设备模板(软删除) + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.Response' + summary: 删除设备模板 + tags: + - 设备模板管理 + get: + description: 根据设备模板ID获取单个设备模板的详细信息 + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceTemplateResponse' + type: object + summary: 获取设备模板信息 + tags: + - 设备模板管理 + put: + consumes: + - application/json + description: 根据设备模板ID更新一个已存在的设备模板信息 + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + - description: 要更新的设备模板信息 + in: body + name: deviceTemplate + required: true + schema: + $ref: '#/definitions/dto.UpdateDeviceTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceTemplateResponse' + type: object + summary: 更新设备模板信息 + tags: + - 设备模板管理 + /api/v1/devices: + get: + description: 获取系统中所有设备的列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.DeviceResponse' + type: array + type: object + summary: 获取设备列表 + tags: + - 设备管理 + post: + consumes: + - application/json + description: 根据提供的信息创建一个新设备 + parameters: + - description: 设备信息 + in: body + name: device + required: true + schema: + $ref: '#/definitions/dto.CreateDeviceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceResponse' + type: object + summary: 创建新设备 + tags: + - 设备管理 + /api/v1/devices/{id}: + delete: + description: 根据设备ID删除一个设备(软删除) + parameters: + - description: 设备ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.Response' + summary: 删除设备 + tags: + - 设备管理 + get: + description: 根据设备ID获取单个设备的详细信息 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceResponse' + type: object + summary: 获取设备信息 + tags: + - 设备管理 + put: + consumes: + - application/json + description: 根据设备ID更新一个已存在的设备信息 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: string + - description: 要更新的设备信息 + in: body + name: device + required: true + schema: + $ref: '#/definitions/dto.UpdateDeviceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.DeviceResponse' + type: object + summary: 更新设备信息 + tags: + - 设备管理 + /api/v1/pens: + get: + description: 获取所有猪栏的列表 + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PenResponse' + type: array + type: object + summary: 获取猪栏列表 + tags: + - 猪场管理 + post: + consumes: + - application/json + description: 创建一个新的猪栏 + parameters: + - description: 猪栏信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreatePenRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 创建猪栏 + tags: + - 猪场管理 + /api/v1/pens/{id}: + delete: + description: 根据ID删除一个猪栏 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪栏 + tags: + - 猪场管理 + get: + description: 根据ID获取单个猪栏信息 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 获取单个猪栏 + tags: + - 猪场管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪栏信息 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + - description: 猪栏信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePenRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 更新猪栏 + tags: + - 猪场管理 + /api/v1/pig-batches: + get: + description: 获取所有猪批次的列表,支持按活跃状态筛选 + parameters: + - description: 是否活跃 (true/false) + in: query + name: is_active + type: boolean + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: array + type: object + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 获取猪批次列表 + tags: + - 猪批次管理 + post: + consumes: + - application/json + description: 创建一个新的猪批次 + parameters: + - description: 猪批次信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchCreateDTO' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 创建猪批次 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}: + delete: + description: 根据ID删除一个猪批次 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + "400": + description: 无效的ID格式 + schema: + $ref: '#/definitions/controller.Response' + "404": + description: 猪批次不存在 + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪批次 + tags: + - 猪批次管理 + get: + description: 根据ID获取单个猪批次信息 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + "400": + description: 无效的ID格式 + schema: + $ref: '#/definitions/controller.Response' + "404": + description: 猪批次不存在 + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 获取单个猪批次 + tags: + - 猪批次管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪批次信息 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 猪批次信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchUpdateDTO' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigBatchResponseDTO' + type: object + "400": + description: 请求参数错误或无效的ID格式 + schema: + $ref: '#/definitions/controller.Response' + "404": + description: 猪批次不存在 + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 更新猪批次 + tags: + - 猪批次管理 + /api/v1/pig-houses: + get: + description: 获取所有猪舍的列表 + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PigHouseResponse' + type: array + type: object + summary: 获取猪舍列表 + tags: + - 猪场管理 + post: + consumes: + - application/json + description: 创建一个新的猪舍 + parameters: + - description: 猪舍信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreatePigHouseRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 创建猪舍 + tags: + - 猪场管理 + /api/v1/pig-houses/{id}: + delete: + description: 根据ID删除一个猪舍 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 删除猪舍 + tags: + - 猪场管理 + get: + description: 根据ID获取单个猪舍信息 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 获取单个猪舍 + tags: + - 猪场管理 + put: + consumes: + - application/json + description: 更新一个已存在的猪舍信息 + parameters: + - description: 猪舍ID + in: path + name: id + required: true + type: integer + - description: 猪舍信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePigHouseRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PigHouseResponse' + type: object + summary: 更新猪舍 + tags: + - 猪场管理 + /api/v1/plans: + get: + description: 获取所有计划的列表 + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取列表 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/dto.PlanResponse' + type: array + type: object + summary: 获取计划列表 + tags: + - 计划管理 + post: + consumes: + - application/json + description: 创建一个新的计划,包括其基本信息和所有关联的子计划/任务。 + parameters: + - description: 计划信息 + in: body + name: plan + required: true + schema: + $ref: '#/definitions/dto.CreatePlanRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PlanResponse' + type: object + summary: 创建计划 + tags: + - 计划管理 + /api/v1/plans/{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' + 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.PlanResponse' + type: object + summary: 获取计划详情 + tags: + - 计划管理 + put: + consumes: + - application/json + description: 根据计划ID更新计划的详细信息。 + parameters: + - description: 计划ID + in: path + name: id + required: true + type: integer + - description: 更新后的计划信息 + in: body + name: plan + required: true + schema: + $ref: '#/definitions/dto.UpdatePlanRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PlanResponse' + type: object + summary: 更新计划 + tags: + - 计划管理 + /api/v1/plans/{id}/start: + post: + 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' + summary: 启动计划 + tags: + - 计划管理 + /api/v1/plans/{id}/stop: + post: + 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' + summary: 停止计划 + tags: + - 计划管理 + /api/v1/users: + post: + consumes: + - application/json + description: 根据用户名和密码创建一个新的系统用户。 + parameters: + - description: 用户信息 + in: body + name: user + required: true + schema: + $ref: '#/definitions/dto.CreateUserRequest' + produces: + - application/json + responses: + "200": + description: 业务码为201代表创建成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.CreateUserResponse' + type: object + summary: 创建新用户 + tags: + - 用户管理 + /api/v1/users/{id}/history: + get: + description: 根据用户ID,分页获取该用户的操作审计日志。 + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页大小 + in: query + name: page_size + type: integer + - description: 按操作类型过滤 + in: query + name: action_type + type: string + produces: + - application/json + responses: + "200": + description: 业务码为200代表成功获取 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ListHistoryResponse' + type: object + summary: 获取指定用户的操作历史 + tags: + - 用户管理 + /api/v1/users/login: + post: + consumes: + - application/json + description: 用户可以使用用户名、邮箱、手机号、微信号或飞书账号进行登录,成功后返回 JWT 令牌。 + parameters: + - description: 登录凭证 + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/dto.LoginRequest' + produces: + - application/json + responses: + "200": + description: 业务码为200代表登录成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.LoginResponse' + type: object + summary: 用户登录 + tags: + - 用户管理 +swagger: "2.0" From 8bb0a54f1838805d85fc4b6c67704e279c2653b7 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 00:47:27 +0800 Subject: [PATCH 20/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=89=B9=E6=AC=A1=E7=BB=91=E5=AE=9A=E7=9A=84=E7=8C=AA=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 68 ++++++++ docs/swagger.json | 68 ++++++++ docs/swagger.yaml | 45 ++++++ internal/app/api/api.go | 1 + .../management/pig_batch_controller.go | 45 ++++++ .../management/pig_farm_controller.go | 8 +- internal/app/dto/pig_batch_dto.go | 7 +- internal/app/service/pig_batch_service.go | 149 ++++++++++++++++-- internal/app/service/pig_farm_service.go | 4 +- internal/core/application.go | 5 +- internal/infra/models/farm_asset.go | 2 +- .../infra/repository/pig_farm_repository.go | 65 +++++++- internal/infra/repository/unit_of_work.go | 58 +++++++ 13 files changed, 498 insertions(+), 27 deletions(-) create mode 100644 internal/infra/repository/unit_of_work.go diff --git a/docs/docs.go b/docs/docs.go index 2f528e9..82ab941 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1085,6 +1085,71 @@ const docTemplate = `{ } } }, + "/api/v1/pig-batches/{id}/pens": { + "put": { + "description": "更新指定猪批次当前关联的猪栏列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次关联的猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次关联的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次或猪栏不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "409": { + "description": "业务逻辑冲突 (如猪栏已被占用)", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -2280,6 +2345,9 @@ const docTemplate = `{ } } }, + "dto.PigBatchUpdatePensRequest": { + "type": "object" + }, "dto.PigHouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7ce156b..44ea4f2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1074,6 +1074,71 @@ } } }, + "/api/v1/pig-batches/{id}/pens": { + "put": { + "description": "更新指定猪批次当前关联的猪栏列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "更新猪批次关联的猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "猪批次关联的猪栏ID列表", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "400": { + "description": "请求参数错误或无效的ID格式", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "404": { + "description": "猪批次或猪栏不存在", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "409": { + "description": "业务逻辑冲突 (如猪栏已被占用)", + "schema": { + "$ref": "#/definitions/controller.Response" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -2269,6 +2334,9 @@ } } }, + "dto.PigBatchUpdatePensRequest": { + "type": "object" + }, "dto.PigHouseResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4bf7ca4..0292e48 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -401,6 +401,8 @@ definitions: - $ref: '#/definitions/models.PigBatchStatus' description: 批次状态,可选 type: object + dto.PigBatchUpdatePensRequest: + type: object dto.PigHouseResponse: properties: description: @@ -1438,6 +1440,49 @@ paths: summary: 更新猪批次 tags: - 猪批次管理 + /api/v1/pig-batches/{id}/pens: + put: + consumes: + - application/json + description: 更新指定猪批次当前关联的猪栏列表 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 猪批次关联的猪栏ID列表 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.PigBatchUpdatePensRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/controller.Response' + "400": + description: 请求参数错误或无效的ID格式 + schema: + $ref: '#/definitions/controller.Response' + "404": + description: 猪批次或猪栏不存在 + schema: + $ref: '#/definitions/controller.Response' + "409": + description: 业务逻辑冲突 (如猪栏已被占用) + schema: + $ref: '#/definitions/controller.Response' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/controller.Response' + summary: 更新猪批次关联的猪栏 + tags: + - 猪批次管理 /api/v1/pig-houses: get: description: 获取所有猪舍的列表 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 6c00efd..fc70039 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -233,6 +233,7 @@ func (a *API) setupRoutes() { pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) + pigBatchGroup.PUT("/:id/pens", a.pigBatchController.UpdatePigBatchPens) } a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 538947f..5965c49 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -188,3 +188,48 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs) } + +// UpdatePigBatchPens godoc +// @Summary 更新猪批次关联的猪栏 +// @Description 更新指定猪批次当前关联的猪栏列表 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.PigBatchUpdatePensRequest true "猪批次关联的猪栏ID列表" +// @Success 200 {object} controller.Response "更新成功" +// @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" +// @Failure 404 {object} controller.Response "猪批次或猪栏不存在" +// @Failure 409 {object} controller.Response "业务逻辑冲突 (如猪栏已被占用)" +// @Failure 500 {object} controller.Response "内部服务器错误" +// @Router /api/v1/pig-batches/{id}/pens [put] +func (c *PigBatchController) UpdatePigBatchPens(ctx *gin.Context) { + const action = "更新猪批次关联猪栏" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.PigBatchUpdatePensRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + err = c.service.UpdatePigBatchPens(uint(batchID), req.PenIDs) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenStatusInvalidForAllocation) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次关联猪栏失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", nil, action, "更新成功", batchID) +} diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index 9f37005..fc0defa 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -229,7 +229,7 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) } @@ -267,7 +267,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) } @@ -296,7 +296,7 @@ func (c *PigFarmController) ListPens(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, }) } @@ -344,7 +344,7 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) { HouseID: pen.HouseID, Capacity: pen.Capacity, Status: pen.Status, - PigBatchID: pen.PigBatchID, + PigBatchID: *pen.PigBatchID, } controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) } diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go index c90bedd..1587bd1 100644 --- a/internal/app/dto/pig_batch_dto.go +++ b/internal/app/dto/pig_batch_dto.go @@ -3,7 +3,7 @@ package dto import ( "time" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" // 导入 models 包以使用 PigBatchOriginType 和 PigBatchStatus + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) // PigBatchCreateDTO 定义了创建猪批次的请求结构 @@ -43,3 +43,8 @@ type PigBatchResponseDTO struct { CreateTime time.Time `json:"create_time"` // 创建时间 UpdateTime time.Time `json:"update_time"` // 更新时间 } + +// PigBatchUpdatePensRequest 用于更新猪批次关联猪栏的请求体 +type PigBatchUpdatePensRequest struct { + PenIDs []uint `json:"penIDs" binding:"required,min=0" example:"[1,2,3]"` +} diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 5b6eb13..f75f0ac 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "fmt" "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -12,8 +13,13 @@ import ( ) var ( - ErrPigBatchNotFound = errors.New("指定的猪批次不存在") - ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") // 新增错误:活跃的猪批次不能被删除 + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") + ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") + ErrPenNotFound = errors.New("指定的猪栏不存在") + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次占用") + ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") + ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") ) // PigBatchService 提供了猪批次管理的业务逻辑 @@ -23,18 +29,24 @@ type PigBatchService interface { UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) + // UpdatePigBatchPens 更新猪批次关联的猪栏 + UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error } type pigBatchService struct { - logger *logs.Logger - repo repository.PigBatchRepository + logger *logs.Logger + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigFarmRepo repository.PigFarmRepository // 猪场资产仓库 (包含猪栏操作) + uow repository.UnitOfWork // 工作单元,用于事务管理 } // NewPigBatchService 创建一个新的 PigBatchService 实例 -func NewPigBatchService(repo repository.PigBatchRepository, logger *logs.Logger) PigBatchService { +func NewPigBatchService(pigBatchRepo repository.PigBatchRepository, pigFarmRepo repository.PigFarmRepository, uow repository.UnitOfWork, logger *logs.Logger) PigBatchService { return &pigBatchService{ - logger: logger, - repo: repo, + logger: logger, + pigBatchRepo: pigBatchRepo, + pigFarmRepo: pigFarmRepo, + uow: uow, } } @@ -67,7 +79,7 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa Status: dto.Status, } - createdBatch, err := s.repo.CreatePigBatch(batch) + createdBatch, err := s.pigBatchRepo.CreatePigBatch(batch) if err != nil { s.logger.Errorf("创建猪批次失败: %v", err) return nil, err @@ -78,7 +90,7 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa // GetPigBatch 处理获取单个猪批次的业务逻辑 func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) { - batch, err := s.repo.GetPigBatchByID(id) + batch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPigBatchNotFound @@ -92,7 +104,7 @@ func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) // UpdatePigBatch 处理更新猪批次的业务逻辑 func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { - existingBatch, err := s.repo.GetPigBatchByID(id) + existingBatch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrPigBatchNotFound @@ -121,7 +133,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* existingBatch.Status = *dto.Status } - updatedBatch, err := s.repo.UpdatePigBatch(existingBatch) + updatedBatch, err := s.pigBatchRepo.UpdatePigBatch(existingBatch) if err != nil { s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err) return nil, err @@ -133,7 +145,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* // DeletePigBatch 处理删除猪批次的业务逻辑 func (s *pigBatchService) DeletePigBatch(id uint) error { // 1. 获取猪批次信息 - batch, err := s.repo.GetPigBatchByID(id) + batch, err := s.pigBatchRepo.GetPigBatchByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPigBatchNotFound @@ -148,7 +160,7 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { } // 3. 执行删除操作 - err = s.repo.DeletePigBatch(id) + err = s.pigBatchRepo.DeletePigBatch(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) || errors.New("未找到要删除的猪批次").Error() == err.Error() { return ErrPigBatchNotFound @@ -161,7 +173,7 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { // ListPigBatches 处理批量查询猪批次的业务逻辑 func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) { - batches, err := s.repo.ListPigBatches(isActive) + batches, err := s.pigBatchRepo.ListPigBatches(isActive) if err != nil { s.logger.Errorf("批量查询猪批次失败,错误: %v", err) return nil, err @@ -174,3 +186,112 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons return responseDTOs, nil } + +// UpdatePigBatchPens 更新猪批次关联的猪栏 +func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { + // 使用工作单元执行事务 + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次 + pigBatch, err := s.pigFarmRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + s.logger.Errorf("更新猪批次猪栏失败: 获取猪批次信息错误,ID: %d, 错误: %v", batchID, err) + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 获取当前关联的猪栏 + currentPens, err := s.pigFarmRepo.GetPensByBatchID(tx, batchID) + if err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 获取当前关联猪栏错误,批次ID: %d, 错误: %v", batchID, err) + return fmt.Errorf("获取当前关联猪栏失败: %w", err) + } + + currentPenMap := make(map[uint]models.Pen) + currentPenIDsSet := make(map[uint]struct{}) + for _, pen := range currentPens { + currentPenMap[pen.ID] = pen + currentPenIDsSet[pen.ID] = struct{}{} // 用于快速查找 + } + + // 3. 构建期望猪栏集合 + desiredPenIDsSet := make(map[uint]struct{}) + for _, penID := range desiredPenIDs { + desiredPenIDsSet[penID] = struct{}{} // 用于快速查找 + } + + // 4. 计算需要添加和移除的猪栏 + var pensToRemove []uint + for penID := range currentPenIDsSet { + if _, found := desiredPenIDsSet[penID]; !found { + pensToRemove = append(pensToRemove, penID) + } + } + + var pensToAdd []uint + for _, penID := range desiredPenIDs { + if _, found := currentPenIDsSet[penID]; !found { + pensToAdd = append(pensToAdd, penID) + } + } + + // 5. 处理移除猪栏 + for _, penID := range pensToRemove { + currentPen := currentPenMap[penID] + // 验证:确保猪栏确实与当前批次关联 + if currentPen.PigBatchID == nil || *currentPen.PigBatchID != batchID { + s.logger.Warnf("尝试移除未与批次 %d 关联的猪栏 %d", batchID, penID) + return fmt.Errorf("猪栏 %d 未与该批次关联,无法移除", penID) + } + + updates := make(map[string]interface{}) + updates["pig_batch_id"] = nil // 总是将 PigBatchID 设为 nil + + // 只有当猪栏当前状态是“占用”时,才将其状态改回“空闲” + if currentPen.Status == models.PenStatusOccupied { + updates["status"] = models.PenStatusEmpty + } + + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 移除猪栏 %d 失败: %v", penID, err) + return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) + } + } + + // 6. 处理添加猪栏 + for _, penID := range pensToAdd { + actualPen, err := s.pigFarmRepo.GetPenByIDTx(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + s.logger.Errorf("更新猪批次猪栏失败: 获取猪栏 %d 信息错误: %v", penID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 验证:猪栏必须是“空闲”状态且未被任何批次占用,才能被分配 + if actualPen.Status != models.PenStatusEmpty { + return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) + } + if actualPen.PigBatchID != nil { + return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) + } + + updates := map[string]interface{}{ + "pig_batch_id": &batchID, // 将 PigBatchID 设为当前批次ID的指针 + "status": models.PenStatusOccupied, // 分配后,状态变为“占用” + } + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + s.logger.Errorf("更新猪批次猪栏失败: 添加猪栏 %d 失败: %v", penID, err) + return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) + } + } + + return nil + }) +} diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 433e5e3..11478df 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -163,8 +163,8 @@ func (s *pigFarmService) DeletePen(id uint) error { } // 检查猪栏是否关联了活跃批次 - if pen.PigBatchID != 0 { - pigBatch, err := s.repo.GetPigBatchByID(pen.PigBatchID) + if *pen.PigBatchID != 0 { + pigBatch, err := s.repo.GetPigBatchByID(*pen.PigBatchID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } diff --git a/internal/core/application.go b/internal/core/application.go index 02039e2..80c2764 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -74,9 +74,12 @@ func NewApplication(configPath string) (*Application, error) { userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) + // 初始化事务管理器 + unitOfWork := repository.NewGormUnitOfWork(storage.GetDB()) + // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) - pigBatchService := service.NewPigBatchService(pigBatchRepo, logger) + pigBatchService := service.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork, logger) // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 90266ed..28e7c05 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -33,7 +33,7 @@ type Pen struct { gorm.Model PenNumber string `gorm:"not null;comment:猪栏的唯一编号, 如 A-01"` HouseID uint `gorm:"index;comment:所属猪舍ID"` - PigBatchID uint `gorm:"index;comment:关联的猪批次ID"` + PigBatchID *uint `gorm:"index;comment:关联的猪批次ID"` Capacity int `gorm:"not null;comment:设计容量 (头)"` Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` } diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index 366bd33..24b17c5 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -17,13 +17,23 @@ type PigFarmRepository interface { // Pen methods CreatePen(pen *models.Pen) error + // GetPenByID 根据ID获取单个猪栏 (非事务性) GetPenByID(id uint) (*models.Pen, error) + // GetPenByIDTx 根据ID获取单个猪栏 (事务性) + GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) ListPens() ([]models.Pen, error) UpdatePen(pen *models.Pen) error DeletePen(id uint) error + // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) + GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) + // UpdatePenFields 更新猪栏的指定字段 (事务性) + UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error // PigBatch methods + // GetPigBatchByID 根据ID获取单个猪批次 (非事务性) GetPigBatchByID(id uint) (*models.PigBatch, error) + // GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) + GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) } // gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 @@ -38,10 +48,12 @@ func NewGormPigFarmRepository(db *gorm.DB) PigFarmRepository { // --- PigHouse Implementation --- +// CreatePigHouse 创建一个新的猪舍 func (r *gormPigFarmRepository) CreatePigHouse(house *models.PigHouse) error { return r.db.Create(house).Error } +// GetPigHouseByID 根据ID获取单个猪舍 func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, error) { var house models.PigHouse if err := r.db.First(&house, id).Error; err != nil { @@ -50,6 +62,7 @@ func (r *gormPigFarmRepository) GetPigHouseByID(id uint) (*models.PigHouse, erro return &house, nil } +// ListPigHouses 列出所有猪舍 func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { var houses []models.PigHouse if err := r.db.Find(&houses).Error; err != nil { @@ -58,28 +71,31 @@ func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { return houses, nil } +// UpdatePigHouse 更新一个猪舍 func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error { result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要更新的猪舍或数据未改变 } return nil } +// DeletePigHouse 根据ID删除一个猪舍 func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { result := r.db.Delete(&models.PigHouse{}, id) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要删除的猪舍 } return nil } +// CountPensInHouse 统计猪舍中的猪栏数量 func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { var count int64 err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error @@ -88,10 +104,12 @@ func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { // --- Pen Implementation --- +// CreatePen 创建一个新的猪栏 func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { return r.db.Create(pen).Error } +// GetPenByID 根据ID获取单个猪栏 (非事务性) func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { var pen models.Pen if err := r.db.First(&pen, id).Error; err != nil { @@ -100,6 +118,16 @@ func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { return &pen, nil } +// GetPenByIDTx 根据ID获取单个猪栏 (事务性) +func (r *gormPigFarmRepository) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) { + var pen models.Pen + if err := tx.First(&pen, id).Error; err != nil { + return nil, err + } + return &pen, nil +} + +// ListPens 列出所有猪栏 func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { var pens []models.Pen if err := r.db.Find(&pens).Error; err != nil { @@ -108,30 +136,50 @@ func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { return pens, nil } +// UpdatePen 更新一个猪栏 func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要更新的猪栏或数据未改变 } return nil } +// DeletePen 根据ID删除一个猪栏 func (r *gormPigFarmRepository) DeletePen(id uint) error { result := r.db.Delete(&models.Pen{}, id) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound + return gorm.ErrRecordNotFound // 未找到要删除的猪栏 } return nil } +// GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) +func (r *gormPigFarmRepository) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) { + var pens []models.Pen + // 注意:PigBatchID 是指针类型,需要处理 nil 值 + result := tx.Where("pig_batch_id = ?", batchID).Find(&pens) + if result.Error != nil { + return nil, result.Error + } + return pens, nil +} + +// UpdatePenFields 更新猪栏的指定字段 (事务性) +func (r *gormPigFarmRepository) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) + return result.Error +} + // --- PigBatch Implementation --- +// GetPigBatchByID 根据ID获取单个猪批次 (非事务性) func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { var batch models.PigBatch if err := r.db.First(&batch, id).Error; err != nil { @@ -139,3 +187,12 @@ func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, erro } return &batch, nil } + +// GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) +func (r *gormPigFarmRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) { + var batch models.PigBatch + if err := tx.First(&batch, id).Error; err != nil { + return nil, err + } + return &batch, nil +} diff --git a/internal/infra/repository/unit_of_work.go b/internal/infra/repository/unit_of_work.go new file mode 100644 index 0000000..0d45573 --- /dev/null +++ b/internal/infra/repository/unit_of_work.go @@ -0,0 +1,58 @@ +package repository + +import ( + "fmt" + + "gorm.io/gorm" +) + +// UnitOfWork 定义了工作单元接口,用于抽象事务管理 +type UnitOfWork interface { + // ExecuteInTransaction 在一个数据库事务中执行给定的函数。 + // 如果函数返回错误,事务将被回滚;否则,事务将被提交。 + // tx 参数是当前事务的 GORM DB 实例,应传递给所有仓库方法。 + ExecuteInTransaction(fn func(tx *gorm.DB) error) error +} + +// gormUnitOfWork 是 UnitOfWork 接口的 GORM 实现 +type gormUnitOfWork struct { + db *gorm.DB +} + +// NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例 +func NewGormUnitOfWork(db *gorm.DB) UnitOfWork { + return &gormUnitOfWork{db: db} +} + +// ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑 +func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error { + tx := u.db.Begin() + if tx.Error != nil { + return fmt.Errorf("开启事务失败: %w", tx.Error) + } + + defer func() { + if r := recover(); r != nil { + tx.Rollback() + // 可以选择在此处记录 panic 日志 + // u.logger.Errorf("事务中发生 panic,已回滚: %v", r) + } else if tx.Error != nil { // 如果函数执行过程中返回错误,或者事务本身有错误,则回滚 + tx.Rollback() + // 可以选择在此处记录错误日志 + // u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) + } + }() + + // 执行业务逻辑函数 + if err := fn(tx); err != nil { + tx.Rollback() + return err // 返回业务逻辑函数中的错误 + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + return fmt.Errorf("提交事务失败: %w", err) + } + + return nil +} From 1bc36f5e1055784613b8aedfa47452cec4b99709 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 00:58:29 +0800 Subject: [PATCH 21/65] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_batch_service.go | 16 ++++-- internal/app/service/pig_farm_service.go | 39 ++++++++----- .../infra/repository/pig_batch_repository.go | 28 ++++------ .../infra/repository/pig_farm_repository.go | 56 ++++++++----------- 4 files changed, 73 insertions(+), 66 deletions(-) diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index f75f0ac..5d94f77 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -133,11 +133,15 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* existingBatch.Status = *dto.Status } - updatedBatch, err := s.pigBatchRepo.UpdatePigBatch(existingBatch) + updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(existingBatch) if err != nil { s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err) return nil, err } + // 如果没有行受影响,则认为猪批次不存在 + if rowsAffected == 0 { + return nil, ErrPigBatchNotFound + } return s.toPigBatchResponseDTO(updatedBatch), nil } @@ -160,14 +164,16 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { } // 3. 执行删除操作 - err = s.pigBatchRepo.DeletePigBatch(id) + rowsAffected, err := s.pigBatchRepo.DeletePigBatch(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) || errors.New("未找到要删除的猪批次").Error() == err.Error() { - return ErrPigBatchNotFound - } s.logger.Errorf("删除猪批次失败,ID: %d, 错误: %v", id, err) return err } + // 如果没有行受影响,则认为猪批次不存在 + if rowsAffected == 0 { + return ErrPigBatchNotFound + } + return nil } diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 11478df..f342520 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -71,10 +71,13 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod Name: name, Description: description, } - err := s.repo.UpdatePigHouse(house) + rowsAffected, err := s.repo.UpdatePigHouse(house) if err != nil { return nil, err } + if rowsAffected == 0 { + return nil, ErrHouseNotFound + } // 返回更新后的完整信息 return s.repo.GetPigHouseByID(id) } @@ -90,11 +93,14 @@ func (s *pigFarmService) DeletePigHouse(id uint) error { } // 调用仓库层进行删除 - err = s.repo.DeletePigHouse(id) - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrHouseNotFound // 或者直接返回 gorm.ErrRecordNotFound,取决于业务需求 + rowsAffected, err := s.repo.DeletePigHouse(id) + if err != nil { + return err } - return err + if rowsAffected == 0 { + return ErrHouseNotFound + } + return nil } // --- Pen Implementation --- @@ -144,10 +150,13 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa Capacity: capacity, Status: status, } - err = s.repo.UpdatePen(pen) + rowsAffected, err := s.repo.UpdatePen(pen) if err != nil { return nil, err } + if rowsAffected == 0 { + return nil, ErrPenNotFound + } // 返回更新后的完整信息 return s.repo.GetPenByID(id) } @@ -157,27 +166,31 @@ func (s *pigFarmService) DeletePen(id uint) error { pen, err := s.repo.GetPenByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return gorm.ErrRecordNotFound // 猪栏不存在 + return ErrPenNotFound // 猪栏不存在 } return err } // 检查猪栏是否关联了活跃批次 - if *pen.PigBatchID != 0 { + // 注意:pen.PigBatchID 是指针类型,需要检查是否为 nil + if pen.PigBatchID != nil && *pen.PigBatchID != 0 { pigBatch, err := s.repo.GetPigBatchByID(*pen.PigBatchID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 如果批次活跃,则不能删除猪栏 - if pigBatch.IsActive() { + if pigBatch != nil && pigBatch.IsActive() { return ErrPenInUse } } // 调用仓库层进行删除 - err = s.repo.DeletePen(id) - if errors.Is(err, gorm.ErrRecordNotFound) { - return gorm.ErrRecordNotFound // 猪栏不存在 + rowsAffected, err := s.repo.DeletePen(id) + if err != nil { + return err } - return err + if rowsAffected == 0 { + return ErrPenNotFound + } + return nil } diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index eb3bdf3..13e49bb 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -1,8 +1,6 @@ package repository import ( - "errors" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) @@ -11,8 +9,10 @@ import ( type PigBatchRepository interface { CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) GetPigBatchByID(id uint) (*models.PigBatch, error) - UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) - DeletePigBatch(id uint) error + // UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误 + UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) + // DeletePigBatch 根据ID删除一个猪批次,返回受影响的行数和错误 + DeletePigBatch(id uint) (int64, error) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) } @@ -44,27 +44,23 @@ func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, err } // UpdatePigBatch 更新一个猪批次 -func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { +func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) { result := r.db.Model(&models.PigBatch{}).Where("id = ?", batch.ID).Updates(batch) if result.Error != nil { - return nil, result.Error + return nil, 0, result.Error } - if result.RowsAffected == 0 { - return nil, errors.New("未找到要更新的猪批次或数据未改变") // 明确返回错误,而不是 gorm.ErrRecordNotFound - } - return batch, nil + // 返回更新后的批次、受影响的行数和错误 + return batch, result.RowsAffected, nil } // DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除) -func (r *gormPigBatchRepository) DeletePigBatch(id uint) error { +func (r *gormPigBatchRepository) DeletePigBatch(id uint) (int64, error) { result := r.db.Delete(&models.PigBatch{}, id) if result.Error != nil { - return result.Error + return 0, result.Error } - if result.RowsAffected == 0 { - return errors.New("未找到要删除的猪批次") // 明确返回错误 - } - return nil + // 返回受影响的行数和错误 + return result.RowsAffected, nil } // ListPigBatches 批量查询猪批次,支持根据 IsActive 筛选 diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index 24b17c5..47edb2a 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -11,8 +11,10 @@ type PigFarmRepository interface { CreatePigHouse(house *models.PigHouse) error GetPigHouseByID(id uint) (*models.PigHouse, error) ListPigHouses() ([]models.PigHouse, error) - UpdatePigHouse(house *models.PigHouse) error - DeletePigHouse(id uint) error + // UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误 + UpdatePigHouse(house *models.PigHouse) (int64, error) + // DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误 + DeletePigHouse(id uint) (int64, error) CountPensInHouse(houseID uint) (int64, error) // Pen methods @@ -22,8 +24,10 @@ type PigFarmRepository interface { // GetPenByIDTx 根据ID获取单个猪栏 (事务性) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) ListPens() ([]models.Pen, error) - UpdatePen(pen *models.Pen) error - DeletePen(id uint) error + // UpdatePen 更新一个猪栏,返回受影响的行数和错误 + UpdatePen(pen *models.Pen) (int64, error) + // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 + DeletePen(id uint) (int64, error) // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) // UpdatePenFields 更新猪栏的指定字段 (事务性) @@ -71,28 +75,22 @@ func (r *gormPigFarmRepository) ListPigHouses() ([]models.PigHouse, error) { return houses, nil } -// UpdatePigHouse 更新一个猪舍 -func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) error { +// UpdatePigHouse 更新一个猪舍,返回受影响的行数和错误 +func (r *gormPigFarmRepository) UpdatePigHouse(house *models.PigHouse) (int64, error) { result := r.db.Model(&models.PigHouse{}).Where("id = ?", house.ID).Updates(house) if result.Error != nil { - return result.Error + return 0, result.Error } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound // 未找到要更新的猪舍或数据未改变 - } - return nil + return result.RowsAffected, nil } -// DeletePigHouse 根据ID删除一个猪舍 -func (r *gormPigFarmRepository) DeletePigHouse(id uint) error { +// DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误 +func (r *gormPigFarmRepository) DeletePigHouse(id uint) (int64, error) { result := r.db.Delete(&models.PigHouse{}, id) if result.Error != nil { - return result.Error + return 0, result.Error } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound // 未找到要删除的猪舍 - } - return nil + return result.RowsAffected, nil } // CountPensInHouse 统计猪舍中的猪栏数量 @@ -136,28 +134,22 @@ func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { return pens, nil } -// UpdatePen 更新一个猪栏 -func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) error { +// UpdatePen 更新一个猪栏,返回受影响的行数和错误 +func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) (int64, error) { result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) if result.Error != nil { - return result.Error + return 0, result.Error } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound // 未找到要更新的猪栏或数据未改变 - } - return nil + return result.RowsAffected, nil } -// DeletePen 根据ID删除一个猪栏 -func (r *gormPigFarmRepository) DeletePen(id uint) error { +// DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 +func (r *gormPigFarmRepository) DeletePen(id uint) (int64, error) { result := r.db.Delete(&models.Pen{}, id) if result.Error != nil { - return result.Error + return 0, result.Error } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound // 未找到要删除的猪栏 - } - return nil + return result.RowsAffected, nil } // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) From 5403be5e7da3f783927e2a5dbc49820e911f36cb Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 01:02:40 +0800 Subject: [PATCH 22/65] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 2 +- internal/infra/repository/unit_of_work.go | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/core/application.go b/internal/core/application.go index 80c2764..ce85f66 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -75,7 +75,7 @@ func NewApplication(configPath string) (*Application, error) { pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) // 初始化事务管理器 - unitOfWork := repository.NewGormUnitOfWork(storage.GetDB()) + unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) diff --git a/internal/infra/repository/unit_of_work.go b/internal/infra/repository/unit_of_work.go index 0d45573..7929476 100644 --- a/internal/infra/repository/unit_of_work.go +++ b/internal/infra/repository/unit_of_work.go @@ -3,6 +3,7 @@ package repository import ( "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "gorm.io/gorm" ) @@ -16,30 +17,30 @@ type UnitOfWork interface { // gormUnitOfWork 是 UnitOfWork 接口的 GORM 实现 type gormUnitOfWork struct { - db *gorm.DB + db *gorm.DB + logger *logs.Logger // 添加日志记录器 } // NewGormUnitOfWork 创建一个新的 gormUnitOfWork 实例 -func NewGormUnitOfWork(db *gorm.DB) UnitOfWork { - return &gormUnitOfWork{db: db} +func NewGormUnitOfWork(db *gorm.DB, logger *logs.Logger) UnitOfWork { + return &gormUnitOfWork{db: db, logger: logger} } // ExecuteInTransaction 实现了 UnitOfWork 接口的事务执行逻辑 func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error { tx := u.db.Begin() if tx.Error != nil { + u.logger.Errorf("开启数据库事务失败: %v", tx.Error) // 记录错误日志 return fmt.Errorf("开启事务失败: %w", tx.Error) } defer func() { if r := recover(); r != nil { tx.Rollback() - // 可以选择在此处记录 panic 日志 - // u.logger.Errorf("事务中发生 panic,已回滚: %v", r) + u.logger.Errorf("事务中发生 panic,已回滚: %v", r) // 记录 panic 日志 } else if tx.Error != nil { // 如果函数执行过程中返回错误,或者事务本身有错误,则回滚 tx.Rollback() - // 可以选择在此处记录错误日志 - // u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) + u.logger.Errorf("事务执行失败,已回滚: %v", tx.Error) // 记录错误日志 } }() @@ -51,6 +52,7 @@ func (u *gormUnitOfWork) ExecuteInTransaction(fn func(tx *gorm.DB) error) error // 提交事务 if err := tx.Commit().Error; err != nil { + u.logger.Errorf("提交数据库事务失败: %v", err) // 记录错误日志 return fmt.Errorf("提交事务失败: %w", err) } From 8d9e4286b0197cf4c113f7f8551090409ecf54aa Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 01:28:05 +0800 Subject: [PATCH 23/65] =?UTF-8?q?=E5=8D=95=E7=8B=AC=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=8C=AA=E5=9C=88=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/api.go | 1 + .../management/pig_farm_controller.go | 78 +++++++++++++++++-- internal/app/dto/pig_farm_dto.go | 14 ++-- internal/app/service/pig_batch_service.go | 1 - internal/app/service/pig_farm_service.go | 70 ++++++++++++++++- 5 files changed, 147 insertions(+), 17 deletions(-) diff --git a/internal/app/api/api.go b/internal/app/api/api.go index fc70039..3533ecc 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -222,6 +222,7 @@ func (a *API) setupRoutes() { penGroup.GET("/:id", a.pigFarmController.GetPen) penGroup.PUT("/:id", a.pigFarmController.UpdatePen) penGroup.DELETE("/:id", a.pigFarmController.DeletePen) + penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) } a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/management/pig_farm_controller.go b/internal/app/controller/management/pig_farm_controller.go index fc0defa..84a6a05 100644 --- a/internal/app/controller/management/pig_farm_controller.go +++ b/internal/app/controller/management/pig_farm_controller.go @@ -9,7 +9,6 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/service" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "github.com/gin-gonic/gin" - "gorm.io/gorm" ) // --- 控制器定义 --- @@ -80,7 +79,7 @@ func (c *PigFarmController) GetPigHouse(ctx *gin.Context) { house, err := c.service.GetPigHouseByID(uint(id)) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrHouseNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) return } @@ -151,7 +150,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx *gin.Context) { house, err := c.service.UpdatePigHouse(uint(id), req.Name, req.Description) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrHouseNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) return } @@ -185,10 +184,15 @@ func (c *PigFarmController) DeletePigHouse(ctx *gin.Context) { } if err := c.service.DeletePigHouse(uint(id)); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrHouseNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪舍不存在", action, "猪舍不存在", id) return } + // 检查是否是业务逻辑错误 + if errors.Is(err, service.ErrHouseContainsPens) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id) + return + } c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id) return @@ -218,6 +222,11 @@ func (c *PigFarmController) CreatePen(ctx *gin.Context) { pen, err := c.service.CreatePen(req.PenNumber, req.HouseID, req.Capacity) if err != nil { + // 检查是否是业务逻辑错误 + if errors.Is(err, service.ErrHouseNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), req) + return + } c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req) return @@ -252,7 +261,7 @@ func (c *PigFarmController) GetPen(ctx *gin.Context) { pen, err := c.service.GetPenByID(uint(id)) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrPenNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) return } @@ -329,10 +338,11 @@ func (c *PigFarmController) UpdatePen(ctx *gin.Context) { pen, err := c.service.UpdatePen(uint(id), req.PenNumber, req.HouseID, req.Capacity, req.Status) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrPenNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) return } + // 其他业务逻辑错误可以在这里添加处理 c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) return @@ -366,10 +376,15 @@ func (c *PigFarmController) DeletePen(ctx *gin.Context) { } if err := c.service.DeletePen(uint(id)); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if errors.Is(err, service.ErrPenNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪栏不存在", action, "猪栏不存在", id) return } + // 检查是否是业务逻辑错误 + if errors.Is(err, service.ErrPenInUse) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id) + return + } c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除失败", action, "业务逻辑失败", id) return @@ -377,3 +392,52 @@ func (c *PigFarmController) DeletePen(ctx *gin.Context) { controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id) } + +// UpdatePenStatus godoc +// @Summary 更新猪栏状态 +// @Description 更新指定猪栏的当前状态 +// @Tags 猪场管理 +// @Accept json +// @Produce json +// @Param id path int true "猪栏ID" +// @Param body body dto.UpdatePenStatusRequest true "新的猪栏状态" +// @Success 200 {object} controller.Response{data=dto.PenResponse} "更新成功" +// @Router /api/v1/pens/{id}/status [put] +func (c *PigFarmController) UpdatePenStatus(ctx *gin.Context) { + const action = "更新猪栏状态" + id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.UpdatePenStatusRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + pen, err := c.service.UpdatePenStatus(uint(id), req.Status) + if err != nil { + if errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id) + return + } else if errors.Is(err, service.ErrPenStatusInvalidForOccupiedPen) || errors.Is(err, service.ErrPenStatusInvalidForUnoccupiedPen) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id) + return + } + + resp := dto.PenResponse{ + ID: pen.ID, + PenNumber: pen.PenNumber, + HouseID: pen.HouseID, + Capacity: pen.Capacity, + Status: pen.Status, + PigBatchID: *pen.PigBatchID, + } + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) +} diff --git a/internal/app/dto/pig_farm_dto.go b/internal/app/dto/pig_farm_dto.go index 2b5b29b..b10e274 100644 --- a/internal/app/dto/pig_farm_dto.go +++ b/internal/app/dto/pig_farm_dto.go @@ -33,10 +33,9 @@ type UpdatePigHouseRequest struct { // CreatePenRequest 定义了创建猪栏的请求结构 type CreatePenRequest struct { - PenNumber string `json:"pen_number" binding:"required"` - HouseID uint `json:"house_id" binding:"required"` - Capacity int `json:"capacity" binding:"required"` - Status models.PenStatus `json:"status" binding:"required"` + PenNumber string `json:"pen_number" binding:"required"` + HouseID uint `json:"house_id" binding:"required"` + Capacity int `json:"capacity" binding:"required"` } // UpdatePenRequest 定义了更新猪栏的请求结构 @@ -44,5 +43,10 @@ type UpdatePenRequest struct { PenNumber string `json:"pen_number" binding:"required"` HouseID uint `json:"house_id" binding:"required"` Capacity int `json:"capacity" binding:"required"` - Status models.PenStatus `json:"status" binding:"required"` + Status models.PenStatus `json:"status" binding:"required,oneof=空闲 占用 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验 +} + +// UpdatePenStatusRequest 定义了更新猪栏状态的请求结构 +type UpdatePenStatusRequest struct { + Status models.PenStatus `json:"status" binding:"required,oneof=空闲 占用 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"` } diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 5d94f77..3879c0d 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -16,7 +16,6 @@ var ( ErrPigBatchNotFound = errors.New("指定的猪批次不存在") ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") - ErrPenNotFound = errors.New("指定的猪栏不存在") ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次占用") ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index f342520..d59347c 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "fmt" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" @@ -11,9 +12,12 @@ import ( ) var ( - ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍") - ErrHouseNotFound = errors.New("指定的猪舍不存在") - ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") + ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍") + ErrHouseNotFound = errors.New("指定的猪舍不存在") + ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") + ErrPenNotFound = errors.New("指定的猪栏不存在") + ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次占用,无法设置为非占用状态") + ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次占用,无法设置为占用状态") ) // PigFarmService 提供了猪场资产管理的业务逻辑 @@ -31,18 +35,22 @@ type PigFarmService interface { ListPens() ([]models.Pen, error) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) DeletePen(id uint) error + // UpdatePenStatus 更新猪栏状态 + UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) } type pigFarmService struct { logger *logs.Logger repo repository.PigFarmRepository + uow repository.UnitOfWork // 工作单元,用于事务管理 } // NewPigFarmService 创建一个新的 PigFarmService 实例 -func NewPigFarmService(repo repository.PigFarmRepository, logger *logs.Logger) PigFarmService { +func NewPigFarmService(repo repository.PigFarmRepository, uow repository.UnitOfWork, logger *logs.Logger) PigFarmService { return &pigFarmService{ logger: logger, repo: repo, + uow: uow, } } @@ -194,3 +202,57 @@ func (s *pigFarmService) DeletePen(id uint) error { } return nil } + +// UpdatePenStatus 更新猪栏状态 +func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) { + var updatedPen *models.Pen + err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + pen, err := s.repo.GetPenByIDTx(tx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + s.logger.Errorf("更新猪栏状态失败: 获取猪栏 %d 信息错误: %v", id, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", id, err) + } + + // 业务逻辑:根据猪栏的 PigBatchID 和当前状态,判断是否允许设置为 newStatus + if pen.PigBatchID != nil && *pen.PigBatchID != 0 { // 猪栏已被批次占用 + if newStatus == models.PenStatusEmpty { // 猪栏已被批次占用,不能直接设置为空闲 + return ErrPenStatusInvalidForOccupiedPen + } + } else { // 猪栏未被批次占用 (PigBatchID == nil) + if newStatus == models.PenStatusOccupied { // 猪栏未被批次占用,不能设置为占用 + return ErrPenStatusInvalidForUnoccupiedPen + } + } + + // 如果新状态与旧状态相同,则无需更新 + if pen.Status == newStatus { + updatedPen = pen // 返回原始猪栏,因为没有实际更新 + return nil + } + + updates := map[string]interface{}{ + "status": newStatus, + } + + if err := s.repo.UpdatePenFields(tx, id, updates); err != nil { + s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err) + return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err) + } + + // 获取更新后的猪栏信息 + updatedPen, err = s.repo.GetPenByIDTx(tx, id) + if err != nil { + s.logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err) + return fmt.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %w", id, err) + } + return nil + }) + + if err != nil { + return nil, err + } + return updatedPen, nil +} From 740e14e6cc713ee11dee7b46fdb150d1823bdd5e Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 4 Oct 2025 01:31:35 +0800 Subject: [PATCH 24/65] swag --- docs/docs.go | 102 ++++++++++++++++-- docs/swagger.json | 102 ++++++++++++++++-- docs/swagger.yaml | 66 ++++++++++-- .../management/pig_batch_controller.go | 2 +- internal/app/dto/pig_farm_dto.go | 4 +- internal/app/service/pig_batch_service.go | 10 +- internal/app/service/pig_farm_service.go | 12 +-- internal/core/application.go | 2 +- internal/infra/models/farm_asset.go | 2 +- 9 files changed, 264 insertions(+), 38 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 82ab941..12ddeb1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -807,6 +807,59 @@ const docTemplate = `{ } } }, + "/api/v1/pens/{id}/status": { + "put": { + "description": "更新指定猪栏的当前状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏状态", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的猪栏状态", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/pig-batches": { "get": { "description": "获取所有猪批次的列表,支持按活跃状态筛选", @@ -1136,7 +1189,7 @@ const docTemplate = `{ } }, "409": { - "description": "业务逻辑冲突 (如猪栏已被占用)", + "description": "业务逻辑冲突 (如猪栏已被使用)", "schema": { "$ref": "#/definitions/controller.Response" } @@ -1937,8 +1990,7 @@ const docTemplate = `{ "required": [ "capacity", "house_id", - "pen_number", - "status" + "pen_number" ], "properties": { "capacity": { @@ -1949,9 +2001,6 @@ const docTemplate = `{ }, "pen_number": { "type": "string" - }, - "status": { - "$ref": "#/definitions/models.PenStatus" } } }, @@ -2616,7 +2665,44 @@ const docTemplate = `{ "type": "string" }, "status": { - "$ref": "#/definitions/models.PenStatus" + "description": "添加oneof校验", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ] + } + } + }, + "dto.UpdatePenStatusRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ], + "example": "病猪栏" } } }, @@ -2693,7 +2779,7 @@ const docTemplate = `{ "type": "string", "enum": [ "空闲", - "占用", + "使用中", "病猪栏", "康复栏", "清洗消毒", diff --git a/docs/swagger.json b/docs/swagger.json index 44ea4f2..349588d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -796,6 +796,59 @@ } } }, + "/api/v1/pens/{id}/status": { + "put": { + "description": "更新指定猪栏的当前状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪场管理" + ], + "summary": "更新猪栏状态", + "parameters": [ + { + "type": "integer", + "description": "猪栏ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新的猪栏状态", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdatePenStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.PenResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/pig-batches": { "get": { "description": "获取所有猪批次的列表,支持按活跃状态筛选", @@ -1125,7 +1178,7 @@ } }, "409": { - "description": "业务逻辑冲突 (如猪栏已被占用)", + "description": "业务逻辑冲突 (如猪栏已被使用)", "schema": { "$ref": "#/definitions/controller.Response" } @@ -1926,8 +1979,7 @@ "required": [ "capacity", "house_id", - "pen_number", - "status" + "pen_number" ], "properties": { "capacity": { @@ -1938,9 +1990,6 @@ }, "pen_number": { "type": "string" - }, - "status": { - "$ref": "#/definitions/models.PenStatus" } } }, @@ -2605,7 +2654,44 @@ "type": "string" }, "status": { - "$ref": "#/definitions/models.PenStatus" + "description": "添加oneof校验", + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ] + } + } + }, + "dto.UpdatePenStatusRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "enum": [ + "空闲", + "使用中", + "病猪栏", + "康复栏", + "清洗消毒", + "维修中" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PenStatus" + } + ], + "example": "病猪栏" } } }, @@ -2682,7 +2768,7 @@ "type": "string", "enum": [ "空闲", - "占用", + "使用中", "病猪栏", "康复栏", "清洗消毒", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0292e48..b74f124 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -132,13 +132,10 @@ definitions: type: integer pen_number: type: string - status: - $ref: '#/definitions/models.PenStatus' required: - capacity - house_id - pen_number - - status type: object dto.CreatePigHouseRequest: properties: @@ -578,13 +575,38 @@ definitions: pen_number: type: string status: - $ref: '#/definitions/models.PenStatus' + allOf: + - $ref: '#/definitions/models.PenStatus' + description: 添加oneof校验 + enum: + - 空闲 + - 使用中 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 required: - capacity - house_id - pen_number - status type: object + dto.UpdatePenStatusRequest: + properties: + status: + allOf: + - $ref: '#/definitions/models.PenStatus' + enum: + - 空闲 + - 使用中 + - 病猪栏 + - 康复栏 + - 清洗消毒 + - 维修中 + example: 病猪栏 + required: + - status + type: object dto.UpdatePigHouseRequest: properties: description: @@ -634,7 +656,7 @@ definitions: models.PenStatus: enum: - 空闲 - - 占用 + - 使用中 - 病猪栏 - 康复栏 - 清洗消毒 @@ -1268,6 +1290,38 @@ paths: summary: 更新猪栏 tags: - 猪场管理 + /api/v1/pens/{id}/status: + put: + consumes: + - application/json + description: 更新指定猪栏的当前状态 + parameters: + - description: 猪栏ID + in: path + name: id + required: true + type: integer + - description: 新的猪栏状态 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdatePenStatusRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.PenResponse' + type: object + summary: 更新猪栏状态 + tags: + - 猪场管理 /api/v1/pig-batches: get: description: 获取所有猪批次的列表,支持按活跃状态筛选 @@ -1473,7 +1527,7 @@ paths: schema: $ref: '#/definitions/controller.Response' "409": - description: 业务逻辑冲突 (如猪栏已被占用) + description: 业务逻辑冲突 (如猪栏已被使用) schema: $ref: '#/definitions/controller.Response' "500": diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 5965c49..46199b6 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -200,7 +200,7 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { // @Success 200 {object} controller.Response "更新成功" // @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" // @Failure 404 {object} controller.Response "猪批次或猪栏不存在" -// @Failure 409 {object} controller.Response "业务逻辑冲突 (如猪栏已被占用)" +// @Failure 409 {object} controller.Response "业务逻辑冲突 (如猪栏已被使用)" // @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches/{id}/pens [put] func (c *PigBatchController) UpdatePigBatchPens(ctx *gin.Context) { diff --git a/internal/app/dto/pig_farm_dto.go b/internal/app/dto/pig_farm_dto.go index b10e274..09f450a 100644 --- a/internal/app/dto/pig_farm_dto.go +++ b/internal/app/dto/pig_farm_dto.go @@ -43,10 +43,10 @@ type UpdatePenRequest struct { PenNumber string `json:"pen_number" binding:"required"` HouseID uint `json:"house_id" binding:"required"` Capacity int `json:"capacity" binding:"required"` - Status models.PenStatus `json:"status" binding:"required,oneof=空闲 占用 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验 + Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中"` // 添加oneof校验 } // UpdatePenStatusRequest 定义了更新猪栏状态的请求结构 type UpdatePenStatusRequest struct { - Status models.PenStatus `json:"status" binding:"required,oneof=空闲 占用 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"` + Status models.PenStatus `json:"status" binding:"required,oneof=空闲 使用中 病猪栏 康复栏 清洗消毒 维修中" example:"病猪栏"` } diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 3879c0d..c6c45d3 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -16,7 +16,7 @@ var ( ErrPigBatchNotFound = errors.New("指定的猪批次不存在") ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") - ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次占用") + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") ) @@ -257,7 +257,7 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) updates := make(map[string]interface{}) updates["pig_batch_id"] = nil // 总是将 PigBatchID 设为 nil - // 只有当猪栏当前状态是“占用”时,才将其状态改回“空闲” + // 只有当猪栏当前状态是“使用中”时,才将其状态改回“空闲” if currentPen.Status == models.PenStatusOccupied { updates["status"] = models.PenStatusEmpty } @@ -279,17 +279,17 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) } - // 验证:猪栏必须是“空闲”状态且未被任何批次占用,才能被分配 + // 验证:猪栏必须是“空闲”状态且未被任何批次使用,才能被分配 if actualPen.Status != models.PenStatusEmpty { return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) } if actualPen.PigBatchID != nil { - return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) + return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用,无法分配: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) } updates := map[string]interface{}{ "pig_batch_id": &batchID, // 将 PigBatchID 设为当前批次ID的指针 - "status": models.PenStatusOccupied, // 分配后,状态变为“占用” + "status": models.PenStatusOccupied, // 分配后,状态变为“使用中” } if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { s.logger.Errorf("更新猪批次猪栏失败: 添加猪栏 %d 失败: %v", penID, err) diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index d59347c..e4166c2 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -16,8 +16,8 @@ var ( ErrHouseNotFound = errors.New("指定的猪舍不存在") ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") ErrPenNotFound = errors.New("指定的猪栏不存在") - ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次占用,无法设置为非占用状态") - ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次占用,无法设置为占用状态") + ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次使用,无法设置为非使用中状态") + ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次使用,无法设置为使用中状态") ) // PigFarmService 提供了猪场资产管理的业务逻辑 @@ -217,12 +217,12 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (* } // 业务逻辑:根据猪栏的 PigBatchID 和当前状态,判断是否允许设置为 newStatus - if pen.PigBatchID != nil && *pen.PigBatchID != 0 { // 猪栏已被批次占用 - if newStatus == models.PenStatusEmpty { // 猪栏已被批次占用,不能直接设置为空闲 + if pen.PigBatchID != nil && *pen.PigBatchID != 0 { // 猪栏已被批次使用 + if newStatus == models.PenStatusEmpty { // 猪栏已被批次使用,不能直接设置为空闲 return ErrPenStatusInvalidForOccupiedPen } - } else { // 猪栏未被批次占用 (PigBatchID == nil) - if newStatus == models.PenStatusOccupied { // 猪栏未被批次占用,不能设置为占用 + } else { // 猪栏未被批次使用 (PigBatchID == nil) + if newStatus == models.PenStatusOccupied { // 猪栏未被批次使用,不能设置为使用中 return ErrPenStatusInvalidForUnoccupiedPen } } diff --git a/internal/core/application.go b/internal/core/application.go index ce85f66..ef9b7bc 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -78,7 +78,7 @@ func NewApplication(configPath string) (*Application, error) { unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // --- 业务逻辑处理器初始化 --- - pigFarmService := service.NewPigFarmService(pigFarmRepo, logger) + pigFarmService := service.NewPigFarmService(pigFarmRepo, unitOfWork, logger) pigBatchService := service.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork, logger) // 初始化审计服务 diff --git a/internal/infra/models/farm_asset.go b/internal/infra/models/farm_asset.go index 28e7c05..7d5731c 100644 --- a/internal/infra/models/farm_asset.go +++ b/internal/infra/models/farm_asset.go @@ -21,7 +21,7 @@ type PenStatus string const ( PenStatusEmpty PenStatus = "空闲" - PenStatusOccupied PenStatus = "占用" + PenStatusOccupied PenStatus = "使用中" PenStatusSickPen PenStatus = "病猪栏" PenStatusRecovering PenStatus = "康复栏" PenStatusCleaning PenStatus = "清洗消毒" From 6d080d250d0fe23519324b460ff9056a93e732fd Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 16:37:12 +0800 Subject: [PATCH 25/65] =?UTF-8?q?=E7=8C=AA=E7=BE=A4=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=81=9A=E5=90=88=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_batch_service.go | 228 ++++------------------ internal/core/application.go | 6 +- internal/domain/pig/pig_batch.go | 50 +++++ internal/domain/pig/pig_batch_service.go | 192 ++++++++++++++++++ 4 files changed, 290 insertions(+), 186 deletions(-) create mode 100644 internal/domain/pig/pig_batch.go create mode 100644 internal/domain/pig/pig_batch_service.go diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index c6c45d3..236fb29 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -1,55 +1,37 @@ package service import ( - "errors" - "fmt" - "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" + domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" "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 ( - ErrPigBatchNotFound = errors.New("指定的猪批次不存在") - ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") - ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") - ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") - ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") - ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") -) - -// PigBatchService 提供了猪批次管理的业务逻辑 +// PigBatchService 接口定义保持不变,继续作为应用层对外的契约。 type PigBatchService interface { CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) - // UpdatePigBatchPens 更新猪批次关联的猪栏 UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error } +// pigBatchService 的实现现在依赖于领域服务接口。 type pigBatchService struct { - logger *logs.Logger - pigBatchRepo repository.PigBatchRepository // 猪批次仓库 - pigFarmRepo repository.PigFarmRepository // 猪场资产仓库 (包含猪栏操作) - uow repository.UnitOfWork // 工作单元,用于事务管理 + logger *logs.Logger + domainService domain_pig.PigBatchService // 依赖注入领域服务 } -// NewPigBatchService 创建一个新的 PigBatchService 实例 -func NewPigBatchService(pigBatchRepo repository.PigBatchRepository, pigFarmRepo repository.PigFarmRepository, uow repository.UnitOfWork, logger *logs.Logger) PigBatchService { +// NewPigBatchService 构造函数被修改,以注入领域服务。 +func NewPigBatchService(domainService domain_pig.PigBatchService, logger *logs.Logger) PigBatchService { return &pigBatchService{ - logger: logger, - pigBatchRepo: pigBatchRepo, - pigFarmRepo: pigFarmRepo, - uow: uow, + logger: logger, + domainService: domainService, } } -// toPigBatchResponseDTO 将 models.PigBatch 转换为 dto.PigBatchResponseDTO +// toPigBatchResponseDTO 负责将领域模型转换为应用层DTO,这个职责保留在应用层。 func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.PigBatchResponseDTO { if batch == nil { return nil @@ -62,14 +44,15 @@ func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.Pig EndDate: batch.EndDate, InitialCount: batch.InitialCount, Status: batch.Status, - IsActive: batch.IsActive(), // 使用模型自带的 IsActive 方法 + IsActive: batch.IsActive(), CreateTime: batch.CreatedAt, UpdateTime: batch.UpdatedAt, } } -// CreatePigBatch 处理创建猪批次的业务逻辑 +// CreatePigBatch 现在将请求委托给领域服务处理。 func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) { + // 1. DTO -> 领域模型 batch := &models.PigBatch{ BatchNumber: dto.BatchNumber, OriginType: dto.OriginType, @@ -78,41 +61,38 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa Status: dto.Status, } - createdBatch, err := s.pigBatchRepo.CreatePigBatch(batch) + // 2. 调用领域服务 + createdBatch, err := s.domainService.CreatePigBatch(batch) if err != nil { - s.logger.Errorf("创建猪批次失败: %v", err) - return nil, err + s.logger.Errorf("应用层: 创建猪批次失败: %v", err) + return nil, err // 将领域层的错误传递上去 } + // 3. 领域模型 -> DTO return s.toPigBatchResponseDTO(createdBatch), nil } -// GetPigBatch 处理获取单个猪批次的业务逻辑 +// GetPigBatch 从领域服务获取数据并转换为DTO。 func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) { - batch, err := s.pigBatchRepo.GetPigBatchByID(id) + batch, err := s.domainService.GetPigBatch(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrPigBatchNotFound - } - s.logger.Errorf("获取猪批次失败,ID: %d, 错误: %v", id, err) + s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err) return nil, err } return s.toPigBatchResponseDTO(batch), nil } -// UpdatePigBatch 处理更新猪批次的业务逻辑 +// UpdatePigBatch 协调获取、更新和保存的流程。 func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { - existingBatch, err := s.pigBatchRepo.GetPigBatchByID(id) + // 1. 先获取最新的领域模型 + existingBatch, err := s.domainService.GetPigBatch(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrPigBatchNotFound - } - s.logger.Errorf("更新猪批次失败,获取原批次信息错误,ID: %d, 错误: %v", id, err) + s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err) return nil, err } - // 根据 DTO 中的非空字段更新模型 + // 2. 将DTO中的变更应用到模型上 if dto.BatchNumber != nil { existingBatch.BatchNumber = *dto.BatchNumber } @@ -132,55 +112,32 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* existingBatch.Status = *dto.Status } - updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(existingBatch) + // 3. 调用领域服务执行更新 + updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch) if err != nil { - s.logger.Errorf("更新猪批次失败,ID: %d, 错误: %v", id, err) + s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err) return nil, err } - // 如果没有行受影响,则认为猪批次不存在 - if rowsAffected == 0 { - return nil, ErrPigBatchNotFound - } + // 4. 转换并返回结果 return s.toPigBatchResponseDTO(updatedBatch), nil } -// DeletePigBatch 处理删除猪批次的业务逻辑 +// DeletePigBatch 将删除操作委托给领域服务。 func (s *pigBatchService) DeletePigBatch(id uint) error { - // 1. 获取猪批次信息 - batch, err := s.pigBatchRepo.GetPigBatchByID(id) + err := s.domainService.DeletePigBatch(id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound - } - s.logger.Errorf("删除猪批次失败,获取批次信息错误,ID: %d, 错误: %v", id, err) + s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err) return err } - - // 2. 检查猪批次是否活跃 - if batch.IsActive() { - return ErrPigBatchActive // 如果活跃,则不允许删除 - } - - // 3. 执行删除操作 - rowsAffected, err := s.pigBatchRepo.DeletePigBatch(id) - if err != nil { - s.logger.Errorf("删除猪批次失败,ID: %d, 错误: %v", id, err) - return err - } - // 如果没有行受影响,则认为猪批次不存在 - if rowsAffected == 0 { - return ErrPigBatchNotFound - } - return nil } -// ListPigBatches 处理批量查询猪批次的业务逻辑 +// ListPigBatches 从领域服务获取列表并进行转换。 func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) { - batches, err := s.pigBatchRepo.ListPigBatches(isActive) + batches, err := s.domainService.ListPigBatches(isActive) if err != nil { - s.logger.Errorf("批量查询猪批次失败,错误: %v", err) + s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err) return nil, err } @@ -192,111 +149,12 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons return responseDTOs, nil } -// UpdatePigBatchPens 更新猪批次关联的猪栏 +// UpdatePigBatchPens 将关联猪栏的复杂操作委托给领域服务。 func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { - // 使用工作单元执行事务 - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 验证猪批次 - pigBatch, err := s.pigFarmRepo.GetPigBatchByIDTx(tx, batchID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound - } - s.logger.Errorf("更新猪批次猪栏失败: 获取猪批次信息错误,ID: %d, 错误: %v", batchID, err) - return fmt.Errorf("获取猪批次信息失败: %w", err) - } - - if !pigBatch.IsActive() { - return ErrPigBatchNotActive - } - - // 2. 获取当前关联的猪栏 - currentPens, err := s.pigFarmRepo.GetPensByBatchID(tx, batchID) - if err != nil { - s.logger.Errorf("更新猪批次猪栏失败: 获取当前关联猪栏错误,批次ID: %d, 错误: %v", batchID, err) - return fmt.Errorf("获取当前关联猪栏失败: %w", err) - } - - currentPenMap := make(map[uint]models.Pen) - currentPenIDsSet := make(map[uint]struct{}) - for _, pen := range currentPens { - currentPenMap[pen.ID] = pen - currentPenIDsSet[pen.ID] = struct{}{} // 用于快速查找 - } - - // 3. 构建期望猪栏集合 - desiredPenIDsSet := make(map[uint]struct{}) - for _, penID := range desiredPenIDs { - desiredPenIDsSet[penID] = struct{}{} // 用于快速查找 - } - - // 4. 计算需要添加和移除的猪栏 - var pensToRemove []uint - for penID := range currentPenIDsSet { - if _, found := desiredPenIDsSet[penID]; !found { - pensToRemove = append(pensToRemove, penID) - } - } - - var pensToAdd []uint - for _, penID := range desiredPenIDs { - if _, found := currentPenIDsSet[penID]; !found { - pensToAdd = append(pensToAdd, penID) - } - } - - // 5. 处理移除猪栏 - for _, penID := range pensToRemove { - currentPen := currentPenMap[penID] - // 验证:确保猪栏确实与当前批次关联 - if currentPen.PigBatchID == nil || *currentPen.PigBatchID != batchID { - s.logger.Warnf("尝试移除未与批次 %d 关联的猪栏 %d", batchID, penID) - return fmt.Errorf("猪栏 %d 未与该批次关联,无法移除", penID) - } - - updates := make(map[string]interface{}) - updates["pig_batch_id"] = nil // 总是将 PigBatchID 设为 nil - - // 只有当猪栏当前状态是“使用中”时,才将其状态改回“空闲” - if currentPen.Status == models.PenStatusOccupied { - updates["status"] = models.PenStatusEmpty - } - - if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { - s.logger.Errorf("更新猪批次猪栏失败: 移除猪栏 %d 失败: %v", penID, err) - return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) - } - } - - // 6. 处理添加猪栏 - for _, penID := range pensToAdd { - actualPen, err := s.pigFarmRepo.GetPenByIDTx(tx, penID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) - } - s.logger.Errorf("更新猪批次猪栏失败: 获取猪栏 %d 信息错误: %v", penID, err) - return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) - } - - // 验证:猪栏必须是“空闲”状态且未被任何批次使用,才能被分配 - if actualPen.Status != models.PenStatusEmpty { - return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) - } - if actualPen.PigBatchID != nil { - return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用,无法分配: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) - } - - updates := map[string]interface{}{ - "pig_batch_id": &batchID, // 将 PigBatchID 设为当前批次ID的指针 - "status": models.PenStatusOccupied, // 分配后,状态变为“使用中” - } - if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { - s.logger.Errorf("更新猪批次猪栏失败: 添加猪栏 %d 失败: %v", penID, err) - return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) - } - } - - return nil - }) + err := s.domainService.UpdatePigBatchPens(batchID, desiredPenIDs) + if err != nil { + s.logger.Errorf("应用层: 更新猪批次猪栏关联失败, 批次ID: %d, 错误: %v", batchID, err) + return err + } + return nil } diff --git a/internal/core/application.go b/internal/core/application.go index ef9b7bc..ff7fbbe 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -12,6 +12,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/app/webhook" "git.huangwc.com/pig/pig-farm-controller/internal/domain/audit" "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" + "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" "git.huangwc.com/pig/pig-farm-controller/internal/domain/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" @@ -77,9 +78,12 @@ func NewApplication(configPath string) (*Application, error) { // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) + // 初始化猪群管理服务 + pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork) + // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, unitOfWork, logger) - pigBatchService := service.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork, logger) + pigBatchService := service.NewPigBatchService(pigBatchDomain, logger) // 初始化审计服务 auditService := audit.NewService(userActionLogRepo, logger) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go new file mode 100644 index 0000000..ee86e60 --- /dev/null +++ b/internal/domain/pig/pig_batch.go @@ -0,0 +1,50 @@ +package pig + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// --- 业务错误定义 --- + +var ( + // ErrPigBatchNotFound 表示当尝试访问一个不存在的猪批次时发生的错误。 + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + // ErrPigBatchActive 表示当尝试对一个活跃的猪批次执行不允许的操作(如删除)时发生的错误。 + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") + // ErrPigBatchNotActive 表示当猪批次不处于活跃状态,但执行了需要其活跃的操作时发生的错误。 + ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") + // ErrPenOccupiedByOtherBatch 表示当尝试将一个已经被其他批次占用的猪栏分配给新批次时发生的错误。 + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") + // ErrPenStatusInvalidForAllocation 表示猪栏的当前状态(例如,'维修中')不允许被分配。 + ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") + // ErrPenNotFound 表示猪栏不存在 + ErrPenNotFound = errors.New("指定的猪栏不存在") +) + +// --- 领域服务接口 --- + +// PigBatchService 定义了猪批次管理的核心业务逻辑接口。 +// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 +type PigBatchService interface { + // CreatePigBatch 创建一个新的猪批次。 + CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + + // GetPigBatch 根据ID获取单个猪批次的详细信息。 + GetPigBatch(id uint) (*models.PigBatch, error) + + // UpdatePigBatch 更新一个已存在的猪批次信息。 + UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + + // DeletePigBatch 删除一个指定的猪批次。 + // 实现时需要包含业务规则校验,例如,活跃的批次不能被删除。 + DeletePigBatch(id uint) error + + // ListPigBatches 根据是否活跃的状态,列出所有符合条件的猪批次。 + ListPigBatches(isActive *bool) ([]*models.PigBatch, error) + + // UpdatePigBatchPens 负责原子性地更新一个猪批次所关联的所有猪栏。 + // 它会处理猪栏的添加、移除,并确保数据的一致性。 + UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error +} diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go new file mode 100644 index 0000000..7d85852 --- /dev/null +++ b/internal/domain/pig/pig_batch_service.go @@ -0,0 +1,192 @@ +package pig + +import ( + "errors" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "gorm.io/gorm" +) + +// --- 领域服务实现 --- + +// pigBatchService 是 PigBatchService 接口的具体实现。 +// 它封装了业务逻辑所需的所有依赖,如数据库仓库和工作单元。 +type pigBatchService struct { + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigFarmRepo repository.PigFarmRepository // 猪场资产仓库 (包含猪栏操作) + uow repository.UnitOfWork // 工作单元,用于管理事务 +} + +// NewPigBatchService 是 pigBatchService 的构造函数。 +// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 +func NewPigBatchService( + pigBatchRepo repository.PigBatchRepository, + pigFarmRepo repository.PigFarmRepository, + uow repository.UnitOfWork, +) PigBatchService { + return &pigBatchService{ + pigBatchRepo: pigBatchRepo, + pigFarmRepo: pigFarmRepo, + uow: uow, + } +} + +// CreatePigBatch 实现了创建猪批次的逻辑。 +func (s *pigBatchService) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { + // 业务规则可以在这里添加,例如检查批次号是否唯一等 + return s.pigBatchRepo.CreatePigBatch(batch) +} + +// GetPigBatch 实现了获取单个猪批次的逻辑。 +func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) { + batch, err := s.pigBatchRepo.GetPigBatchByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPigBatchNotFound + } + return nil, err + } + return batch, nil +} + +// UpdatePigBatch 实现了更新猪批次的逻辑。 +func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { + // 可以在这里添加更新前的业务校验 + updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch) + if err != nil { + return nil, err + } + if rowsAffected == 0 { + return nil, ErrPigBatchNotFound // 如果没有行被更新,可能意味着记录不存在 + } + return updatedBatch, nil +} + +// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。 +func (s *pigBatchService) DeletePigBatch(id uint) error { + // 1. 获取猪批次信息 + batch, err := s.GetPigBatch(id) // 复用 GetPigBatch 方法 + if err != nil { + return err // GetPigBatch 已经处理了 ErrRecordNotFound 的情况 + } + + // 2. 核心业务规则:检查猪批次是否为活跃状态 + if batch.IsActive() { + return ErrPigBatchActive // 如果活跃,则不允许删除 + } + + // 3. 执行删除 + rowsAffected, err := s.pigBatchRepo.DeletePigBatch(id) + if err != nil { + return err + } + if rowsAffected == 0 { + return ErrPigBatchNotFound + } + + return nil +} + +// ListPigBatches 实现了批量查询猪批次的逻辑。 +func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) { + return s.pigBatchRepo.ListPigBatches(isActive) +} + +// UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 +func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { + // 使用工作单元来确保操作的原子性 + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次是否存在且活跃 + pigBatch, err := s.pigFarmRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 获取当前关联的猪栏 + currentPens, err := s.pigFarmRepo.GetPensByBatchID(tx, batchID) + if err != nil { + return fmt.Errorf("获取当前关联猪栏失败: %w", err) + } + + currentPenMap := make(map[uint]models.Pen) + currentPenIDsSet := make(map[uint]struct{}) + for _, pen := range currentPens { + currentPenMap[pen.ID] = pen + currentPenIDsSet[pen.ID] = struct{}{} + } + + // 3. 构建期望猪栏ID集合 + desiredPenIDsSet := make(map[uint]struct{}) + for _, penID := range desiredPenIDs { + desiredPenIDsSet[penID] = struct{}{} + } + + // 4. 计算需要添加和移除的猪栏 + var pensToRemove []uint + for penID := range currentPenIDsSet { + if _, found := desiredPenIDsSet[penID]; !found { + pensToRemove = append(pensToRemove, penID) + } + } + + var pensToAdd []uint + for _, penID := range desiredPenIDs { + if _, found := currentPenIDsSet[penID]; !found { + pensToAdd = append(pensToAdd, penID) + } + } + + // 5. 处理移除猪栏的逻辑 + for _, penID := range pensToRemove { + currentPen := currentPenMap[penID] + updates := make(map[string]interface{}) + updates["pig_batch_id"] = nil + + if currentPen.Status == models.PenStatusOccupied { + updates["status"] = models.PenStatusEmpty + } + + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) + } + } + + // 6. 处理添加猪栏的逻辑 + for _, penID := range pensToAdd { + actualPen, err := s.pigFarmRepo.GetPenByIDTx(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 核心业务规则:校验猪栏是否可被分配 + if actualPen.Status != models.PenStatusEmpty { + return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) + } + if actualPen.PigBatchID != nil { + return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) + } + + updates := map[string]interface{}{ + "pig_batch_id": &batchID, + "status": models.PenStatusOccupied, + } + if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) + } + } + + return nil + }) +} From 01327eb8d232cb1eaeffcd162e3501e642831e54 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 17:30:39 +0800 Subject: [PATCH 26/65] =?UTF-8?q?=E7=8C=AA=E7=BE=A4=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=81=9A=E5=90=88=E6=9C=8D=E5=8A=A1=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=B0=83=E6=A0=8F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_batch_service.go | 22 +-- internal/app/service/pig_farm_service.go | 9 -- internal/app/service/pig_service.go | 48 ++++++ internal/core/application.go | 6 +- internal/domain/pig/pen_transfer_manager.go | 63 ++++++++ internal/domain/pig/pig_batch.go | 8 + internal/domain/pig/pig_batch_service.go | 152 ++++++++++++++++-- internal/infra/models/pig_transfer.go | 19 +++ internal/infra/repository/pen_repository.go | 46 ++++++ .../infra/repository/pig_batch_repository.go | 10 ++ 10 files changed, 350 insertions(+), 33 deletions(-) create mode 100644 internal/domain/pig/pen_transfer_manager.go create mode 100644 internal/infra/models/pig_transfer.go create mode 100644 internal/infra/repository/pen_repository.go diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 236fb29..d4a5149 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -65,31 +65,31 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa createdBatch, err := s.domainService.CreatePigBatch(batch) if err != nil { s.logger.Errorf("应用层: 创建猪批次失败: %v", err) - return nil, err // 将领域层的错误传递上去 + return nil, mapDomainError(err) } // 3. 领域模型 -> DTO return s.toPigBatchResponseDTO(createdBatch), nil } -// GetPigBatch 从领域服务获取数据并转换为DTO。 +// GetPigBatch 从领域服务获取数据并转换为DTO,同时处理错误转换。 func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) { batch, err := s.domainService.GetPigBatch(id) if err != nil { s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err) - return nil, err + return nil, mapDomainError(err) } return s.toPigBatchResponseDTO(batch), nil } -// UpdatePigBatch 协调获取、更新和保存的流程。 +// UpdatePigBatch 协调获取、更新和保存的流程,并处理错误转换。 func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { // 1. 先获取最新的领域模型 existingBatch, err := s.domainService.GetPigBatch(id) if err != nil { s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err) - return nil, err + return nil, mapDomainError(err) } // 2. 将DTO中的变更应用到模型上 @@ -116,19 +116,19 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch) if err != nil { s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err) - return nil, err + return nil, mapDomainError(err) } // 4. 转换并返回结果 return s.toPigBatchResponseDTO(updatedBatch), nil } -// DeletePigBatch 将删除操作委托给领域服务。 +// DeletePigBatch 将删除操作委托给领域服务,并转换领域错误为应用层错误。 func (s *pigBatchService) DeletePigBatch(id uint) error { err := s.domainService.DeletePigBatch(id) if err != nil { s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err) - return err + return mapDomainError(err) } return nil } @@ -138,7 +138,7 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons batches, err := s.domainService.ListPigBatches(isActive) if err != nil { s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err) - return nil, err + return nil, mapDomainError(err) } var responseDTOs []*dto.PigBatchResponseDTO @@ -149,12 +149,12 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons return responseDTOs, nil } -// UpdatePigBatchPens 将关联猪栏的复杂操作委托给领域服务。 +// UpdatePigBatchPens 将关联猪栏的复杂操作委托给领域服务,并处理错误转换。 func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { err := s.domainService.UpdatePigBatchPens(batchID, desiredPenIDs) if err != nil { s.logger.Errorf("应用层: 更新猪批次猪栏关联失败, 批次ID: %d, 错误: %v", batchID, err) - return err + return mapDomainError(err) } return nil } diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index e4166c2..50d084a 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -11,15 +11,6 @@ import ( "gorm.io/gorm" ) -var ( - ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍") - ErrHouseNotFound = errors.New("指定的猪舍不存在") - ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") - ErrPenNotFound = errors.New("指定的猪栏不存在") - ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次使用,无法设置为非使用中状态") - ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次使用,无法设置为使用中状态") -) - // PigFarmService 提供了猪场资产管理的业务逻辑 type PigFarmService interface { // PigHouse methods diff --git a/internal/app/service/pig_service.go b/internal/app/service/pig_service.go index 6d43c33..8ebe135 100644 --- a/internal/app/service/pig_service.go +++ b/internal/app/service/pig_service.go @@ -1 +1,49 @@ package service + +import ( + "errors" + + domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" +) + +var ( + ErrHouseContainsPens = errors.New("无法删除包含猪栏的猪舍") + ErrHouseNotFound = errors.New("指定的猪舍不存在") + ErrPenInUse = errors.New("猪栏正在被活跃批次使用,无法删除") + ErrPenNotFound = errors.New("指定的猪栏不存在") + ErrPenStatusInvalidForOccupiedPen = errors.New("猪栏已被批次使用,无法设置为非使用中状态") + ErrPenStatusInvalidForUnoccupiedPen = errors.New("猪栏未被批次使用,无法设置为使用中状态") + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") + ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") + ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") + ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") +) + +// mapDomainError 将领域层的错误转换为应用服务层的公共错误。 +func mapDomainError(err error) error { + if err == nil { + return nil + } + + switch { + case errors.Is(err, domain_pig.ErrPigBatchNotFound): + return ErrPigBatchNotFound + case errors.Is(err, domain_pig.ErrPigBatchActive): + return ErrPigBatchActive + case errors.Is(err, domain_pig.ErrPigBatchNotActive): + return ErrPigBatchNotActive + case errors.Is(err, domain_pig.ErrPenOccupiedByOtherBatch): + return ErrPenOccupiedByOtherBatch + case errors.Is(err, domain_pig.ErrPenStatusInvalidForAllocation): + return ErrPenStatusInvalidForAllocation + case errors.Is(err, domain_pig.ErrPenNotAssociatedWithBatch): + return ErrPenNotAssociatedWithBatch + case errors.Is(err, domain_pig.ErrPenNotFound): + return ErrPenNotFound + // 可以添加更多领域错误到应用层错误的映射 + default: + return err // 对于未知的领域错误,直接返回 + } +} diff --git a/internal/core/application.go b/internal/core/application.go index ff7fbbe..11c175c 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -74,12 +74,14 @@ func NewApplication(configPath string) (*Application, error) { pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) + penRepo := repository.NewPenRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) - // 初始化猪群管理服务 - pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigFarmRepo, unitOfWork) + // 初始化猪群管理领域 + penTransferManager := pig.NewPenTransferManager(penRepo) + pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, unitOfWork, penTransferManager) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, unitOfWork, logger) diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go new file mode 100644 index 0000000..14358d9 --- /dev/null +++ b/internal/domain/pig/pen_transfer_manager.go @@ -0,0 +1,63 @@ +package pig + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "gorm.io/gorm" +) + +// PenTransferManager 定义了与猪只位置转移相关的底层数据库操作。 +// 它是一个内部服务,被主服务 PigBatchService 调用。 +type PenTransferManager interface { + // LogTransfer 在数据库中创建一条猪只迁移日志。 + LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error + + // GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。 + // 注意: 此方法依赖于您在 PenRepository 中添加对应的 GetPenByIDTx 方法。 + GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) + + // GetPensByBatchID 获取一个猪群当前关联的所有猪栏。 + // 注意: 此方法依赖于您在 PenRepository 中添加对应的 GetPensByBatchIDTx 方法。 + GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) + + // UpdatePenFields 更新一个猪栏的指定字段。 + // 注意: 此方法依赖于您在 PenRepository 中添加对应的 UpdatePenFieldsTx 方法。 + UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error +} + +// penTransferManager 是 PenTransferManager 接口的具体实现。 +// 它作为调栏管理器,处理底层的数据库交互。 +type penTransferManager struct { + penRepo repository.PenRepository +} + +// NewPenTransferManager 是 penTransferManager 的构造函数。 +func NewPenTransferManager(penRepo repository.PenRepository) PenTransferManager { + return &penTransferManager{ + penRepo: penRepo, + } +} + +// LogTransfer 实现了在数据库中创建迁移日志的逻辑。 +func (s *penTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error { + // 直接使用事务对象创建记录。 + return tx.Create(log).Error +} + +// GetPenByID 实现了获取猪栏信息的逻辑。 +// 注意: 此处调用了一个假设存在的方法 GetPenByIDTx。 +func (s *penTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) { + return s.penRepo.GetPenByIDTx(tx, penID) +} + +// GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。 +// 注意: 此处调用了一个假设存在的方法 GetPensByBatchIDTx。 +func (s *penTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { + return s.penRepo.GetPensByBatchIDTx(tx, batchID) +} + +// UpdatePenFields 实现了更新猪栏字段的逻辑。 +// 注意: 此处调用了一个假设存在的方法 UpdatePenFieldsTx。 +func (s *penTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + return s.penRepo.UpdatePenFieldsTx(tx, penID, updates) +} diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index ee86e60..a404a0d 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -21,6 +21,8 @@ var ( ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") // ErrPenNotFound 表示猪栏不存在 ErrPenNotFound = errors.New("指定的猪栏不存在") + // ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联 + ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") ) // --- 领域服务接口 --- @@ -28,6 +30,12 @@ var ( // PigBatchService 定义了猪批次管理的核心业务逻辑接口。 // 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 type PigBatchService interface { + // TransferPigsWithinBatch 处理同一个猪群内部的调栏业务。 + TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint) error + + // TransferPigsAcrossBatches 处理跨猪群的调栏业务。 + TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint) error + // CreatePigBatch 创建一个新的猪批次。 CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 7d85852..97c26e1 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -3,33 +3,35 @@ package pig import ( "errors" "fmt" + "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "github.com/google/uuid" "gorm.io/gorm" ) // --- 领域服务实现 --- // pigBatchService 是 PigBatchService 接口的具体实现。 -// 它封装了业务逻辑所需的所有依赖,如数据库仓库和工作单元。 +// 它作为猪群领域的主服务,封装了所有业务逻辑。 type pigBatchService struct { pigBatchRepo repository.PigBatchRepository // 猪批次仓库 - pigFarmRepo repository.PigFarmRepository // 猪场资产仓库 (包含猪栏操作) uow repository.UnitOfWork // 工作单元,用于管理事务 + transferSvc PenTransferManager // 调栏子服务 } // NewPigBatchService 是 pigBatchService 的构造函数。 // 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 func NewPigBatchService( pigBatchRepo repository.PigBatchRepository, - pigFarmRepo repository.PigFarmRepository, uow repository.UnitOfWork, + transferSvc PenTransferManager, ) PigBatchService { return &pigBatchService{ pigBatchRepo: pigBatchRepo, - pigFarmRepo: pigFarmRepo, uow: uow, + transferSvc: transferSvc, } } @@ -95,11 +97,13 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, er } // UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 +// 它通过调用底层的 PenTransferManager 来执行数据库操作,从而保持了职责的清晰。 func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { // 使用工作单元来确保操作的原子性 return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { // 1. 验证猪批次是否存在且活跃 - pigBatch, err := s.pigFarmRepo.GetPigBatchByIDTx(tx, batchID) + // 注意: 此处依赖一个假设存在的 pigBatchRepo.GetPigBatchByIDTx 方法 + pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPigBatchNotFound @@ -111,8 +115,8 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) return ErrPigBatchNotActive } - // 2. 获取当前关联的猪栏 - currentPens, err := s.pigFarmRepo.GetPensByBatchID(tx, batchID) + // 2. 获取当前关联的猪栏 (通过子服务) + currentPens, err := s.transferSvc.GetPensByBatchID(tx, batchID) if err != nil { return fmt.Errorf("获取当前关联猪栏失败: %w", err) } @@ -120,7 +124,7 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) currentPenMap := make(map[uint]models.Pen) currentPenIDsSet := make(map[uint]struct{}) for _, pen := range currentPens { - currentPenMap[pen.ID] = pen + currentPenMap[pen.ID] = *pen currentPenIDsSet[pen.ID] = struct{}{} } @@ -155,14 +159,15 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) updates["status"] = models.PenStatusEmpty } - if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) } } // 6. 处理添加猪栏的逻辑 for _, penID := range pensToAdd { - actualPen, err := s.pigFarmRepo.GetPenByIDTx(tx, penID) + // 通过子服务获取猪栏信息 + actualPen, err := s.transferSvc.GetPenByID(tx, penID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) @@ -182,7 +187,7 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) "pig_batch_id": &batchID, "status": models.PenStatusOccupied, } - if err := s.pigFarmRepo.UpdatePenFields(tx, penID, updates); err != nil { + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) } } @@ -190,3 +195,128 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) return nil }) } + +// --- 新增的调栏业务实现 --- + +// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 +func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType string) error { + // 1. 生成关联ID + correlationID := uuid.New().String() + + // 2. 创建调出日志 + logOut := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: fromBatchID, + PenID: fromPenID, + Quantity: -quantity, // 调出为负数 + Type: transferType, + CorrelationID: correlationID, + } + + // 3. 创建调入日志 + logIn := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: toBatchID, + PenID: toPenID, + Quantity: quantity, // 调入为正数 + Type: transferType, + CorrelationID: correlationID, + } + + // 4. 调用子服务记录日志 + if err := s.transferSvc.LogTransfer(tx, logOut); err != nil { + return fmt.Errorf("记录调出日志失败: %w", err) + } + if err := s.transferSvc.LogTransfer(tx, logIn); err != nil { + return fmt.Errorf("记录调入日志失败: %w", err) + } + + return nil +} + +// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。 +func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint) error { + if fromPenID == toPenID { + return errors.New("源猪栏和目标猪栏不能相同") + } + if quantity == 0 { + return errors.New("迁移数量不能为零") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 核心业务规则校验 + fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) + if err != nil { + return fmt.Errorf("获取源猪栏信息失败: %w", err) + } + toPen, err := s.transferSvc.GetPenByID(tx, toPenID) + if err != nil { + return fmt.Errorf("获取目标猪栏信息失败: %w", err) + } + + if fromPen.PigBatchID == nil || *fromPen.PigBatchID != batchID { + return fmt.Errorf("源猪栏 %d 不属于指定的猪群 %d", fromPenID, batchID) + } + if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID { + return fmt.Errorf("目标猪栏 %d 已被其他猪群占用", toPenID) + } + + // 2. 调用通用辅助方法执行日志记录 + err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏") + if err != nil { + return err + } + + // 3. 群内调栏,猪群总数不变 + return nil + }) +} + +// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。 +func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint) error { + if sourceBatchID == destBatchID { + return errors.New("源猪群和目标猪群不能相同") + } + if quantity == 0 { + return errors.New("迁移数量不能为零") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 核心业务规则校验 + sourceBatch, err := s.pigBatchRepo.GetPigBatchByID(sourceBatchID) + if err != nil { + return fmt.Errorf("获取源猪群信息失败: %w", err) + } + destBatch, err := s.pigBatchRepo.GetPigBatchByID(destBatchID) + if err != nil { + return fmt.Errorf("获取目标猪群信息失败: %w", err) + } + + fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) + if err != nil { + return fmt.Errorf("获取源猪栏信息失败: %w", err) + } + if fromPen.PigBatchID == nil || *fromPen.PigBatchID != sourceBatchID { + return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID) + } + + // 2. 调用通用辅助方法执行日志记录 + err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏") + if err != nil { + return err + } + + // 3. 修改本聚合的数据(猪群总数) + sourceBatch.InitialCount -= int(quantity) + destBatch.InitialCount += int(quantity) + + if _, _, err := s.pigBatchRepo.UpdatePigBatch(sourceBatch); err != nil { + return fmt.Errorf("更新源猪群数量失败: %w", err) + } + if _, _, err := s.pigBatchRepo.UpdatePigBatch(destBatch); err != nil { + return fmt.Errorf("更新目标猪群数量失败: %w", err) + } + + return nil + }) +} diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go new file mode 100644 index 0000000..811076d --- /dev/null +++ b/internal/infra/models/pig_transfer.go @@ -0,0 +1,19 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。 +// 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。 +type PigTransferLog struct { + gorm.Model + TransferTime time.Time `json:"transfer_time"` // 迁移发生时间 + PigBatchID uint `json:"pig_batch_id"` // 关联的猪群ID + PenID uint `json:"pen_id"` // 发生变动的猪栏ID + Quantity int `json:"quantity"` // 变动数量(正数表示增加,负数表示减少) + Type string `json:"type"` // 变动类型 (e.g., "群内调栏", "跨群调栏", "销售", "死亡", "新购入") + CorrelationID string `json:"correlation_id"` // 用于关联一次完整操作(如一次调栏会产生两条日志) +} diff --git a/internal/infra/repository/pen_repository.go b/internal/infra/repository/pen_repository.go new file mode 100644 index 0000000..c3a6d40 --- /dev/null +++ b/internal/infra/repository/pen_repository.go @@ -0,0 +1,46 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PenRepository 定义了与猪栏模型相关的数据库操作接口。 +type PenRepository interface { + GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) + GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) + UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error +} + +// penRepository 是 PenRepository 接口的 gorm 实现。 +type penRepository struct { + db *gorm.DB +} + +// NewPenRepository 创建一个新的 PenRepository 实例。 +func NewPenRepository(db *gorm.DB) PenRepository { + return &penRepository{db: db} +} + +// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 +func (r *penRepository) GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) { + var pen models.Pen + if err := tx.First(&pen, penID).Error; err != nil { + return nil, err + } + return &pen, nil +} + +// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 +func (r *penRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { + var pens []*models.Pen + if err := tx.Where("pig_batch_id = ?", batchID).Find(&pens).Error; err != nil { + return nil, err + } + return pens, nil +} + +// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 +func (r *penRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + return tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates).Error +} diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index 13e49bb..3f06887 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -9,6 +9,7 @@ import ( type PigBatchRepository interface { CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) GetPigBatchByID(id uint) (*models.PigBatch, error) + GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) // UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误 UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) // DeletePigBatch 根据ID删除一个猪批次,返回受影响的行数和错误 @@ -83,3 +84,12 @@ func (r *gormPigBatchRepository) ListPigBatches(isActive *bool) ([]*models.PigBa } return batches, nil } + +// GetPigBatchByIDTx 在指定的事务中,通过ID获取单个猪批次 +func (r *gormPigBatchRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) { + var batch models.PigBatch + if err := tx.First(&batch, id).Error; err != nil { + return nil, err + } + return &batch, nil +} From b3933b6d633e8576be5450eb5f8e185dae9db486 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 17:42:27 +0800 Subject: [PATCH 27/65] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=BD=92=E5=B1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_farm_service.go | 56 ++++---- internal/core/application.go | 8 +- internal/domain/pig/pen_transfer_manager.go | 10 +- internal/infra/repository/pen_repository.go | 46 ------- .../infra/repository/pig_farm_repository.go | 85 ------------- .../infra/repository/pig_pen_repository.go | 120 ++++++++++++++++++ 6 files changed, 158 insertions(+), 167 deletions(-) delete mode 100644 internal/infra/repository/pen_repository.go create mode 100644 internal/infra/repository/pig_pen_repository.go diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 50d084a..8b79c6e 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -31,17 +31,19 @@ type PigFarmService interface { } type pigFarmService struct { - logger *logs.Logger - repo repository.PigFarmRepository - uow repository.UnitOfWork // 工作单元,用于事务管理 + logger *logs.Logger + farmRepository repository.PigFarmRepository + penRepository repository.PigPenRepository + uow repository.UnitOfWork // 工作单元,用于事务管理 } // NewPigFarmService 创建一个新的 PigFarmService 实例 -func NewPigFarmService(repo repository.PigFarmRepository, uow repository.UnitOfWork, logger *logs.Logger) PigFarmService { +func NewPigFarmService(farmRepository repository.PigFarmRepository, penRepository repository.PigPenRepository, uow repository.UnitOfWork, logger *logs.Logger) PigFarmService { return &pigFarmService{ - logger: logger, - repo: repo, - uow: uow, + logger: logger, + farmRepository: farmRepository, + penRepository: penRepository, + uow: uow, } } @@ -52,16 +54,16 @@ func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHo Name: name, Description: description, } - err := s.repo.CreatePigHouse(house) + err := s.farmRepository.CreatePigHouse(house) return house, err } func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) { - return s.repo.GetPigHouseByID(id) + return s.farmRepository.GetPigHouseByID(id) } func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) { - return s.repo.ListPigHouses() + return s.farmRepository.ListPigHouses() } func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) { @@ -70,7 +72,7 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod Name: name, Description: description, } - rowsAffected, err := s.repo.UpdatePigHouse(house) + rowsAffected, err := s.farmRepository.UpdatePigHouse(house) if err != nil { return nil, err } @@ -78,12 +80,12 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod return nil, ErrHouseNotFound } // 返回更新后的完整信息 - return s.repo.GetPigHouseByID(id) + return s.farmRepository.GetPigHouseByID(id) } func (s *pigFarmService) DeletePigHouse(id uint) error { // 业务逻辑:检查猪舍是否包含猪栏 - penCount, err := s.repo.CountPensInHouse(id) + penCount, err := s.farmRepository.CountPensInHouse(id) if err != nil { return err } @@ -92,7 +94,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error { } // 调用仓库层进行删除 - rowsAffected, err := s.repo.DeletePigHouse(id) + rowsAffected, err := s.farmRepository.DeletePigHouse(id) if err != nil { return err } @@ -106,7 +108,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error { func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) { // 业务逻辑:验证所属猪舍是否存在 - _, err := s.repo.GetPigHouseByID(houseID) + _, err := s.farmRepository.GetPigHouseByID(houseID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrHouseNotFound @@ -120,21 +122,21 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) Capacity: capacity, Status: models.PenStatusEmpty, } - err = s.repo.CreatePen(pen) + err = s.penRepository.CreatePen(pen) return pen, err } func (s *pigFarmService) GetPenByID(id uint) (*models.Pen, error) { - return s.repo.GetPenByID(id) + return s.penRepository.GetPenByID(id) } func (s *pigFarmService) ListPens() ([]models.Pen, error) { - return s.repo.ListPens() + return s.penRepository.ListPens() } func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { // 业务逻辑:验证所属猪舍是否存在 - _, err := s.repo.GetPigHouseByID(houseID) + _, err := s.farmRepository.GetPigHouseByID(houseID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrHouseNotFound @@ -149,7 +151,7 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa Capacity: capacity, Status: status, } - rowsAffected, err := s.repo.UpdatePen(pen) + rowsAffected, err := s.penRepository.UpdatePen(pen) if err != nil { return nil, err } @@ -157,12 +159,12 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa return nil, ErrPenNotFound } // 返回更新后的完整信息 - return s.repo.GetPenByID(id) + return s.penRepository.GetPenByID(id) } func (s *pigFarmService) DeletePen(id uint) error { // 业务逻辑:检查猪栏是否被活跃批次使用 - pen, err := s.repo.GetPenByID(id) + pen, err := s.penRepository.GetPenByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPenNotFound // 猪栏不存在 @@ -173,7 +175,7 @@ func (s *pigFarmService) DeletePen(id uint) error { // 检查猪栏是否关联了活跃批次 // 注意:pen.PigBatchID 是指针类型,需要检查是否为 nil if pen.PigBatchID != nil && *pen.PigBatchID != 0 { - pigBatch, err := s.repo.GetPigBatchByID(*pen.PigBatchID) + pigBatch, err := s.farmRepository.GetPigBatchByID(*pen.PigBatchID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } @@ -184,7 +186,7 @@ func (s *pigFarmService) DeletePen(id uint) error { } // 调用仓库层进行删除 - rowsAffected, err := s.repo.DeletePen(id) + rowsAffected, err := s.penRepository.DeletePen(id) if err != nil { return err } @@ -198,7 +200,7 @@ func (s *pigFarmService) DeletePen(id uint) error { func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) { var updatedPen *models.Pen err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - pen, err := s.repo.GetPenByIDTx(tx, id) + pen, err := s.penRepository.GetPenByIDTx(tx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrPenNotFound @@ -228,13 +230,13 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (* "status": newStatus, } - if err := s.repo.UpdatePenFields(tx, id, updates); err != nil { + if err := s.penRepository.UpdatePenFields(tx, id, updates); err != nil { s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err) return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err) } // 获取更新后的猪栏信息 - updatedPen, err = s.repo.GetPenByIDTx(tx, id) + updatedPen, err = s.penRepository.GetPenByIDTx(tx, id) if err != nil { s.logger.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %v", id, err) return fmt.Errorf("更新猪栏状态后获取猪栏 %d 信息失败: %w", id, err) diff --git a/internal/core/application.go b/internal/core/application.go index 11c175c..28b7232 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -66,7 +66,6 @@ func NewApplication(configPath string) (*Application, error) { areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB()) deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB()) planRepo := repository.NewGormPlanRepository(storage.GetDB()) - pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) pendingTaskRepo := repository.NewGormPendingTaskRepository(storage.GetDB()) executionLogRepo := repository.NewGormExecutionLogRepository(storage.GetDB()) sensorDataRepo := repository.NewGormSensorDataRepository(storage.GetDB()) @@ -74,17 +73,18 @@ func NewApplication(configPath string) (*Application, error) { pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) - penRepo := repository.NewPenRepository(storage.GetDB()) + pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) + pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // 初始化猪群管理领域 - penTransferManager := pig.NewPenTransferManager(penRepo) + penTransferManager := pig.NewPenTransferManager(pigPenRepo) pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, unitOfWork, penTransferManager) // --- 业务逻辑处理器初始化 --- - pigFarmService := service.NewPigFarmService(pigFarmRepo, unitOfWork, logger) + pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, unitOfWork, logger) pigBatchService := service.NewPigBatchService(pigBatchDomain, logger) // 初始化审计服务 diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index 14358d9..a277354 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -13,26 +13,26 @@ type PenTransferManager interface { LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error // GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。 - // 注意: 此方法依赖于您在 PenRepository 中添加对应的 GetPenByIDTx 方法。 + // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPenByIDTx 方法。 GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) // GetPensByBatchID 获取一个猪群当前关联的所有猪栏。 - // 注意: 此方法依赖于您在 PenRepository 中添加对应的 GetPensByBatchIDTx 方法。 + // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPensByBatchIDTx 方法。 GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) // UpdatePenFields 更新一个猪栏的指定字段。 - // 注意: 此方法依赖于您在 PenRepository 中添加对应的 UpdatePenFieldsTx 方法。 + // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 UpdatePenFieldsTx 方法。 UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error } // penTransferManager 是 PenTransferManager 接口的具体实现。 // 它作为调栏管理器,处理底层的数据库交互。 type penTransferManager struct { - penRepo repository.PenRepository + penRepo repository.PigPenRepository } // NewPenTransferManager 是 penTransferManager 的构造函数。 -func NewPenTransferManager(penRepo repository.PenRepository) PenTransferManager { +func NewPenTransferManager(penRepo repository.PigPenRepository) PenTransferManager { return &penTransferManager{ penRepo: penRepo, } diff --git a/internal/infra/repository/pen_repository.go b/internal/infra/repository/pen_repository.go deleted file mode 100644 index c3a6d40..0000000 --- a/internal/infra/repository/pen_repository.go +++ /dev/null @@ -1,46 +0,0 @@ -package repository - -import ( - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "gorm.io/gorm" -) - -// PenRepository 定义了与猪栏模型相关的数据库操作接口。 -type PenRepository interface { - GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) - GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) - UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error -} - -// penRepository 是 PenRepository 接口的 gorm 实现。 -type penRepository struct { - db *gorm.DB -} - -// NewPenRepository 创建一个新的 PenRepository 实例。 -func NewPenRepository(db *gorm.DB) PenRepository { - return &penRepository{db: db} -} - -// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 -func (r *penRepository) GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) { - var pen models.Pen - if err := tx.First(&pen, penID).Error; err != nil { - return nil, err - } - return &pen, nil -} - -// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 -func (r *penRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { - var pens []*models.Pen - if err := tx.Where("pig_batch_id = ?", batchID).Find(&pens).Error; err != nil { - return nil, err - } - return pens, nil -} - -// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 -func (r *penRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error { - return tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates).Error -} diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index 47edb2a..fbd656f 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -17,22 +17,6 @@ type PigFarmRepository interface { DeletePigHouse(id uint) (int64, error) CountPensInHouse(houseID uint) (int64, error) - // Pen methods - CreatePen(pen *models.Pen) error - // GetPenByID 根据ID获取单个猪栏 (非事务性) - GetPenByID(id uint) (*models.Pen, error) - // GetPenByIDTx 根据ID获取单个猪栏 (事务性) - GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) - ListPens() ([]models.Pen, error) - // UpdatePen 更新一个猪栏,返回受影响的行数和错误 - UpdatePen(pen *models.Pen) (int64, error) - // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 - DeletePen(id uint) (int64, error) - // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) - GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) - // UpdatePenFields 更新猪栏的指定字段 (事务性) - UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error - // PigBatch methods // GetPigBatchByID 根据ID获取单个猪批次 (非事务性) GetPigBatchByID(id uint) (*models.PigBatch, error) @@ -100,75 +84,6 @@ func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { return count, err } -// --- Pen Implementation --- - -// CreatePen 创建一个新的猪栏 -func (r *gormPigFarmRepository) CreatePen(pen *models.Pen) error { - return r.db.Create(pen).Error -} - -// GetPenByID 根据ID获取单个猪栏 (非事务性) -func (r *gormPigFarmRepository) GetPenByID(id uint) (*models.Pen, error) { - var pen models.Pen - if err := r.db.First(&pen, id).Error; err != nil { - return nil, err - } - return &pen, nil -} - -// GetPenByIDTx 根据ID获取单个猪栏 (事务性) -func (r *gormPigFarmRepository) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) { - var pen models.Pen - if err := tx.First(&pen, id).Error; err != nil { - return nil, err - } - return &pen, nil -} - -// ListPens 列出所有猪栏 -func (r *gormPigFarmRepository) ListPens() ([]models.Pen, error) { - var pens []models.Pen - if err := r.db.Find(&pens).Error; err != nil { - return nil, err - } - return pens, nil -} - -// UpdatePen 更新一个猪栏,返回受影响的行数和错误 -func (r *gormPigFarmRepository) UpdatePen(pen *models.Pen) (int64, error) { - result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) - if result.Error != nil { - return 0, result.Error - } - return result.RowsAffected, nil -} - -// DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 -func (r *gormPigFarmRepository) DeletePen(id uint) (int64, error) { - result := r.db.Delete(&models.Pen{}, id) - if result.Error != nil { - return 0, result.Error - } - return result.RowsAffected, nil -} - -// GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) -func (r *gormPigFarmRepository) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) { - var pens []models.Pen - // 注意:PigBatchID 是指针类型,需要处理 nil 值 - result := tx.Where("pig_batch_id = ?", batchID).Find(&pens) - if result.Error != nil { - return nil, result.Error - } - return pens, nil -} - -// UpdatePenFields 更新猪栏的指定字段 (事务性) -func (r *gormPigFarmRepository) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { - result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) - return result.Error -} - // --- PigBatch Implementation --- // GetPigBatchByID 根据ID获取单个猪批次 (非事务性) diff --git a/internal/infra/repository/pig_pen_repository.go b/internal/infra/repository/pig_pen_repository.go new file mode 100644 index 0000000..8d894cc --- /dev/null +++ b/internal/infra/repository/pig_pen_repository.go @@ -0,0 +1,120 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigPenRepository 定义了与猪栏模型相关的数据库操作接口。 +type PigPenRepository interface { + CreatePen(pen *models.Pen) error + // GetPenByID 根据ID获取单个猪栏 (非事务性) + GetPenByID(id uint) (*models.Pen, error) + // GetPenByIDTx 根据ID获取单个猪栏 (事务性) + GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) + ListPens() ([]models.Pen, error) + // UpdatePen 更新一个猪栏,返回受影响的行数和错误 + UpdatePen(pen *models.Pen) (int64, error) + // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 + DeletePen(id uint) (int64, error) + // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) + GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) + // UpdatePenFields 更新猪栏的指定字段 (事务性) + UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error + + GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) + UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error +} + +// pigPenRepository 是 PigPenRepository 接口的 gorm 实现。 +type pigPenRepository struct { + db *gorm.DB +} + +// NewGormPigPenRepository 创建一个新的 PigPenRepository 实例。 +func NewGormPigPenRepository(db *gorm.DB) PigPenRepository { + return &pigPenRepository{db: db} +} + +// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 +func (r *pigPenRepository) GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) { + var pen models.Pen + if err := tx.First(&pen, penID).Error; err != nil { + return nil, err + } + return &pen, nil +} + +// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 +func (r *pigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { + var pens []*models.Pen + if err := tx.Where("pig_batch_id = ?", batchID).Find(&pens).Error; err != nil { + return nil, err + } + return pens, nil +} + +// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 +func (r *pigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + return tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates).Error +} + +// --- Pen Implementation --- + +// CreatePen 创建一个新的猪栏 +func (r *pigPenRepository) CreatePen(pen *models.Pen) error { + return r.db.Create(pen).Error +} + +// GetPenByID 根据ID获取单个猪栏 (非事务性) +func (r *pigPenRepository) GetPenByID(id uint) (*models.Pen, error) { + var pen models.Pen + if err := r.db.First(&pen, id).Error; err != nil { + return nil, err + } + return &pen, nil +} + +// ListPens 列出所有猪栏 +func (r *pigPenRepository) ListPens() ([]models.Pen, error) { + var pens []models.Pen + if err := r.db.Find(&pens).Error; err != nil { + return nil, err + } + return pens, nil +} + +// UpdatePen 更新一个猪栏,返回受影响的行数和错误 +func (r *pigPenRepository) UpdatePen(pen *models.Pen) (int64, error) { + result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) + if result.Error != nil { + return 0, result.Error + } + return result.RowsAffected, nil +} + +// DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 +func (r *pigPenRepository) DeletePen(id uint) (int64, error) { + result := r.db.Delete(&models.Pen{}, id) + if result.Error != nil { + return 0, result.Error + } + return result.RowsAffected, nil +} + +// GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) +func (r *pigPenRepository) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) { + var pens []models.Pen + // 注意:PigBatchID 是指针类型,需要处理 nil 值 + result := tx.Where("pig_batch_id = ?", batchID).Find(&pens) + if result.Error != nil { + return nil, result.Error + } + return pens, nil +} + +// UpdatePenFields 更新猪栏的指定字段 (事务性) +func (r *pigPenRepository) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { + result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) + return result.Error +} From b6e68e861beff85af383cbc187b7f7b948176fcf Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 17:46:03 +0800 Subject: [PATCH 28/65] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=BD=92=E5=B1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_farm_service.go | 26 ++++++++++++------- internal/core/application.go | 2 +- .../infra/repository/pig_batch_repository.go | 6 +---- .../infra/repository/pig_farm_repository.go | 26 ------------------- 4 files changed, 18 insertions(+), 42 deletions(-) diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index 8b79c6e..bb6f746 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -31,19 +31,25 @@ type PigFarmService interface { } type pigFarmService struct { - logger *logs.Logger - farmRepository repository.PigFarmRepository - penRepository repository.PigPenRepository - uow repository.UnitOfWork // 工作单元,用于事务管理 + logger *logs.Logger + farmRepository repository.PigFarmRepository + penRepository repository.PigPenRepository + batchRepository repository.PigBatchRepository + uow repository.UnitOfWork // 工作单元,用于事务管理 } // NewPigFarmService 创建一个新的 PigFarmService 实例 -func NewPigFarmService(farmRepository repository.PigFarmRepository, penRepository repository.PigPenRepository, uow repository.UnitOfWork, logger *logs.Logger) PigFarmService { +func NewPigFarmService(farmRepository repository.PigFarmRepository, + penRepository repository.PigPenRepository, + batchRepository repository.PigBatchRepository, + uow repository.UnitOfWork, + logger *logs.Logger) PigFarmService { return &pigFarmService{ - logger: logger, - farmRepository: farmRepository, - penRepository: penRepository, - uow: uow, + logger: logger, + farmRepository: farmRepository, + penRepository: penRepository, + batchRepository: batchRepository, + uow: uow, } } @@ -175,7 +181,7 @@ func (s *pigFarmService) DeletePen(id uint) error { // 检查猪栏是否关联了活跃批次 // 注意:pen.PigBatchID 是指针类型,需要检查是否为 nil if pen.PigBatchID != nil && *pen.PigBatchID != 0 { - pigBatch, err := s.farmRepository.GetPigBatchByID(*pen.PigBatchID) + pigBatch, err := s.batchRepository.GetPigBatchByID(*pen.PigBatchID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } diff --git a/internal/core/application.go b/internal/core/application.go index 28b7232..c99c89e 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -84,7 +84,7 @@ func NewApplication(configPath string) (*Application, error) { pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, unitOfWork, penTransferManager) // --- 业务逻辑处理器初始化 --- - pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, unitOfWork, logger) + pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger) pigBatchService := service.NewPigBatchService(pigBatchDomain, logger) // 初始化审计服务 diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index 3f06887..51876a0 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -37,11 +37,7 @@ func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models // GetPigBatchByID 根据ID获取单个猪批次 func (r *gormPigBatchRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { - var batch models.PigBatch - if err := r.db.First(&batch, id).Error; err != nil { - return nil, err - } - return &batch, nil + return r.GetPigBatchByIDTx(r.db, id) } // UpdatePigBatch 更新一个猪批次 diff --git a/internal/infra/repository/pig_farm_repository.go b/internal/infra/repository/pig_farm_repository.go index fbd656f..954f1f5 100644 --- a/internal/infra/repository/pig_farm_repository.go +++ b/internal/infra/repository/pig_farm_repository.go @@ -16,12 +16,6 @@ type PigFarmRepository interface { // DeletePigHouse 根据ID删除一个猪舍,返回受影响的行数和错误 DeletePigHouse(id uint) (int64, error) CountPensInHouse(houseID uint) (int64, error) - - // PigBatch methods - // GetPigBatchByID 根据ID获取单个猪批次 (非事务性) - GetPigBatchByID(id uint) (*models.PigBatch, error) - // GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) - GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) } // gormPigFarmRepository 是 PigFarmRepository 的 GORM 实现 @@ -83,23 +77,3 @@ func (r *gormPigFarmRepository) CountPensInHouse(houseID uint) (int64, error) { err := r.db.Model(&models.Pen{}).Where("house_id = ?", houseID).Count(&count).Error return count, err } - -// --- PigBatch Implementation --- - -// GetPigBatchByID 根据ID获取单个猪批次 (非事务性) -func (r *gormPigFarmRepository) GetPigBatchByID(id uint) (*models.PigBatch, error) { - var batch models.PigBatch - if err := r.db.First(&batch, id).Error; err != nil { - return nil, err - } - return &batch, nil -} - -// GetPigBatchByIDTx 根据ID获取单个猪批次 (事务性) -func (r *gormPigFarmRepository) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) { - var batch models.PigBatch - if err := tx.First(&batch, id).Error; err != nil { - return nil, err - } - return &batch, nil -} From 9c35372720c9782e60d88cd5aa03d0434669136c Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 18:00:50 +0800 Subject: [PATCH 29/65] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/service/pig_farm_service.go | 2 +- .../infra/repository/pig_pen_repository.go | 67 ++++++------------- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/internal/app/service/pig_farm_service.go b/internal/app/service/pig_farm_service.go index bb6f746..5f4b0c5 100644 --- a/internal/app/service/pig_farm_service.go +++ b/internal/app/service/pig_farm_service.go @@ -236,7 +236,7 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (* "status": newStatus, } - if err := s.penRepository.UpdatePenFields(tx, id, updates); err != nil { + if err := s.penRepository.UpdatePenFieldsTx(tx, id, updates); err != nil { s.logger.Errorf("更新猪栏 %d 状态失败: %v", id, err) return fmt.Errorf("更新猪栏 %d 状态失败: %w", id, err) } diff --git a/internal/infra/repository/pig_pen_repository.go b/internal/infra/repository/pig_pen_repository.go index 8d894cc..84a1b69 100644 --- a/internal/infra/repository/pig_pen_repository.go +++ b/internal/infra/repository/pig_pen_repository.go @@ -17,66 +17,43 @@ type PigPenRepository interface { UpdatePen(pen *models.Pen) (int64, error) // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 DeletePen(id uint) (int64, error) - // GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) - GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) - // UpdatePenFields 更新猪栏的指定字段 (事务性) - UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error - + // GetPensByBatchIDTx 根据批次ID获取所有关联的猪栏 (事务性) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) + // UpdatePenFieldsTx 更新猪栏的指定字段 (事务性) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error } -// pigPenRepository 是 PigPenRepository 接口的 gorm 实现。 -type pigPenRepository struct { +// gormPigPenRepository 是 PigPenRepository 接口的 GORM 实现。 +type gormPigPenRepository struct { db *gorm.DB } -// NewGormPigPenRepository 创建一个新的 PigPenRepository 实例。 +// NewGormPigPenRepository 创建一个新的 PigPenRepository GORM 实现实例。 func NewGormPigPenRepository(db *gorm.DB) PigPenRepository { - return &pigPenRepository{db: db} + return &gormPigPenRepository{db: db} } -// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 -func (r *pigPenRepository) GetPenByIDTx(tx *gorm.DB, penID uint) (*models.Pen, error) { - var pen models.Pen - if err := tx.First(&pen, penID).Error; err != nil { - return nil, err - } - return &pen, nil -} - -// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 -func (r *pigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { - var pens []*models.Pen - if err := tx.Where("pig_batch_id = ?", batchID).Find(&pens).Error; err != nil { - return nil, err - } - return pens, nil -} - -// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 -func (r *pigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error { - return tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates).Error -} - -// --- Pen Implementation --- - // CreatePen 创建一个新的猪栏 -func (r *pigPenRepository) CreatePen(pen *models.Pen) error { +func (r *gormPigPenRepository) CreatePen(pen *models.Pen) error { return r.db.Create(pen).Error } // GetPenByID 根据ID获取单个猪栏 (非事务性) -func (r *pigPenRepository) GetPenByID(id uint) (*models.Pen, error) { +func (r *gormPigPenRepository) GetPenByID(id uint) (*models.Pen, error) { + return r.GetPenByIDTx(r.db, id) // 非Tx方法直接调用Tx方法 +} + +// GetPenByIDTx 在指定的事务中,通过ID获取单个猪栏信息。 +func (r *gormPigPenRepository) GetPenByIDTx(tx *gorm.DB, id uint) (*models.Pen, error) { var pen models.Pen - if err := r.db.First(&pen, id).Error; err != nil { + if err := tx.First(&pen, id).Error; err != nil { return nil, err } return &pen, nil } // ListPens 列出所有猪栏 -func (r *pigPenRepository) ListPens() ([]models.Pen, error) { +func (r *gormPigPenRepository) ListPens() ([]models.Pen, error) { var pens []models.Pen if err := r.db.Find(&pens).Error; err != nil { return nil, err @@ -85,7 +62,7 @@ func (r *pigPenRepository) ListPens() ([]models.Pen, error) { } // UpdatePen 更新一个猪栏,返回受影响的行数和错误 -func (r *pigPenRepository) UpdatePen(pen *models.Pen) (int64, error) { +func (r *gormPigPenRepository) UpdatePen(pen *models.Pen) (int64, error) { result := r.db.Model(&models.Pen{}).Where("id = ?", pen.ID).Updates(pen) if result.Error != nil { return 0, result.Error @@ -94,7 +71,7 @@ func (r *pigPenRepository) UpdatePen(pen *models.Pen) (int64, error) { } // DeletePen 根据ID删除一个猪栏,返回受影响的行数和错误 -func (r *pigPenRepository) DeletePen(id uint) (int64, error) { +func (r *gormPigPenRepository) DeletePen(id uint) (int64, error) { result := r.db.Delete(&models.Pen{}, id) if result.Error != nil { return 0, result.Error @@ -102,9 +79,9 @@ func (r *pigPenRepository) DeletePen(id uint) (int64, error) { return result.RowsAffected, nil } -// GetPensByBatchID 根据批次ID获取所有关联的猪栏 (事务性) -func (r *pigPenRepository) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models.Pen, error) { - var pens []models.Pen +// GetPensByBatchIDTx 在指定的事务中,获取一个猪群当前关联的所有猪栏。 +func (r *gormPigPenRepository) GetPensByBatchIDTx(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { + var pens []*models.Pen // 注意:PigBatchID 是指针类型,需要处理 nil 值 result := tx.Where("pig_batch_id = ?", batchID).Find(&pens) if result.Error != nil { @@ -113,8 +90,8 @@ func (r *pigPenRepository) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]models return pens, nil } -// UpdatePenFields 更新猪栏的指定字段 (事务性) -func (r *pigPenRepository) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { +// UpdatePenFieldsTx 在指定的事务中,更新一个猪栏的指定字段。 +func (r *gormPigPenRepository) UpdatePenFieldsTx(tx *gorm.DB, penID uint, updates map[string]interface{}) error { result := tx.Model(&models.Pen{}).Where("id = ?", penID).Updates(updates) return result.Error } From 2aa0f090790b5d5690f79801dab6a1b7b6617151 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 18:28:16 +0800 Subject: [PATCH 30/65] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=89=B9=E6=AC=A1?= =?UTF-8?q?=E6=97=B6=E6=8F=92=E5=85=A5=E4=B8=80=E6=9D=A1=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/controller/auth_utils.go | 47 ++++++++++++++++ .../management/pig_batch_controller.go | 4 +- internal/app/service/pig_batch_service.go | 6 +- internal/core/application.go | 3 +- internal/domain/pig/pig_batch.go | 2 +- internal/domain/pig/pig_batch_service.go | 56 ++++++++++++++++--- internal/infra/models/pig.go | 18 +++--- .../repository/pig_batch_log_repository.go | 27 +++++++++ .../infra/repository/pig_batch_repository.go | 8 ++- 9 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 internal/app/controller/auth_utils.go create mode 100644 internal/infra/repository/pig_batch_log_repository.go diff --git a/internal/app/controller/auth_utils.go b/internal/app/controller/auth_utils.go new file mode 100644 index 0000000..08a9589 --- /dev/null +++ b/internal/app/controller/auth_utils.go @@ -0,0 +1,47 @@ +package controller + +import ( + "errors" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/gin-gonic/gin" +) + +var ( + // ErrUserNotFoundInContext 表示在 gin.Context 中未找到用户信息。 + ErrUserNotFoundInContext = errors.New("context中未找到用户信息") + // ErrInvalidUserType 表示从 gin.Context 中获取的用户信息类型不正确。 + ErrInvalidUserType = errors.New("context中用户信息类型不正确") +) + +// GetOperatorIDFromContext 从 gin.Context 中提取操作者ID。 +// 假设操作者ID是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 ID 字段。 +func GetOperatorIDFromContext(c *gin.Context) (uint, error) { + userVal, exists := c.Get(models.ContextUserKey.String()) + if !exists { + return 0, ErrUserNotFoundInContext + } + + user, ok := userVal.(*models.User) + if !ok { + return 0, ErrInvalidUserType + } + + return user.ID, nil +} + +// GetOperatorFromContext 从 gin.Context 中提取操作者。 +// 假设操作者是由 AuthMiddleware 存储到 context 中的 *models.User 对象的 字段。 +func GetOperatorFromContext(c *gin.Context) (*models.User, error) { + userVal, exists := c.Get(models.ContextUserKey.String()) + if !exists { + return nil, ErrUserNotFoundInContext + } + + user, ok := userVal.(*models.User) + if !ok { + return nil, ErrInvalidUserType + } + + return user, nil +} diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 46199b6..4debac0 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -45,7 +45,9 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { return } - respDTO, err := c.service.CreatePigBatch(&req) + userID, err := controller.GetOperatorIDFromContext(ctx) + + respDTO, err := c.service.CreatePigBatch(userID, &req) if err != nil { c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪批次失败", action, "业务逻辑失败", req) diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index d4a5149..da50327 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -9,7 +9,7 @@ import ( // PigBatchService 接口定义保持不变,继续作为应用层对外的契约。 type PigBatchService interface { - CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) + CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error @@ -51,7 +51,7 @@ func (s *pigBatchService) toPigBatchResponseDTO(batch *models.PigBatch) *dto.Pig } // CreatePigBatch 现在将请求委托给领域服务处理。 -func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) { +func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) { // 1. DTO -> 领域模型 batch := &models.PigBatch{ BatchNumber: dto.BatchNumber, @@ -62,7 +62,7 @@ func (s *pigBatchService) CreatePigBatch(dto *dto.PigBatchCreateDTO) (*dto.PigBa } // 2. 调用领域服务 - createdBatch, err := s.domainService.CreatePigBatch(batch) + createdBatch, err := s.domainService.CreatePigBatch(operatorID, batch) if err != nil { s.logger.Errorf("应用层: 创建猪批次失败: %v", err) return nil, mapDomainError(err) diff --git a/internal/core/application.go b/internal/core/application.go index c99c89e..f11b397 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -73,6 +73,7 @@ func NewApplication(configPath string) (*Application, error) { pendingCollectionRepo := repository.NewGormPendingCollectionRepository(storage.GetDB()) userActionLogRepo := repository.NewGormUserActionLogRepository(storage.GetDB()) pigBatchRepo := repository.NewGormPigBatchRepository(storage.GetDB()) + pigBatchLogRepo := repository.NewGormPigBatchLogRepository(storage.GetDB()) pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB()) @@ -81,7 +82,7 @@ func NewApplication(configPath string) (*Application, error) { // 初始化猪群管理领域 penTransferManager := pig.NewPenTransferManager(pigPenRepo) - pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, unitOfWork, penTransferManager) + pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, penTransferManager) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index a404a0d..1983684 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -37,7 +37,7 @@ type PigBatchService interface { TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint) error // CreatePigBatch 创建一个新的猪批次。 - CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) // GetPigBatch 根据ID获取单个猪批次的详细信息。 GetPigBatch(id uint) (*models.PigBatch, error) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 97c26e1..12d2868 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -16,29 +16,67 @@ import ( // pigBatchService 是 PigBatchService 接口的具体实现。 // 它作为猪群领域的主服务,封装了所有业务逻辑。 type pigBatchService struct { - pigBatchRepo repository.PigBatchRepository // 猪批次仓库 - uow repository.UnitOfWork // 工作单元,用于管理事务 - transferSvc PenTransferManager // 调栏子服务 + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 + uow repository.UnitOfWork // 工作单元,用于管理事务 + transferSvc PenTransferManager // 调栏子服务 } // NewPigBatchService 是 pigBatchService 的构造函数。 // 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 func NewPigBatchService( pigBatchRepo repository.PigBatchRepository, + pigBatchLogRepo repository.PigBatchLogRepository, uow repository.UnitOfWork, transferSvc PenTransferManager, ) PigBatchService { return &pigBatchService{ - pigBatchRepo: pigBatchRepo, - uow: uow, - transferSvc: transferSvc, + pigBatchRepo: pigBatchRepo, + pigBatchLogRepo: pigBatchLogRepo, + uow: uow, + transferSvc: transferSvc, } } -// CreatePigBatch 实现了创建猪批次的逻辑。 -func (s *pigBatchService) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { +// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。 +func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) { // 业务规则可以在这里添加,例如检查批次号是否唯一等 - return s.pigBatchRepo.CreatePigBatch(batch) + + var createdBatch *models.PigBatch + err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 创建猪批次 + // 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法 + var err error + createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch) + if err != nil { + return fmt.Errorf("创建猪批次失败: %w", err) + } + + // 2. 创建初始批次日志 + initialLog := &models.PigBatchLog{ + PigBatchID: createdBatch.ID, + HappenedAt: time.Now(), + ChangeType: models.ChangeTypeCorrection, // 初始创建可视为一种校正 + ChangeCount: createdBatch.InitialCount, + Reason: fmt.Sprintf("创建了新的猪批次 %s,初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount), + BeforeCount: 0, // 初始创建前数量为0 + AfterCount: int(createdBatch.InitialCount), + OperatorID: 0, // 假设初始创建没有特定操作员ID,或需要从上下文传入 + } + + // 3. 记录批次日志 + if err := s.pigBatchLogRepo.Create(tx, initialLog); err != nil { + return fmt.Errorf("记录初始批次日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return createdBatch, nil } // GetPigBatch 实现了获取单个猪批次的逻辑。 diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig.go index 8042528..1fb5313 100644 --- a/internal/infra/models/pig.go +++ b/internal/infra/models/pig.go @@ -67,16 +67,14 @@ const ( // PigBatchLog 记录了猪批次数量或状态的每一次变更 type PigBatchLog struct { gorm.Model - PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` - ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` - ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` - Reason string `gorm:"size:255;comment:变更原因描述"` - BeforeCount int `gorm:"not null;comment:变更前总数"` - AfterCount int `gorm:"not null;comment:变更后总数"` - BeforeSickCount int `gorm:"not null;comment:变更前病猪数"` - AfterSickCount int `gorm:"not null;comment:变更后病猪数"` - OperatorID uint `gorm:"comment:操作员ID"` - HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` + ChangeType LogChangeType `gorm:"size:20;not null;comment:变更类型"` + ChangeCount int `gorm:"not null;comment:数量变化,负数表示减少"` + Reason string `gorm:"size:255;comment:变更原因描述"` + BeforeCount int `gorm:"not null;comment:变更前总数"` + AfterCount int `gorm:"not null;comment:变更后总数"` + OperatorID uint `gorm:"comment:操作员ID"` + HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` } func (PigBatchLog) TableName() string { diff --git a/internal/infra/repository/pig_batch_log_repository.go b/internal/infra/repository/pig_batch_log_repository.go new file mode 100644 index 0000000..aaed13d --- /dev/null +++ b/internal/infra/repository/pig_batch_log_repository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigBatchLogRepository 定义了与猪批次日志相关的数据库操作接口。 +type PigBatchLogRepository interface { + // Create 在指定的事务中创建一条新的猪批次日志。 + Create(tx *gorm.DB, log *models.PigBatchLog) error +} + +// gormPigBatchLogRepository 是 PigBatchLogRepository 的 GORM 实现。 +type gormPigBatchLogRepository struct { + db *gorm.DB +} + +// NewGormPigBatchLogRepository 创建一个新的 PigBatchLogRepository 实例。 +func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository { + return &gormPigBatchLogRepository{db: db} +} + +// Create 实现了创建猪批次日志的逻辑。 +func (r *gormPigBatchLogRepository) Create(tx *gorm.DB, log *models.PigBatchLog) error { + return tx.Create(log).Error +} diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index 51876a0..3b57b72 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -8,6 +8,7 @@ import ( // PigBatchRepository 定义了与猪批次相关的数据库操作接口 type PigBatchRepository interface { CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) GetPigBatchByID(id uint) (*models.PigBatch, error) GetPigBatchByIDTx(tx *gorm.DB, id uint) (*models.PigBatch, error) // UpdatePigBatch 更新一个猪批次,返回更新后的批次、受影响的行数和错误 @@ -29,7 +30,12 @@ func NewGormPigBatchRepository(db *gorm.DB) PigBatchRepository { // CreatePigBatch 创建一个新的猪批次 func (r *gormPigBatchRepository) CreatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { - if err := r.db.Create(batch).Error; err != nil { + return r.CreatePigBatchTx(r.db, batch) +} + +// CreatePigBatchTx 在指定的事务中,创建一个新的猪批次 +func (r *gormPigBatchRepository) CreatePigBatchTx(tx *gorm.DB, batch *models.PigBatch) (*models.PigBatch, error) { + if err := tx.Create(batch).Error; err != nil { return nil, err } return batch, nil From cb207322054e2aeba23a7d7bfaf23de9fdaaf7e6 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 18:29:08 +0800 Subject: [PATCH 31/65] =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=94=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/{pig.go => pig_batch.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/infra/models/{pig.go => pig_batch.go} (100%) diff --git a/internal/infra/models/pig.go b/internal/infra/models/pig_batch.go similarity index 100% rename from internal/infra/models/pig.go rename to internal/infra/models/pig_batch.go From 811c6a09c51ead7e04914cbed4292945c1df8b1b Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 18:32:18 +0800 Subject: [PATCH 32/65] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig_batch.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index 1fb5313..c2f02ae 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -59,8 +59,6 @@ const ( ChangeTypeSale LogChangeType = "销售" ChangeTypeTransferIn LogChangeType = "转入" ChangeTypeTransferOut LogChangeType = "转出" - ChangeTypeTreatment LogChangeType = "治疗" // 仅改变健康状态,不改变总数 - ChangeTypeRecovering LogChangeType = "康复" // 仅改变健康状态,不改变总数 ChangeTypeCorrection LogChangeType = "盘点校正" ) From 47c72dff3e1f1e2857ab3dc8007fd137143d3082 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 21:20:22 +0800 Subject: [PATCH 33/65] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=B0=83=E7=8C=AA?= =?UTF-8?q?=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 4 ++-- internal/domain/pig/pig_batch_service.go | 14 +++++++---- internal/infra/database/postgres.go | 2 ++ internal/infra/models/models.go | 1 + internal/infra/models/pig_transfer.go | 30 +++++++++++++++++++----- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 1983684..53ffa8f 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -31,10 +31,10 @@ var ( // 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 type PigBatchService interface { // TransferPigsWithinBatch 处理同一个猪群内部的调栏业务。 - TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint) error + TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error // TransferPigsAcrossBatches 处理跨猪群的调栏业务。 - TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint) error + TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error // CreatePigBatch 创建一个新的猪批次。 CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 12d2868..cdc03b7 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -237,7 +237,7 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) // --- 新增的调栏业务实现 --- // executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 -func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType string) error { +func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error { // 1. 生成关联ID correlationID := uuid.New().String() @@ -249,6 +249,8 @@ func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatc Quantity: -quantity, // 调出为负数 Type: transferType, CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: remarks, } // 3. 创建调入日志 @@ -259,6 +261,8 @@ func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatc Quantity: quantity, // 调入为正数 Type: transferType, CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: remarks, } // 4. 调用子服务记录日志 @@ -273,7 +277,7 @@ func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatc } // TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。 -func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint) error { +func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { if fromPenID == toPenID { return errors.New("源猪栏和目标猪栏不能相同") } @@ -300,7 +304,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, } // 2. 调用通用辅助方法执行日志记录 - err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏") + err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks) if err != nil { return err } @@ -311,7 +315,7 @@ func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, } // TransferPigsAcrossBatches 实现了跨猪群的调栏业务。 -func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint) error { +func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { if sourceBatchID == destBatchID { return errors.New("源猪群和目标猪群不能相同") } @@ -339,7 +343,7 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc } // 2. 调用通用辅助方法执行日志记录 - err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏") + err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks) if err != nil { return err } diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 04058ff..0dfc003 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -167,6 +167,7 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.PigBatchLog{}, "happened_at"}, {models.WeighingBatch{}, "weighing_time"}, {models.WeighingRecord{}, "weighing_time"}, + {models.PigTransferLog{}, "transfer_time"}, } for _, table := range tablesToConvert { @@ -203,6 +204,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { {models.PigBatchLog{}, "pig_batch_id"}, {models.WeighingBatch{}, "pig_batch_id"}, {models.WeighingRecord{}, "weighing_batch_id"}, + {models.PigTransferLog{}, "pig_batch_id"}, } for _, policy := range policies { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 1e6c116..6b4296a 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -41,6 +41,7 @@ func GetAllModels() []interface{} { &PigBatchLog{}, &WeighingBatch{}, &WeighingRecord{}, + &PigTransferLog{}, // Feed Models &RawMaterial{}, diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go index 811076d..2c2a7b0 100644 --- a/internal/infra/models/pig_transfer.go +++ b/internal/infra/models/pig_transfer.go @@ -6,14 +6,32 @@ import ( "gorm.io/gorm" ) +// PigTransferType 定义了猪只迁移的类型 +type PigTransferType string + +const ( + PigTransferTypeInternal PigTransferType = "群内调栏" // 同一猪群内猪栏间的调动 + PigTransferTypeCrossBatch PigTransferType = "跨群调栏" // 不同猪群间的调动 + PigTransferTypeSale PigTransferType = "销售" // 猪只售出 + PigTransferTypeDeath PigTransferType = "死亡" // 猪只死亡 + PigTransferTypePurchase PigTransferType = "新购入" // 新购入猪只 + // 可以根据业务需求添加更多类型,例如:淘汰、转出到其他农场等 +) + // PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。 // 它作为事件溯源的基础,用于推算任意时间点猪栏的猪只数量。 type PigTransferLog struct { gorm.Model - TransferTime time.Time `json:"transfer_time"` // 迁移发生时间 - PigBatchID uint `json:"pig_batch_id"` // 关联的猪群ID - PenID uint `json:"pen_id"` // 发生变动的猪栏ID - Quantity int `json:"quantity"` // 变动数量(正数表示增加,负数表示减少) - Type string `json:"type"` // 变动类型 (e.g., "群内调栏", "跨群调栏", "销售", "死亡", "新购入") - CorrelationID string `json:"correlation_id"` // 用于关联一次完整操作(如一次调栏会产生两条日志) + TransferTime time.Time `gorm:"primaryKey;comment:迁移发生时间" json:"transfer_time"` // 迁移发生时间,作为联合主键 + PigBatchID uint `gorm:"primaryKey;comment:关联的猪群ID" json:"pig_batch_id"` // 关联的猪群ID,作为联合主键 + PenID uint `gorm:"primaryKey;comment:发生变动的猪栏ID" json:"pen_id"` // 发生变动的猪栏ID,作为联合主键 + Quantity int `gorm:"not null;comment:变动数量(正数表示增加,负数表示减少)" json:"quantity"` // 变动数量(正数表示增加,负数减少) + Type PigTransferType `gorm:"not null;comment:变动类型" json:"type"` // 变动类型,使用枚举类型 + CorrelationID string `gorm:"comment:用于关联一次完整操作(如一次调栏会产生两条日志)" json:"correlation_id"` // 用于关联一次完整操作 + OperatorID uint `gorm:"not null;comment:操作员ID" json:"operator_id"` // 操作员ID + Remarks string `gorm:"comment:备注" json:"remarks"` +} + +func (p PigTransferLog) TableName() string { + return "pig_transfer_logs" } From b1e1dcdcadbfeff1253a33bbf4ab485a21553858 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 21:25:13 +0800 Subject: [PATCH 34/65] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig_transfer.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go index 2c2a7b0..70361f2 100644 --- a/internal/infra/models/pig_transfer.go +++ b/internal/infra/models/pig_transfer.go @@ -10,12 +10,14 @@ import ( type PigTransferType string const ( - PigTransferTypeInternal PigTransferType = "群内调栏" // 同一猪群内猪栏间的调动 - PigTransferTypeCrossBatch PigTransferType = "跨群调栏" // 不同猪群间的调动 - PigTransferTypeSale PigTransferType = "销售" // 猪只售出 - PigTransferTypeDeath PigTransferType = "死亡" // 猪只死亡 - PigTransferTypePurchase PigTransferType = "新购入" // 新购入猪只 - // 可以根据业务需求添加更多类型,例如:淘汰、转出到其他农场等 + PigTransferTypeInternal PigTransferType = "群内调栏" // 同一猪群内猪栏间的调动 + PigTransferTypeCrossBatch PigTransferType = "跨群调栏" // 不同猪群间的调动 + PigTransferTypeSale PigTransferType = "销售" // 猪只售出 + PigTransferTypeDeath PigTransferType = "死亡" // 猪只死亡 + PigTransferTypeEliminate PigTransferType = "淘汰" // 猪只淘汰 + PigTransferTypePurchase PigTransferType = "新购入" // 新购入猪只 + PigTransferTypeDeliveryRoomTransfor PigTransferType = "产房转入" // 产房转入 + // 可以根据业务需求添加更多类型,例如:转出到其他农场等 ) // PigTransferLog 记录了每一次猪只数量在猪栏间的变动事件。 From 1652df1533e2ae04f8673f304a782ef97cc5c852 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 21:58:06 +0800 Subject: [PATCH 35/65] =?UTF-8?q?=E7=97=85=E7=8C=AA=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/postgres.go | 2 ++ internal/infra/models/models.go | 1 + internal/infra/models/pig_batch.go | 48 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 0dfc003..5f213d0 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -168,6 +168,7 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.WeighingBatch{}, "weighing_time"}, {models.WeighingRecord{}, "weighing_time"}, {models.PigTransferLog{}, "transfer_time"}, + {models.PigBatchSickPigLog{}, "happened_at"}, } for _, table := range tablesToConvert { @@ -205,6 +206,7 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { {models.WeighingBatch{}, "pig_batch_id"}, {models.WeighingRecord{}, "weighing_batch_id"}, {models.PigTransferLog{}, "pig_batch_id"}, + {models.PigBatchSickPigLog{}, "pig_batch_id"}, } for _, policy := range policies { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 6b4296a..6f10dbc 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -42,6 +42,7 @@ func GetAllModels() []interface{} { &WeighingBatch{}, &WeighingRecord{}, &PigTransferLog{}, + &PigBatchSickPigLog{}, // Feed Models &RawMaterial{}, diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index c2f02ae..8357e3e 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -105,3 +105,51 @@ type WeighingRecord struct { func (WeighingRecord) TableName() string { return "weighing_records" } + +// PigBatchSickPigLogChangeType 定义了病猪变化日志的类型 +type PigBatchSickPigLogChangeType string + +const ( + SickPigChangeTypeAdd PigBatchSickPigLogChangeType = "新增" // 新增病猪 + SickPigChangeTypeRemove PigBatchSickPigLogChangeType = "移除" // 移除病猪 (康复, 死亡, 转出等) +) + +// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点 +type PigBatchSickPigTreatmentLocation string + +const ( + TreatmentLocationOnSite PigBatchSickPigTreatmentLocation = "原地治疗" + TreatmentLocationSickBay PigBatchSickPigTreatmentLocation = "病猪栏治疗" +) + +// PigBatchSickPigReasonType 定义了病猪变化的原因类型 +type PigBatchSickPigReasonType string + +const ( + SickPigReasonTypeIllness PigBatchSickPigReasonType = "患病" // 猪只患病 + SickPigReasonTypeRecovery PigBatchSickPigReasonType = "康复" // 猪只康复 + SickPigReasonTypeDeath PigBatchSickPigReasonType = "死亡" // 猪只死亡 + SickPigReasonTypeEliminate PigBatchSickPigReasonType = "淘汰" // 猪只淘汰 + SickPigReasonTypeTransferIn PigBatchSickPigReasonType = "转入" // 病猪转入当前批次 + SickPigReasonTypeTransferOut PigBatchSickPigReasonType = "转出" // 病猪转出当前批次 (例如转到其他批次或出售) + SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因 +) + +// PigBatchSickPigLog 记录了猪批次中病猪数量的变化日志 +type PigBatchSickPigLog struct { + gorm.Model + PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` + PenID uint `gorm:"not null;index;comment:所在猪圈ID"` + PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` + ChangeType PigBatchSickPigLogChangeType `gorm:"size:20;not null;comment:变化类型 (新增, 移除)"` + ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` + Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` + Remarks string `gorm:"size:255;comment:备注"` + TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` + OperatorID uint `gorm:"comment:操作员ID"` + HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` +} + +func (PigBatchSickPigLog) TableName() string { + return "pig_batch_sick_pig_logs" +} From c76c976cc84dc97fc88fdc23348a5aaf81796eda Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sun, 5 Oct 2025 22:09:25 +0800 Subject: [PATCH 36/65] =?UTF-8?q?=E4=B9=B0=E5=8D=96=E7=8C=AA=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/database/postgres.go | 4 +++ internal/infra/models/models.go | 4 +++ internal/infra/models/pig_buy_sell.go | 41 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 internal/infra/models/pig_buy_sell.go diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index 5f213d0..dff2280 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -169,6 +169,8 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.WeighingRecord{}, "weighing_time"}, {models.PigTransferLog{}, "transfer_time"}, {models.PigBatchSickPigLog{}, "happened_at"}, + {models.PigPurchase{}, "purchase_date"}, + {models.PigSale{}, "sale_date"}, } for _, table := range tablesToConvert { @@ -207,6 +209,8 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { {models.WeighingRecord{}, "weighing_batch_id"}, {models.PigTransferLog{}, "pig_batch_id"}, {models.PigBatchSickPigLog{}, "pig_batch_id"}, + {models.PigPurchase{}, "pig_batch_id"}, + {models.PigSale{}, "pig_batch_id"}, } for _, policy := range policies { diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 6f10dbc..38a319f 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -44,6 +44,10 @@ func GetAllModels() []interface{} { &PigTransferLog{}, &PigBatchSickPigLog{}, + // Pig Buy & Sell + &PigPurchase{}, + &PigSale{}, + // Feed Models &RawMaterial{}, &RawMaterialPurchase{}, diff --git a/internal/infra/models/pig_buy_sell.go b/internal/infra/models/pig_buy_sell.go new file mode 100644 index 0000000..4d4e835 --- /dev/null +++ b/internal/infra/models/pig_buy_sell.go @@ -0,0 +1,41 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// PigPurchase 记录了猪只采购信息 +type PigPurchase struct { + gorm.Model + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` + PurchaseDate time.Time `gorm:"primaryKey;comment:采购日期"` + Supplier string `gorm:"comment:供应商"` + Quantity int `gorm:"not null;comment:采购数量"` + UnitPrice float64 `gorm:"not null;comment:单价"` + TotalPrice float64 `gorm:"not null;comment:总价"` + Remarks string `gorm:"size:255;comment:备注"` + OperatorID uint `gorm:"comment:操作员ID"` +} + +func (PigPurchase) TableName() string { + return "pig_purchases" +} + +// PigSale 记录了猪只销售信息 +type PigSale struct { + gorm.Model + PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` + SaleDate time.Time `gorm:"primaryKey;comment:销售日期"` + Buyer string `gorm:"comment:购买方"` + Quantity int `gorm:"not null;comment:销售数量"` + UnitPrice float64 `gorm:"not null;comment:单价"` + TotalPrice float64 `gorm:"not null;comment:总价"` + Remarks string `gorm:"size:255;comment:备注"` + OperatorID uint `gorm:"comment:操作员ID"` +} + +func (PigSale) TableName() string { + return "pig_sales" +} From 759b31bce3a26c30f4c429a3e266e13c2db33e3f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 11:42:56 +0800 Subject: [PATCH 37/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=AD=90=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 7 ++- internal/domain/pig/pen_transfer_manager.go | 29 ++++++------ internal/domain/pig/pig_batch_service.go | 13 ++++-- internal/domain/pig/pig_trade_manager.go | 46 +++++++++++++++++++ .../models/{pig_buy_sell.go => pig_trade.go} | 0 .../infra/repository/pig_trade_repository.go | 36 +++++++++++++++ .../repository/pig_transfer_log_repository.go | 27 +++++++++++ 7 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 internal/domain/pig/pig_trade_manager.go rename internal/infra/models/{pig_buy_sell.go => pig_trade.go} (100%) create mode 100644 internal/infra/repository/pig_trade_repository.go create mode 100644 internal/infra/repository/pig_transfer_log_repository.go diff --git a/internal/core/application.go b/internal/core/application.go index f11b397..618b39a 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -76,13 +76,16 @@ func NewApplication(configPath string) (*Application, error) { pigBatchLogRepo := repository.NewGormPigBatchLogRepository(storage.GetDB()) pigFarmRepo := repository.NewGormPigFarmRepository(storage.GetDB()) pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB()) + pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB()) + pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // 初始化猪群管理领域 - penTransferManager := pig.NewPenTransferManager(pigPenRepo) - pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, penTransferManager) + pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo) + pigTradeManager := pig.NewPigTradeManager(pigTradeRepo) + pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, pigPenTransferManager, pigTradeManager) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger) diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index a277354..ed16b62 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -6,9 +6,9 @@ import ( "gorm.io/gorm" ) -// PenTransferManager 定义了与猪只位置转移相关的底层数据库操作。 +// PigPenTransferManager 定义了与猪只位置转移相关的底层数据库操作。 // 它是一个内部服务,被主服务 PigBatchService 调用。 -type PenTransferManager interface { +type PigPenTransferManager interface { // LogTransfer 在数据库中创建一条猪只迁移日志。 LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error @@ -25,39 +25,42 @@ type PenTransferManager interface { UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error } -// penTransferManager 是 PenTransferManager 接口的具体实现。 +// pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 // 它作为调栏管理器,处理底层的数据库交互。 -type penTransferManager struct { +type pigPenTransferManager struct { penRepo repository.PigPenRepository + logRepo repository.PigTransferLogRepository } -// NewPenTransferManager 是 penTransferManager 的构造函数。 -func NewPenTransferManager(penRepo repository.PigPenRepository) PenTransferManager { - return &penTransferManager{ +// NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。 +// 修改构造函数以接收 PigTransferLogRepository 依赖 +func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository) PigPenTransferManager { + return &pigPenTransferManager{ penRepo: penRepo, + logRepo: logRepo, } } // LogTransfer 实现了在数据库中创建迁移日志的逻辑。 -func (s *penTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error { - // 直接使用事务对象创建记录。 - return tx.Create(log).Error +func (s *pigPenTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error { + // 使用新的仓库接口进行操作 + return s.logRepo.CreatePigTransferLog(tx, log) } // GetPenByID 实现了获取猪栏信息的逻辑。 // 注意: 此处调用了一个假设存在的方法 GetPenByIDTx。 -func (s *penTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) { +func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) { return s.penRepo.GetPenByIDTx(tx, penID) } // GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。 // 注意: 此处调用了一个假设存在的方法 GetPensByBatchIDTx。 -func (s *penTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { +func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { return s.penRepo.GetPensByBatchIDTx(tx, batchID) } // UpdatePenFields 实现了更新猪栏字段的逻辑。 // 注意: 此处调用了一个假设存在的方法 UpdatePenFieldsTx。 -func (s *penTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { +func (s *pigPenTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { return s.penRepo.UpdatePenFieldsTx(tx, penID, updates) } diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index cdc03b7..5688971 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -19,7 +19,8 @@ type pigBatchService struct { pigBatchRepo repository.PigBatchRepository // 猪批次仓库 pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 uow repository.UnitOfWork // 工作单元,用于管理事务 - transferSvc PenTransferManager // 调栏子服务 + transferSvc PigPenTransferManager // 调栏子服务 + tradeSvc PigTradeManager // 交易子服务 } // NewPigBatchService 是 pigBatchService 的构造函数。 @@ -28,13 +29,15 @@ func NewPigBatchService( pigBatchRepo repository.PigBatchRepository, pigBatchLogRepo repository.PigBatchLogRepository, uow repository.UnitOfWork, - transferSvc PenTransferManager, + transferSvc PigPenTransferManager, + tradeSvc PigTradeManager, ) PigBatchService { return &pigBatchService{ pigBatchRepo: pigBatchRepo, pigBatchLogRepo: pigBatchLogRepo, uow: uow, transferSvc: transferSvc, + tradeSvc: tradeSvc, } } @@ -60,8 +63,8 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch ChangeCount: createdBatch.InitialCount, Reason: fmt.Sprintf("创建了新的猪批次 %s,初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount), BeforeCount: 0, // 初始创建前数量为0 - AfterCount: int(createdBatch.InitialCount), - OperatorID: 0, // 假设初始创建没有特定操作员ID,或需要从上下文传入 + AfterCount: createdBatch.InitialCount, + OperatorID: operatorID, } // 3. 记录批次日志 @@ -135,7 +138,7 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, er } // UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 -// 它通过调用底层的 PenTransferManager 来执行数据库操作,从而保持了职责的清晰。 +// 它通过调用底层的 PigPenTransferManager 来执行数据库操作,从而保持了职责的清晰。 func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { // 使用工作单元来确保操作的原子性 return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { diff --git a/internal/domain/pig/pig_trade_manager.go b/internal/domain/pig/pig_trade_manager.go new file mode 100644 index 0000000..394b411 --- /dev/null +++ b/internal/domain/pig/pig_trade_manager.go @@ -0,0 +1,46 @@ +package pig + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" // 引入基础设施层的仓库接口 + "gorm.io/gorm" +) + +// PigTradeManager 定义了与猪只交易相关的操作接口。 +// 这是一个领域服务,负责协调业务逻辑。 +type PigTradeManager interface { + // SellPig 处理卖猪的业务逻辑,通过仓库接口创建 PigSale 记录。 + SellPig(tx *gorm.DB, sale *models.PigSale) error + + // BuyPig 处理买猪的业务逻辑,通过仓库接口创建 PigPurchase 记录。 + BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error +} + +// pigTradeManager 是 PigTradeManager 接口的具体实现。 +// 它依赖于 repository.PigTradeRepository 接口来执行数据持久化操作。 +type pigTradeManager struct { + tradeRepo repository.PigTradeRepository // 依赖于基础设施层定义的仓库接口 +} + +// NewPigTradeManager 是 pigTradeManager 的构造函数。 +func NewPigTradeManager(tradeRepo repository.PigTradeRepository) PigTradeManager { + return &pigTradeManager{ + tradeRepo: tradeRepo, + } +} + +// SellPig 实现了卖猪的逻辑。 +// 它通过调用 tradeRepo 来持久化销售记录。 +func (s *pigTradeManager) SellPig(tx *gorm.DB, sale *models.PigSale) error { + // 在此处可以添加更复杂的卖猪前置校验或业务逻辑 + // 例如:检查猪只库存、更新猪只状态等。 + return s.tradeRepo.CreatePigSaleTx(tx, sale) +} + +// BuyPig 实现了买猪的逻辑。 +// 它通过调用 tradeRepo 来持久化采购记录。 +func (s *pigTradeManager) BuyPig(tx *gorm.DB, purchase *models.PigPurchase) error { + // 在此处可以添加更复杂的买猪前置校验或业务逻辑 + // 例如:检查资金、更新猪只状态等。 + return s.tradeRepo.CreatePigPurchaseTx(tx, purchase) +} diff --git a/internal/infra/models/pig_buy_sell.go b/internal/infra/models/pig_trade.go similarity index 100% rename from internal/infra/models/pig_buy_sell.go rename to internal/infra/models/pig_trade.go diff --git a/internal/infra/repository/pig_trade_repository.go b/internal/infra/repository/pig_trade_repository.go new file mode 100644 index 0000000..a1d7b68 --- /dev/null +++ b/internal/infra/repository/pig_trade_repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigTradeRepository 定义了猪只交易数据持久化的接口。 +// 领域服务通过此接口与数据层交互,实现解耦。 +type PigTradeRepository interface { + // CreatePigSaleTx 在数据库中创建一条猪只销售记录。 + CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error + + // CreatePigPurchaseTx 在数据库中创建一条猪只采购记录。 + CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error +} + +// gormPigTradeRepository 是 PigTradeRepository 接口的 GORM 实现。 +type gormPigTradeRepository struct { + db *gorm.DB +} + +// NewGormPigTradeRepository 创建一个新的 PigTradeRepository GORM 实现实例。 +func NewGormPigTradeRepository(db *gorm.DB) PigTradeRepository { + return &gormPigTradeRepository{db: db} +} + +// CreatePigSaleTx 实现了在数据库中创建猪只销售记录的逻辑。 +func (r *gormPigTradeRepository) CreatePigSaleTx(tx *gorm.DB, sale *models.PigSale) error { + return tx.Create(sale).Error +} + +// CreatePigPurchaseTx 实现了在数据库中创建猪只采购记录的逻辑。 +func (r *gormPigTradeRepository) CreatePigPurchaseTx(tx *gorm.DB, purchase *models.PigPurchase) error { + return tx.Create(purchase).Error +} diff --git a/internal/infra/repository/pig_transfer_log_repository.go b/internal/infra/repository/pig_transfer_log_repository.go new file mode 100644 index 0000000..7854717 --- /dev/null +++ b/internal/infra/repository/pig_transfer_log_repository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigTransferLogRepository 定义了猪只迁移日志数据持久化的接口。 +type PigTransferLogRepository interface { + // CreatePigTransferLog 在数据库中创建一条猪只迁移日志记录。 + CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error +} + +// gormPigTransferLogRepository 是 PigTransferLogRepository 接口的 GORM 实现。 +type gormPigTransferLogRepository struct { + db *gorm.DB +} + +// NewGormPigTransferLogRepository 创建一个新的 PigTransferLogRepository GORM 实现实例。 +func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository { + return &gormPigTransferLogRepository{db: db} +} + +// CreatePigTransferLog 实现了在数据库中创建猪只迁移日志记录的逻辑。 +func (r *gormPigTransferLogRepository) CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error { + return tx.Create(log).Error +} From 448b721af56e7ace722ce50b4515d10c7d02438a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 11:58:26 +0800 Subject: [PATCH 38/65] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=AD=98=E6=94=BE=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch_service.go | 130 ----------------- .../pig/pig_batch_service_pen_transfer.go | 138 ++++++++++++++++++ .../domain/pig/pig_batch_service_pig_trade.go | 1 + 3 files changed, 139 insertions(+), 130 deletions(-) create mode 100644 internal/domain/pig/pig_batch_service_pen_transfer.go create mode 100644 internal/domain/pig/pig_batch_service_pig_trade.go diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 5688971..7b711d4 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -7,7 +7,6 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" - "github.com/google/uuid" "gorm.io/gorm" ) @@ -236,132 +235,3 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) return nil }) } - -// --- 新增的调栏业务实现 --- - -// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 -func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error { - // 1. 生成关联ID - correlationID := uuid.New().String() - - // 2. 创建调出日志 - logOut := &models.PigTransferLog{ - TransferTime: time.Now(), - PigBatchID: fromBatchID, - PenID: fromPenID, - Quantity: -quantity, // 调出为负数 - Type: transferType, - CorrelationID: correlationID, - OperatorID: operatorID, - Remarks: remarks, - } - - // 3. 创建调入日志 - logIn := &models.PigTransferLog{ - TransferTime: time.Now(), - PigBatchID: toBatchID, - PenID: toPenID, - Quantity: quantity, // 调入为正数 - Type: transferType, - CorrelationID: correlationID, - OperatorID: operatorID, - Remarks: remarks, - } - - // 4. 调用子服务记录日志 - if err := s.transferSvc.LogTransfer(tx, logOut); err != nil { - return fmt.Errorf("记录调出日志失败: %w", err) - } - if err := s.transferSvc.LogTransfer(tx, logIn); err != nil { - return fmt.Errorf("记录调入日志失败: %w", err) - } - - return nil -} - -// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。 -func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { - if fromPenID == toPenID { - return errors.New("源猪栏和目标猪栏不能相同") - } - if quantity == 0 { - return errors.New("迁移数量不能为零") - } - - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 核心业务规则校验 - fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) - if err != nil { - return fmt.Errorf("获取源猪栏信息失败: %w", err) - } - toPen, err := s.transferSvc.GetPenByID(tx, toPenID) - if err != nil { - return fmt.Errorf("获取目标猪栏信息失败: %w", err) - } - - if fromPen.PigBatchID == nil || *fromPen.PigBatchID != batchID { - return fmt.Errorf("源猪栏 %d 不属于指定的猪群 %d", fromPenID, batchID) - } - if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID { - return fmt.Errorf("目标猪栏 %d 已被其他猪群占用", toPenID) - } - - // 2. 调用通用辅助方法执行日志记录 - err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks) - if err != nil { - return err - } - - // 3. 群内调栏,猪群总数不变 - return nil - }) -} - -// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。 -func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { - if sourceBatchID == destBatchID { - return errors.New("源猪群和目标猪群不能相同") - } - if quantity == 0 { - return errors.New("迁移数量不能为零") - } - - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 核心业务规则校验 - sourceBatch, err := s.pigBatchRepo.GetPigBatchByID(sourceBatchID) - if err != nil { - return fmt.Errorf("获取源猪群信息失败: %w", err) - } - destBatch, err := s.pigBatchRepo.GetPigBatchByID(destBatchID) - if err != nil { - return fmt.Errorf("获取目标猪群信息失败: %w", err) - } - - fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) - if err != nil { - return fmt.Errorf("获取源猪栏信息失败: %w", err) - } - if fromPen.PigBatchID == nil || *fromPen.PigBatchID != sourceBatchID { - return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID) - } - - // 2. 调用通用辅助方法执行日志记录 - err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks) - if err != nil { - return err - } - - // 3. 修改本聚合的数据(猪群总数) - sourceBatch.InitialCount -= int(quantity) - destBatch.InitialCount += int(quantity) - - if _, _, err := s.pigBatchRepo.UpdatePigBatch(sourceBatch); err != nil { - return fmt.Errorf("更新源猪群数量失败: %w", err) - } - if _, _, err := s.pigBatchRepo.UpdatePigBatch(destBatch); err != nil { - return fmt.Errorf("更新目标猪群数量失败: %w", err) - } - - return nil - }) -} diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go new file mode 100644 index 0000000..0a216b5 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -0,0 +1,138 @@ +package pig + +import ( + "errors" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 +func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error { + // 1. 生成关联ID + correlationID := uuid.New().String() + + // 2. 创建调出日志 + logOut := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: fromBatchID, + PenID: fromPenID, + Quantity: -quantity, // 调出为负数 + Type: transferType, + CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: remarks, + } + + // 3. 创建调入日志 + logIn := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: toBatchID, + PenID: toPenID, + Quantity: quantity, // 调入为正数 + Type: transferType, + CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: remarks, + } + + // 4. 调用子服务记录日志 + if err := s.transferSvc.LogTransfer(tx, logOut); err != nil { + return fmt.Errorf("记录调出日志失败: %w", err) + } + if err := s.transferSvc.LogTransfer(tx, logIn); err != nil { + return fmt.Errorf("记录调入日志失败: %w", err) + } + + return nil +} + +// TransferPigsWithinBatch 实现了同一个猪群内部的调栏业务。 +func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { + if fromPenID == toPenID { + return errors.New("源猪栏和目标猪栏不能相同") + } + if quantity == 0 { + return errors.New("迁移数量不能为零") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 核心业务规则校验 + fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) + if err != nil { + return fmt.Errorf("获取源猪栏信息失败: %w", err) + } + toPen, err := s.transferSvc.GetPenByID(tx, toPenID) + if err != nil { + return fmt.Errorf("获取目标猪栏信息失败: %w", err) + } + + if fromPen.PigBatchID == nil || *fromPen.PigBatchID != batchID { + return fmt.Errorf("源猪栏 %d 不属于指定的猪群 %d", fromPenID, batchID) + } + if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID { + return fmt.Errorf("目标猪栏 %d 已被其他猪群占用", toPenID) + } + + // 2. 调用通用辅助方法执行日志记录 + err = s.executeTransferAndLog(tx, batchID, batchID, fromPenID, toPenID, int(quantity), "群内调栏", operatorID, remarks) + if err != nil { + return err + } + + // 3. 群内调栏,猪群总数不变 + return nil + }) +} + +// TransferPigsAcrossBatches 实现了跨猪群的调栏业务。 +func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { + if sourceBatchID == destBatchID { + return errors.New("源猪群和目标猪群不能相同") + } + if quantity == 0 { + return errors.New("迁移数量不能为零") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 核心业务规则校验 + sourceBatch, err := s.pigBatchRepo.GetPigBatchByID(sourceBatchID) + if err != nil { + return fmt.Errorf("获取源猪群信息失败: %w", err) + } + destBatch, err := s.pigBatchRepo.GetPigBatchByID(destBatchID) + if err != nil { + return fmt.Errorf("获取目标猪群信息失败: %w", err) + } + + fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) + if err != nil { + return fmt.Errorf("获取源猪栏信息失败: %w", err) + } + if fromPen.PigBatchID == nil || *fromPen.PigBatchID != sourceBatchID { + return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID) + } + + // 2. 调用通用辅助方法执行日志记录 + err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks) + if err != nil { + return err + } + + // 3. 修改本聚合的数据(猪群总数) + sourceBatch.InitialCount -= int(quantity) + destBatch.InitialCount += int(quantity) + + if _, _, err := s.pigBatchRepo.UpdatePigBatch(sourceBatch); err != nil { + return fmt.Errorf("更新源猪群数量失败: %w", err) + } + if _, _, err := s.pigBatchRepo.UpdatePigBatch(destBatch); err != nil { + return fmt.Errorf("更新目标猪群数量失败: %w", err) + } + + return nil + }) +} diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go new file mode 100644 index 0000000..467de02 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -0,0 +1 @@ +package pig From c49844feea58932ec011439f6761ad995f98a45a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 14:32:52 +0800 Subject: [PATCH 39/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=E4=B8=BB=E6=9C=8D=E5=8A=A1=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 34 +++-- internal/domain/pig/pig_batch_service.go | 39 ++++++ .../domain/pig/pig_batch_service_pig_trade.go | 125 ++++++++++++++++++ internal/infra/models/pig_batch.go | 1 + .../repository/pig_batch_log_repository.go | 28 ++++ 5 files changed, 208 insertions(+), 19 deletions(-) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 53ffa8f..63a2d93 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -2,6 +2,7 @@ package pig import ( "errors" + "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" ) @@ -30,29 +31,24 @@ var ( // PigBatchService 定义了猪批次管理的核心业务逻辑接口。 // 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 type PigBatchService interface { - // TransferPigsWithinBatch 处理同一个猪群内部的调栏业务。 - TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error - - // TransferPigsAcrossBatches 处理跨猪群的调栏业务。 - TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error - - // CreatePigBatch 创建一个新的猪批次。 + // CreatePigBatch 创建猪批次,并记录初始日志。 CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) - - // GetPigBatch 根据ID获取单个猪批次的详细信息。 + // GetPigBatch 获取单个猪批次。 GetPigBatch(id uint) (*models.PigBatch, error) - - // UpdatePigBatch 更新一个已存在的猪批次信息。 + // UpdatePigBatch 更新猪批次信息。 UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) - - // DeletePigBatch 删除一个指定的猪批次。 - // 实现时需要包含业务规则校验,例如,活跃的批次不能被删除。 + // DeletePigBatch 删除猪批次,包含业务规则校验。 DeletePigBatch(id uint) error - - // ListPigBatches 根据是否活跃的状态,列出所有符合条件的猪批次。 + // ListPigBatches 批量查询猪批次。 ListPigBatches(isActive *bool) ([]*models.PigBatch, error) - - // UpdatePigBatchPens 负责原子性地更新一个猪批次所关联的所有猪栏。 - // 它会处理猪栏的添加、移除,并确保数据的一致性。 + // UpdatePigBatchPens 更新猪批次关联的猪栏。 UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error + + // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 + GetCurrentPigQuantity(batchID uint) (int, error) + + // SellPigs 处理卖猪的业务逻辑。 + SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + // BuyPigs 处理买猪的业务逻辑。 + BuyPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error } diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 7b711d4..110c0cb 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -235,3 +235,42 @@ func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) return nil }) } + +// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。 +func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) { + var getErr error + var quantity int + err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID) + return getErr + }) + if err != nil { + return 0, err + } + return quantity, nil +} + +// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。 +func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) { + // 1. 获取猪批次初始信息 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, ErrPigBatchNotFound + } + return 0, fmt.Errorf("获取猪批次 %d 初始信息失败: %w", batchID, err) + } + + // 2. 尝试获取该批次的最后一条日志记录 + lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量 + return batch.InitialCount, nil + } + return 0, fmt.Errorf("获取猪批次 %d 最后一条日志失败: %w", batchID, err) + } + + // 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount + return lastLog.AfterCount, nil +} diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go index 467de02..48603e1 100644 --- a/internal/domain/pig/pig_batch_service_pig_trade.go +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -1 +1,126 @@ package pig + +import ( + "errors" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// SellPigs 处理批量销售猪的业务逻辑。 +func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + if quantity <= 0 { + return errors.New("销售数量必须大于0") + } + + // 1. 获取猪批次信息 + _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + } + + // 2. 业务校验:检查销售数量是否超过当前批次数量 + currentQuantity, err := s.getCurrentPigQuantityTx(tx, batchID) + if err != nil { + return fmt.Errorf("获取猪批次 %d 当前数量失败: %w", batchID, err) + } + + if quantity > currentQuantity { + return fmt.Errorf("销售数量 %d 超过当前批次 %d 数量 %d", quantity, batchID, currentQuantity) + } + + // 3. 记录销售交易 + sale := &models.PigSale{ + PigBatchID: batchID, + SaleDate: tradeDate, + Buyer: traderName, + Quantity: quantity, + UnitPrice: unitPrice, + TotalPrice: tatalPrice, // 总价不一定是单价x数量, 所以要传进来 + Remarks: remarks, + OperatorID: operatorID, + } + if err := s.tradeSvc.SellPig(tx, sale); err != nil { + return fmt.Errorf("记录销售交易失败: %w", err) + } + + // 4. 记录批次日志 + log := &models.PigBatchLog{ + PigBatchID: batchID, + HappenedAt: time.Now(), + ChangeType: models.ChangeTypeSale, + ChangeCount: -quantity, + Reason: fmt.Sprintf("猪批次 %d 销售 %d 头猪给 %s", batchID, quantity, traderName), + BeforeCount: currentQuantity, + AfterCount: currentQuantity - quantity, + OperatorID: operatorID, + } + if err := s.pigBatchLogRepo.Create(tx, log); err != nil { + return fmt.Errorf("记录销售批次日志失败: %w", err) + } + + return nil + }) +} + +// BuyPigs 处理批量购买猪的业务逻辑。 +func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + if quantity <= 0 { + return errors.New("采购数量必须大于0") + } + + // 1. 获取猪批次信息 + _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + } + + // 2. 获取当前猪批次数量 + currentQuantity, err := s.getCurrentPigQuantityTx(tx, batchID) + if err != nil { + return fmt.Errorf("获取猪批次 %d 当前数量失败: %w", batchID, err) + } + + // 3. 记录采购交易 + purchase := &models.PigPurchase{ + PigBatchID: batchID, + PurchaseDate: tradeDate, + Supplier: traderName, + Quantity: quantity, + UnitPrice: unitPrice, + TotalPrice: totalPrice, // 总价不一定是单价x数量, 所以要传进来 + Remarks: remarks, + OperatorID: operatorID, + } + if err := s.tradeSvc.BuyPig(tx, purchase); err != nil { + return fmt.Errorf("记录采购交易失败: %w", err) + } + + // 4. 记录批次日志 + log := &models.PigBatchLog{ + PigBatchID: batchID, + HappenedAt: time.Now(), + ChangeType: models.ChangeTypeBuy, + ChangeCount: quantity, + Reason: fmt.Sprintf("猪批次 %d 采购 %d 头猪从 %s", batchID, quantity, traderName), + BeforeCount: currentQuantity, + AfterCount: currentQuantity + quantity, + OperatorID: operatorID, + } + if err := s.pigBatchLogRepo.Create(tx, log); err != nil { + return fmt.Errorf("记录采购批次日志失败: %w", err) + } + + return nil + }) +} diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index 8357e3e..db17bd3 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -57,6 +57,7 @@ const ( ChangeTypeDeath LogChangeType = "死亡" ChangeTypeCull LogChangeType = "淘汰" ChangeTypeSale LogChangeType = "销售" + ChangeTypeBuy LogChangeType = "购买" ChangeTypeTransferIn LogChangeType = "转入" ChangeTypeTransferOut LogChangeType = "转出" ChangeTypeCorrection LogChangeType = "盘点校正" diff --git a/internal/infra/repository/pig_batch_log_repository.go b/internal/infra/repository/pig_batch_log_repository.go index aaed13d..283731f 100644 --- a/internal/infra/repository/pig_batch_log_repository.go +++ b/internal/infra/repository/pig_batch_log_repository.go @@ -1,6 +1,8 @@ package repository import ( + "time" // 引入 time 包 + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) @@ -9,6 +11,12 @@ import ( type PigBatchLogRepository interface { // Create 在指定的事务中创建一条新的猪批次日志。 Create(tx *gorm.DB, log *models.PigBatchLog) error + + // GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。 + GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) + + // GetLastLogByBatchIDTx 在指定的事务中,获取某批次的最后一条日志记录。 + GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) } // gormPigBatchLogRepository 是 PigBatchLogRepository 的 GORM 实现。 @@ -25,3 +33,23 @@ func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository { func (r *gormPigBatchLogRepository) Create(tx *gorm.DB, log *models.PigBatchLog) error { return tx.Create(log).Error } + +// GetLogsByBatchIDAndDateRangeTx 实现了在指定的事务中,获取指定批次在特定时间范围内的所有日志记录的逻辑。 +func (r *gormPigBatchLogRepository) GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) { + var logs []*models.PigBatchLog + err := tx.Where("pig_batch_id = ? AND created_at >= ? AND created_at <= ?", batchID, startDate, endDate).Find(&logs).Error + if err != nil { + return nil, err + } + return logs, nil +} + +// GetLastLogByBatchIDTx 实现了在指定的事务中,获取某批次的最后一条日志记录的逻辑。 +func (r *gormPigBatchLogRepository) GetLastLogByBatchIDTx(tx *gorm.DB, batchID uint) (*models.PigBatchLog, error) { + var log models.PigBatchLog + err := tx.Where("pig_batch_id = ?", batchID).Order("id DESC").First(&log).Error + if err != nil { + return nil, err + } + return &log, nil +} From 59b69773675bcf38137df7b94ebcfd8498337a7e Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 14:48:47 +0800 Subject: [PATCH 40/65] =?UTF-8?q?=E8=B0=83=E6=95=B4model=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/models/pig_batch.go | 48 ------------------------------ internal/infra/models/pig_sick.go | 46 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 48 deletions(-) create mode 100644 internal/infra/models/pig_sick.go diff --git a/internal/infra/models/pig_batch.go b/internal/infra/models/pig_batch.go index db17bd3..658c31b 100644 --- a/internal/infra/models/pig_batch.go +++ b/internal/infra/models/pig_batch.go @@ -106,51 +106,3 @@ type WeighingRecord struct { func (WeighingRecord) TableName() string { return "weighing_records" } - -// PigBatchSickPigLogChangeType 定义了病猪变化日志的类型 -type PigBatchSickPigLogChangeType string - -const ( - SickPigChangeTypeAdd PigBatchSickPigLogChangeType = "新增" // 新增病猪 - SickPigChangeTypeRemove PigBatchSickPigLogChangeType = "移除" // 移除病猪 (康复, 死亡, 转出等) -) - -// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点 -type PigBatchSickPigTreatmentLocation string - -const ( - TreatmentLocationOnSite PigBatchSickPigTreatmentLocation = "原地治疗" - TreatmentLocationSickBay PigBatchSickPigTreatmentLocation = "病猪栏治疗" -) - -// PigBatchSickPigReasonType 定义了病猪变化的原因类型 -type PigBatchSickPigReasonType string - -const ( - SickPigReasonTypeIllness PigBatchSickPigReasonType = "患病" // 猪只患病 - SickPigReasonTypeRecovery PigBatchSickPigReasonType = "康复" // 猪只康复 - SickPigReasonTypeDeath PigBatchSickPigReasonType = "死亡" // 猪只死亡 - SickPigReasonTypeEliminate PigBatchSickPigReasonType = "淘汰" // 猪只淘汰 - SickPigReasonTypeTransferIn PigBatchSickPigReasonType = "转入" // 病猪转入当前批次 - SickPigReasonTypeTransferOut PigBatchSickPigReasonType = "转出" // 病猪转出当前批次 (例如转到其他批次或出售) - SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因 -) - -// PigBatchSickPigLog 记录了猪批次中病猪数量的变化日志 -type PigBatchSickPigLog struct { - gorm.Model - PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` - PenID uint `gorm:"not null;index;comment:所在猪圈ID"` - PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` - ChangeType PigBatchSickPigLogChangeType `gorm:"size:20;not null;comment:变化类型 (新增, 移除)"` - ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` - Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` - Remarks string `gorm:"size:255;comment:备注"` - TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` - OperatorID uint `gorm:"comment:操作员ID"` - HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` -} - -func (PigBatchSickPigLog) TableName() string { - return "pig_batch_sick_pig_logs" -} diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go new file mode 100644 index 0000000..73eed01 --- /dev/null +++ b/internal/infra/models/pig_sick.go @@ -0,0 +1,46 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// PigBatchSickPigTreatmentLocation 定义了病猪治疗地点 +type PigBatchSickPigTreatmentLocation string + +const ( + TreatmentLocationOnSite PigBatchSickPigTreatmentLocation = "原地治疗" + TreatmentLocationSickBay PigBatchSickPigTreatmentLocation = "病猪栏治疗" +) + +// PigBatchSickPigReasonType 定义了病猪变化的原因类型 +type PigBatchSickPigReasonType string + +const ( + SickPigReasonTypeIllness PigBatchSickPigReasonType = "患病" // 猪只患病 + SickPigReasonTypeRecovery PigBatchSickPigReasonType = "康复" // 猪只康复 + SickPigReasonTypeDeath PigBatchSickPigReasonType = "死亡" // 猪只死亡 + SickPigReasonTypeEliminate PigBatchSickPigReasonType = "淘汰" // 猪只淘汰 + SickPigReasonTypeTransferIn PigBatchSickPigReasonType = "转入" // 病猪转入当前批次 + SickPigReasonTypeTransferOut PigBatchSickPigReasonType = "转出" // 病猪转出当前批次 (例如转到其他批次或出售) + SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因 +) + +// PigBatchSickPigLog 记录了猪批次中病猪数量的变化日志 +type PigBatchSickPigLog struct { + gorm.Model + PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` + PenID uint `gorm:"not null;index;comment:所在猪圈ID"` + PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` + ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` + Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` + Remarks string `gorm:"size:255;comment:备注"` + TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` + OperatorID uint `gorm:"comment:操作员ID"` + HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` +} + +func (PigBatchSickPigLog) TableName() string { + return "pig_batch_sick_pig_logs" +} From 91e18c432cf3511a2f9638b361f03d9a1a57b988 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 15:08:32 +0800 Subject: [PATCH 41/65] =?UTF-8?q?=E6=8F=90=E5=8F=96=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=8C=AA=E7=BE=A4=E6=95=B0=E9=87=8F=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/device_controller_test.go | 12 +++--- .../controller/user/user_controller_test.go | 18 ++++----- internal/domain/pig/pig_batch.go | 4 ++ internal/domain/pig/pig_batch_service.go | 32 +++++++++++++++- .../domain/pig/pig_batch_service_pig_trade.go | 38 ++++--------------- .../repository/execution_log_repository.go | 2 +- .../repository/pig_batch_log_repository.go | 6 +-- .../infra/repository/plan_repository_test.go | 8 ++-- 8 files changed, 66 insertions(+), 54 deletions(-) diff --git a/internal/app/controller/device/device_controller_test.go b/internal/app/controller/device/device_controller_test.go index e943533..7bb1239 100644 --- a/internal/app/controller/device/device_controller_test.go +++ b/internal/app/controller/device/device_controller_test.go @@ -26,7 +26,7 @@ type MockDeviceRepository struct { mock.Mock } -// Create 模拟 DeviceRepository 的 Create 方法 +// CreateTx 模拟 DeviceRepository 的 CreateTx 方法 func (m *MockDeviceRepository) Create(device *models.Device) error { args := m.Called(device) return args.Error(0) @@ -169,7 +169,7 @@ func TestCreateDevice(t *testing.T) { Properties: controller.Properties(`{"lora_address":"0x1234"}`), }, mockRepoSetup: func(m *MockDeviceRepository) { - m.On("Create", mock.MatchedBy(func(dev *models.Device) bool { + m.On("CreateTx", mock.MatchedBy(func(dev *models.Device) bool { // 检查 Name 字段 nameMatch := dev.Name == "主控A" // 检查 Type 字段 @@ -215,7 +215,7 @@ func TestCreateDevice(t *testing.T) { Properties: controller.Properties(`{"bus_id":1,"bus_address":10}`), }, mockRepoSetup: func(m *MockDeviceRepository) { - m.On("Create", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + m.On("CreateTx", mock.Anything).Return(nil).Run(func(args mock.Arguments) { arg := args.Get(0).(*models.Device) arg.ID = 2 arg.CreatedAt = time.Now() @@ -259,7 +259,7 @@ func TestCreateDevice(t *testing.T) { Type: models.DeviceTypeDevice, }, mockRepoSetup: func(m *MockDeviceRepository) { - m.On("Create", mock.Anything).Return(errors.New("db error")).Once() + m.On("CreateTx", mock.Anything).Return(errors.New("db error")).Once() }, expectedStatus: http.StatusOK, expectedCode: controller.CodeInternalError, @@ -276,9 +276,9 @@ func TestCreateDevice(t *testing.T) { Properties: controller.Properties(`{invalid json}`), }, mockRepoSetup: func(m *MockDeviceRepository) { - // 期望 Create 方法被调用,并返回一个模拟的数据库错误 + // 期望 CreateTx 方法被调用,并返回一个模拟的数据库错误 // 这个错误模拟的是数据库层因为 Properties 字段的 JSON 格式无效而拒绝保存 - m.On("Create", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) { + m.On("CreateTx", mock.Anything).Return(errors.New("database error: invalid json format")).Run(func(args mock.Arguments) { dev := args.Get(0).(*models.Device) assert.Equal(t, "无效JSON设备", dev.Name) assert.Equal(t, models.DeviceTypeDevice, dev.Type) diff --git a/internal/app/controller/user/user_controller_test.go b/internal/app/controller/user/user_controller_test.go index 2cd004c..c447f11 100644 --- a/internal/app/controller/user/user_controller_test.go +++ b/internal/app/controller/user/user_controller_test.go @@ -25,7 +25,7 @@ type MockUserRepository struct { mock.Mock } -// Create 模拟 UserRepository 的 Create 方法 +// CreateTx 模拟 UserRepository 的 CreateTx 方法 func (m *MockUserRepository) Create(user *models.User) error { args := m.Called(user) return args.Error(0) @@ -90,8 +90,8 @@ func TestCreateUser(t *testing.T) { Password: "password123", }, mockRepoSetup: func(m *MockUserRepository) { - // 模拟 Create 成功 - m.On("Create", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) { + // 模拟 CreateTx 成功 + m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(nil).Run(func(args mock.Arguments) { // 模拟数据库自动填充 ID userArg := args.Get(0).(*models.User) userArg.ID = 1 // 设置一个非零的 ID @@ -114,7 +114,7 @@ func TestCreateUser(t *testing.T) { Password: "123", // 密码少于6位 }, mockRepoSetup: func(m *MockUserRepository) { - // 不会调用 Create 或 FindByUsername + // 不会调用 CreateTx 或 FindByUsername }, expectedResponse: map[string]interface{}{ "code": float64(controller.CodeBadRequest), @@ -128,7 +128,7 @@ func TestCreateUser(t *testing.T) { Password: "password123", }, mockRepoSetup: func(m *MockUserRepository) { - // 不会调用 Create 或 FindByUsername + // 不会调用 CreateTx 或 FindByUsername }, expectedResponse: map[string]interface{}{ "code": float64(controller.CodeBadRequest), @@ -143,8 +143,8 @@ func TestCreateUser(t *testing.T) { Password: "password123", }, mockRepoSetup: func(m *MockUserRepository) { - // 模拟 Create 失败,因为用户名已存在 - m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once() + // 模拟 CreateTx 失败,因为用户名已存在 + m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("duplicate entry")).Once() // 模拟 FindByUsername 找到用户,确认是用户名重复 m.On("FindByUsername", "existinguser").Return(&models.User{Username: "existinguser"}, nil).Once() }, @@ -161,8 +161,8 @@ func TestCreateUser(t *testing.T) { Password: "password123", }, mockRepoSetup: func(m *MockUserRepository) { - // 模拟 Create 失败,通用数据库错误 - m.On("Create", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once() + // 模拟 CreateTx 失败,通用数据库错误 + m.On("CreateTx", mock.AnythingOfType("*models.User")).Return(errors.New("database error")).Once() // 模拟 FindByUsername 找不到用户,确认不是用户名重复 m.On("FindByUsername", "db_error_user").Return(nil, gorm.ErrRecordNotFound).Once() }, diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 63a2d93..2d56781 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -24,6 +24,8 @@ var ( ErrPenNotFound = errors.New("指定的猪栏不存在") // ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联 ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") + // ErrInvalidOperation 非法操作 + ErrInvalidOperation = errors.New("非法操作") ) // --- 领域服务接口 --- @@ -51,4 +53,6 @@ type PigBatchService interface { SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error // BuyPigs 处理买猪的业务逻辑。 BuyPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + + UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error } diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 110c0cb..65b0e07 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -67,7 +67,7 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch } // 3. 记录批次日志 - if err := s.pigBatchLogRepo.Create(tx, initialLog); err != nil { + if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil { return fmt.Errorf("记录初始批次日志失败: %w", err) } @@ -274,3 +274,33 @@ func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (in // 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount return lastLog.AfterCount, nil } + +func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt) + }) +} + +func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { + lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) + if err != nil { + return err + } + // 检查数量不应该减到小于零 + if changeAmount < 0 { + if lastLog.AfterCount+changeAmount < 0 { + return ErrInvalidOperation + } + } + pigBatchLog := &models.PigBatchLog{ + PigBatchID: batchID, + ChangeType: changeType, + ChangeCount: changeAmount, + Reason: changeReason, + BeforeCount: lastLog.AfterCount, + AfterCount: lastLog.AfterCount + changeAmount, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog) +} diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go index 48603e1..de327a0 100644 --- a/internal/domain/pig/pig_batch_service_pig_trade.go +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -51,18 +51,10 @@ func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64 } // 4. 记录批次日志 - log := &models.PigBatchLog{ - PigBatchID: batchID, - HappenedAt: time.Now(), - ChangeType: models.ChangeTypeSale, - ChangeCount: -quantity, - Reason: fmt.Sprintf("猪批次 %d 销售 %d 头猪给 %s", batchID, quantity, traderName), - BeforeCount: currentQuantity, - AfterCount: currentQuantity - quantity, - OperatorID: operatorID, - } - if err := s.pigBatchLogRepo.Create(tx, log); err != nil { - return fmt.Errorf("记录销售批次日志失败: %w", err) + if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeSale, -quantity, + fmt.Sprintf("猪批次 %d 销售 %d 头猪给 %s", batchID, quantity, traderName), + tradeDate); err != nil { + return fmt.Errorf("更新猪批次数量失败: %w", err) } return nil @@ -85,12 +77,6 @@ func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) } - // 2. 获取当前猪批次数量 - currentQuantity, err := s.getCurrentPigQuantityTx(tx, batchID) - if err != nil { - return fmt.Errorf("获取猪批次 %d 当前数量失败: %w", batchID, err) - } - // 3. 记录采购交易 purchase := &models.PigPurchase{ PigBatchID: batchID, @@ -107,18 +93,10 @@ func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, } // 4. 记录批次日志 - log := &models.PigBatchLog{ - PigBatchID: batchID, - HappenedAt: time.Now(), - ChangeType: models.ChangeTypeBuy, - ChangeCount: quantity, - Reason: fmt.Sprintf("猪批次 %d 采购 %d 头猪从 %s", batchID, quantity, traderName), - BeforeCount: currentQuantity, - AfterCount: currentQuantity + quantity, - OperatorID: operatorID, - } - if err := s.pigBatchLogRepo.Create(tx, log); err != nil { - return fmt.Errorf("记录采购批次日志失败: %w", err) + if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeBuy, quantity, + fmt.Sprintf("猪批次 %d 采购 %d 头猪从 %s", batchID, quantity, traderName), + tradeDate); err != nil { + return fmt.Errorf("更新猪批次数量失败: %w", err) } return nil diff --git a/internal/infra/repository/execution_log_repository.go b/internal/infra/repository/execution_log_repository.go index d804247..33323e6 100644 --- a/internal/infra/repository/execution_log_repository.go +++ b/internal/infra/repository/execution_log_repository.go @@ -96,7 +96,7 @@ func (r *gormExecutionLogRepository) CreateTaskExecutionLogsInBatch(logs []*mode if len(logs) == 0 { return nil } - // GORM 的 Create 传入一个切片指针会执行批量插入。 + // GORM 的 CreateTx 传入一个切片指针会执行批量插入。 return r.db.Create(&logs).Error } diff --git a/internal/infra/repository/pig_batch_log_repository.go b/internal/infra/repository/pig_batch_log_repository.go index 283731f..34b4b59 100644 --- a/internal/infra/repository/pig_batch_log_repository.go +++ b/internal/infra/repository/pig_batch_log_repository.go @@ -9,8 +9,8 @@ import ( // PigBatchLogRepository 定义了与猪批次日志相关的数据库操作接口。 type PigBatchLogRepository interface { - // Create 在指定的事务中创建一条新的猪批次日志。 - Create(tx *gorm.DB, log *models.PigBatchLog) error + // CreateTx 在指定的事务中创建一条新的猪批次日志。 + CreateTx(tx *gorm.DB, log *models.PigBatchLog) error // GetLogsByBatchIDAndDateRangeTx 在指定的事务中,获取指定批次在特定时间范围内的所有日志记录。 GetLogsByBatchIDAndDateRangeTx(tx *gorm.DB, batchID uint, startDate, endDate time.Time) ([]*models.PigBatchLog, error) @@ -30,7 +30,7 @@ func NewGormPigBatchLogRepository(db *gorm.DB) PigBatchLogRepository { } // Create 实现了创建猪批次日志的逻辑。 -func (r *gormPigBatchLogRepository) Create(tx *gorm.DB, log *models.PigBatchLog) error { +func (r *gormPigBatchLogRepository) CreateTx(tx *gorm.DB, log *models.PigBatchLog) error { return tx.Create(log).Error } diff --git a/internal/infra/repository/plan_repository_test.go b/internal/infra/repository/plan_repository_test.go index a7b5389..ab511be 100644 --- a/internal/infra/repository/plan_repository_test.go +++ b/internal/infra/repository/plan_repository_test.go @@ -870,7 +870,7 @@ func TestPlanRepository_Create(t *testing.T) { type testCase struct { name string setupDB func(db *gorm.DB) // 准备数据库的初始状态 - inputPlan *models.Plan // 传入 Create 方法的计划对象 + inputPlan *models.Plan // 传入 CreateTx 方法的计划对象 expectedError error // 期望的错误类型 verifyDB func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) // 验证数据库状态 } @@ -1040,7 +1040,7 @@ func TestPlanRepository_Create(t *testing.T) { {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 }, }, - expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 + expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 CreateTx 方法会返回此错误 verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { var count int64 db.Model(&models.Plan{}).Where("name = ?", "重复任务顺序计划").Count(&count) @@ -1061,7 +1061,7 @@ func TestPlanRepository_Create(t *testing.T) { {ChildPlanID: 2, ExecutionOrder: 1}, // 重复的顺序 }, }, - expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 Create 方法会返回此错误 + expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 CreateTx 方法会返回此错误 verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { var count int64 db.Model(&models.Plan{}).Where("name = ?", "重复子计划顺序计划").Count(&count) @@ -1078,7 +1078,7 @@ func TestPlanRepository_Create(t *testing.T) { // 准备数据库状态 tc.setupDB(db) - // 执行 Create 操作 + // 执行 CreateTx 操作 err := repo.CreatePlan(tc.inputPlan) // 断言错误 From 1b026d6106fe92d61c7808f21c5c443afcca7e23 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 15:26:44 +0800 Subject: [PATCH 42/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=97=85=E7=8C=AA?= =?UTF-8?q?=E7=94=A8=E8=8D=AF=E4=B8=A4=E4=B8=AArepo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 2 + internal/domain/pig/pig_sick_manager.go | 1 + internal/infra/models/pig_sick.go | 10 +- .../group_medication_log_repository.go | 26 + .../infra/repository/pig_sick_repository.go | 26 + .../infra/repository/plan_repository_test.go | 1225 ----------------- 6 files changed, 61 insertions(+), 1229 deletions(-) create mode 100644 internal/domain/pig/pig_sick_manager.go create mode 100644 internal/infra/repository/group_medication_log_repository.go create mode 100644 internal/infra/repository/pig_sick_repository.go delete mode 100644 internal/infra/repository/plan_repository_test.go diff --git a/internal/core/application.go b/internal/core/application.go index 618b39a..9dcd5f3 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -78,6 +78,8 @@ func NewApplication(configPath string) (*Application, error) { pigPenRepo := repository.NewGormPigPenRepository(storage.GetDB()) pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB()) pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB()) + pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB()) + pigGroupMedicationLogRepo := repository.NewGormGroupMedicationLogRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go new file mode 100644 index 0000000..467de02 --- /dev/null +++ b/internal/domain/pig/pig_sick_manager.go @@ -0,0 +1 @@ +package pig diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go index 73eed01..9b8ef3f 100644 --- a/internal/infra/models/pig_sick.go +++ b/internal/infra/models/pig_sick.go @@ -27,20 +27,22 @@ const ( SickPigReasonTypeOther PigBatchSickPigReasonType = "其他" // 其他原因 ) -// PigBatchSickPigLog 记录了猪批次中病猪数量的变化日志 -type PigBatchSickPigLog struct { +// PigSickLog 记录了猪批次中病猪数量的变化日志 +type PigSickLog struct { gorm.Model PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` PenID uint `gorm:"not null;index;comment:所在猪圈ID"` PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` + BeforeCount int `gorm:"comment:变化前的数量"` + AfterCount int `gorm:"comment:变化后的数量"` Remarks string `gorm:"size:255;comment:备注"` TreatmentLocation PigBatchSickPigTreatmentLocation `gorm:"size:50;comment:治疗地点"` OperatorID uint `gorm:"comment:操作员ID"` HappenedAt time.Time `gorm:"primaryKey;comment:事件发生时间"` } -func (PigBatchSickPigLog) TableName() string { - return "pig_batch_sick_pig_logs" +func (PigSickLog) TableName() string { + return "pig_sick_logs" } diff --git a/internal/infra/repository/group_medication_log_repository.go b/internal/infra/repository/group_medication_log_repository.go new file mode 100644 index 0000000..4b5be01 --- /dev/null +++ b/internal/infra/repository/group_medication_log_repository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// GroupMedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。 +type GroupMedicationLogRepository interface { + CreateGroupMedicationLog(log *models.GroupMedicationLog) error +} + +// gormGroupMedicationLogRepository 是 GroupMedicationLogRepository 接口的 GORM 实现。 +type gormGroupMedicationLogRepository struct { + db *gorm.DB +} + +// NewGormGroupMedicationLogRepository 创建一个新的 GroupMedicationLogRepository GORM 实现实例。 +func NewGormGroupMedicationLogRepository(db *gorm.DB) GroupMedicationLogRepository { + return &gormGroupMedicationLogRepository{db: db} +} + +// CreateGroupMedicationLog 创建一条新的群体用药日志记录 +func (r *gormGroupMedicationLogRepository) CreateGroupMedicationLog(log *models.GroupMedicationLog) error { + return r.db.Create(log).Error +} diff --git a/internal/infra/repository/pig_sick_repository.go b/internal/infra/repository/pig_sick_repository.go new file mode 100644 index 0000000..c2ff29d --- /dev/null +++ b/internal/infra/repository/pig_sick_repository.go @@ -0,0 +1,26 @@ +package repository + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。 +type PigSickLogRepository interface { + CreatePigSickLog(log *models.PigSickLog) error +} + +// gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。 +type gormPigSickLogRepository struct { + db *gorm.DB +} + +// NewGormPigSickLogRepository 创建一个新的 PigSickLogRepository GORM 实现实例。 +func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository { + return &gormPigSickLogRepository{db: db} +} + +// CreatePigSickLog 创建一条新的病猪日志记录 +func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error { + return r.db.Create(log).Error +} diff --git a/internal/infra/repository/plan_repository_test.go b/internal/infra/repository/plan_repository_test.go deleted file mode 100644 index ab511be..0000000 --- a/internal/infra/repository/plan_repository_test.go +++ /dev/null @@ -1,1225 +0,0 @@ -package repository_test - -import ( - "errors" - "fmt" - "testing" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" - "github.com/stretchr/testify/assert" - "gorm.io/gorm" -) - -// createTestPlan 是一个辅助函数,用于创建测试计划。 -func createTestPlan(name, description string, execType models.PlanExecutionType, contentType models.PlanContentType) models.Plan { - return models.Plan{ - Name: name, - Description: description, - ExecutionType: execType, - ContentType: contentType, - } -} - -// TestListBasicPlans 测试 ListBasicPlans 方法,确保它能正确返回所有计划的基本信息。 -func TestListBasicPlans(t *testing.T) { - tests := []struct { - name string - setupPlans []models.Plan - expectedCount int - expectedError error - }{ - { - name: "数据库中没有计划", - setupPlans: []models.Plan{}, - expectedCount: 0, - expectedError: nil, - }, - { - name: "数据库中有多个计划", - setupPlans: []models.Plan{ - createTestPlan("计划 A", "描述 A", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - createTestPlan("计划 B", "描述 B", models.PlanExecutionTypeManual, models.PlanContentTypeSubPlans), - }, - expectedCount: 2, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - for i := range tt.setupPlans { - err := db.Create(&tt.setupPlans[i]).Error - assert.NoError(t, err, "插入设置计划失败") - } - - plans, err := repo.ListBasicPlans() - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError)) - } else { - assert.NoError(t, err) - assert.Len(t, plans, tt.expectedCount) - if tt.expectedCount > 0 { - // 验证返回的计划是否包含预期的ID - var actualIDs []uint - for _, p := range plans { - actualIDs = append(actualIDs, p.ID) - assert.Empty(t, p.SubPlans, "ListBasicPlans 不应加载子计划") - assert.Empty(t, p.Tasks, "ListBasicPlans 不应加载任务") - } - for _, setupPlan := range tt.setupPlans { - assert.Contains(t, actualIDs, setupPlan.ID, "返回的计划应包含设置计划的ID") - } - } - } - }) - } -} - -// TestGetBasicPlanByID 测试 GetBasicPlanByID 方法,确保它能根据ID正确返回计划的基本信息。 -func TestGetBasicPlanByID(t *testing.T) { - tests := []struct { - name string - setupPlan models.Plan - idToFetch uint - expectFound bool - expectedError error - }{ - { - name: "通过ID找到计划", - setupPlan: createTestPlan("计划 C", "描述 C", models.PlanExecutionTypeAutomatic, models.PlanContentTypeTasks), - idToFetch: 0, // 创建后设置 - expectFound: true, - }, - { - name: "通过ID未找到计划", - setupPlan: models.Plan{}, // 此情况下无需设置计划 - idToFetch: 999, - expectFound: false, - expectedError: gorm.ErrRecordNotFound, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - if tt.setupPlan.Name != "" { // 仅在有效设置时创建计划 - err := db.Create(&tt.setupPlan).Error - assert.NoError(t, err, "插入设置计划失败") - tt.idToFetch = tt.setupPlan.ID // 使用数据库生成的ID - } - - fetchedPlan, err := repo.GetBasicPlanByID(tt.idToFetch) - - if tt.expectedError != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tt.expectedError), "预期错误类型不匹配") - assert.Nil(t, fetchedPlan) - } else { - assert.NoError(t, err) - assert.NotNil(t, fetchedPlan) - assert.Equal(t, tt.setupPlan.Name, fetchedPlan.Name) - assert.Equal(t, tt.setupPlan.Description, fetchedPlan.Description) - assert.Equal(t, tt.setupPlan.ExecutionType, fetchedPlan.ExecutionType) - assert.Equal(t, tt.setupPlan.ContentType, fetchedPlan.ContentType) - assert.Empty(t, fetchedPlan.SubPlans, "GetBasicPlanByID 不应加载子计划") - assert.Empty(t, fetchedPlan.Tasks, "GetBasicPlanByID 不应加载任务") - } - }) - } -} - -// TestGetPlanByID 测试 GetPlanByID 方法,确保它能根据ID正确返回计划的完整信息,包括关联数据。 -func TestGetPlanByID(t *testing.T) { - type testCase struct { - name string - setupData func(db *gorm.DB) // 用于在测试前插入数据 - planID uint - expectedPlan *models.Plan - expectedError string - } - - testCases := []testCase{ - { - name: "PlanNotFound", - setupData: func(db *gorm.DB) { - // 不插入任何数据 - }, - planID: 999, - expectedPlan: nil, - expectedError: "record not found", - }, - { - name: "PlanWithTasks", - setupData: func(db *gorm.DB) { - // 使用硬编码的ID创建计划,使测试可预测 - plan := models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - } - db.Create(&plan) - // 创建任务,它们的ID将由数据库自动生成 - db.Create(&models.Task{PlanID: 1, Name: "Task 2", ExecutionOrder: 1}) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 2}) - }, - planID: 1, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan A", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // 期望按 "order" 字段升序排序 - {PlanID: 1, Name: "Task 2", ExecutionOrder: 1}, - {PlanID: 1, Name: "Task 1", ExecutionOrder: 2}, - }, - }, - expectedError: "", - }, - { - name: "PlanWithMultiLevelSubPlans", - setupData: func(db *gorm.DB) { - // 创建一个三层结构的计划 - db.Create(&models.Plan{Model: gorm.Model{ID: 20}, Name: "Grandparent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 21}, Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 22}, Name: "Child Plan With Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}) - - // 创建关联关系 - db.Create(&models.SubPlan{ParentPlanID: 20, ChildPlanID: 21, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 21, ChildPlanID: 22, ExecutionOrder: 1}) - }, - planID: 20, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 20}, - Name: "Grandparent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 20, - ChildPlanID: 21, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 21}, - Name: "Parent Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 21, - ChildPlanID: 22, - ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 22}, - Name: "Child Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {PlanID: 22, Name: "Grandchild's Task", ExecutionOrder: 1}, - }, - }, - }, - }, - }, - }, - }, - }, - expectedError: "", - }, - { - name: "UnknownContentType", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 30}, - Name: "Unknown Type Plan", - ContentType: "INVALID_TYPE", - }) - }, - planID: 30, - expectedPlan: nil, - expectedError: fmt.Sprintf("未知的计划内容类型: INVALID_TYPE; 计划ID: 30"), - }, - // 新增场景:测试空的关联数据 - { - name: "PlanWithTasksType_ButNoTasks", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - }) - }, - planID: 50, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 50}, - Name: "Plan with empty tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{}, // 期望一个空的切片 - }, - expectedError: "", - }, - // 新增场景:测试复杂的同级排序 - { - name: "PlanWithSubPlans_ComplexSorting", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 60}, Name: "Main Plan For Sorting", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks}) - // 故意打乱顺序插入 - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1}) - db.Create(&models.SubPlan{ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2}) - }, - planID: 60, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 60}, - Name: "Main Plan For Sorting", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 60, ChildPlanID: 62, ExecutionOrder: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 62}, Name: "SubPlan A", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 63, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 63}, Name: "SubPlan B", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - {ParentPlanID: 60, ChildPlanID: 61, ExecutionOrder: 3, ChildPlan: &models.Plan{Model: gorm.Model{ID: 61}, Name: "SubPlan C", ContentType: models.PlanContentTypeTasks, Tasks: []models.Task{}}}, - }, - }, - expectedError: "", - }, - // 新增场景:测试混合内容的子计划树 - { - name: "PlanWithSubPlans_MixedContentTypes", - setupData: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 70}, Name: "Mixed Main Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}) - db.Create(&models.Plan{Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks}) - db.Create(&models.Task{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}) - - // 创建关联 - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1}) // Main -> Child with SubPlans - db.Create(&models.SubPlan{ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2}) // Main -> Child with Tasks - db.Create(&models.SubPlan{ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1}) // Child with SubPlans -> Grandchild - }, - planID: 70, - expectedPlan: &models.Plan{ - Model: gorm.Model{ID: 70}, - Name: "Mixed Main Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - { - ParentPlanID: 70, ChildPlanID: 71, ExecutionOrder: 1, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 71}, Name: "Child with SubPlans", ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ParentPlanID: 71, ChildPlanID: 72, ExecutionOrder: 1, ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 72}, Name: "Grandchild with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 72, Name: "Grandchild's Task", ExecutionOrder: 1}}, - }}, - }, - }, - }, - { - ParentPlanID: 70, ChildPlanID: 73, ExecutionOrder: 2, - ChildPlan: &models.Plan{ - Model: gorm.Model{ID: 73}, Name: "Child with Tasks", ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{{PlanID: 73, Name: "Child's Task", ExecutionOrder: 1}}, - }, - }, - }, - }, - expectedError: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - // 使用 defer 确保数据库连接在测试结束后关闭 - sqlDB, _ := db.DB() - defer sqlDB.Close() - - tc.setupData(db) - - repo := repository.NewGormPlanRepository(db) - plan, err := repo.GetPlanByID(tc.planID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - assert.Nil(t, plan) - } else { - assert.NoError(t, err) - assert.NotNil(t, plan) - - // 在比较之前,清理实际结果和期望结果中所有不确定的、由数据库自动生成的字段 - cleanPlanForComparison(plan) - cleanPlanForComparison(tc.expectedPlan) - - assert.Equal(t, tc.expectedPlan, plan) - } - }) - } -} - -// cleanPlanForComparison 递归地重置 Plan 对象及其关联对象中由数据库自动生成的字段(如ID和时间戳), -// 以便在测试中断言它们与期望值相等。 -func cleanPlanForComparison(p *models.Plan) { - if p == nil { - return - } - - // 重置 Plan 自身的时间戳 - p.CreatedAt = time.Time{} - p.UpdatedAt = time.Time{} - p.DeletedAt = gorm.DeletedAt{} - - // 重置所有 Task 的自动生成字段 - for i := range p.Tasks { - p.Tasks[i].ID = 0 // ID是自动生成的,必须重置为0才能与期望值匹配 - p.Tasks[i].CreatedAt = time.Time{} - p.Tasks[i].UpdatedAt = time.Time{} - p.Tasks[i].DeletedAt = gorm.DeletedAt{} - } - - // 重置所有 SubPlan 的自动生成字段,并递归清理子计划 - for i := range p.SubPlans { - p.SubPlans[i].ID = 0 // SubPlan 连接记录的ID也是自动生成的 - p.SubPlans[i].CreatedAt = time.Time{} - p.SubPlans[i].UpdatedAt = time.Time{} - p.SubPlans[i].DeletedAt = gorm.DeletedAt{} - - // 递归调用以清理嵌套的子计划 - cleanPlanForComparison(p.SubPlans[i].ChildPlan) - } -} - -// TestUpdatePlan_Validation 专注于测试 UpdatePlan 中前置检查逻辑的各种失败和成功场景。 -func TestUpdatePlan_Validation(t *testing.T) { - // 定义Go测试中使用的计划实体 - planA := &models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A", ContentType: models.PlanContentTypeSubPlans} - planB := &models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B", ContentType: models.PlanContentTypeSubPlans} - planC := &models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C", ContentType: models.PlanContentTypeSubPlans} - planD := &models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D", ContentType: models.PlanContentTypeTasks} - planNew := &models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} // ID为0的新计划 - - type testCase struct { - name string - setupDB func(db *gorm.DB) - buildInput func() *models.Plan // 修改为构建函数 - expectedError string // 保持 string 类型 - } - - testCases := []testCase{ - { - name: "成功-合法的菱形依赖树", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 4}}) - }, - buildInput: func() *models.Plan { - planD.ContentType = models.PlanContentTypeTasks - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 4, ChildPlan: planD}} - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, - } - return planA - }, - expectedError: "", // 期望没有错误 - }, - { - name: "错误-根节点ID为零", - setupDB: func(db *gorm.DB) {}, - buildInput: func() *models.Plan { return planNew }, - expectedError: repository.ErrUpdateWithInvalidRoot.Error(), - }, - { - name: "错误-子计划ID为零", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlan: planNew}} - return planA - }, - expectedError: repository.ErrNewSubPlanInUpdate.Error(), - }, - { - name: "错误-节点在数据库中不存在", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 2, ChildPlan: planB, ExecutionOrder: 1}, - {ChildPlanID: 3, ChildPlan: planC, ExecutionOrder: 2}, // C 不存在 - } - return planA - }, - expectedError: repository.ErrNodeDoesNotExist.Error(), - }, - { - name: "错误-简单循环引用(A->B->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-复杂循环引用(A->B->C->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 3}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planB.ContentType = models.PlanContentTypeSubPlans - planB.SubPlans = []models.SubPlan{{ChildPlanID: 3, ChildPlan: planC}} - planC.ContentType = models.PlanContentTypeSubPlans - planC.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-自引用(A->A)", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 1, ChildPlan: planA}} - return planA - }, - expectedError: "检测到循环引用:计划 (ID: 1)", - }, - { - name: "错误-根节点内容混合", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{{ChildPlanID: 2, ChildPlan: planB}} - planA.Tasks = []models.Task{{Name: "A's Task"}} - return planA - }, - expectedError: "不能同时包含任务和子计划", - }, - { - name: "错误-任务执行顺序重复", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeTasks - planA.Tasks = []models.Task{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 - } - return planA - }, - expectedError: fmt.Sprintf("任务执行顺序重复: %d", 1), - }, { - name: "错误-子计划执行顺序重复", - setupDB: func(db *gorm.DB) { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}}) - db.Create(&models.Plan{Model: gorm.Model{ID: 11}}) - }, - buildInput: func() *models.Plan { - planA.ContentType = models.PlanContentTypeSubPlans - planA.SubPlans = []models.SubPlan{ - {ChildPlanID: 10, ChildPlan: &models.Plan{Model: gorm.Model{ID: 10}}, ExecutionOrder: 1}, - {ChildPlanID: 11, ChildPlan: &models.Plan{Model: gorm.Model{ID: 11}}, ExecutionOrder: 1}, // 重复的顺序 - } - return planA - }, - expectedError: fmt.Sprintf("子计划执行顺序重复: %d", 1), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // 1. 为每个测试用例重置基础对象的状态 - *planA = models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan A"} - *planB = models.Plan{Model: gorm.Model{ID: 2}, Name: "Plan B"} - *planC = models.Plan{Model: gorm.Model{ID: 3}, Name: "Plan C"} - *planD = models.Plan{Model: gorm.Model{ID: 4}, Name: "Plan D"} - *planNew = models.Plan{Model: gorm.Model{ID: 0}, Name: "New Plan"} - - // 2. 设置数据库 - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - tc.setupDB(db) - - // 3. 在对象重置后,构建本次测试需要的输入结构 - input := tc.buildInput() - - // 4. 执行测试 - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - - // 5. 断言结果 - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - }) - } -} - -// TestUpdatePlan_Reconciliation 专注于测试 UpdatePlan 成功执行后,数据库状态是否与预期一致。 -func TestUpdatePlan_Reconciliation(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (rootPlanID uint) - buildInput func(db *gorm.DB) *models.Plan - verifyDB func(t *testing.T, db *gorm.DB, rootPlanID uint) - } - - testCases := []testCase{ - { - name: "任务协调-新增一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 1}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - {Name: "New Task 2", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks").First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "New Task 2", finalPlan.Tasks[1].Name) - }, - }, - { - name: "任务协调-删除一个任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan With Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task to Delete", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan With Tasks", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1", ExecutionOrder: 1}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var tasks []models.Task - db.Where("plan_id = ?", rootPlanID).Find(&tasks) - assert.Len(t, tasks, 1) - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count, "被删除的任务不应再存在") - }, - }, - { - name: "任务协调-更新并重排序任务", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B", ExecutionOrder: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Model: gorm.Model{ID: 11}, PlanID: 1, Name: "B Updated", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "A", ExecutionOrder: 2}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - assert.Len(t, finalPlan.Tasks, 2) - assert.Equal(t, "B Updated", finalPlan.Tasks[0].Name) - assert.Equal(t, uint(11), finalPlan.Tasks[0].ID) - }, - }, - { - name: "任务协调-混沌的同步操作(增删改重排)", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Original)", ExecutionOrder: 1}) - db.Create(&models.Task{Model: gorm.Model{ID: 11}, PlanID: 1, Name: "Task 2 (To Be Deleted)", ExecutionOrder: 2}) - db.Create(&models.Task{Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 3}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - // T4 (新) -> T3 (不变) -> T1 (更新) - {Name: "Task 4 (New)", ExecutionOrder: 1}, - {Model: gorm.Model{ID: 12}, PlanID: 1, Name: "Task 3 (Original)", ExecutionOrder: 2}, - {Model: gorm.Model{ID: 10}, PlanID: 1, Name: "Task 1 (Updated)", ExecutionOrder: 3}, - }, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var finalPlan models.Plan - db.Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("execution_order") - }).First(&finalPlan, rootPlanID) - - // 验证最终数量 - assert.Len(t, finalPlan.Tasks, 3) - - // 验证被删除的 T2 不存在 - var count int64 - db.Model(&models.Task{}).Where("id = ?", 11).Count(&count) - assert.Equal(t, int64(0), count) - - // 验证顺序和内容 - assert.Equal(t, "Task 4 (New)", finalPlan.Tasks[0].Name) - assert.Equal(t, "Task 3 (Original)", finalPlan.Tasks[1].Name) - assert.Equal(t, "Task 1 (Updated)", finalPlan.Tasks[2].Name) - }, - }, - { - name: "子计划协调-新增一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Existing Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 2}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var links []models.SubPlan - db.Where("parent_plan_id = ?", rootPlanID).Find(&links) - assert.Len(t, links, 1) - assert.Equal(t, uint(2), links[0].ChildPlanID) - }, - }, - { - name: "子计划协调-删除一个关联", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Parent", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 2}, Name: "Child To Unlink"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 2}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Parent", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount) - - var planCount int64 - db.Model(&models.Plan{}).Where("id = ?", 2).Count(&planCount) - assert.Equal(t, int64(1), planCount, "子计划本身不应被删除") - }, - }, - { - name: "类型转换-从任务切换到子计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: 1, Name: "Old Task"}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "New Child"}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{{ChildPlanID: 10}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "旧任务应被清理") - - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(1), linkCount, "新关联应被创建") - }, - }, - { - name: "类型转换-从子计划切换到任务", - setupDB: func(db *gorm.DB) uint { - db.Create(&models.Plan{Model: gorm.Model{ID: 1}, Name: "Plan", ContentType: models.PlanContentTypeSubPlans}) - db.Create(&models.Plan{Model: gorm.Model{ID: 10}, Name: "Old Child"}) - db.Create(&models.SubPlan{ParentPlanID: 1, ChildPlanID: 10}) - return 1 - }, - buildInput: func(db *gorm.DB) *models.Plan { - return &models.Plan{ - Model: gorm.Model{ID: 1}, - Name: "Plan", - ContentType: models.PlanContentTypeTasks, // 类型变更 - Tasks: []models.Task{{Name: "New Task"}}, - } - }, - verifyDB: func(t *testing.T, db *gorm.DB, rootPlanID uint) { - var linkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", rootPlanID).Count(&linkCount) - assert.Equal(t, int64(0), linkCount, "旧的子计划关联应被清理") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", rootPlanID).Count(&taskCount) - assert.Equal(t, int64(1), taskCount, "新任务应被创建") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - rootID := tc.setupDB(db) - input := tc.buildInput(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.UpdatePlan(input) - assert.NoError(t, err) - - tc.verifyDB(t, db, rootID) - }) - } -} - -// createExistingPlan 辅助函数,用于在数据库中创建已存在的计划 -func createExistingPlan(db *gorm.DB, name string, contentType models.PlanContentType) *models.Plan { - plan := &models.Plan{ - Name: name, - ContentType: contentType, - } - db.Create(plan) - return plan -} - -func TestPlanRepository_Create(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) // 准备数据库的初始状态 - inputPlan *models.Plan // 传入 CreateTx 方法的计划对象 - expectedError error // 期望的错误类型 - verifyDB func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) // 验证数据库状态 - } - - testCases := []testCase{ - { - name: "成功创建-只包含基本信息", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "简单计划", - Description: "一个不包含任务或子计划的简单计划", - ContentType: models.PlanContentTypeTasks, // 修改为有效的 ContentType - Tasks: []models.Task{}, // 明确为空任务列表 - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") - var foundPlan models.Plan - err := db.First(&foundPlan, createdPlan.ID).Error - assert.NoError(t, err) - assert.Equal(t, "简单计划", foundPlan.Name) - assert.Equal(t, models.PlanContentTypeTasks, foundPlan.ContentType) - var tasks []models.Task - db.Where("plan_id = ?", createdPlan.ID).Find(&tasks) - assert.Len(t, tasks, 0, "不应创建任何任务") - }, - }, - { - name: "成功创建-包含任务", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "任务计划", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Name: "任务A", ExecutionOrder: 1}, - {Name: "任务B", ExecutionOrder: 2}, - }, - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "计划ID不应为0") - var foundPlan models.Plan - db.Preload("Tasks").First(&foundPlan, createdPlan.ID) - assert.Len(t, foundPlan.Tasks, 2, "应创建两个任务") - assert.NotZero(t, foundPlan.Tasks[0].ID, "任务ID不应为0") - assert.Equal(t, "任务A", foundPlan.Tasks[0].Name) - }, - }, - { - name: "成功创建-包含子计划关联", - setupDB: func(db *gorm.DB) { - // 预先创建子计划实体,使用有效的 ContentType - createExistingPlan(db, "子计划1", models.PlanContentTypeTasks) - createExistingPlan(db, "子计划2", models.PlanContentTypeTasks) - }, - inputPlan: &models.Plan{ - Name: "父计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 1, ExecutionOrder: 1}, // 关联已存在的子计划1 - {ChildPlanID: 2, ExecutionOrder: 2, ChildPlan: &models.Plan{Model: gorm.Model{ID: 2}}}, // 关联已存在的子计划2 - }, - }, - expectedError: nil, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - assert.NotZero(t, createdPlan.ID, "创建后计划ID不应为0") - - // 直接查询 SubPlan 关联记录 - var foundSubPlanLinks []models.SubPlan - err := db.Where("parent_plan_id = ?", createdPlan.ID).Find(&foundSubPlanLinks).Error - assert.NoError(t, err) - - assert.Len(t, foundSubPlanLinks, 2, "应创建两个子计划关联") - assert.NotZero(t, foundSubPlanLinks[0].ID, "子计划关联ID不应为0") - assert.Equal(t, createdPlan.ID, foundSubPlanLinks[0].ParentPlanID) - assert.Equal(t, uint(1), foundSubPlanLinks[0].ChildPlanID) - }, - }, - { - name: "失败-计划ID不为0", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Model: gorm.Model{ID: 100}, // ID不为0 - Name: "无效计划", - }, - expectedError: repository.ErrCreateWithNonZeroID, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - // 验证数据库中没有创建该计划 - var count int64 - db.Model(&models.Plan{}).Where("id = ?", 100).Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-同时包含任务和子计划", - setupDB: func(db *gorm.DB) { - createExistingPlan(db, "子计划", models.PlanContentTypeTasks) // 使用有效的 ContentType - }, - inputPlan: &models.Plan{ - Name: "混合内容计划", - ContentType: models.PlanContentTypeTasks, // 声明为任务类型 - Tasks: []models.Task{{Name: "任务A"}}, - SubPlans: []models.SubPlan{{ChildPlanID: 1, ChildPlan: &models.Plan{Model: gorm.Model{ID: 1}}}}, // 但也包含子计划 - }, - expectedError: repository.ErrMixedContent, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - // 验证数据库中没有创建该计划 - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "混合内容计划").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-子计划ID为0", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "无效子计划关联", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 0, ChildPlan: &models.Plan{Model: gorm.Model{ID: 0}}}, // 子计划ID为0 - }, - }, - expectedError: repository.ErrSubPlanIDIsZeroOnCreate, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "无效子计划关联").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-子计划在数据库中不存在", - setupDB: func(db *gorm.DB) { - // 不创建ID为999的计划 - }, - inputPlan: &models.Plan{ - Name: "不存在的子计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 999, ChildPlan: &models.Plan{Model: gorm.Model{ID: 999}}}, // 关联一个不存在的ID - }, - }, - expectedError: repository.ErrNodeDoesNotExist, - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "不存在的子计划").Count(&count) - assert.Equal(t, int64(0), count, "计划不应被创建") - }, - }, - { - name: "失败-任务执行顺序重复", - setupDB: func(db *gorm.DB) { - // 无需额外设置 - }, - inputPlan: &models.Plan{ - Name: "重复任务顺序计划", - ContentType: models.PlanContentTypeTasks, - Tasks: []models.Task{ - {Name: "Task 1", ExecutionOrder: 1}, - {Name: "Task 2", ExecutionOrder: 1}, // 重复的顺序 - }, - }, - expectedError: fmt.Errorf("任务执行顺序重复: %d", 1), // 假设 CreateTx 方法会返回此错误 - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "重复任务顺序计划").Count(&count) - assert.Equal(t, int64(0), count, "重复任务顺序的计划不应被创建") - }, - }, - { - name: "失败-子计划执行顺序重复", - setupDB: func(db *gorm.DB) { - createExistingPlan(db, "子计划A", models.PlanContentTypeTasks) - createExistingPlan(db, "子计划B", models.PlanContentTypeTasks) - }, - inputPlan: &models.Plan{ - Name: "重复子计划顺序计划", - ContentType: models.PlanContentTypeSubPlans, - SubPlans: []models.SubPlan{ - {ChildPlanID: 1, ExecutionOrder: 1}, - {ChildPlanID: 2, ExecutionOrder: 1}, // 重复的顺序 - }, - }, - expectedError: fmt.Errorf("子计划执行顺序重复: %d", 1), // 假设 CreateTx 方法会返回此错误 - verifyDB: func(t *testing.T, db *gorm.DB, createdPlan *models.Plan) { - var count int64 - db.Model(&models.Plan{}).Where("name = ?", "重复子计划顺序计划").Count(&count) - assert.Equal(t, int64(0), count, "重复子计划顺序的计划不应被创建") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - repo := repository.NewGormPlanRepository(db) - - // 准备数据库状态 - tc.setupDB(db) - - // 执行 CreateTx 操作 - err := repo.CreatePlan(tc.inputPlan) - - // 断言错误 - if tc.expectedError != nil { - assert.Error(t, err) - // 使用 Contains 检查错误信息,因为 fmt.Errorf 会创建新的错误实例 - assert.Contains(t, err.Error(), tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - - // 验证数据库状态 - tc.verifyDB(t, db, tc.inputPlan) - }) - } -} - -func TestPlanRepository_DeletePlan(t *testing.T) { - type testCase struct { - name string - setupDB func(db *gorm.DB) (planToDeleteID uint) - expectedError string - verifyDB func(t *testing.T, db *gorm.DB, planToDeleteID uint) - } - - testCases := []testCase{ - { - name: "成功删除-包含任务的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Plan with Tasks", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 1"}) - db.Create(&models.Task{PlanID: plan.ID, Name: "Task 2"}) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - - var taskCount int64 - db.Model(&models.Task{}).Where("plan_id = ?", planToDeleteID).Count(&taskCount) - assert.Equal(t, int64(0), taskCount, "关联任务应被删除") - }, - }, - { - name: "成功删除-包含子计划链接的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return parentPlan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var parentPlan models.Plan - err := db.First(&parentPlan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "父计划应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("parent_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(0), subPlanLinkCount, "子计划链接应被删除") - - // 验证子计划本身未被删除 - var childPlan models.Plan - err = db.First(&childPlan, 2).Error // Assuming childPlan.ID is 2 from setup - assert.NoError(t, err, "子计划本身不应被删除") - }, - }, - { - name: "失败删除-作为子计划的计划", - setupDB: func(db *gorm.DB) uint { - parentPlan := models.Plan{Name: "Parent Plan", ContentType: models.PlanContentTypeSubPlans} - childPlan := models.Plan{Name: "Child Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&parentPlan) - db.Create(&childPlan) - db.Create(&models.SubPlan{ParentPlanID: parentPlan.ID, ChildPlanID: childPlan.ID}) - return childPlan.ID // 尝试删除子计划 - }, - expectedError: repository.ErrDeleteWithReferencedPlan.Error(), - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var childPlan models.Plan - err := db.First(&childPlan, planToDeleteID).Error - assert.NoError(t, err, "子计划不应被删除") - - var subPlanLinkCount int64 - db.Model(&models.SubPlan{}).Where("child_plan_id = ?", planToDeleteID).Count(&subPlanLinkCount) - assert.Equal(t, int64(1), subPlanLinkCount, "子计划链接不应被删除") - }, - }, - { - name: "失败删除-不存在的计划", - setupDB: func(db *gorm.DB) uint { - return 999 // 不存在的ID - }, - expectedError: "record not found", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - // 数据库状态应保持不变 - }, - }, - { - name: "成功删除-不含任何关联的计划", - setupDB: func(db *gorm.DB) uint { - plan := models.Plan{Name: "Simple Plan", ContentType: models.PlanContentTypeTasks} - db.Create(&plan) - return plan.ID - }, - expectedError: "", - verifyDB: func(t *testing.T, db *gorm.DB, planToDeleteID uint) { - var plan models.Plan - err := db.First(&plan, planToDeleteID).Error - assert.Error(t, err) - assert.True(t, errors.Is(err, gorm.ErrRecordNotFound), "计划应被删除") - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - db := setupTestDB(t) - sqlDB, _ := db.DB() - defer sqlDB.Close() - - planToDeleteID := tc.setupDB(db) - - repo := repository.NewGormPlanRepository(db) - err := repo.DeletePlan(planToDeleteID) - - if tc.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - } else { - assert.NoError(t, err) - } - - tc.verifyDB(t, db, planToDeleteID) - }) - } -} From 648a790cec14a48a1acc332e547d4f60180c119e Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 15:35:20 +0800 Subject: [PATCH 43/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=97=85=E7=8C=AA?= =?UTF-8?q?=E5=AD=90=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 6 ++-- internal/domain/pig/pig_batch.go | 32 +++++++++++++++++++ internal/domain/pig/pig_batch_service.go | 29 ----------------- internal/domain/pig/pig_sick_manager.go | 27 ++++++++++++++++ internal/infra/database/postgres.go | 8 ++--- internal/infra/models/medication.go | 8 ++--- internal/infra/models/models.go | 4 +-- .../group_medication_log_repository.go | 20 ++++++------ 8 files changed, 83 insertions(+), 51 deletions(-) diff --git a/internal/core/application.go b/internal/core/application.go index 9dcd5f3..a15f3de 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -79,7 +79,7 @@ func NewApplication(configPath string) (*Application, error) { pigTransferLogRepo := repository.NewGormPigTransferLogRepository(storage.GetDB()) pigTradeRepo := repository.NewGormPigTradeRepository(storage.GetDB()) pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB()) - pigGroupMedicationLogRepo := repository.NewGormGroupMedicationLogRepository(storage.GetDB()) + medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) @@ -87,7 +87,9 @@ func NewApplication(configPath string) (*Application, error) { // 初始化猪群管理领域 pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo) pigTradeManager := pig.NewPigTradeManager(pigTradeRepo) - pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, pigPenTransferManager, pigTradeManager) + pigSickManager := pig.NewSickPigManager(pigSickPigLogRepo, medicationLogRepo) + pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, + pigPenTransferManager, pigTradeManager, pigSickManager) // --- 业务逻辑处理器初始化 --- pigFarmService := service.NewPigFarmService(pigFarmRepo, pigPenRepo, pigBatchRepo, unitOfWork, logger) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 2d56781..f6a4214 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -5,6 +5,7 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) // --- 业务错误定义 --- @@ -56,3 +57,34 @@ type PigBatchService interface { UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error } + +// pigBatchService 是 PigBatchService 接口的具体实现。 +// 它作为猪群领域的主服务,封装了所有业务逻辑。 +type pigBatchService struct { + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 + uow repository.UnitOfWork // 工作单元,用于管理事务 + transferSvc PigPenTransferManager // 调栏子服务 + tradeSvc PigTradeManager // 交易子服务 + sickSvc SickPigManager // 病猪子服务 +} + +// NewPigBatchService 是 pigBatchService 的构造函数。 +// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 +func NewPigBatchService( + pigBatchRepo repository.PigBatchRepository, + pigBatchLogRepo repository.PigBatchLogRepository, + uow repository.UnitOfWork, + transferSvc PigPenTransferManager, + tradeSvc PigTradeManager, + sickSvc SickPigManager, +) PigBatchService { + return &pigBatchService{ + pigBatchRepo: pigBatchRepo, + pigBatchLogRepo: pigBatchLogRepo, + uow: uow, + transferSvc: transferSvc, + tradeSvc: tradeSvc, + sickSvc: sickSvc, + } +} diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 65b0e07..55cfc15 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -6,40 +6,11 @@ import ( "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "gorm.io/gorm" ) // --- 领域服务实现 --- -// pigBatchService 是 PigBatchService 接口的具体实现。 -// 它作为猪群领域的主服务,封装了所有业务逻辑。 -type pigBatchService struct { - pigBatchRepo repository.PigBatchRepository // 猪批次仓库 - pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 - uow repository.UnitOfWork // 工作单元,用于管理事务 - transferSvc PigPenTransferManager // 调栏子服务 - tradeSvc PigTradeManager // 交易子服务 -} - -// NewPigBatchService 是 pigBatchService 的构造函数。 -// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 -func NewPigBatchService( - pigBatchRepo repository.PigBatchRepository, - pigBatchLogRepo repository.PigBatchLogRepository, - uow repository.UnitOfWork, - transferSvc PigPenTransferManager, - tradeSvc PigTradeManager, -) PigBatchService { - return &pigBatchService{ - pigBatchRepo: pigBatchRepo, - pigBatchLogRepo: pigBatchLogRepo, - uow: uow, - transferSvc: transferSvc, - tradeSvc: tradeSvc, - } -} - // CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。 func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) { // 业务规则可以在这里添加,例如检查批次号是否唯一等 diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go index 467de02..b93e217 100644 --- a/internal/domain/pig/pig_sick_manager.go +++ b/internal/domain/pig/pig_sick_manager.go @@ -1 +1,28 @@ package pig + +import ( + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" +) + +// SickPigManager 定义了与病猪管理相关的操作接口。 +// 这是一个领域服务,负责协调病猪记录、用药等业务逻辑。 +type SickPigManager interface { +} + +// sickPigManager 是 SickPigManager 接口的具体实现。 +// 它依赖于仓库接口来执行数据持久化操作。 +type sickPigManager struct { + sickLogRepo repository.PigSickLogRepository + medicationLogRepo repository.MedicationLogRepository +} + +// NewSickPigManager 是 sickPigManager 的构造函数。 +func NewSickPigManager( + sickLogRepo repository.PigSickLogRepository, + medicationLogRepo repository.MedicationLogRepository, +) SickPigManager { + return &sickPigManager{ + sickLogRepo: sickLogRepo, + medicationLogRepo: medicationLogRepo, + } +} diff --git a/internal/infra/database/postgres.go b/internal/infra/database/postgres.go index dff2280..5b868c5 100644 --- a/internal/infra/database/postgres.go +++ b/internal/infra/database/postgres.go @@ -163,12 +163,12 @@ func (ps *PostgresStorage) creatingHyperTable() error { {models.RawMaterialPurchase{}, "purchase_date"}, {models.RawMaterialStockLog{}, "happened_at"}, {models.FeedUsageRecord{}, "recorded_at"}, - {models.GroupMedicationLog{}, "happened_at"}, + {models.MedicationLog{}, "happened_at"}, {models.PigBatchLog{}, "happened_at"}, {models.WeighingBatch{}, "weighing_time"}, {models.WeighingRecord{}, "weighing_time"}, {models.PigTransferLog{}, "transfer_time"}, - {models.PigBatchSickPigLog{}, "happened_at"}, + {models.PigSickLog{}, "happened_at"}, {models.PigPurchase{}, "purchase_date"}, {models.PigSale{}, "sale_date"}, } @@ -203,12 +203,12 @@ func (ps *PostgresStorage) applyCompressionPolicies() error { {models.RawMaterialPurchase{}, "raw_material_id"}, {models.RawMaterialStockLog{}, "raw_material_id"}, {models.FeedUsageRecord{}, "pen_id"}, - {models.GroupMedicationLog{}, "pig_batch_id"}, + {models.MedicationLog{}, "pig_batch_id"}, {models.PigBatchLog{}, "pig_batch_id"}, {models.WeighingBatch{}, "pig_batch_id"}, {models.WeighingRecord{}, "weighing_batch_id"}, {models.PigTransferLog{}, "pig_batch_id"}, - {models.PigBatchSickPigLog{}, "pig_batch_id"}, + {models.PigSickLog{}, "pig_batch_id"}, {models.PigPurchase{}, "pig_batch_id"}, {models.PigSale{}, "pig_batch_id"}, } diff --git a/internal/infra/models/medication.go b/internal/infra/models/medication.go index c6ead4a..bd60718 100644 --- a/internal/infra/models/medication.go +++ b/internal/infra/models/medication.go @@ -80,8 +80,8 @@ const ( ReasonTypeHealthCare MedicationReasonType = "保健" ) -// GroupMedicationLog 记录了对整个猪批次的用药情况 -type GroupMedicationLog struct { +// MedicationLog 记录了对整个猪批次的用药情况 +type MedicationLog struct { gorm.Model PigBatchID uint `gorm:"not null;index;comment:关联的猪批次ID"` MedicationID uint `gorm:"not null;index;comment:关联的药品ID"` @@ -94,6 +94,6 @@ type GroupMedicationLog struct { HappenedAt time.Time `gorm:"primaryKey;comment:用药时间"` } -func (GroupMedicationLog) TableName() string { - return "group_medication_logs" +func (MedicationLog) TableName() string { + return "medication_logs" } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 38a319f..7344a8a 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -42,7 +42,7 @@ func GetAllModels() []interface{} { &WeighingBatch{}, &WeighingRecord{}, &PigTransferLog{}, - &PigBatchSickPigLog{}, + &PigSickLog{}, // Pig Buy & Sell &PigPurchase{}, @@ -58,7 +58,7 @@ func GetAllModels() []interface{} { // Medication Models &Medication{}, - &GroupMedicationLog{}, + &MedicationLog{}, } } diff --git a/internal/infra/repository/group_medication_log_repository.go b/internal/infra/repository/group_medication_log_repository.go index 4b5be01..2b048f5 100644 --- a/internal/infra/repository/group_medication_log_repository.go +++ b/internal/infra/repository/group_medication_log_repository.go @@ -5,22 +5,22 @@ import ( "gorm.io/gorm" ) -// GroupMedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。 -type GroupMedicationLogRepository interface { - CreateGroupMedicationLog(log *models.GroupMedicationLog) error +// MedicationLogRepository 定义了与群体用药日志模型相关的数据库操作接口。 +type MedicationLogRepository interface { + CreateMedicationLog(log *models.MedicationLog) error } -// gormGroupMedicationLogRepository 是 GroupMedicationLogRepository 接口的 GORM 实现。 -type gormGroupMedicationLogRepository struct { +// gormMedicationLogRepository 是 MedicationLogRepository 接口的 GORM 实现。 +type gormMedicationLogRepository struct { db *gorm.DB } -// NewGormGroupMedicationLogRepository 创建一个新的 GroupMedicationLogRepository GORM 实现实例。 -func NewGormGroupMedicationLogRepository(db *gorm.DB) GroupMedicationLogRepository { - return &gormGroupMedicationLogRepository{db: db} +// NewGormMedicationLogRepository 创建一个新的 MedicationLogRepository GORM 实现实例。 +func NewGormMedicationLogRepository(db *gorm.DB) MedicationLogRepository { + return &gormMedicationLogRepository{db: db} } -// CreateGroupMedicationLog 创建一条新的群体用药日志记录 -func (r *gormGroupMedicationLogRepository) CreateGroupMedicationLog(log *models.GroupMedicationLog) error { +// CreateMedicationLog 创建一条新的群体用药日志记录 +func (r *gormMedicationLogRepository) CreateMedicationLog(log *models.MedicationLog) error { return r.db.Create(log).Error } From 3b109d15477dc8cce498edf172acb0f399f441b2 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 16:19:48 +0800 Subject: [PATCH 44/65] =?UTF-8?q?=E4=B9=B0=E5=8D=96=E7=8C=AA=E8=A6=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=8C=AA=E6=A0=8F=E5=AD=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/core/application.go | 2 +- internal/domain/pig/pen_transfer_manager.go | 79 +++++++++++++--- internal/domain/pig/pig_batch.go | 4 +- .../domain/pig/pig_batch_service_pig_trade.go | 94 ++++++++++++++----- .../repository/pig_transfer_log_repository.go | 12 +++ 5 files changed, 153 insertions(+), 38 deletions(-) diff --git a/internal/core/application.go b/internal/core/application.go index a15f3de..25e596f 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -85,7 +85,7 @@ func NewApplication(configPath string) (*Application, error) { unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) // 初始化猪群管理领域 - pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo) + pigPenTransferManager := pig.NewPigPenTransferManager(pigPenRepo, pigTransferLogRepo, pigBatchRepo) pigTradeManager := pig.NewPigTradeManager(pigTradeRepo) pigSickManager := pig.NewSickPigManager(pigSickPigLogRepo, medicationLogRepo) pigBatchDomain := pig.NewPigBatchService(pigBatchRepo, pigBatchLogRepo, unitOfWork, diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index ed16b62..41b7cfb 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -1,6 +1,8 @@ package pig import ( + "errors" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "gorm.io/gorm" @@ -13,54 +15,105 @@ type PigPenTransferManager interface { LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error // GetPenByID 用于获取猪栏的详细信息,供上层服务进行业务校验。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPenByIDTx 方法。 GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) // GetPensByBatchID 获取一个猪群当前关联的所有猪栏。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 GetPensByBatchIDTx 方法。 GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) // UpdatePenFields 更新一个猪栏的指定字段。 - // 注意: 此方法依赖于您在 PigPenRepository 中添加对应的 UpdatePenFieldsTx 方法。 UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error + + // GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。 + GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) } // pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 // 它作为调栏管理器,处理底层的数据库交互。 type pigPenTransferManager struct { - penRepo repository.PigPenRepository - logRepo repository.PigTransferLogRepository + penRepo repository.PigPenRepository + logRepo repository.PigTransferLogRepository + pigBatchRepo repository.PigBatchRepository } // NewPigPenTransferManager 是 pigPenTransferManager 的构造函数。 -// 修改构造函数以接收 PigTransferLogRepository 依赖 -func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository) PigPenTransferManager { +func NewPigPenTransferManager(penRepo repository.PigPenRepository, logRepo repository.PigTransferLogRepository, pigBatchRepo repository.PigBatchRepository) PigPenTransferManager { return &pigPenTransferManager{ - penRepo: penRepo, - logRepo: logRepo, + penRepo: penRepo, + logRepo: logRepo, + pigBatchRepo: pigBatchRepo, } } // LogTransfer 实现了在数据库中创建迁移日志的逻辑。 func (s *pigPenTransferManager) LogTransfer(tx *gorm.DB, log *models.PigTransferLog) error { - // 使用新的仓库接口进行操作 return s.logRepo.CreatePigTransferLog(tx, log) } // GetPenByID 实现了获取猪栏信息的逻辑。 -// 注意: 此处调用了一个假设存在的方法 GetPenByIDTx。 func (s *pigPenTransferManager) GetPenByID(tx *gorm.DB, penID uint) (*models.Pen, error) { return s.penRepo.GetPenByIDTx(tx, penID) } // GetPensByBatchID 实现了获取猪群关联猪栏列表的逻辑。 -// 注意: 此处调用了一个假设存在的方法 GetPensByBatchIDTx。 func (s *pigPenTransferManager) GetPensByBatchID(tx *gorm.DB, batchID uint) ([]*models.Pen, error) { return s.penRepo.GetPensByBatchIDTx(tx, batchID) } // UpdatePenFields 实现了更新猪栏字段的逻辑。 -// 注意: 此处调用了一个假设存在的方法 UpdatePenFieldsTx。 func (s *pigPenTransferManager) UpdatePenFields(tx *gorm.DB, penID uint, updates map[string]interface{}) error { return s.penRepo.UpdatePenFieldsTx(tx, penID, updates) } + +// GetCurrentPigsInPen 实现了计算猪栏当前猪只数量的逻辑。 +func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) { + // 1. 通过猪栏ID查出所属猪群信息 + pen, err := s.penRepo.GetPenByIDTx(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, ErrPenNotFound + } + return 0, err + } + + // 如果猪栏没有关联任何猪群,那么猪只数必为0 + if pen.PigBatchID == nil || *pen.PigBatchID == 0 { + return 0, nil + } + currentBatchID := *pen.PigBatchID + + // 2. 根据猪群ID获取猪群的起始日期 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, currentBatchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, ErrPigBatchNotFound + } + return 0, err + } + batchStartDate := batch.StartDate + + // 3. 调用仓库方法,获取从猪群开始至今,该猪栏的所有倒序日志 + logs, err := s.logRepo.GetLogsForPenSince(tx, penID, batchStartDate) + if err != nil { + return 0, err + } + + // 如果没有日志,猪只数为0 + if len(logs) == 0 { + return 0, nil + } + + // 4. 在内存中筛选出最后一段连续日志,并进行计算 + var totalPigs int + // 再次确认当前猪群ID,以最新的日志为准,防止在极小时间窗口内猪栏被快速切换 + latestBatchID := *pen.PigBatchID + + for _, log := range logs { + // 一旦发现日志不属于最新的猪群,立即停止计算 + if log.PigBatchID != latestBatchID { + break + } + totalPigs += log.Quantity + } + + return totalPigs, nil +} diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index f6a4214..001c0aa 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -51,9 +51,9 @@ type PigBatchService interface { GetCurrentPigQuantity(batchID uint) (int, error) // SellPigs 处理卖猪的业务逻辑。 - SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error // BuyPigs 处理买猪的业务逻辑。 - BuyPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error } diff --git a/internal/domain/pig/pig_batch_service_pig_trade.go b/internal/domain/pig/pig_batch_service_pig_trade.go index de327a0..d2cec43 100644 --- a/internal/domain/pig/pig_batch_service_pig_trade.go +++ b/internal/domain/pig/pig_batch_service_pig_trade.go @@ -10,32 +10,37 @@ import ( ) // SellPigs 处理批量销售猪的业务逻辑。 -func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { +func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { if quantity <= 0 { return errors.New("销售数量必须大于0") } - // 1. 获取猪批次信息 - _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + // 1. 校验猪栏信息 + pen, err := s.transferSvc.GetPenByID(tx, penID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound + return ErrPenNotFound } - return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) } - // 2. 业务校验:检查销售数量是否超过当前批次数量 - currentQuantity, err := s.getCurrentPigQuantityTx(tx, batchID) + // 校验猪栏是否属于该批次 + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return ErrPenNotAssociatedWithBatch + } + + // 2. 业务校验:检查销售数量是否超过猪栏当前猪只数 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) if err != nil { - return fmt.Errorf("获取猪批次 %d 当前数量失败: %w", batchID, err) + return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err) } - if quantity > currentQuantity { - return fmt.Errorf("销售数量 %d 超过当前批次 %d 数量 %d", quantity, batchID, currentQuantity) + if quantity > currentPigsInPen { + return fmt.Errorf("销售数量 %d 超过猪栏 %d 当前猪只数 %d", quantity, penID, currentPigsInPen) } - // 3. 记录销售交易 + // 3. 记录销售交易 (财务) sale := &models.PigSale{ PigBatchID: batchID, SaleDate: tradeDate, @@ -50,9 +55,23 @@ func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64 return fmt.Errorf("记录销售交易失败: %w", err) } - // 4. 记录批次日志 + // 4. 创建猪只转移日志 (物理) + transferLog := &models.PigTransferLog{ + TransferTime: tradeDate, + PigBatchID: batchID, + PenID: penID, + Quantity: -quantity, // 销售导致数量减少 + Type: models.PigTransferTypeSale, + OperatorID: operatorID, + Remarks: fmt.Sprintf("销售给 %s", traderName), + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("创建猪只转移日志失败: %w", err) + } + + // 5. 记录批次数量变更日志 (逻辑) if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeSale, -quantity, - fmt.Sprintf("猪批次 %d 销售 %d 头猪给 %s", batchID, quantity, traderName), + fmt.Sprintf("猪批次 %d 从猪栏 %d 销售 %d 头猪给 %s", batchID, penID, quantity, traderName), tradeDate); err != nil { return fmt.Errorf("更新猪批次数量失败: %w", err) } @@ -62,22 +81,39 @@ func (s *pigBatchService) SellPigs(batchID uint, quantity int, unitPrice float64 } // BuyPigs 处理批量购买猪的业务逻辑。 -func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { +func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, totalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { if quantity <= 0 { return errors.New("采购数量必须大于0") } - // 1. 获取猪批次信息 - _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) // 仅用于校验批次是否存在 + // 1. 校验猪栏信息 + pen, err := s.transferSvc.GetPenByID(tx, penID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound + return ErrPenNotFound } - return fmt.Errorf("获取猪批次 %d 信息失败: %w", batchID, err) + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) } - // 3. 记录采购交易 + // 校验猪栏是否属于该批次 + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return ErrPenNotAssociatedWithBatch + } + + // 2. 业务校验:检查猪栏容量,如果超出,在备注中记录警告 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %d 当前猪只数失败: %w", penID, err) + } + + transferRemarks := fmt.Sprintf("从 %s 采购", traderName) + if currentPigsInPen+quantity > pen.Capacity { + warning := fmt.Sprintf("[警告]猪栏容量超出: 当前 %d, 采购 %d, 容量 %d.", currentPigsInPen, quantity, pen.Capacity) + transferRemarks = fmt.Sprintf("%s %s", transferRemarks, warning) + } + + // 3. 记录采购交易 (财务) purchase := &models.PigPurchase{ PigBatchID: batchID, PurchaseDate: tradeDate, @@ -85,16 +121,30 @@ func (s *pigBatchService) BuyPigs(batchID uint, quantity int, unitPrice float64, Quantity: quantity, UnitPrice: unitPrice, TotalPrice: totalPrice, // 总价不一定是单价x数量, 所以要传进来 - Remarks: remarks, + Remarks: remarks, // 用户传入的备注 OperatorID: operatorID, } if err := s.tradeSvc.BuyPig(tx, purchase); err != nil { return fmt.Errorf("记录采购交易失败: %w", err) } - // 4. 记录批次日志 + // 4. 创建猪只转移日志 (物理) + transferLog := &models.PigTransferLog{ + TransferTime: tradeDate, + PigBatchID: batchID, + PenID: penID, + Quantity: quantity, // 采购导致数量增加 + Type: models.PigTransferTypePurchase, + OperatorID: operatorID, + Remarks: transferRemarks, // 包含系统生成的备注和潜在的警告 + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("创建猪只转移日志失败: %w", err) + } + + // 5. 记录批次数量变更日志 (逻辑) if err := s.updatePigBatchQuantityTx(tx, operatorID, batchID, models.ChangeTypeBuy, quantity, - fmt.Sprintf("猪批次 %d 采购 %d 头猪从 %s", batchID, quantity, traderName), + fmt.Sprintf("猪批次 %d 在猪栏 %d 采购 %d 头猪从 %s", batchID, penID, quantity, traderName), tradeDate); err != nil { return fmt.Errorf("更新猪批次数量失败: %w", err) } diff --git a/internal/infra/repository/pig_transfer_log_repository.go b/internal/infra/repository/pig_transfer_log_repository.go index 7854717..4d934fe 100644 --- a/internal/infra/repository/pig_transfer_log_repository.go +++ b/internal/infra/repository/pig_transfer_log_repository.go @@ -1,6 +1,8 @@ package repository import ( + "time" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) @@ -9,6 +11,9 @@ import ( type PigTransferLogRepository interface { // CreatePigTransferLog 在数据库中创建一条猪只迁移日志记录。 CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error + + // GetLogsForPenSince 获取指定猪栏自特定时间点以来的所有迁移日志,按时间倒序排列。 + GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) } // gormPigTransferLogRepository 是 PigTransferLogRepository 接口的 GORM 实现。 @@ -25,3 +30,10 @@ func NewGormPigTransferLogRepository(db *gorm.DB) PigTransferLogRepository { func (r *gormPigTransferLogRepository) CreatePigTransferLog(tx *gorm.DB, log *models.PigTransferLog) error { return tx.Create(log).Error } + +// GetLogsForPenSince 实现了获取猪栏自特定时间点以来所有迁移日志的逻辑。 +func (r *gormPigTransferLogRepository) GetLogsForPenSince(tx *gorm.DB, penID uint, since time.Time) ([]*models.PigTransferLog, error) { + var logs []*models.PigTransferLog + err := tx.Where("pen_id = ? AND transfer_time >= ?", penID, since).Order("transfer_time DESC").Find(&logs).Error + return logs, err +} From 189d532ac97614137f68bdb801acc3feef62ad80 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 16:31:24 +0800 Subject: [PATCH 45/65] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B0=83=E6=A0=8F?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch_service_pen_transfer.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index 0a216b5..d50244a 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -12,6 +12,17 @@ import ( // executeTransferAndLog 是一个私有辅助方法,用于封装创建和记录迁移日志的通用逻辑。 func (s *pigBatchService) executeTransferAndLog(tx *gorm.DB, fromBatchID, toBatchID, fromPenID, toPenID uint, quantity int, transferType models.PigTransferType, operatorID uint, remarks string) error { + // 通用校验:任何调出操作都不能超过源猪栏的当前存栏数 + if quantity < 0 { // 当调出时才需要检查 + currentPigsInFromPen, err := s.transferSvc.GetCurrentPigsInPen(tx, fromPenID) + if err != nil { + return fmt.Errorf("获取源猪栏 %d 当前猪只数失败: %w", fromPenID, err) + } + if currentPigsInFromPen+quantity < 0 { + return fmt.Errorf("调出数量 %d 超过源猪栏 %d 当前存栏数 %d", -quantity, fromPenID, currentPigsInFromPen) + } + } + // 1. 生成关联ID correlationID := uuid.New().String() From 51b776f3932062481e3ace2b94352242b9cbdc77 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 17:13:30 +0800 Subject: [PATCH 46/65] =?UTF-8?q?=E8=B7=A8=E7=BE=A4=E8=B0=83=E6=A0=8F?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E8=B0=83=E6=95=B4=E7=8C=AA=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 9 ++++- .../pig/pig_batch_service_pen_transfer.go | 37 ++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 001c0aa..26eca87 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -50,12 +50,19 @@ type PigBatchService interface { // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 GetCurrentPigQuantity(batchID uint) (int, error) + UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error + + // 交易子服务 + // SellPigs 处理卖猪的业务逻辑。 SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error // BuyPigs 处理买猪的业务逻辑。 BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error - UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error + // 调栏子服务 + + TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error } // pigBatchService 是 PigBatchService 接口的具体实现。 diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index d50244a..95cefa1 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -110,15 +110,21 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { // 1. 核心业务规则校验 - sourceBatch, err := s.pigBatchRepo.GetPigBatchByID(sourceBatchID) - if err != nil { + // 1.1 校验猪群存在 + if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, sourceBatchID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("源猪群 %d 不存在", sourceBatchID) + } return fmt.Errorf("获取源猪群信息失败: %w", err) } - destBatch, err := s.pigBatchRepo.GetPigBatchByID(destBatchID) - if err != nil { + if _, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, destBatchID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("目标猪群 %d 不存在", destBatchID) + } return fmt.Errorf("获取目标猪群信息失败: %w", err) } + // 1.2 校验猪栏归属 fromPen, err := s.transferSvc.GetPenByID(tx, fromPenID) if err != nil { return fmt.Errorf("获取源猪栏信息失败: %w", err) @@ -127,21 +133,26 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc return fmt.Errorf("源猪栏 %d 不属于源猪群 %d", fromPenID, sourceBatchID) } - // 2. 调用通用辅助方法执行日志记录 + // 2. 调用通用辅助方法执行猪只物理转移的日志记录 err = s.executeTransferAndLog(tx, sourceBatchID, destBatchID, fromPenID, toPenID, int(quantity), "跨群调栏", operatorID, remarks) if err != nil { return err } - // 3. 修改本聚合的数据(猪群总数) - sourceBatch.InitialCount -= int(quantity) - destBatch.InitialCount += int(quantity) - - if _, _, err := s.pigBatchRepo.UpdatePigBatch(sourceBatch); err != nil { - return fmt.Errorf("更新源猪群数量失败: %w", err) + // 3. 通过创建批次日志来修改猪群总数,确保数据可追溯 + now := time.Now() + // 3.1 记录源猪群数量减少 + reasonOut := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调出至批次 %d。备注: %s", quantity, sourceBatchID, destBatchID, remarks) + err = s.updatePigBatchQuantityTx(tx, operatorID, sourceBatchID, models.ChangeTypeTransferOut, -int(quantity), reasonOut, now) + if err != nil { + return fmt.Errorf("更新源猪群 %d 数量失败: %w", sourceBatchID, err) } - if _, _, err := s.pigBatchRepo.UpdatePigBatch(destBatch); err != nil { - return fmt.Errorf("更新目标猪群数量失败: %w", err) + + // 3.2 记录目标猪群数量增加 + reasonIn := fmt.Sprintf("跨群调栏: %d头猪从批次 %d 调入。备注: %s", quantity, sourceBatchID, remarks) + err = s.updatePigBatchQuantityTx(tx, operatorID, destBatchID, models.ChangeTypeTransferIn, int(quantity), reasonIn, now) + if err != nil { + return fmt.Errorf("更新目标猪群 %d 数量失败: %w", destBatchID, err) } return nil From efbe7d167c117708e6f6c9ae59190964b1bdedf3 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 17:44:00 +0800 Subject: [PATCH 47/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0AssignEmptyPensToBatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 12 ++-- .../pig/pig_batch_service_pen_transfer.go | 57 +++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 26eca87..4576d7e 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -46,21 +46,25 @@ type PigBatchService interface { ListPigBatches(isActive *bool) ([]*models.PigBatch, error) // UpdatePigBatchPens 更新猪批次关联的猪栏。 UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error + // AssignEmptyPensToBatch 为猪群分配空栏 + AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error + // MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 + MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error + // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 + ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 GetCurrentPigQuantity(batchID uint) (int, error) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error - // 交易子服务 - + // ---交易子服务--- // SellPigs 处理卖猪的业务逻辑。 SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error // BuyPigs 处理买猪的业务逻辑。 BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error - // 调栏子服务 - + // ---调栏子服务 --- TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error } diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index 95cefa1..2c6b7f6 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -158,3 +158,60 @@ func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatc return nil }) } + +// AssignEmptyPensToBatch 为猪群分配空栏 +func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次是否存在且活跃 + pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 遍历并校验每一个待分配的猪栏 + for _, penID := range penIDs { + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 核心业务规则:校验猪栏是否完全空闲 + if pen.Status != models.PenStatusEmpty { + return fmt.Errorf("猪栏 %s 状态不为空 (%s),无法分配", pen.PenNumber, pen.Status) + } + if pen.PigBatchID != nil { + return fmt.Errorf("猪栏 %s 已被其他批次 %d 占用,无法分配", pen.PenNumber, *pen.PigBatchID) + } + + // 3. 更新猪栏的归属 + updates := map[string]interface{}{ + "pig_batch_id": &batchID, + "status": models.PenStatusOccupied, + } + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("分配猪栏 %d 失败: %w", penID, err) + } + } + + return nil + }) +} + +// MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 +func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error { + panic("implement me") +} + +// ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 +func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error { + panic("implement me") +} From 5e49cd3f955e4793e7a5f1249100326d09192f99 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 17:56:13 +0800 Subject: [PATCH 48/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0MovePigsIntoPen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pen_transfer_manager.go | 26 +++++++ .../pig/pig_batch_service_pen_transfer.go | 68 ++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index 41b7cfb..99bc80d 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -2,6 +2,7 @@ package pig import ( "errors" + "fmt" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" @@ -25,6 +26,9 @@ type PigPenTransferManager interface { // GetCurrentPigsInPen 通过汇总猪只迁移日志,计算给定猪栏中的当前猪只数量。 GetCurrentPigsInPen(tx *gorm.DB, penID uint) (int, error) + + // GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数 + GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) } // pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 @@ -117,3 +121,25 @@ func (s *pigPenTransferManager) GetCurrentPigsInPen(tx *gorm.DB, penID uint) (in return totalPigs, nil } + +// GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数 +// 该方法通过遍历猪群下的每个猪栏,并调用 GetCurrentPigsInPen 来累加存栏数。 +func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) { + // 1. 获取该批次下所有猪栏的列表 + pensInBatch, err := s.GetPensByBatchID(tx, batchID) + if err != nil { + return 0, fmt.Errorf("获取猪群 %d 下属猪栏失败: %w", batchID, err) + } + + totalPigs := 0 + // 2. 遍历每个猪栏,累加其存栏数 + for _, pen := range pensInBatch { + pigsInPen, err := s.GetCurrentPigsInPen(tx, pen.ID) + if err != nil { + return 0, fmt.Errorf("获取猪栏 %d 存栏数失败: %w", pen.ID, err) + } + totalPigs += pigsInPen + } + + return totalPigs, nil +} diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index 2c6b7f6..e1d8d86 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -208,7 +208,73 @@ func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, op // MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error { - panic("implement me") + if quantity <= 0 { + return errors.New("迁移数量必须大于零") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次是否存在且活跃 + pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 校验目标猪栏 + toPen, err := s.transferSvc.GetPenByID(tx, toPenID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("目标猪栏 %d 不存在: %w", toPenID, ErrPenNotFound) + } + return fmt.Errorf("获取目标猪栏 %d 信息失败: %w", toPenID, err) + } + + // 校验目标猪栏的归属和状态 + if toPen.PigBatchID == nil { + return fmt.Errorf("目标猪栏 %s 不属于当前批次 %s", toPen.PenNumber, batchID) + } + if toPen.PigBatchID != nil && *toPen.PigBatchID != batchID { + return fmt.Errorf("目标猪栏 %s 已被其他批次 %d 占用,无法移入", toPen.PenNumber, *toPen.PigBatchID) + } + + // 3. 校验猪群中有足够的“未分配”猪只 + currentBatchTotal, err := s.getCurrentPigQuantityTx(tx, batchID) + if err != nil { + return fmt.Errorf("获取猪群 %d 当前总数量失败: %w", batchID, err) + } + + // 获取该批次下所有猪栏的当前总存栏数 + totalPigsInPens, err := s.transferSvc.GetTotalPigsInPensForBatchTx(tx, batchID) + if err != nil { + return fmt.Errorf("计算猪群 %d 下属猪栏总存栏失败: %w", batchID, err) + } + + unassignedPigs := currentBatchTotal - totalPigsInPens + if unassignedPigs < quantity { + return fmt.Errorf("猪群 %d 未分配猪只不足,当前未分配 %d 头,需要移入 %d 头", batchID, unassignedPigs, quantity) + } + + // 4. 记录转移日志 + logIn := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: batchID, + PenID: toPenID, + Quantity: quantity, // 调入为正数 + Type: models.PigTransferTypeInternal, // 首次入栏 + OperatorID: operatorID, + Remarks: remarks, + } + if err := s.transferSvc.LogTransfer(tx, logIn); err != nil { + return fmt.Errorf("记录入栏日志失败: %w", err) + } + + return nil + }) } // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 From 0576a790dda1d4f093d73fd030692320376b432f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 18:08:56 +0800 Subject: [PATCH 49/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20ReclassifyPenToNewBa?= =?UTF-8?q?tch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pig/pig_batch_service_pen_transfer.go | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/internal/domain/pig/pig_batch_service_pen_transfer.go b/internal/domain/pig/pig_batch_service_pen_transfer.go index e1d8d86..0ef76a7 100644 --- a/internal/domain/pig/pig_batch_service_pen_transfer.go +++ b/internal/domain/pig/pig_batch_service_pen_transfer.go @@ -279,5 +279,105 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error { - panic("implement me") + if fromBatchID == toBatchID { + return errors.New("源猪群和目标猪群不能相同") + } + + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 核心业务规则校验 + // 1.1 校验猪群存在 + fromBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, fromBatchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("源猪群 %d 不存在", fromBatchID) + } + return fmt.Errorf("获取源猪群信息失败: %w", err) + } + toBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, toBatchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("目标猪群 %d 不存在", toBatchID) + } + return fmt.Errorf("获取目标猪群信息失败: %w", err) + } + + // 1.2 校验猪栏归属 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != fromBatchID { + return fmt.Errorf("猪栏 %v 不属于源猪群 %v,无法划拨", pen.PenNumber, fromBatch.BatchNumber) + } + + // 2. 获取猪栏当前存栏数 + quantity, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %v 存栏数失败: %w", pen.PenNumber, err) + } + + // 3. 更新猪栏的归属 + updates := map[string]interface{}{ + "pig_batch_id": &toBatchID, + } + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("更新猪栏 %v 归属失败: %w", pen.PenNumber, err) + } + // 如果猪栏是空的,则只进行归属变更,不影响猪群数量 + if quantity == 0 { + return nil // 空栏划拨,不涉及猪只数量变更 + } + + // 4. 记录猪只从旧批次“迁出”的猪栏日志 + correlationID := uuid.New().String() + logOut := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: fromBatchID, + PenID: penID, + Quantity: -quantity, // 迁出为负数 + Type: models.PigTransferTypeCrossBatch, + CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: fmt.Sprintf("整栏划拨迁出: %d头猪从批次 %v 随猪栏 %v 划拨至批次 %v。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, toBatch.BatchNumber, remarks), + } + if err := s.transferSvc.LogTransfer(tx, logOut); err != nil { + return fmt.Errorf("记录猪栏 %d 迁出日志失败: %w", penID, err) + } + + // 5. 记录猪只到新批次“迁入”的猪栏日志 + logIn := &models.PigTransferLog{ + TransferTime: time.Now(), + PigBatchID: toBatchID, + PenID: penID, + Quantity: quantity, // 迁入为正数 + Type: models.PigTransferTypeCrossBatch, + CorrelationID: correlationID, + OperatorID: operatorID, + Remarks: fmt.Sprintf("整栏划拨迁入: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, fromBatch.BatchNumber, pen.PenNumber, remarks), + } + if err := s.transferSvc.LogTransfer(tx, logIn); err != nil { + return fmt.Errorf("记录猪栏 %d 迁入日志失败: %w", penID, err) + } + + // 7. 通过创建批次日志来修改猪群总数,确保数据可追溯 + now := time.Now() + // 7.1 记录源猪群数量减少 + reasonOutBatch := fmt.Sprintf("整栏划拨: %d头猪随猪栏 %v 从批次 %v 划拨至批次 %v。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, toBatchID, remarks) + err = s.updatePigBatchQuantityTx(tx, operatorID, fromBatchID, models.ChangeTypeTransferOut, -quantity, reasonOutBatch, now) + if err != nil { + return fmt.Errorf("更新源猪群 %v 数量失败: %w", fromBatch.BatchNumber, err) + } + + // 7.2 记录目标猪群数量增加 + reasonInBatch := fmt.Sprintf("整栏划拨: %v头猪随猪栏 %v 从批次 %v 划拨入。备注: %s", quantity, pen.PenNumber, fromBatch.BatchNumber, remarks) + err = s.updatePigBatchQuantityTx(tx, operatorID, toBatchID, models.ChangeTypeTransferIn, quantity, reasonInBatch, now) + if err != nil { + return fmt.Errorf("更新目标猪群 %v 数量失败: %w", toBatch.BatchNumber, err) + } + + return nil + }) } From 67b45d2e05d35b3f7f174b4e3f627bcb06172220 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 18:15:47 +0800 Subject: [PATCH 50/65] =?UTF-8?q?=E5=88=A0=E6=89=B9=E6=AC=A1=E6=97=B6?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E7=8C=AA=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pen_transfer_manager.go | 30 ++++++++++ internal/domain/pig/pig_batch_service.go | 55 +++++++++++++------ .../infra/repository/pig_batch_repository.go | 7 ++- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/internal/domain/pig/pen_transfer_manager.go b/internal/domain/pig/pen_transfer_manager.go index 99bc80d..9de7fbd 100644 --- a/internal/domain/pig/pen_transfer_manager.go +++ b/internal/domain/pig/pen_transfer_manager.go @@ -29,6 +29,9 @@ type PigPenTransferManager interface { // GetTotalPigsInPensForBatchTx 计算指定猪群下所有猪栏的当前总存栏数 GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchID uint) (int, error) + + // ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。 + ReleasePen(tx *gorm.DB, penID uint) error } // pigPenTransferManager 是 PigPenTransferManager 接口的具体实现。 @@ -143,3 +146,30 @@ func (s *pigPenTransferManager) GetTotalPigsInPensForBatchTx(tx *gorm.DB, batchI return totalPigs, nil } + +// ReleasePen 将猪栏的猪群归属移除,并将其状态标记为空闲。 +// 此操作通常在猪栏被清空后调用。 +func (s *pigPenTransferManager) ReleasePen(tx *gorm.DB, penID uint) error { + // 1. 获取猪栏信息 + pen, err := s.penRepo.GetPenByIDTx(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 2. 更新猪栏字段 + // 将 pig_batch_id 设置为 nil (SQL NULL) + // 将 status 设置为 PenStatusEmpty + updates := map[string]interface{}{ + "pig_batch_id": nil, // 使用 nil 来表示 SQL NULL + "status": models.PenStatusEmpty, + } + + if err := s.penRepo.UpdatePenFieldsTx(tx, penID, updates); err != nil { + return fmt.Errorf("释放猪栏 %v 失败: %w", pen.PenNumber, err) + } + + return nil +} diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 55cfc15..cd6383e 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -79,27 +79,46 @@ func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBat // DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。 func (s *pigBatchService) DeletePigBatch(id uint) error { - // 1. 获取猪批次信息 - batch, err := s.GetPigBatch(id) // 复用 GetPigBatch 方法 - if err != nil { - return err // GetPigBatch 已经处理了 ErrRecordNotFound 的情况 - } + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 获取猪批次信息 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return err + } - // 2. 核心业务规则:检查猪批次是否为活跃状态 - if batch.IsActive() { - return ErrPigBatchActive // 如果活跃,则不允许删除 - } + // 2. 核心业务规则:检查猪批次是否为活跃状态 + if batch.IsActive() { + return ErrPigBatchActive // 如果活跃,则不允许删除 + } - // 3. 执行删除 - rowsAffected, err := s.pigBatchRepo.DeletePigBatch(id) - if err != nil { - return err - } - if rowsAffected == 0 { - return ErrPigBatchNotFound - } + // 3. 释放所有关联的猪栏 + // 获取该批次下所有猪栏 + pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id) + if err != nil { + return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err) + } - return nil + // 逐一释放猪栏 + for _, pen := range pensInBatch { + if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil { + return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err) + } + } + + // 4. 执行删除猪批次 + rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id) + if err != nil { + return err + } + if rowsAffected == 0 { + return ErrPigBatchNotFound + } + + return nil + }) } // ListPigBatches 实现了批量查询猪批次的逻辑。 diff --git a/internal/infra/repository/pig_batch_repository.go b/internal/infra/repository/pig_batch_repository.go index 3b57b72..1c46c5a 100644 --- a/internal/infra/repository/pig_batch_repository.go +++ b/internal/infra/repository/pig_batch_repository.go @@ -15,6 +15,7 @@ type PigBatchRepository interface { UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, int64, error) // DeletePigBatch 根据ID删除一个猪批次,返回受影响的行数和错误 DeletePigBatch(id uint) (int64, error) + DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) } @@ -58,7 +59,11 @@ func (r *gormPigBatchRepository) UpdatePigBatch(batch *models.PigBatch) (*models // DeletePigBatch 根据ID删除一个猪批次 (GORM 会执行软删除) func (r *gormPigBatchRepository) DeletePigBatch(id uint) (int64, error) { - result := r.db.Delete(&models.PigBatch{}, id) + return r.DeletePigBatchTx(r.db, id) +} + +func (r *gormPigBatchRepository) DeletePigBatchTx(tx *gorm.DB, id uint) (int64, error) { + result := tx.Delete(&models.PigBatch{}, id) if result.Error != nil { return 0, result.Error } From 691810c591a311ae7abfaef2c686c98bd14b429a Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 18:50:22 +0800 Subject: [PATCH 51/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20SickPigManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_sick_manager.go | 99 +++++++++++++++++++ internal/infra/models/pig_sick.go | 1 - .../infra/repository/pig_sick_repository.go | 29 +++++- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/internal/domain/pig/pig_sick_manager.go b/internal/domain/pig/pig_sick_manager.go index b93e217..5f10870 100644 --- a/internal/domain/pig/pig_sick_manager.go +++ b/internal/domain/pig/pig_sick_manager.go @@ -1,12 +1,24 @@ package pig import ( + "errors" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "gorm.io/gorm" ) // SickPigManager 定义了与病猪管理相关的操作接口。 // 这是一个领域服务,负责协调病猪记录、用药等业务逻辑。 type SickPigManager interface { + // ProcessSickPigLog 处理病猪相关的日志事件。 + // log 包含事件的基本信息,如 PigBatchID, PenID, PigIDs, ChangeCount, Reason, TreatmentLocation, Remarks, OperatorID, HappenedAt。 + // Manager 内部会计算并填充 BeforeCount 和 AfterCount,并进行必要的业务校验和副作用处理。 + ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error + + // GetCurrentSickPigCount 获取指定批次当前患病猪只的总数 + GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error) } // sickPigManager 是 SickPigManager 接口的具体实现。 @@ -26,3 +38,90 @@ func NewSickPigManager( medicationLogRepo: medicationLogRepo, } } + +func (s *sickPigManager) ProcessSickPigLog(tx *gorm.DB, log *models.PigSickLog) error { + // 1. 输入校验 + if log == nil { + return errors.New("病猪日志不能为空") + } + + // 关键字段校验 + var missingFields []string + if log.PigBatchID == 0 { + missingFields = append(missingFields, "PigBatchID") + } + if log.ChangeCount == 0 { + missingFields = append(missingFields, "ChangeCount") + } + if log.Reason == "" { + missingFields = append(missingFields, "Reason") + } + if log.TreatmentLocation == "" { + missingFields = append(missingFields, "TreatmentLocation") + } + if log.HappenedAt.IsZero() { + missingFields = append(missingFields, "HappenedAt") + } + if log.OperatorID == 0 { + missingFields = append(missingFields, "OperatorID") + } + if log.PenID == 0 { + missingFields = append(missingFields, "PenID") + } + + if len(missingFields) > 0 { + return fmt.Errorf("以下关键字段不能为空或零值: %v", missingFields) + } + + // 业务规则校验 - ChangeCount 与 Reason 的一致性 + switch log.Reason { + case models.SickPigReasonTypeIllness, models.SickPigReasonTypeTransferIn: + if log.ChangeCount < 0 { + return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为正数", log.Reason) + } + case models.SickPigReasonTypeRecovery, models.SickPigReasonTypeDeath, models.SickPigReasonTypeEliminate, models.SickPigReasonTypeTransferOut: + if log.ChangeCount > 0 { + return fmt.Errorf("原因 '%s' 的 ChangeCount 必须为负数", log.Reason) + } + case models.SickPigReasonTypeOther: + // 其他原因,ChangeCount 可以是任意值,但不能为0 + if log.ChangeCount == 0 { + return errors.New("原因 '其他' 的 ChangeCount 不能为零") + } + default: + return fmt.Errorf("未知的病猪日志原因类型: %s", log.Reason) + } + + // 2. 获取当前病猪数量 (BeforeCount) + beforeCount, err := s.GetCurrentSickPigCount(tx, log.PigBatchID) + if err != nil { + return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", log.PigBatchID, err) + } + log.BeforeCount = beforeCount + + // 3. 计算变化后的数量 (AfterCount) + log.AfterCount = log.BeforeCount + log.ChangeCount + + // 4. 业务规则校验 - 数量合法性 + if log.AfterCount < 0 { + return fmt.Errorf("操作后病猪数量不能为负数,当前 %d,变化 %d", log.BeforeCount, log.ChangeCount) + } + + // 5. 持久化 PigSickLog + if err := s.sickLogRepo.CreatePigSickLogTx(tx, log); err != nil { + return fmt.Errorf("创建 PigSickLog 失败: %w", err) + } + + return nil +} + +func (s *sickPigManager) GetCurrentSickPigCount(tx *gorm.DB, batchID uint) (int, error) { + lastLog, err := s.sickLogRepo.GetLastLogByBatchTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, nil // 如果没有找到任何日志,表示当前病猪数量为0 + } + return 0, fmt.Errorf("获取批次 %d 的最新病猪日志失败: %w", batchID, err) + } + return lastLog.AfterCount, nil +} diff --git a/internal/infra/models/pig_sick.go b/internal/infra/models/pig_sick.go index 9b8ef3f..caf21a9 100644 --- a/internal/infra/models/pig_sick.go +++ b/internal/infra/models/pig_sick.go @@ -32,7 +32,6 @@ type PigSickLog struct { gorm.Model PigBatchID uint `gorm:"primaryKey;comment:关联的猪批次ID"` PenID uint `gorm:"not null;index;comment:所在猪圈ID"` - PigIDs string `gorm:"size:500;comment:涉及的猪只ID列表,逗号分隔"` ChangeCount int `gorm:"not null;comment:变化数量, 正数表示新增, 负数表示移除"` Reason PigBatchSickPigReasonType `gorm:"size:20;not null;comment:变化原因 (如: 患病, 康复, 死亡, 转入, 转出, 其他)"` BeforeCount int `gorm:"comment:变化前的数量"` diff --git a/internal/infra/repository/pig_sick_repository.go b/internal/infra/repository/pig_sick_repository.go index c2ff29d..0e5dd36 100644 --- a/internal/infra/repository/pig_sick_repository.go +++ b/internal/infra/repository/pig_sick_repository.go @@ -1,13 +1,20 @@ package repository import ( + "errors" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "gorm.io/gorm" ) // PigSickLogRepository 定义了与病猪日志模型相关的数据库操作接口。 type PigSickLogRepository interface { + // CreatePigSickLog 创建一条新的病猪日志记录 CreatePigSickLog(log *models.PigSickLog) error + CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error + + // GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录 + GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error) } // gormPigSickLogRepository 是 PigSickLogRepository 接口的 GORM 实现。 @@ -22,5 +29,25 @@ func NewGormPigSickLogRepository(db *gorm.DB) PigSickLogRepository { // CreatePigSickLog 创建一条新的病猪日志记录 func (r *gormPigSickLogRepository) CreatePigSickLog(log *models.PigSickLog) error { - return r.db.Create(log).Error + return r.CreatePigSickLogTx(r.db, log) +} +func (r *gormPigSickLogRepository) CreatePigSickLogTx(tx *gorm.DB, log *models.PigSickLog) error { + return tx.Create(log).Error +} + +// GetLastLogByBatchTx 在事务中获取指定批次和猪栏的最新一条 PigSickLog 记录 +func (r *gormPigSickLogRepository) GetLastLogByBatchTx(tx *gorm.DB, batchID uint) (*models.PigSickLog, error) { + var lastLog models.PigSickLog + err := tx. + Where("pig_batch_id = ?", batchID). + Order("happened_at DESC"). // 按时间降序排列 + First(&lastLog).Error // 获取第一条记录 (即最新一条) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, gorm.ErrRecordNotFound // 明确返回记录未找到错误 + } + return nil, err + } + return &lastLog, nil } From 84c22e342cfae6e2e49257620231980b9d81fa39 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 21:27:23 +0800 Subject: [PATCH 52/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=97=85=E7=8C=AA?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 10 +++++++ .../domain/pig/pig_batch_service_pig_sick.go | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 internal/domain/pig/pig_batch_service_pig_sick.go diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go index 4576d7e..7d14efd 100644 --- a/internal/domain/pig/pig_batch.go +++ b/internal/domain/pig/pig_batch.go @@ -67,6 +67,16 @@ type PigBatchService interface { // ---调栏子服务 --- TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + + // --- 病猪管理相关方法 --- + // RecordSickPigs 记录新增病猪事件。 + RecordSickPigs(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, diagnosis string, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigRecovery 记录病猪康复事件。 + RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigDeath 记录病猪死亡事件。 + RecordSickPigDeath(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigCull 记录病猪淘汰事件。 + RecordSickPigCull(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error } // pigBatchService 是 PigBatchService 接口的具体实现。 diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go new file mode 100644 index 0000000..f088a06 --- /dev/null +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -0,0 +1,27 @@ +package pig + +import ( + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// RecordSickPigs 记录新增病猪事件。 +func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, diagnosis string, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + panic("implement me") +} + +// RecordSickPigRecovery 记录病猪康复事件。 +func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + panic("implement me") +} + +// RecordSickPigDeath 记录病猪死亡事件。 +func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + panic("implement me") +} + +// RecordSickPigCull 记录病猪淘汰事件。 +func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + panic("implement me") +} From 4fb8729a2aaa3840b37d4f63643201caf74444ac Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 21:50:39 +0800 Subject: [PATCH 53/65] =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=97=85=E7=8C=AA?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch.go | 111 ------ internal/domain/pig/pig_batch_service.go | 363 +++++------------- .../domain/pig/pig_batch_service_method.go | 296 ++++++++++++++ .../domain/pig/pig_batch_service_pig_sick.go | 16 +- 4 files changed, 396 insertions(+), 390 deletions(-) delete mode 100644 internal/domain/pig/pig_batch.go create mode 100644 internal/domain/pig/pig_batch_service_method.go diff --git a/internal/domain/pig/pig_batch.go b/internal/domain/pig/pig_batch.go deleted file mode 100644 index 7d14efd..0000000 --- a/internal/domain/pig/pig_batch.go +++ /dev/null @@ -1,111 +0,0 @@ -package pig - -import ( - "errors" - "time" - - "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" -) - -// --- 业务错误定义 --- - -var ( - // ErrPigBatchNotFound 表示当尝试访问一个不存在的猪批次时发生的错误。 - ErrPigBatchNotFound = errors.New("指定的猪批次不存在") - // ErrPigBatchActive 表示当尝试对一个活跃的猪批次执行不允许的操作(如删除)时发生的错误。 - ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") - // ErrPigBatchNotActive 表示当猪批次不处于活跃状态,但执行了需要其活跃的操作时发生的错误。 - ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") - // ErrPenOccupiedByOtherBatch 表示当尝试将一个已经被其他批次占用的猪栏分配给新批次时发生的错误。 - ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") - // ErrPenStatusInvalidForAllocation 表示猪栏的当前状态(例如,'维修中')不允许被分配。 - ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") - // ErrPenNotFound 表示猪栏不存在 - ErrPenNotFound = errors.New("指定的猪栏不存在") - // ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联 - ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") - // ErrInvalidOperation 非法操作 - ErrInvalidOperation = errors.New("非法操作") -) - -// --- 领域服务接口 --- - -// PigBatchService 定义了猪批次管理的核心业务逻辑接口。 -// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 -type PigBatchService interface { - // CreatePigBatch 创建猪批次,并记录初始日志。 - CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) - // GetPigBatch 获取单个猪批次。 - GetPigBatch(id uint) (*models.PigBatch, error) - // UpdatePigBatch 更新猪批次信息。 - UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) - // DeletePigBatch 删除猪批次,包含业务规则校验。 - DeletePigBatch(id uint) error - // ListPigBatches 批量查询猪批次。 - ListPigBatches(isActive *bool) ([]*models.PigBatch, error) - // UpdatePigBatchPens 更新猪批次关联的猪栏。 - UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error - // AssignEmptyPensToBatch 为猪群分配空栏 - AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error - // MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 - MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error - // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 - ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error - - // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 - GetCurrentPigQuantity(batchID uint) (int, error) - - UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error - - // ---交易子服务--- - // SellPigs 处理卖猪的业务逻辑。 - SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error - // BuyPigs 处理买猪的业务逻辑。 - BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error - - // ---调栏子服务 --- - TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error - TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error - - // --- 病猪管理相关方法 --- - // RecordSickPigs 记录新增病猪事件。 - RecordSickPigs(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, diagnosis string, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error - // RecordSickPigRecovery 记录病猪康复事件。 - RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error - // RecordSickPigDeath 记录病猪死亡事件。 - RecordSickPigDeath(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error - // RecordSickPigCull 记录病猪淘汰事件。 - RecordSickPigCull(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error -} - -// pigBatchService 是 PigBatchService 接口的具体实现。 -// 它作为猪群领域的主服务,封装了所有业务逻辑。 -type pigBatchService struct { - pigBatchRepo repository.PigBatchRepository // 猪批次仓库 - pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 - uow repository.UnitOfWork // 工作单元,用于管理事务 - transferSvc PigPenTransferManager // 调栏子服务 - tradeSvc PigTradeManager // 交易子服务 - sickSvc SickPigManager // 病猪子服务 -} - -// NewPigBatchService 是 pigBatchService 的构造函数。 -// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 -func NewPigBatchService( - pigBatchRepo repository.PigBatchRepository, - pigBatchLogRepo repository.PigBatchLogRepository, - uow repository.UnitOfWork, - transferSvc PigPenTransferManager, - tradeSvc PigTradeManager, - sickSvc SickPigManager, -) PigBatchService { - return &pigBatchService{ - pigBatchRepo: pigBatchRepo, - pigBatchLogRepo: pigBatchLogRepo, - uow: uow, - transferSvc: transferSvc, - tradeSvc: tradeSvc, - sickSvc: sickSvc, - } -} diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index cd6383e..39c4e92 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -2,295 +2,110 @@ package pig import ( "errors" - "fmt" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "gorm.io/gorm" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" ) -// --- 领域服务实现 --- +// --- 业务错误定义 --- -// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。 -func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) { - // 业务规则可以在这里添加,例如检查批次号是否唯一等 +var ( + // ErrPigBatchNotFound 表示当尝试访问一个不存在的猪批次时发生的错误。 + ErrPigBatchNotFound = errors.New("指定的猪批次不存在") + // ErrPigBatchActive 表示当尝试对一个活跃的猪批次执行不允许的操作(如删除)时发生的错误。 + ErrPigBatchActive = errors.New("活跃的猪批次不能被删除") + // ErrPigBatchNotActive 表示当猪批次不处于活跃状态,但执行了需要其活跃的操作时发生的错误。 + ErrPigBatchNotActive = errors.New("猪批次不处于活跃状态,无法修改关联猪栏") + // ErrPenOccupiedByOtherBatch 表示当尝试将一个已经被其他批次占用的猪栏分配给新批次时发生的错误。 + ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") + // ErrPenStatusInvalidForAllocation 表示猪栏的当前状态(例如,'维修中')不允许被分配。 + ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") + // ErrPenNotFound 表示猪栏不存在 + ErrPenNotFound = errors.New("指定的猪栏不存在") + // ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联 + ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") + // ErrInvalidOperation 非法操作 + ErrInvalidOperation = errors.New("非法操作") +) - var createdBatch *models.PigBatch - err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 创建猪批次 - // 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法 - var err error - createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch) - if err != nil { - return fmt.Errorf("创建猪批次失败: %w", err) - } +// --- 领域服务接口 --- - // 2. 创建初始批次日志 - initialLog := &models.PigBatchLog{ - PigBatchID: createdBatch.ID, - HappenedAt: time.Now(), - ChangeType: models.ChangeTypeCorrection, // 初始创建可视为一种校正 - ChangeCount: createdBatch.InitialCount, - Reason: fmt.Sprintf("创建了新的猪批次 %s,初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount), - BeforeCount: 0, // 初始创建前数量为0 - AfterCount: createdBatch.InitialCount, - OperatorID: operatorID, - } +// PigBatchService 定义了猪批次管理的核心业务逻辑接口。 +// 它抽象了所有与猪批次相关的操作,使得应用层可以依赖于此接口,而不是具体的实现。 +type PigBatchService interface { + // CreatePigBatch 创建猪批次,并记录初始日志。 + CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) + // GetPigBatch 获取单个猪批次。 + GetPigBatch(id uint) (*models.PigBatch, error) + // UpdatePigBatch 更新猪批次信息。 + UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) + // DeletePigBatch 删除猪批次,包含业务规则校验。 + DeletePigBatch(id uint) error + // ListPigBatches 批量查询猪批次。 + ListPigBatches(isActive *bool) ([]*models.PigBatch, error) + // UpdatePigBatchPens 更新猪批次关联的猪栏。 + UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error + // AssignEmptyPensToBatch 为猪群分配空栏 + AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error + // MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 + MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error + // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 + ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error - // 3. 记录批次日志 - if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil { - return fmt.Errorf("记录初始批次日志失败: %w", err) - } + // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 + GetCurrentPigQuantity(batchID uint) (int, error) - return nil - }) + UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error - if err != nil { - return nil, err - } + // ---交易子服务--- + // SellPigs 处理卖猪的业务逻辑。 + SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + // BuyPigs 处理买猪的业务逻辑。 + BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error - return createdBatch, nil + // ---调栏子服务 --- + TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + + // --- 病猪管理相关方法 --- + // RecordSickPigs 记录新增病猪事件。 + RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigRecovery 记录病猪康复事件。 + RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigDeath 记录病猪死亡事件。 + RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + // RecordSickPigCull 记录病猪淘汰事件。 + RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error } -// GetPigBatch 实现了获取单个猪批次的逻辑。 -func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) { - batch, err := s.pigBatchRepo.GetPigBatchByID(id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrPigBatchNotFound - } - return nil, err - } - return batch, nil +// pigBatchService 是 PigBatchService 接口的具体实现。 +// 它作为猪群领域的主服务,封装了所有业务逻辑。 +type pigBatchService struct { + pigBatchRepo repository.PigBatchRepository // 猪批次仓库 + pigBatchLogRepo repository.PigBatchLogRepository // 猪批次日志仓库 + uow repository.UnitOfWork // 工作单元,用于管理事务 + transferSvc PigPenTransferManager // 调栏子服务 + tradeSvc PigTradeManager // 交易子服务 + sickSvc SickPigManager // 病猪子服务 } -// UpdatePigBatch 实现了更新猪批次的逻辑。 -func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { - // 可以在这里添加更新前的业务校验 - updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch) - if err != nil { - return nil, err +// NewPigBatchService 是 pigBatchService 的构造函数。 +// 它通过依赖注入的方式,创建并返回一个 PigBatchService 接口的实例。 +func NewPigBatchService( + pigBatchRepo repository.PigBatchRepository, + pigBatchLogRepo repository.PigBatchLogRepository, + uow repository.UnitOfWork, + transferSvc PigPenTransferManager, + tradeSvc PigTradeManager, + sickSvc SickPigManager, +) PigBatchService { + return &pigBatchService{ + pigBatchRepo: pigBatchRepo, + pigBatchLogRepo: pigBatchLogRepo, + uow: uow, + transferSvc: transferSvc, + tradeSvc: tradeSvc, + sickSvc: sickSvc, } - if rowsAffected == 0 { - return nil, ErrPigBatchNotFound // 如果没有行被更新,可能意味着记录不存在 - } - return updatedBatch, nil -} - -// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。 -func (s *pigBatchService) DeletePigBatch(id uint) error { - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 获取猪批次信息 - batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法 - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound - } - return err - } - - // 2. 核心业务规则:检查猪批次是否为活跃状态 - if batch.IsActive() { - return ErrPigBatchActive // 如果活跃,则不允许删除 - } - - // 3. 释放所有关联的猪栏 - // 获取该批次下所有猪栏 - pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id) - if err != nil { - return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err) - } - - // 逐一释放猪栏 - for _, pen := range pensInBatch { - if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil { - return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err) - } - } - - // 4. 执行删除猪批次 - rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id) - if err != nil { - return err - } - if rowsAffected == 0 { - return ErrPigBatchNotFound - } - - return nil - }) -} - -// ListPigBatches 实现了批量查询猪批次的逻辑。 -func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) { - return s.pigBatchRepo.ListPigBatches(isActive) -} - -// UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 -// 它通过调用底层的 PigPenTransferManager 来执行数据库操作,从而保持了职责的清晰。 -func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { - // 使用工作单元来确保操作的原子性 - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 验证猪批次是否存在且活跃 - // 注意: 此处依赖一个假设存在的 pigBatchRepo.GetPigBatchByIDTx 方法 - pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound - } - return fmt.Errorf("获取猪批次信息失败: %w", err) - } - - if !pigBatch.IsActive() { - return ErrPigBatchNotActive - } - - // 2. 获取当前关联的猪栏 (通过子服务) - currentPens, err := s.transferSvc.GetPensByBatchID(tx, batchID) - if err != nil { - return fmt.Errorf("获取当前关联猪栏失败: %w", err) - } - - currentPenMap := make(map[uint]models.Pen) - currentPenIDsSet := make(map[uint]struct{}) - for _, pen := range currentPens { - currentPenMap[pen.ID] = *pen - currentPenIDsSet[pen.ID] = struct{}{} - } - - // 3. 构建期望猪栏ID集合 - desiredPenIDsSet := make(map[uint]struct{}) - for _, penID := range desiredPenIDs { - desiredPenIDsSet[penID] = struct{}{} - } - - // 4. 计算需要添加和移除的猪栏 - var pensToRemove []uint - for penID := range currentPenIDsSet { - if _, found := desiredPenIDsSet[penID]; !found { - pensToRemove = append(pensToRemove, penID) - } - } - - var pensToAdd []uint - for _, penID := range desiredPenIDs { - if _, found := currentPenIDsSet[penID]; !found { - pensToAdd = append(pensToAdd, penID) - } - } - - // 5. 处理移除猪栏的逻辑 - for _, penID := range pensToRemove { - currentPen := currentPenMap[penID] - updates := make(map[string]interface{}) - updates["pig_batch_id"] = nil - - if currentPen.Status == models.PenStatusOccupied { - updates["status"] = models.PenStatusEmpty - } - - if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { - return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) - } - } - - // 6. 处理添加猪栏的逻辑 - for _, penID := range pensToAdd { - // 通过子服务获取猪栏信息 - actualPen, err := s.transferSvc.GetPenByID(tx, penID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) - } - return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) - } - - // 核心业务规则:校验猪栏是否可被分配 - if actualPen.Status != models.PenStatusEmpty { - return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) - } - if actualPen.PigBatchID != nil { - return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) - } - - updates := map[string]interface{}{ - "pig_batch_id": &batchID, - "status": models.PenStatusOccupied, - } - if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { - return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) - } - } - - return nil - }) -} - -// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。 -func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) { - var getErr error - var quantity int - err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID) - return getErr - }) - if err != nil { - return 0, err - } - return quantity, nil -} - -// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。 -func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) { - // 1. 获取猪批次初始信息 - batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, ErrPigBatchNotFound - } - return 0, fmt.Errorf("获取猪批次 %d 初始信息失败: %w", batchID, err) - } - - // 2. 尝试获取该批次的最后一条日志记录 - lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量 - return batch.InitialCount, nil - } - return 0, fmt.Errorf("获取猪批次 %d 最后一条日志失败: %w", batchID, err) - } - - // 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount - return lastLog.AfterCount, nil -} - -func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt) - }) -} - -func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { - lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) - if err != nil { - return err - } - // 检查数量不应该减到小于零 - if changeAmount < 0 { - if lastLog.AfterCount+changeAmount < 0 { - return ErrInvalidOperation - } - } - pigBatchLog := &models.PigBatchLog{ - PigBatchID: batchID, - ChangeType: changeType, - ChangeCount: changeAmount, - Reason: changeReason, - BeforeCount: lastLog.AfterCount, - AfterCount: lastLog.AfterCount + changeAmount, - OperatorID: operatorID, - HappenedAt: happenedAt, - } - return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog) } diff --git a/internal/domain/pig/pig_batch_service_method.go b/internal/domain/pig/pig_batch_service_method.go new file mode 100644 index 0000000..cd6383e --- /dev/null +++ b/internal/domain/pig/pig_batch_service_method.go @@ -0,0 +1,296 @@ +package pig + +import ( + "errors" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// --- 领域服务实现 --- + +// CreatePigBatch 实现了创建猪批次的逻辑,并同时创建初始批次日志。 +func (s *pigBatchService) CreatePigBatch(operatorID uint, batch *models.PigBatch) (*models.PigBatch, error) { + // 业务规则可以在这里添加,例如检查批次号是否唯一等 + + var createdBatch *models.PigBatch + err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 创建猪批次 + // 注意: 此处依赖一个假设存在的 pigBatchRepo.CreatePigBatchTx 方法 + var err error + createdBatch, err = s.pigBatchRepo.CreatePigBatchTx(tx, batch) + if err != nil { + return fmt.Errorf("创建猪批次失败: %w", err) + } + + // 2. 创建初始批次日志 + initialLog := &models.PigBatchLog{ + PigBatchID: createdBatch.ID, + HappenedAt: time.Now(), + ChangeType: models.ChangeTypeCorrection, // 初始创建可视为一种校正 + ChangeCount: createdBatch.InitialCount, + Reason: fmt.Sprintf("创建了新的猪批次 %s,初始数量 %d", createdBatch.BatchNumber, createdBatch.InitialCount), + BeforeCount: 0, // 初始创建前数量为0 + AfterCount: createdBatch.InitialCount, + OperatorID: operatorID, + } + + // 3. 记录批次日志 + if err := s.pigBatchLogRepo.CreateTx(tx, initialLog); err != nil { + return fmt.Errorf("记录初始批次日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return createdBatch, nil +} + +// GetPigBatch 实现了获取单个猪批次的逻辑。 +func (s *pigBatchService) GetPigBatch(id uint) (*models.PigBatch, error) { + batch, err := s.pigBatchRepo.GetPigBatchByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrPigBatchNotFound + } + return nil, err + } + return batch, nil +} + +// UpdatePigBatch 实现了更新猪批次的逻辑。 +func (s *pigBatchService) UpdatePigBatch(batch *models.PigBatch) (*models.PigBatch, error) { + // 可以在这里添加更新前的业务校验 + updatedBatch, rowsAffected, err := s.pigBatchRepo.UpdatePigBatch(batch) + if err != nil { + return nil, err + } + if rowsAffected == 0 { + return nil, ErrPigBatchNotFound // 如果没有行被更新,可能意味着记录不存在 + } + return updatedBatch, nil +} + +// DeletePigBatch 实现了删除猪批次的逻辑,并包含业务规则校验。 +func (s *pigBatchService) DeletePigBatch(id uint) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 获取猪批次信息 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, id) // 使用事务内方法 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return err + } + + // 2. 核心业务规则:检查猪批次是否为活跃状态 + if batch.IsActive() { + return ErrPigBatchActive // 如果活跃,则不允许删除 + } + + // 3. 释放所有关联的猪栏 + // 获取该批次下所有猪栏 + pensInBatch, err := s.transferSvc.GetPensByBatchID(tx, id) + if err != nil { + return fmt.Errorf("获取猪批次 %d 关联猪栏失败: %w", id, err) + } + + // 逐一释放猪栏 + for _, pen := range pensInBatch { + if err := s.transferSvc.ReleasePen(tx, pen.ID); err != nil { + return fmt.Errorf("释放猪栏 %d 失败: %w", pen.ID, err) + } + } + + // 4. 执行删除猪批次 + rowsAffected, err := s.pigBatchRepo.DeletePigBatchTx(tx, id) + if err != nil { + return err + } + if rowsAffected == 0 { + return ErrPigBatchNotFound + } + + return nil + }) +} + +// ListPigBatches 实现了批量查询猪批次的逻辑。 +func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, error) { + return s.pigBatchRepo.ListPigBatches(isActive) +} + +// UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 +// 它通过调用底层的 PigPenTransferManager 来执行数据库操作,从而保持了职责的清晰。 +func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { + // 使用工作单元来确保操作的原子性 + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 验证猪批次是否存在且活跃 + // 注意: 此处依赖一个假设存在的 pigBatchRepo.GetPigBatchByIDTx 方法 + pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取猪批次信息失败: %w", err) + } + + if !pigBatch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 获取当前关联的猪栏 (通过子服务) + currentPens, err := s.transferSvc.GetPensByBatchID(tx, batchID) + if err != nil { + return fmt.Errorf("获取当前关联猪栏失败: %w", err) + } + + currentPenMap := make(map[uint]models.Pen) + currentPenIDsSet := make(map[uint]struct{}) + for _, pen := range currentPens { + currentPenMap[pen.ID] = *pen + currentPenIDsSet[pen.ID] = struct{}{} + } + + // 3. 构建期望猪栏ID集合 + desiredPenIDsSet := make(map[uint]struct{}) + for _, penID := range desiredPenIDs { + desiredPenIDsSet[penID] = struct{}{} + } + + // 4. 计算需要添加和移除的猪栏 + var pensToRemove []uint + for penID := range currentPenIDsSet { + if _, found := desiredPenIDsSet[penID]; !found { + pensToRemove = append(pensToRemove, penID) + } + } + + var pensToAdd []uint + for _, penID := range desiredPenIDs { + if _, found := currentPenIDsSet[penID]; !found { + pensToAdd = append(pensToAdd, penID) + } + } + + // 5. 处理移除猪栏的逻辑 + for _, penID := range pensToRemove { + currentPen := currentPenMap[penID] + updates := make(map[string]interface{}) + updates["pig_batch_id"] = nil + + if currentPen.Status == models.PenStatusOccupied { + updates["status"] = models.PenStatusEmpty + } + + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) + } + } + + // 6. 处理添加猪栏的逻辑 + for _, penID := range pensToAdd { + // 通过子服务获取猪栏信息 + actualPen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) + } + return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) + } + + // 核心业务规则:校验猪栏是否可被分配 + if actualPen.Status != models.PenStatusEmpty { + return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) + } + if actualPen.PigBatchID != nil { + return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) + } + + updates := map[string]interface{}{ + "pig_batch_id": &batchID, + "status": models.PenStatusOccupied, + } + if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { + return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) + } + } + + return nil + }) +} + +// GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。 +func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) { + var getErr error + var quantity int + err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + quantity, getErr = s.getCurrentPigQuantityTx(tx, batchID) + return getErr + }) + if err != nil { + return 0, err + } + return quantity, nil +} + +// getCurrentPigQuantityTx 实现了获取指定猪批次的当前猪只数量的逻辑。 +func (s *pigBatchService) getCurrentPigQuantityTx(tx *gorm.DB, batchID uint) (int, error) { + // 1. 获取猪批次初始信息 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, ErrPigBatchNotFound + } + return 0, fmt.Errorf("获取猪批次 %d 初始信息失败: %w", batchID, err) + } + + // 2. 尝试获取该批次的最后一条日志记录 + lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // 如果没有找到任何日志记录(除了初始创建),则当前数量就是初始数量 + return batch.InitialCount, nil + } + return 0, fmt.Errorf("获取猪批次 %d 最后一条日志失败: %w", batchID, err) + } + + // 3. 如果找到最后一条日志,则当前数量为该日志的 AfterCount + return lastLog.AfterCount, nil +} + +func (s *pigBatchService) UpdatePigBatchQuantity(operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + return s.updatePigBatchQuantityTx(tx, operatorID, batchID, changeType, changeAmount, changeReason, happenedAt) + }) +} + +func (s *pigBatchService) updatePigBatchQuantityTx(tx *gorm.DB, operatorID uint, batchID uint, changeType models.LogChangeType, changeAmount int, changeReason string, happenedAt time.Time) error { + lastLog, err := s.pigBatchLogRepo.GetLastLogByBatchIDTx(tx, batchID) + if err != nil { + return err + } + // 检查数量不应该减到小于零 + if changeAmount < 0 { + if lastLog.AfterCount+changeAmount < 0 { + return ErrInvalidOperation + } + } + pigBatchLog := &models.PigBatchLog{ + PigBatchID: batchID, + ChangeType: changeType, + ChangeCount: changeAmount, + Reason: changeReason, + BeforeCount: lastLog.AfterCount, + AfterCount: lastLog.AfterCount + changeAmount, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + return s.pigBatchLogRepo.CreateTx(tx, pigBatchLog) +} diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index f088a06..fecac92 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -7,21 +7,27 @@ import ( ) // RecordSickPigs 记录新增病猪事件。 -func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, diagnosis string, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { - panic("implement me") +func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + // 1. 检查批次是否活跃 + + // 2. 检查猪栏是否关联 + + // 3. 检查剩余健康猪不能少于即将转化的病猪数量 + + // 4. 创建病猪日志 } // RecordSickPigRecovery 记录病猪康复事件。 -func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { +func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { panic("implement me") } // RecordSickPigDeath 记录病猪死亡事件。 -func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { +func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { panic("implement me") } // RecordSickPigCull 记录病猪淘汰事件。 -func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, pigIDs string, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { +func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { panic("implement me") } From 9a7b765b7113bab21c14fef00c20511b5dd574e3 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 21:54:55 +0800 Subject: [PATCH 54/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20RecordSickPigs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/pig/pig_batch_service_pig_sick.go | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index fecac92..2897268 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -1,20 +1,87 @@ package pig import ( + "errors" + "fmt" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" ) // RecordSickPigs 记录新增病猪事件。 func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { - // 1. 检查批次是否活跃 + if quantity <= 0 { + return errors.New("新增病猪数量必须大于0") + } - // 2. 检查猪栏是否关联 + var err error + // 1. 开启事务 + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1.1 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录病猪事件", batchID) + } - // 3. 检查剩余健康猪不能少于即将转化的病猪数量 + // 1.2 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } - // 4. 创建病猪日志 + // 1.3 检查剩余健康猪不能少于即将转化的病猪数量 + totalPigsInBatch, err := s.getCurrentPigQuantityTx(tx, batchID) + if err != nil { + return fmt.Errorf("获取批次 %d 总猪只数量失败: %w", batchID, err) + } + + currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID) + if err != nil { + return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err) + } + + healthyPigs := totalPigsInBatch - currentSickPigs + if healthyPigs < quantity { + return fmt.Errorf("健康猪数量不足,当前健康猪 %d 头,尝试记录病猪 %d 头", healthyPigs, quantity) + } + + // 1.4 创建病猪日志 + sickLog := &models.PigSickLog{ + PigBatchID: batchID, + PenID: penID, + ChangeCount: quantity, // 新增病猪,ChangeCount 为正数 + Reason: models.SickPigReasonTypeIllness, + TreatmentLocation: treatmentLocation, + Remarks: remarks, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + + if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil { + return fmt.Errorf("处理病猪日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录新增病猪事件失败: %w", err) + } + + return nil } // RecordSickPigRecovery 记录病猪康复事件。 From 73de8ad04f77449498620c764ecf15b20cec1ad2 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 21:57:53 +0800 Subject: [PATCH 55/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20RecordSickPigRecover?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/pig/pig_batch_service_pig_sick.go | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index 2897268..1fd9c2d 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -86,7 +86,70 @@ func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID ui // RecordSickPigRecovery 记录病猪康复事件。 func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { - panic("implement me") + if quantity <= 0 { + return errors.New("康复猪只数量必须大于0") + } + + var err error + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录病猪康复事件", batchID) + } + + // 2. 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } + + // 3. 检查当前病猪数量是否足够康复 + currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID) + if err != nil { + return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err) + } + + if currentSickPigs < quantity { + return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试康复 %d 头", currentSickPigs, quantity) + } + + // 4. 创建病猪日志 + sickLog := &models.PigSickLog{ + PigBatchID: batchID, + PenID: penID, + ChangeCount: -quantity, // 康复病猪,ChangeCount 为负数 + Reason: models.SickPigReasonTypeRecovery, + TreatmentLocation: treatmentLocation, + Remarks: remarks, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + + if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil { + return fmt.Errorf("处理病猪康复日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录病猪康复事件失败: %w", err) + } + + return nil } // RecordSickPigDeath 记录病猪死亡事件。 From 1290676fe41a6ee46dabdb8606b8f5264d6563df Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 22:08:09 +0800 Subject: [PATCH 56/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20RecordSickPigCull=20?= =?UTF-8?q?=E5=92=8C=20RecordSickPigDeath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/pig/pig_batch_service_pig_sick.go | 184 +++++++++++++++++- 1 file changed, 182 insertions(+), 2 deletions(-) diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index 1fd9c2d..53dc18b 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -154,10 +154,190 @@ func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, p // RecordSickPigDeath 记录病猪死亡事件。 func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { - panic("implement me") + if quantity <= 0 { + return errors.New("死亡猪只数量必须大于0") + } + + var err error + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录病猪死亡事件", batchID) + } + + // 2. 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } + + // 3. 检查当前病猪数量是否足够死亡 + currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID) + if err != nil { + return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err) + } + + if currentSickPigs < quantity { + return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试记录死亡 %d 头", currentSickPigs, quantity) + } + + // 4. 检查猪栏内猪只数量是否足够死亡 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err) + } + if currentPigsInPen < quantity { + return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity) + } + + // 5. 创建病猪日志 (减少病猪数量) + sickLog := &models.PigSickLog{ + PigBatchID: batchID, + PenID: penID, + ChangeCount: -quantity, // 死亡病猪,ChangeCount 为负数 + Reason: models.SickPigReasonTypeDeath, + TreatmentLocation: treatmentLocation, + Remarks: remarks, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil { + return fmt.Errorf("处理病猪死亡日志失败: %w", err) + } + + // 6. 更新批次总猪只数量 (减少批次总数) + if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil { + return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err) + } + + // 7. 记录猪只转移日志 (减少猪栏内猪只数量) + transferLog := &models.PigTransferLog{ + TransferTime: happenedAt, + PigBatchID: batchID, + PenID: penID, + Quantity: -quantity, // 减少猪只数量 + Type: models.PigTransferTypeDeath, + OperatorID: operatorID, + Remarks: remarks, + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("记录猪只死亡转移日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录病猪死亡事件失败: %w", err) + } + + return nil } // RecordSickPigCull 记录病猪淘汰事件。 func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { - panic("implement me") + if quantity <= 0 { + return errors.New("淘汰猪只数量必须大于0") + } + + var err error + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录病猪淘汰事件", batchID) + } + + // 2. 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } + + // 3. 检查当前病猪数量是否足够淘汰 + currentSickPigs, err := s.sickSvc.GetCurrentSickPigCount(tx, batchID) + if err != nil { + return fmt.Errorf("获取批次 %d 当前病猪数量失败: %w", batchID, err) + } + + if currentSickPigs < quantity { + return fmt.Errorf("当前病猪数量不足,当前病猪 %d 头,尝试淘汰 %d 头", currentSickPigs, quantity) + } + + // 4. 检查猪栏内猪只数量是否足够淘汰 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err) + } + if currentPigsInPen < quantity { + return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity) + } + + // 5. 创建病猪日志 (减少病猪数量) + sickLog := &models.PigSickLog{ + PigBatchID: batchID, + PenID: penID, + ChangeCount: -quantity, // 淘汰病猪,ChangeCount 为负数 + Reason: models.SickPigReasonTypeEliminate, + TreatmentLocation: treatmentLocation, + Remarks: remarks, + OperatorID: operatorID, + HappenedAt: happenedAt, + } + if err := s.sickSvc.ProcessSickPigLog(tx, sickLog); err != nil { + return fmt.Errorf("处理病猪淘汰日志失败: %w", err) + } + + // 6. 更新批次总猪只数量 (减少批次总数) + if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil { + return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err) + } + + // 7. 记录猪只转移日志 (减少猪栏内猪只数量) + transferLog := &models.PigTransferLog{ + TransferTime: happenedAt, + PigBatchID: batchID, + PenID: penID, + Quantity: -quantity, // 减少猪只数量 + Type: models.PigTransferTypeEliminate, // 淘汰类型 + OperatorID: operatorID, + Remarks: remarks, + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录病猪淘汰事件失败: %w", err) + } + + return nil } From 035da5293bfeb667072330aeec52d984bd6d87ab Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 22:22:10 +0800 Subject: [PATCH 57/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20RecordCull=20?= =?UTF-8?q?=E5=92=8C=20RecordDeath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch_service.go | 6 + .../domain/pig/pig_batch_service_pig_sick.go | 144 +++++++++++++++++- internal/infra/models/pig_transfer.go | 2 +- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 39c4e92..adc5d6a 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -77,6 +77,12 @@ type PigBatchService interface { RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error // RecordSickPigCull 记录病猪淘汰事件。 RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + + // --- 正常猪只管理相关方法 --- + // RecordDeath 记录正常猪只死亡事件。 + RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error + // RecordCull 记录正常猪只淘汰事件。 + RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error } // pigBatchService 是 PigBatchService 接口的具体实现。 diff --git a/internal/domain/pig/pig_batch_service_pig_sick.go b/internal/domain/pig/pig_batch_service_pig_sick.go index 53dc18b..b5bbe78 100644 --- a/internal/domain/pig/pig_batch_service_pig_sick.go +++ b/internal/domain/pig/pig_batch_service_pig_sick.go @@ -323,8 +323,8 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID TransferTime: happenedAt, PigBatchID: batchID, PenID: penID, - Quantity: -quantity, // 减少猪只数量 - Type: models.PigTransferTypeEliminate, // 淘汰类型 + Quantity: -quantity, // 减少猪只数量 + Type: models.PigTransferTypeCull, // 淘汰类型 OperatorID: operatorID, Remarks: remarks, } @@ -341,3 +341,143 @@ func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID return nil } + +// RecordDeath 记录正常猪只死亡事件。 +func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error { + if quantity <= 0 { + return errors.New("死亡猪只数量必须大于0") + } + + var err error + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录死亡事件", batchID) + } + + // 2. 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } + + // 3. 检查猪栏内猪只数量是否足够死亡 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err) + } + if currentPigsInPen < quantity { + return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录死亡 %d 头", penID, currentPigsInPen, quantity) + } + + // 4. 更新批次总猪只数量 (减少批次总数) + if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeDeath, -quantity, remarks, happenedAt); err != nil { + return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err) + } + + // 5. 记录猪只转移日志 (减少猪栏内猪只数量) + transferLog := &models.PigTransferLog{ + TransferTime: happenedAt, + PigBatchID: batchID, + PenID: penID, + Quantity: -quantity, // 减少猪只数量 + Type: models.PigTransferTypeDeath, + OperatorID: operatorID, + Remarks: remarks, + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("记录猪只死亡转移日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录正常猪只死亡事件失败: %w", err) + } + + return nil +} + +// RecordCull 记录正常猪只淘汰事件。 +func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error { + if quantity <= 0 { + return errors.New("淘汰猪只数量必须大于0") + } + + var err error + err = s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查批次是否活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return fmt.Errorf("获取批次 %d 失败: %w", batchID, err) + } + if !batch.IsActive() { + return fmt.Errorf("批次 %d 不活跃,无法记录淘汰事件", batchID) + } + + // 2. 检查猪栏是否关联 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return fmt.Errorf("获取猪栏 %d 失败: %w", penID, err) + } + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return fmt.Errorf("猪栏 %d 未与批次 %d 关联", penID, batchID) + } + + // 3. 检查猪栏内猪只数量是否足够淘汰 + currentPigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return fmt.Errorf("获取猪栏 %d 当前猪只数量失败: %w", penID, err) + } + if currentPigsInPen < quantity { + return fmt.Errorf("猪栏 %d 内猪只数量不足,当前 %d 头,尝试记录淘汰 %d 头", penID, currentPigsInPen, quantity) + } + + // 4. 更新批次总猪只数量 (减少批次总数) + if err := s.UpdatePigBatchQuantity(operatorID, batchID, models.ChangeTypeCull, -quantity, remarks, happenedAt); err != nil { + return fmt.Errorf("更新批次 %d 总猪只数量失败: %w", batchID, err) + } + + // 5. 记录猪只转移日志 (减少猪栏内猪只数量) + transferLog := &models.PigTransferLog{ + TransferTime: happenedAt, + PigBatchID: batchID, + PenID: penID, + Quantity: -quantity, // 减少猪只数量 + Type: models.PigTransferTypeCull, + OperatorID: operatorID, + Remarks: remarks, + } + if err := s.transferSvc.LogTransfer(tx, transferLog); err != nil { + return fmt.Errorf("记录猪只淘汰转移日志失败: %w", err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("记录正常猪只淘汰事件失败: %w", err) + } + + return nil +} diff --git a/internal/infra/models/pig_transfer.go b/internal/infra/models/pig_transfer.go index 70361f2..3fc3a04 100644 --- a/internal/infra/models/pig_transfer.go +++ b/internal/infra/models/pig_transfer.go @@ -14,7 +14,7 @@ const ( PigTransferTypeCrossBatch PigTransferType = "跨群调栏" // 不同猪群间的调动 PigTransferTypeSale PigTransferType = "销售" // 猪只售出 PigTransferTypeDeath PigTransferType = "死亡" // 猪只死亡 - PigTransferTypeEliminate PigTransferType = "淘汰" // 猪只淘汰 + PigTransferTypeCull PigTransferType = "淘汰" // 猪只淘汰 PigTransferTypePurchase PigTransferType = "新购入" // 新购入猪只 PigTransferTypeDeliveryRoomTransfor PigTransferType = "产房转入" // 产房转入 // 可以根据业务需求添加更多类型,例如:转出到其他农场等 From 18b45b223c49f3d3a4107bb7949c5e8bcaede82f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 22:26:57 +0800 Subject: [PATCH 58/65] =?UTF-8?q?=E8=B0=83=E6=95=B4swag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 96 ------------------- docs/swagger.json | 96 ------------------- docs/swagger.yaml | 64 ------------- .../management/pig_batch_controller.go | 16 ---- 4 files changed, 272 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 12ddeb1..5826ff3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -899,12 +899,6 @@ const docTemplate = `{ } ] } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -949,18 +943,6 @@ const docTemplate = `{ } ] } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } @@ -1002,24 +984,6 @@ const docTemplate = `{ } ] } - }, - "400": { - "description": "无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -1071,24 +1035,6 @@ const docTemplate = `{ } ] } - }, - "400": { - "description": "请求参数错误或无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -1116,24 +1062,6 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/controller.Response" } - }, - "400": { - "description": "无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } @@ -1175,30 +1103,6 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/controller.Response" } - }, - "400": { - "description": "请求参数错误或无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次或猪栏不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "409": { - "description": "业务逻辑冲突 (如猪栏已被使用)", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } diff --git a/docs/swagger.json b/docs/swagger.json index 349588d..44315f0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -888,12 +888,6 @@ } ] } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -938,18 +932,6 @@ } ] } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } @@ -991,24 +973,6 @@ } ] } - }, - "400": { - "description": "无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -1060,24 +1024,6 @@ } ] } - }, - "400": { - "description": "请求参数错误或无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } }, @@ -1105,24 +1051,6 @@ "schema": { "$ref": "#/definitions/controller.Response" } - }, - "400": { - "description": "无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } @@ -1164,30 +1092,6 @@ "schema": { "$ref": "#/definitions/controller.Response" } - }, - "400": { - "description": "请求参数错误或无效的ID格式", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "404": { - "description": "猪批次或猪栏不存在", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "409": { - "description": "业务逻辑冲突 (如猪栏已被使用)", - "schema": { - "$ref": "#/definitions/controller.Response" - } - }, - "500": { - "description": "内部服务器错误", - "schema": { - "$ref": "#/definitions/controller.Response" - } } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b74f124..aa93e84 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1344,10 +1344,6 @@ paths: $ref: '#/definitions/dto.PigBatchResponseDTO' type: array type: object - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 获取猪批次列表 tags: - 猪批次管理 @@ -1374,14 +1370,6 @@ paths: data: $ref: '#/definitions/dto.PigBatchResponseDTO' type: object - "400": - description: 请求参数错误 - schema: - $ref: '#/definitions/controller.Response' - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 创建猪批次 tags: - 猪批次管理 @@ -1401,18 +1389,6 @@ paths: description: 删除成功 schema: $ref: '#/definitions/controller.Response' - "400": - description: 无效的ID格式 - schema: - $ref: '#/definitions/controller.Response' - "404": - description: 猪批次不存在 - schema: - $ref: '#/definitions/controller.Response' - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 删除猪批次 tags: - 猪批次管理 @@ -1436,18 +1412,6 @@ paths: data: $ref: '#/definitions/dto.PigBatchResponseDTO' type: object - "400": - description: 无效的ID格式 - schema: - $ref: '#/definitions/controller.Response' - "404": - description: 猪批次不存在 - schema: - $ref: '#/definitions/controller.Response' - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 获取单个猪批次 tags: - 猪批次管理 @@ -1479,18 +1443,6 @@ paths: data: $ref: '#/definitions/dto.PigBatchResponseDTO' type: object - "400": - description: 请求参数错误或无效的ID格式 - schema: - $ref: '#/definitions/controller.Response' - "404": - description: 猪批次不存在 - schema: - $ref: '#/definitions/controller.Response' - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 更新猪批次 tags: - 猪批次管理 @@ -1518,22 +1470,6 @@ paths: description: 更新成功 schema: $ref: '#/definitions/controller.Response' - "400": - description: 请求参数错误或无效的ID格式 - schema: - $ref: '#/definitions/controller.Response' - "404": - description: 猪批次或猪栏不存在 - schema: - $ref: '#/definitions/controller.Response' - "409": - description: 业务逻辑冲突 (如猪栏已被使用) - schema: - $ref: '#/definitions/controller.Response' - "500": - description: 内部服务器错误 - schema: - $ref: '#/definitions/controller.Response' summary: 更新猪批次关联的猪栏 tags: - 猪批次管理 diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 4debac0..a08f83d 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -34,8 +34,6 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) // @Produce json // @Param body body dto.PigBatchCreateDTO true "猪批次信息" // @Success 201 {object} controller.Response{data=dto.PigBatchResponseDTO} "创建成功" -// @Failure 400 {object} controller.Response "请求参数错误" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches [post] func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { const action = "创建猪批次" @@ -64,9 +62,6 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { // @Produce json // @Param id path int true "猪批次ID" // @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功" -// @Failure 400 {object} controller.Response "无效的ID格式" -// @Failure 404 {object} controller.Response "猪批次不存在" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches/{id} [get] func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { const action = "获取猪批次" @@ -99,9 +94,6 @@ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { // @Param id path int true "猪批次ID" // @Param body body dto.PigBatchUpdateDTO true "猪批次信息" // @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "更新成功" -// @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" -// @Failure 404 {object} controller.Response "猪批次不存在" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches/{id} [put] func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { const action = "更新猪批次" @@ -138,9 +130,6 @@ func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { // @Produce json // @Param id path int true "猪批次ID" // @Success 200 {object} controller.Response "删除成功" -// @Failure 400 {object} controller.Response "无效的ID格式" -// @Failure 404 {object} controller.Response "猪批次不存在" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches/{id} [delete] func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { const action = "删除猪批次" @@ -170,7 +159,6 @@ func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { // @Produce json // @Param is_active query bool false "是否活跃 (true/false)" // @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches [get] func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { const action = "获取猪批次列表" @@ -200,10 +188,6 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { // @Param id path int true "猪批次ID" // @Param body body dto.PigBatchUpdatePensRequest true "猪批次关联的猪栏ID列表" // @Success 200 {object} controller.Response "更新成功" -// @Failure 400 {object} controller.Response "请求参数错误或无效的ID格式" -// @Failure 404 {object} controller.Response "猪批次或猪栏不存在" -// @Failure 409 {object} controller.Response "业务逻辑冲突 (如猪栏已被使用)" -// @Failure 500 {object} controller.Response "内部服务器错误" // @Router /api/v1/pig-batches/{id}/pens [put] func (c *PigBatchController) UpdatePigBatchPens(ctx *gin.Context) { const action = "更新猪批次关联猪栏" From aac032461610ac9eb0bbc526088d3a3346fcf14b Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 22:47:47 +0800 Subject: [PATCH 59/65] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20RemoveEmptyPenFromBa?= =?UTF-8?q?tch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch_service.go | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index adc5d6a..2d5f6b2 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -6,6 +6,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "gorm.io/gorm" ) // --- 业务错误定义 --- @@ -25,6 +26,8 @@ var ( ErrPenNotFound = errors.New("指定的猪栏不存在") // ErrPenNotAssociatedWithBatch 表示猪栏未与该批次关联 ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") + // ErrPenNotEmpty 表示猪栏内仍有猪只,不允许执行当前操作。 + ErrPenNotEmpty = errors.New("猪栏内仍有猪只,无法执行此操作") // ErrInvalidOperation 非法操作 ErrInvalidOperation = errors.New("非法操作") ) @@ -52,6 +55,8 @@ type PigBatchService interface { MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error // ReclassifyPenToNewBatch 连猪带栏,整体划拨到另一个猪群 ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error + // RemoveEmptyPenFromBatch 将一个猪栏移除出猪群,此方法需要在猪栏为空的情况下执行。 + RemoveEmptyPenFromBatch(batchID uint, penID uint) error // GetCurrentPigQuantity 获取指定猪批次的当前猪只数量。 GetCurrentPigQuantity(batchID uint) (int, error) @@ -115,3 +120,48 @@ func NewPigBatchService( sickSvc: sickSvc, } } + +func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error { + return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { + // 1. 检查猪批次是否存在且活跃 + batch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPigBatchNotFound + } + return err + } + if !batch.IsActive() { + return ErrPigBatchNotActive + } + + // 2. 检查猪栏是否存在 + pen, err := s.transferSvc.GetPenByID(tx, penID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrPenNotFound + } + return err + } + + // 3. 检查猪栏是否与当前批次关联 + if pen.PigBatchID == nil || *pen.PigBatchID != batchID { + return ErrPenNotAssociatedWithBatch + } + + // 4. 检查猪栏是否为空 + pigsInPen, err := s.transferSvc.GetCurrentPigsInPen(tx, penID) + if err != nil { + return err + } + if pigsInPen > 0 { + return ErrPenNotEmpty + } + + // 5. 释放猪栏 (将 pig_batch_id 设置为 nil,状态设置为空闲) + if err := s.transferSvc.ReleasePen(tx, penID); err != nil { + return err + } + return nil + }) +} From 632bd20e7d6a5536403576c98f599586e61599d1 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 23:10:58 +0800 Subject: [PATCH 60/65] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=8C=AA=E7=BE=A4?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=8C=AA=E6=A0=8F=E6=8E=A5=E5=8F=A3=E5=8F=98?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 181 +++++++++++++++++- docs/swagger.json | 181 +++++++++++++++++- docs/swagger.yaml | 127 +++++++++++- internal/app/api/api.go | 5 +- .../management/pig_batch_controller.go | 154 +++++++++++++-- internal/app/dto/pig_batch_dto.go | 25 ++- internal/app/service/pig_batch_service.go | 57 ++++-- internal/app/service/pig_service.go | 10 +- 8 files changed, 680 insertions(+), 60 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 5826ff3..d74aae3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -947,6 +947,83 @@ const docTemplate = `{ } } }, + "/api/v1/pig-batches/{batchID}/remove-pen/{penID}": { + "delete": { + "description": "将一个空闲猪栏从指定的猪批次中移除", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "从猪批次移除空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "batchID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "待移除的猪栏ID", + "name": "penID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "移除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{fromBatchID}/reclassify-pen": { + "post": { + "description": "将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "将猪栏划拨到新批次", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "fromBatchID", + "in": "path", + "required": true + }, + { + "description": "划拨请求信息 (包含目标批次ID、猪栏ID和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReclassifyPenToNewBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "划拨成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-batches/{id}": { "get": { "description": "根据ID获取单个猪批次信息", @@ -1066,9 +1143,9 @@ const docTemplate = `{ } } }, - "/api/v1/pig-batches/{id}/pens": { - "put": { - "description": "更新指定猪批次当前关联的猪栏列表", + "/api/v1/pig-batches/{id}/assign-pens": { + "post": { + "description": "将一个或多个空闲猪栏分配给指定的猪批次", "consumes": [ "application/json" ], @@ -1078,7 +1155,7 @@ const docTemplate = `{ "tags": [ "猪批次管理" ], - "summary": "更新猪批次关联的猪栏", + "summary": "为猪批次分配空栏", "parameters": [ { "type": "integer", @@ -1088,18 +1165,59 @@ const docTemplate = `{ "required": true }, { - "description": "猪批次关联的猪栏ID列表", + "description": "待分配的猪栏ID列表", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + "$ref": "#/definitions/dto.AssignEmptyPensToBatchRequest" } } ], "responses": { "200": { - "description": "更新成功", + "description": "分配成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/move-pigs-into-pen": { + "post": { + "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "将猪只从“虚拟库存”移入指定猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "移入猪只请求信息 (包含目标猪栏ID、数量和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MovePigsIntoPenRequest" + } + } + ], + "responses": { + "200": { + "description": "移入成功", "schema": { "$ref": "#/definitions/controller.Response" } @@ -1809,6 +1927,9 @@ const docTemplate = `{ } } }, + "dto.AssignEmptyPensToBatchRequest": { + "type": "object" + }, "dto.CreateAreaControllerRequest": { "type": "object", "required": [ @@ -2144,6 +2265,28 @@ const docTemplate = `{ } } }, + "dto.MovePigsIntoPenRequest": { + "type": "object", + "required": [ + "quantity", + "toPenID" + ], + "properties": { + "quantity": { + "description": "移入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, "dto.PenResponse": { "type": "object", "properties": { @@ -2298,9 +2441,6 @@ const docTemplate = `{ } } }, - "dto.PigBatchUpdatePensRequest": { - "type": "object" - }, "dto.PigHouseResponse": { "type": "object", "properties": { @@ -2380,6 +2520,27 @@ const docTemplate = `{ } } }, + "dto.ReclassifyPenToNewBatchRequest": { + "type": "object", + "required": [ + "penID", + "toBatchID" + ], + "properties": { + "penID": { + "description": "待划拨的猪栏ID", + "type": "integer" + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toBatchID": { + "description": "目标猪批次ID", + "type": "integer" + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 44315f0..3cad7f5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -936,6 +936,83 @@ } } }, + "/api/v1/pig-batches/{batchID}/remove-pen/{penID}": { + "delete": { + "description": "将一个空闲猪栏从指定的猪批次中移除", + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "从猪批次移除空栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "batchID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "待移除的猪栏ID", + "name": "penID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "移除成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{fromBatchID}/reclassify-pen": { + "post": { + "description": "将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "将猪栏划拨到新批次", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "fromBatchID", + "in": "path", + "required": true + }, + { + "description": "划拨请求信息 (包含目标批次ID、猪栏ID和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReclassifyPenToNewBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "划拨成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-batches/{id}": { "get": { "description": "根据ID获取单个猪批次信息", @@ -1055,9 +1132,9 @@ } } }, - "/api/v1/pig-batches/{id}/pens": { - "put": { - "description": "更新指定猪批次当前关联的猪栏列表", + "/api/v1/pig-batches/{id}/assign-pens": { + "post": { + "description": "将一个或多个空闲猪栏分配给指定的猪批次", "consumes": [ "application/json" ], @@ -1067,7 +1144,7 @@ "tags": [ "猪批次管理" ], - "summary": "更新猪批次关联的猪栏", + "summary": "为猪批次分配空栏", "parameters": [ { "type": "integer", @@ -1077,18 +1154,59 @@ "required": true }, { - "description": "猪批次关联的猪栏ID列表", + "description": "待分配的猪栏ID列表", "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dto.PigBatchUpdatePensRequest" + "$ref": "#/definitions/dto.AssignEmptyPensToBatchRequest" } } ], "responses": { "200": { - "description": "更新成功", + "description": "分配成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/move-pigs-into-pen": { + "post": { + "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "将猪只从“虚拟库存”移入指定猪栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "移入猪只请求信息 (包含目标猪栏ID、数量和备注)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.MovePigsIntoPenRequest" + } + } + ], + "responses": { + "200": { + "description": "移入成功", "schema": { "$ref": "#/definitions/controller.Response" } @@ -1798,6 +1916,9 @@ } } }, + "dto.AssignEmptyPensToBatchRequest": { + "type": "object" + }, "dto.CreateAreaControllerRequest": { "type": "object", "required": [ @@ -2133,6 +2254,28 @@ } } }, + "dto.MovePigsIntoPenRequest": { + "type": "object", + "required": [ + "quantity", + "toPenID" + ], + "properties": { + "quantity": { + "description": "移入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, "dto.PenResponse": { "type": "object", "properties": { @@ -2287,9 +2430,6 @@ } } }, - "dto.PigBatchUpdatePensRequest": { - "type": "object" - }, "dto.PigHouseResponse": { "type": "object", "properties": { @@ -2369,6 +2509,27 @@ } } }, + "dto.ReclassifyPenToNewBatchRequest": { + "type": "object", + "required": [ + "penID", + "toBatchID" + ], + "properties": { + "penID": { + "description": "待划拨的猪栏ID", + "type": "integer" + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toBatchID": { + "description": "目标猪批次ID", + "type": "integer" + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index aa93e84..65a1b65 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -69,6 +69,8 @@ definitions: updated_at: type: string type: object + dto.AssignEmptyPensToBatchRequest: + type: object dto.CreateAreaControllerRequest: properties: location: @@ -298,6 +300,22 @@ definitions: example: testuser type: string type: object + dto.MovePigsIntoPenRequest: + properties: + quantity: + description: 移入猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - quantity + - toPenID + type: object dto.PenResponse: properties: capacity: @@ -398,8 +416,6 @@ definitions: - $ref: '#/definitions/models.PigBatchStatus' description: 批次状态,可选 type: object - dto.PigBatchUpdatePensRequest: - type: object dto.PigHouseResponse: properties: description: @@ -450,6 +466,21 @@ definitions: $ref: '#/definitions/dto.TaskResponse' type: array type: object + dto.ReclassifyPenToNewBatchRequest: + properties: + penID: + description: 待划拨的猪栏ID + type: integer + remarks: + description: 备注 + type: string + toBatchID: + description: 目标猪批次ID + type: integer + required: + - penID + - toBatchID + type: object dto.SubPlanResponse: properties: child_plan: @@ -1373,6 +1404,57 @@ paths: summary: 创建猪批次 tags: - 猪批次管理 + /api/v1/pig-batches/{batchID}/remove-pen/{penID}: + delete: + description: 将一个空闲猪栏从指定的猪批次中移除 + parameters: + - description: 猪批次ID + in: path + name: batchID + required: true + type: integer + - description: 待移除的猪栏ID + in: path + name: penID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 移除成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 从猪批次移除空栏 + tags: + - 猪批次管理 + /api/v1/pig-batches/{fromBatchID}/reclassify-pen: + post: + consumes: + - application/json + description: 将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次 + parameters: + - description: 源猪批次ID + in: path + name: fromBatchID + required: true + type: integer + - description: 划拨请求信息 (包含目标批次ID、猪栏ID和备注) + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.ReclassifyPenToNewBatchRequest' + produces: + - application/json + responses: + "200": + description: 划拨成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 将猪栏划拨到新批次 + tags: + - 猪批次管理 /api/v1/pig-batches/{id}: delete: description: 根据ID删除一个猪批次 @@ -1446,31 +1528,58 @@ paths: summary: 更新猪批次 tags: - 猪批次管理 - /api/v1/pig-batches/{id}/pens: - put: + /api/v1/pig-batches/{id}/assign-pens: + post: consumes: - application/json - description: 更新指定猪批次当前关联的猪栏列表 + description: 将一个或多个空闲猪栏分配给指定的猪批次 parameters: - description: 猪批次ID in: path name: id required: true type: integer - - description: 猪批次关联的猪栏ID列表 + - description: 待分配的猪栏ID列表 in: body name: body required: true schema: - $ref: '#/definitions/dto.PigBatchUpdatePensRequest' + $ref: '#/definitions/dto.AssignEmptyPensToBatchRequest' produces: - application/json responses: "200": - description: 更新成功 + description: 分配成功 schema: $ref: '#/definitions/controller.Response' - summary: 更新猪批次关联的猪栏 + summary: 为猪批次分配空栏 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/move-pigs-into-pen: + post: + consumes: + - application/json + description: 将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 移入猪只请求信息 (包含目标猪栏ID、数量和备注) + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.MovePigsIntoPenRequest' + produces: + - application/json + responses: + "200": + description: 移入成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 将猪只从“虚拟库存”移入指定猪栏 tags: - 猪批次管理 /api/v1/pig-houses: diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 3533ecc..745a036 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -234,7 +234,10 @@ func (a *API) setupRoutes() { pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) - pigBatchGroup.PUT("/:id/pens", a.pigBatchController.UpdatePigBatchPens) + pigBatchGroup.POST("/:id/assign-pens", a.pigBatchController.AssignEmptyPensToBatch) + pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch) + pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch) + pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen) } a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index a08f83d..45d7e00 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -179,43 +179,171 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs) } -// UpdatePigBatchPens godoc -// @Summary 更新猪批次关联的猪栏 -// @Description 更新指定猪批次当前关联的猪栏列表 +// AssignEmptyPensToBatch godoc +// @Summary 为猪批次分配空栏 +// @Description 将一个或多个空闲猪栏分配给指定的猪批次 // @Tags 猪批次管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" -// @Param body body dto.PigBatchUpdatePensRequest true "猪批次关联的猪栏ID列表" -// @Success 200 {object} controller.Response "更新成功" -// @Router /api/v1/pig-batches/{id}/pens [put] -func (c *PigBatchController) UpdatePigBatchPens(ctx *gin.Context) { - const action = "更新猪批次关联猪栏" +// @Param body body dto.AssignEmptyPensToBatchRequest true "待分配的猪栏ID列表" +// @Success 200 {object} controller.Response "分配成功" +// @Router /api/v1/pig-batches/{id}/assign-pens [post] +func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) { + const action = "为猪批次分配空栏" batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) if err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) return } - var req dto.PigBatchUpdatePensRequest + var req dto.AssignEmptyPensToBatchRequest if err := ctx.ShouldBindJSON(&req); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) return } - err = c.service.UpdatePigBatchPens(uint(batchID), req.PenIDs) + userID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.AssignEmptyPensToBatch(uint(batchID), req.PenIDs, userID) if err != nil { if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) return - } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenStatusInvalidForAllocation) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenStatusInvalidForAllocation) { controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) return } c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次关联猪栏失败", action, err.Error(), batchID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "分配空栏失败", action, err.Error(), batchID) return } - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", nil, action, "更新成功", batchID) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "分配成功", nil, action, "分配成功", batchID) +} + +// ReclassifyPenToNewBatch godoc +// @Summary 将猪栏划拨到新批次 +// @Description 将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param fromBatchID path int true "源猪批次ID" +// @Param body body dto.ReclassifyPenToNewBatchRequest true "划拨请求信息 (包含目标批次ID、猪栏ID和备注)" +// @Success 200 {object} controller.Response "划拨成功" +// @Router /api/v1/pig-batches/{fromBatchID}/reclassify-pen [post] +func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) { + const action = "划拨猪栏到新批次" + fromBatchID, err := strconv.ParseUint(ctx.Param("fromBatchID"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的源猪批次ID格式", action, "ID格式错误", ctx.Param("fromBatchID")) + return + } + + var req dto.ReclassifyPenToNewBatchRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + userID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.ReclassifyPenToNewBatch(uint(fromBatchID), req.ToBatchID, req.PenID, userID, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), fromBatchID) + return + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrInvalidOperation) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), fromBatchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "划拨猪栏失败", action, err.Error(), fromBatchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "划拨成功", nil, action, "划拨成功", fromBatchID) +} + +// RemoveEmptyPenFromBatch godoc +// @Summary 从猪批次移除空栏 +// @Description 将一个空闲猪栏从指定的猪批次中移除 +// @Tags 猪批次管理 +// @Produce json +// @Param batchID path int true "猪批次ID" +// @Param penID path int true "待移除的猪栏ID" +// @Success 200 {object} controller.Response "移除成功" +// @Router /api/v1/pig-batches/{batchID}/remove-pen/{penID} [delete] +func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) { + const action = "从猪批次移除空栏" + batchID, err := strconv.ParseUint(ctx.Param("batchID"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("batchID")) + return + } + + penID, err := strconv.ParseUint(ctx.Param("penID"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪栏ID格式", action, "ID格式错误", ctx.Param("penID")) + return + } + + err = c.service.RemoveEmptyPenFromBatch(uint(batchID), uint(penID)) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "移除空栏失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "移除成功", nil, action, "移除成功", batchID) +} + +// MovePigsIntoPen godoc +// @Summary 将猪只从“虚拟库存”移入指定猪栏 +// @Description 将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.MovePigsIntoPenRequest true "移入猪只请求信息 (包含目标猪栏ID、数量和备注)" +// @Success 200 {object} controller.Response "移入成功" +// @Router /api/v1/pig-batches/{id}/move-pigs-into-pen [post] +func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) { + const action = "将猪只移入猪栏" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.MovePigsIntoPenRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + userID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.MovePigsIntoPen(uint(batchID), req.ToPenID, req.Quantity, userID, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrInvalidOperation) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "移入猪只失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "移入成功", nil, action, "移入成功", batchID) } diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go index 1587bd1..47b539e 100644 --- a/internal/app/dto/pig_batch_dto.go +++ b/internal/app/dto/pig_batch_dto.go @@ -44,7 +44,26 @@ type PigBatchResponseDTO struct { UpdateTime time.Time `json:"update_time"` // 更新时间 } -// PigBatchUpdatePensRequest 用于更新猪批次关联猪栏的请求体 -type PigBatchUpdatePensRequest struct { - PenIDs []uint `json:"penIDs" binding:"required,min=0" example:"[1,2,3]"` +// AssignEmptyPensToBatchRequest 用于为猪批次分配空栏的请求体 +type AssignEmptyPensToBatchRequest struct { + PenIDs []uint `json:"penIDs" binding:"required,min=1" example:"[1,2,3]"` // 待分配的猪栏ID列表 +} + +// ReclassifyPenToNewBatchRequest 用于将猪栏划拨到新批次的请求体 +type ReclassifyPenToNewBatchRequest struct { + ToBatchID uint `json:"toBatchID" binding:"required"` // 目标猪批次ID + PenID uint `json:"penID" binding:"required"` // 待划拨的猪栏ID + Remarks string `json:"remarks"` // 备注 +} + +// RemoveEmptyPenFromBatchRequest 用于从猪批次移除空栏的请求体 +type RemoveEmptyPenFromBatchRequest struct { + PenID uint `json:"penID" binding:"required"` // 待移除的猪栏ID +} + +// MovePigsIntoPenRequest 用于将猪只从“虚拟库存”移入指定猪栏的请求体 +type MovePigsIntoPenRequest struct { + ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 移入猪只数量 + Remarks string `json:"remarks"` // 备注 } diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index da50327..32111d4 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -14,7 +14,10 @@ type PigBatchService interface { UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) - UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error + AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error + ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error + RemoveEmptyPenFromBatch(batchID uint, penID uint) error + MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error } // pigBatchService 的实现现在依赖于领域服务接口。 @@ -65,7 +68,7 @@ func (s *pigBatchService) CreatePigBatch(operatorID uint, dto *dto.PigBatchCreat createdBatch, err := s.domainService.CreatePigBatch(operatorID, batch) if err != nil { s.logger.Errorf("应用层: 创建猪批次失败: %v", err) - return nil, mapDomainError(err) + return nil, MapDomainError(err) } // 3. 领域模型 -> DTO @@ -77,7 +80,7 @@ func (s *pigBatchService) GetPigBatch(id uint) (*dto.PigBatchResponseDTO, error) batch, err := s.domainService.GetPigBatch(id) if err != nil { s.logger.Warnf("应用层: 获取猪批次失败, ID: %d, 错误: %v", id, err) - return nil, mapDomainError(err) + return nil, MapDomainError(err) } return s.toPigBatchResponseDTO(batch), nil @@ -89,7 +92,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* existingBatch, err := s.domainService.GetPigBatch(id) if err != nil { s.logger.Warnf("应用层: 更新猪批次失败,获取原批次信息错误, ID: %d, 错误: %v", id, err) - return nil, mapDomainError(err) + return nil, MapDomainError(err) } // 2. 将DTO中的变更应用到模型上 @@ -116,7 +119,7 @@ func (s *pigBatchService) UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (* updatedBatch, err := s.domainService.UpdatePigBatch(existingBatch) if err != nil { s.logger.Errorf("应用层: 更新猪批次失败, ID: %d, 错误: %v", id, err) - return nil, mapDomainError(err) + return nil, MapDomainError(err) } // 4. 转换并返回结果 @@ -128,7 +131,7 @@ func (s *pigBatchService) DeletePigBatch(id uint) error { err := s.domainService.DeletePigBatch(id) if err != nil { s.logger.Errorf("应用层: 删除猪批次失败, ID: %d, 错误: %v", id, err) - return mapDomainError(err) + return MapDomainError(err) } return nil } @@ -138,7 +141,7 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons batches, err := s.domainService.ListPigBatches(isActive) if err != nil { s.logger.Errorf("应用层: 批量查询猪批次失败: %v", err) - return nil, mapDomainError(err) + return nil, MapDomainError(err) } var responseDTOs []*dto.PigBatchResponseDTO @@ -149,12 +152,42 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*dto.PigBatchRespons return responseDTOs, nil } -// UpdatePigBatchPens 将关联猪栏的复杂操作委托给领域服务,并处理错误转换。 -func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { - err := s.domainService.UpdatePigBatchPens(batchID, desiredPenIDs) +// AssignEmptyPensToBatch 委托给领域服务 +func (s *pigBatchService) AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error { + err := s.domainService.AssignEmptyPensToBatch(batchID, penIDs, operatorID) if err != nil { - s.logger.Errorf("应用层: 更新猪批次猪栏关联失败, 批次ID: %d, 错误: %v", batchID, err) - return mapDomainError(err) + s.logger.Errorf("应用层: 为猪批次分配空栏失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// ReclassifyPenToNewBatch 委托给领域服务 +func (s *pigBatchService) ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error { + err := s.domainService.ReclassifyPenToNewBatch(fromBatchID, toBatchID, penID, operatorID, remarks) + if err != nil { + s.logger.Errorf("应用层: 划拨猪栏到新批次失败, 源批次ID: %d, 错误: %v", fromBatchID, err) + return MapDomainError(err) + } + return nil +} + +// RemoveEmptyPenFromBatch 委托给领域服务 +func (s *pigBatchService) RemoveEmptyPenFromBatch(batchID uint, penID uint) error { + err := s.domainService.RemoveEmptyPenFromBatch(batchID, penID) + if err != nil { + s.logger.Errorf("应用层: 从猪批次移除空栏失败, 批次ID: %d, 猪栏ID: %d, 错误: %v", batchID, penID, err) + return MapDomainError(err) + } + return nil +} + +// MovePigsIntoPen 委托给领域服务 +func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error { + err := s.domainService.MovePigsIntoPen(batchID, toPenID, quantity, operatorID, remarks) + if err != nil { + s.logger.Errorf("应用层: 将猪只移入猪栏失败, 批次ID: %d, 目标猪栏ID: %d, 错误: %v", batchID, toPenID, err) + return MapDomainError(err) } return nil } diff --git a/internal/app/service/pig_service.go b/internal/app/service/pig_service.go index 8ebe135..37e9dc3 100644 --- a/internal/app/service/pig_service.go +++ b/internal/app/service/pig_service.go @@ -19,10 +19,12 @@ var ( ErrPenOccupiedByOtherBatch = errors.New("猪栏已被其他批次使用") ErrPenStatusInvalidForAllocation = errors.New("猪栏状态不允许分配") ErrPenNotAssociatedWithBatch = errors.New("猪栏未与该批次关联") + ErrPenNotEmpty = errors.New("猪栏内仍有猪只") + ErrInvalidOperation = errors.New("非法操作") ) -// mapDomainError 将领域层的错误转换为应用服务层的公共错误。 -func mapDomainError(err error) error { +// MapDomainError 将领域层的错误转换为应用服务层的公共错误。 +func MapDomainError(err error) error { if err == nil { return nil } @@ -42,6 +44,10 @@ func mapDomainError(err error) error { return ErrPenNotAssociatedWithBatch case errors.Is(err, domain_pig.ErrPenNotFound): return ErrPenNotFound + case errors.Is(err, domain_pig.ErrPenNotEmpty): + return ErrPenNotEmpty + case errors.Is(err, domain_pig.ErrInvalidOperation): + return ErrInvalidOperation // 可以添加更多领域错误到应用层错误的映射 default: return err // 对于未知的领域错误,直接返回 From e142405bb359a4b6660d6db3560453f816e862f2 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 23:22:47 +0800 Subject: [PATCH 61/65] =?UTF-8?q?=E7=8C=AA=E7=BE=A4=E9=A2=86=E5=9F=9F?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E6=96=B9=E6=B3=95=E6=98=A0=E5=B0=84=E5=88=B0?= =?UTF-8?q?api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 784 +++++++++++++++++- docs/swagger.json | 784 +++++++++++++++++- docs/swagger.yaml | 544 +++++++++++- internal/app/api/api.go | 12 +- .../management/pig_batch_controller.go | 18 +- .../management/pig_batch_health_controller.go | 269 ++++++ .../management/pig_batch_trade_controller.go | 97 +++ .../pig_batch_transfer_controller.go | 97 +++ internal/app/dto/pig_batch_dto.go | 91 ++ internal/app/service/pig_batch_service.go | 122 +++ 10 files changed, 2783 insertions(+), 35 deletions(-) create mode 100644 internal/app/controller/management/pig_batch_health_controller.go create mode 100644 internal/app/controller/management/pig_batch_trade_controller.go create mode 100644 internal/app/controller/management/pig_batch_transfer_controller.go diff --git a/docs/docs.go b/docs/docs.go index d74aae3..cd81b29 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -867,7 +867,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "获取猪批次列表", "parameters": [ @@ -911,7 +911,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "创建猪批次", "parameters": [ @@ -954,7 +954,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "从猪批次移除空栏", "parameters": [ @@ -993,7 +993,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "将猪栏划拨到新批次", "parameters": [ @@ -1031,7 +1031,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "获取单个猪批次", "parameters": [ @@ -1073,7 +1073,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "更新猪批次", "parameters": [ @@ -1121,7 +1121,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "删除猪批次", "parameters": [ @@ -1153,7 +1153,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "为猪批次分配空栏", "parameters": [ @@ -1184,6 +1184,47 @@ const docTemplate = `{ } } }, + "/api/v1/pig-batches/{id}/buy-pigs": { + "post": { + "description": "记录猪批次中的猪只购买事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理买猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "买猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BuyPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "买猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-batches/{id}/move-pigs-into-pen": { "post": { "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", @@ -1194,7 +1235,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "将猪只从“虚拟库存”移入指定猪栏", "parameters": [ @@ -1225,6 +1266,375 @@ const docTemplate = `{ } } }, + "/api/v1/pig-batches/{id}/record-cull": { + "post": { + "description": "记录猪批次中正常猪只淘汰的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-death": { + "post": { + "description": "记录猪批次中正常猪只死亡的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-cull": { + "post": { + "description": "记录猪批次中病猪淘汰的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-death": { + "post": { + "description": "记录猪批次中病猪死亡的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-recovery": { + "post": { + "description": "记录猪批次中病猪康复的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪康复事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪康复请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigRecoveryRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pigs": { + "post": { + "description": "记录猪批次中新增病猪的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录新增病猪事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/sell-pigs": { + "post": { + "description": "记录猪批次中的猪只出售事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理卖猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "卖猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SellPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "卖猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/transfer-within-batch": { + "post": { + "description": "将指定数量的猪只在同一个猪群的不同猪栏间调动", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "群内调栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "群内调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsWithinBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{sourceBatchID}/transfer-across-batches": { + "post": { + "description": "将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "跨猪群调栏", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "sourceBatchID", + "in": "path", + "required": true + }, + { + "description": "跨群调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsAcrossBatchesRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -1930,6 +2340,50 @@ const docTemplate = `{ "dto.AssignEmptyPensToBatchRequest": { "type": "object" }, + "dto.BuyPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "买入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, "dto.CreateAreaControllerRequest": { "type": "object", "required": [ @@ -2541,6 +2995,248 @@ const docTemplate = `{ } } }, + "dto.RecordCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordSickPigCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigRecoveryRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "康复猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigsRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "病猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.SellPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "卖出猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -2631,6 +3327,65 @@ const docTemplate = `{ } } }, + "dto.TransferPigsAcrossBatchesRequest": { + "type": "object", + "required": [ + "destBatchID", + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "destBatchID": { + "description": "目标猪批次ID", + "type": "integer" + }, + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.TransferPigsWithinBatchRequest": { + "type": "object", + "required": [ + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, "dto.UpdateAreaControllerRequest": { "type": "object", "required": [ @@ -2870,6 +3625,17 @@ const docTemplate = `{ "OriginTypePurchased" ] }, + "models.PigBatchSickPigTreatmentLocation": { + "type": "string", + "enum": [ + "原地治疗", + "病猪栏治疗" + ], + "x-enum-varnames": [ + "TreatmentLocationOnSite", + "TreatmentLocationSickBay" + ] + }, "models.PigBatchStatus": { "type": "string", "enum": [ diff --git a/docs/swagger.json b/docs/swagger.json index 3cad7f5..c14cfa3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -856,7 +856,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "获取猪批次列表", "parameters": [ @@ -900,7 +900,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "创建猪批次", "parameters": [ @@ -943,7 +943,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "从猪批次移除空栏", "parameters": [ @@ -982,7 +982,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "将猪栏划拨到新批次", "parameters": [ @@ -1020,7 +1020,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "获取单个猪批次", "parameters": [ @@ -1062,7 +1062,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "更新猪批次", "parameters": [ @@ -1110,7 +1110,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "删除猪批次", "parameters": [ @@ -1142,7 +1142,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "为猪批次分配空栏", "parameters": [ @@ -1173,6 +1173,47 @@ } } }, + "/api/v1/pig-batches/{id}/buy-pigs": { + "post": { + "description": "记录猪批次中的猪只购买事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理买猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "买猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.BuyPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "买猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-batches/{id}/move-pigs-into-pen": { "post": { "description": "将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏", @@ -1183,7 +1224,7 @@ "application/json" ], "tags": [ - "猪批次管理" + "猪群管理" ], "summary": "将猪只从“虚拟库存”移入指定猪栏", "parameters": [ @@ -1214,6 +1255,375 @@ } } }, + "/api/v1/pig-batches/{id}/record-cull": { + "post": { + "description": "记录猪批次中正常猪只淘汰的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-death": { + "post": { + "description": "记录猪批次中正常猪只死亡的数量和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录正常猪只死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录正常猪只死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-cull": { + "post": { + "description": "记录猪批次中病猪淘汰的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪淘汰事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪淘汰请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigCullRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-death": { + "post": { + "description": "记录猪批次中病猪死亡的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪死亡事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪死亡请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigDeathRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pig-recovery": { + "post": { + "description": "记录猪批次中病猪康复的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录病猪康复事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪康复请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigRecoveryRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/record-sick-pigs": { + "post": { + "description": "记录猪批次中新增病猪的数量、治疗地点和发生时间", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "记录新增病猪事件", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "记录病猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RecordSickPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "记录成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/sell-pigs": { + "post": { + "description": "记录猪批次中的猪只出售事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "处理卖猪的业务逻辑", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "卖猪请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SellPigsRequest" + } + } + ], + "responses": { + "200": { + "description": "卖猪成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{id}/transfer-within-batch": { + "post": { + "description": "将指定数量的猪只在同一个猪群的不同猪栏间调动", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "群内调栏", + "parameters": [ + { + "type": "integer", + "description": "猪批次ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "群内调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsWithinBatchRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, + "/api/v1/pig-batches/{sourceBatchID}/transfer-across-batches": { + "post": { + "description": "将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "猪批次管理" + ], + "summary": "跨猪群调栏", + "parameters": [ + { + "type": "integer", + "description": "源猪批次ID", + "name": "sourceBatchID", + "in": "path", + "required": true + }, + { + "description": "跨群调栏请求信息", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TransferPigsAcrossBatchesRequest" + } + } + ], + "responses": { + "200": { + "description": "调栏成功", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/pig-houses": { "get": { "description": "获取所有猪舍的列表", @@ -1919,6 +2329,50 @@ "dto.AssignEmptyPensToBatchRequest": { "type": "object" }, + "dto.BuyPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "买入猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, "dto.CreateAreaControllerRequest": { "type": "object", "required": [ @@ -2530,6 +2984,248 @@ } } }, + "dto.RecordCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + } + } + }, + "dto.RecordSickPigCullRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "淘汰猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigDeathRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "死亡猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigRecoveryRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "康复猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.RecordSickPigsRequest": { + "type": "object", + "required": [ + "happenedAt", + "penID", + "quantity", + "treatmentLocation" + ], + "properties": { + "happenedAt": { + "description": "发生时间", + "type": "string" + }, + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "病猪数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "treatmentLocation": { + "description": "治疗地点", + "allOf": [ + { + "$ref": "#/definitions/models.PigBatchSickPigTreatmentLocation" + } + ] + } + } + }, + "dto.SellPigsRequest": { + "type": "object", + "required": [ + "penID", + "quantity", + "totalPrice", + "tradeDate", + "traderName", + "unitPrice" + ], + "properties": { + "penID": { + "description": "猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "卖出猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "totalPrice": { + "description": "总价", + "type": "number", + "minimum": 0 + }, + "tradeDate": { + "description": "交易日期", + "type": "string" + }, + "traderName": { + "description": "交易方名称", + "type": "string" + }, + "unitPrice": { + "description": "单价", + "type": "number", + "minimum": 0 + } + } + }, "dto.SubPlanResponse": { "type": "object", "properties": { @@ -2620,6 +3316,65 @@ } } }, + "dto.TransferPigsAcrossBatchesRequest": { + "type": "object", + "required": [ + "destBatchID", + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "destBatchID": { + "description": "目标猪批次ID", + "type": "integer" + }, + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, + "dto.TransferPigsWithinBatchRequest": { + "type": "object", + "required": [ + "fromPenID", + "quantity", + "toPenID" + ], + "properties": { + "fromPenID": { + "description": "源猪栏ID", + "type": "integer" + }, + "quantity": { + "description": "调栏猪只数量", + "type": "integer", + "minimum": 1 + }, + "remarks": { + "description": "备注", + "type": "string" + }, + "toPenID": { + "description": "目标猪栏ID", + "type": "integer" + } + } + }, "dto.UpdateAreaControllerRequest": { "type": "object", "required": [ @@ -2859,6 +3614,17 @@ "OriginTypePurchased" ] }, + "models.PigBatchSickPigTreatmentLocation": { + "type": "string", + "enum": [ + "原地治疗", + "病猪栏治疗" + ], + "x-enum-varnames": [ + "TreatmentLocationOnSite", + "TreatmentLocationSickBay" + ] + }, "models.PigBatchStatus": { "type": "string", "enum": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 65a1b65..e5dcd7c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -71,6 +71,40 @@ definitions: type: object dto.AssignEmptyPensToBatchRequest: type: object + dto.BuyPigsRequest: + properties: + penID: + description: 猪栏ID + type: integer + quantity: + description: 买入猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + totalPrice: + description: 总价 + minimum: 0 + type: number + tradeDate: + description: 交易日期 + type: string + traderName: + description: 交易方名称 + type: string + unitPrice: + description: 单价 + minimum: 0 + type: number + required: + - penID + - quantity + - totalPrice + - tradeDate + - traderName + - unitPrice + type: object dto.CreateAreaControllerRequest: properties: location: @@ -481,6 +515,180 @@ definitions: - penID - toBatchID type: object + dto.RecordCullRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 淘汰猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + required: + - happenedAt + - penID + - quantity + type: object + dto.RecordDeathRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 死亡猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + required: + - happenedAt + - penID + - quantity + type: object + dto.RecordSickPigCullRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 淘汰猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigDeathRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 死亡猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigRecoveryRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 康复猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.RecordSickPigsRequest: + properties: + happenedAt: + description: 发生时间 + type: string + penID: + description: 猪栏ID + type: integer + quantity: + description: 病猪数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + treatmentLocation: + allOf: + - $ref: '#/definitions/models.PigBatchSickPigTreatmentLocation' + description: 治疗地点 + required: + - happenedAt + - penID + - quantity + - treatmentLocation + type: object + dto.SellPigsRequest: + properties: + penID: + description: 猪栏ID + type: integer + quantity: + description: 卖出猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + totalPrice: + description: 总价 + minimum: 0 + type: number + tradeDate: + description: 交易日期 + type: string + traderName: + description: 交易方名称 + type: string + unitPrice: + description: 单价 + minimum: 0 + type: number + required: + - penID + - quantity + - totalPrice + - tradeDate + - traderName + - unitPrice + type: object dto.SubPlanResponse: properties: child_plan: @@ -542,6 +750,50 @@ definitions: - $ref: '#/definitions/models.TaskType' example: 等待 type: object + dto.TransferPigsAcrossBatchesRequest: + properties: + destBatchID: + description: 目标猪批次ID + type: integer + fromPenID: + description: 源猪栏ID + type: integer + quantity: + description: 调栏猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - destBatchID + - fromPenID + - quantity + - toPenID + type: object + dto.TransferPigsWithinBatchRequest: + properties: + fromPenID: + description: 源猪栏ID + type: integer + quantity: + description: 调栏猪只数量 + minimum: 1 + type: integer + remarks: + description: 备注 + type: string + toPenID: + description: 目标猪栏ID + type: integer + required: + - fromPenID + - quantity + - toPenID + type: object dto.UpdateAreaControllerRequest: properties: location: @@ -708,6 +960,14 @@ definitions: x-enum-varnames: - OriginTypeSelfFarrowed - OriginTypePurchased + models.PigBatchSickPigTreatmentLocation: + enum: + - 原地治疗 + - 病猪栏治疗 + type: string + x-enum-varnames: + - TreatmentLocationOnSite + - TreatmentLocationSickBay models.PigBatchStatus: enum: - 保育 @@ -1377,7 +1637,7 @@ paths: type: object summary: 获取猪批次列表 tags: - - 猪批次管理 + - 猪群管理 post: consumes: - application/json @@ -1403,7 +1663,7 @@ paths: type: object summary: 创建猪批次 tags: - - 猪批次管理 + - 猪群管理 /api/v1/pig-batches/{batchID}/remove-pen/{penID}: delete: description: 将一个空闲猪栏从指定的猪批次中移除 @@ -1427,7 +1687,7 @@ paths: $ref: '#/definitions/controller.Response' summary: 从猪批次移除空栏 tags: - - 猪批次管理 + - 猪群管理 /api/v1/pig-batches/{fromBatchID}/reclassify-pen: post: consumes: @@ -1454,7 +1714,7 @@ paths: $ref: '#/definitions/controller.Response' summary: 将猪栏划拨到新批次 tags: - - 猪批次管理 + - 猪群管理 /api/v1/pig-batches/{id}: delete: description: 根据ID删除一个猪批次 @@ -1473,7 +1733,7 @@ paths: $ref: '#/definitions/controller.Response' summary: 删除猪批次 tags: - - 猪批次管理 + - 猪群管理 get: description: 根据ID获取单个猪批次信息 parameters: @@ -1496,7 +1756,7 @@ paths: type: object summary: 获取单个猪批次 tags: - - 猪批次管理 + - 猪群管理 put: consumes: - application/json @@ -1527,7 +1787,7 @@ paths: type: object summary: 更新猪批次 tags: - - 猪批次管理 + - 猪群管理 /api/v1/pig-batches/{id}/assign-pens: post: consumes: @@ -1554,6 +1814,33 @@ paths: $ref: '#/definitions/controller.Response' summary: 为猪批次分配空栏 tags: + - 猪群管理 + /api/v1/pig-batches/{id}/buy-pigs: + post: + consumes: + - application/json + description: 记录猪批次中的猪只购买事件 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 买猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.BuyPigsRequest' + produces: + - application/json + responses: + "200": + description: 买猪成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 处理买猪的业务逻辑 + tags: - 猪批次管理 /api/v1/pig-batches/{id}/move-pigs-into-pen: post: @@ -1581,6 +1868,249 @@ paths: $ref: '#/definitions/controller.Response' summary: 将猪只从“虚拟库存”移入指定猪栏 tags: + - 猪群管理 + /api/v1/pig-batches/{id}/record-cull: + post: + consumes: + - application/json + description: 记录猪批次中正常猪只淘汰的数量和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录正常猪只淘汰请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordCullRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录正常猪只淘汰事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-death: + post: + consumes: + - application/json + description: 记录猪批次中正常猪只死亡的数量和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录正常猪只死亡请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordDeathRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录正常猪只死亡事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-cull: + post: + consumes: + - application/json + description: 记录猪批次中病猪淘汰的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪淘汰请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigCullRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪淘汰事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-death: + post: + consumes: + - application/json + description: 记录猪批次中病猪死亡的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪死亡请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigDeathRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪死亡事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pig-recovery: + post: + consumes: + - application/json + description: 记录猪批次中病猪康复的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪康复请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigRecoveryRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录病猪康复事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/record-sick-pigs: + post: + consumes: + - application/json + description: 记录猪批次中新增病猪的数量、治疗地点和发生时间 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 记录病猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.RecordSickPigsRequest' + produces: + - application/json + responses: + "200": + description: 记录成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 记录新增病猪事件 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/sell-pigs: + post: + consumes: + - application/json + description: 记录猪批次中的猪只出售事件 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 卖猪请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.SellPigsRequest' + produces: + - application/json + responses: + "200": + description: 卖猪成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 处理卖猪的业务逻辑 + tags: + - 猪批次管理 + /api/v1/pig-batches/{id}/transfer-within-batch: + post: + consumes: + - application/json + description: 将指定数量的猪只在同一个猪群的不同猪栏间调动 + parameters: + - description: 猪批次ID + in: path + name: id + required: true + type: integer + - description: 群内调栏请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.TransferPigsWithinBatchRequest' + produces: + - application/json + responses: + "200": + description: 调栏成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 群内调栏 + tags: + - 猪批次管理 + /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches: + post: + consumes: + - application/json + description: 将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏 + parameters: + - description: 源猪批次ID + in: path + name: sourceBatchID + required: true + type: integer + - description: 跨群调栏请求信息 + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.TransferPigsAcrossBatchesRequest' + produces: + - application/json + responses: + "200": + description: 调栏成功 + schema: + $ref: '#/definitions/controller.Response' + summary: 跨猪群调栏 + tags: - 猪批次管理 /api/v1/pig-houses: get: diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 745a036..56aa248 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -226,7 +226,7 @@ func (a *API) setupRoutes() { } a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") - // 猪批次相关路由组 + // 猪群相关路由组 pigBatchGroup := authGroup.Group("/pig-batches") { pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch) @@ -238,6 +238,16 @@ func (a *API) setupRoutes() { pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch) pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch) pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen) + pigBatchGroup.POST("/:id/sell-pigs", a.pigBatchController.SellPigs) + pigBatchGroup.POST("/:id/buy-pigs", a.pigBatchController.BuyPigs) + pigBatchGroup.POST("/:sourceBatchID/transfer-across-batches", a.pigBatchController.TransferPigsAcrossBatches) + pigBatchGroup.POST("/:id/transfer-within-batch", a.pigBatchController.TransferPigsWithinBatch) + pigBatchGroup.POST("/:id/record-sick-pigs", a.pigBatchController.RecordSickPigs) + pigBatchGroup.POST("/:id/record-sick-pig-recovery", a.pigBatchController.RecordSickPigRecovery) + pigBatchGroup.POST("/:id/record-sick-pig-death", a.pigBatchController.RecordSickPigDeath) + pigBatchGroup.POST("/:id/record-sick-pig-cull", a.pigBatchController.RecordSickPigCull) + pigBatchGroup.POST("/:id/record-death", a.pigBatchController.RecordDeath) + pigBatchGroup.POST("/:id/record-cull", a.pigBatchController.RecordCull) } a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 45d7e00..623d680 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -29,7 +29,7 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) // CreatePigBatch godoc // @Summary 创建猪批次 // @Description 创建一个新的猪批次 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param body body dto.PigBatchCreateDTO true "猪批次信息" @@ -58,7 +58,7 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { // GetPigBatch godoc // @Summary 获取单个猪批次 // @Description 根据ID获取单个猪批次信息 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Produce json // @Param id path int true "猪批次ID" // @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功" @@ -88,7 +88,7 @@ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { // UpdatePigBatch godoc // @Summary 更新猪批次 // @Description 更新一个已存在的猪批次信息 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -126,7 +126,7 @@ func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { // DeletePigBatch godoc // @Summary 删除猪批次 // @Description 根据ID删除一个猪批次 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Produce json // @Param id path int true "猪批次ID" // @Success 200 {object} controller.Response "删除成功" @@ -155,7 +155,7 @@ func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { // ListPigBatches godoc // @Summary 获取猪批次列表 // @Description 获取所有猪批次的列表,支持按活跃状态筛选 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Produce json // @Param is_active query bool false "是否活跃 (true/false)" // @Success 200 {object} controller.Response{data=[]dto.PigBatchResponseDTO} "获取成功" @@ -182,7 +182,7 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { // AssignEmptyPensToBatch godoc // @Summary 为猪批次分配空栏 // @Description 将一个或多个空闲猪栏分配给指定的猪批次 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -225,7 +225,7 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) { // ReclassifyPenToNewBatch godoc // @Summary 将猪栏划拨到新批次 // @Description 将一个猪栏(连同其中的猪只)从一个批次整体划拨到另一个批次 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param fromBatchID path int true "源猪批次ID" @@ -268,7 +268,7 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) { // RemoveEmptyPenFromBatch godoc // @Summary 从猪批次移除空栏 // @Description 将一个空闲猪栏从指定的猪批次中移除 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Produce json // @Param batchID path int true "猪批次ID" // @Param penID path int true "待移除的猪栏ID" @@ -308,7 +308,7 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) { // MovePigsIntoPen godoc // @Summary 将猪只从“虚拟库存”移入指定猪栏 // @Description 将指定数量的猪只从批次的“虚拟库存”移入一个已分配的猪栏 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" diff --git a/internal/app/controller/management/pig_batch_health_controller.go b/internal/app/controller/management/pig_batch_health_controller.go new file mode 100644 index 0000000..3b4c09f --- /dev/null +++ b/internal/app/controller/management/pig_batch_health_controller.go @@ -0,0 +1,269 @@ +package management + +import ( + "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" + "github.com/gin-gonic/gin" +) + +// RecordSickPigs godoc +// @Summary 记录新增病猪事件 +// @Description 记录猪批次中新增病猪的数量、治疗地点和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordSickPigsRequest true "记录病猪请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-sick-pigs [post] +func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) { + const action = "记录新增病猪事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordSickPigsRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordSickPigs(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录新增病猪事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} + +// RecordSickPigRecovery godoc +// @Summary 记录病猪康复事件 +// @Description 记录猪批次中病猪康复的数量、治疗地点和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordSickPigRecoveryRequest true "记录病猪康复请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-sick-pig-recovery [post] +func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) { + const action = "记录病猪康复事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordSickPigRecoveryRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordSickPigRecovery(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪康复事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} + +// RecordSickPigDeath godoc +// @Summary 记录病猪死亡事件 +// @Description 记录猪批次中病猪死亡的数量、治疗地点和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordSickPigDeathRequest true "记录病猪死亡请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-sick-pig-death [post] +func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) { + const action = "记录病猪死亡事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordSickPigDeathRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordSickPigDeath(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪死亡事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} + +// RecordSickPigCull godoc +// @Summary 记录病猪淘汰事件 +// @Description 记录猪批次中病猪淘汰的数量、治疗地点和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordSickPigCullRequest true "记录病猪淘汰请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-sick-pig-cull [post] +func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) { + const action = "记录病猪淘汰事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordSickPigCullRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordSickPigCull(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪淘汰事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} + +// RecordDeath godoc +// @Summary 记录正常猪只死亡事件 +// @Description 记录猪批次中正常猪只死亡的数量和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordDeathRequest true "记录正常猪只死亡请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-death [post] +func (c *PigBatchController) RecordDeath(ctx *gin.Context) { + const action = "记录正常猪只死亡事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordDeathRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordDeath(operatorID, uint(batchID), req.PenID, req.Quantity, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录正常猪只死亡事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} + +// RecordCull godoc +// @Summary 记录正常猪只淘汰事件 +// @Description 记录猪批次中正常猪只淘汰的数量和发生时间 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.RecordCullRequest true "记录正常猪只淘汰请求信息" +// @Success 200 {object} controller.Response "记录成功" +// @Router /api/v1/pig-batches/{id}/record-cull [post] +func (c *PigBatchController) RecordCull(ctx *gin.Context) { + const action = "记录正常猪只淘汰事件" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.RecordCullRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.RecordCull(operatorID, uint(batchID), req.PenID, req.Quantity, req.HappenedAt, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录正常猪只淘汰事件失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) +} diff --git a/internal/app/controller/management/pig_batch_trade_controller.go b/internal/app/controller/management/pig_batch_trade_controller.go new file mode 100644 index 0000000..821203c --- /dev/null +++ b/internal/app/controller/management/pig_batch_trade_controller.go @@ -0,0 +1,97 @@ +package management + +import ( + "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" + "github.com/gin-gonic/gin" +) + +// SellPigs godoc +// @Summary 处理卖猪的业务逻辑 +// @Description 记录猪批次中的猪只出售事件 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.SellPigsRequest true "卖猪请求信息" +// @Success 200 {object} controller.Response "卖猪成功" +// @Router /api/v1/pig-batches/{id}/sell-pigs [post] +func (c *PigBatchController) SellPigs(ctx *gin.Context) { + const action = "卖猪" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.SellPigsRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.SellPigs(uint(batchID), req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "卖猪失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "卖猪成功", nil, action, "卖猪成功", batchID) +} + +// BuyPigs godoc +// @Summary 处理买猪的业务逻辑 +// @Description 记录猪批次中的猪只购买事件 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.BuyPigsRequest true "买猪请求信息" +// @Success 200 {object} controller.Response "买猪成功" +// @Router /api/v1/pig-batches/{id}/buy-pigs [post] +func (c *PigBatchController) BuyPigs(ctx *gin.Context) { + const action = "买猪" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.BuyPigsRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.BuyPigs(uint(batchID), req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "买猪失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "买猪成功", nil, action, "买猪成功", batchID) +} diff --git a/internal/app/controller/management/pig_batch_transfer_controller.go b/internal/app/controller/management/pig_batch_transfer_controller.go new file mode 100644 index 0000000..637367b --- /dev/null +++ b/internal/app/controller/management/pig_batch_transfer_controller.go @@ -0,0 +1,97 @@ +package management + +import ( + "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" + "github.com/gin-gonic/gin" +) + +// TransferPigsAcrossBatches godoc +// @Summary 跨猪群调栏 +// @Description 将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param sourceBatchID path int true "源猪批次ID" +// @Param body body dto.TransferPigsAcrossBatchesRequest true "跨群调栏请求信息" +// @Success 200 {object} controller.Response "调栏成功" +// @Router /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches [post] +func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) { + const action = "跨猪群调栏" + sourceBatchID, err := strconv.ParseUint(ctx.Param("sourceBatchID"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的源猪批次ID格式", action, "ID格式错误", ctx.Param("sourceBatchID")) + return + } + + var req dto.TransferPigsAcrossBatchesRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.TransferPigsAcrossBatches(uint(sourceBatchID), req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), sourceBatchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), sourceBatchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "跨猪群调栏失败", action, err.Error(), sourceBatchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "调栏成功", nil, action, "调栏成功", sourceBatchID) +} + +// TransferPigsWithinBatch godoc +// @Summary 群内调栏 +// @Description 将指定数量的猪只在同一个猪群的不同猪栏间调动 +// @Tags 猪批次管理 +// @Accept json +// @Produce json +// @Param id path int true "猪批次ID" +// @Param body body dto.TransferPigsWithinBatchRequest true "群内调栏请求信息" +// @Success 200 {object} controller.Response "调栏成功" +// @Router /api/v1/pig-batches/{id}/transfer-within-batch [post] +func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) { + const action = "群内调栏" + batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) + return + } + + var req dto.TransferPigsWithinBatchRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) + return + } + + operatorID, err := controller.GetOperatorIDFromContext(ctx) + + err = c.service.TransferPigsWithinBatch(uint(batchID), req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) + if err != nil { + if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) + return + } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) + return + } + c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "群内调栏失败", action, err.Error(), batchID) + return + } + + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "调栏成功", nil, action, "调栏成功", batchID) +} diff --git a/internal/app/dto/pig_batch_dto.go b/internal/app/dto/pig_batch_dto.go index 47b539e..8485d87 100644 --- a/internal/app/dto/pig_batch_dto.go +++ b/internal/app/dto/pig_batch_dto.go @@ -67,3 +67,94 @@ type MovePigsIntoPenRequest struct { Quantity int `json:"quantity" binding:"required,min=1"` // 移入猪只数量 Remarks string `json:"remarks"` // 备注 } + +// SellPigsRequest 用于处理卖猪的请求体 +type SellPigsRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 卖出猪只数量 + UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价 + TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价 + TraderName string `json:"traderName" binding:"required"` // 交易方名称 + TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期 + Remarks string `json:"remarks"` // 备注 +} + +// BuyPigsRequest 用于处理买猪的请求体 +type BuyPigsRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 买入猪只数量 + UnitPrice float64 `json:"unitPrice" binding:"required,min=0"` // 单价 + TotalPrice float64 `json:"totalPrice" binding:"required,min=0"` // 总价 + TraderName string `json:"traderName" binding:"required"` // 交易方名称 + TradeDate time.Time `json:"tradeDate" binding:"required"` // 交易日期 + Remarks string `json:"remarks"` // 备注 +} + +// TransferPigsAcrossBatchesRequest 用于跨猪群调栏的请求体 +type TransferPigsAcrossBatchesRequest struct { + DestBatchID uint `json:"destBatchID" binding:"required"` // 目标猪批次ID + FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID + ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID + Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量 + Remarks string `json:"remarks"` // 备注 +} + +// TransferPigsWithinBatchRequest 用于群内调栏的请求体 +type TransferPigsWithinBatchRequest struct { + FromPenID uint `json:"fromPenID" binding:"required"` // 源猪栏ID + ToPenID uint `json:"toPenID" binding:"required"` // 目标猪栏ID + Quantity uint `json:"quantity" binding:"required,min=1"` // 调栏猪只数量 + Remarks string `json:"remarks"` // 备注 +} + +// RecordSickPigsRequest 用于记录新增病猪事件的请求体 +type RecordSickPigsRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 病猪数量 + TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} + +// RecordSickPigRecoveryRequest 用于记录病猪康复事件的请求体 +type RecordSickPigRecoveryRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 康复猪数量 + TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} + +// RecordSickPigDeathRequest 用于记录病猪死亡事件的请求体 +type RecordSickPigDeathRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量 + TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} + +// RecordSickPigCullRequest 用于记录病猪淘汰事件的请求体 +type RecordSickPigCullRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量 + TreatmentLocation models.PigBatchSickPigTreatmentLocation `json:"treatmentLocation" binding:"required"` // 治疗地点 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} + +// RecordDeathRequest 用于记录正常猪只死亡事件的请求体 +type RecordDeathRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 死亡猪数量 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} + +// RecordCullRequest 用于记录正常猪只淘汰事件的请求体 +type RecordCullRequest struct { + PenID uint `json:"penID" binding:"required"` // 猪栏ID + Quantity int `json:"quantity" binding:"required,min=1"` // 淘汰猪数量 + HappenedAt time.Time `json:"happenedAt" binding:"required"` // 发生时间 + Remarks string `json:"remarks"` // 备注 +} diff --git a/internal/app/service/pig_batch_service.go b/internal/app/service/pig_batch_service.go index 32111d4..4f362c5 100644 --- a/internal/app/service/pig_batch_service.go +++ b/internal/app/service/pig_batch_service.go @@ -1,6 +1,8 @@ package service import ( + "time" + "git.huangwc.com/pig/pig-farm-controller/internal/app/dto" domain_pig "git.huangwc.com/pig/pig-farm-controller/internal/domain/pig" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -14,10 +16,30 @@ type PigBatchService interface { UpdatePigBatch(id uint, dto *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) DeletePigBatch(id uint) error ListPigBatches(isActive *bool) ([]*dto.PigBatchResponseDTO, error) + + // Pig Pen Management AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error ReclassifyPenToNewBatch(fromBatchID uint, toBatchID uint, penID uint, operatorID uint, remarks string) error RemoveEmptyPenFromBatch(batchID uint, penID uint) error MovePigsIntoPen(batchID uint, toPenID uint, quantity int, operatorID uint, remarks string) error + + // Trade Sub-service + SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error + + // Transfer Sub-service + TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error + + // Sick Pig Management + RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error + + // Normal Pig Management + RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error + RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error } // pigBatchService 的实现现在依赖于领域服务接口。 @@ -191,3 +213,103 @@ func (s *pigBatchService) MovePigsIntoPen(batchID uint, toPenID uint, quantity i } return nil } + +// SellPigs 委托给领域服务 +func (s *pigBatchService) SellPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { + err := s.domainService.SellPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID) + if err != nil { + s.logger.Errorf("应用层: 卖猪失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// BuyPigs 委托给领域服务 +func (s *pigBatchService) BuyPigs(batchID uint, penID uint, quantity int, unitPrice float64, tatalPrice float64, traderName string, tradeDate time.Time, remarks string, operatorID uint) error { + err := s.domainService.BuyPigs(batchID, penID, quantity, unitPrice, tatalPrice, traderName, tradeDate, remarks, operatorID) + if err != nil { + s.logger.Errorf("应用层: 买猪失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// TransferPigsAcrossBatches 委托给领域服务 +func (s *pigBatchService) TransferPigsAcrossBatches(sourceBatchID uint, destBatchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { + err := s.domainService.TransferPigsAcrossBatches(sourceBatchID, destBatchID, fromPenID, toPenID, quantity, operatorID, remarks) + if err != nil { + s.logger.Errorf("应用层: 跨群调栏失败, 源批次ID: %d, 错误: %v", sourceBatchID, err) + return MapDomainError(err) + } + return nil +} + +// TransferPigsWithinBatch 委托给领域服务 +func (s *pigBatchService) TransferPigsWithinBatch(batchID uint, fromPenID uint, toPenID uint, quantity uint, operatorID uint, remarks string) error { + err := s.domainService.TransferPigsWithinBatch(batchID, fromPenID, toPenID, quantity, operatorID, remarks) + if err != nil { + s.logger.Errorf("应用层: 群内调栏失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordSickPigs 委托给领域服务 +func (s *pigBatchService) RecordSickPigs(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordSickPigs(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录病猪事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordSickPigRecovery 委托给领域服务 +func (s *pigBatchService) RecordSickPigRecovery(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordSickPigRecovery(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录病猪康复事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordSickPigDeath 委托给领域服务 +func (s *pigBatchService) RecordSickPigDeath(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordSickPigDeath(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录病猪死亡事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordSickPigCull 委托给领域服务 +func (s *pigBatchService) RecordSickPigCull(operatorID uint, batchID uint, penID uint, quantity int, treatmentLocation models.PigBatchSickPigTreatmentLocation, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordSickPigCull(operatorID, batchID, penID, quantity, treatmentLocation, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录病猪淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordDeath 委托给领域服务 +func (s *pigBatchService) RecordDeath(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordDeath(operatorID, batchID, penID, quantity, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录正常猪只死亡事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} + +// RecordCull 委托给领域服务 +func (s *pigBatchService) RecordCull(operatorID uint, batchID uint, penID uint, quantity int, happenedAt time.Time, remarks string) error { + err := s.domainService.RecordCull(operatorID, batchID, penID, quantity, happenedAt, remarks) + if err != nil { + s.logger.Errorf("应用层: 记录正常猪只淘汰事件失败, 批次ID: %d, 错误: %v", batchID, err) + return MapDomainError(err) + } + return nil +} From 5e84b473f66ffbc95dc4320c509d4e16baa12510 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 23:24:16 +0800 Subject: [PATCH 62/65] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=BA=9F=E5=BC=83?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/domain/pig/pig_batch_service.go | 2 - .../domain/pig/pig_batch_service_method.go | 100 ------------------ 2 files changed, 102 deletions(-) diff --git a/internal/domain/pig/pig_batch_service.go b/internal/domain/pig/pig_batch_service.go index 2d5f6b2..d0911d1 100644 --- a/internal/domain/pig/pig_batch_service.go +++ b/internal/domain/pig/pig_batch_service.go @@ -47,8 +47,6 @@ type PigBatchService interface { DeletePigBatch(id uint) error // ListPigBatches 批量查询猪批次。 ListPigBatches(isActive *bool) ([]*models.PigBatch, error) - // UpdatePigBatchPens 更新猪批次关联的猪栏。 - UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error // AssignEmptyPensToBatch 为猪群分配空栏 AssignEmptyPensToBatch(batchID uint, penIDs []uint, operatorID uint) error // MovePigsIntoPen 将猪只从“虚拟库存”移入指定猪栏 diff --git a/internal/domain/pig/pig_batch_service_method.go b/internal/domain/pig/pig_batch_service_method.go index cd6383e..5b4422e 100644 --- a/internal/domain/pig/pig_batch_service_method.go +++ b/internal/domain/pig/pig_batch_service_method.go @@ -126,106 +126,6 @@ func (s *pigBatchService) ListPigBatches(isActive *bool) ([]*models.PigBatch, er return s.pigBatchRepo.ListPigBatches(isActive) } -// UpdatePigBatchPens 实现了在事务中更新猪批次关联猪栏的复杂逻辑。 -// 它通过调用底层的 PigPenTransferManager 来执行数据库操作,从而保持了职责的清晰。 -func (s *pigBatchService) UpdatePigBatchPens(batchID uint, desiredPenIDs []uint) error { - // 使用工作单元来确保操作的原子性 - return s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { - // 1. 验证猪批次是否存在且活跃 - // 注意: 此处依赖一个假设存在的 pigBatchRepo.GetPigBatchByIDTx 方法 - pigBatch, err := s.pigBatchRepo.GetPigBatchByIDTx(tx, batchID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrPigBatchNotFound - } - return fmt.Errorf("获取猪批次信息失败: %w", err) - } - - if !pigBatch.IsActive() { - return ErrPigBatchNotActive - } - - // 2. 获取当前关联的猪栏 (通过子服务) - currentPens, err := s.transferSvc.GetPensByBatchID(tx, batchID) - if err != nil { - return fmt.Errorf("获取当前关联猪栏失败: %w", err) - } - - currentPenMap := make(map[uint]models.Pen) - currentPenIDsSet := make(map[uint]struct{}) - for _, pen := range currentPens { - currentPenMap[pen.ID] = *pen - currentPenIDsSet[pen.ID] = struct{}{} - } - - // 3. 构建期望猪栏ID集合 - desiredPenIDsSet := make(map[uint]struct{}) - for _, penID := range desiredPenIDs { - desiredPenIDsSet[penID] = struct{}{} - } - - // 4. 计算需要添加和移除的猪栏 - var pensToRemove []uint - for penID := range currentPenIDsSet { - if _, found := desiredPenIDsSet[penID]; !found { - pensToRemove = append(pensToRemove, penID) - } - } - - var pensToAdd []uint - for _, penID := range desiredPenIDs { - if _, found := currentPenIDsSet[penID]; !found { - pensToAdd = append(pensToAdd, penID) - } - } - - // 5. 处理移除猪栏的逻辑 - for _, penID := range pensToRemove { - currentPen := currentPenMap[penID] - updates := make(map[string]interface{}) - updates["pig_batch_id"] = nil - - if currentPen.Status == models.PenStatusOccupied { - updates["status"] = models.PenStatusEmpty - } - - if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { - return fmt.Errorf("移除猪栏 %d 失败: %w", penID, err) - } - } - - // 6. 处理添加猪栏的逻辑 - for _, penID := range pensToAdd { - // 通过子服务获取猪栏信息 - actualPen, err := s.transferSvc.GetPenByID(tx, penID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("猪栏 %d 不存在: %w", penID, ErrPenNotFound) - } - return fmt.Errorf("获取猪栏 %d 信息失败: %w", penID, err) - } - - // 核心业务规则:校验猪栏是否可被分配 - if actualPen.Status != models.PenStatusEmpty { - return fmt.Errorf("猪栏 %s 状态为 %s,无法分配: %w", actualPen.PenNumber, actualPen.Status, ErrPenStatusInvalidForAllocation) - } - if actualPen.PigBatchID != nil { - return fmt.Errorf("猪栏 %s 已被其他批次 %d 使用: %w", actualPen.PenNumber, *actualPen.PigBatchID, ErrPenOccupiedByOtherBatch) - } - - updates := map[string]interface{}{ - "pig_batch_id": &batchID, - "status": models.PenStatusOccupied, - } - if err := s.transferSvc.UpdatePenFields(tx, penID, updates); err != nil { - return fmt.Errorf("添加猪栏 %d 失败: %w", penID, err) - } - } - - return nil - }) -} - // GetCurrentPigQuantity 实现了获取指定猪批次的当前猪只数量的逻辑。 func (s *pigBatchService) GetCurrentPigQuantity(batchID uint) (int, error) { var getErr error From 21661eb7481320e698b68eeb6a062be5bca43749 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Mon, 6 Oct 2025 23:48:31 +0800 Subject: [PATCH 63/65] =?UTF-8?q?=E7=AE=80=E5=8C=96=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8=E5=B1=82=E9=87=8D=E5=A4=8D=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/controller_helpers.go | 294 ++++++++++++++++++ .../management/pig_batch_controller.go | 286 ++++++----------- .../management/pig_batch_health_controller.go | 215 +++---------- .../management/pig_batch_trade_controller.go | 75 +---- .../pig_batch_transfer_controller.go | 82 ++--- 5 files changed, 477 insertions(+), 475 deletions(-) create mode 100644 internal/app/controller/management/controller_helpers.go diff --git a/internal/app/controller/management/controller_helpers.go b/internal/app/controller/management/controller_helpers.go new file mode 100644 index 0000000..ee43ed9 --- /dev/null +++ b/internal/app/controller/management/controller_helpers.go @@ -0,0 +1,294 @@ +package management + +import ( + "errors" + "strconv" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/app/service" + "github.com/gin-gonic/gin" +) + +// mapAndSendError 统一映射服务层错误并发送响应。 +// 这个函数将服务层返回的错误转换为控制器层应返回的HTTP状态码和审计信息。 +func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err error, id uint) { + if errors.Is(err, service.ErrPigBatchNotFound) || + errors.Is(err, service.ErrPenNotFound) || + errors.Is(err, service.ErrPenNotAssociatedWithBatch) { + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), id) + } else if errors.Is(err, service.ErrInvalidOperation) || + errors.Is(err, service.ErrPigBatchActive) || + errors.Is(err, service.ErrPigBatchNotActive) || + errors.Is(err, service.ErrPenOccupiedByOtherBatch) || + errors.Is(err, service.ErrPenStatusInvalidForAllocation) || + errors.Is(err, service.ErrPenNotEmpty) { + controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), id) + } else { + c.logger.Errorf("操作[%s]业务逻辑失败: %v", action, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "操作失败", action, err.Error(), id) + } +} + +// idExtractorFunc 定义了一个函数类型,用于从gin.Context中提取主ID。 +type idExtractorFunc func(ctx *gin.Context) (uint, error) + +// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。 +// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。 +// +// 参数: +// +// c: *PigBatchController - 控制器实例,用于访问其服务和日志。 +// ctx: *gin.Context - Gin上下文。 +// action: string - 当前操作的描述,用于日志和审计。 +// reqDTO: Req - 请求数据传输对象。 +// serviceExecutor: func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error - 实际执行服务层逻辑的回调函数。 +// 这个回调函数负责从ctx中解析所有必要的路径参数(除了primaryID,如果idExtractorFunc提供了), +// 获取operatorID,并调用具体的服务方法。 +// successMsg: string - 操作成功时返回给客户端的消息。 +// idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。 +func handleAPIRequest[Req any]( // 使用泛型Req + c *PigBatchController, + ctx *gin.Context, + action string, + reqDTO Req, + serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error, + successMsg string, + idExtractor idExtractorFunc, +) { + var primaryID uint // 用于审计和日志的主ID + + // 1. 绑定请求体 + if err := ctx.ShouldBindJSON(&reqDTO); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO) + return + } + + // 2. 获取操作员ID + operatorID, err := controller.GetOperatorIDFromContext(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) + return + } + + // 3. 提取主ID + if idExtractor != nil { + primaryID, err = idExtractor(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) + return + } + } else { // 默认从 ":id" 路径参数提取 + idParam := ctx.Param("id") + if idParam == "" { // 有些端点可能没有 "id" 参数,例如列表或创建操作 + // 如果没有ID参数且没有自定义提取器,primaryID保持为0,这对于某些操作是可接受的 + } else { + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) + return + } + primaryID = uint(parsedID) + } + } + + // 4. 执行服务层逻辑 + err = serviceExecutor(ctx, operatorID, primaryID, reqDTO) + if err != nil { + mapAndSendError(c, ctx, action, err, primaryID) + return + } + + // 5. 发送成功响应 + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID) +} + +// handleNoBodyAPIRequest 封装了处理不带请求体,但有路径参数和操作员ID的API请求的通用逻辑。 +func handleNoBodyAPIRequest( + c *PigBatchController, + ctx *gin.Context, + action string, + serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) error, + successMsg string, + idExtractor idExtractorFunc, +) { + var primaryID uint // 用于审计和日志的主ID + + // 1. 获取操作员ID + operatorID, err := controller.GetOperatorIDFromContext(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) + return + } + + // 2. 提取主ID + if idExtractor != nil { + primaryID, err = idExtractor(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) + return + } + } else { // 默认从 ":id" 路径参数提取 + idParam := ctx.Param("id") + if idParam == "" { + // 如果没有ID参数且没有自定义提取器,primaryID保持为0 + } else { + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) + return + } + primaryID = uint(parsedID) + } + } + + // 3. 执行服务层逻辑 + err = serviceExecutor(ctx, operatorID, primaryID) + if err != nil { + mapAndSendError(c, ctx, action, err, primaryID) + return + } + + // 4. 发送成功响应 + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID) +} + +// handleAPIRequestWithResponse 封装了控制器中处理带有请求体、路径参数并返回响应DTO的API请求的通用逻辑。 +func handleAPIRequestWithResponse[Req any, Resp any]( + c *PigBatchController, + ctx *gin.Context, + action string, + reqDTO Req, + serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) (Resp, error), // serviceExecutor现在返回Resp + successMsg string, + idExtractor idExtractorFunc, +) { + var primaryID uint // 用于审计和日志的主ID + + // 1. 绑定请求体 + if err := ctx.ShouldBindJSON(&reqDTO); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO) + return + } + + // 2. 获取操作员ID + operatorID, err := controller.GetOperatorIDFromContext(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) + return + } + + // 3. 提取主ID + if idExtractor != nil { + primaryID, err = idExtractor(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) + return + } + } else { // 默认从 ":id" 路径参数提取 + idParam := ctx.Param("id") + if idParam == "" { // 有些端点可能没有 "id" 参数,例如列表或创建操作 + // 如果没有ID参数且没有自定义提取器,primaryID保持为0,这对于某些操作是可接受的 + } else { + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) + return + } + primaryID = uint(parsedID) + } + } + + // 4. 执行服务层逻辑 + respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO) + if err != nil { + mapAndSendError(c, ctx, action, err, primaryID) + return + } + + // 5. 发送成功响应 + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID) +} + +// handleNoBodyAPIRequestWithResponse 封装了处理不带请求体,但有路径参数和操作员ID,并返回响应DTO的API请求的通用逻辑。 +func handleNoBodyAPIRequestWithResponse[Resp any]( + c *PigBatchController, + ctx *gin.Context, + action string, + serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint) (Resp, error), // serviceExecutor现在返回Resp + successMsg string, + idExtractor idExtractorFunc, +) { + var primaryID uint // 用于审计和日志的主ID + + // 1. 获取操作员ID + operatorID, err := controller.GetOperatorIDFromContext(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) + return + } + + // 2. 提取主ID + if idExtractor != nil { + primaryID, err = idExtractor(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) + return + } + } else { // 默认从 ":id" 路径参数提取 + idParam := ctx.Param("id") + if idParam == "" { + // 如果没有ID参数且没有自定义提取器,primaryID保持为0 + } else { + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) + return + } + primaryID = uint(parsedID) + } + } + + // 3. 执行服务层逻辑 + respDTO, err := serviceExecutor(ctx, operatorID, primaryID) + if err != nil { + mapAndSendError(c, ctx, action, err, primaryID) + return + } + + // 4. 发送成功响应 + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID) +} + +// handleQueryAPIRequestWithResponse 封装了处理带有查询参数并返回响应DTO的API请求的通用逻辑。 +func handleQueryAPIRequestWithResponse[Query any, Resp any]( + c *PigBatchController, + ctx *gin.Context, + action string, + queryDTO Query, + serviceExecutor func(ctx *gin.Context, operatorID uint, query Query) (Resp, error), // serviceExecutor现在接收queryDTO + successMsg string, +) { + // 1. 绑定查询参数 + if err := ctx.ShouldBindQuery(&queryDTO); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", queryDTO) + return + } + + // 2. 获取操作员ID + operatorID, err := controller.GetOperatorIDFromContext(ctx) + if err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) + return + } + + // 3. 执行服务层逻辑 + respDTO, err := serviceExecutor(ctx, operatorID, queryDTO) + if err != nil { + // 对于列表查询,通常没有primaryID,所以传递0 + mapAndSendError(c, ctx, action, err, 0) + return + } + + // 4. 发送成功响应 + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, nil) +} diff --git a/internal/app/controller/management/pig_batch_controller.go b/internal/app/controller/management/pig_batch_controller.go index 623d680..79a6bff 100644 --- a/internal/app/controller/management/pig_batch_controller.go +++ b/internal/app/controller/management/pig_batch_controller.go @@ -1,10 +1,8 @@ package management import ( - "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" @@ -38,21 +36,16 @@ func NewPigBatchController(logger *logs.Logger, service service.PigBatchService) func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { const action = "创建猪批次" var req dto.PigBatchCreateDTO - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - userID, err := controller.GetOperatorIDFromContext(ctx) - - respDTO, err := c.service.CreatePigBatch(userID, &req) - if err != nil { - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪批次失败", action, "业务逻辑失败", req) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", respDTO, action, "创建成功", respDTO) + handleAPIRequestWithResponse( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchCreateDTO) (*dto.PigBatchResponseDTO, error) { + // 对于创建操作,primaryID通常不从路径中获取,而是由服务层生成 + return c.service.CreatePigBatch(operatorID, req) + }, + "创建成功", + nil, // 无需自定义ID提取器,primaryID将为0 + ) } // GetPigBatch godoc @@ -62,27 +55,18 @@ func (c *PigBatchController) CreatePigBatch(ctx *gin.Context) { // @Produce json // @Param id path int true "猪批次ID" // @Success 200 {object} controller.Response{data=dto.PigBatchResponseDTO} "获取成功" -// @Router /api/v1/pig-batches/{id} [get] +// @Router /api/v1/pig-batches/{id} [get]\ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { const action = "获取猪批次" - id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - respDTO, err := c.service.GetPigBatch(uint(id)) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次失败", action, "业务逻辑失败", id) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTO, action, "获取成功", respDTO) + handleNoBodyAPIRequestWithResponse( + c, ctx, action, + func(ctx *gin.Context, operatorID uint, primaryID uint) (*dto.PigBatchResponseDTO, error) { + return c.service.GetPigBatch(primaryID) + }, + "获取成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // UpdatePigBatch godoc @@ -97,30 +81,16 @@ func (c *PigBatchController) GetPigBatch(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id} [put] func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { const action = "更新猪批次" - id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.PigBatchUpdateDTO - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - respDTO, err := c.service.UpdatePigBatch(uint(id), &req) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪批次失败", action, "业务逻辑失败", req) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", respDTO, action, "更新成功", respDTO) + handleAPIRequestWithResponse( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.PigBatchUpdateDTO) (*dto.PigBatchResponseDTO, error) { + return c.service.UpdatePigBatch(primaryID, req) + }, + "更新成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // DeletePigBatch godoc @@ -133,23 +103,15 @@ func (c *PigBatchController) UpdatePigBatch(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id} [delete] func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { const action = "删除猪批次" - id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - if err := c.service.DeletePigBatch(uint(id)); err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "猪批次不存在", action, "猪批次不存在", id) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除猪批次失败", action, "业务逻辑失败", id) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "删除成功", nil, action, "删除成功", id) + handleNoBodyAPIRequest( + c, ctx, action, + func(ctx *gin.Context, operatorID uint, primaryID uint) error { + return c.service.DeletePigBatch(primaryID) + }, + "删除成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // ListPigBatches godoc @@ -163,20 +125,14 @@ func (c *PigBatchController) DeletePigBatch(ctx *gin.Context) { func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { const action = "获取猪批次列表" var query dto.PigBatchQueryDTO - // ShouldBindQuery 会自动处理 URL 查询参数,例如 ?is_active=true - if err := ctx.ShouldBindQuery(&query); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数", action, "查询参数绑定失败", nil) - return - } - respDTOs, err := c.service.ListPigBatches(query.IsActive) - if err != nil { - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次列表失败", action, "业务逻辑失败", nil) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", respDTOs, action, "获取成功", respDTOs) + handleQueryAPIRequestWithResponse( + c, ctx, action, &query, + func(ctx *gin.Context, operatorID uint, query *dto.PigBatchQueryDTO) ([]*dto.PigBatchResponseDTO, error) { + return c.service.ListPigBatches(query.IsActive) + }, + "获取成功", + ) } // AssignEmptyPensToBatch godoc @@ -191,35 +147,16 @@ func (c *PigBatchController) ListPigBatches(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/assign-pens [post] func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) { const action = "为猪批次分配空栏" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.AssignEmptyPensToBatchRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - userID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.AssignEmptyPensToBatch(uint(batchID), req.PenIDs, userID) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenStatusInvalidForAllocation) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "分配空栏失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "分配成功", nil, action, "分配成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.AssignEmptyPensToBatchRequest) error { + return c.service.AssignEmptyPensToBatch(primaryID, req.PenIDs, operatorID) + }, + "分配成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // ReclassifyPenToNewBatch godoc @@ -234,35 +171,24 @@ func (c *PigBatchController) AssignEmptyPensToBatch(ctx *gin.Context) { // @Router /api/v1/pig-batches/{fromBatchID}/reclassify-pen [post] func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) { const action = "划拨猪栏到新批次" - fromBatchID, err := strconv.ParseUint(ctx.Param("fromBatchID"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的源猪批次ID格式", action, "ID格式错误", ctx.Param("fromBatchID")) - return - } - var req dto.ReclassifyPenToNewBatchRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - userID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.ReclassifyPenToNewBatch(uint(fromBatchID), req.ToBatchID, req.PenID, userID, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), fromBatchID) - return - } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenOccupiedByOtherBatch) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrInvalidOperation) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), fromBatchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "划拨猪栏失败", action, err.Error(), fromBatchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "划拨成功", nil, action, "划拨成功", fromBatchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.ReclassifyPenToNewBatchRequest) error { + // primaryID 在这里是 fromBatchID + return c.service.ReclassifyPenToNewBatch(primaryID, req.ToBatchID, req.PenID, operatorID, req.Remarks) + }, + "划拨成功", + func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":fromBatchID" 路径参数提取 + idParam := ctx.Param("fromBatchID") + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return 0, err + } + return uint(parsedID), nil + }, + ) } // RemoveEmptyPenFromBatch godoc @@ -276,33 +202,28 @@ func (c *PigBatchController) ReclassifyPenToNewBatch(ctx *gin.Context) { // @Router /api/v1/pig-batches/{batchID}/remove-pen/{penID} [delete] func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) { const action = "从猪批次移除空栏" - batchID, err := strconv.ParseUint(ctx.Param("batchID"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("batchID")) - return - } - penID, err := strconv.ParseUint(ctx.Param("penID"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪栏ID格式", action, "ID格式错误", ctx.Param("penID")) - return - } - - err = c.service.RemoveEmptyPenFromBatch(uint(batchID), uint(penID)) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "移除空栏失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "移除成功", nil, action, "移除成功", batchID) + handleNoBodyAPIRequest( + c, ctx, action, + func(ctx *gin.Context, operatorID uint, primaryID uint) error { + // primaryID 在这里是 batchID + penIDParam := ctx.Param("penID") + penID, err := strconv.ParseUint(penIDParam, 10, 32) + if err != nil { + return err // 返回错误,因为 penID 格式无效 + } + return c.service.RemoveEmptyPenFromBatch(primaryID, uint(penID)) + }, + "移除成功", + func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":batchID" 路径参数提取 + idParam := ctx.Param("batchID") + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return 0, err + } + return uint(parsedID), nil + }, + ) } // MovePigsIntoPen godoc @@ -317,33 +238,14 @@ func (c *PigBatchController) RemoveEmptyPenFromBatch(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/move-pigs-into-pen [post] func (c *PigBatchController) MovePigsIntoPen(ctx *gin.Context) { const action = "将猪只移入猪栏" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.MovePigsIntoPenRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - userID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.MovePigsIntoPen(uint(batchID), req.ToPenID, req.Quantity, userID, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrInvalidOperation) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "移入猪只失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "移入成功", nil, action, "移入成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.MovePigsIntoPenRequest) error { + return c.service.MovePigsIntoPen(primaryID, req.ToPenID, req.Quantity, operatorID, req.Remarks) + }, + "移入成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } diff --git a/internal/app/controller/management/pig_batch_health_controller.go b/internal/app/controller/management/pig_batch_health_controller.go index 3b4c09f..6afb94a 100644 --- a/internal/app/controller/management/pig_batch_health_controller.go +++ b/internal/app/controller/management/pig_batch_health_controller.go @@ -1,12 +1,7 @@ package management import ( - "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" "github.com/gin-gonic/gin" ) @@ -22,35 +17,16 @@ import ( // @Router /api/v1/pig-batches/{id}/record-sick-pigs [post] func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) { const action = "记录新增病猪事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordSickPigsRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordSickPigs(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录新增病猪事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigsRequest) error { + return c.service.RecordSickPigs(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // RecordSickPigRecovery godoc @@ -65,35 +41,16 @@ func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/record-sick-pig-recovery [post] func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) { const action = "记录病猪康复事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordSickPigRecoveryRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordSickPigRecovery(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪康复事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigRecoveryRequest) error { + return c.service.RecordSickPigRecovery(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // RecordSickPigDeath godoc @@ -108,35 +65,16 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/record-sick-pig-death [post] func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) { const action = "记录病猪死亡事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordSickPigDeathRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordSickPigDeath(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪死亡事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigDeathRequest) error { + return c.service.RecordSickPigDeath(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // RecordSickPigCull godoc @@ -151,35 +89,16 @@ func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/record-sick-pig-cull [post] func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) { const action = "记录病猪淘汰事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordSickPigCullRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordSickPigCull(operatorID, uint(batchID), req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录病猪淘汰事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordSickPigCullRequest) error { + return c.service.RecordSickPigCull(operatorID, primaryID, req.PenID, req.Quantity, req.TreatmentLocation, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // RecordDeath godoc @@ -194,35 +113,16 @@ func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/record-death [post] func (c *PigBatchController) RecordDeath(ctx *gin.Context) { const action = "记录正常猪只死亡事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordDeathRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordDeath(operatorID, uint(batchID), req.PenID, req.Quantity, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录正常猪只死亡事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordDeathRequest) error { + return c.service.RecordDeath(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // RecordCull godoc @@ -237,33 +137,14 @@ func (c *PigBatchController) RecordDeath(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/record-cull [post] func (c *PigBatchController) RecordCull(ctx *gin.Context) { const action = "记录正常猪只淘汰事件" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.RecordCullRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.RecordCull(operatorID, uint(batchID), req.PenID, req.Quantity, req.HappenedAt, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "记录正常猪只淘汰事件失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "记录成功", nil, action, "记录成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.RecordCullRequest) error { + return c.service.RecordCull(operatorID, primaryID, req.PenID, req.Quantity, req.HappenedAt, req.Remarks) + }, + "记录成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } diff --git a/internal/app/controller/management/pig_batch_trade_controller.go b/internal/app/controller/management/pig_batch_trade_controller.go index 821203c..08f2505 100644 --- a/internal/app/controller/management/pig_batch_trade_controller.go +++ b/internal/app/controller/management/pig_batch_trade_controller.go @@ -1,12 +1,7 @@ package management import ( - "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" "github.com/gin-gonic/gin" ) @@ -22,35 +17,16 @@ import ( // @Router /api/v1/pig-batches/{id}/sell-pigs [post] func (c *PigBatchController) SellPigs(ctx *gin.Context) { const action = "卖猪" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.SellPigsRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.SellPigs(uint(batchID), req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) || errors.Is(err, service.ErrPenNotEmpty) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "卖猪失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "卖猪成功", nil, action, "卖猪成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.SellPigsRequest) error { + return c.service.SellPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) + }, + "卖猪成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } // BuyPigs godoc @@ -65,33 +41,14 @@ func (c *PigBatchController) SellPigs(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/buy-pigs [post] func (c *PigBatchController) BuyPigs(ctx *gin.Context) { const action = "买猪" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.BuyPigsRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.BuyPigs(uint(batchID), req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "买猪失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "买猪成功", nil, action, "买猪成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.BuyPigsRequest) error { + return c.service.BuyPigs(primaryID, req.PenID, req.Quantity, req.UnitPrice, req.TotalPrice, req.TraderName, req.TradeDate, req.Remarks, operatorID) + }, + "买猪成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } diff --git a/internal/app/controller/management/pig_batch_transfer_controller.go b/internal/app/controller/management/pig_batch_transfer_controller.go index 637367b..005a234 100644 --- a/internal/app/controller/management/pig_batch_transfer_controller.go +++ b/internal/app/controller/management/pig_batch_transfer_controller.go @@ -1,12 +1,9 @@ package management import ( - "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" "github.com/gin-gonic/gin" ) @@ -22,35 +19,24 @@ import ( // @Router /api/v1/pig-batches/{sourceBatchID}/transfer-across-batches [post] func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) { const action = "跨猪群调栏" - sourceBatchID, err := strconv.ParseUint(ctx.Param("sourceBatchID"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的源猪批次ID格式", action, "ID格式错误", ctx.Param("sourceBatchID")) - return - } - var req dto.TransferPigsAcrossBatchesRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.TransferPigsAcrossBatches(uint(sourceBatchID), req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), sourceBatchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), sourceBatchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "跨猪群调栏失败", action, err.Error(), sourceBatchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "调栏成功", nil, action, "调栏成功", sourceBatchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsAcrossBatchesRequest) error { + // primaryID 在这里是 sourceBatchID + return c.service.TransferPigsAcrossBatches(primaryID, req.DestBatchID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) + }, + "调栏成功", + func(ctx *gin.Context) (uint, error) { // 自定义ID提取器,从 ":sourceBatchID" 路径参数提取 + idParam := ctx.Param("sourceBatchID") + parsedID, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return 0, err + } + return uint(parsedID), nil + }, + ) } // TransferPigsWithinBatch godoc @@ -65,33 +51,15 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) { // @Router /api/v1/pig-batches/{id}/transfer-within-batch [post] func (c *PigBatchController) TransferPigsWithinBatch(ctx *gin.Context) { const action = "群内调栏" - batchID, err := strconv.ParseUint(ctx.Param("id"), 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪批次ID格式", action, "ID格式错误", ctx.Param("id")) - return - } - var req dto.TransferPigsWithinBatchRequest - if err := ctx.ShouldBindJSON(&req); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", req) - return - } - operatorID, err := controller.GetOperatorIDFromContext(ctx) - - err = c.service.TransferPigsWithinBatch(uint(batchID), req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) - if err != nil { - if errors.Is(err, service.ErrPigBatchNotFound) || errors.Is(err, service.ErrPenNotFound) { - controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), action, err.Error(), batchID) - return - } else if errors.Is(err, service.ErrInvalidOperation) || errors.Is(err, service.ErrPigBatchNotActive) || errors.Is(err, service.ErrPenNotAssociatedWithBatch) { - controller.SendErrorWithAudit(ctx, controller.CodeConflict, err.Error(), action, err.Error(), batchID) - return - } - c.logger.Errorf("%s: 业务逻辑失败: %v", action, err) - controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "群内调栏失败", action, err.Error(), batchID) - return - } - - controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "调栏成功", nil, action, "调栏成功", batchID) + handleAPIRequest( + c, ctx, action, &req, + func(ctx *gin.Context, operatorID uint, primaryID uint, req *dto.TransferPigsWithinBatchRequest) error { + // primaryID 在这里是 batchID + return c.service.TransferPigsWithinBatch(primaryID, req.FromPenID, req.ToPenID, req.Quantity, operatorID, req.Remarks) + }, + "调栏成功", + nil, // 默认从 ":id" 路径参数提取ID + ) } From 77ab434d17b66d6ce7e00877b41562ccc068a2e8 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 7 Oct 2025 00:18:17 +0800 Subject: [PATCH 64/65] =?UTF-8?q?=E7=AE=80=E5=8C=96=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8=E5=B1=82=E9=87=8D=E5=A4=8D=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/controller_helpers.go | 184 ++++++------------ .../management/pig_batch_health_controller.go | 12 +- .../management/pig_batch_trade_controller.go | 4 +- .../pig_batch_transfer_controller.go | 4 +- 4 files changed, 74 insertions(+), 130 deletions(-) diff --git a/internal/app/controller/management/controller_helpers.go b/internal/app/controller/management/controller_helpers.go index ee43ed9..a37bd27 100644 --- a/internal/app/controller/management/controller_helpers.go +++ b/internal/app/controller/management/controller_helpers.go @@ -32,50 +32,40 @@ func mapAndSendError(c *PigBatchController, ctx *gin.Context, action string, err // idExtractorFunc 定义了一个函数类型,用于从gin.Context中提取主ID。 type idExtractorFunc func(ctx *gin.Context) (uint, error) -// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。 -// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。 +// extractOperatorAndPrimaryID 封装了从gin.Context中提取操作员ID和主ID的通用逻辑。 +// 它负责处理ID提取过程中的错误,并发送相应的HTTP响应。 // // 参数: // -// c: *PigBatchController - 控制器实例,用于访问其服务和日志。 +// c: *PigBatchController - 控制器实例,用于访问其日志。 // ctx: *gin.Context - Gin上下文。 // action: string - 当前操作的描述,用于日志和审计。 -// reqDTO: Req - 请求数据传输对象。 -// serviceExecutor: func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error - 实际执行服务层逻辑的回调函数。 -// 这个回调函数负责从ctx中解析所有必要的路径参数(除了primaryID,如果idExtractorFunc提供了), -// 获取operatorID,并调用具体的服务方法。 -// successMsg: string - 操作成功时返回给客户端的消息。 // idExtractor: idExtractorFunc - 可选函数,用于从ctx中提取主ID。如果为nil,则尝试从":id"路径参数中提取。 -func handleAPIRequest[Req any]( // 使用泛型Req +// +// 返回值: +// +// operatorID: uint - 提取到的操作员ID。 +// primaryID: uint - 提取到的主ID。 +// ok: bool - 如果ID提取成功且没有发送错误响应,则为true。 +func extractOperatorAndPrimaryID( c *PigBatchController, ctx *gin.Context, action string, - reqDTO Req, - serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error, - successMsg string, idExtractor idExtractorFunc, -) { - var primaryID uint // 用于审计和日志的主ID - - // 1. 绑定请求体 - if err := ctx.ShouldBindJSON(&reqDTO); err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO) - return - } - - // 2. 获取操作员ID +) (operatorID uint, primaryID uint, ok bool) { + // 1. 获取操作员ID operatorID, err := controller.GetOperatorIDFromContext(ctx) if err != nil { controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) - return + return 0, 0, false } - // 3. 提取主ID + // 2. 提取主ID if idExtractor != nil { primaryID, err = idExtractor(ctx) if err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) - return + return 0, 0, false } } else { // 默认从 ":id" 路径参数提取 idParam := ctx.Param("id") @@ -85,20 +75,46 @@ func handleAPIRequest[Req any]( // 使用泛型Req parsedID, err := strconv.ParseUint(idParam, 10, 32) if err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) - return + return 0, 0, false } primaryID = uint(parsedID) } } - // 4. 执行服务层逻辑 - err = serviceExecutor(ctx, operatorID, primaryID, reqDTO) + return operatorID, primaryID, true +} + +// handleAPIRequest 封装了控制器中处理带有请求体和路径参数的API请求的通用逻辑。 +// 它负责请求体绑定、操作员ID获取、服务层调用、错误映射和响应发送。 +func handleAPIRequest[Req any]( + c *PigBatchController, + ctx *gin.Context, + action string, + reqDTO Req, + serviceExecutor func(ctx *gin.Context, operatorID uint, primaryID uint, req Req) error, + successMsg string, + idExtractor idExtractorFunc, +) { + // 1. 绑定请求体 + if err := ctx.ShouldBindJSON(&reqDTO); err != nil { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO) + return + } + + // 2. 提取操作员ID和主ID + operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor) + if !ok { + return // 错误已在 extractOperatorAndPrimaryID 中处理 + } + + // 3. 执行服务层逻辑 + err := serviceExecutor(ctx, operatorID, primaryID, reqDTO) if err != nil { mapAndSendError(c, ctx, action, err, primaryID) return } - // 5. 发送成功响应 + // 4. 发送成功响应 controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID) } @@ -111,44 +127,20 @@ func handleNoBodyAPIRequest( successMsg string, idExtractor idExtractorFunc, ) { - var primaryID uint // 用于审计和日志的主ID - - // 1. 获取操作员ID - operatorID, err := controller.GetOperatorIDFromContext(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) - return + // 1. 提取操作员ID和主ID + operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor) + if !ok { + return // 错误已在 extractOperatorAndPrimaryID 中处理 } - // 2. 提取主ID - if idExtractor != nil { - primaryID, err = idExtractor(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) - return - } - } else { // 默认从 ":id" 路径参数提取 - idParam := ctx.Param("id") - if idParam == "" { - // 如果没有ID参数且没有自定义提取器,primaryID保持为0 - } else { - parsedID, err := strconv.ParseUint(idParam, 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) - return - } - primaryID = uint(parsedID) - } - } - - // 3. 执行服务层逻辑 - err = serviceExecutor(ctx, operatorID, primaryID) + // 2. 执行服务层逻辑 + err := serviceExecutor(ctx, operatorID, primaryID) if err != nil { mapAndSendError(c, ctx, action, err, primaryID) return } - // 4. 发送成功响应 + // 3. 发送成功响应 controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, nil, action, successMsg, primaryID) } @@ -162,50 +154,26 @@ func handleAPIRequestWithResponse[Req any, Resp any]( successMsg string, idExtractor idExtractorFunc, ) { - var primaryID uint // 用于审计和日志的主ID - // 1. 绑定请求体 if err := ctx.ShouldBindJSON(&reqDTO); err != nil { controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体", action, "请求体绑定失败", reqDTO) return } - // 2. 获取操作员ID - operatorID, err := controller.GetOperatorIDFromContext(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) - return + // 2. 提取操作员ID和主ID + operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor) + if !ok { + return // 错误已在 extractOperatorAndPrimaryID 中处理 } - // 3. 提取主ID - if idExtractor != nil { - primaryID, err = idExtractor(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) - return - } - } else { // 默认从 ":id" 路径参数提取 - idParam := ctx.Param("id") - if idParam == "" { // 有些端点可能没有 "id" 参数,例如列表或创建操作 - // 如果没有ID参数且没有自定义提取器,primaryID保持为0,这对于某些操作是可接受的 - } else { - parsedID, err := strconv.ParseUint(idParam, 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) - return - } - primaryID = uint(parsedID) - } - } - - // 4. 执行服务层逻辑 + // 3. 执行服务层逻辑 respDTO, err := serviceExecutor(ctx, operatorID, primaryID, reqDTO) if err != nil { mapAndSendError(c, ctx, action, err, primaryID) return } - // 5. 发送成功响应 + // 4. 发送成功响应 controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID) } @@ -218,44 +186,20 @@ func handleNoBodyAPIRequestWithResponse[Resp any]( successMsg string, idExtractor idExtractorFunc, ) { - var primaryID uint // 用于审计和日志的主ID - - // 1. 获取操作员ID - operatorID, err := controller.GetOperatorIDFromContext(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeUnauthorized, "未授权", action, "无法获取操作员ID", nil) - return + // 1. 提取操作员ID和主ID + operatorID, primaryID, ok := extractOperatorAndPrimaryID(c, ctx, action, idExtractor) + if !ok { + return // 错误已在 extractOperatorAndPrimaryID 中处理 } - // 2. 提取主ID - if idExtractor != nil { - primaryID, err = idExtractor(ctx) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", err.Error()) - return - } - } else { // 默认从 ":id" 路径参数提取 - idParam := ctx.Param("id") - if idParam == "" { - // 如果没有ID参数且没有自定义提取器,primaryID保持为0 - } else { - parsedID, err := strconv.ParseUint(idParam, 10, 32) - if err != nil { - controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的ID格式", action, "ID格式错误", idParam) - return - } - primaryID = uint(parsedID) - } - } - - // 3. 执行服务层逻辑 + // 2. 执行服务层逻辑 respDTO, err := serviceExecutor(ctx, operatorID, primaryID) if err != nil { mapAndSendError(c, ctx, action, err, primaryID) return } - // 4. 发送成功响应 + // 3. 发送成功响应 controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, successMsg, respDTO, action, successMsg, primaryID) } diff --git a/internal/app/controller/management/pig_batch_health_controller.go b/internal/app/controller/management/pig_batch_health_controller.go index 6afb94a..0bcff82 100644 --- a/internal/app/controller/management/pig_batch_health_controller.go +++ b/internal/app/controller/management/pig_batch_health_controller.go @@ -8,7 +8,7 @@ import ( // RecordSickPigs godoc // @Summary 记录新增病猪事件 // @Description 记录猪批次中新增病猪的数量、治疗地点和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -32,7 +32,7 @@ func (c *PigBatchController) RecordSickPigs(ctx *gin.Context) { // RecordSickPigRecovery godoc // @Summary 记录病猪康复事件 // @Description 记录猪批次中病猪康复的数量、治疗地点和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -56,7 +56,7 @@ func (c *PigBatchController) RecordSickPigRecovery(ctx *gin.Context) { // RecordSickPigDeath godoc // @Summary 记录病猪死亡事件 // @Description 记录猪批次中病猪死亡的数量、治疗地点和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -80,7 +80,7 @@ func (c *PigBatchController) RecordSickPigDeath(ctx *gin.Context) { // RecordSickPigCull godoc // @Summary 记录病猪淘汰事件 // @Description 记录猪批次中病猪淘汰的数量、治疗地点和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -104,7 +104,7 @@ func (c *PigBatchController) RecordSickPigCull(ctx *gin.Context) { // RecordDeath godoc // @Summary 记录正常猪只死亡事件 // @Description 记录猪批次中正常猪只死亡的数量和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -128,7 +128,7 @@ func (c *PigBatchController) RecordDeath(ctx *gin.Context) { // RecordCull godoc // @Summary 记录正常猪只淘汰事件 // @Description 记录猪批次中正常猪只淘汰的数量和发生时间 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" diff --git a/internal/app/controller/management/pig_batch_trade_controller.go b/internal/app/controller/management/pig_batch_trade_controller.go index 08f2505..74e5bd8 100644 --- a/internal/app/controller/management/pig_batch_trade_controller.go +++ b/internal/app/controller/management/pig_batch_trade_controller.go @@ -8,7 +8,7 @@ import ( // SellPigs godoc // @Summary 处理卖猪的业务逻辑 // @Description 记录猪批次中的猪只出售事件 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" @@ -32,7 +32,7 @@ func (c *PigBatchController) SellPigs(ctx *gin.Context) { // BuyPigs godoc // @Summary 处理买猪的业务逻辑 // @Description 记录猪批次中的猪只购买事件 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" diff --git a/internal/app/controller/management/pig_batch_transfer_controller.go b/internal/app/controller/management/pig_batch_transfer_controller.go index 005a234..9089791 100644 --- a/internal/app/controller/management/pig_batch_transfer_controller.go +++ b/internal/app/controller/management/pig_batch_transfer_controller.go @@ -10,7 +10,7 @@ import ( // TransferPigsAcrossBatches godoc // @Summary 跨猪群调栏 // @Description 将指定数量的猪只从一个猪群的猪栏调动到另一个猪群的猪栏 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param sourceBatchID path int true "源猪批次ID" @@ -42,7 +42,7 @@ func (c *PigBatchController) TransferPigsAcrossBatches(ctx *gin.Context) { // TransferPigsWithinBatch godoc // @Summary 群内调栏 // @Description 将指定数量的猪只在同一个猪群的不同猪栏间调动 -// @Tags 猪批次管理 +// @Tags 猪群管理 // @Accept json // @Produce json // @Param id path int true "猪批次ID" From 4250f27e11e4a3c55a84dd7ea7540934194e0006 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 7 Oct 2025 13:31:56 +0800 Subject: [PATCH 65/65] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/api/api.go | 146 ++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 56aa248..53da648 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -48,7 +48,7 @@ type API struct { deviceController *device.Controller // 设备控制器实例 planController *plan.Controller // 计划控制器实例 pigFarmController *management.PigFarmController // 猪场管理控制器实例 - pigBatchController *management.PigBatchController // 猪批次控制器实例 + pigBatchController *management.PigBatchController // 猪群控制器实例 listenHandler webhook.ListenHandler // 设备上行事件监听器 analysisTaskManager *task.AnalysisPlanTaskManager // 计划触发器管理器实例 } @@ -63,7 +63,7 @@ func NewAPI(cfg config.ServerConfig, deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 planRepository repository.PlanRepository, pigFarmService service.PigFarmService, - pigBatchService service.PigBatchService, // 添加猪批次服务 + pigBatchService service.PigBatchService, // 添加猪群服务 userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, auditService audit.Service, // 注入审计服务 @@ -98,7 +98,7 @@ func NewAPI(cfg config.ServerConfig, planController: plan.NewController(logger, planRepository, analysisTaskManager), // 在 NewAPI 中初始化猪场管理控制器 pigFarmController: management.NewPigFarmController(logger, pigFarmService), - // 在 NewAPI 中初始化猪批次控制器 + // 在 NewAPI 中初始化猪群控制器 pigBatchController: management.NewPigBatchController(logger, pigBatchService), } @@ -114,142 +114,142 @@ func (a *API) setupRoutes() { // 这些路由不需要身份验证 // 用户注册和登录 - a.engine.POST("/api/v1/users", a.userController.CreateUser) - a.engine.POST("/api/v1/users/login", a.userController.Login) + a.engine.POST("/api/v1/users", a.userController.CreateUser) // 注册新用户 + a.engine.POST("/api/v1/users/login", a.userController.Login) // 用户登录 a.logger.Info("公开接口注册成功:用户注册、登录") // 注册 pprof 路由 pprofGroup := a.engine.Group("/debug/pprof") { - pprofGroup.GET("/", gin.WrapF(pprof.Index)) - pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) - pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) - pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol)) - pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) - pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) - pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) - pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block"))) + pprofGroup.GET("/", gin.WrapF(pprof.Index)) // pprof 索引页 + pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline)) // pprof 命令行参数 + pprofGroup.GET("/profile", gin.WrapF(pprof.Profile)) // pprof CPU profile + pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (POST) + pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol)) // pprof 符号查找 (GET) + pprofGroup.GET("/trace", gin.WrapF(pprof.Trace)) // pprof 跟踪 + pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs"))) // pprof 内存分配 + pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block"))) // pprof 阻塞 pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine"))) - pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap"))) - pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) + pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap"))) // pprof 堆内存 + pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex"))) // pprof 互斥锁 pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate"))) } a.logger.Info("pprof 接口注册成功") // 上行事件监听路由 - a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) + a.engine.POST("/upstream", gin.WrapH(a.listenHandler.Handler())) // 处理设备上行事件 a.logger.Info("上行事件监听接口注册成功") // 添加 Swagger UI 路由, Swagger UI可在 /swagger/index.html 上找到 - a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + a.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Swagger UI 接口 a.logger.Info("Swagger UI 接口注册成功") // --- Authenticated Routes --- // 所有在此注册的路由都需要通过 JWT 身份验证 authGroup := a.engine.Group("/api/v1") - authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证 - authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志 + authGroup.Use(middleware.AuthMiddleware(a.tokenService, a.userRepo)) // 1. 身份认证中间件 + authGroup.Use(middleware.AuditLogMiddleware(a.auditService)) // 2. 审计日志中间件 { // 用户相关路由组 userGroup := authGroup.Group("/users") { - userGroup.GET("/:id/history", a.userController.ListUserHistory) + userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史 } a.logger.Info("用户相关接口注册成功 (需要认证和审计)") // 设备相关路由组 deviceGroup := authGroup.Group("/devices") { - deviceGroup.POST("", a.deviceController.CreateDevice) - deviceGroup.GET("", a.deviceController.ListDevices) - deviceGroup.GET("/:id", a.deviceController.GetDevice) - deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) - deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) + deviceGroup.POST("", a.deviceController.CreateDevice) // 创建设备 + deviceGroup.GET("", a.deviceController.ListDevices) // 获取设备列表 + deviceGroup.GET("/:id", a.deviceController.GetDevice) // 获取单个设备 + deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) // 更新设备 + deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) // 删除设备 } a.logger.Info("设备相关接口注册成功 (需要认证和审计)") // 区域主控相关路由组 areaControllerGroup := authGroup.Group("/area-controllers") { - areaControllerGroup.POST("", a.deviceController.CreateAreaController) - areaControllerGroup.GET("", a.deviceController.ListAreaControllers) - areaControllerGroup.GET("/:id", a.deviceController.GetAreaController) - areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController) - areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) + areaControllerGroup.POST("", a.deviceController.CreateAreaController) // 创建区域主控 + areaControllerGroup.GET("", a.deviceController.ListAreaControllers) // 获取区域主控列表 + areaControllerGroup.GET("/:id", a.deviceController.GetAreaController) // 获取单个区域主控 + areaControllerGroup.PUT("/:id", a.deviceController.UpdateAreaController) // 更新区域主控 + areaControllerGroup.DELETE("/:id", a.deviceController.DeleteAreaController) // 删除区域主控 } a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)") // 设备模板相关路由组 deviceTemplateGroup := authGroup.Group("/device-templates") { - deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate) - deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates) - deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate) - deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) - deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) + deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate) // 创建设备模板 + deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates) // 获取设备模板列表 + deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate) // 获取单个设备模板 + deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) // 更新设备模板 + deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) // 删除设备模板 } a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)") // 计划相关路由组 planGroup := authGroup.Group("/plans") { - planGroup.POST("", a.planController.CreatePlan) - planGroup.GET("", a.planController.ListPlans) - planGroup.GET("/:id", a.planController.GetPlan) - planGroup.PUT("/:id", a.planController.UpdatePlan) - planGroup.DELETE("/:id", a.planController.DeletePlan) - planGroup.POST("/:id/start", a.planController.StartPlan) - planGroup.POST("/:id/stop", a.planController.StopPlan) + planGroup.POST("", a.planController.CreatePlan) // 创建计划 + planGroup.GET("", a.planController.ListPlans) // 获取计划列表 + planGroup.GET("/:id", a.planController.GetPlan) // 获取单个计划 + planGroup.PUT("/:id", a.planController.UpdatePlan) // 更新计划 + planGroup.DELETE("/:id", a.planController.DeletePlan) // 删除计划 + planGroup.POST("/:id/start", a.planController.StartPlan) // 启动计划 + planGroup.POST("/:id/stop", a.planController.StopPlan) // 停止计划 } a.logger.Info("计划相关接口注册成功 (需要认证和审计)") // 猪舍相关路由组 pigHouseGroup := authGroup.Group("/pig-houses") { - pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse) - pigHouseGroup.GET("", a.pigFarmController.ListPigHouses) - pigHouseGroup.GET("/:id", a.pigFarmController.GetPigHouse) - pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) - pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) + pigHouseGroup.POST("", a.pigFarmController.CreatePigHouse) // 创建猪舍 + pigHouseGroup.GET("", a.pigFarmController.ListPigHouses) // 获取猪舍列表 + pigHouseGroup.GET("/:id", a.pigFarmController.GetPigHouse) // 获取单个猪舍 + pigHouseGroup.PUT("/:id", a.pigFarmController.UpdatePigHouse) // 更新猪舍 + pigHouseGroup.DELETE("/:id", a.pigFarmController.DeletePigHouse) // 删除猪舍 } a.logger.Info("猪舍相关接口注册成功 (需要认证和审计)") // 猪圈相关路由组 penGroup := authGroup.Group("/pens") { - penGroup.POST("", a.pigFarmController.CreatePen) - penGroup.GET("", a.pigFarmController.ListPens) - penGroup.GET("/:id", a.pigFarmController.GetPen) - penGroup.PUT("/:id", a.pigFarmController.UpdatePen) - penGroup.DELETE("/:id", a.pigFarmController.DeletePen) - penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) + penGroup.POST("", a.pigFarmController.CreatePen) // 创建猪圈 + penGroup.GET("", a.pigFarmController.ListPens) // 获取猪圈列表 + penGroup.GET("/:id", a.pigFarmController.GetPen) // 获取单个猪圈 + penGroup.PUT("/:id", a.pigFarmController.UpdatePen) // 更新猪圈 + penGroup.DELETE("/:id", a.pigFarmController.DeletePen) // 删除猪圈 + penGroup.PUT("/:id/status", a.pigFarmController.UpdatePenStatus) // 更新猪圈状态 } a.logger.Info("猪圈相关接口注册成功 (需要认证和审计)") // 猪群相关路由组 pigBatchGroup := authGroup.Group("/pig-batches") { - pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch) - pigBatchGroup.GET("", a.pigBatchController.ListPigBatches) - pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) - pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) - pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) - pigBatchGroup.POST("/:id/assign-pens", a.pigBatchController.AssignEmptyPensToBatch) - pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch) - pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch) - pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen) - pigBatchGroup.POST("/:id/sell-pigs", a.pigBatchController.SellPigs) - pigBatchGroup.POST("/:id/buy-pigs", a.pigBatchController.BuyPigs) - pigBatchGroup.POST("/:sourceBatchID/transfer-across-batches", a.pigBatchController.TransferPigsAcrossBatches) - pigBatchGroup.POST("/:id/transfer-within-batch", a.pigBatchController.TransferPigsWithinBatch) - pigBatchGroup.POST("/:id/record-sick-pigs", a.pigBatchController.RecordSickPigs) - pigBatchGroup.POST("/:id/record-sick-pig-recovery", a.pigBatchController.RecordSickPigRecovery) - pigBatchGroup.POST("/:id/record-sick-pig-death", a.pigBatchController.RecordSickPigDeath) - pigBatchGroup.POST("/:id/record-sick-pig-cull", a.pigBatchController.RecordSickPigCull) - pigBatchGroup.POST("/:id/record-death", a.pigBatchController.RecordDeath) - pigBatchGroup.POST("/:id/record-cull", a.pigBatchController.RecordCull) + pigBatchGroup.POST("", a.pigBatchController.CreatePigBatch) // 创建猪群 + pigBatchGroup.GET("", a.pigBatchController.ListPigBatches) // 获取猪群列表 + pigBatchGroup.GET("/:id", a.pigBatchController.GetPigBatch) // 获取单个猪群 + pigBatchGroup.PUT("/:id", a.pigBatchController.UpdatePigBatch) // 更新猪群 + pigBatchGroup.DELETE("/:id", a.pigBatchController.DeletePigBatch) // 删除猪群 + pigBatchGroup.POST("/:id/assign-pens", a.pigBatchController.AssignEmptyPensToBatch) // 为猪群分配空栏 + pigBatchGroup.POST("/:fromBatchID/reclassify-pen", a.pigBatchController.ReclassifyPenToNewBatch) // 将猪栏划拨到新群 + pigBatchGroup.DELETE("/:batchID/remove-pen/:penID", a.pigBatchController.RemoveEmptyPenFromBatch) // 从猪群移除空栏 + pigBatchGroup.POST("/:id/move-pigs-into-pen", a.pigBatchController.MovePigsIntoPen) // 将猪只从“虚拟库存”移入指定猪栏 + pigBatchGroup.POST("/:id/sell-pigs", a.pigBatchController.SellPigs) // 处理卖猪业务 + pigBatchGroup.POST("/:id/buy-pigs", a.pigBatchController.BuyPigs) // 处理买猪业务 + pigBatchGroup.POST("/:sourceBatchID/transfer-across-batches", a.pigBatchController.TransferPigsAcrossBatches) // 跨猪群调栏 + pigBatchGroup.POST("/:id/transfer-within-batch", a.pigBatchController.TransferPigsWithinBatch) // 群内调栏 + pigBatchGroup.POST("/:id/record-sick-pigs", a.pigBatchController.RecordSickPigs) // 记录新增病猪事件 + pigBatchGroup.POST("/:id/record-sick-pig-recovery", a.pigBatchController.RecordSickPigRecovery) // 记录病猪康复事件 + pigBatchGroup.POST("/:id/record-sick-pig-death", a.pigBatchController.RecordSickPigDeath) // 记录病猪死亡事件 + pigBatchGroup.POST("/:id/record-sick-pig-cull", a.pigBatchController.RecordSickPigCull) // 记录病猪淘汰事件 + pigBatchGroup.POST("/:id/record-death", a.pigBatchController.RecordDeath) // 记录正常猪只死亡事件 + pigBatchGroup.POST("/:id/record-cull", a.pigBatchController.RecordCull) // 记录正常猪只淘汰事件 } - a.logger.Info("猪批次相关接口注册成功 (需要认证和审计)") + a.logger.Info("猪群相关接口注册成功 (需要认证和审计)") } }