Merge pull request 'issue_49' (#51) from issue_49 into main
Reviewed-on: #51
This commit is contained in:
		
							
								
								
									
										305
									
								
								docs/docs.go
									
									
									
									
									
								
							
							
						
						
									
										305
									
								
								docs/docs.go
									
									
									
									
									
								
							| @@ -756,17 +756,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -781,12 +781,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "boolean", | ||||
|                         "name": "received_success", | ||||
|                         "name": "receivedSuccess", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -830,22 +830,22 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "feed_formula_id", | ||||
|                         "name": "feedFormulaID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -860,12 +860,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -909,22 +909,22 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "medication_id", | ||||
|                         "name": "medicationID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -939,7 +939,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -949,7 +949,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -993,12 +993,11 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "enum": [ | ||||
|                             7, | ||||
|                             -1, | ||||
|                             0, | ||||
|                             1, | ||||
| @@ -1008,12 +1007,12 @@ const docTemplate = `{ | ||||
|                             5, | ||||
|                             -1, | ||||
|                             5, | ||||
|                             6 | ||||
|                             6, | ||||
|                             7 | ||||
|                         ], | ||||
|                         "type": "integer", | ||||
|                         "format": "int32", | ||||
|                         "x-enum-varnames": [ | ||||
|                             "_numLevels", | ||||
|                             "DebugLevel", | ||||
|                             "InfoLevel", | ||||
|                             "WarnLevel", | ||||
| @@ -1023,7 +1022,8 @@ const docTemplate = `{ | ||||
|                             "FatalLevel", | ||||
|                             "_minLevel", | ||||
|                             "_maxLevel", | ||||
|                             "InvalidLevel" | ||||
|                             "InvalidLevel", | ||||
|                             "_numLevels" | ||||
|                         ], | ||||
|                         "name": "level", | ||||
|                         "in": "query" | ||||
| @@ -1042,12 +1042,12 @@ const docTemplate = `{ | ||||
|                             "NotifierTypeLark", | ||||
|                             "NotifierTypeLog" | ||||
|                         ], | ||||
|                         "name": "notifier_type", | ||||
|                         "name": "notifierType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1062,7 +1062,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1092,7 +1092,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "name": "userID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1136,17 +1136,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1161,7 +1161,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1210,22 +1210,22 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "change_type", | ||||
|                         "name": "changeType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1240,12 +1240,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1289,17 +1289,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1314,12 +1314,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1373,17 +1373,17 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1398,12 +1398,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1447,17 +1447,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1472,12 +1472,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1487,12 +1487,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "treatment_location", | ||||
|                         "name": "treatmentLocation", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1536,22 +1536,22 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "correlation_id", | ||||
|                         "name": "correlationID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1566,22 +1566,22 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "transfer_type", | ||||
|                         "name": "transferType", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1625,12 +1625,12 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1645,12 +1645,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "plan_id", | ||||
|                         "name": "planID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1699,12 +1699,12 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1719,12 +1719,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "raw_material_id", | ||||
|                         "name": "rawMaterialID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1773,12 +1773,12 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1793,22 +1793,22 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "raw_material_id", | ||||
|                         "name": "rawMaterialID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "source_id", | ||||
|                         "name": "sourceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "source_type", | ||||
|                         "name": "sourceType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1852,17 +1852,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1877,12 +1877,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "sensor_type", | ||||
|                         "name": "sensorType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1926,12 +1926,12 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1946,12 +1946,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "plan_execution_log_id", | ||||
|                         "name": "planExecutionLogID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1961,7 +1961,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "task_id", | ||||
|                         "name": "taskID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -2005,17 +2005,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "action_type", | ||||
|                         "name": "actionType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2030,7 +2030,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2040,7 +2040,7 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "name": "userID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2089,12 +2089,12 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2109,12 +2109,12 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -2158,17 +2158,17 @@ const docTemplate = `{ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2183,17 +2183,17 @@ const docTemplate = `{ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "weighing_batch_id", | ||||
|                         "name": "weighingBatchID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -3415,7 +3415,7 @@ const docTemplate = `{ | ||||
|                         "BearerAuth": [] | ||||
|                     } | ||||
|                 ], | ||||
|                 "description": "创建一个新的猪舍", | ||||
|                 "description": "根据提供的信息创建一个新猪舍", | ||||
|                 "consumes": [ | ||||
|                     "application/json" | ||||
|                 ], | ||||
| @@ -4003,97 +4003,6 @@ const docTemplate = `{ | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "/api/v1/users/{id}/history": { | ||||
|             "get": { | ||||
|                 "security": [ | ||||
|                     { | ||||
|                         "BearerAuth": [] | ||||
|                     } | ||||
|                 ], | ||||
|                 "description": "根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。", | ||||
|                 "produces": [ | ||||
|                     "application/json" | ||||
|                 ], | ||||
|                 "tags": [ | ||||
|                     "用户管理" | ||||
|                 ], | ||||
|                 "summary": "获取指定用户的操作历史", | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "description": "用户ID", | ||||
|                         "name": "id", | ||||
|                         "in": "path", | ||||
|                         "required": true | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "action_type", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "page", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pageSize", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "status", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "username", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
|                 "responses": { | ||||
|                     "200": { | ||||
|                         "description": "业务码为200代表成功获取", | ||||
|                         "schema": { | ||||
|                             "allOf": [ | ||||
|                                 { | ||||
|                                     "$ref": "#/definitions/controller.Response" | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     "type": "object", | ||||
|                                     "properties": { | ||||
|                                         "data": { | ||||
|                                             "$ref": "#/definitions/dto.ListUserActionLogResponse" | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             ] | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "/api/v1/users/{id}/notifications/test": { | ||||
|             "post": { | ||||
|                 "security": [ | ||||
| @@ -4166,7 +4075,7 @@ const docTemplate = `{ | ||||
|                     ] | ||||
|                 }, | ||||
|                 "data": { | ||||
|                     "description": "业务数据" | ||||
|                     "description": "业务数据, omitempty表示如果为空则不序列化" | ||||
|                 }, | ||||
|                 "message": { | ||||
|                     "description": "提示信息", | ||||
| @@ -4428,6 +4337,7 @@ const docTemplate = `{ | ||||
|                 }, | ||||
|                 "execute_num": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": 0, | ||||
|                     "example": 10 | ||||
|                 }, | ||||
|                 "execution_type": { | ||||
| @@ -6316,6 +6226,7 @@ const docTemplate = `{ | ||||
|                 }, | ||||
|                 "execute_num": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": 0, | ||||
|                     "example": 10 | ||||
|                 }, | ||||
|                 "execution_type": { | ||||
| @@ -6930,7 +6841,6 @@ const docTemplate = `{ | ||||
|             "type": "integer", | ||||
|             "format": "int32", | ||||
|             "enum": [ | ||||
|                 7, | ||||
|                 -1, | ||||
|                 0, | ||||
|                 1, | ||||
| @@ -6940,10 +6850,10 @@ const docTemplate = `{ | ||||
|                 5, | ||||
|                 -1, | ||||
|                 5, | ||||
|                 6 | ||||
|                 6, | ||||
|                 7 | ||||
|             ], | ||||
|             "x-enum-varnames": [ | ||||
|                 "_numLevels", | ||||
|                 "DebugLevel", | ||||
|                 "InfoLevel", | ||||
|                 "WarnLevel", | ||||
| @@ -6953,7 +6863,8 @@ const docTemplate = `{ | ||||
|                 "FatalLevel", | ||||
|                 "_minLevel", | ||||
|                 "_maxLevel", | ||||
|                 "InvalidLevel" | ||||
|                 "InvalidLevel", | ||||
|                 "_numLevels" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|   | ||||
| @@ -748,17 +748,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -773,12 +773,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "boolean", | ||||
|                         "name": "received_success", | ||||
|                         "name": "receivedSuccess", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -822,22 +822,22 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "feed_formula_id", | ||||
|                         "name": "feedFormulaID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -852,12 +852,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -901,22 +901,22 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "medication_id", | ||||
|                         "name": "medicationID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -931,7 +931,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -941,7 +941,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -985,12 +985,11 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "enum": [ | ||||
|                             7, | ||||
|                             -1, | ||||
|                             0, | ||||
|                             1, | ||||
| @@ -1000,12 +999,12 @@ | ||||
|                             5, | ||||
|                             -1, | ||||
|                             5, | ||||
|                             6 | ||||
|                             6, | ||||
|                             7 | ||||
|                         ], | ||||
|                         "type": "integer", | ||||
|                         "format": "int32", | ||||
|                         "x-enum-varnames": [ | ||||
|                             "_numLevels", | ||||
|                             "DebugLevel", | ||||
|                             "InfoLevel", | ||||
|                             "WarnLevel", | ||||
| @@ -1015,7 +1014,8 @@ | ||||
|                             "FatalLevel", | ||||
|                             "_minLevel", | ||||
|                             "_maxLevel", | ||||
|                             "InvalidLevel" | ||||
|                             "InvalidLevel", | ||||
|                             "_numLevels" | ||||
|                         ], | ||||
|                         "name": "level", | ||||
|                         "in": "query" | ||||
| @@ -1034,12 +1034,12 @@ | ||||
|                             "NotifierTypeLark", | ||||
|                             "NotifierTypeLog" | ||||
|                         ], | ||||
|                         "name": "notifier_type", | ||||
|                         "name": "notifierType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1054,7 +1054,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1084,7 +1084,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "name": "userID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1128,17 +1128,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1153,7 +1153,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1202,22 +1202,22 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "change_type", | ||||
|                         "name": "changeType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1232,12 +1232,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1281,17 +1281,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1306,12 +1306,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1365,17 +1365,17 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1390,12 +1390,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1439,17 +1439,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1464,12 +1464,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1479,12 +1479,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "treatment_location", | ||||
|                         "name": "treatmentLocation", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1528,22 +1528,22 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "correlation_id", | ||||
|                         "name": "correlationID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1558,22 +1558,22 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "transfer_type", | ||||
|                         "name": "transferType", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1617,12 +1617,12 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1637,12 +1637,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "plan_id", | ||||
|                         "name": "planID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1691,12 +1691,12 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1711,12 +1711,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "raw_material_id", | ||||
|                         "name": "rawMaterialID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1765,12 +1765,12 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1785,22 +1785,22 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "raw_material_id", | ||||
|                         "name": "rawMaterialID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "source_id", | ||||
|                         "name": "sourceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "source_type", | ||||
|                         "name": "sourceType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1844,17 +1844,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "device_id", | ||||
|                         "name": "deviceID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1869,12 +1869,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "sensor_type", | ||||
|                         "name": "sensorType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1918,12 +1918,12 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1938,12 +1938,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "plan_execution_log_id", | ||||
|                         "name": "planExecutionLogID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -1953,7 +1953,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "task_id", | ||||
|                         "name": "taskID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -1997,17 +1997,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "action_type", | ||||
|                         "name": "actionType", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2022,7 +2022,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2032,7 +2032,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "name": "userID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2081,12 +2081,12 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2101,12 +2101,12 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pig_batch_id", | ||||
|                         "name": "pigBatchID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -2150,17 +2150,17 @@ | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "name": "endTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "operator_id", | ||||
|                         "name": "operatorID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "name": "orderBy", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
| @@ -2175,17 +2175,17 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pen_id", | ||||
|                         "name": "penID", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "name": "startTime", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "weighing_batch_id", | ||||
|                         "name": "weighingBatchID", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
| @@ -3407,7 +3407,7 @@ | ||||
|                         "BearerAuth": [] | ||||
|                     } | ||||
|                 ], | ||||
|                 "description": "创建一个新的猪舍", | ||||
|                 "description": "根据提供的信息创建一个新猪舍", | ||||
|                 "consumes": [ | ||||
|                     "application/json" | ||||
|                 ], | ||||
| @@ -3995,97 +3995,6 @@ | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "/api/v1/users/{id}/history": { | ||||
|             "get": { | ||||
|                 "security": [ | ||||
|                     { | ||||
|                         "BearerAuth": [] | ||||
|                     } | ||||
|                 ], | ||||
|                 "description": "根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。", | ||||
|                 "produces": [ | ||||
|                     "application/json" | ||||
|                 ], | ||||
|                 "tags": [ | ||||
|                     "用户管理" | ||||
|                 ], | ||||
|                 "summary": "获取指定用户的操作历史", | ||||
|                 "parameters": [ | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "description": "用户ID", | ||||
|                         "name": "id", | ||||
|                         "in": "path", | ||||
|                         "required": true | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "action_type", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "end_time", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "order_by", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "page", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "pageSize", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "start_time", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "status", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "integer", | ||||
|                         "name": "user_id", | ||||
|                         "in": "query" | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "string", | ||||
|                         "name": "username", | ||||
|                         "in": "query" | ||||
|                     } | ||||
|                 ], | ||||
|                 "responses": { | ||||
|                     "200": { | ||||
|                         "description": "业务码为200代表成功获取", | ||||
|                         "schema": { | ||||
|                             "allOf": [ | ||||
|                                 { | ||||
|                                     "$ref": "#/definitions/controller.Response" | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     "type": "object", | ||||
|                                     "properties": { | ||||
|                                         "data": { | ||||
|                                             "$ref": "#/definitions/dto.ListUserActionLogResponse" | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             ] | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "/api/v1/users/{id}/notifications/test": { | ||||
|             "post": { | ||||
|                 "security": [ | ||||
| @@ -4158,7 +4067,7 @@ | ||||
|                     ] | ||||
|                 }, | ||||
|                 "data": { | ||||
|                     "description": "业务数据" | ||||
|                     "description": "业务数据, omitempty表示如果为空则不序列化" | ||||
|                 }, | ||||
|                 "message": { | ||||
|                     "description": "提示信息", | ||||
| @@ -4420,6 +4329,7 @@ | ||||
|                 }, | ||||
|                 "execute_num": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": 0, | ||||
|                     "example": 10 | ||||
|                 }, | ||||
|                 "execution_type": { | ||||
| @@ -6308,6 +6218,7 @@ | ||||
|                 }, | ||||
|                 "execute_num": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": 0, | ||||
|                     "example": 10 | ||||
|                 }, | ||||
|                 "execution_type": { | ||||
| @@ -6922,7 +6833,6 @@ | ||||
|             "type": "integer", | ||||
|             "format": "int32", | ||||
|             "enum": [ | ||||
|                 7, | ||||
|                 -1, | ||||
|                 0, | ||||
|                 1, | ||||
| @@ -6932,10 +6842,10 @@ | ||||
|                 5, | ||||
|                 -1, | ||||
|                 5, | ||||
|                 6 | ||||
|                 6, | ||||
|                 7 | ||||
|             ], | ||||
|             "x-enum-varnames": [ | ||||
|                 "_numLevels", | ||||
|                 "DebugLevel", | ||||
|                 "InfoLevel", | ||||
|                 "WarnLevel", | ||||
| @@ -6945,7 +6855,8 @@ | ||||
|                 "FatalLevel", | ||||
|                 "_minLevel", | ||||
|                 "_maxLevel", | ||||
|                 "InvalidLevel" | ||||
|                 "InvalidLevel", | ||||
|                 "_numLevels" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ definitions: | ||||
|         - $ref: '#/definitions/controller.ResponseCode' | ||||
|         description: 业务状态码 | ||||
|       data: | ||||
|         description: 业务数据 | ||||
|         description: 业务数据, omitempty表示如果为空则不序列化 | ||||
|       message: | ||||
|         description: 提示信息 | ||||
|         type: string | ||||
| @@ -196,6 +196,7 @@ definitions: | ||||
|         type: string | ||||
|       execute_num: | ||||
|         example: 10 | ||||
|         minimum: 0 | ||||
|         type: integer | ||||
|       execution_type: | ||||
|         allOf: | ||||
| @@ -1459,6 +1460,7 @@ definitions: | ||||
|         type: string | ||||
|       execute_num: | ||||
|         example: 10 | ||||
|         minimum: 0 | ||||
|         type: integer | ||||
|       execution_type: | ||||
|         allOf: | ||||
| @@ -1935,7 +1937,6 @@ definitions: | ||||
|     - PlanTypeFilterSystem | ||||
|   zapcore.Level: | ||||
|     enum: | ||||
|     - 7 | ||||
|     - -1 | ||||
|     - 0 | ||||
|     - 1 | ||||
| @@ -1946,10 +1947,10 @@ definitions: | ||||
|     - -1 | ||||
|     - 5 | ||||
|     - 6 | ||||
|     - 7 | ||||
|     format: int32 | ||||
|     type: integer | ||||
|     x-enum-varnames: | ||||
|     - _numLevels | ||||
|     - DebugLevel | ||||
|     - InfoLevel | ||||
|     - WarnLevel | ||||
| @@ -1960,6 +1961,7 @@ definitions: | ||||
|     - _minLevel | ||||
|     - _maxLevel | ||||
|     - InvalidLevel | ||||
|     - _numLevels | ||||
| info: | ||||
|   contact: | ||||
|     email: divano@example.com | ||||
| @@ -2393,13 +2395,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取设备命令日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: device_id | ||||
|         name: deviceID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2408,10 +2410,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: received_success | ||||
|         name: receivedSuccess | ||||
|         type: boolean | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2435,16 +2437,16 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取饲料使用记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: feed_formula_id | ||||
|         name: feedFormulaID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2453,10 +2455,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pen_id | ||||
|         name: penID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2480,16 +2482,16 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取用药记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: medication_id | ||||
|         name: medicationID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2498,13 +2500,13 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: reason | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2528,10 +2530,9 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取通知列表 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - enum: | ||||
|         - 7 | ||||
|         - -1 | ||||
|         - 0 | ||||
|         - 1 | ||||
| @@ -2542,12 +2543,12 @@ paths: | ||||
|         - -1 | ||||
|         - 5 | ||||
|         - 6 | ||||
|         - 7 | ||||
|         format: int32 | ||||
|         in: query | ||||
|         name: level | ||||
|         type: integer | ||||
|         x-enum-varnames: | ||||
|         - _numLevels | ||||
|         - DebugLevel | ||||
|         - InfoLevel | ||||
|         - WarnLevel | ||||
| @@ -2558,13 +2559,14 @@ paths: | ||||
|         - _minLevel | ||||
|         - _maxLevel | ||||
|         - InvalidLevel | ||||
|         - _numLevels | ||||
|       - enum: | ||||
|         - 邮件 | ||||
|         - 企业微信 | ||||
|         - 飞书 | ||||
|         - 日志 | ||||
|         in: query | ||||
|         name: notifier_type | ||||
|         name: notifierType | ||||
|         type: string | ||||
|         x-enum-varnames: | ||||
|         - NotifierTypeSMTP | ||||
| @@ -2572,7 +2574,7 @@ paths: | ||||
|         - NotifierTypeLark | ||||
|         - NotifierTypeLog | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2581,7 +2583,7 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - enum: | ||||
|         - 发送成功 | ||||
| @@ -2603,7 +2605,7 @@ paths: | ||||
|         - NotificationStatusFailed | ||||
|         - NotificationStatusSkipped | ||||
|       - in: query | ||||
|         name: user_id | ||||
|         name: userID | ||||
|         type: integer | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2627,13 +2629,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取待采集请求 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: device_id | ||||
|         name: deviceID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2642,7 +2644,7 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: status | ||||
| @@ -2669,16 +2671,16 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取猪批次日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: change_type | ||||
|         name: changeType | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2687,10 +2689,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2714,13 +2716,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取猪只采购记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2729,10 +2731,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: supplier | ||||
| @@ -2762,13 +2764,13 @@ paths: | ||||
|         name: buyer | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2777,10 +2779,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2804,13 +2806,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取病猪日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2819,19 +2821,19 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pen_id | ||||
|         name: penID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: reason | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: treatment_location | ||||
|         name: treatmentLocation | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2855,16 +2857,16 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取猪只迁移日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: correlation_id | ||||
|         name: correlationID | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2873,16 +2875,16 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pen_id | ||||
|         name: penID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: transfer_type | ||||
|         name: transferType | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -2906,10 +2908,10 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取计划执行日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2918,10 +2920,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: plan_id | ||||
|         name: planID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: status | ||||
| @@ -2948,10 +2950,10 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取原料采购记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -2960,10 +2962,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: raw_material_id | ||||
|         name: rawMaterialID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: supplier | ||||
| @@ -2990,10 +2992,10 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取原料库存日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3002,16 +3004,16 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: raw_material_id | ||||
|         name: rawMaterialID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: source_id | ||||
|         name: sourceID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: source_type | ||||
|         name: sourceType | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -3035,13 +3037,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取传感器数据 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: device_id | ||||
|         name: deviceID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3050,10 +3052,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: sensor_type | ||||
|         name: sensorType | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -3077,10 +3079,10 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取任务执行日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3089,16 +3091,16 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: plan_execution_log_id | ||||
|         name: planExecutionLogID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: status | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: task_id | ||||
|         name: taskID | ||||
|         type: integer | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -3122,13 +3124,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取用户操作日志 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: action_type | ||||
|         name: actionType | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3137,13 +3139,13 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: status | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: user_id | ||||
|         name: userID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: username | ||||
| @@ -3170,10 +3172,10 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取批次称重记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3182,10 +3184,10 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pig_batch_id | ||||
|         name: pigBatchID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -3209,13 +3211,13 @@ paths: | ||||
|       description: 根据提供的过滤条件,分页获取单次称重记录 | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         name: endTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: operator_id | ||||
|         name: operatorID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         name: orderBy | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
| @@ -3224,13 +3226,13 @@ paths: | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pen_id | ||||
|         name: penID | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         name: startTime | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: weighing_batch_id | ||||
|         name: weighingBatchID | ||||
|         type: integer | ||||
|       produces: | ||||
|       - application/json | ||||
| @@ -3974,7 +3976,7 @@ paths: | ||||
|     post: | ||||
|       consumes: | ||||
|       - application/json | ||||
|       description: 创建一个新的猪舍 | ||||
|       description: 根据提供的信息创建一个新猪舍 | ||||
|       parameters: | ||||
|       - description: 猪舍信息 | ||||
|         in: body | ||||
| @@ -4295,59 +4297,6 @@ paths: | ||||
|       summary: 创建新用户 | ||||
|       tags: | ||||
|       - 用户管理 | ||||
|   /api/v1/users/{id}/history: | ||||
|     get: | ||||
|       description: 根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。 | ||||
|       parameters: | ||||
|       - description: 用户ID | ||||
|         in: path | ||||
|         name: id | ||||
|         required: true | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: action_type | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: end_time | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: order_by | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: page | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: pageSize | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: start_time | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: status | ||||
|         type: string | ||||
|       - in: query | ||||
|         name: user_id | ||||
|         type: integer | ||||
|       - in: query | ||||
|         name: username | ||||
|         type: string | ||||
|       produces: | ||||
|       - application/json | ||||
|       responses: | ||||
|         "200": | ||||
|           description: 业务码为200代表成功获取 | ||||
|           schema: | ||||
|             allOf: | ||||
|             - $ref: '#/definitions/controller.Response' | ||||
|             - properties: | ||||
|                 data: | ||||
|                   $ref: '#/definitions/dto.ListUserActionLogResponse' | ||||
|               type: object | ||||
|       security: | ||||
|       - BearerAuth: [] | ||||
|       summary: 获取指定用户的操作历史 | ||||
|       tags: | ||||
|       - 用户管理 | ||||
|   /api/v1/users/{id}/notifications/test: | ||||
|     post: | ||||
|       consumes: | ||||
|   | ||||
| @@ -27,8 +27,6 @@ import ( | ||||
| 	"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" | ||||
| 	domain_device "git.huangwc.com/pig/pig-farm-controller/internal/domain/device" | ||||
| 	domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/domain/token" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config" | ||||
| @@ -63,19 +61,16 @@ type API struct { | ||||
| func NewAPI(cfg config.ServerConfig, | ||||
| 	logger *logs.Logger, | ||||
| 	userRepo repository.UserRepository, | ||||
| 	deviceRepository repository.DeviceRepository, | ||||
| 	areaControllerRepository repository.AreaControllerRepository, | ||||
| 	deviceTemplateRepository repository.DeviceTemplateRepository, | ||||
| 	planRepository repository.PlanRepository, | ||||
| 	pigFarmService service.PigFarmService, | ||||
| 	pigBatchService service.PigBatchService, | ||||
| 	monitorService service.MonitorService, | ||||
| 	deviceService service.DeviceService, | ||||
| 	planService service.PlanService, | ||||
| 	userService service.UserService, | ||||
| 	tokenService token.Service, | ||||
| 	auditService audit.Service, | ||||
| 	notifyService domain_notify.Service, | ||||
| 	deviceService domain_device.Service, | ||||
| 	listenHandler webhook.ListenHandler, | ||||
| 	analysisTaskManager *scheduler.AnalysisPlanTaskManager) *API { | ||||
| ) *API { | ||||
| 	// 使用 echo.New() 创建一个 Echo 引擎实例 | ||||
| 	e := echo.New() | ||||
|  | ||||
| @@ -96,11 +91,11 @@ func NewAPI(cfg config.ServerConfig, | ||||
| 		config:        cfg, | ||||
| 		listenHandler: listenHandler, | ||||
| 		// 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 | ||||
| 		userController: user.NewController(userRepo, monitorService, logger, tokenService, notifyService), | ||||
| 		userController: user.NewController(userService, logger), | ||||
| 		// 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 | ||||
| 		deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, deviceService, logger), | ||||
| 		deviceController: device.NewController(deviceService, logger), | ||||
| 		// 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 | ||||
| 		planController: plan.NewController(logger, planRepository, analysisTaskManager), | ||||
| 		planController: plan.NewController(logger, planService), | ||||
| 		// 在 NewAPI 中初始化猪场管理控制器 | ||||
| 		pigFarmController: management.NewPigFarmController(logger, pigFarmService), | ||||
| 		// 在 NewAPI 中初始化猪群控制器 | ||||
|   | ||||
| @@ -57,7 +57,6 @@ func (a *API) setupRoutes() { | ||||
| 		// 用户相关路由组 | ||||
| 		userGroup := authGroup.Group("/users") | ||||
| 		{ | ||||
| 			userGroup.GET("/:id/history", a.userController.ListUserHistory) // 获取用户操作历史 | ||||
| 			userGroup.POST("/:id/notifications/test", a.userController.SendTestNotification) | ||||
| 		} | ||||
| 		a.logger.Debug("用户相关接口注册成功 (需要认证和审计)") | ||||
|   | ||||
| @@ -1,44 +1,30 @@ | ||||
| package device | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"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/device" | ||||
| 	"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" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // Controller 设备控制器,封装了所有与设备和区域主控相关的业务逻辑 | ||||
| type Controller struct { | ||||
| 	deviceRepo         repository.DeviceRepository | ||||
| 	areaControllerRepo repository.AreaControllerRepository | ||||
| 	deviceTemplateRepo repository.DeviceTemplateRepository | ||||
| 	deviceService      device.Service | ||||
| 	logger             *logs.Logger | ||||
| 	deviceService service.DeviceService | ||||
| 	logger        *logs.Logger | ||||
| } | ||||
|  | ||||
| // NewController 创建一个新的设备控制器实例 | ||||
| func NewController( | ||||
| 	deviceRepo repository.DeviceRepository, | ||||
| 	areaControllerRepo repository.AreaControllerRepository, | ||||
| 	deviceTemplateRepo repository.DeviceTemplateRepository, | ||||
| 	deviceService device.Service, | ||||
| 	deviceService service.DeviceService, | ||||
| 	logger *logs.Logger, | ||||
| ) *Controller { | ||||
| 	return &Controller{ | ||||
| 		deviceRepo:         deviceRepo, | ||||
| 		areaControllerRepo: areaControllerRepo, | ||||
| 		deviceTemplateRepo: deviceTemplateRepo, | ||||
| 		deviceService:      deviceService, | ||||
| 		logger:             logger, | ||||
| 		deviceService: deviceService, | ||||
| 		logger:        logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -62,43 +48,13 @@ func (c *Controller) CreateDevice(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	resp, err := c.deviceService.CreateDevice(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties) | ||||
| 		c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "服务层创建失败", req) | ||||
| 	} | ||||
|  | ||||
| 	device := &models.Device{ | ||||
| 		Name:             req.Name, | ||||
| 		DeviceTemplateID: req.DeviceTemplateID, | ||||
| 		AreaControllerID: req.AreaControllerID, | ||||
| 		Location:         req.Location, | ||||
| 		Properties:       propertiesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := device.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", device) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceRepo.Create(device); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备失败: "+err.Error(), actionType, "数据库创建失败", device) | ||||
| 	} | ||||
|  | ||||
| 	createdDevice, err := c.deviceRepo.FindByID(device.ID) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 重新加载创建的设备失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但重新加载设备失败", actionType, "重新加载设备失败", device) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceResponse(createdDevice) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, createdDevice) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败", actionType, "响应序列化失败", createdDevice) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, device.ID) | ||||
| 	c.logger.Infof("%s: 设备创建成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备创建成功", resp, actionType, "设备创建成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -115,32 +71,17 @@ func (c *Controller) GetDevice(ctx echo.Context) error { | ||||
| 	const actionType = "获取设备" | ||||
| 	deviceID := ctx.Param("id") | ||||
|  | ||||
| 	if deviceID == "" { | ||||
| 		c.logger.Errorf("%s: 设备ID为空", actionType) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备ID不能为空", actionType, "设备ID为空", nil) | ||||
| 	} | ||||
|  | ||||
| 	device, err := c.deviceRepo.FindByIDString(deviceID) | ||||
| 	resp, err := c.deviceService.GetDevice(deviceID) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) | ||||
| 		} | ||||
| 		if strings.Contains(err.Error(), "无效的设备ID格式") { | ||||
| 			c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "数据库查询失败", deviceID) | ||||
| 		c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: "+err.Error(), actionType, "服务层获取失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceResponse(device) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, device) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误", actionType, "响应序列化失败", device) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, device.ID) | ||||
| 	c.logger.Infof("%s: 获取设备信息成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备信息成功", resp, actionType, "获取设备信息成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -154,19 +95,13 @@ func (c *Controller) GetDevice(ctx echo.Context) error { | ||||
| // @Router       /api/v1/devices [get] | ||||
| func (c *Controller) ListDevices(ctx echo.Context) error { | ||||
| 	const actionType = "获取设备列表" | ||||
| 	devices, err := c.deviceRepo.ListAll() | ||||
| 	resp, err := c.deviceService.ListDevices() | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "数据库查询失败", nil) | ||||
| 		c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewListDeviceResponse(devices) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Devices: %+v", actionType, err, devices) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误", actionType, "响应序列化失败", devices) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(devices)) | ||||
| 	c.logger.Infof("%s: 获取设备列表成功, 数量: %d", actionType, len(resp)) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备列表成功", resp, actionType, "获取设备列表成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -185,61 +120,23 @@ func (c *Controller) UpdateDevice(ctx echo.Context) error { | ||||
| 	const actionType = "更新设备" | ||||
| 	deviceID := ctx.Param("id") | ||||
|  | ||||
| 	existingDevice, err := c.deviceRepo.FindByIDString(deviceID) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) | ||||
| 		} | ||||
| 		if strings.Contains(err.Error(), "无效的设备ID格式") { | ||||
| 			c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库查询失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	var req dto.UpdateDeviceRequest | ||||
| 	if err := ctx.Bind(&req); err != nil { | ||||
| 		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	resp, err := c.deviceService.UpdateDevice(deviceID, &req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties) | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "服务层更新失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	existingDevice.Name = req.Name | ||||
| 	existingDevice.DeviceTemplateID = req.DeviceTemplateID | ||||
| 	existingDevice.AreaControllerID = req.AreaControllerID | ||||
| 	existingDevice.Location = req.Location | ||||
| 	existingDevice.Properties = propertiesJSON | ||||
|  | ||||
| 	if err := existingDevice.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 设备属性自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备属性不符合要求: "+err.Error(), actionType, "设备属性自检失败", existingDevice) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceRepo.Update(existingDevice); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库更新失败: %v, Device: %+v", actionType, err, existingDevice) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备失败: "+err.Error(), actionType, "数据库更新失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	updatedDevice, err := c.deviceRepo.FindByID(existingDevice.ID) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 重新加载更新的设备失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但重新加载设备失败", actionType, "重新加载设备失败", existingDevice) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceResponse(updatedDevice) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Device: %+v", actionType, err, updatedDevice) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败", actionType, "响应序列化失败", updatedDevice) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, existingDevice.ID) | ||||
| 	c.logger.Infof("%s: 设备更新成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备更新成功", resp, actionType, "设备更新成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -256,28 +153,16 @@ func (c *Controller) DeleteDevice(ctx echo.Context) error { | ||||
| 	const actionType = "删除设备" | ||||
| 	deviceID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(deviceID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备ID格式", actionType, "设备ID格式错误", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	_, err = c.deviceRepo.FindByIDString(deviceID) | ||||
| 	if err != nil { | ||||
| 	if err := c.deviceService.DeleteDevice(deviceID); err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 查找设备失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: 查找设备时发生内部错误", actionType, "数据库查询失败", deviceID) | ||||
| 		c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "服务层删除失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceRepo.Delete(uint(idUint)); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备失败: "+err.Error(), actionType, "数据库删除失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备删除成功, ID: %d", actionType, idUint) | ||||
| 	c.logger.Infof("%s: 设备删除成功, ID: %s", actionType, deviceID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备删除成功", nil, actionType, "设备删除成功", deviceID) | ||||
| } | ||||
|  | ||||
| @@ -302,45 +187,16 @@ func (c *Controller) ManualControl(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	dev, err := c.deviceRepo.FindByIDString(deviceID) | ||||
| 	if err != nil { | ||||
| 	if err := c.deviceService.ManualControl(deviceID, &req); err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备不存在, ID: %s", actionType, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备未找到", actionType, "设备不存在", deviceID) | ||||
| 		} | ||||
| 		if strings.Contains(err.Error(), "无效的设备ID格式") { | ||||
| 			c.logger.Errorf("%s: 设备ID格式错误: %v, ID: %s", actionType, err, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备ID格式错误", deviceID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "数据库查询失败", deviceID) | ||||
| 		c.logger.Errorf("%s: 服务层手动控制失败: %v, ID: %s", actionType, err, deviceID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "手动控制失败: "+err.Error(), actionType, "服务层手动控制失败", deviceID) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 接收到指令, 设备ID: %s, 动作: %s", actionType, deviceID, req.Action) | ||||
| 	if req.Action == nil { | ||||
| 		err = c.deviceService.Collect(dev.AreaControllerID, []*models.Device{dev}) | ||||
| 		if err != nil { | ||||
| 			c.logger.Errorf("%s: 获取设备状态失败: %v, 设备ID: %s", actionType, err, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备状态失败: "+err.Error(), actionType, "获取设备状态失败", deviceID) | ||||
| 		} | ||||
| 	} else { | ||||
| 		action := device.DeviceActionStart | ||||
| 		switch *req.Action { | ||||
| 		case "off": | ||||
| 			action = device.DeviceActionStop | ||||
| 		case "on": | ||||
| 		default: | ||||
| 			c.logger.Errorf("%s: 无效的动作: %s, 设备ID: %s", actionType, *req.Action, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的动作: "+*req.Action, actionType, "无效的动作", req.Action) | ||||
| 		} | ||||
| 		err = c.deviceService.Switch(dev, action) | ||||
| 		if err != nil { | ||||
| 			c.logger.Errorf("%s: 设备控制失败: %v, 设备ID: %s", actionType, err, deviceID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备控制失败: "+err.Error(), actionType, "设备控制失败", deviceID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", map[string]interface{}{"device_id": deviceID}, actionType, "指令发送成功", map[string]interface{}{"device_id": deviceID, "action": req.Action}) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "指令已发送", nil, actionType, "指令发送成功", nil) | ||||
| } | ||||
|  | ||||
| // --- Controller Methods: Area Controllers --- | ||||
| @@ -363,36 +219,13 @@ func (c *Controller) CreateAreaController(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	resp, err := c.deviceService.CreateAreaController(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties) | ||||
| 		c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "服务层创建失败", req) | ||||
| 	} | ||||
|  | ||||
| 	ac := &models.AreaController{ | ||||
| 		Name:       req.Name, | ||||
| 		NetworkID:  req.NetworkID, | ||||
| 		Location:   req.Location, | ||||
| 		Properties: propertiesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := ac.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", ac) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.areaControllerRepo.Create(ac); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建区域主控失败: "+err.Error(), actionType, "数据库创建失败", ac) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewAreaControllerResponse(ac) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控创建成功,但响应生成失败", actionType, "响应序列化失败", ac) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, ac.ID) | ||||
| 	c.logger.Infof("%s: 区域主控创建成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "区域主控创建成功", resp, actionType, "区域主控创建成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -409,29 +242,17 @@ func (c *Controller) GetAreaController(ctx echo.Context) error { | ||||
| 	const actionType = "获取区域主控" | ||||
| 	acID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(acID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID) | ||||
| 	} | ||||
|  | ||||
| 	ac, err := c.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	resp, err := c.deviceService.GetAreaController(acID) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "数据库查询失败", acID) | ||||
| 		c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: "+err.Error(), actionType, "服务层获取失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewAreaControllerResponse(ac) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, ac) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控信息失败: 内部数据格式错误", actionType, "响应序列化失败", ac) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, ac.ID) | ||||
| 	c.logger.Infof("%s: 获取区域主控信息成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控信息成功", resp, actionType, "获取区域主控信息成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -445,19 +266,13 @@ func (c *Controller) GetAreaController(ctx echo.Context) error { | ||||
| // @Router       /api/v1/area-controllers [get] | ||||
| func (c *Controller) ListAreaControllers(ctx echo.Context) error { | ||||
| 	const actionType = "获取区域主控列表" | ||||
| 	acs, err := c.areaControllerRepo.ListAll() | ||||
| 	resp, err := c.deviceService.ListAreaControllers() | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "数据库查询失败", nil) | ||||
| 		c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewListAreaControllerResponse(acs) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, AreaControllers: %+v", actionType, err, acs) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取区域主控列表失败: 内部数据格式错误", actionType, "响应序列化失败", acs) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(acs)) | ||||
| 	c.logger.Infof("%s: 获取区域主控列表成功, 数量: %d", actionType, len(resp)) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取区域主控列表成功", resp, actionType, "获取区域主控列表成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -476,56 +291,23 @@ func (c *Controller) UpdateAreaController(ctx echo.Context) error { | ||||
| 	const actionType = "更新区域主控" | ||||
| 	acID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(acID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID) | ||||
| 	} | ||||
|  | ||||
| 	existingAC, err := c.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库查询失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	var req dto.UpdateAreaControllerRequest | ||||
| 	if err := ctx.Bind(&req); err != nil { | ||||
| 		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	resp, err := c.deviceService.UpdateAreaController(acID, &req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化属性失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "属性字段格式错误", actionType, "属性序列化失败", req.Properties) | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "服务层更新失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	existingAC.Name = req.Name | ||||
| 	existingAC.NetworkID = req.NetworkID | ||||
| 	existingAC.Location = req.Location | ||||
| 	existingAC.Properties = propertiesJSON | ||||
|  | ||||
| 	if err := existingAC.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 区域主控自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "区域主控参数不符合要求: "+err.Error(), actionType, "区域主控自检失败", existingAC) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.areaControllerRepo.Update(existingAC); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库更新失败: %v, AreaController: %+v", actionType, err, existingAC) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新区域主控失败: "+err.Error(), actionType, "数据库更新失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewAreaControllerResponse(existingAC) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, AreaController: %+v", actionType, err, existingAC) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "区域主控更新成功,但响应生成失败", actionType, "响应序列化失败", existingAC) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, existingAC.ID) | ||||
| 	c.logger.Infof("%s: 区域主控更新成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控更新成功", resp, actionType, "区域主控更新成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -542,28 +324,16 @@ func (c *Controller) DeleteAreaController(ctx echo.Context) error { | ||||
| 	const actionType = "删除区域主控" | ||||
| 	acID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(acID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 区域主控ID格式错误: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的区域主控ID格式", actionType, "ID格式错误", acID) | ||||
| 	} | ||||
|  | ||||
| 	_, err = c.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 	if err := c.deviceService.DeleteAreaController(acID); err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 区域主控不存在, ID: %s", actionType, acID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "区域主控未找到", actionType, "区域主控不存在", acID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 查找区域主控失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: 查找时发生内部错误", actionType, "数据库查询失败", acID) | ||||
| 		c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, acID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "服务层删除失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.areaControllerRepo.Delete(uint(idUint)); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除区域主控失败: "+err.Error(), actionType, "数据库删除失败", acID) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 区域主控删除成功, ID: %d", actionType, idUint) | ||||
| 	c.logger.Infof("%s: 区域主控删除成功, ID: %s", actionType, acID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID) | ||||
| } | ||||
|  | ||||
| @@ -587,44 +357,13 @@ func (c *Controller) CreateDeviceTemplate(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	commandsJSON, err := json.Marshal(req.Commands) | ||||
| 	resp, err := c.deviceService.CreateDeviceTemplate(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands) | ||||
| 		c.logger.Errorf("%s: 服务层创建失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "服务层创建失败", req) | ||||
| 	} | ||||
|  | ||||
| 	valuesJSON, err := json.Marshal(req.Values) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values) | ||||
| 	} | ||||
|  | ||||
| 	deviceTemplate := &models.DeviceTemplate{ | ||||
| 		Name:         req.Name, | ||||
| 		Manufacturer: req.Manufacturer, | ||||
| 		Description:  req.Description, | ||||
| 		Category:     req.Category, | ||||
| 		Commands:     commandsJSON, | ||||
| 		Values:       valuesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := deviceTemplate.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", deviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceTemplateRepo.Create(deviceTemplate); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "数据库创建失败", deviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceTemplateResponse(deviceTemplate) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, deviceTemplate.ID) | ||||
| 	c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -641,29 +380,17 @@ func (c *Controller) GetDeviceTemplate(ctx echo.Context) error { | ||||
| 	const actionType = "获取设备模板" | ||||
| 	dtID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(dtID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) | ||||
| 	} | ||||
|  | ||||
| 	deviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	resp, err := c.deviceService.GetDeviceTemplate(dtID) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "数据库查询失败", dtID) | ||||
| 		c.logger.Errorf("%s: 服务层获取失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "服务层获取失败", dtID) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceTemplateResponse(deviceTemplate) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, deviceTemplate.ID) | ||||
| 	c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -677,19 +404,13 @@ func (c *Controller) GetDeviceTemplate(ctx echo.Context) error { | ||||
| // @Router       /api/v1/device-templates [get] | ||||
| func (c *Controller) ListDeviceTemplates(ctx echo.Context) error { | ||||
| 	const actionType = "获取设备模板列表" | ||||
| 	deviceTemplates, err := c.deviceTemplateRepo.ListAll() | ||||
| 	resp, err := c.deviceService.ListDeviceTemplates() | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "数据库查询失败", nil) | ||||
| 		c.logger.Errorf("%s: 服务层获取列表失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "服务层获取列表失败", nil) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewListDeviceTemplateResponse(deviceTemplates) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(deviceTemplates)) | ||||
| 	c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(resp)) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -708,64 +429,23 @@ func (c *Controller) UpdateDeviceTemplate(ctx echo.Context) error { | ||||
| 	const actionType = "更新设备模板" | ||||
| 	dtID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(dtID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) | ||||
| 	} | ||||
|  | ||||
| 	existingDeviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库查询失败", dtID) | ||||
| 	} | ||||
|  | ||||
| 	var req dto.UpdateDeviceTemplateRequest | ||||
| 	if err := ctx.Bind(&req); err != nil { | ||||
| 		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	commandsJSON, err := json.Marshal(req.Commands) | ||||
| 	resp, err := c.deviceService.UpdateDeviceTemplate(dtID, &req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands) | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 服务层更新失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "服务层更新失败", dtID) | ||||
| 	} | ||||
|  | ||||
| 	valuesJSON, err := json.Marshal(req.Values) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values) | ||||
| 	} | ||||
|  | ||||
| 	existingDeviceTemplate.Name = req.Name | ||||
| 	existingDeviceTemplate.Manufacturer = req.Manufacturer | ||||
| 	existingDeviceTemplate.Description = req.Description | ||||
| 	existingDeviceTemplate.Category = req.Category | ||||
| 	existingDeviceTemplate.Commands = commandsJSON | ||||
| 	existingDeviceTemplate.Values = valuesJSON | ||||
|  | ||||
| 	if err := existingDeviceTemplate.SelfCheck(); err != nil { | ||||
| 		c.logger.Errorf("%s: 设备模板自检失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", existingDeviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库更新失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库更新失败", dtID) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewDeviceTemplateResponse(existingDeviceTemplate) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate) | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, existingDeviceTemplate.ID) | ||||
| 	c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -782,35 +462,15 @@ func (c *Controller) DeleteDeviceTemplate(ctx echo.Context) error { | ||||
| 	const actionType = "删除设备模板" | ||||
| 	dtID := ctx.Param("id") | ||||
|  | ||||
| 	idUint, err := strconv.ParseUint(dtID, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 设备模板ID格式错误: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) | ||||
| 	} | ||||
|  | ||||
| 	// 在尝试删除之前,先检查设备模板是否存在 | ||||
| 	_, err = c.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 	if err := c.deviceService.DeleteDeviceTemplate(dtID); err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 查找设备模板失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: 查找时发生内部错误", actionType, "数据库查询失败", dtID) | ||||
| 		c.logger.Errorf("%s: 服务层删除失败: %v, ID: %s", actionType, err, dtID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "服务层删除失败", dtID) | ||||
| 	} | ||||
|  | ||||
| 	// 调用仓库层的删除方法,该方法会检查模板是否被使用 | ||||
| 	if err := c.deviceTemplateRepo.Delete(uint(idUint)); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint) | ||||
| 		// 如果错误信息包含“设备模板正在被设备使用,无法删除”,则返回特定的错误码 | ||||
| 		if strings.Contains(err.Error(), "设备模板正在被设备使用,无法删除") { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备模板正在使用", dtID) | ||||
| 		} else { | ||||
| 			// 其他数据库错误 | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "数据库删除失败", dtID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	c.logger.Infof("%s: 设备模板删除成功, ID: %d", actionType, idUint) | ||||
| 	c.logger.Infof("%s: 设备模板删除成功, ID: %s", actionType, dtID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID) | ||||
| } | ||||
|   | ||||
| @@ -53,12 +53,7 @@ func (c *PigFarmController) CreatePigHouse(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪舍失败", action, "业务逻辑失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PigHouseResponse{ | ||||
| 		ID:          house.ID, | ||||
| 		Name:        house.Name, | ||||
| 		Description: house.Description, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", house, action, "创建成功", house) | ||||
| } | ||||
|  | ||||
| // GetPigHouse godoc | ||||
| @@ -86,12 +81,7 @@ func (c *PigFarmController) GetPigHouse(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪舍失败", action, "业务逻辑失败", id) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PigHouseResponse{ | ||||
| 		ID:          house.ID, | ||||
| 		Name:        house.Name, | ||||
| 		Description: house.Description, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", house, action, "获取成功", house) | ||||
| } | ||||
|  | ||||
| // ListPigHouses godoc | ||||
| @@ -110,16 +100,7 @@ func (c *PigFarmController) ListPigHouses(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取列表失败", action, "业务逻辑失败", nil) | ||||
| 	} | ||||
|  | ||||
| 	var resp []dto.PigHouseResponse | ||||
| 	for _, house := range houses { | ||||
| 		resp = append(resp, dto.PigHouseResponse{ | ||||
| 			ID:          house.ID, | ||||
| 			Name:        house.Name, | ||||
| 			Description: house.Description, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", resp, action, "获取成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取成功", houses, action, "获取成功", houses) | ||||
| } | ||||
|  | ||||
| // UpdatePigHouse godoc | ||||
| @@ -154,12 +135,7 @@ func (c *PigFarmController) UpdatePigHouse(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PigHouseResponse{ | ||||
| 		ID:          house.ID, | ||||
| 		Name:        house.Name, | ||||
| 		Description: house.Description, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", house, action, "更新成功", house) | ||||
| } | ||||
|  | ||||
| // DeletePigHouse godoc | ||||
| @@ -222,14 +198,7 @@ func (c *PigFarmController) CreatePen(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建猪栏失败", action, "业务逻辑失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PenResponse{ | ||||
| 		ID:        pen.ID, | ||||
| 		PenNumber: pen.PenNumber, | ||||
| 		HouseID:   pen.HouseID, | ||||
| 		Capacity:  pen.Capacity, | ||||
| 		Status:    pen.Status, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", resp, action, "创建成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "创建成功", pen, action, "创建成功", pen) | ||||
| } | ||||
|  | ||||
| // GetPen godoc | ||||
| @@ -312,15 +281,7 @@ func (c *PigFarmController) UpdatePen(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新失败", action, "业务逻辑失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PenResponse{ | ||||
| 		ID:         pen.ID, | ||||
| 		PenNumber:  pen.PenNumber, | ||||
| 		HouseID:    pen.HouseID, | ||||
| 		Capacity:   pen.Capacity, | ||||
| 		Status:     pen.Status, | ||||
| 		PigBatchID: pen.PigBatchID, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", pen, action, "更新成功", pen) | ||||
| } | ||||
|  | ||||
| // DeletePen godoc | ||||
| @@ -388,13 +349,5 @@ func (c *PigFarmController) UpdatePenStatus(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新猪栏状态失败", action, err.Error(), id) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.PenResponse{ | ||||
| 		ID:         pen.ID, | ||||
| 		PenNumber:  pen.PenNumber, | ||||
| 		HouseID:    pen.HouseID, | ||||
| 		Capacity:   pen.Capacity, | ||||
| 		Status:     pen.Status, | ||||
| 		PigBatchID: pen.PigBatchID, | ||||
| 	} | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", resp, action, "更新成功", resp) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "更新成功", pen, action, "更新成功", pen) | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"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" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| ) | ||||
| @@ -44,18 +43,7 @@ func (c *Controller) ListSensorData(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.SensorDataListOptions{ | ||||
| 		DeviceID:  req.DeviceID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	if req.SensorType != nil { | ||||
| 		sensorType := models.SensorType(*req.SensorType) | ||||
| 		opts.SensorType = &sensorType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListSensorData(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListSensorData(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -66,8 +54,7 @@ func (c *Controller) ListSensorData(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取传感器数据失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取传感器数据成功", resp, actionType, "获取传感器数据成功", req) | ||||
| } | ||||
|  | ||||
| @@ -89,15 +76,7 @@ func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.DeviceCommandLogListOptions{ | ||||
| 		DeviceID:        req.DeviceID, | ||||
| 		ReceivedSuccess: req.ReceivedSuccess, | ||||
| 		OrderBy:         req.OrderBy, | ||||
| 		StartTime:       req.StartTime, | ||||
| 		EndTime:         req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListDeviceCommandLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListDeviceCommandLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -108,8 +87,7 @@ func (c *Controller) ListDeviceCommandLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备命令日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备命令日志成功", resp, actionType, "获取设备命令日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -131,18 +109,7 @@ func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PlanExecutionLogListOptions{ | ||||
| 		PlanID:    req.PlanID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.ExecutionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	planLogs, plans, total, err := c.monitorService.ListPlanExecutionLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPlanExecutionLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -153,8 +120,7 @@ func (c *Controller) ListPlanExecutionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划执行日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPlanExecutionLogResponse(planLogs, plans, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(planLogs), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划执行日志成功", resp, actionType, "获取计划执行日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -176,19 +142,7 @@ func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.TaskExecutionLogListOptions{ | ||||
| 		PlanExecutionLogID: req.PlanExecutionLogID, | ||||
| 		TaskID:             req.TaskID, | ||||
| 		OrderBy:            req.OrderBy, | ||||
| 		StartTime:          req.StartTime, | ||||
| 		EndTime:            req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.ExecutionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListTaskExecutionLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListTaskExecutionLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -199,8 +153,7 @@ func (c *Controller) ListTaskExecutionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取任务执行日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取任务执行日志成功", resp, actionType, "获取任务执行日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -222,18 +175,7 @@ func (c *Controller) ListPendingCollections(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PendingCollectionListOptions{ | ||||
| 		DeviceID:  req.DeviceID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.PendingCollectionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPendingCollections(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPendingCollections(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -244,8 +186,7 @@ func (c *Controller) ListPendingCollections(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取待采集请求失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取待采集请求成功", resp, actionType, "获取待采集请求成功", req) | ||||
| } | ||||
|  | ||||
| @@ -267,20 +208,7 @@ func (c *Controller) ListUserActionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.UserActionLogListOptions{ | ||||
| 		UserID:     req.UserID, | ||||
| 		Username:   req.Username, | ||||
| 		ActionType: req.ActionType, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.AuditStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListUserActionLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListUserActionLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -291,8 +219,7 @@ func (c *Controller) ListUserActionLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户操作日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作日志成功", resp, actionType, "获取用户操作日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -314,15 +241,7 @@ func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.RawMaterialPurchaseListOptions{ | ||||
| 		RawMaterialID: req.RawMaterialID, | ||||
| 		Supplier:      req.Supplier, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListRawMaterialPurchases(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListRawMaterialPurchases(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -333,8 +252,7 @@ func (c *Controller) ListRawMaterialPurchases(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料采购记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料采购记录成功", resp, actionType, "获取原料采购记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -356,19 +274,7 @@ func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.RawMaterialStockLogListOptions{ | ||||
| 		RawMaterialID: req.RawMaterialID, | ||||
| 		SourceID:      req.SourceID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
| 	if req.SourceType != nil { | ||||
| 		sourceType := models.StockLogSourceType(*req.SourceType) | ||||
| 		opts.SourceType = &sourceType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListRawMaterialStockLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListRawMaterialStockLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -379,8 +285,7 @@ func (c *Controller) ListRawMaterialStockLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取原料库存日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取原料库存日志成功", resp, actionType, "获取原料库存日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -402,16 +307,7 @@ func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.FeedUsageRecordListOptions{ | ||||
| 		PenID:         req.PenID, | ||||
| 		FeedFormulaID: req.FeedFormulaID, | ||||
| 		OperatorID:    req.OperatorID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListFeedUsageRecords(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListFeedUsageRecords(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -422,8 +318,7 @@ func (c *Controller) ListFeedUsageRecords(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取饲料使用记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取饲料使用记录成功", resp, actionType, "获取饲料使用记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -445,20 +340,7 @@ func (c *Controller) ListMedicationLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.MedicationLogListOptions{ | ||||
| 		PigBatchID:   req.PigBatchID, | ||||
| 		MedicationID: req.MedicationID, | ||||
| 		OperatorID:   req.OperatorID, | ||||
| 		OrderBy:      req.OrderBy, | ||||
| 		StartTime:    req.StartTime, | ||||
| 		EndTime:      req.EndTime, | ||||
| 	} | ||||
| 	if req.Reason != nil { | ||||
| 		reason := models.MedicationReasonType(*req.Reason) | ||||
| 		opts.Reason = &reason | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListMedicationLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListMedicationLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -469,8 +351,7 @@ func (c *Controller) ListMedicationLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用药记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用药记录成功", resp, actionType, "获取用药记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -492,19 +373,7 @@ func (c *Controller) ListPigBatchLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PigBatchLogListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.ChangeType != nil { | ||||
| 		changeType := models.LogChangeType(*req.ChangeType) | ||||
| 		opts.ChangeType = &changeType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPigBatchLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPigBatchLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -515,8 +384,7 @@ func (c *Controller) ListPigBatchLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪批次日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪批次日志成功", resp, actionType, "获取猪批次日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -538,14 +406,7 @@ func (c *Controller) ListWeighingBatches(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.WeighingBatchListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListWeighingBatches(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListWeighingBatches(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -556,8 +417,7 @@ func (c *Controller) ListWeighingBatches(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取批次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取批次称重记录成功", resp, actionType, "获取批次称重记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -579,16 +439,7 @@ func (c *Controller) ListWeighingRecords(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.WeighingRecordListOptions{ | ||||
| 		WeighingBatchID: req.WeighingBatchID, | ||||
| 		PenID:           req.PenID, | ||||
| 		OperatorID:      req.OperatorID, | ||||
| 		OrderBy:         req.OrderBy, | ||||
| 		StartTime:       req.StartTime, | ||||
| 		EndTime:         req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListWeighingRecords(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListWeighingRecords(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -599,8 +450,7 @@ func (c *Controller) ListWeighingRecords(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取单次称重记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取单次称重记录成功", resp, actionType, "获取单次称重记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -622,21 +472,7 @@ func (c *Controller) ListPigTransferLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PigTransferLogListOptions{ | ||||
| 		PigBatchID:    req.PigBatchID, | ||||
| 		PenID:         req.PenID, | ||||
| 		OperatorID:    req.OperatorID, | ||||
| 		CorrelationID: req.CorrelationID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
| 	if req.TransferType != nil { | ||||
| 		transferType := models.PigTransferType(*req.TransferType) | ||||
| 		opts.TransferType = &transferType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPigTransferLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPigTransferLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -647,8 +483,7 @@ func (c *Controller) ListPigTransferLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只迁移日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只迁移日志成功", resp, actionType, "获取猪只迁移日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -670,24 +505,7 @@ func (c *Controller) ListPigSickLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PigSickLogListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		PenID:      req.PenID, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.Reason != nil { | ||||
| 		reason := models.PigBatchSickPigReasonType(*req.Reason) | ||||
| 		opts.Reason = &reason | ||||
| 	} | ||||
| 	if req.TreatmentLocation != nil { | ||||
| 		treatmentLocation := models.PigBatchSickPigTreatmentLocation(*req.TreatmentLocation) | ||||
| 		opts.TreatmentLocation = &treatmentLocation | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPigSickLogs(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPigSickLogs(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -698,8 +516,7 @@ func (c *Controller) ListPigSickLogs(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取病猪日志失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取病猪日志成功", resp, actionType, "获取病猪日志成功", req) | ||||
| } | ||||
|  | ||||
| @@ -721,16 +538,7 @@ func (c *Controller) ListPigPurchases(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PigPurchaseListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		Supplier:   req.Supplier, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPigPurchases(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPigPurchases(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -741,8 +549,7 @@ func (c *Controller) ListPigPurchases(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只采购记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只采购记录成功", resp, actionType, "获取猪只采购记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -764,16 +571,7 @@ func (c *Controller) ListPigSales(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.PigSaleListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		Buyer:      req.Buyer, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListPigSales(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListPigSales(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -784,8 +582,7 @@ func (c *Controller) ListPigSales(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取猪只售卖记录失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取猪只售卖记录成功", resp, actionType, "获取猪只售卖记录成功", req) | ||||
| } | ||||
|  | ||||
| @@ -807,17 +604,7 @@ func (c *Controller) ListNotifications(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	opts := repository.NotificationListOptions{ | ||||
| 		UserID:       req.UserID, | ||||
| 		NotifierType: req.NotifierType, | ||||
| 		Level:        req.Level, | ||||
| 		StartTime:    req.StartTime, | ||||
| 		EndTime:      req.EndTime, | ||||
| 		OrderBy:      req.OrderBy, | ||||
| 		Status:       req.Status, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := c.monitorService.ListNotifications(opts, req.Page, req.PageSize) | ||||
| 	resp, err := c.monitorService.ListNotifications(&req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| @@ -828,7 +615,6 @@ func (c *Controller) ListNotifications(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "批量查询通知失败: "+err.Error(), actionType, "服务层查询失败", req) | ||||
| 	} | ||||
|  | ||||
| 	resp := dto.NewListNotificationResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(data), total) | ||||
| 	c.logger.Infof("%s: 成功, 获取到 %d 条记录, 总计 %d 条", actionType, len(resp.List), resp.Pagination.Total) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "批量查询通知成功", resp, actionType, "批量查询通知成功", req) | ||||
| } | ||||
|   | ||||
| @@ -6,29 +6,24 @@ import ( | ||||
|  | ||||
| 	"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/scheduler" | ||||
| 	"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" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // --- 控制器定义 --- | ||||
|  | ||||
| // Controller 定义了计划相关的控制器 | ||||
| type Controller struct { | ||||
| 	logger                  *logs.Logger | ||||
| 	planRepo                repository.PlanRepository | ||||
| 	analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager | ||||
| 	logger      *logs.Logger | ||||
| 	planService service.PlanService | ||||
| } | ||||
|  | ||||
| // NewController 创建一个新的 Controller 实例 | ||||
| func NewController(logger *logs.Logger, planRepo repository.PlanRepository, analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager) *Controller { | ||||
| func NewController(logger *logs.Logger, planService service.PlanService) *Controller { | ||||
| 	return &Controller{ | ||||
| 		logger:                  logger, | ||||
| 		planRepo:                planRepo, | ||||
| 		analysisPlanTaskManager: analysisPlanTaskManager, | ||||
| 		logger:      logger, | ||||
| 		planService: planService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -52,46 +47,19 @@ func (c *Controller) CreatePlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 使用已有的转换函数,它已经包含了验证和重排逻辑 | ||||
| 	planToCreate, err := dto.NewPlanFromCreateRequest(&req) | ||||
| 	// 调用服务层创建计划 | ||||
| 	resp, err := c.planService.CreatePlan(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// --- 业务规则处理 --- | ||||
| 	// 1. 设置计划类型:用户创建的计划永远是自定义计划 | ||||
| 	planToCreate.PlanType = models.PlanTypeCustom | ||||
|  | ||||
| 	// 2. 自动判断 ContentType | ||||
| 	if len(req.SubPlanIDs) > 0 { | ||||
| 		planToCreate.ContentType = models.PlanContentTypeSubPlans | ||||
| 	} else { | ||||
| 		// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供) | ||||
| 		planToCreate.ContentType = models.PlanContentTypeTasks | ||||
| 	} | ||||
|  | ||||
| 	// 调用仓库方法创建计划 | ||||
| 	if err := c.planRepo.CreatePlan(planToCreate); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "数据库创建计划失败", planToCreate) | ||||
| 	} | ||||
|  | ||||
| 	// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列 | ||||
| 	if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil { | ||||
| 		// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功 | ||||
| 		c.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	// 使用已有的转换函数将创建后的模型转换为响应对象 | ||||
| 	resp, err := dto.NewPlanToResponse(planToCreate) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败", actionType, "响应序列化失败", planToCreate) | ||||
| 		c.logger.Errorf("%s: 服务层创建计划失败: %v", actionType, err) | ||||
| 		// 根据服务层返回的错误类型,转换为相应的HTTP状态码 | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划数据校验失败或关联计划不存在", req) | ||||
| 		} | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建计划失败: "+err.Error(), actionType, "服务层创建计划失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 使用统一的成功响应函数 | ||||
| 	c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID) | ||||
| 	c.logger.Infof("%s: 计划创建成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "计划创建成功", resp, actionType, "计划创建成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -114,24 +82,14 @@ func (c *Controller) GetPlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 调用仓库层获取计划详情 | ||||
| 	plan, err := c.planRepo.GetPlanByID(uint(id)) | ||||
| 	// 调用服务层获取计划详情 | ||||
| 	resp, err := c.planService.GetPlanByID(uint(id)) | ||||
| 	if err != nil { | ||||
| 		// 判断是否为“未找到”错误 | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) | ||||
| 		c.logger.Errorf("%s: 服务层获取计划详情失败: %v, ID: %d", actionType, err, id) | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) | ||||
| 		} | ||||
| 		// 其他数据库错误视为内部错误 | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情时发生内部错误", actionType, "数据库查询失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 将模型转换为响应 DTO | ||||
| 	resp, err := dto.NewPlanToResponse(plan) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误", actionType, "响应序列化失败", plan) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划详情失败: "+err.Error(), actionType, "服务层获取计划详情失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 4. 发送成功响应 | ||||
| @@ -156,31 +114,14 @@ func (c *Controller) ListPlans(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "查询参数绑定失败", query) | ||||
| 	} | ||||
|  | ||||
| 	// 1. 调用仓库层获取所有计划 | ||||
| 	opts := repository.ListPlansOptions{PlanType: query.PlanType} | ||||
| 	plans, total, err := c.planRepo.ListPlans(opts, query.Page, query.PageSize) | ||||
| 	// 调用服务层获取计划列表 | ||||
| 	resp, err := c.planService.ListPlans(&query) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表时发生内部错误", actionType, "数据库查询失败", nil) | ||||
| 		c.logger.Errorf("%s: 服务层获取计划列表失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: "+err.Error(), actionType, "服务层获取计划列表失败", nil) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 将模型转换为响应 DTO | ||||
| 	planResponses := make([]dto.PlanResponse, 0, len(plans)) | ||||
| 	for _, p := range plans { | ||||
| 		resp, err := dto.NewPlanToResponse(&p) | ||||
| 		if err != nil { | ||||
| 			c.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, p) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误", actionType, "响应序列化失败", p) | ||||
| 		} | ||||
| 		planResponses = append(planResponses, *resp) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 构造并发送成功响应 | ||||
| 	resp := dto.ListPlansResponse{ | ||||
| 		Plans: planResponses, | ||||
| 		Total: total, | ||||
| 	} | ||||
| 	c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses)) | ||||
| 	c.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(resp.Plans)) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取计划列表成功", resp, actionType, "获取计划列表成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -212,71 +153,20 @@ func (c *Controller) UpdatePlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 检查计划是否存在 | ||||
| 	existingPlan, err := c.planRepo.GetBasicPlanByID(uint(id)) | ||||
| 	// 调用服务层更新计划 | ||||
| 	resp, err := c.planService.UpdatePlan(uint(id), &req) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) | ||||
| 		c.logger.Errorf("%s: 服务层更新计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanCannotBeModified) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许修改", id) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 4. 业务规则:系统计划不允许修改 | ||||
| 	if existingPlan.PlanType == models.PlanTypeSystem { | ||||
| 		c.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许修改", actionType, "尝试修改系统计划", id) | ||||
| 	} | ||||
|  | ||||
| 	// 5. 将请求转换为模型(转换函数带校验) | ||||
| 	planToUpdate, err := dto.NewPlanFromUpdateRequest(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划数据校验失败: "+err.Error(), actionType, "计划数据校验失败", req) | ||||
| 	} | ||||
| 	planToUpdate.ID = uint(id) // 确保ID被设置 | ||||
|  | ||||
| 	// --- 自动判断 ContentType --- | ||||
| 	if len(req.SubPlanIDs) > 0 { | ||||
| 		planToUpdate.ContentType = models.PlanContentTypeSubPlans | ||||
| 	} else { | ||||
| 		// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供) | ||||
| 		planToUpdate.ContentType = models.PlanContentTypeTasks | ||||
| 	} | ||||
|  | ||||
| 	// 6. 调用仓库方法更新计划 | ||||
| 	// 只要是更新任务,就重置执行计数器 | ||||
| 	planToUpdate.ExecuteCount = 0 // 重置计数器 | ||||
| 	c.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID) | ||||
|  | ||||
| 	if err := c.planRepo.UpdatePlan(planToUpdate); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "数据库更新计划失败", planToUpdate) | ||||
| 	} | ||||
|  | ||||
| 	// 更新成功后,调用 manager 确保触发器任务定义存在 | ||||
| 	if err := c.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil { | ||||
| 		// 这是一个非阻塞性错误,我们只记录日志 | ||||
| 		c.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	// 7. 获取更新后的完整计划用于响应 | ||||
| 	updatedPlan, err := c.planRepo.GetPlanByID(uint(id)) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取更新后计划详情时发生内部错误", actionType, "获取更新后计划详情失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 8. 将模型转换为响应 DTO | ||||
| 	resp, err := dto.NewPlanToResponse(updatedPlan) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败", actionType, "响应序列化失败", updatedPlan) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划失败: "+err.Error(), actionType, "服务层更新计划失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 9. 发送成功响应 | ||||
| 	c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID) | ||||
| 	c.logger.Infof("%s: 计划更新成功, ID: %d", actionType, resp.ID) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "计划更新成功", resp, actionType, "计划更新成功", resp) | ||||
| } | ||||
|  | ||||
| @@ -299,35 +189,16 @@ func (c *Controller) DeletePlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 检查计划是否存在 | ||||
| 	plan, err := c.planRepo.GetBasicPlanByID(uint(id)) | ||||
| 	// 调用服务层删除计划 | ||||
| 	err = c.planService.DeletePlan(uint(id)) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) | ||||
| 		c.logger.Errorf("%s: 服务层删除计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanCannotBeDeleted) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许删除", id) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 业务规则:系统计划不允许删除 | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		c.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许删除", actionType, "尝试删除系统计划", id) | ||||
| 	} | ||||
|  | ||||
| 	// 4. 停止这个计划 | ||||
| 	if plan.Status == models.PlanStatusEnabled { | ||||
| 		if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil { | ||||
| 			c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 5. 调用仓库层删除计划 | ||||
| 	if err := c.planRepo.DeletePlan(uint(id)); err != nil { | ||||
| 		c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划时发生内部错误", actionType, "数据库删除失败", id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除计划失败: "+err.Error(), actionType, "服务层删除计划失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 6. 发送成功响应 | ||||
| @@ -354,56 +225,18 @@ func (c *Controller) StartPlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 检查计划是否存在 | ||||
| 	plan, err := c.planRepo.GetBasicPlanByID(uint(id)) | ||||
| 	// 调用服务层启动计划 | ||||
| 	err = c.planService.StartPlan(uint(id)) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) | ||||
| 		c.logger.Errorf("%s: 服务层启动计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanCannotBeStarted) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许手动启动", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanAlreadyEnabled) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划已处于启动状态", id) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 业务规则检查 | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		c.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许手动启动", actionType, "尝试手动启动系统计划", id) | ||||
| 	} | ||||
| 	if plan.Status == models.PlanStatusEnabled { | ||||
| 		c.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", id) | ||||
| 	} | ||||
|  | ||||
| 	// 4. 检查并重置执行计数器,然后更新计划状态为“已启动” | ||||
| 	// 只有当计划是从非 Enabled 状态(如 Disabled, Stopeed, Failed)启动时,才需要重置计数器 | ||||
| 	if plan.Status != models.PlanStatusEnabled { | ||||
| 		// 如果计划是从停止或失败状态重新启动,且计数器不为0,则重置执行计数 | ||||
| 		if plan.ExecuteCount > 0 { | ||||
| 			if err := c.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil { | ||||
| 				c.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 				return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "重置计划执行计数失败", actionType, "重置执行计数失败", plan.ID) | ||||
| 			} | ||||
| 			c.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID) | ||||
| 		} | ||||
|  | ||||
| 		// 更新计划状态为“已启动” | ||||
| 		if err := c.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil { | ||||
| 			c.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新计划状态失败", actionType, "更新计划状态失败", plan.ID) | ||||
| 		} | ||||
| 		c.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID) | ||||
| 	} else { | ||||
| 		// 如果计划已经处于 Enabled 状态,则无需更新 | ||||
| 		c.logger.Infof("计划 #%d 已处于启动状态,无需重复操作。", plan.ID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划已处于启动状态,无需重复操作", actionType, "计划已处于启动状态", plan.ID) | ||||
| 	} | ||||
|  | ||||
| 	// 5. 为计划创建或更新触发器 | ||||
| 	if err := c.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil { | ||||
| 		// 此处错误不回滚状态,因为状态更新已成功,但需要明确告知用户触发器创建失败 | ||||
| 		c.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "计划状态已更新,但创建执行触发器失败,请检查计划配置或稍后重试", actionType, "创建执行触发器失败", plan.ID) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "启动计划失败: "+err.Error(), actionType, "服务层启动计划失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 6. 发送成功响应 | ||||
| @@ -430,33 +263,18 @@ func (c *Controller) StopPlan(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的计划ID格式", actionType, "计划ID格式错误", idStr) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 检查计划是否存在 | ||||
| 	plan, err := c.planRepo.GetBasicPlanByID(uint(id)) | ||||
| 	// 调用服务层停止计划 | ||||
| 	err = c.planService.StopPlan(uint(id)) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			c.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "计划不存在", actionType, "计划不存在", id) | ||||
| 		c.logger.Errorf("%s: 服务层停止计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		if errors.Is(err, service.ErrPlanNotFound) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "计划不存在", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanCannotBeStopped) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, err.Error(), actionType, "系统计划不允许停止", id) | ||||
| 		} else if errors.Is(err, service.ErrPlanNotEnabled) { | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "计划未启用", id) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取计划信息时发生内部错误", actionType, "数据库查询失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 业务规则:系统计划不允许停止 | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		c.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeForbidden, "系统计划不允许停止", actionType, "尝试停止系统计划", id) | ||||
| 	} | ||||
|  | ||||
| 	// 4. 检查计划当前状态 | ||||
| 	if plan.Status != models.PlanStatusEnabled { | ||||
| 		c.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "计划当前不是启用状态", actionType, "计划未启用", id) | ||||
| 	} | ||||
|  | ||||
| 	// 5. 调用仓库层方法,该方法内部处理事务 | ||||
| 	if err := c.planRepo.StopPlanTransactionally(uint(id)); err != nil { | ||||
| 		c.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划时发生内部错误: "+err.Error(), actionType, "停止计划失败", id) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "停止计划失败: "+err.Error(), actionType, "服务层停止计划失败", id) | ||||
| 	} | ||||
|  | ||||
| 	// 6. 发送成功响应 | ||||
|   | ||||
| @@ -1,44 +1,29 @@ | ||||
| package user | ||||
|  | ||||
| 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" | ||||
| 	domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" | ||||
| 	"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" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // Controller 用户控制器 | ||||
| type Controller struct { | ||||
| 	userRepo       repository.UserRepository | ||||
| 	monitorService service.MonitorService | ||||
| 	tokenService   token.Service | ||||
| 	notifyService  domain_notify.Service | ||||
| 	logger         *logs.Logger | ||||
| 	userService service.UserService | ||||
| 	logger      *logs.Logger | ||||
| } | ||||
|  | ||||
| // NewController 创建用户控制器实例 | ||||
| func NewController( | ||||
| 	userRepo repository.UserRepository, | ||||
| 	monitorService service.MonitorService, | ||||
| 	userService service.UserService, | ||||
| 	logger *logs.Logger, | ||||
| 	tokenService token.Service, | ||||
| 	notifyService domain_notify.Service, | ||||
| ) *Controller { | ||||
| 	return &Controller{ | ||||
| 		userRepo:       userRepo, | ||||
| 		monitorService: monitorService, | ||||
| 		tokenService:   tokenService, | ||||
| 		notifyService:  notifyService, | ||||
| 		logger:         logger, | ||||
| 		userService: userService, | ||||
| 		logger:      logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -60,28 +45,13 @@ func (c *Controller) CreateUser(ctx echo.Context) error { | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	user := &models.User{ | ||||
| 		Username: req.Username, | ||||
| 		Password: req.Password, // 密码会在 BeforeSave 钩子中哈希 | ||||
| 	resp, err := c.userService.CreateUser(&req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("创建用户: 服务层调用失败: %v", err) | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeInternalError, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if err := c.userRepo.Create(user); err != nil { | ||||
| 		c.logger.Errorf("创建用户: 创建用户失败: %v", err) | ||||
|  | ||||
| 		// 尝试查询用户,以判断是否是用户名重复导致的错误 | ||||
| 		_, findErr := c.userRepo.FindByUsername(req.Username) | ||||
| 		if findErr == nil { // 如果能找到用户,说明是用户名重复 | ||||
| 			return controller.SendErrorResponse(ctx, controller.CodeConflict, "用户名已存在") | ||||
| 		} | ||||
|  | ||||
| 		// 其他创建失败的情况 | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeInternalError, "创建用户失败") | ||||
| 	} | ||||
|  | ||||
| 	return controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", dto.CreateUserResponse{ | ||||
| 		Username: user.Username, | ||||
| 		ID:       user.ID, | ||||
| 	}) | ||||
| 	return controller.SendResponse(ctx, controller.CodeCreated, "用户创建成功", resp) | ||||
| } | ||||
|  | ||||
| // Login godoc | ||||
| @@ -100,94 +70,13 @@ func (c *Controller) Login(ctx echo.Context) error { | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeBadRequest, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户 | ||||
| 	user, err := c.userRepo.FindUserForLogin(req.Identifier) | ||||
| 	resp, err := c.userService.Login(&req) | ||||
| 	if err != nil { | ||||
| 		if err == gorm.ErrRecordNotFound { | ||||
| 			return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确") | ||||
| 		} | ||||
| 		c.logger.Errorf("登录: 查询用户失败: %v", err) | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败") | ||||
| 		c.logger.Errorf("登录: 服务层调用失败: %v", err) | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if !user.CheckPassword(req.Password) { | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeUnauthorized, "登录凭证不正确") | ||||
| 	} | ||||
|  | ||||
| 	// 登录成功,生成 JWT token | ||||
| 	tokenString, err := c.tokenService.GenerateToken(user.ID) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("登录: 生成令牌失败: %v", err) | ||||
| 		return controller.SendErrorResponse(ctx, controller.CodeInternalError, "登录失败,无法生成认证信息") | ||||
| 	} | ||||
|  | ||||
| 	return controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", dto.LoginResponse{ | ||||
| 		Username: user.Username, | ||||
| 		ID:       user.ID, | ||||
| 		Token:    tokenString, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // ListUserHistory godoc | ||||
| // @Summary      获取指定用户的操作历史 | ||||
| // @Description  根据用户ID,分页获取该用户的操作审计日志。支持与通用日志查询接口相同的过滤和排序参数。 | ||||
| // @Tags         用户管理 | ||||
| // @Security     BearerAuth | ||||
| // @Produce      json | ||||
| // @Param        id    path      int  true  "用户ID" | ||||
| // @Param        query query     dto.ListUserActionLogRequest false "查询参数 (除了 user_id,它被路径中的ID覆盖)" | ||||
| // @Success      200   {object}  controller.Response{data=dto.ListUserActionLogResponse} "业务码为200代表成功获取" | ||||
| // @Router       /api/v1/users/{id}/history [get] | ||||
| func (c *Controller) ListUserHistory(ctx echo.Context) error { | ||||
| 	const actionType = "获取用户操作历史" | ||||
|  | ||||
| 	// 1. 解析路径中的用户ID,它的优先级最高 | ||||
| 	userIDStr := ctx.Param("id") | ||||
| 	userID, err := strconv.ParseUint(userIDStr, 10, 64) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 无效的用户ID格式: %v, ID: %s", actionType, err, userIDStr) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的用户ID格式", actionType, "无效的用户ID格式", userIDStr) | ||||
| 	} | ||||
|  | ||||
| 	// 2. 绑定通用的查询请求 DTO | ||||
| 	var req dto.ListUserActionLogRequest | ||||
| 	if err := ctx.Bind(&req); err != nil { | ||||
| 		c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的查询参数: "+err.Error(), actionType, "参数绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 准备 Service 调用参数,并强制使用路径中的 UserID | ||||
| 	uid := uint(userID) | ||||
| 	req.UserID = &uid // 强制覆盖 | ||||
|  | ||||
| 	opts := repository.UserActionLogListOptions{ | ||||
| 		UserID:     req.UserID, | ||||
| 		Username:   req.Username, | ||||
| 		ActionType: req.ActionType, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.AuditStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	// 4. 调用 monitorService,复用其业务逻辑 | ||||
| 	data, total, err := c.monitorService.ListUserActionLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, repository.ErrInvalidPagination) { | ||||
| 			c.logger.Warnf("%s: 无效的分页参数: %v", actionType, err) | ||||
| 			return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的分页参数: "+err.Error(), actionType, "无效分页参数", opts) | ||||
| 		} | ||||
| 		c.logger.Errorf("%s: 服务层查询失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取用户历史记录失败", actionType, "服务层查询失败", opts) | ||||
| 	} | ||||
|  | ||||
| 	// 5. 使用复用的 DTO 构建并发送成功响应 | ||||
| 	resp := dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize) | ||||
| 	c.logger.Infof("%s: 成功获取用户 %d 的操作历史, 数量: %d", actionType, userID, len(data)) | ||||
| 	return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取用户操作历史成功", resp, actionType, "获取用户操作历史成功", opts) | ||||
| 	return controller.SendResponse(ctx, controller.CodeSuccess, "登录成功", resp) | ||||
| } | ||||
|  | ||||
| // SendTestNotification godoc | ||||
| @@ -218,8 +107,8 @@ func (c *Controller) SendTestNotification(ctx echo.Context) error { | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "请求体格式错误或缺少 'type' 字段: "+err.Error(), actionType, "请求体绑定失败", req) | ||||
| 	} | ||||
|  | ||||
| 	// 3. 调用领域服务 | ||||
| 	err = c.notifyService.SendTestMessage(uint(userID), req.Type) | ||||
| 	// 3. 调用服务层 | ||||
| 	err = c.userService.SendTestNotification(uint(userID), &req) | ||||
| 	if err != nil { | ||||
| 		c.logger.Errorf("%s: 服务层调用失败: %v", actionType, err) | ||||
| 		return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "发送测试消息失败: "+err.Error(), actionType, "服务层调用失败", map[string]interface{}{"userID": userID, "type": req.Type}) | ||||
|   | ||||
							
								
								
									
										373
									
								
								internal/app/service/device_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								internal/app/service/device_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,373 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/domain/device" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| ) | ||||
|  | ||||
| // DeviceService 定义了应用层的设备服务接口,用于协调设备相关的业务逻辑。 | ||||
| type DeviceService interface { | ||||
| 	CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) | ||||
| 	GetDevice(id string) (*dto.DeviceResponse, error) | ||||
| 	ListDevices() ([]*dto.DeviceResponse, error) | ||||
| 	UpdateDevice(id string, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) | ||||
| 	DeleteDevice(id string) error | ||||
| 	ManualControl(id string, req *dto.ManualControlDeviceRequest) error | ||||
|  | ||||
| 	CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) | ||||
| 	GetAreaController(id string) (*dto.AreaControllerResponse, error) | ||||
| 	ListAreaControllers() ([]*dto.AreaControllerResponse, error) | ||||
| 	UpdateAreaController(id string, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) | ||||
| 	DeleteAreaController(id string) error | ||||
|  | ||||
| 	CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) | ||||
| 	GetDeviceTemplate(id string) (*dto.DeviceTemplateResponse, error) | ||||
| 	ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error) | ||||
| 	UpdateDeviceTemplate(id string, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) | ||||
| 	DeleteDeviceTemplate(id string) error | ||||
| } | ||||
|  | ||||
| // deviceService 是 DeviceService 接口的具体实现。 | ||||
| type deviceService struct { | ||||
| 	deviceRepo         repository.DeviceRepository | ||||
| 	areaControllerRepo repository.AreaControllerRepository | ||||
| 	deviceTemplateRepo repository.DeviceTemplateRepository | ||||
| 	deviceDomainSvc    device.Service // 依赖领域服务 | ||||
| } | ||||
|  | ||||
| // NewDeviceService 创建一个新的 DeviceService 实例。 | ||||
| func NewDeviceService( | ||||
| 	deviceRepo repository.DeviceRepository, | ||||
| 	areaControllerRepo repository.AreaControllerRepository, | ||||
| 	deviceTemplateRepo repository.DeviceTemplateRepository, | ||||
| 	deviceDomainSvc device.Service, | ||||
| ) DeviceService { | ||||
| 	return &deviceService{ | ||||
| 		deviceRepo:         deviceRepo, | ||||
| 		areaControllerRepo: areaControllerRepo, | ||||
| 		deviceTemplateRepo: deviceTemplateRepo, | ||||
| 		deviceDomainSvc:    deviceDomainSvc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // --- Devices --- | ||||
|  | ||||
| func (s *deviceService) CreateDevice(req *dto.CreateDeviceRequest) (*dto.DeviceResponse, error) { | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	if err != nil { | ||||
| 		return nil, err // Consider wrapping this error for better context | ||||
| 	} | ||||
|  | ||||
| 	device := &models.Device{ | ||||
| 		Name:             req.Name, | ||||
| 		DeviceTemplateID: req.DeviceTemplateID, | ||||
| 		AreaControllerID: req.AreaControllerID, | ||||
| 		Location:         req.Location, | ||||
| 		Properties:       propertiesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := device.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.deviceRepo.Create(device); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	createdDevice, err := s.deviceRepo.FindByID(device.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewDeviceResponse(createdDevice) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) GetDevice(id string) (*dto.DeviceResponse, error) { | ||||
| 	device, err := s.deviceRepo.FindByIDString(id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewDeviceResponse(device) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) ListDevices() ([]*dto.DeviceResponse, error) { | ||||
| 	devices, err := s.deviceRepo.ListAll() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewListDeviceResponse(devices) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) UpdateDevice(id string, req *dto.UpdateDeviceRequest) (*dto.DeviceResponse, error) { | ||||
| 	existingDevice, err := s.deviceRepo.FindByIDString(id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingDevice.Name = req.Name | ||||
| 	existingDevice.DeviceTemplateID = req.DeviceTemplateID | ||||
| 	existingDevice.AreaControllerID = req.AreaControllerID | ||||
| 	existingDevice.Location = req.Location | ||||
| 	existingDevice.Properties = propertiesJSON | ||||
|  | ||||
| 	if err := existingDevice.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.deviceRepo.Update(existingDevice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	updatedDevice, err := s.deviceRepo.FindByID(existingDevice.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewDeviceResponse(updatedDevice) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) DeleteDevice(id string) error { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Check if device exists before deleting | ||||
| 	_, err = s.deviceRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return s.deviceRepo.Delete(uint(idUint)) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) ManualControl(id string, req *dto.ManualControlDeviceRequest) error { | ||||
| 	dev, err := s.deviceRepo.FindByIDString(id) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if req.Action == nil { | ||||
| 		return s.deviceDomainSvc.Collect(dev.AreaControllerID, []*models.Device{dev}) | ||||
| 	} else { | ||||
| 		action := device.DeviceActionStart | ||||
| 		switch *req.Action { | ||||
| 		case "off": | ||||
| 			action = device.DeviceActionStop | ||||
| 		case "on": | ||||
| 			action = device.DeviceActionStart | ||||
| 		default: | ||||
| 			return errors.New("invalid action") | ||||
| 		} | ||||
| 		return s.deviceDomainSvc.Switch(dev, action) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // --- Area Controllers --- | ||||
|  | ||||
| func (s *deviceService) CreateAreaController(req *dto.CreateAreaControllerRequest) (*dto.AreaControllerResponse, error) { | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	ac := &models.AreaController{ | ||||
| 		Name:       req.Name, | ||||
| 		NetworkID:  req.NetworkID, | ||||
| 		Location:   req.Location, | ||||
| 		Properties: propertiesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := ac.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.areaControllerRepo.Create(ac); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewAreaControllerResponse(ac) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) GetAreaController(id string) (*dto.AreaControllerResponse, error) { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ac, err := s.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewAreaControllerResponse(ac) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) ListAreaControllers() ([]*dto.AreaControllerResponse, error) { | ||||
| 	acs, err := s.areaControllerRepo.ListAll() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewListAreaControllerResponse(acs) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) UpdateAreaController(id string, req *dto.UpdateAreaControllerRequest) (*dto.AreaControllerResponse, error) { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingAC, err := s.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	propertiesJSON, err := json.Marshal(req.Properties) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingAC.Name = req.Name | ||||
| 	existingAC.NetworkID = req.NetworkID | ||||
| 	existingAC.Location = req.Location | ||||
| 	existingAC.Properties = propertiesJSON | ||||
|  | ||||
| 	if err := existingAC.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.areaControllerRepo.Update(existingAC); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewAreaControllerResponse(existingAC) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) DeleteAreaController(id string) error { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = s.areaControllerRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return s.areaControllerRepo.Delete(uint(idUint)) | ||||
| } | ||||
|  | ||||
| // --- Device Templates --- | ||||
|  | ||||
| func (s *deviceService) CreateDeviceTemplate(req *dto.CreateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) { | ||||
| 	commandsJSON, err := json.Marshal(req.Commands) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	valuesJSON, err := json.Marshal(req.Values) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	deviceTemplate := &models.DeviceTemplate{ | ||||
| 		Name:         req.Name, | ||||
| 		Manufacturer: req.Manufacturer, | ||||
| 		Description:  req.Description, | ||||
| 		Category:     req.Category, | ||||
| 		Commands:     commandsJSON, | ||||
| 		Values:       valuesJSON, | ||||
| 	} | ||||
|  | ||||
| 	if err := deviceTemplate.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.deviceTemplateRepo.Create(deviceTemplate); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewDeviceTemplateResponse(deviceTemplate) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) GetDeviceTemplate(id string) (*dto.DeviceTemplateResponse, error) { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	deviceTemplate, err := s.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewDeviceTemplateResponse(deviceTemplate) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) ListDeviceTemplates() ([]*dto.DeviceTemplateResponse, error) { | ||||
| 	deviceTemplates, err := s.deviceTemplateRepo.ListAll() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return dto.NewListDeviceTemplateResponse(deviceTemplates) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) UpdateDeviceTemplate(id string, req *dto.UpdateDeviceTemplateRequest) (*dto.DeviceTemplateResponse, error) { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingDeviceTemplate, err := s.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	commandsJSON, err := json.Marshal(req.Commands) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	valuesJSON, err := json.Marshal(req.Values) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingDeviceTemplate.Name = req.Name | ||||
| 	existingDeviceTemplate.Manufacturer = req.Manufacturer | ||||
| 	existingDeviceTemplate.Description = req.Description | ||||
| 	existingDeviceTemplate.Category = req.Category | ||||
| 	existingDeviceTemplate.Commands = commandsJSON | ||||
| 	existingDeviceTemplate.Values = valuesJSON | ||||
|  | ||||
| 	if err := existingDeviceTemplate.SelfCheck(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewDeviceTemplateResponse(existingDeviceTemplate) | ||||
| } | ||||
|  | ||||
| func (s *deviceService) DeleteDeviceTemplate(id string) error { | ||||
| 	idUint, err := strconv.ParseUint(id, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = s.deviceTemplateRepo.FindByID(uint(idUint)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return s.deviceTemplateRepo.Delete(uint(idUint)) | ||||
| } | ||||
| @@ -1,30 +1,31 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" | ||||
| ) | ||||
|  | ||||
| // MonitorService 定义了监控相关的业务逻辑服务接口 | ||||
| type MonitorService interface { | ||||
| 	ListSensorData(opts repository.SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) | ||||
| 	ListDeviceCommandLogs(opts repository.DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error) | ||||
| 	ListPlanExecutionLogs(opts repository.PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, []models.Plan, int64, error) | ||||
| 	ListTaskExecutionLogs(opts repository.TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error) | ||||
| 	ListPendingCollections(opts repository.PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error) | ||||
| 	ListUserActionLogs(opts repository.UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error) | ||||
| 	ListRawMaterialPurchases(opts repository.RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) | ||||
| 	ListRawMaterialStockLogs(opts repository.RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) | ||||
| 	ListFeedUsageRecords(opts repository.FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) | ||||
| 	ListMedicationLogs(opts repository.MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error) | ||||
| 	ListPigBatchLogs(opts repository.PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) | ||||
| 	ListWeighingBatches(opts repository.WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error) | ||||
| 	ListWeighingRecords(opts repository.WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error) | ||||
| 	ListPigTransferLogs(opts repository.PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) | ||||
| 	ListPigSickLogs(opts repository.PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) | ||||
| 	ListPigPurchases(opts repository.PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error) | ||||
| 	ListPigSales(opts repository.PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error) | ||||
| 	ListNotifications(opts repository.NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error) | ||||
| 	ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error) | ||||
| 	ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error) | ||||
| 	ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error) | ||||
| 	ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error) | ||||
| 	ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error) | ||||
| 	ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error) | ||||
| 	ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) | ||||
| 	ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) | ||||
| 	ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) | ||||
| 	ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) | ||||
| 	ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error) | ||||
| 	ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error) | ||||
| 	ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error) | ||||
| 	ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error) | ||||
| 	ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error) | ||||
| 	ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error) | ||||
| 	ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error) | ||||
| 	ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error) | ||||
| } | ||||
|  | ||||
| // monitorService 是 MonitorService 接口的具体实现 | ||||
| @@ -81,22 +82,63 @@ func NewMonitorService( | ||||
| } | ||||
|  | ||||
| // ListSensorData 负责处理查询传感器数据列表的业务逻辑 | ||||
| func (s *monitorService) ListSensorData(opts repository.SensorDataListOptions, page, pageSize int) ([]models.SensorData, int64, error) { | ||||
| 	return s.sensorDataRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListSensorData(req *dto.ListSensorDataRequest) (*dto.ListSensorDataResponse, error) { | ||||
| 	opts := repository.SensorDataListOptions{ | ||||
| 		DeviceID:  req.DeviceID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	if req.SensorType != nil { | ||||
| 		sensorType := models.SensorType(*req.SensorType) | ||||
| 		opts.SensorType = &sensorType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.sensorDataRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListSensorDataResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListDeviceCommandLogs 负责处理查询设备命令日志列表的业务逻辑 | ||||
| func (s *monitorService) ListDeviceCommandLogs(opts repository.DeviceCommandLogListOptions, page, pageSize int) ([]models.DeviceCommandLog, int64, error) { | ||||
| 	return s.deviceCommandLogRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListDeviceCommandLogs(req *dto.ListDeviceCommandLogRequest) (*dto.ListDeviceCommandLogResponse, error) { | ||||
| 	opts := repository.DeviceCommandLogListOptions{ | ||||
| 		DeviceID:        req.DeviceID, | ||||
| 		ReceivedSuccess: req.ReceivedSuccess, | ||||
| 		OrderBy:         req.OrderBy, | ||||
| 		StartTime:       req.StartTime, | ||||
| 		EndTime:         req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.deviceCommandLogRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListDeviceCommandLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPlanExecutionLogs 负责处理查询计划执行日志列表的业务逻辑 | ||||
| func (s *monitorService) ListPlanExecutionLogs(opts repository.PlanExecutionLogListOptions, page, pageSize int) ([]models.PlanExecutionLog, []models.Plan, int64, error) { | ||||
| 	planLogs, total, err := s.executionLogRepo.ListPlanExecutionLogs(opts, page, pageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| func (s *monitorService) ListPlanExecutionLogs(req *dto.ListPlanExecutionLogRequest) (*dto.ListPlanExecutionLogResponse, error) { | ||||
| 	opts := repository.PlanExecutionLogListOptions{ | ||||
| 		PlanID:    req.PlanID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	planIds := []uint{} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.ExecutionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	planLogs, total, err := s.executionLogRepo.ListPlanExecutionLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	planIds := make([]uint, 0, len(planLogs)) | ||||
| 	for _, datum := range planLogs { | ||||
| 		has := false | ||||
| 		for _, id := range planIds { | ||||
| @@ -111,82 +153,322 @@ func (s *monitorService) ListPlanExecutionLogs(opts repository.PlanExecutionLogL | ||||
| 	} | ||||
| 	plans, err := s.planRepository.GetPlansByIDs(planIds) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return planLogs, plans, total, nil | ||||
| 	return dto.NewListPlanExecutionLogResponse(planLogs, plans, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListTaskExecutionLogs 负责处理查询任务执行日志列表的业务逻辑 | ||||
| func (s *monitorService) ListTaskExecutionLogs(opts repository.TaskExecutionLogListOptions, page, pageSize int) ([]models.TaskExecutionLog, int64, error) { | ||||
| 	return s.executionLogRepo.ListTaskExecutionLogs(opts, page, pageSize) | ||||
| func (s *monitorService) ListTaskExecutionLogs(req *dto.ListTaskExecutionLogRequest) (*dto.ListTaskExecutionLogResponse, error) { | ||||
| 	opts := repository.TaskExecutionLogListOptions{ | ||||
| 		PlanExecutionLogID: req.PlanExecutionLogID, | ||||
| 		TaskID:             req.TaskID, | ||||
| 		OrderBy:            req.OrderBy, | ||||
| 		StartTime:          req.StartTime, | ||||
| 		EndTime:            req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.ExecutionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.executionLogRepo.ListTaskExecutionLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListTaskExecutionLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPendingCollections 负责处理查询待采集请求列表的业务逻辑 | ||||
| func (s *monitorService) ListPendingCollections(opts repository.PendingCollectionListOptions, page, pageSize int) ([]models.PendingCollection, int64, error) { | ||||
| 	return s.pendingCollectionRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListPendingCollections(req *dto.ListPendingCollectionRequest) (*dto.ListPendingCollectionResponse, error) { | ||||
| 	opts := repository.PendingCollectionListOptions{ | ||||
| 		DeviceID:  req.DeviceID, | ||||
| 		OrderBy:   req.OrderBy, | ||||
| 		StartTime: req.StartTime, | ||||
| 		EndTime:   req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.PendingCollectionStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pendingCollectionRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPendingCollectionResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListUserActionLogs 负责处理查询用户操作日志列表的业务逻辑 | ||||
| func (s *monitorService) ListUserActionLogs(opts repository.UserActionLogListOptions, page, pageSize int) ([]models.UserActionLog, int64, error) { | ||||
| 	return s.userActionLogRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListUserActionLogs(req *dto.ListUserActionLogRequest) (*dto.ListUserActionLogResponse, error) { | ||||
| 	opts := repository.UserActionLogListOptions{ | ||||
| 		UserID:     req.UserID, | ||||
| 		Username:   req.Username, | ||||
| 		ActionType: req.ActionType, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.Status != nil { | ||||
| 		status := models.AuditStatus(*req.Status) | ||||
| 		opts.Status = &status | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.userActionLogRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListUserActionLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListRawMaterialPurchases 负责处理查询原料采购记录列表的业务逻辑 | ||||
| func (s *monitorService) ListRawMaterialPurchases(opts repository.RawMaterialPurchaseListOptions, page, pageSize int) ([]models.RawMaterialPurchase, int64, error) { | ||||
| 	return s.rawMaterialRepo.ListRawMaterialPurchases(opts, page, pageSize) | ||||
| func (s *monitorService) ListRawMaterialPurchases(req *dto.ListRawMaterialPurchaseRequest) (*dto.ListRawMaterialPurchaseResponse, error) { | ||||
| 	opts := repository.RawMaterialPurchaseListOptions{ | ||||
| 		RawMaterialID: req.RawMaterialID, | ||||
| 		Supplier:      req.Supplier, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.rawMaterialRepo.ListRawMaterialPurchases(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListRawMaterialPurchaseResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListRawMaterialStockLogs 负责处理查询原料库存日志列表的业务逻辑 | ||||
| func (s *monitorService) ListRawMaterialStockLogs(opts repository.RawMaterialStockLogListOptions, page, pageSize int) ([]models.RawMaterialStockLog, int64, error) { | ||||
| 	return s.rawMaterialRepo.ListRawMaterialStockLogs(opts, page, pageSize) | ||||
| func (s *monitorService) ListRawMaterialStockLogs(req *dto.ListRawMaterialStockLogRequest) (*dto.ListRawMaterialStockLogResponse, error) { | ||||
| 	opts := repository.RawMaterialStockLogListOptions{ | ||||
| 		RawMaterialID: req.RawMaterialID, | ||||
| 		SourceID:      req.SourceID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
| 	if req.SourceType != nil { | ||||
| 		sourceType := models.StockLogSourceType(*req.SourceType) | ||||
| 		opts.SourceType = &sourceType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.rawMaterialRepo.ListRawMaterialStockLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListRawMaterialStockLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListFeedUsageRecords 负责处理查询饲料使用记录列表的业务逻辑 | ||||
| func (s *monitorService) ListFeedUsageRecords(opts repository.FeedUsageRecordListOptions, page, pageSize int) ([]models.FeedUsageRecord, int64, error) { | ||||
| 	return s.rawMaterialRepo.ListFeedUsageRecords(opts, page, pageSize) | ||||
| func (s *monitorService) ListFeedUsageRecords(req *dto.ListFeedUsageRecordRequest) (*dto.ListFeedUsageRecordResponse, error) { | ||||
| 	opts := repository.FeedUsageRecordListOptions{ | ||||
| 		PenID:         req.PenID, | ||||
| 		FeedFormulaID: req.FeedFormulaID, | ||||
| 		OperatorID:    req.OperatorID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.rawMaterialRepo.ListFeedUsageRecords(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListFeedUsageRecordResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListMedicationLogs 负责处理查询用药记录列表的业务逻辑 | ||||
| func (s *monitorService) ListMedicationLogs(opts repository.MedicationLogListOptions, page, pageSize int) ([]models.MedicationLog, int64, error) { | ||||
| 	return s.medicationRepo.ListMedicationLogs(opts, page, pageSize) | ||||
| func (s *monitorService) ListMedicationLogs(req *dto.ListMedicationLogRequest) (*dto.ListMedicationLogResponse, error) { | ||||
| 	opts := repository.MedicationLogListOptions{ | ||||
| 		PigBatchID:   req.PigBatchID, | ||||
| 		MedicationID: req.MedicationID, | ||||
| 		OperatorID:   req.OperatorID, | ||||
| 		OrderBy:      req.OrderBy, | ||||
| 		StartTime:    req.StartTime, | ||||
| 		EndTime:      req.EndTime, | ||||
| 	} | ||||
| 	if req.Reason != nil { | ||||
| 		reason := models.MedicationReasonType(*req.Reason) | ||||
| 		opts.Reason = &reason | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.medicationRepo.ListMedicationLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListMedicationLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPigBatchLogs 负责处理查询猪批次日志列表的业务逻辑 | ||||
| func (s *monitorService) ListPigBatchLogs(opts repository.PigBatchLogListOptions, page, pageSize int) ([]models.PigBatchLog, int64, error) { | ||||
| 	return s.pigBatchLogRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListPigBatchLogs(req *dto.ListPigBatchLogRequest) (*dto.ListPigBatchLogResponse, error) { | ||||
| 	opts := repository.PigBatchLogListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.ChangeType != nil { | ||||
| 		changeType := models.LogChangeType(*req.ChangeType) | ||||
| 		opts.ChangeType = &changeType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigBatchLogRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPigBatchLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListWeighingBatches 负责处理查询批次称重记录列表的业务逻辑 | ||||
| func (s *monitorService) ListWeighingBatches(opts repository.WeighingBatchListOptions, page, pageSize int) ([]models.WeighingBatch, int64, error) { | ||||
| 	return s.pigBatchRepo.ListWeighingBatches(opts, page, pageSize) | ||||
| func (s *monitorService) ListWeighingBatches(req *dto.ListWeighingBatchRequest) (*dto.ListWeighingBatchResponse, error) { | ||||
| 	opts := repository.WeighingBatchListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigBatchRepo.ListWeighingBatches(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListWeighingBatchResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListWeighingRecords 负责处理查询单次称重记录列表的业务逻辑 | ||||
| func (s *monitorService) ListWeighingRecords(opts repository.WeighingRecordListOptions, page, pageSize int) ([]models.WeighingRecord, int64, error) { | ||||
| 	return s.pigBatchRepo.ListWeighingRecords(opts, page, pageSize) | ||||
| func (s *monitorService) ListWeighingRecords(req *dto.ListWeighingRecordRequest) (*dto.ListWeighingRecordResponse, error) { | ||||
| 	opts := repository.WeighingRecordListOptions{ | ||||
| 		WeighingBatchID: req.WeighingBatchID, | ||||
| 		PenID:           req.PenID, | ||||
| 		OperatorID:      req.OperatorID, | ||||
| 		OrderBy:         req.OrderBy, | ||||
| 		StartTime:       req.StartTime, | ||||
| 		EndTime:         req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigBatchRepo.ListWeighingRecords(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListWeighingRecordResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPigTransferLogs 负责处理查询猪只迁移日志列表的业务逻辑 | ||||
| func (s *monitorService) ListPigTransferLogs(opts repository.PigTransferLogListOptions, page, pageSize int) ([]models.PigTransferLog, int64, error) { | ||||
| 	return s.pigTransferLogRepo.ListPigTransferLogs(opts, page, pageSize) | ||||
| func (s *monitorService) ListPigTransferLogs(req *dto.ListPigTransferLogRequest) (*dto.ListPigTransferLogResponse, error) { | ||||
| 	opts := repository.PigTransferLogListOptions{ | ||||
| 		PigBatchID:    req.PigBatchID, | ||||
| 		PenID:         req.PenID, | ||||
| 		OperatorID:    req.OperatorID, | ||||
| 		CorrelationID: req.CorrelationID, | ||||
| 		OrderBy:       req.OrderBy, | ||||
| 		StartTime:     req.StartTime, | ||||
| 		EndTime:       req.EndTime, | ||||
| 	} | ||||
| 	if req.TransferType != nil { | ||||
| 		transferType := models.PigTransferType(*req.TransferType) | ||||
| 		opts.TransferType = &transferType | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigTransferLogRepo.ListPigTransferLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPigTransferLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPigSickLogs 负责处理查询病猪日志列表的业务逻辑 | ||||
| func (s *monitorService) ListPigSickLogs(opts repository.PigSickLogListOptions, page, pageSize int) ([]models.PigSickLog, int64, error) { | ||||
| 	return s.pigSickLogRepo.ListPigSickLogs(opts, page, pageSize) | ||||
| func (s *monitorService) ListPigSickLogs(req *dto.ListPigSickLogRequest) (*dto.ListPigSickLogResponse, error) { | ||||
| 	opts := repository.PigSickLogListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		PenID:      req.PenID, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
| 	if req.Reason != nil { | ||||
| 		reason := models.PigBatchSickPigReasonType(*req.Reason) | ||||
| 		opts.Reason = &reason | ||||
| 	} | ||||
| 	if req.TreatmentLocation != nil { | ||||
| 		treatmentLocation := models.PigBatchSickPigTreatmentLocation(*req.TreatmentLocation) | ||||
| 		opts.TreatmentLocation = &treatmentLocation | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigSickLogRepo.ListPigSickLogs(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPigSickLogResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPigPurchases 负责处理查询猪只采购记录列表的业务逻辑 | ||||
| func (s *monitorService) ListPigPurchases(opts repository.PigPurchaseListOptions, page, pageSize int) ([]models.PigPurchase, int64, error) { | ||||
| 	return s.pigTradeRepo.ListPigPurchases(opts, page, pageSize) | ||||
| func (s *monitorService) ListPigPurchases(req *dto.ListPigPurchaseRequest) (*dto.ListPigPurchaseResponse, error) { | ||||
| 	opts := repository.PigPurchaseListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		Supplier:   req.Supplier, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigTradeRepo.ListPigPurchases(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPigPurchaseResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListPigSales 负责处理查询猪只销售记录列表的业务逻辑 | ||||
| func (s *monitorService) ListPigSales(opts repository.PigSaleListOptions, page, pageSize int) ([]models.PigSale, int64, error) { | ||||
| 	return s.pigTradeRepo.ListPigSales(opts, page, pageSize) | ||||
| func (s *monitorService) ListPigSales(req *dto.ListPigSaleRequest) (*dto.ListPigSaleResponse, error) { | ||||
| 	opts := repository.PigSaleListOptions{ | ||||
| 		PigBatchID: req.PigBatchID, | ||||
| 		Buyer:      req.Buyer, | ||||
| 		OperatorID: req.OperatorID, | ||||
| 		OrderBy:    req.OrderBy, | ||||
| 		StartTime:  req.StartTime, | ||||
| 		EndTime:    req.EndTime, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.pigTradeRepo.ListPigSales(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListPigSaleResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|  | ||||
| // ListNotifications 负责处理查询通知列表的业务逻辑 | ||||
| func (s *monitorService) ListNotifications(opts repository.NotificationListOptions, page, pageSize int) ([]models.Notification, int64, error) { | ||||
| 	return s.notificationRepo.List(opts, page, pageSize) | ||||
| func (s *monitorService) ListNotifications(req *dto.ListNotificationRequest) (*dto.ListNotificationResponse, error) { | ||||
| 	opts := repository.NotificationListOptions{ | ||||
| 		UserID:       req.UserID, | ||||
| 		NotifierType: req.NotifierType, | ||||
| 		Level:        req.Level, | ||||
| 		StartTime:    req.StartTime, | ||||
| 		EndTime:      req.EndTime, | ||||
| 		OrderBy:      req.OrderBy, | ||||
| 		Status:       req.Status, | ||||
| 	} | ||||
|  | ||||
| 	data, total, err := s.notificationRepo.List(opts, req.Page, req.PageSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return dto.NewListNotificationResponse(data, total, req.Page, req.PageSize), nil | ||||
| } | ||||
|   | ||||
| @@ -16,20 +16,20 @@ import ( | ||||
| // 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) | ||||
| 	CreatePigHouse(name, description string) (*dto.PigHouseResponse, error) | ||||
| 	GetPigHouseByID(id uint) (*dto.PigHouseResponse, error) | ||||
| 	ListPigHouses() ([]dto.PigHouseResponse, error) | ||||
| 	UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error) | ||||
| 	DeletePigHouse(id uint) error | ||||
|  | ||||
| 	// Pen methods | ||||
| 	CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) | ||||
| 	CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) | ||||
| 	GetPenByID(id uint) (*dto.PenResponse, error) | ||||
| 	ListPens() ([]*dto.PenResponse, error) | ||||
| 	UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) | ||||
| 	UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) | ||||
| 	DeletePen(id uint) error | ||||
| 	// UpdatePenStatus 更新猪栏状态 | ||||
| 	UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) | ||||
| 	UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error) | ||||
| } | ||||
|  | ||||
| type pigFarmService struct { | ||||
| @@ -60,24 +60,51 @@ func NewPigFarmService(farmRepository repository.PigFarmRepository, | ||||
|  | ||||
| // --- PigHouse Implementation --- | ||||
|  | ||||
| func (s *pigFarmService) CreatePigHouse(name, description string) (*models.PigHouse, error) { | ||||
| func (s *pigFarmService) CreatePigHouse(name, description string) (*dto.PigHouseResponse, error) { | ||||
| 	house := &models.PigHouse{ | ||||
| 		Name:        name, | ||||
| 		Description: description, | ||||
| 	} | ||||
| 	err := s.farmRepository.CreatePigHouse(house) | ||||
| 	return house, err | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &dto.PigHouseResponse{ | ||||
| 		ID:          house.ID, | ||||
| 		Name:        house.Name, | ||||
| 		Description: house.Description, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) GetPigHouseByID(id uint) (*models.PigHouse, error) { | ||||
| 	return s.farmRepository.GetPigHouseByID(id) | ||||
| func (s *pigFarmService) GetPigHouseByID(id uint) (*dto.PigHouseResponse, error) { | ||||
| 	house, err := s.farmRepository.GetPigHouseByID(id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &dto.PigHouseResponse{ | ||||
| 		ID:          house.ID, | ||||
| 		Name:        house.Name, | ||||
| 		Description: house.Description, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) ListPigHouses() ([]models.PigHouse, error) { | ||||
| 	return s.farmRepository.ListPigHouses() | ||||
| func (s *pigFarmService) ListPigHouses() ([]dto.PigHouseResponse, error) { | ||||
| 	houses, err := s.farmRepository.ListPigHouses() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var resp []dto.PigHouseResponse | ||||
| 	for _, house := range houses { | ||||
| 		resp = append(resp, dto.PigHouseResponse{ | ||||
| 			ID:          house.ID, | ||||
| 			Name:        house.Name, | ||||
| 			Description: house.Description, | ||||
| 		}) | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*models.PigHouse, error) { | ||||
| func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*dto.PigHouseResponse, error) { | ||||
| 	house := &models.PigHouse{ | ||||
| 		Model:       gorm.Model{ID: id}, | ||||
| 		Name:        name, | ||||
| @@ -91,7 +118,15 @@ func (s *pigFarmService) UpdatePigHouse(id uint, name, description string) (*mod | ||||
| 		return nil, ErrHouseNotFound | ||||
| 	} | ||||
| 	// 返回更新后的完整信息 | ||||
| 	return s.farmRepository.GetPigHouseByID(id) | ||||
| 	updatedHouse, err := s.farmRepository.GetPigHouseByID(id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &dto.PigHouseResponse{ | ||||
| 		ID:          updatedHouse.ID, | ||||
| 		Name:        updatedHouse.Name, | ||||
| 		Description: updatedHouse.Description, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) DeletePigHouse(id uint) error { | ||||
| @@ -117,7 +152,7 @@ func (s *pigFarmService) DeletePigHouse(id uint) error { | ||||
|  | ||||
| // --- Pen Implementation --- | ||||
|  | ||||
| func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*models.Pen, error) { | ||||
| func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) (*dto.PenResponse, error) { | ||||
| 	// 业务逻辑:验证所属猪舍是否存在 | ||||
| 	_, err := s.farmRepository.GetPigHouseByID(houseID) | ||||
| 	if err != nil { | ||||
| @@ -134,7 +169,16 @@ func (s *pigFarmService) CreatePen(penNumber string, houseID uint, capacity int) | ||||
| 		Status:    models.PenStatusEmpty, | ||||
| 	} | ||||
| 	err = s.penRepository.CreatePen(pen) | ||||
| 	return pen, err | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &dto.PenResponse{ | ||||
| 		ID:        pen.ID, | ||||
| 		PenNumber: pen.PenNumber, | ||||
| 		HouseID:   pen.HouseID, | ||||
| 		Capacity:  pen.Capacity, | ||||
| 		Status:    pen.Status, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) GetPenByID(id uint) (*dto.PenResponse, error) { | ||||
| @@ -197,7 +241,7 @@ func (s *pigFarmService) ListPens() ([]*dto.PenResponse, error) { | ||||
| 	return response, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*models.Pen, error) { | ||||
| func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capacity int, status models.PenStatus) (*dto.PenResponse, error) { | ||||
| 	// 业务逻辑:验证所属猪舍是否存在 | ||||
| 	_, err := s.farmRepository.GetPigHouseByID(houseID) | ||||
| 	if err != nil { | ||||
| @@ -222,7 +266,18 @@ func (s *pigFarmService) UpdatePen(id uint, penNumber string, houseID uint, capa | ||||
| 		return nil, ErrPenNotFound | ||||
| 	} | ||||
| 	// 返回更新后的完整信息 | ||||
| 	return s.penRepository.GetPenByID(id) | ||||
| 	updatedPen, err := s.penRepository.GetPenByID(id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &dto.PenResponse{ | ||||
| 		ID:         updatedPen.ID, | ||||
| 		PenNumber:  updatedPen.PenNumber, | ||||
| 		HouseID:    updatedPen.HouseID, | ||||
| 		Capacity:   updatedPen.Capacity, | ||||
| 		Status:     updatedPen.Status, | ||||
| 		PigBatchID: updatedPen.PigBatchID, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *pigFarmService) DeletePen(id uint) error { | ||||
| @@ -260,7 +315,7 @@ func (s *pigFarmService) DeletePen(id uint) error { | ||||
| } | ||||
|  | ||||
| // UpdatePenStatus 更新猪栏状态 | ||||
| func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*models.Pen, error) { | ||||
| func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (*dto.PenResponse, error) { | ||||
| 	var updatedPen *models.Pen | ||||
| 	err := s.uow.ExecuteInTransaction(func(tx *gorm.DB) error { | ||||
| 		pen, err := s.penRepository.GetPenByIDTx(tx, id) | ||||
| @@ -310,5 +365,12 @@ func (s *pigFarmService) UpdatePenStatus(id uint, newStatus models.PenStatus) (* | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return updatedPen, nil | ||||
| 	return &dto.PenResponse{ | ||||
| 		ID:         updatedPen.ID, | ||||
| 		PenNumber:  updatedPen.PenNumber, | ||||
| 		HouseID:    updatedPen.HouseID, | ||||
| 		Capacity:   updatedPen.Capacity, | ||||
| 		Status:     updatedPen.Status, | ||||
| 		PigBatchID: updatedPen.PigBatchID, | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										344
									
								
								internal/app/service/plan_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								internal/app/service/plan_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,344 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/domain/scheduler" | ||||
| 	"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 ( | ||||
| 	// ErrPlanNotFound 表示未找到计划 | ||||
| 	ErrPlanNotFound = errors.New("计划不存在") | ||||
| 	// ErrPlanCannotBeModified 表示计划不允许修改 | ||||
| 	ErrPlanCannotBeModified = errors.New("系统计划不允许修改") | ||||
| 	// ErrPlanCannotBeDeleted 表示计划不允许删除 | ||||
| 	ErrPlanCannotBeDeleted = errors.New("系统计划不允许删除") | ||||
| 	// ErrPlanCannotBeStarted 表示计划不允许手动启动 | ||||
| 	ErrPlanCannotBeStarted = errors.New("系统计划不允许手动启动") | ||||
| 	// ErrPlanAlreadyEnabled 表示计划已处于启动状态 | ||||
| 	ErrPlanAlreadyEnabled = errors.New("计划已处于启动状态,无需重复操作") | ||||
| 	// ErrPlanNotEnabled 表示计划未处于启动状态 | ||||
| 	ErrPlanNotEnabled = errors.New("计划当前不是启用状态") | ||||
| 	// ErrPlanCannotBeStopped 表示计划不允许停止 | ||||
| 	ErrPlanCannotBeStopped = errors.New("系统计划不允许停止") | ||||
| ) | ||||
|  | ||||
| // PlanService 定义了计划相关的应用服务接口 | ||||
| type PlanService interface { | ||||
| 	// CreatePlan 创建一个新的计划 | ||||
| 	CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) | ||||
| 	// GetPlanByID 根据ID获取计划详情 | ||||
| 	GetPlanByID(id uint) (*dto.PlanResponse, error) | ||||
| 	// ListPlans 获取计划列表,支持过滤和分页 | ||||
| 	ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) | ||||
| 	// UpdatePlan 更新计划 | ||||
| 	UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) | ||||
| 	// DeletePlan 删除计划(软删除) | ||||
| 	DeletePlan(id uint) error | ||||
| 	// StartPlan 启动计划 | ||||
| 	StartPlan(id uint) error | ||||
| 	// StopPlan 停止计划 | ||||
| 	StopPlan(id uint) error | ||||
| } | ||||
|  | ||||
| // planService 是 PlanService 接口的实现 | ||||
| type planService struct { | ||||
| 	logger                  *logs.Logger | ||||
| 	planRepo                repository.PlanRepository | ||||
| 	analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager | ||||
| } | ||||
|  | ||||
| // NewPlanService 创建一个新的 PlanService 实例 | ||||
| func NewPlanService( | ||||
| 	logger *logs.Logger, | ||||
| 	planRepo repository.PlanRepository, | ||||
| 	analysisPlanTaskManager *scheduler.AnalysisPlanTaskManager, | ||||
| ) PlanService { | ||||
| 	return &planService{ | ||||
| 		logger:                  logger, | ||||
| 		planRepo:                planRepo, | ||||
| 		analysisPlanTaskManager: analysisPlanTaskManager, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CreatePlan 创建一个新的计划 | ||||
| func (s *planService) CreatePlan(req *dto.CreatePlanRequest) (*dto.PlanResponse, error) { | ||||
| 	const actionType = "服务层:创建计划" | ||||
|  | ||||
| 	// 使用已有的转换函数,它已经包含了验证和重排逻辑 | ||||
| 	planToCreate, err := dto.NewPlanFromCreateRequest(req) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// --- 业务规则处理 --- | ||||
| 	// 1. 设置计划类型:用户创建的计划永远是自定义计划 | ||||
| 	planToCreate.PlanType = models.PlanTypeCustom | ||||
|  | ||||
| 	// 2. 自动判断 ContentType | ||||
| 	if len(req.SubPlanIDs) > 0 { | ||||
| 		planToCreate.ContentType = models.PlanContentTypeSubPlans | ||||
| 	} else { | ||||
| 		// 如果 SubPlanIDs 未提供,则默认为 Tasks 类型(即使 Tasks 字段也未提供) | ||||
| 		planToCreate.ContentType = models.PlanContentTypeTasks | ||||
| 	} | ||||
|  | ||||
| 	// 调用仓库方法创建计划 | ||||
| 	if err := s.planRepo.CreatePlan(planToCreate); err != nil { | ||||
| 		s.logger.Errorf("%s: 数据库创建计划失败: %v", actionType, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// 创建成功后,调用 manager 确保触发器任务定义存在,但不立即加入待执行队列 | ||||
| 	if err := s.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToCreate.ID); err != nil { | ||||
| 		// 这是一个非阻塞性错误,我们只记录日志,因为主流程(创建计划)已经成功 | ||||
| 		s.logger.Errorf("为新创建的计划 %d 确保触发器任务定义失败: %v", planToCreate.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	// 使用已有的转换函数将创建后的模型转换为响应对象 | ||||
| 	resp, err := dto.NewPlanToResponse(planToCreate) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, planToCreate) | ||||
| 		return nil, errors.New("计划创建成功,但响应生成失败") | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 计划创建成功, ID: %d", actionType, planToCreate.ID) | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| // GetPlanByID 根据ID获取计划详情 | ||||
| func (s *planService) GetPlanByID(id uint) (*dto.PlanResponse, error) { | ||||
| 	const actionType = "服务层:获取计划详情" | ||||
|  | ||||
| 	plan, err := s.planRepo.GetPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return nil, ErrPlanNotFound | ||||
| 		} | ||||
| 		s.logger.Errorf("%s: 数据库查询失败: %v, ID: %d", actionType, err, id) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewPlanToResponse(plan) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 序列化响应失败: %v, Plan: %+v", actionType, err, plan) | ||||
| 		return nil, errors.New("获取计划详情失败: 内部数据格式错误") | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 获取计划详情成功, ID: %d", actionType, id) | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| // ListPlans 获取计划列表,支持过滤和分页 | ||||
| func (s *planService) ListPlans(query *dto.ListPlansQuery) (*dto.ListPlansResponse, error) { | ||||
| 	const actionType = "服务层:获取计划列表" | ||||
|  | ||||
| 	opts := repository.ListPlansOptions{PlanType: query.PlanType} | ||||
| 	plans, total, err := s.planRepo.ListPlans(opts, query.Page, query.PageSize) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	planResponses := make([]dto.PlanResponse, 0, len(plans)) | ||||
| 	for _, p := range plans { | ||||
| 		resp, err := dto.NewPlanToResponse(&p) | ||||
| 		if err != nil { | ||||
| 			s.logger.Errorf("%s: 序列化单个计划响应失败: %v, Plan: %+v", actionType, err, p) | ||||
| 			// 这里选择跳过有问题的计划,并记录错误,而不是中断整个列表的返回 | ||||
| 			continue | ||||
| 		} | ||||
| 		planResponses = append(planResponses, *resp) | ||||
| 	} | ||||
|  | ||||
| 	resp := &dto.ListPlansResponse{ | ||||
| 		Plans: planResponses, | ||||
| 		Total: total, | ||||
| 	} | ||||
| 	s.logger.Infof("%s: 获取计划列表成功, 数量: %d", actionType, len(planResponses)) | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| // UpdatePlan 更新计划 | ||||
| func (s *planService) UpdatePlan(id uint, req *dto.UpdatePlanRequest) (*dto.PlanResponse, error) { | ||||
| 	const actionType = "服务层:更新计划" | ||||
|  | ||||
| 	existingPlan, err := s.planRepo.GetBasicPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return nil, ErrPlanNotFound | ||||
| 		} | ||||
| 		s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if existingPlan.PlanType == models.PlanTypeSystem { | ||||
| 		s.logger.Warnf("%s: 尝试修改系统计划, ID: %d", actionType, id) | ||||
| 		return nil, ErrPlanCannotBeModified | ||||
| 	} | ||||
|  | ||||
| 	planToUpdate, err := dto.NewPlanFromUpdateRequest(req) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 计划数据校验失败: %v", actionType, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	planToUpdate.ID = id // 确保ID被设置 | ||||
|  | ||||
| 	if len(req.SubPlanIDs) > 0 { | ||||
| 		planToUpdate.ContentType = models.PlanContentTypeSubPlans | ||||
| 	} else { | ||||
| 		planToUpdate.ContentType = models.PlanContentTypeTasks | ||||
| 	} | ||||
|  | ||||
| 	// 只要是更新任务,就重置执行计数器 | ||||
| 	planToUpdate.ExecuteCount = 0 | ||||
| 	s.logger.Infof("计划 #%d 被更新,执行计数器已重置为 0。", planToUpdate.ID) | ||||
|  | ||||
| 	if err := s.planRepo.UpdatePlan(planToUpdate); err != nil { | ||||
| 		s.logger.Errorf("%s: 数据库更新计划失败: %v, Plan: %+v", actionType, err, planToUpdate) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.analysisPlanTaskManager.EnsureAnalysisTaskDefinition(planToUpdate.ID); err != nil { | ||||
| 		s.logger.Errorf("为更新后的计划 %d 确保触发器任务定义失败: %v", planToUpdate.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	updatedPlan, err := s.planRepo.GetPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 获取更新后计划详情失败: %v, ID: %d", actionType, err, id) | ||||
| 		return nil, errors.New("获取更新后计划详情时发生内部错误") | ||||
| 	} | ||||
|  | ||||
| 	resp, err := dto.NewPlanToResponse(updatedPlan) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("%s: 序列化响应失败: %v, Updated Plan: %+v", actionType, err, updatedPlan) | ||||
| 		return nil, errors.New("计划更新成功,但响应生成失败") | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 计划更新成功, ID: %d", actionType, updatedPlan.ID) | ||||
| 	return resp, nil | ||||
| } | ||||
|  | ||||
| // DeletePlan 删除计划(软删除) | ||||
| func (s *planService) DeletePlan(id uint) error { | ||||
| 	const actionType = "服务层:删除计划" | ||||
|  | ||||
| 	plan, err := s.planRepo.GetBasicPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return ErrPlanNotFound | ||||
| 		} | ||||
| 		s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		s.logger.Warnf("%s: 尝试删除系统计划, ID: %d", actionType, id) | ||||
| 		return ErrPlanCannotBeDeleted | ||||
| 	} | ||||
|  | ||||
| 	if plan.Status == models.PlanStatusEnabled { | ||||
| 		if err := s.planRepo.StopPlanTransactionally(id); err != nil { | ||||
| 			s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := s.planRepo.DeletePlan(id); err != nil { | ||||
| 		s.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, id) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 计划删除成功, ID: %d", actionType, id) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // StartPlan 启动计划 | ||||
| func (s *planService) StartPlan(id uint) error { | ||||
| 	const actionType = "服务层:启动计划" | ||||
|  | ||||
| 	plan, err := s.planRepo.GetBasicPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return ErrPlanNotFound | ||||
| 		} | ||||
| 		s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		s.logger.Warnf("%s: 尝试手动启动系统计划, ID: %d", actionType, id) | ||||
| 		return ErrPlanCannotBeStarted | ||||
| 	} | ||||
| 	if plan.Status == models.PlanStatusEnabled { | ||||
| 		s.logger.Warnf("%s: 计划已处于启动状态,无需重复操作, ID: %d", actionType, id) | ||||
| 		return ErrPlanAlreadyEnabled | ||||
| 	} | ||||
|  | ||||
| 	if plan.Status != models.PlanStatusEnabled { | ||||
| 		if plan.ExecuteCount > 0 { | ||||
| 			if err := s.planRepo.UpdateExecuteCount(plan.ID, 0); err != nil { | ||||
| 				s.logger.Errorf("%s: 重置计划执行计数失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 				return err | ||||
| 			} | ||||
| 			s.logger.Infof("计划 #%d 的执行计数器已重置为 0。", plan.ID) | ||||
| 		} | ||||
|  | ||||
| 		if err := s.planRepo.UpdatePlanStatus(plan.ID, models.PlanStatusEnabled); err != nil { | ||||
| 			s.logger.Errorf("%s: 更新计划状态失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 			return err | ||||
| 		} | ||||
| 		s.logger.Infof("已成功更新计划 #%d 的状态为 '已启动'。", plan.ID) | ||||
| 	} | ||||
|  | ||||
| 	if err := s.analysisPlanTaskManager.CreateOrUpdateTrigger(plan.ID); err != nil { | ||||
| 		s.logger.Errorf("%s: 创建或更新触发器失败: %v, ID: %d", actionType, err, plan.ID) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 计划已成功启动, ID: %d", actionType, id) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // StopPlan 停止计划 | ||||
| func (s *planService) StopPlan(id uint) error { | ||||
| 	const actionType = "服务层:停止计划" | ||||
|  | ||||
| 	plan, err := s.planRepo.GetBasicPlanByID(id) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			s.logger.Warnf("%s: 计划不存在, ID: %d", actionType, id) | ||||
| 			return ErrPlanNotFound | ||||
| 		} | ||||
| 		s.logger.Errorf("%s: 获取计划信息失败: %v, ID: %d", actionType, err, id) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if plan.PlanType == models.PlanTypeSystem { | ||||
| 		s.logger.Warnf("%s: 尝试停止系统计划, ID: %d", actionType, id) | ||||
| 		return ErrPlanCannotBeStopped | ||||
| 	} | ||||
|  | ||||
| 	if plan.Status != models.PlanStatusEnabled { | ||||
| 		s.logger.Warnf("%s: 计划当前不是启用状态, ID: %d, Status: %s", actionType, id, plan.Status) | ||||
| 		return ErrPlanNotEnabled | ||||
| 	} | ||||
|  | ||||
| 	if err := s.planRepo.StopPlanTransactionally(id); err != nil { | ||||
| 		s.logger.Errorf("%s: 停止计划失败: %v, ID: %d", actionType, err, id) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	s.logger.Infof("%s: 计划已成功停止, ID: %d", actionType, id) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										110
									
								
								internal/app/service/user_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								internal/app/service/user_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/dto" | ||||
| 	domain_notify "git.huangwc.com/pig/pig-farm-controller/internal/domain/notify" | ||||
| 	"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" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // UserService 定义用户服务接口 | ||||
| type UserService interface { | ||||
| 	CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) | ||||
| 	Login(req *dto.LoginRequest) (*dto.LoginResponse, error) | ||||
| 	SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error | ||||
| } | ||||
|  | ||||
| // userService 实现了 UserService 接口 | ||||
| type userService struct { | ||||
| 	userRepo      repository.UserRepository | ||||
| 	tokenService  token.Service | ||||
| 	notifyService domain_notify.Service | ||||
| 	logger        *logs.Logger | ||||
| } | ||||
|  | ||||
| // NewUserService 创建并返回一个新的 UserService 实例 | ||||
| func NewUserService( | ||||
| 	userRepo repository.UserRepository, | ||||
| 	tokenService token.Service, | ||||
| 	notifyService domain_notify.Service, | ||||
| 	logger *logs.Logger, | ||||
| ) UserService { | ||||
| 	return &userService{ | ||||
| 		userRepo:      userRepo, | ||||
| 		tokenService:  tokenService, | ||||
| 		notifyService: notifyService, | ||||
| 		logger:        logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CreateUser 创建新用户 | ||||
| func (s *userService) CreateUser(req *dto.CreateUserRequest) (*dto.CreateUserResponse, error) { | ||||
| 	user := &models.User{ | ||||
| 		Username: req.Username, | ||||
| 		Password: req.Password, // 密码会在 BeforeSave 钩子中哈希 | ||||
| 	} | ||||
|  | ||||
| 	if err := s.userRepo.Create(user); err != nil { | ||||
| 		s.logger.Errorf("创建用户: 创建用户失败: %v", err) | ||||
|  | ||||
| 		// 尝试查询用户,以判断是否是用户名重复导致的错误 | ||||
| 		_, findErr := s.userRepo.FindByUsername(req.Username) | ||||
| 		if findErr == nil { // 如果能找到用户,说明是用户名重复 | ||||
| 			return nil, errors.New("用户名已存在") | ||||
| 		} | ||||
|  | ||||
| 		// 其他创建失败的情况 | ||||
| 		return nil, errors.New("创建用户失败") | ||||
| 	} | ||||
|  | ||||
| 	return &dto.CreateUserResponse{ | ||||
| 		Username: user.Username, | ||||
| 		ID:       user.ID, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Login 用户登录 | ||||
| func (s *userService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) { | ||||
| 	// 使用新的方法,通过唯一标识符(用户名、邮箱等)查找用户 | ||||
| 	user, err := s.userRepo.FindUserForLogin(req.Identifier) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||
| 			return nil, errors.New("登录凭证不正确") | ||||
| 		} | ||||
| 		s.logger.Errorf("登录: 查询用户失败: %v", err) | ||||
| 		return nil, errors.New("登录失败") | ||||
| 	} | ||||
|  | ||||
| 	if !user.CheckPassword(req.Password) { | ||||
| 		return nil, errors.New("登录凭证不正确") | ||||
| 	} | ||||
|  | ||||
| 	// 登录成功,生成 JWT token | ||||
| 	tokenString, err := s.tokenService.GenerateToken(user.ID) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("登录: 生成令牌失败: %v", err) | ||||
| 		return nil, errors.New("登录失败,无法生成认证信息") | ||||
| 	} | ||||
|  | ||||
| 	return &dto.LoginResponse{ | ||||
| 		Username: user.Username, | ||||
| 		ID:       user.ID, | ||||
| 		Token:    tokenString, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // SendTestNotification 发送测试通知 | ||||
| func (s *userService) SendTestNotification(userID uint, req *dto.SendTestNotificationRequest) error { | ||||
| 	err := s.notifyService.SendTestMessage(userID, req.Type) | ||||
| 	if err != nil { | ||||
| 		s.logger.Errorf("发送测试通知: 服务层调用失败: %v", err) | ||||
| 		return errors.New("发送测试消息失败: " + err.Error()) | ||||
| 	} | ||||
| 	s.logger.Infof("发送测试通知: 成功为用户 %d 发送类型为 %s 的测试消息", userID, req.Type) | ||||
| 	return nil | ||||
| } | ||||
| @@ -45,19 +45,15 @@ func NewApplication(configPath string) (*Application, error) { | ||||
| 		cfg.Server, | ||||
| 		logger, | ||||
| 		infra.Repos.UserRepo, | ||||
| 		infra.Repos.DeviceRepo, | ||||
| 		infra.Repos.AreaControllerRepo, | ||||
| 		infra.Repos.DeviceTemplateRepo, | ||||
| 		infra.Repos.PlanRepo, | ||||
| 		appServices.PigFarmService, | ||||
| 		appServices.PigBatchService, | ||||
| 		appServices.MonitorService, | ||||
| 		appServices.DeviceService, | ||||
| 		appServices.PlanService, | ||||
| 		appServices.UserService, | ||||
| 		infra.TokenService, | ||||
| 		appServices.AuditService, | ||||
| 		infra.NotifyService, | ||||
| 		domain.GeneralDeviceService, | ||||
| 		infra.Lora.ListenHandler, | ||||
| 		domain.AnalysisPlanTaskManager, | ||||
| 	) | ||||
|  | ||||
| 	// 4. 组装 Application 对象 | ||||
|   | ||||
| @@ -185,6 +185,9 @@ type AppServices struct { | ||||
| 	PigFarmService  service.PigFarmService | ||||
| 	PigBatchService service.PigBatchService | ||||
| 	MonitorService  service.MonitorService | ||||
| 	DeviceService   service.DeviceService | ||||
| 	PlanService     service.PlanService | ||||
| 	UserService     service.UserService | ||||
| 	AuditService    audit.Service | ||||
| } | ||||
|  | ||||
| @@ -208,13 +211,24 @@ func initAppServices(infra *Infrastructure, domainServices *DomainServices, logg | ||||
| 		infra.Repos.PigTradeRepo, | ||||
| 		infra.Repos.NotificationRepo, | ||||
| 	) | ||||
| 	deviceService := service.NewDeviceService( | ||||
| 		infra.Repos.DeviceRepo, | ||||
| 		infra.Repos.AreaControllerRepo, | ||||
| 		infra.Repos.DeviceTemplateRepo, | ||||
| 		domainServices.GeneralDeviceService, | ||||
| 	) | ||||
| 	auditService := audit.NewService(infra.Repos.UserActionLogRepo, logger) | ||||
| 	planService := service.NewPlanService(logger, infra.Repos.PlanRepo, domainServices.AnalysisPlanTaskManager) | ||||
| 	userService := service.NewUserService(infra.Repos.UserRepo, infra.TokenService, infra.NotifyService, logger) | ||||
|  | ||||
| 	return &AppServices{ | ||||
| 		PigFarmService:  pigFarmService, | ||||
| 		PigBatchService: pigBatchService, | ||||
| 		MonitorService:  monitorService, | ||||
| 		DeviceService:   deviceService, | ||||
| 		AuditService:    auditService, | ||||
| 		PlanService:     planService, | ||||
| 		UserService:     userService, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,61 +0,0 @@ | ||||
| package task_test | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/task" | ||||
| ) | ||||
|  | ||||
| func TestNewDelayTask(t *testing.T) { | ||||
| 	id := "test-delay-task-1" | ||||
| 	duration := 100 * time.Millisecond | ||||
| 	priority := 1 | ||||
|  | ||||
| 	dt := task.NewDelayTask(id, duration, priority) | ||||
|  | ||||
| 	if dt.GetID() != id { | ||||
| 		t.Errorf("期望任务ID为 %s, 实际为 %s", id, dt.GetID()) | ||||
| 	} | ||||
| 	if dt.GetPriority() != priority { | ||||
| 		t.Errorf("期望任务优先级为 %d, 实际为 %d", priority, dt.GetPriority()) | ||||
| 	} | ||||
| 	if dt.IsDone() != false { | ||||
| 		t.Error("任务初始状态不应为已完成") | ||||
| 	} | ||||
| 	// 动态生成的描述,需要匹配 GetDescription 的实现 | ||||
| 	expectedDesc := fmt.Sprintf("延迟任务,ID: %s,延迟时间: %s", id, duration) | ||||
| 	if dt.GetDescription() != expectedDesc { | ||||
| 		t.Errorf("期望任务描述为 %s, 实际为 %s", expectedDesc, dt.GetDescription()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDelayTaskExecute(t *testing.T) { | ||||
| 	id := "test-delay-task-execute" | ||||
| 	duration := 50 * time.Millisecond // 使用较短的延迟以加快测试速度 | ||||
| 	priority := 1 | ||||
|  | ||||
| 	dt := task.NewDelayTask(id, duration, priority) | ||||
|  | ||||
| 	if dt.IsDone() { | ||||
| 		t.Error("任务执行前不应为已完成状态") | ||||
| 	} | ||||
|  | ||||
| 	startTime := time.Now() | ||||
| 	err := dt.Execute() | ||||
| 	endTime := time.Now() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Execute 方法返回错误: %v", err) | ||||
| 	} | ||||
| 	if !dt.IsDone() { | ||||
| 		t.Error("任务执行后应为已完成状态") | ||||
| 	} | ||||
|  | ||||
| 	// 验证延迟时间大致正确,允许一些误差 | ||||
| 	elapsed := endTime.Sub(startTime) | ||||
| 	if elapsed < duration || elapsed > duration*2 { | ||||
| 		t.Errorf("期望执行时间在 %v 左右, 但实际耗时 %v", duration, elapsed) | ||||
| 	} | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| package token_test | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| ) | ||||
|  | ||||
| func TestGenerateToken(t *testing.T) { | ||||
| 	// 使用一个测试密钥初始化 TokenService | ||||
| 	testSecret := []byte("test_secret_key") | ||||
| 	service := token.NewTokenService(testSecret) | ||||
|  | ||||
| 	userID := uint(123) | ||||
| 	tokenString, err := service.GenerateToken(userID) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("生成令牌失败: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if tokenString == "" { | ||||
| 		t.Fatal("生成的令牌字符串为空") | ||||
| 	} | ||||
|  | ||||
| 	// 解析 token 以确保其有效性及声明 | ||||
| 	claims, err := service.ParseToken(tokenString) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("生成后解析令牌失败: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if claims.UserID != userID { | ||||
| 		t.Errorf("期望用户ID %d, 实际为 %d", userID, claims.UserID) | ||||
| 	} | ||||
|  | ||||
| 	// 检查 token 是否未过期 (在合理范围内) | ||||
| 	if claims.ExpiresAt == nil || claims.ExpiresAt.Time.Before(time.Now().Add(-time.Minute)) { | ||||
| 		t.Errorf("令牌过期时间无效或已过期") | ||||
| 	} | ||||
|  | ||||
| 	if claims.Issuer != "pig-farm-controller" { | ||||
| 		t.Errorf("期望签发者 \"pig-farm-controller\", 实际为 \"%s\"", claims.Issuer) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestParseToken(t *testing.T) { | ||||
| 	// 使用两个不同的测试密钥 | ||||
| 	correctSecret := []byte("the_correct_secret") | ||||
| 	wrongSecret := []byte("a_very_wrong_secret") | ||||
|  | ||||
| 	serviceWithCorrectKey := token.NewTokenService(correctSecret) | ||||
| 	serviceWithWrongKey := token.NewTokenService(wrongSecret) | ||||
|  | ||||
| 	userID := uint(456) | ||||
|  | ||||
| 	// 1. 生成一个有效的 token | ||||
| 	validToken, err := serviceWithCorrectKey.GenerateToken(userID) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("为解析测试生成有效令牌失败: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// 测试用例 1: 使用正确的密钥成功解析 | ||||
| 	claims, err := serviceWithCorrectKey.ParseToken(validToken) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("使用正确密钥解析有效令牌失败: %v", err) | ||||
| 	} | ||||
| 	if claims.UserID != userID { | ||||
| 		t.Errorf("解析有效令牌时期望用户ID %d, 实际为 %d", userID, claims.UserID) | ||||
| 	} | ||||
|  | ||||
| 	// 测试用例 2: 无效 token (例如, 格式错误的字符串) | ||||
| 	invalidTokenString := "this.is.not.a.valid.jwt" | ||||
| 	_, err = serviceWithCorrectKey.ParseToken(invalidTokenString) | ||||
| 	if err == nil { | ||||
| 		t.Error("解析格式错误的令牌意外成功") | ||||
| 	} | ||||
|  | ||||
| 	// 测试用C:\Users\divano\Desktop\work\AA-Pig\pig-farm-controller\internal\infra\repository\plan_repository_test.go例 3: 过期 token | ||||
| 	expiredClaims := token.Claims{ | ||||
| 		UserID: userID, | ||||
| 		RegisteredClaims: jwt.RegisteredClaims{ | ||||
| 			ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // 1 小时前 | ||||
| 			Issuer:    "pig-farm-controller", | ||||
| 		}, | ||||
| 	} | ||||
| 	expiredTokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims) | ||||
| 	expiredTokenString, err := expiredTokenClaims.SignedString(correctSecret) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("生成过期令牌失败: %v", err) | ||||
| 	} | ||||
| 	_, err = serviceWithCorrectKey.ParseToken(expiredTokenString) | ||||
| 	if err == nil { | ||||
| 		t.Error("解析过期令牌意外成功") | ||||
| 	} | ||||
|  | ||||
| 	// 新增测试用例 4: 使用错误的密钥解析 | ||||
| 	_, err = serviceWithWrongKey.ParseToken(validToken) | ||||
| 	if err == nil { | ||||
| 		t.Error("使用错误密钥解析令牌意外成功") | ||||
| 	} | ||||
| 	// 我们可以更精确地检查错误类型,以确保它是签名错误 | ||||
| 	if !errors.Is(err, jwt.ErrTokenSignatureInvalid) { | ||||
| 		t.Errorf("期望得到签名无效错误 (ErrTokenSignatureInvalid),但得到了: %v", err) | ||||
| 	} | ||||
| } | ||||
| @@ -1,166 +0,0 @@ | ||||
| package logs_test | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/config" | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"go.uber.org/zap" | ||||
| 	"go.uber.org/zap/zapcore" | ||||
| ) | ||||
|  | ||||
| // captureOutput 是一个辅助函数,用于捕获 logger 的输出到内存缓冲区 | ||||
| func captureOutput(cfg config.LogConfig) (*logs.Logger, *bytes.Buffer) { | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	encoder := logs.GetEncoder(cfg.Format) | ||||
|  | ||||
| 	writer := zapcore.AddSync(&buf) | ||||
|  | ||||
| 	level := zap.NewAtomicLevel() | ||||
| 	_ = level.UnmarshalText([]byte(cfg.Level)) | ||||
|  | ||||
| 	core := zapcore.NewCore(encoder, writer, level) | ||||
| 	// 匹配 logs.go 中 NewLogger 的行为,添加调用者信息 | ||||
| 	zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) | ||||
|  | ||||
| 	logger := &logs.Logger{SugaredLogger: zapLogger.Sugar()} | ||||
| 	return logger, &buf | ||||
| } | ||||
| func TestNewLogger(t *testing.T) { | ||||
| 	t.Run("日志级别应生效", func(t *testing.T) { | ||||
| 		// 1. 创建一个级别为 WARN 的 logger | ||||
| 		logger, buf := captureOutput(config.LogConfig{Level: "warn", Format: "console"}) | ||||
|  | ||||
| 		// 2. 调用不同级别的日志方法 | ||||
| 		logger.Info("这条 info 日志不应被打印") | ||||
| 		logger.Warn("这条 warn 日志应该被打印") | ||||
|  | ||||
| 		// 3. 断言输出 | ||||
| 		output := buf.String() | ||||
| 		assert.NotContains(t, output, "这条 info 日志不应被打印") | ||||
| 		assert.Contains(t, output, "这条 warn 日志应该被打印") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("JSON 格式应生效", func(t *testing.T) { | ||||
| 		// 1. 创建一个格式为 JSON 的 logger | ||||
| 		logger, buf := captureOutput(config.LogConfig{Level: "info", Format: "json"}) | ||||
|  | ||||
| 		// 2. 打印一条日志 | ||||
| 		logger.Info("测试json输出") | ||||
|  | ||||
| 		// 3. 断言输出 | ||||
| 		output := buf.String() | ||||
| 		// 验证它是否是合法的 JSON,并且包含预期的键值对 | ||||
| 		var logEntry map[string]interface{} | ||||
| 		// 注意:由于日志库可能会在行尾添加换行符,我们先 trim space | ||||
| 		err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry) | ||||
| 		assert.NoError(t, err, "日志输出应为合法的JSON") | ||||
| 		assert.Equal(t, "INFO", logEntry["level"]) | ||||
| 		assert.Equal(t, "测试json输出", logEntry["msg"]) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("文件日志构造函数不应 panic", func(t *testing.T) { | ||||
| 		// 这个测试保持原样,只验证构造函数在启用文件时不会崩溃 | ||||
| 		// 注意:我们不在单元测试中实际写入文件 | ||||
| 		cfgFile := config.LogConfig{ | ||||
| 			Level:      "info", | ||||
| 			EnableFile: true, | ||||
| 			FilePath:   "test.log", // 在测试环境中,这个文件不会被真正创建 | ||||
| 		} | ||||
| 		assert.NotPanics(t, func() { logs.NewLogger(cfgFile) }) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestLogger_Write_ForGin(t *testing.T) { | ||||
| 	logger, buf := captureOutput(config.LogConfig{Level: "info"}) | ||||
|  | ||||
| 	ginLog := "[GIN-debug] Listening and serving HTTP on :8080\n" | ||||
| 	_, err := logger.Write([]byte(ginLog)) | ||||
|  | ||||
| 	assert.NoError(t, err) | ||||
| 	output := buf.String() | ||||
| 	// logger.Write 会将 gin 的日志转为 info 级别 | ||||
| 	assert.Contains(t, output, "INFO") | ||||
| 	assert.Contains(t, output, strings.TrimSpace(ginLog)) | ||||
| } | ||||
|  | ||||
| func TestGormLogger(t *testing.T) { | ||||
| 	logger, buf := captureOutput(config.LogConfig{Level: "debug"}) // 设置为 debug 以捕获所有级别 | ||||
| 	gormLogger := logs.NewGormLogger(logger) | ||||
|  | ||||
| 	// 模拟 GORM 的 Trace 调用参数 | ||||
| 	ctx := context.Background() | ||||
| 	sql := "SELECT * FROM users WHERE id = 1" | ||||
| 	rows := int64(1) | ||||
| 	fc := func() (string, int64) { | ||||
| 		return sql, rows | ||||
| 	} | ||||
|  | ||||
| 	t.Run("慢查询应记录为警告", func(t *testing.T) { | ||||
| 		buf.Reset() | ||||
| 		// 模拟一个耗时超过 200ms 的查询 | ||||
| 		begin := time.Now().Add(-300 * time.Millisecond) | ||||
| 		gormLogger.Trace(ctx, begin, fc, nil) | ||||
|  | ||||
| 		output := buf.String() | ||||
| 		assert.Contains(t, output, "WARN", "应包含 WARN 级别") | ||||
| 		assert.Contains(t, output, "[GORM] slow query", "应包含慢查询信息") | ||||
| 		assert.Contains(t, output, "SELECT * FROM users WHERE id = 1", "应包含 SQL 语句") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("普通错误应记录为Error", func(t *testing.T) { | ||||
| 		buf.Reset() | ||||
| 		queryError := errors.New("syntax error") | ||||
| 		gormLogger.Trace(ctx, time.Now(), fc, queryError) | ||||
|  | ||||
| 		output := buf.String() | ||||
| 		assert.Contains(t, output, "ERROR") | ||||
| 		assert.Contains(t, output, "[GORM] error: syntax error") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("当SkipErrRecordNotFound为true时应跳过RecordNotFound错误", func(t *testing.T) { | ||||
| 		buf.Reset() | ||||
| 		// 确保默认设置是 true | ||||
| 		gormLogger.SkipErrRecordNotFound = true | ||||
| 		// 错误必须包含 "record not found" 字符串以匹配 logs.go 中的判断逻辑 | ||||
| 		queryError := errors.New("record not found") | ||||
| 		gormLogger.Trace(ctx, time.Now(), fc, queryError) | ||||
|  | ||||
| 		assert.Empty(t, buf.String(), "开启 SkipErrRecordNotFound 后,record not found 错误不应产生任何日志") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("当SkipErrRecordNotFound为false时应记录RecordNotFound错误", func(t *testing.T) { | ||||
| 		buf.Reset() | ||||
| 		// 手动将 SkipErrRecordNotFound 设置为 false | ||||
| 		gormLogger.SkipErrRecordNotFound = false | ||||
|  | ||||
| 		queryError := errors.New("record not found") | ||||
| 		gormLogger.Trace(ctx, time.Now(), fc, queryError) | ||||
|  | ||||
| 		// 恢复设置,避免影响其他测试 | ||||
| 		gormLogger.SkipErrRecordNotFound = true | ||||
|  | ||||
| 		output := buf.String() | ||||
| 		assert.NotEmpty(t, output, "关闭 SkipErrRecordNotFound 后,record not found 错误应该产生日志") | ||||
| 		assert.Contains(t, output, "ERROR") | ||||
| 		assert.Contains(t, output, "[GORM] error: record not found") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("正常查询应记录为Debug", func(t *testing.T) { | ||||
| 		buf.Reset() | ||||
| 		// 模拟一个快速查询 | ||||
| 		gormLogger.Trace(ctx, time.Now(), fc, nil) | ||||
|  | ||||
| 		output := buf.String() | ||||
| 		assert.Contains(t, output, "DEBUG") // 正常查询是 Debug 级别 | ||||
| 		assert.Contains(t, output, "[GORM] trace") | ||||
| 	}) | ||||
| } | ||||
| @@ -1,202 +0,0 @@ | ||||
| package models_test | ||||
|  | ||||
| import ( | ||||
| 	"sort" | ||||
| 	"testing" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPlan_ReorderSteps(t *testing.T) { | ||||
| 	type testCase struct { | ||||
| 		name           string | ||||
| 		initialPlan    *models.Plan | ||||
| 		expectedOrders []int | ||||
| 	} | ||||
|  | ||||
| 	testCases := []testCase{ | ||||
| 		// --- Test Cases for Tasks --- | ||||
| 		{ | ||||
| 			name: "Tasks: 完美顺序", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 					{ExecutionOrder: 3}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 有间断", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 3}, | ||||
| 					{ExecutionOrder: 5}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 从0开始", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: 0}, | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 完全无序", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: 8}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 					{ExecutionOrder: 4}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 包含负数", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: -5}, | ||||
| 					{ExecutionOrder: 10}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 空切片", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks:       []models.Task{}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Tasks: 单个元素", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeTasks, | ||||
| 				Tasks: []models.Task{ | ||||
| 					{ExecutionOrder: 100}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1}, | ||||
| 		}, | ||||
| 		// --- Test Cases for SubPlans --- | ||||
| 		{ | ||||
| 			name: "SubPlans: 完美顺序", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 					{ExecutionOrder: 3}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 有间断", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 3}, | ||||
| 					{ExecutionOrder: 5}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 从0开始", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: 0}, | ||||
| 					{ExecutionOrder: 1}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 完全无序", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: 8}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 					{ExecutionOrder: 4}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 包含负数", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: -5}, | ||||
| 					{ExecutionOrder: 10}, | ||||
| 					{ExecutionOrder: 2}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1, 2, 3}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 空切片", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans:    []models.SubPlan{}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SubPlans: 单个元素", | ||||
| 			initialPlan: &models.Plan{ | ||||
| 				ContentType: models.PlanContentTypeSubPlans, | ||||
| 				SubPlans: []models.SubPlan{ | ||||
| 					{ExecutionOrder: 100}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedOrders: []int{1}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			// 调用被测试的方法 | ||||
| 			tc.initialPlan.ReorderSteps() | ||||
|  | ||||
| 			// 提取并验证最终的顺序 | ||||
| 			finalOrders := make([]int, 0) | ||||
| 			if tc.initialPlan.ContentType == models.PlanContentTypeTasks { | ||||
| 				for _, task := range tc.initialPlan.Tasks { | ||||
| 					finalOrders = append(finalOrders, task.ExecutionOrder) | ||||
| 				} | ||||
| 			} else if tc.initialPlan.ContentType == models.PlanContentTypeSubPlans { | ||||
| 				for _, subPlan := range tc.initialPlan.SubPlans { | ||||
| 					finalOrders = append(finalOrders, subPlan.ExecutionOrder) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 对 finalOrders 进行排序,以确保比较的一致性,因为 ReorderSteps 后的顺序是固定的 | ||||
| 			sort.Ints(finalOrders) | ||||
|  | ||||
| 			assert.Equal(t, tc.expectedOrders, finalOrders, "The final execution orders should be a continuous sequence starting from 1.") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| // Package models_test 包含对 models 包的单元测试 | ||||
| package models_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| func TestUser_CheckPassword(t *testing.T) { | ||||
| 	plainPassword := "my-secret-password" | ||||
|  | ||||
| 	// 1. 生成一个密码哈希用于测试 | ||||
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) | ||||
| 	assert.NoError(t, err, "生成密码哈希不应出错") | ||||
|  | ||||
| 	user := &models.User{ | ||||
| 		Password: string(hashedPassword), | ||||
| 	} | ||||
|  | ||||
| 	t.Run("密码正确", func(t *testing.T) { | ||||
| 		// 2. 使用正确的明文密码进行校验 | ||||
| 		match := user.CheckPassword(plainPassword) | ||||
| 		assert.True(t, match, "正确的密码应该校验通过") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("密码错误", func(t *testing.T) { | ||||
| 		// 3. 使用错误的明文密码进行校验 | ||||
| 		match := user.CheckPassword("wrong-password") | ||||
| 		assert.False(t, match, "错误的密码应该校验失败") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("空密码", func(t *testing.T) { | ||||
| 		// 4. 使用空字符串作为密码进行校验 | ||||
| 		match := user.CheckPassword("") | ||||
| 		assert.False(t, match, "空密码应该校验失败") | ||||
| 	}) | ||||
| } | ||||
| func TestUser_BeforeCreate(t *testing.T) { | ||||
| 	t.Run("密码应被成功哈希", func(t *testing.T) { | ||||
| 		plainPassword := "securepassword123" | ||||
| 		user := &models.User{ | ||||
| 			Username: "testuser", | ||||
| 			Password: plainPassword, | ||||
| 		} | ||||
|  | ||||
| 		// 模拟 GORM 钩子调用 | ||||
| 		err := user.BeforeCreate(nil) // GORM 钩子通常接收 *gorm.DB,这里我们传入 nil,因为 BeforeCreate 不依赖 DB | ||||
| 		assert.NoError(t, err, "BeforeCreate 不应返回错误") | ||||
|  | ||||
| 		// 验证密码是否已被哈希(不再是明文) | ||||
| 		assert.NotEqual(t, plainPassword, user.Password, "密码应已被哈希") | ||||
|  | ||||
| 		// 验证哈希后的密码是否能被正确校验 | ||||
| 		assert.True(t, user.CheckPassword(plainPassword), "哈希后的密码应能通过校验") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("空密码不应被哈希", func(t *testing.T) { | ||||
| 		plainPassword := "" | ||||
| 		user := &models.User{ | ||||
| 			Username: "empty_pass_user", | ||||
| 			Password: plainPassword, | ||||
| 		} | ||||
|  | ||||
| 		// 模拟 GORM 钩子调用 | ||||
| 		err := user.BeforeCreate(nil) | ||||
| 		assert.NoError(t, err, "BeforeCreate 不应返回错误") | ||||
|  | ||||
| 		// 验证密码仍然是空字符串 | ||||
| 		assert.Equal(t, plainPassword, user.Password, "空密码不应被哈希") | ||||
| 	}) | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package repository_test | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gorm.io/driver/sqlite" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // setupTestDB 是一个共享的辅助函数,用于为集成测试创建一个干净的、内存中的 SQLite 数据库实例。 | ||||
| func setupTestDB(t *testing.T) *gorm.DB { | ||||
| 	// "file::memory:?cache=shared" 是 GORM 连接内存 SQLite 的标准方式,确保在同一测试中的不同连接可以访问相同的数据,而我们显然不需要这个 | ||||
| 	db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) | ||||
| 	assert.NoError(t, err, "连接内存数据库时发生错误") | ||||
|  | ||||
| 	// 自动迁移所有需要的表结构 | ||||
| 	err = db.AutoMigrate(models.GetAllModels()...) | ||||
| 	assert.NoError(t, err, "数据库迁移时发生错误") | ||||
|  | ||||
| 	return db | ||||
| } | ||||
|  | ||||
| // TestMain 是一个特殊的函数,它会在包内的所有测试运行之前被调用。 | ||||
| // 我们可以在这里进行一些全局的设置和清理工作。 | ||||
| func TestMain(m *testing.M) { | ||||
| 	// 在所有测试运行前可以执行一些设置代码 | ||||
|  | ||||
| 	// 运行包中的所有测试 | ||||
| 	code := m.Run() | ||||
|  | ||||
| 	// 在所有测试运行后可以执行一些清理代码 | ||||
|  | ||||
| 	// 退出测试 | ||||
| 	os.Exit(code) | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| // Package repository_test 包含对 repository 包的集成测试 | ||||
| package repository_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| func TestGormUserRepository(t *testing.T) { | ||||
| 	db := setupTestDB(t) | ||||
| 	repo := repository.NewGormUserRepository(db) | ||||
|  | ||||
| 	plainPassword := "my-secret-password" | ||||
| 	userToCreate := &models.User{ | ||||
| 		Username: "testuser", | ||||
| 		Password: plainPassword, // 我们提供的是明文密码 | ||||
| 	} | ||||
|  | ||||
| 	t.Run("创建 - 成功创建并验证密码哈希", func(t *testing.T) { | ||||
| 		err := repo.Create(userToCreate) | ||||
| 		assert.NoError(t, err) | ||||
|  | ||||
| 		// 验证用户已被创建 | ||||
| 		assert.NotZero(t, userToCreate.ID) | ||||
|  | ||||
| 		// 从数据库中直接取回记录,以验证 BeforeSave 钩子是否生效 | ||||
| 		var savedUser models.User | ||||
| 		db.First(&savedUser, userToCreate.ID) | ||||
|  | ||||
| 		// 验证密码字段存储的不是明文 | ||||
| 		assert.NotEqual(t, plainPassword, savedUser.Password, "数据库中存储的密码不应是明文") | ||||
|  | ||||
| 		// 验证存储的哈希是正确的 | ||||
| 		assert.True(t, savedUser.CheckPassword(plainPassword), "存储的密码哈希应该能与原明文匹配") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("创建 - 用户名冲突", func(t *testing.T) { | ||||
| 		// 尝试创建一个同名用户 | ||||
| 		duplicateUser := &models.User{Username: "testuser", Password: "anypassword"} | ||||
| 		err := repo.Create(duplicateUser) | ||||
|  | ||||
| 		// 我们期望一个错误,因为用户名是唯一的 | ||||
| 		assert.Error(t, err, "创建同名用户应该返回错误") | ||||
| 		// 更精确地,可以检查是否是唯一键冲突错误 | ||||
| 		assert.Contains(t, err.Error(), "UNIQUE constraint failed: users.username", "错误信息应包含唯一键冲突") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("按用户名查找 - 找到用户", func(t *testing.T) { | ||||
| 		foundUser, err := repo.FindByUsername("testuser") | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, foundUser) | ||||
| 		assert.Equal(t, userToCreate.ID, foundUser.ID) | ||||
| 		assert.Equal(t, "testuser", foundUser.Username) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("按用户名查找 - 未找到用户", func(t *testing.T) { | ||||
| 		_, err := repo.FindByUsername("nonexistent") | ||||
| 		assert.Error(t, err, "查找不存在的用户应该返回错误") | ||||
| 		assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "错误类型应为 gorm.ErrRecordNotFound") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("按ID查找 - 找到用户", func(t *testing.T) { | ||||
| 		foundUser, err := repo.FindByID(userToCreate.ID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotNil(t, foundUser) | ||||
| 		assert.Equal(t, userToCreate.ID, foundUser.ID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("按ID查找 - 未找到用户", func(t *testing.T) { | ||||
| 		_, err := repo.FindByID(99999) | ||||
| 		assert.Error(t, err, "查找不存在的ID应该返回错误") | ||||
| 		assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "错误类型应为 gorm.ErrRecordNotFound") | ||||
| 	}) | ||||
| } | ||||
| @@ -0,0 +1,414 @@ | ||||
| # `monitor` 模块重构设计 | ||||
|  | ||||
| ## Context | ||||
|  | ||||
| 当前, `monitor` 模块的数据转换逻辑(例如, 将 `repository` 层返回的 `models` 实体转换为 `dto` 对象)主要存在于 | ||||
| `internal/app/controller/monitor/monitor_controller.go` 文件中。 | ||||
|  | ||||
| 这种设计导致了以下问题: | ||||
|  | ||||
| - **职责不清**:控制器层承担了过多的数据处理任务, 违反了“关注点分离”原则。控制器应主要负责处理 HTTP 请求、参数绑定和调用服务, | ||||
|   而非执行业务或数据转换逻辑。 | ||||
| - **代码重复**:如果未来有其他服务需要类似的数据转换, 可能会导致代码重复。 | ||||
| - **可测试性差**:由于转换逻辑与 `echo.Context` 紧密耦合, 对其进行单元测试变得更加复杂。 | ||||
|  | ||||
| ## Goals / Non-Goals | ||||
|  | ||||
| ### Goals | ||||
|  | ||||
| - **迁移数据转换逻辑**:将 `monitor` 模块中所有的数据转换逻辑从控制器层 (`monitor_controller.go`) 迁移到服务层 ( | ||||
|   `monitor_service.go`)。 | ||||
| - **统一服务层接口**:使服务层的方法直接接收请求 DTO, 并返回响应 DTO, 从而使服务本身成为一个完整的、自包含的业务逻辑单元。 | ||||
| - **简化控制器**:精简控制器中的代码, 使其只关注其核心职责:请求处理和响应发送。 | ||||
|  | ||||
| ### Non-Goals | ||||
|  | ||||
| - **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。例如, `ListPlanExecutionLogs` 中获取关联计划信息的逻辑必须保持不变。 | ||||
| - **不改变 API 契约**:API 的请求参数和响应结构对最终用户保持不变。 | ||||
| - **不引入新的依赖**:仅在现有框架和依赖下进行代码调整。 | ||||
|  | ||||
| ## Decisions | ||||
|  | ||||
| - **决策:在服务层完成 DTO 转换** | ||||
|     - **理由**:服务层是封装业务逻辑的核心, 将数据从领域模型 (`models`) 转换为外部表示 (`dto`) | ||||
|       是业务服务的一部分。这样做可以确保任何调用该服务的客户端(无论是控制器、gRPC 服务还是其他服务)都能获得一致的、随时可用的数据结构。 | ||||
|     - **替代方案**:曾考虑在 `dto` 包中创建一个独立的转换层。但最终认为, 将转换逻辑内聚到服务层更能体现其业务属性, | ||||
|       因为服务层最清楚需要暴露哪些数据以及如何组织这些数据。 | ||||
|  | ||||
| - **决策:修改服务层接口以直接处理 DTO** | ||||
|     - **具体实现**:计划将 `MonitorService` 接口中的所有 `List...` 方法签名从 | ||||
|       `ListSomething(opts repository.ListOptions, page, pageSize int) ([]models.Something, int64, error)` 修改为 | ||||
|       `ListSomething(req *dto.ListSomethingRequest) (*dto.ListSomethingResponse, error)`。 | ||||
|     - **理由**:这种设计将极大地简化控制器与服务之间的交互。控制器将不再需要手动构建 `repository.ListOptions` | ||||
|       或在调用服务后手动组装响应 DTO。它只需传递请求 DTO, 然后直接使用服务返回的响应 DTO, 从而实现彻底的解耦。 | ||||
|  | ||||
| ## Risks / Trade-offs | ||||
|  | ||||
| - **风险:意外修改或丢失现有业务逻辑** | ||||
|     - **描述**:在移动代码的过程中, 尤其是像 `ListPlanExecutionLogs` 这样包含特定业务逻辑(获取关联 `plans`)的方法, | ||||
|       存在逻辑被无意中删除或修改的风险。 | ||||
|     - **缓解措施**: | ||||
|         1. **代码审查**:在重构前后仔细比对原有逻辑, 确保其被完整地迁移到了新的服务层方法中。 | ||||
|         2. **保留原有实现**:在新的服务层方法中, 将严格按照控制器中原有的顺序——先构建查询选项, 再调用仓库, | ||||
|            最后进行数据转换——来组织代码, 确保逻辑的等效性。 | ||||
|         3. **测试**:在完成重构后, 必须进行完整的回归测试, 确保所有受影响的 API 端点的行为与重构前完全一致。 | ||||
|  | ||||
| ## Migration Plan | ||||
|  | ||||
| 本次重构将按以下步骤进行: | ||||
|  | ||||
| 1. **修改服务层 (`internal/app/service/monitor_service.go`)** | ||||
|     - **更新接口**:修改 `MonitorService` 接口中所有 `List...` 方法的签名, 使其接收请求 DTO 并返回响应 DTO。 | ||||
|     - **实现数据转换**:在每个 `List...` 方法的实现中, 添加从请求 DTO 到 `repository.ListOptions` 的转换逻辑, 以及从业仓库返回的 | ||||
|       `models` 到响应 DTO 的转换逻辑。对于 `ListPlanExecutionLogs` 等方法, 确保原有的附加业务逻辑(如查询关联 `Plan` | ||||
|       信息)被完整保留。 | ||||
|  | ||||
| 2. **修改控制器层 (`internal/app/controller/monitor/monitor_controller.go`)** | ||||
|     - **移除转换逻辑**:删除所有手动构建 `repository.ListOptions` 和调用 `dto.NewList...Response` 的代码。 | ||||
|     - **更新服务调用**:修改对 `monitorService` 的调用, 使其传递完整的请求 DTO, 并直接处理返回的响应 DTO。 | ||||
|     - **简化日志**:调整日志记录, 以便从服务层返回的 DTO 中获取列表长度和总记录数。 | ||||
|  | ||||
| 3. **验证** | ||||
|     - 通过静态代码分析和审查, 确认代码风格和逻辑的正确性。 | ||||
|     - 进行完整的单元测试和集成测试, 以确保重构没有引入任何回归问题。 | ||||
|  | ||||
| ## Open Questions | ||||
|  | ||||
| - 暂无。 | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## `device` 模块重构设计 | ||||
|  | ||||
| ### Context | ||||
|  | ||||
| `device_controller.go` 当前直接依赖多个 `repository` 和 `domain.Service`,并在其方法内部执行了大量本应属于应用服务层的逻辑,包括: | ||||
|  | ||||
| - **直接的数据库操作**:调用 `repository` 的 `Create`, `Update`, `Delete`, `Find` 等方法。 | ||||
| - **领域模型实例化**:通过 `&models.Device{...}` 直接创建数据库模型。 | ||||
| - **内部字段序列化**:对 `Properties`, `Commands`, `Values` 等字段执行 `json.Marshal`。 | ||||
| - **业务规则验证**:调用 `model.SelfCheck()`。 | ||||
| - **复杂的错误处理**:通过 `errors.Is` 和 `strings.Contains` 解析底层数据库错误。 | ||||
| - **DTO 转换**:在方法末尾调用 `dto.New...Response`。 | ||||
|  | ||||
| 这种设计导致控制器与基础设施层和领域层紧密耦合,违反了分层架构的原则。 | ||||
|  | ||||
| ### Goals / Non-Goals | ||||
|  | ||||
| #### Goals | ||||
|  | ||||
| - **创建应用服务层**:引入一个新的 `internal/app/service/device_service.go` 来封装业务逻辑。 | ||||
| - **迁移业务逻辑**:将上述所有在控制器中识别出的业务逻辑和数据处理任务,全部迁移到新的 `DeviceService` 中。 | ||||
| - **简化控制器**:使 `device_controller.go` 只负责 HTTP 请求处理和对新 `DeviceService` 的调用。 | ||||
| - **保持领域服务纯粹**:确保 `internal/domain/device/device_service.go` 继续专注于核心领域逻辑,不与 DTO 发生耦合。 | ||||
|  | ||||
| #### Non-Goals | ||||
|  | ||||
| - **不改变领域服务**:不对 `domain.device.Service` 的接口和实现进行任何修改。 | ||||
| - **不改变 API 契约**:对外暴露的 API 接口、请求和响应格式保持不变。 | ||||
|  | ||||
| ### Decisions | ||||
|  | ||||
| - **决策:引入新的应用服务 `DeviceService`** | ||||
|     - **理由**:这是解决控制器职责过重和分层不清问题的标准做法。该服务将作为应用层门面,协调 `repository` 和 | ||||
|       `domain.Service`,并为控制器提供一个清晰、稳定的接口。 | ||||
|     - **结构**:`DeviceService` 将依赖于 `DeviceRepository`, `AreaControllerRepository`, `DeviceTemplateRepository` 和 | ||||
|       `domain.device.Service`。 | ||||
|  | ||||
| - **决策:`DeviceService` 接口全面采用 DTO** | ||||
|     - **具体实现**:接口方法将接收 `dto.Create...Request` 等请求 DTO,并返回 `*dto....Response` 响应 DTO。 | ||||
|     - **理由**:这与 `monitor` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。 | ||||
|  | ||||
| ### Migration Plan | ||||
|  | ||||
| 1. **创建 `internal/app/service/device_service.go` 文件** | ||||
|     - 定义 `DeviceService` 接口,为控制器中的每个处理器方法(`CreateDevice`, `UpdateDevice`, `GetDevice`, `ListDevices`, | ||||
|       `DeleteDevice`, `ManualControl` 等)创建相应的方法。 | ||||
|     - 定义 `deviceService` 结构体,并实现 `DeviceService` 接口。 | ||||
|     - **`Create/Update` 方法实现**: | ||||
|         1. 接收请求 DTO。 | ||||
|         2. 执行 `json.Marshal` 转换 `Properties` 等字段。 | ||||
|         3. 创建 `models.Xxx` 实例。 | ||||
|         4. 调用 `model.SelfCheck()`。 | ||||
|         5. 调用 `repository.Create/Update`。 | ||||
|         6. 调用 `repository.FindByID` 重新加载模型(确保关联数据完整)。 | ||||
|         7. 调用 `dto.New...Response` 将模型转换为响应 DTO 并返回。 | ||||
|     - **`Get/List` 方法实现**: | ||||
|         1. 调用 `repository.Find/List`。 | ||||
|         2. 调用 `dto.New...Response` 转换并返回。 | ||||
|     - **`Delete` 方法实现**: | ||||
|         1. 调用 `repository.Delete`。 | ||||
|         2. 捕获并转换特定的“资源被使用”错误。 | ||||
|     - **`ManualControl` 方法实现**: | ||||
|         1. 调用 `repository.FindByIDString` 加载模型。 | ||||
|         2. 实现 `action` 字符串到 `device.DeviceAction` 的映射。 | ||||
|         3. 调用 `domain.device.Service.Switch/Collect`。 | ||||
|  | ||||
| 2. **修改 `internal/app/controller/device/device_controller.go`** | ||||
|     - **更新依赖**:将 `Controller` 的依赖从多个 `repository` 和 `domain.Service` 替换为唯一的 | ||||
|       `app/service.DeviceService`。 | ||||
|     - **简化所有处理器方法**: | ||||
|         1. 移除所有业务逻辑(`json.Marshal`, `SelfCheck`, `repository` 调用, `dto` 转换等)。 | ||||
|         2. 每个方法仅保留:参数绑定、调用 `c.deviceService.Method(req)`、错误处理和成功响应。 | ||||
|  | ||||
| 3. **修改 `internal/core/component_initializers.go`** | ||||
|     - 在 `AppServices` 结构体中增加 `DeviceService service.DeviceService` 字段。 | ||||
|     - 在 `initAppServices` 函数中,调用 `service.NewDeviceService` 创建实例,并将其注入到 `AppServices` 中。 | ||||
|  | ||||
| 4. **修改 `internal/app/api/api.go`** | ||||
|     - 更新 `NewAPI` 函数的参数,使其接收新的 `app/service.DeviceService`。 | ||||
|     - 更新 `device.NewController` 的调用,将多个仓库和领域服务的依赖替换为单一的 `DeviceService` 依赖。 | ||||
|  | ||||
| ### Open Questions | ||||
|  | ||||
| - 暂无。 | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## `pig-farm` 模块重构设计 | ||||
|  | ||||
| ### Context | ||||
|  | ||||
| 与 `monitor` 模块类似, `pig_farm_controller.go` 当前包含了将 `service` 层返回的 `models.PigHouse` 和 `models.Pen` | ||||
| 实体手动转换为 `dto.PigHouseResponse` 和 `dto.PenResponse` 的逻辑。此外, | ||||
| 控制器还处理了部分本应由服务层处理的业务错误判断 (例如 `service.ErrHouseNotFound`)。 | ||||
|  | ||||
| 这种模式导致了与 `monitor` 模块相同的职责不清、代码重复和可测试性差的问题。 | ||||
|  | ||||
| ### Goals / Non-Goals | ||||
|  | ||||
| #### Goals | ||||
|  | ||||
| - **迁移数据转换逻辑**: 将 `pig-farm` 模块中所有的数据转换逻辑从控制器层 (`pig_farm_controller.go`) 迁移到服务层 ( | ||||
|   `pig_farm_service.go`)。 | ||||
| - **统一服务层接口**: 修改 `PigFarmService` 接口, 使其直接返回响应 DTO (`dto.XxxResponse`)。 | ||||
| - **简化控制器**: 精简 `PigFarmController` 中的代码, 移除所有 `models` 到 `dto` 的转换代码, 使其直接使用服务层返回的 | ||||
|   DTO。 | ||||
|  | ||||
| #### Non-Goals | ||||
|  | ||||
| - **不修改业务逻辑**: 本次重构严格保证业务逻辑不变。服务层将精确复制控制器层现有的转换逻辑, 不增加或减少任何字段。 | ||||
| - **不改变 API 契约**: API 的请求和响应对最终用户保持完全一致。 | ||||
|  | ||||
| ### Decisions | ||||
|  | ||||
| - **决策:在服务层完成 `models` 到 `dto` 的转换** | ||||
|     - **理由**: 与其他模块保持一致, 将数据转换视为服务层业务逻辑的一部分。这确保了服务接口的稳定性和调用方的便利性。 | ||||
|     - **具体实现**: `pig_farm_service.go` 中的方法在从 `repository` 获取 `models` 实体后, 将其转换为对应的 `dto` 再返回。 | ||||
|  | ||||
| ### Migration Plan | ||||
|  | ||||
| 1. **修改 `internal/app/service/pig_farm_service.go`** | ||||
|     - **更新 `PigFarmService` 接口**: | ||||
|         - `CreatePigHouse(...) (*models.PigHouse, error)` -> `CreatePigHouse(...) (*dto.PigHouseResponse, error)` | ||||
|         - `GetPigHouseByID(...) (*models.PigHouse, error)` -> `GetPigHouseByID(...) (*dto.PigHouseResponse, error)` | ||||
|         - `ListPigHouses(...) ([]models.PigHouse, error)` -> `ListPigHouses(...) ([]dto.PigHouseResponse, error)` | ||||
|         - `UpdatePigHouse(...) (*models.PigHouse, error)` -> `UpdatePigHouse(...) (*dto.PigHouseResponse, error)` | ||||
|         - `CreatePen(...) (*models.Pen, error)` -> `CreatePen(...) (*dto.PenResponse, error)` | ||||
|         - `UpdatePen(...) (*models.Pen, error)` -> `UpdatePen(...) (*dto.PenResponse, error)` | ||||
|         - `UpdatePenStatus(...) (*models.Pen, error)` -> `UpdatePenStatus(...) (*dto.PenResponse, error)` | ||||
|     - **实现数据转换**: | ||||
|         - 在上述每个方法的实现中, 在从 `repository` 获得 `models` 对象后, 添加代码将其转换为对应的 `dto.XxxResponse` 对象。 | ||||
|         - 转换逻辑将严格按照 `pig_farm_controller.go` 中现有的实现, 确保字段一一对应, 无任何增删。 | ||||
|         - 例如, 在 `UpdatePigHouse` 中: | ||||
|  | ||||
|  | ||||
| 2. **修改 `internal/app/controller/management/pig_farm_controller.go`** | ||||
|     - **移除 DTO 转换代码**: | ||||
|         - 在 `CreatePigHouse`, `GetPigHouse`, `UpdatePigHouse` 方法中, 删除手动创建 `dto.PigHouseResponse` 的代码。 | ||||
|         - 在 `ListPigHouses` 方法中, 删除用于遍历 `houses` 并创建 `[]dto.PigHouseResponse` 的 `for` 循环。 | ||||
|         - 在 `CreatePen`, `UpdatePen`, `UpdatePenStatus` 方法中, 删除手动创建 `dto.PenResponse` 的代码。 | ||||
|     - **更新服务调用**: | ||||
|         - 将服务层返回的 DTO 对象直接传递给 `controller.SendSuccessWithAudit`。 | ||||
|  | ||||
|  | ||||
| 3. **验证** | ||||
|     - 通过代码审查确认转换逻辑被精确迁移。 | ||||
|     - 运行相关测试, 并通过手动 API 测试验证端点行为与重构前完全一致。 | ||||
|  | ||||
| ### Open Questions | ||||
|  | ||||
| - 暂无。 | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## `plan` 模块重构设计 | ||||
|  | ||||
| ### Context | ||||
|  | ||||
| `plan_controller.go` 当前包含了大量的业务逻辑,这违反了控制器层应只负责请求处理和响应发送的原则。具体问题包括: | ||||
|  | ||||
| - **业务规则判断**:控制器中直接判断计划类型(如 `models.PlanTypeSystem`)、计划状态(如 `models.PlanStatusEnabled`)以及 | ||||
|   `ContentType` 的自动判断。 | ||||
| - **领域对象创建与转换**:控制器直接使用 `dto.NewPlanFromCreateRequest` 和 `dto.NewPlanFromUpdateRequest` 将请求 DTO 转换为 | ||||
|   `models.Plan`,并在响应前将 `models.Plan` 转换为 `dto.PlanResponse`。 | ||||
| - **直接调用仓库层**:控制器直接调用 `planRepo` 的 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, | ||||
|   `GetBasicPlanByID`, `UpdatePlanStatus`, `UpdateExecuteCount`, `StopPlanTransactionally` 等方法。 | ||||
| - **协调领域服务**:控制器直接协调 `analysisPlanTaskManager` 的 `EnsureAnalysisTaskDefinition` 和 `CreateOrUpdateTrigger` | ||||
|   方法。 | ||||
| - **错误处理**:控制器直接通过 `errors.Is(err, gorm.ErrRecordNotFound)` 判断仓库层错误,并根据错误类型返回不同的 HTTP 状态码。 | ||||
| - **执行计数器重置**:在 `UpdatePlan` 和 `StartPlan` 中,控制器直接处理 `ExecuteCount` 的重置逻辑。 | ||||
|  | ||||
| 这种设计导致控制器层职责过重,业务逻辑分散,难以维护和测试。 | ||||
|  | ||||
| ### Goals / Non-Goals | ||||
|  | ||||
| #### Goals | ||||
|  | ||||
| - **创建应用服务层**:引入一个新的 `internal/app/service/plan_service.go` 来封装 `plan` 模块的所有业务逻辑。 | ||||
| - **迁移业务逻辑**:将 `plan_controller.go` 中识别出的所有业务规则判断、领域对象创建与转换、对仓库层的直接调用、对 | ||||
|   `analysisPlanTaskManager` 的协调以及错误处理逻辑,全部迁移到新的 `PlanService` 中。 | ||||
| - **简化控制器**:使 `plan_controller.go` 只负责 HTTP 请求处理、参数绑定、调用新的 `PlanService` 方法,并处理服务层返回的 | ||||
|   DTO。 | ||||
| - **统一服务层接口**:`PlanService` 的方法将接收 DTO 作为输入,并返回 DTO 作为输出,实现服务层接口的标准化。 | ||||
|  | ||||
| #### Non-Goals | ||||
|  | ||||
| - **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。所有业务逻辑将原封不动地从控制器迁移到服务层。 | ||||
| - **不改变 API 契约**:对外暴露的 API 接口、请求参数和响应结构对最终用户保持不变。 | ||||
| - **不改变领域服务**:不对 `internal/domain/scheduler/analysis_plan_task_manager.go` 的接口和实现进行任何修改。 | ||||
| - **不改变仓库层接口**:不对 `internal/infra/repository/plan_repository.go` 的接口进行任何修改。 | ||||
|  | ||||
| ### Decisions | ||||
|  | ||||
| - **决策:引入新的应用服务 `PlanService`** | ||||
|     - **理由**:这是解决控制器职责过重和分层不清问题的标准做法。`PlanService` 将作为应用层门面,协调 `PlanRepository` 和 | ||||
|       `AnalysisPlanTaskManager`,并为控制器提供一个清晰、稳定的接口。 | ||||
|     - **结构**:`PlanService` 将依赖于 `PlanRepository` 和 `AnalysisPlanTaskManager`。 | ||||
|  | ||||
| - **决策:`PlanService` 接口全面采用 DTO** | ||||
|     - **具体实现**:接口方法将接收 `dto.CreatePlanRequest`, `dto.UpdatePlanRequest`, `dto.ListPlansQuery` 等请求 DTO,并返回 | ||||
|       `*dto.PlanResponse`, `*dto.ListPlansResponse` 等响应 DTO。 | ||||
|     - **理由**:这与 `monitor`、`device` 和 `pig-farm` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。服务层内部将负责 | ||||
|       `DTO` 到 `models` 的转换以及 `models` 到 `DTO` 的转换。 | ||||
|  | ||||
| - **决策:将控制器中的业务规则判断和错误处理下沉到服务层** | ||||
|     - **理由**:控制器应专注于 HTTP 协议相关的职责。所有业务规则的判断(如计划类型、状态检查、ContentType | ||||
|       自动判断、执行计数器重置)以及对底层错误的具体判断(如 `gorm.ErrRecordNotFound` | ||||
|       )都属于业务逻辑范畴,应由服务层处理。服务层将返回更抽象的业务错误,控制器只需根据这些抽象错误进行统一的 HTTP 响应处理。 | ||||
|  | ||||
| ### Risks / Trade-offs | ||||
|  | ||||
| - **风险:意外修改或丢失现有业务逻辑** | ||||
|     - **描述**:在将控制器中分散的业务逻辑迁移到服务层时,存在逻辑被无意中删除、修改或遗漏的风险,尤其是在处理计划状态转换、执行计数器重置和 | ||||
|       `ContentType` 自动判断等复杂逻辑时。 | ||||
|     - **缓解措施**: | ||||
|         1. **逐行迁移与比对**:在迁移过程中,将控制器中的每一段业务逻辑代码逐行复制到服务层,并仔细比对,确保逻辑的等效性。 | ||||
|         2. **详细注释**:在服务层中对迁移过来的业务逻辑添加详细注释,解释其来源和作用。 | ||||
|         3. **回归测试**:在完成重构后,必须进行完整的回归测试,确保所有受影响的 API 端点的行为与重构前完全一致。 | ||||
|  | ||||
| ### Migration Plan | ||||
|  | ||||
| 1. **创建 `internal/app/service/plan_service.go` 文件**: | ||||
|     - 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, `StartPlan`, | ||||
|       `StopPlan` 等方法。 | ||||
|     - 定义 `planService` 结构体,并实现 `PlanService` 接口。 | ||||
|     - 在 `planService` 的实现中,将 `plan_controller.go` 中所有相关的业务逻辑(包括 DTO 转换、业务规则判断、对 `planRepo` 和 | ||||
|       `analysisPlanTaskManager` 的调用、错误处理)精确迁移到对应的方法中。 | ||||
|  | ||||
| 2. **修改 `internal/app/controller/plan/plan_controller.go`**: | ||||
|     - 更新 `Controller` 结构体,将 `planRepo` 和 `analysisPlanTaskManager` 替换为 `service.PlanService`。 | ||||
|     - 修改 `NewController` 函数,注入 `service.PlanService`。 | ||||
|     - 简化所有处理器方法,移除所有业务逻辑,只保留请求参数绑定、调用 `service.PlanService` 方法、错误处理和响应构建。 | ||||
|  | ||||
| 3. **修改 `internal/core/component_initializers.go`**: | ||||
|     - 在 `AppServices` 结构体中添加 `PlanService service.PlanService` 字段。 | ||||
|     - 在 `initAppServices` 函数中,初始化 `PlanService` 实例,并将其注入到 `AppServices` 中。 | ||||
|  | ||||
| 4. **修改 `internal/app/api/api.go`**: | ||||
|     - 更新 `NewAPI` 函数的参数,移除 `planRepository` 和 `analysisTaskManager`,添加 `service.PlanService`。 | ||||
|     - 更新 `plan.NewController` 的调用,传入新的 `service.PlanService` 依赖。 | ||||
|  | ||||
| ### Open Questions | ||||
|  | ||||
| - 暂无。 | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## `user` 模块重构设计 | ||||
|  | ||||
| ### Context | ||||
|  | ||||
| `user_controller.go` 当前直接依赖 `repository.UserRepository`、`token.Service` 和 `domain_notify.Service` | ||||
| ,并在其方法内部执行了大量本应属于应用服务层的逻辑,包括: | ||||
|  | ||||
| - **直接的数据库操作**:调用 `userRepo` 的 `Create`, `FindByUsername`, `FindUserForLogin` 等方法。 | ||||
| - **领域模型实例化**:通过 `&models.User{...}` 直接创建数据库模型。 | ||||
| - **业务规则验证**:例如在 `CreateUser` 中判断用户名是否重复,在 `Login` 中进行密码验证。 | ||||
| - **协调领域服务**:在 `Login` 中协调 `tokenService` 生成 JWT,在 `SendTestNotification` 中协调 `domain_notify.Service` | ||||
|   发送测试消息。 | ||||
| - **复杂的错误处理**:通过 `errors.Is` 和 `gorm.ErrRecordNotFound` 解析底层错误。 | ||||
| - **DTO 转换**:在方法末尾将 `models.User` 转换为 `dto.CreateUserResponse` 或 `dto.LoginResponse`。 | ||||
|  | ||||
| 这种设计导致控制器与基础设施层和领域层紧密耦合,违反了分层架构的原则。 | ||||
|  | ||||
| ### Goals / Non-Goals | ||||
|  | ||||
| #### Goals | ||||
|  | ||||
| - **创建应用服务层**:引入一个新的 `internal/app/service/user_service.go` 来封装业务逻辑。 | ||||
| - **迁移业务逻辑**:将上述所有在控制器中识别出的业务逻辑和数据处理任务,全部迁移到新的 `UserService` 中。 | ||||
| - **简化控制器**:使 `user_controller.go` 只负责 HTTP 请求处理和对新 `UserService` 的调用。 | ||||
| - **保持领域服务纯粹**:确保 `internal/domain/token.Service` 和 `internal/domain/notify.Service` 继续专注于核心领域逻辑,不与 | ||||
|   DTO 发生耦合。 | ||||
|  | ||||
| #### Non-Goals | ||||
|  | ||||
| - **不修改业务逻辑**:本次重构不涉及任何已有业务规则的变更。所有业务逻辑将原封不动地从控制器迁移到服务层。 | ||||
| - **不改变 API 契约**:对外暴露的 API 接口、请求和响应格式保持不变。 | ||||
| - **不改变领域服务**:不对 `domain.token.Service` 和 `domain.notify.Service` 的接口和实现进行任何修改。 | ||||
| - **不改变仓库层接口**:不对 `internal/infra/repository/user_repository.go` 的接口进行任何修改。 | ||||
| - **不涉及 `ListUserHistory` 方法**:该方法已从重构范围中移除。 | ||||
|  | ||||
| ### Decisions | ||||
|  | ||||
| - **决策:引入新的应用服务 `UserService`** | ||||
|     - **理由**:这是解决控制器职责过重和分层不清问题的标准做法。该服务将作为应用层门面,协调 `UserRepository`、 | ||||
|       `token.Service` 和 `domain_notify.Service`,并为控制器提供一个清晰、稳定的接口。 | ||||
|     - **结构**:`UserService` 将依赖于 `repository.UserRepository`, `token.Service`, `domain_notify.Service` 和 | ||||
|       `logs.Logger`。 | ||||
|  | ||||
| - **决策:`UserService` 接口全面采用 DTO** | ||||
|     - **具体实现**:接口方法将接收 `dto.CreateUserRequest`, `dto.LoginRequest`, `dto.SendTestNotificationRequest` 等请求 | ||||
|       DTO,并返回 `*dto.CreateUserResponse`, `*dto.LoginResponse` 等响应 DTO。 | ||||
|     - **理由**:这与 `monitor`、`device`、`pig-farm` 和 `plan` 模块的重构决策一致,可以确保应用服务层的接口统一、清晰,并与上层(控制器)和下层(领域/仓库)完全解耦。服务层内部将负责 | ||||
|       DTO 到 `models` 的转换以及 `models` 到 DTO 的转换。 | ||||
|  | ||||
| - **决策:将控制器中的业务规则判断和错误处理下沉到服务层** | ||||
|     - **理由**:控制器应专注于 HTTP 协议相关的职责。所有业务规则的判断(如用户名重复检查、密码验证)以及对底层错误的具体判断(如 | ||||
|       `gorm.ErrRecordNotFound`)都属于业务逻辑范畴,应由服务层处理。服务层将返回更抽象的业务错误,控制器只需根据这些抽象错误进行统一的 | ||||
|       HTTP 响应处理。 | ||||
|  | ||||
| ### Risks / Trade-offs | ||||
|  | ||||
| - **风险:意外修改或丢失现有业务逻辑** | ||||
|     - **描述**:在将控制器中分散的业务逻辑迁移到服务层时,存在逻辑被无意中删除、修改或遗漏的风险,尤其是在处理用户创建、登录和通知发送等复杂逻辑时。 | ||||
|     - **缓解措施**: | ||||
|         1. **逐行迁移与比对**:在迁移过程中,将控制器中的每一段业务逻辑代码逐行复制到服务层,并仔细比对,确保逻辑的等效性。 | ||||
|         2. **详细注释**:在服务层中对迁移过来的业务逻辑添加详细注释,解释其来源和作用。 | ||||
|         3. **回归测试**:在完成重构后,必须进行完整的回归测试,确保所有受影响的 API 端点的行为与重构前完全一致。 | ||||
|  | ||||
| ### Migration Plan | ||||
|  | ||||
| 1. **创建 `internal/app/service/user_service.go` 文件**: | ||||
|     - 定义 `UserService` 接口,包含 `CreateUser`, `Login`, `SendTestNotification` 等方法。 | ||||
|     - 定义 `userService` 结构体,并实现 `UserService` 接口。 | ||||
|     - 在 `userService` 的实现中,将 `user_controller.go` 中所有相关的业务逻辑(包括 DTO 转换、业务规则判断、对 `userRepo`、 | ||||
|       `tokenService` 和 `notifyService` 的调用、错误处理)精确迁移到对应的方法中。 | ||||
|  | ||||
| 2. **修改 `internal/app/controller/user/user_controller.go`**: | ||||
|     - 更新 `Controller` 结构体,将 `userRepo`, `tokenService`, `notifyService` 替换为 `service.UserService`。 | ||||
|     - 修改 `NewController` 函数,注入 `service.UserService`。 | ||||
|     - 简化所有处理器方法,移除所有业务逻辑,只保留请求参数绑定、调用 `service.UserService` 方法、错误处理和响应构建。 | ||||
|  | ||||
| 3. **修改 `internal/core/component_initializers.go`**: | ||||
|     - 在 `AppServices` 结构体中添加 `UserService service.UserService` 字段。 | ||||
|     - 在 `initAppServices` 函数中,初始化 `UserService` 实例,并将其注入到 `AppServices` 中。 | ||||
|  | ||||
| 4. **修改 `internal/app/api/api.go`**: | ||||
|     - 更新 `NewAPI` 函数的参数,移除 `userRepo`, `tokenService`, `notifyService`,添加 `service.UserService`。 | ||||
|     - 更新 `user.NewController` 的调用,传入新的 `service.UserService` 依赖。 | ||||
|  | ||||
| ### Open Questions | ||||
|  | ||||
| - 暂无。 | ||||
| @@ -0,0 +1,46 @@ | ||||
| ## Why | ||||
| 当前项目中,控制器层与服务层、仓库层之间存在严重的领域侵入问题。具体表现为: | ||||
| 1.  **服务层直接吐出数据库模型:** 导致控制器层直接感知并操作领域模型,增加了控制器与数据持久化细节的耦合。 | ||||
| 2.  **服务层接收数据库对象或仓库层特定结构:** 控制器层直接构建数据库模型或仓库层查询选项并传递给服务层/仓库层,使得服务层接口不够抽象,且控制器承担了不应有的数据转换职责。 | ||||
| 3.  **业务逻辑散落在控制器层:** 控制器层包含了大量的业务规则判断、领域对象的创建与验证、以及对仓库层和领域服务的直接协调,这违反了控制器层应只做数据校验、绑定解析和调用服务层方法的原则,导致业务逻辑分散、难以维护和测试。 | ||||
|     *   **控制器直接进行领域模型内部字段的序列化/反序列化:** 例如,控制器直接对 `req.Properties` 进行 `json.Marshal` 操作,将领域模型的内部结构(如 JSON 字符串存储)暴露给控制器。 | ||||
|     *   **控制器直接实例化领域模型对象:** 控制器直接通过 `&models.Xxx{...}` 实例化领域模型对象,而非通过服务层进行创建。 | ||||
|     *   **控制器通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断:** 例如,通过 `strings.Contains(err.Error(), "...")` 或 `errors.Is(err, service.ErrXxx)` 来判断具体的业务错误类型,使得控制器与底层实现细节紧密耦合。 | ||||
|  | ||||
| 这些问题导致了代码的紧密耦合、可维护性差、测试困难,并且不利于后续的业务扩展和架构演进。 | ||||
|  | ||||
| ## What Changes | ||||
| 本次重构旨在解决上述领域侵入问题,明确各层的职责,提升代码质量。主要变更包括: | ||||
| -   **服务层接口标准化:** 确保服务层方法只接收 DTO 或基本参数,并只返回 DTO 或业务领域对象(而非数据库模型)。 | ||||
| -   **控制器层职责收敛:** 控制器层将仅负责请求参数的绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都将从控制器层移除并下沉到服务层。 | ||||
|     *   **移除控制器中的领域模型内部字段序列化/反序列化逻辑:** 将此类操作下沉到服务层或专门的转换器中。 | ||||
|     *   **移除控制器中直接实例化领域模型对象的逻辑:** 领域模型的创建应通过服务层完成。 | ||||
|     *   **优化控制器中的业务错误处理:** 服务层将返回更抽象的业务错误,控制器层根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。 | ||||
| -   **DTO 转换逻辑下沉:** 将数据库模型与 DTO 之间的转换逻辑从控制器层移动到服务层内部或专门的转换器中。 | ||||
| -   **业务错误处理优化:** 服务层将返回更抽象的业务错误,控制器层根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型。 | ||||
|  | ||||
| **BREAKING**:本次变更将涉及服务层接口的修改,以及控制器层对服务层调用的调整,可能对依赖这些接口的代码造成影响。 | ||||
|  | ||||
| ## Impact | ||||
| -   **Affected specs:** | ||||
|     -   `specs/monitor/spec.md` (如果存在,需要更新服务层返回 DTO 的要求) | ||||
|     -   `specs/device/spec.md` (如果存在,需要更新服务层返回 DTO 的要求) | ||||
|     -   `specs/pig-farm/spec.md` (如果存在,需要更新服务层返回 DTO 的要求) | ||||
|     -   `specs/plan/spec.md` (如果存在,需要更新服务层返回 DTO 的要求) | ||||
|     -   `specs/user/spec.md` (如果存在,需要更新服务层返回 DTO 的要求) | ||||
| -   **Affected code:** | ||||
|     -   `internal/app/controller/monitor/monitor_controller.go` | ||||
|     -   `internal/app/controller/device/device_controller.go` | ||||
|     -   `internal/app/controller/management/pig_farm_controller.go` | ||||
|     -   `internal/app/controller/plan/plan_controller.go` | ||||
|     -   `internal/app/controller/user/user_controller.go` | ||||
|     -   `internal/app/service/monitor_service.go` (及其实现) | ||||
|     -   `internal/app/service/device_service.go` (及其实现) | ||||
|     -   `internal/app/service/pig_farm_service.go` (及其实现) | ||||
|     -   `internal/app/service/plan_service.go` (及其实现) | ||||
|     -   `internal/app/service/user_service.go` (及其实现) | ||||
|     -   `internal/infra/repository/*.go` (可能需要调整接口,以适应服务层接收 DTO 的变化) | ||||
|     -   `internal/infra/models/*.go` (可能需要添加或修改 DTO 转换方法) | ||||
|     -   `internal/app/dto/*.go` (可能需要添加新的 DTO 或修改现有 DTO 的构造函数) | ||||
|     -   `internal/core/component_initializers.go` | ||||
|     -   `internal/app/api/api.go` | ||||
| @@ -0,0 +1,70 @@ | ||||
| # 业务逻辑分层重构规范 | ||||
|  | ||||
| ## Purpose | ||||
| 本规范旨在明确业务逻辑分层重构的目标、变更内容和预期行为,以解决控制器层职责过重、代码耦合严重、可维护性差的问题。通过本次重构,我们将实现各层职责的清晰划分,提升代码质量和可测试性。 | ||||
|  | ||||
| ## ADDED Requirements | ||||
|  | ||||
| ### Requirement: 服务层接口标准化 | ||||
| - **说明**: 服务层方法现在 **MUST** 只接收数据传输对象 (DTO) 或基本参数,并 **MUST** 只返回 DTO 或业务领域对象,不再直接暴露数据库模型。 | ||||
| - **理由**: 减少服务层与持久化细节的耦合,提高接口的抽象性和稳定性。 | ||||
| - **影响**: 高。所有调用服务层的方法都需要调整。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层方法接收 DTO 作为输入 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法被调用。 | ||||
| - **当**: `CreateUser` 方法接收 `dto.CreateUserRequest` 作为参数。 | ||||
| - **那么**: `UserService` 内部负责将 `dto.CreateUserRequest` 转换为 `models.User` 进行处理。 | ||||
|  | ||||
| #### Scenario: 服务层方法返回 DTO 作为输出 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法执行成功。 | ||||
| - **当**: `CreateUser` 方法返回 `*dto.CreateUserResponse`。 | ||||
| - **那么**: 调用方可以直接使用 `dto.CreateUserResponse`,无需进行额外的模型转换。 | ||||
|  | ||||
| ### Requirement: 控制器层职责收敛 | ||||
| - **说明**: 控制器层现在 **MUST** 仅负责 HTTP 请求的参数绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都 **MUST** 从控制器层移除并下沉到服务层。 | ||||
| - **理由**: 遵循“关注点分离”原则,使控制器层专注于 HTTP 协议处理,提高代码的可维护性和可测试性。 | ||||
| - **影响**: 高。所有控制器方法都需要大幅简化。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接进行领域模型内部字段的序列化/反序列化 | ||||
| - **假如**: `DeviceController` 的 `CreateDevice` 方法被调用。 | ||||
| - **当**: `CreateDevice` 方法不再包含 `json.Marshal` 或 `json.Unmarshal` 等操作来处理 `Properties` 等字段。 | ||||
| - **那么**: 这些序列化/反序列化逻辑已下沉到 `DeviceService` 中。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接实例化领域模型对象 | ||||
| - **假如**: `UserController` 的 `CreateUser` 方法被调用。 | ||||
| - **当**: `CreateUser` 方法不再包含 `&models.User{...}` 这样的代码。 | ||||
| - **那么**: 领域模型的创建已通过 `UserService` 完成。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接调用仓库层方法 | ||||
| - **假如**: `PlanController` 的 `ListPlans` 方法被调用。 | ||||
| - **当**: `ListPlans` 方法不再直接调用 `planRepo.ListPlans`。 | ||||
| - **那么**: `PlanService` 负责协调 `PlanRepository`。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接进行业务规则判断 | ||||
| - **假如**: `PlanController` 的 `UpdatePlan` 方法被调用。 | ||||
| - **当**: `UpdatePlan` 方法不再包含对计划类型、状态或 `ContentType` 的直接判断逻辑。 | ||||
| - **那么**: 这些业务规则判断已下沉到 `PlanService` 中。 | ||||
|  | ||||
| ### Requirement: DTO 转换逻辑下沉 | ||||
| - **说明**: 数据库模型与 DTO 之间的转换逻辑 **MUST** 从控制器层移动到服务层内部或专门的转换器中。 | ||||
| - **理由**: 确保数据转换逻辑与业务逻辑紧密结合,避免控制器层承担不必要的职责。 | ||||
| - **影响**: 中。主要影响数据流转和转换点。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层负责将数据库模型转换为响应 DTO | ||||
| - **假如**: `PigFarmService` 的 `GetPigHouseByID` 方法从 `repository` 获取到 `models.PigHouse`。 | ||||
| - **当**: `GetPigHouseByID` 方法在返回前将 `models.PigHouse` 转换为 `dto.PigHouseResponse`。 | ||||
| - **那么**: 控制器直接接收 `dto.PigHouseResponse`。 | ||||
|  | ||||
| ### Requirement: 业务错误处理优化 | ||||
| - **说明**: 服务层现在 **MUST** 返回更抽象的业务错误,控制器层 **MUST** 根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。 | ||||
| - **理由**: 提高错误处理的一致性和可维护性,解耦控制器与底层错误实现。 | ||||
| - **影响**: 中。影响错误处理流程。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层返回抽象业务错误 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法因用户名重复而失败。 | ||||
| - **当**: `UserService` 返回一个表示“用户名已存在”的抽象错误(例如自定义错误类型或包装后的错误)。 | ||||
| - **那么**: `UserController` 接收到此抽象错误后,可以统一转换为相应的 HTTP 状态码和错误信息,而无需解析底层 `gorm.ErrDuplicatedKey` 等具体错误。 | ||||
| @@ -0,0 +1,123 @@ | ||||
| ## 1. 准备工作 | ||||
|  | ||||
| - [x] 1.1 阅读并理解 `openspec/changes/refactor-business-logic-layering/proposal.md`。 | ||||
| - [x] 1.2 阅读并理解 `openspec/changes/refactor-business-logic-layering/design.md`。 | ||||
| - [x] 1.3 阅读并理解 'AGENTS.md' | ||||
|  | ||||
| ## 2. 统一服务层接口输入输出为 DTO | ||||
|  | ||||
| ### 2.1 `monitor` 模块 | ||||
|  | ||||
| - [x] 2.1.1 **修改 `internal/app/service/monitor_service.go`:** | ||||
|     - [x] 将所有 `List...` 方法的 `opts repository.ListOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。 | ||||
|     - [x] 将所有 `List...` 方法的返回值 `[]models.Xxx` 替换为 `[]dto.XxxResponse`。 | ||||
|     - [x] 调整 `List...` 方法的实现,在服务层内部将服务层查询 DTO 转换为 `repository.ListOptions`。 | ||||
|     - [x] 调整 `List...` 方法的实现,在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。 | ||||
| - [x] 2.1.2 **修改 `internal/app/controller/monitor/monitor_controller.go`:** | ||||
|     - [x] 移除控制器中构建 `repository.ListOptions` 的逻辑。 | ||||
|     - [x] 移除控制器中将 `models` 转换为 `dto.NewList...Response` 的逻辑。 | ||||
|     - [x] 移除控制器中直接使用 `models` 进行枚举类型转换的逻辑,将其下沉到服务层或 DTO 转换逻辑中。 | ||||
|     - [x] 调整服务层方法的调用,使其接收新的服务层查询 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。 | ||||
|  | ||||
| ### 2.2 `device` 模块 | ||||
|  | ||||
| - [x] 2.2.1 **创建并修改 `internal/app/service/device_service.go`:** | ||||
|     - [x] 定义 `DeviceService` 接口,包含 `CreateDevice`, `UpdateDevice`, `CreateAreaController`, `UpdateAreaController`, | ||||
|       `CreateDeviceTemplate`, `UpdateDeviceTemplate`, `GetDevice`, `ListDevices`, `GetAreaController`, | ||||
|       `ListAreaControllers`, `GetDeviceTemplate`, `ListDeviceTemplates`, `ManualControl` 等方法。 | ||||
|     - [x] 为 `CreateDevice`, `UpdateDevice`, `CreateAreaController`, `UpdateAreaController`, `CreateDeviceTemplate`, | ||||
|       `UpdateDeviceTemplate`, `ManualControl` 方法定义并接收 DTO 作为输入。 | ||||
|     - [x] 将 `GetDevice`, `ListDevices`, `GetAreaController`, `ListAreaControllers`, `GetDeviceTemplate`, | ||||
|       `ListDeviceTemplates` 方法的返回值 `models.Xxx` 或 `[]models.Xxx` 替换为 `dto.XxxResponse` 或 `[]dto.XxxResponse`。 | ||||
|     - [x] 实现 `DeviceService` 接口。 | ||||
|     - [x] 在此服务层内部将输入 DTO 转换为 `models` 对象。 | ||||
|     - [x] 在此服务层内部将 `repository` 或 `domain` 层返回的 `models` 对象转换为 `dto.XxxResponse`。 | ||||
|     - [x] 将控制器中 `SelfCheck()` 验证逻辑移入此服务层。 | ||||
|     - [x] 将控制器中 `Properties`, `Commands`, `Values` 的 JSON 序列化逻辑移入此服务层。 | ||||
|     - [x] 将控制器中 `ManualControl` 的业务逻辑(如动作映射)移入此服务层。 | ||||
|     - [x] 将控制器中直接调用 `repository` 方法的逻辑移入此服务层。 | ||||
|     - [x] 将控制器中通过检查 `repository` 错误信息处理业务规则的逻辑移入此服务层。 | ||||
|     - [x] 调整此服务层对 `internal/domain/device.Service` 的调用,确保传递的是 `models` 或领域对象,而不是 DTO。 | ||||
| - [x] 2.2.2 **修改 `internal/app/controller/device/device_controller.go`:** | ||||
|     - [x] 引入并使用新创建的 `internal/app/service.DeviceService`。 | ||||
|     - [x] 移除控制器中直接创建 `models.Device`, `models.AreaController`, `models.DeviceTemplate` 对象的逻辑。 | ||||
|     - [x] 移除控制器中直接调用 `SelfCheck()` 的逻辑。 | ||||
|     - [x] 移除控制器中直接调用 `repository` 方法的逻辑。 | ||||
|     - [x] 移除控制器中通过检查 `repository` 错误信息处理业务规则的逻辑。 | ||||
|     - [x] 移除控制器中 `Properties`, `Commands`, `Values` 的 JSON 序列化逻辑。 | ||||
|     - [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。 | ||||
| - [x] 2.2.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `DeviceService`。 | ||||
| - [x] 2.2.4 **修改 `internal/app/api/api.go`**:更新 `DeviceController` 的依赖注入。 | ||||
|  | ||||
| ### 2.3 `pig-farm` 模块 | ||||
|  | ||||
| - [x] 2.3.1 **修改 `internal/app/service/pig_farm_service.go`:** | ||||
|     - [x] 将 `CreatePigHouse`, `GetPigHouseByID`, `ListPigHouses`, `UpdatePigHouse`, `CreatePen`, `GetPenByID`, | ||||
|       `ListPens`, `UpdatePen`, `UpdatePenStatus` 方法的返回值 `models.Xxx` 或 `[]models.Xxx` 替换为 `dto.XxxResponse` 或 | ||||
|       `[]dto.XxxResponse`。 | ||||
|     - [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。 | ||||
|     - [x] 将控制器中处理服务层特定业务错误(如 `service.ErrHouseNotFound`)的逻辑移入服务层,服务层应返回更抽象的错误或直接返回 | ||||
|       DTO。 | ||||
| - [x] 2.3.2 **修改 `internal/app/controller/management/pig_farm_controller.go`:** | ||||
|     - [x] 移除控制器中手动将领域实体转换为 DTO 的逻辑。 | ||||
|     - [x] 移除控制器中直接处理服务层特定业务错误类型的逻辑。 | ||||
|     - [x] 调整服务层方法的调用,使其直接处理服务层返回的 `dto.XxxResponse`。 | ||||
|  | ||||
| ### 2.4 `plan` 模块 | ||||
|  | ||||
| - [x] 2.4.1 **创建并修改 `internal/app/service/plan_service.go`:** | ||||
|     - [x] 定义 `PlanService` 接口,包含 `CreatePlan`, `GetPlanByID`, `ListPlans`, `UpdatePlan`, `DeletePlan`, | ||||
|       `StartPlan`, `StopPlan` 等方法。 | ||||
|     - [x] 为 `CreatePlan`, `UpdatePlan` 方法定义并接收 DTO 作为输入。 | ||||
|     - [x] 将 `GetPlanByID`, `ListPlans` 方法的返回值 `models.Plan` 或 `[]models.Plan` 替换为 `dto.PlanResponse` 或 | ||||
|       `[]dto.PlanResponse`。 | ||||
|     - [x] 调整 `ListPlans` 方法的 `opts repository.ListPlansOptions` 参数替换为服务层自定义的查询 DTO 或一系列基本参数。 | ||||
|     - [x] 调整 `DeletePlan`, `StartPlan`, `StopPlan` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。 | ||||
|     - [x] 实现 `PlanService` 接口。 | ||||
|     - [x] 在服务层内部将输入 DTO 转换为 `models` 对象。 | ||||
|     - [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。 | ||||
|     - [x] 将 `internal/app/controller/plan/plan_controller.go` 中所有的业务规则判断(计划类型检查、状态检查、执行计数器重置、ContentType | ||||
|       自动判断)移入服务层。 | ||||
|     - [x] 将 `internal/app/controller/plan/plan_controller.go` 中对 `repository` 方法的直接调用移入服务层。 | ||||
|     - [x] 将 `internal/app/controller/plan/plan_controller.go` 中对 `analysisPlanTaskManager` 的协调移入服务层。 | ||||
|     - [x] 将 `internal/app/controller/plan/plan_controller.go` 中处理仓库层特有错误(`gorm.ErrRecordNotFound`)的逻辑移入服务层。 | ||||
| - [x] 2.4.2 **修改 `internal/app/controller/plan/plan_controller.go`:** | ||||
|     - [x] 引入并使用新创建的 `plan_service`。 | ||||
|     - [x] 移除控制器中直接创建 `models.Plan` 对象和 `repository.ListPlansOptions` 的逻辑。 | ||||
|     - [x] 移除控制器中所有的业务规则判断。 | ||||
|     - [x] 移除控制器中直接调用 `repository` 方法的逻辑。 | ||||
|     - [x] 移除控制器中直接协调 `analysisPlanTaskManager` 的逻辑。 | ||||
|     - [x] 移除控制器中直接处理仓库层特有错误的逻辑。 | ||||
|     - [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。 | ||||
| - [x] 2.4.3 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `PlanService`。 | ||||
| - [x] 2.4.4 **修改 `internal/app/api/api.go`**:更新 `PlanController` 的依赖注入。 | ||||
|  | ||||
| ### 2.5 `user` 模块 | ||||
|  | ||||
| - [x] 2.5.1 **创建并修改 `internal/app/service/user_service.go`:** | ||||
|     - [x] 定义 `UserService` 接口,包含 `CreateUser`, `Login`, `SendTestNotification` 等方法。 | ||||
|     - [x] 为 `CreateUser`, `Login` 方法定义并接收 DTO 作为输入。 | ||||
|     - [x] 将 `CreateUser`, `Login` 方法的返回值 `models.User` 替换为 `dto.CreateUserResponse` 或 `dto.LoginResponse`。 | ||||
|     - [x] 调整 `SendTestNotification` 方法,使其接收 DTO 或基本参数,并封装所有业务逻辑。 | ||||
|     - [x] 实现 `UserService` 接口。 | ||||
|     - [x] 在服务层内部将输入 DTO 转换为 `models` 对象。 | ||||
|     - [x] 在服务层内部将 `repository` 返回的 `models` 对象转换为 `dto.XxxResponse`。 | ||||
|     - [x] 将 `CreateUser` 中处理用户名重复的业务逻辑从控制器移入服务层。 | ||||
|     - [x] 将 `Login` 中进行密码验证的业务逻辑和协调 `tokenService` 的逻辑从控制器移入服务层。 | ||||
|     - [x] 将 `SendTestNotification` 中调用 `domain_notify.Service` 的逻辑移入服务层。 | ||||
|     - [x] 将控制器中通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断的逻辑移入服务层。 | ||||
| - [x] 2.5.2 **修改 `internal/app/controller/user/user_controller.go`:** | ||||
|     - [x] 引入并使用新创建的 `user_service`。 | ||||
|     - [x] 移除控制器中直接创建 `models.User` 对象的逻辑。 | ||||
|     - [x] 移除控制器中处理用户名重复的业务逻辑。 | ||||
|     - [x] 移除控制器中进行密码验证的业务逻辑和协调 `tokenService` 的逻辑。 | ||||
|     - [x] 移除控制器中通过检查底层(仓库层或服务层)的特定错误类型或错误信息来执行业务判断的逻辑。 | ||||
|     - [x] 调整服务层方法的调用,使其接收新的服务层输入 DTO 或基本参数,并直接处理服务层返回的 `dto.XxxResponse`。 | ||||
| - [x] 2.5.2 **修改 `internal/core/component_initializers.go`**:创建并提供新的 `UserService`。 | ||||
| - [x] 2.5.3 **修改 `internal/app/api/api.go`**:更新 `UserController` 的依赖注入。 | ||||
|  | ||||
| ## 3. 验证与测试 | ||||
|  | ||||
| - [x] 3.1 运行所有单元测试和集成测试,确保重构没有引入新的问题。 | ||||
| - [x] 3.2 针对受影响的 API 接口进行手动测试,验证功能是否正常。 | ||||
| - [x] 3.3 确保日志输出和审计记录仍然准确无误. | ||||
							
								
								
									
										69
									
								
								openspec/specs/business-logic-layering/spec.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								openspec/specs/business-logic-layering/spec.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| # business-logic-layering Specification | ||||
|  | ||||
| ## Purpose | ||||
| TBD - created by archiving change refactor-business-logic-layering. Update Purpose after archive. | ||||
| ## Requirements | ||||
| ### Requirement: 服务层接口标准化 | ||||
| - **说明**: 服务层方法现在 **MUST** 只接收数据传输对象 (DTO) 或基本参数,并 **MUST** 只返回 DTO 或业务领域对象,不再直接暴露数据库模型。 | ||||
| - **理由**: 减少服务层与持久化细节的耦合,提高接口的抽象性和稳定性。 | ||||
| - **影响**: 高。所有调用服务层的方法都需要调整。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层方法接收 DTO 作为输入 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法被调用。 | ||||
| - **当**: `CreateUser` 方法接收 `dto.CreateUserRequest` 作为参数。 | ||||
| - **那么**: `UserService` 内部负责将 `dto.CreateUserRequest` 转换为 `models.User` 进行处理。 | ||||
|  | ||||
| #### Scenario: 服务层方法返回 DTO 作为输出 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法执行成功。 | ||||
| - **当**: `CreateUser` 方法返回 `*dto.CreateUserResponse`。 | ||||
| - **那么**: 调用方可以直接使用 `dto.CreateUserResponse`,无需进行额外的模型转换。 | ||||
|  | ||||
| ### Requirement: 控制器层职责收敛 | ||||
| - **说明**: 控制器层现在 **MUST** 仅负责 HTTP 请求的参数绑定与校验、调用服务层方法,并将服务层返回的 DTO 转换为 HTTP 响应。所有业务逻辑、领域对象的创建与验证、以及与仓库层的直接交互都 **MUST** 从控制器层移除并下沉到服务层。 | ||||
| - **理由**: 遵循“关注点分离”原则,使控制器层专注于 HTTP 协议处理,提高代码的可维护性和可测试性。 | ||||
| - **影响**: 高。所有控制器方法都需要大幅简化。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接进行领域模型内部字段的序列化/反序列化 | ||||
| - **假如**: `DeviceController` 的 `CreateDevice` 方法被调用。 | ||||
| - **当**: `CreateDevice` 方法不再包含 `json.Marshal` 或 `json.Unmarshal` 等操作来处理 `Properties` 等字段。 | ||||
| - **那么**: 这些序列化/反序列化逻辑已下沉到 `DeviceService` 中。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接实例化领域模型对象 | ||||
| - **假如**: `UserController` 的 `CreateUser` 方法被调用。 | ||||
| - **当**: `CreateUser` 方法不再包含 `&models.User{...}` 这样的代码。 | ||||
| - **那么**: 领域模型的创建已通过 `UserService` 完成。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接调用仓库层方法 | ||||
| - **假如**: `PlanController` 的 `ListPlans` 方法被调用。 | ||||
| - **当**: `ListPlans` 方法不再直接调用 `planRepo.ListPlans`。 | ||||
| - **那么**: `PlanService` 负责协调 `PlanRepository`。 | ||||
|  | ||||
| #### Scenario: 控制器不再直接进行业务规则判断 | ||||
| - **假如**: `PlanController` 的 `UpdatePlan` 方法被调用。 | ||||
| - **当**: `UpdatePlan` 方法不再包含对计划类型、状态或 `ContentType` 的直接判断逻辑。 | ||||
| - **那么**: 这些业务规则判断已下沉到 `PlanService` 中。 | ||||
|  | ||||
| ### Requirement: DTO 转换逻辑下沉 | ||||
| - **说明**: 数据库模型与 DTO 之间的转换逻辑 **MUST** 从控制器层移动到服务层内部或专门的转换器中。 | ||||
| - **理由**: 确保数据转换逻辑与业务逻辑紧密结合,避免控制器层承担不必要的职责。 | ||||
| - **影响**: 中。主要影响数据流转和转换点。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层负责将数据库模型转换为响应 DTO | ||||
| - **假如**: `PigFarmService` 的 `GetPigHouseByID` 方法从 `repository` 获取到 `models.PigHouse`。 | ||||
| - **当**: `GetPigHouseByID` 方法在返回前将 `models.PigHouse` 转换为 `dto.PigHouseResponse`。 | ||||
| - **那么**: 控制器直接接收 `dto.PigHouseResponse`。 | ||||
|  | ||||
| ### Requirement: 业务错误处理优化 | ||||
| - **说明**: 服务层现在 **MUST** 返回更抽象的业务错误,控制器层 **MUST** 根据这些抽象错误进行统一的 HTTP 响应处理,避免直接依赖仓库层或服务层内部的具体错误类型或错误信息。 | ||||
| - **理由**: 提高错误处理的一致性和可维护性,解耦控制器与底层错误实现。 | ||||
| - **影响**: 中。影响错误处理流程。 | ||||
| - **受影响的模块**: `monitor`, `device`, `pig-farm`, `plan`, `user`。 | ||||
|  | ||||
| #### Scenario: 服务层返回抽象业务错误 | ||||
| - **假如**: `UserService` 的 `CreateUser` 方法因用户名重复而失败。 | ||||
| - **当**: `UserService` 返回一个表示“用户名已存在”的抽象错误(例如自定义错误类型或包装后的错误)。 | ||||
| - **那么**: `UserController` 接收到此抽象错误后,可以统一转换为相应的 HTTP 状态码和错误信息,而无需解析底层 `gorm.ErrDuplicatedKey` 等具体错误。 | ||||
|  | ||||
		Reference in New Issue
	
	Block a user