Merge pull request 'issue_49' (#51) from issue_49 into main

Reviewed-on: #51
This commit is contained in:
2025-10-31 17:05:22 +08:00
29 changed files with 2521 additions and 2473 deletions

View File

@@ -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"
]
}
},

View File

@@ -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"
]
}
},

View File

@@ -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:

View File

@@ -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 中初始化猪群控制器

View File

@@ -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("用户相关接口注册成功 (需要认证和审计)")

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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. 发送成功响应

View File

@@ -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})

View 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))
}

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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 对象

View File

@@ -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,
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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")
})
}

View File

@@ -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.")
})
}
}

View File

@@ -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, "空密码不应被哈希")
})
}

View File

@@ -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)
}

View File

@@ -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")
})
}

View File

@@ -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
- 暂无。

View File

@@ -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`

View File

@@ -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` 等具体错误。

View File

@@ -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 确保日志输出和审计记录仍然准确无误.

View 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` 等具体错误。