From 077e866915036c716952d1be334be9b7f057a799 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 30 Sep 2025 22:07:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AE=BE=E5=A4=87=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 356 ++++++++++++++++++ docs/swagger.json | 356 ++++++++++++++++++ docs/swagger.yaml | 232 ++++++++++++ internal/app/api/api.go | 14 +- .../controller/device/device_controller.go | 348 +++++++++++++++++ internal/core/application.go | 4 + .../infra/repository/device_repository.go | 13 + .../repository/device_template_repository.go | 94 +++++ 8 files changed, 1416 insertions(+), 1 deletion(-) create mode 100644 internal/infra/repository/device_template_repository.go diff --git a/docs/docs.go b/docs/docs.go index 56b9975..2116d75 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -213,6 +213,204 @@ const docTemplate = `{ } } }, + "/api/v1/device-templates": { + "get": { + "description": "获取系统中所有设备模板的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备模板", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "创建新设备模板", + "parameters": [ + { + "description": "设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/device.CreateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/device-templates/{id}": { + "get": { + "description": "根据设备模板ID获取单个设备模板的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备模板ID更新一个已存在的设备模板信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "更新设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备模板ID删除一个设备模板(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "删除设备模板", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/devices": { "get": { "description": "获取系统中所有设备的列表", @@ -960,6 +1158,74 @@ const docTemplate = `{ } } }, + "device.CreateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "device.DeviceTemplateResponse": { + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, "device.UpdateAreaControllerRequest": { "type": "object", "required": [ @@ -1008,6 +1274,38 @@ const docTemplate = `{ } } }, + "device.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { "type": "object", "properties": { @@ -1044,6 +1342,17 @@ const docTemplate = `{ } } }, + "models.DeviceCategory": { + "type": "string", + "enum": [ + "actuator", + "sensor" + ], + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] + }, "models.PlanContentType": { "type": "string", "enum": [ @@ -1110,6 +1419,37 @@ const docTemplate = `{ "PlanStatusFailed" ] }, + "models.SensorType": { + "type": "string", + "enum": [ + "signal_metrics", + "battery_level", + "temperature", + "humidity", + "weight" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, "models.TaskType": { "type": "string", "enum": [ @@ -1133,6 +1473,22 @@ const docTemplate = `{ "TaskTypeReleaseFeedWeight" ] }, + "models.ValueDescriptor": { + "type": "object", + "properties": { + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" + }, + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" + }, + "type": { + "$ref": "#/definitions/models.SensorType" + } + } + }, "plan.CreatePlanRequest": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index a5ef27e..1a18dfe 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -202,6 +202,204 @@ } } }, + "/api/v1/device-templates": { + "get": { + "description": "获取系统中所有设备模板的列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "根据提供的信息创建一个新设备模板", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "创建新设备模板", + "parameters": [ + { + "description": "设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/device.CreateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/device-templates/{id}": { + "get": { + "description": "根据设备模板ID获取单个设备模板的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "获取设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "put": { + "description": "根据设备模板ID更新一个已存在的设备模板信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "更新设备模板信息", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "要更新的设备模板信息", + "name": "deviceTemplate", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/device.UpdateDeviceTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/device.DeviceTemplateResponse" + } + } + } + ] + } + } + } + }, + "delete": { + "description": "根据设备模板ID删除一个设备模板(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "设备模板管理" + ], + "summary": "删除设备模板", + "parameters": [ + { + "type": "string", + "description": "设备模板ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + }, "/api/v1/devices": { "get": { "description": "获取系统中所有设备的列表", @@ -949,6 +1147,74 @@ } } }, + "device.CreateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, + "device.DeviceTemplateResponse": { + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, "device.UpdateAreaControllerRequest": { "type": "object", "required": [ @@ -997,6 +1263,38 @@ } } }, + "device.UpdateDeviceTemplateRequest": { + "type": "object", + "required": [ + "category", + "commands", + "name" + ], + "properties": { + "category": { + "$ref": "#/definitions/models.DeviceCategory" + }, + "commands": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueDescriptor" + } + } + } + }, "git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse": { "type": "object", "properties": { @@ -1033,6 +1331,17 @@ } } }, + "models.DeviceCategory": { + "type": "string", + "enum": [ + "actuator", + "sensor" + ], + "x-enum-varnames": [ + "CategoryActuator", + "CategorySensor" + ] + }, "models.PlanContentType": { "type": "string", "enum": [ @@ -1099,6 +1408,37 @@ "PlanStatusFailed" ] }, + "models.SensorType": { + "type": "string", + "enum": [ + "signal_metrics", + "battery_level", + "temperature", + "humidity", + "weight" + ], + "x-enum-comments": { + "SensorTypeBatteryLevel": "电池电量", + "SensorTypeHumidity": "湿度", + "SensorTypeSignalMetrics": "信号强度", + "SensorTypeTemperature": "温度", + "SensorTypeWeight": "重量" + }, + "x-enum-descriptions": [ + "信号强度", + "电池电量", + "温度", + "湿度", + "重量" + ], + "x-enum-varnames": [ + "SensorTypeSignalMetrics", + "SensorTypeBatteryLevel", + "SensorTypeTemperature", + "SensorTypeHumidity", + "SensorTypeWeight" + ] + }, "models.TaskType": { "type": "string", "enum": [ @@ -1122,6 +1462,22 @@ "TaskTypeReleaseFeedWeight" ] }, + "models.ValueDescriptor": { + "type": "object", + "properties": { + "multiplier": { + "description": "乘数,用于原始数据转换", + "type": "number" + }, + "offset": { + "description": "偏移量,用于原始数据转换", + "type": "number" + }, + "type": { + "$ref": "#/definitions/models.SensorType" + } + } + }, "plan.CreatePlanRequest": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8e062b6..25c09d9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -102,6 +102,52 @@ definitions: - device_template_id - name type: object + device.CreateDeviceTemplateRequest: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + description: + type: string + manufacturer: + type: string + name: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + required: + - category + - commands + - name + type: object + device.DeviceTemplateResponse: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + created_at: + type: string + description: + type: string + id: + type: integer + manufacturer: + type: string + name: + type: string + updated_at: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + type: object device.UpdateAreaControllerRequest: properties: location: @@ -135,6 +181,28 @@ definitions: - device_template_id - name type: object + device.UpdateDeviceTemplateRequest: + properties: + category: + $ref: '#/definitions/models.DeviceCategory' + commands: + additionalProperties: true + type: object + description: + type: string + manufacturer: + type: string + name: + type: string + values: + items: + $ref: '#/definitions/models.ValueDescriptor' + type: array + required: + - category + - commands + - name + type: object git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse: properties: area_controller_id: @@ -159,6 +227,14 @@ definitions: updated_at: type: string type: object + models.DeviceCategory: + enum: + - actuator + - sensor + type: string + x-enum-varnames: + - CategoryActuator + - CategorySensor models.PlanContentType: enum: - sub_plans @@ -210,6 +286,32 @@ definitions: - PlanStatusEnabled - PlanStatusStopeed - PlanStatusFailed + models.SensorType: + enum: + - signal_metrics + - battery_level + - temperature + - humidity + - weight + type: string + x-enum-comments: + SensorTypeBatteryLevel: 电池电量 + SensorTypeHumidity: 湿度 + SensorTypeSignalMetrics: 信号强度 + SensorTypeTemperature: 温度 + SensorTypeWeight: 重量 + x-enum-descriptions: + - 信号强度 + - 电池电量 + - 温度 + - 湿度 + - 重量 + x-enum-varnames: + - SensorTypeSignalMetrics + - SensorTypeBatteryLevel + - SensorTypeTemperature + - SensorTypeHumidity + - SensorTypeWeight models.TaskType: enum: - plan_analysis @@ -228,6 +330,17 @@ definitions: - TaskPlanAnalysis - TaskTypeWaiting - TaskTypeReleaseFeedWeight + models.ValueDescriptor: + properties: + multiplier: + description: 乘数,用于原始数据转换 + type: number + offset: + description: 偏移量,用于原始数据转换 + type: number + type: + $ref: '#/definitions/models.SensorType' + type: object plan.CreatePlanRequest: properties: cron_expression: @@ -594,6 +707,125 @@ paths: summary: 更新区域主控信息 tags: - 区域主控管理 + /api/v1/device-templates: + get: + description: 获取系统中所有设备模板的列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + items: + $ref: '#/definitions/device.DeviceTemplateResponse' + type: array + type: object + summary: 获取设备模板列表 + tags: + - 设备模板管理 + post: + consumes: + - application/json + description: 根据提供的信息创建一个新设备模板 + parameters: + - description: 设备模板信息 + in: body + name: deviceTemplate + required: true + schema: + $ref: '#/definitions/device.CreateDeviceTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/device.DeviceTemplateResponse' + type: object + summary: 创建新设备模板 + tags: + - 设备模板管理 + /api/v1/device-templates/{id}: + delete: + description: 根据设备模板ID删除一个设备模板(软删除) + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.Response' + summary: 删除设备模板 + tags: + - 设备模板管理 + get: + description: 根据设备模板ID获取单个设备模板的详细信息 + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/device.DeviceTemplateResponse' + type: object + summary: 获取设备模板信息 + tags: + - 设备模板管理 + put: + consumes: + - application/json + description: 根据设备模板ID更新一个已存在的设备模板信息 + parameters: + - description: 设备模板ID + in: path + name: id + required: true + type: string + - description: 要更新的设备模板信息 + in: body + name: deviceTemplate + required: true + schema: + $ref: '#/definitions/device.UpdateDeviceTemplateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/device.DeviceTemplateResponse' + type: object + summary: 更新设备模板信息 + tags: + - 设备模板管理 /api/v1/devices: get: description: 获取系统中所有设备的列表 diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 6c29ff5..f53a5ed 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -56,6 +56,7 @@ func NewAPI(cfg config.ServerConfig, userRepo repository.UserRepository, deviceRepository repository.DeviceRepository, areaControllerRepository repository.AreaControllerRepository, + deviceTemplateRepository repository.DeviceTemplateRepository, // 添加设备模板仓库 planRepository repository.PlanRepository, userActionLogRepository repository.UserActionLogRepository, tokenService token.TokenService, @@ -86,7 +87,7 @@ func NewAPI(cfg config.ServerConfig, // 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 userController: user.NewController(userRepo, userActionLogRepository, logger, tokenService), // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 - deviceController: device.NewController(deviceRepository, areaControllerRepository, logger), + deviceController: device.NewController(deviceRepository, areaControllerRepository, deviceTemplateRepository, logger), // 在 NewAPI 中初始化计划控制器,并将其作为 API 结构体的成员 planController: plan.NewController(logger, planRepository, analysisTaskManager), } @@ -171,6 +172,17 @@ func (a *API) setupRoutes() { } a.logger.Info("区域主控相关接口注册成功 (需要认证和审计)") + // 设备模板相关路由组 + deviceTemplateGroup := authGroup.Group("/device-templates") + { + deviceTemplateGroup.POST("", a.deviceController.CreateDeviceTemplate) + deviceTemplateGroup.GET("", a.deviceController.ListDeviceTemplates) + deviceTemplateGroup.GET("/:id", a.deviceController.GetDeviceTemplate) + deviceTemplateGroup.PUT("/:id", a.deviceController.UpdateDeviceTemplate) + deviceTemplateGroup.DELETE("/:id", a.deviceController.DeleteDeviceTemplate) + } + a.logger.Info("设备模板相关接口注册成功 (需要认证和审计)") + // 计划相关路由组 planGroup := authGroup.Group("/plans") { diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index f917a87..ddbbc25 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -20,6 +20,7 @@ import ( type Controller struct { deviceRepo repository.DeviceRepository areaControllerRepo repository.AreaControllerRepository + deviceTemplateRepo repository.DeviceTemplateRepository // 设备模板仓库 logger *logs.Logger } @@ -27,11 +28,13 @@ type Controller struct { func NewController( deviceRepo repository.DeviceRepository, areaControllerRepo repository.AreaControllerRepository, + deviceTemplateRepo repository.DeviceTemplateRepository, // 注入设备模板仓库 logger *logs.Logger, ) *Controller { return &Controller{ deviceRepo: deviceRepo, areaControllerRepo: areaControllerRepo, + deviceTemplateRepo: deviceTemplateRepo, // 初始化设备模板仓库 logger: logger, } } @@ -72,6 +75,26 @@ type UpdateAreaControllerRequest struct { Properties map[string]interface{} `json:"properties,omitempty"` } +// CreateDeviceTemplateRequest 定义了创建设备模板时需要传入的参数 +type CreateDeviceTemplateRequest struct { + Name string `json:"name" binding:"required"` + Manufacturer string `json:"manufacturer,omitempty"` + Description string `json:"description,omitempty"` + Category models.DeviceCategory `json:"category" binding:"required"` + Commands map[string]interface{} `json:"commands" binding:"required"` + Values []models.ValueDescriptor `json:"values,omitempty"` +} + +// UpdateDeviceTemplateRequest 定义了更新设备模板时需要传入的参数 +type UpdateDeviceTemplateRequest struct { + Name string `json:"name" binding:"required"` + Manufacturer string `json:"manufacturer,omitempty"` + Description string `json:"description,omitempty"` + Category models.DeviceCategory `json:"category" binding:"required"` + Commands map[string]interface{} `json:"commands" binding:"required"` + Values []models.ValueDescriptor `json:"values,omitempty"` +} + // --- Response DTOs --- // DeviceResponse 定义了返回给客户端的单个设备信息的结构 @@ -100,6 +123,19 @@ type AreaControllerResponse struct { UpdatedAt string `json:"updated_at"` } +// DeviceTemplateResponse 定义了返回给客户端的单个设备模板信息的结构 +type DeviceTemplateResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Manufacturer string `json:"manufacturer"` + Description string `json:"description"` + Category models.DeviceCategory `json:"category"` + Commands map[string]interface{} `json:"commands"` + Values []models.ValueDescriptor `json:"values"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + // --- DTO 转换函数 --- // newDeviceResponse 从数据库模型创建一个新的设备响应 DTO @@ -191,6 +227,50 @@ func newListAreaControllerResponse(acs []*models.AreaController) ([]*AreaControl return list, nil } +// newDeviceTemplateResponse 从数据库模型创建一个新的设备模板响应 DTO +func newDeviceTemplateResponse(dt *models.DeviceTemplate) (*DeviceTemplateResponse, error) { + if dt == nil { + return nil, nil + } + + var commands map[string]interface{} + if err := dt.ParseCommands(&commands); err != nil { + return nil, fmt.Errorf("解析设备模板命令失败 (ID: %d): %w", dt.ID, err) + } + + var values []models.ValueDescriptor + if dt.Category == models.CategorySensor { + if err := dt.ParseValues(&values); err != nil { + return nil, fmt.Errorf("解析设备模板值描述符失败 (ID: %d): %w", dt.ID, err) + } + } + + return &DeviceTemplateResponse{ + ID: dt.ID, + Name: dt.Name, + Manufacturer: dt.Manufacturer, + Description: dt.Description, + Category: dt.Category, + Commands: commands, + Values: values, + CreatedAt: dt.CreatedAt.Format(time.RFC3339), + UpdatedAt: dt.UpdatedAt.Format(time.RFC3339), + }, nil +} + +// newListDeviceTemplateResponse 从数据库模型切片创建一个新的设备模板列表响应 DTO 切片 +func newListDeviceTemplateResponse(dts []*models.DeviceTemplate) ([]*DeviceTemplateResponse, error) { + list := make([]*DeviceTemplateResponse, 0, len(dts)) + for _, dt := range dts { + resp, err := newDeviceTemplateResponse(dt) + if err != nil { + return nil, err + } + list = append(list, resp) + } + return list, nil +} + // --- Controller Methods: Devices --- // CreateDevice godoc @@ -692,3 +772,271 @@ func (c *Controller) DeleteAreaController(ctx *gin.Context) { c.logger.Infof("%s: 区域主控删除成功, ID: %d", actionType, idUint) controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "区域主控删除成功", nil, actionType, "区域主控删除成功", acID) } + +// --- Controller Methods: Device Templates --- + +// CreateDeviceTemplate godoc +// @Summary 创建新设备模板 +// @Description 根据提供的信息创建一个新设备模板 +// @Tags 设备模板管理 +// @Accept json +// @Produce json +// @Param deviceTemplate body CreateDeviceTemplateRequest true "设备模板信息" +// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Router /api/v1/device-templates [post] +func (c *Controller) CreateDeviceTemplate(ctx *gin.Context) { + const actionType = "创建设备模板" + var req CreateDeviceTemplateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) + return + } + + commandsJSON, err := json.Marshal(req.Commands) + if err != nil { + c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands) + return + } + + valuesJSON, err := json.Marshal(req.Values) + if err != nil { + c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values) + return + } + + 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) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", deviceTemplate) + return + } + + if err := c.deviceTemplateRepo.Create(deviceTemplate); err != nil { + c.logger.Errorf("%s: 数据库操作失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "创建设备模板失败: "+err.Error(), actionType, "数据库创建失败", deviceTemplate) + return + } + + resp, err := newDeviceTemplateResponse(deviceTemplate) + if err != nil { + c.logger.Errorf("%s: 序列化响应失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板创建成功,但响应生成失败", actionType, "响应序列化失败", deviceTemplate) + return + } + + c.logger.Infof("%s: 设备模板创建成功, ID: %d", actionType, deviceTemplate.ID) + controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "设备模板创建成功", resp, actionType, "设备模板创建成功", resp) +} + +// GetDeviceTemplate godoc +// @Summary 获取设备模板信息 +// @Description 根据设备模板ID获取单个设备模板的详细信息 +// @Tags 设备模板管理 +// @Produce json +// @Param id path string true "设备模板ID" +// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Router /api/v1/device-templates/{id} [get] +func (c *Controller) GetDeviceTemplate(ctx *gin.Context) { + 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) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) + return + } + + deviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) + return + } + c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: "+err.Error(), actionType, "数据库查询失败", dtID) + return + } + + resp, err := newDeviceTemplateResponse(deviceTemplate) + if err != nil { + c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, deviceTemplate) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板信息失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplate) + return + } + + c.logger.Infof("%s: 获取设备模板信息成功, ID: %d", actionType, deviceTemplate.ID) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板信息成功", resp, actionType, "获取设备模板信息成功", resp) +} + +// ListDeviceTemplates godoc +// @Summary 获取设备模板列表 +// @Description 获取系统中所有设备模板的列表 +// @Tags 设备模板管理 +// @Produce json +// @Success 200 {object} controller.Response{data=[]DeviceTemplateResponse} +// @Router /api/v1/device-templates [get] +func (c *Controller) ListDeviceTemplates(ctx *gin.Context) { + const actionType = "获取设备模板列表" + deviceTemplates, err := c.deviceTemplateRepo.ListAll() + if err != nil { + c.logger.Errorf("%s: 数据库查询失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: "+err.Error(), actionType, "数据库查询失败", nil) + return + } + + resp, err := newListDeviceTemplateResponse(deviceTemplates) + if err != nil { + c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplates: %+v", actionType, err, deviceTemplates) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "获取设备模板列表失败: 内部数据格式错误", actionType, "响应序列化失败", deviceTemplates) + return + } + + c.logger.Infof("%s: 获取设备模板列表成功, 数量: %d", actionType, len(deviceTemplates)) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "获取设备模板列表成功", resp, actionType, "获取设备模板列表成功", resp) +} + +// UpdateDeviceTemplate godoc +// @Summary 更新设备模板信息 +// @Description 根据设备模板ID更新一个已存在的设备模板信息 +// @Tags 设备模板管理 +// @Accept json +// @Produce json +// @Param id path string true "设备模板ID" +// @Param deviceTemplate body UpdateDeviceTemplateRequest true "要更新的设备模板信息" +// @Success 200 {object} controller.Response{data=DeviceTemplateResponse} +// @Router /api/v1/device-templates/{id} [put] +func (c *Controller) UpdateDeviceTemplate(ctx *gin.Context) { + 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) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) + return + } + + existingDeviceTemplate, err := c.deviceTemplateRepo.FindByID(uint(idUint)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) + return + } + c.logger.Errorf("%s: 数据库查询失败: %v, ID: %s", actionType, err, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库查询失败", dtID) + return + } + + var req UpdateDeviceTemplateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("%s: 参数绑定失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的请求体: "+err.Error(), actionType, "请求体绑定失败", req) + return + } + + commandsJSON, err := json.Marshal(req.Commands) + if err != nil { + c.logger.Errorf("%s: 序列化命令失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "命令字段格式错误", actionType, "命令序列化失败", req.Commands) + return + } + + valuesJSON, err := json.Marshal(req.Values) + if err != nil { + c.logger.Errorf("%s: 序列化值描述符失败: %v", actionType, err) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "值描述符字段格式错误", actionType, "值描述符序列化失败", req.Values) + return + } + + 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) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "设备模板参数不符合要求: "+err.Error(), actionType, "设备模板自检失败", existingDeviceTemplate) + return + } + + if err := c.deviceTemplateRepo.Update(existingDeviceTemplate); err != nil { + c.logger.Errorf("%s: 数据库更新失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "更新设备模板失败: "+err.Error(), actionType, "数据库更新失败", existingDeviceTemplate) + return + } + + resp, err := newDeviceTemplateResponse(existingDeviceTemplate) + if err != nil { + c.logger.Errorf("%s: 序列化响应失败: %v, DeviceTemplate: %+v", actionType, err, existingDeviceTemplate) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "设备模板更新成功,但响应生成失败", actionType, "响应序列化失败", existingDeviceTemplate) + return + } + + c.logger.Infof("%s: 设备模板更新成功, ID: %d", actionType, existingDeviceTemplate.ID) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板更新成功", resp, actionType, "设备模板更新成功", resp) +} + +// DeleteDeviceTemplate godoc +// @Summary 删除设备模板 +// @Description 根据设备模板ID删除一个设备模板(软删除) +// @Tags 设备模板管理 +// @Produce json +// @Param id path string true "设备模板ID" +// @Success 200 {object} controller.Response +// @Router /api/v1/device-templates/{id} [delete] +func (c *Controller) DeleteDeviceTemplate(ctx *gin.Context) { + 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) + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的设备模板ID格式", actionType, "ID格式错误", dtID) + return + } + + // 在尝试删除之前,先检查设备模板是否存在 + _, err = c.deviceTemplateRepo.FindByID(uint(idUint)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.logger.Warnf("%s: 设备模板不存在, ID: %s", actionType, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeNotFound, "设备模板未找到", actionType, "设备模板不存在", dtID) + return + } + c.logger.Errorf("%s: 查找设备模板失败: %v, ID: %s", actionType, err, dtID) + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: 查找时发生内部错误", actionType, "数据库查询失败", dtID) + return + } + + // 调用仓库层的删除方法,该方法会检查模板是否被使用 + if err := c.deviceTemplateRepo.Delete(uint(idUint)); err != nil { + c.logger.Errorf("%s: 数据库删除失败: %v, ID: %d", actionType, err, idUint) + // 如果错误信息包含“设备模板正在被设备使用,无法删除”,则返回特定的错误码 + if strings.Contains(err.Error(), "设备模板正在被设备使用,无法删除") { + controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, err.Error(), actionType, "设备模板正在使用", dtID) + } else { + // 其他数据库错误 + controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "删除设备模板失败: "+err.Error(), actionType, "数据库删除失败", dtID) + } + return + } + + c.logger.Infof("%s: 设备模板删除成功, ID: %d", actionType, idUint) + controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "设备模板删除成功", nil, actionType, "设备模板删除成功", dtID) +} diff --git a/internal/core/application.go b/internal/core/application.go index 0b317de..a68c4f2 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -67,6 +67,9 @@ func NewApplication(configPath string) (*Application, error) { // 初始化区域主控仓库 areaControllerRepo := repository.NewGormAreaControllerRepository(storage.GetDB()) + // 初始化设备模板仓库 + deviceTemplateRepo := repository.NewGormDeviceTemplateRepository(storage.GetDB()) + // 初始化计划仓库 planRepo := repository.NewGormPlanRepository(storage.GetDB()) @@ -130,6 +133,7 @@ func NewApplication(configPath string) (*Application, error) { userRepo, deviceRepo, areaControllerRepo, + deviceTemplateRepo, planRepo, userActionLogRepo, tokenService, diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index b8f42ca..c22a584 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -26,6 +26,9 @@ type DeviceRepository interface { // ListByAreaControllerID 根据区域主控 ID 列出所有子设备。 ListByAreaControllerID(areaControllerID uint) ([]*models.Device, error) + // FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备 + FindByDeviceTemplateID(deviceTemplateID uint) ([]*models.Device, error) + // Update 更新一个已有的设备信息 Update(device *models.Device) error @@ -91,6 +94,16 @@ func (r *gormDeviceRepository) ListByAreaControllerID(areaControllerID uint) ([] return devices, nil } +// FindByDeviceTemplateID 根据设备模板ID查找所有使用该模板的设备 +func (r *gormDeviceRepository) FindByDeviceTemplateID(deviceTemplateID uint) ([]*models.Device, error) { + var devices []*models.Device + err := r.db.Where("device_template_id = ?", deviceTemplateID).Find(&devices).Error + if err != nil { + return nil, fmt.Errorf("查询使用设备模板ID %d 的设备失败: %w", deviceTemplateID, err) + } + return devices, nil +} + // Update 更新一个已有的设备信息 // GORM 的 Save 方法会自动处理主键存在时更新,不存在时创建的逻辑,但这里我们明确用于更新。 func (r *gormDeviceRepository) Update(device *models.Device) error { diff --git a/internal/infra/repository/device_template_repository.go b/internal/infra/repository/device_template_repository.go new file mode 100644 index 0000000..442ca3b --- /dev/null +++ b/internal/infra/repository/device_template_repository.go @@ -0,0 +1,94 @@ +package repository + +import ( + "errors" + "fmt" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "gorm.io/gorm" +) + +// DeviceTemplateRepository 定义了设备模板数据访问的接口 +type DeviceTemplateRepository interface { + Create(deviceTemplate *models.DeviceTemplate) error + FindByID(id uint) (*models.DeviceTemplate, error) + FindByName(name string) (*models.DeviceTemplate, error) + ListAll() ([]*models.DeviceTemplate, error) + Update(deviceTemplate *models.DeviceTemplate) error + Delete(id uint) error + IsInUse(id uint) (bool, error) +} + +// gormDeviceTemplateRepository 是 DeviceTemplateRepository 的 GORM 实现 +type gormDeviceTemplateRepository struct { + db *gorm.DB +} + +// NewGormDeviceTemplateRepository 创建一个新的 gormDeviceTemplateRepository 实例 +func NewGormDeviceTemplateRepository(db *gorm.DB) DeviceTemplateRepository { + return &gormDeviceTemplateRepository{db: db} +} + +// Create 在数据库中创建一个新的设备模板 +func (r *gormDeviceTemplateRepository) Create(deviceTemplate *models.DeviceTemplate) error { + return r.db.Create(deviceTemplate).Error +} + +// FindByID 根据ID查找设备模板 +func (r *gormDeviceTemplateRepository) FindByID(id uint) (*models.DeviceTemplate, error) { + var deviceTemplate models.DeviceTemplate + if err := r.db.First(&deviceTemplate, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("设备模板未找到: %w", err) + } + return nil, fmt.Errorf("查询设备模板失败: %w", err) + } + return &deviceTemplate, nil +} + +// FindByName 根据名称查找设备模板 +func (r *gormDeviceTemplateRepository) FindByName(name string) (*models.DeviceTemplate, error) { + var deviceTemplate models.DeviceTemplate + if err := r.db.Where("name = ?", name).First(&deviceTemplate).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("设备模板未找到: %w", err) + } + return nil, fmt.Errorf("查询设备模板失败: %w", err) + } + return &deviceTemplate, nil +} + +// ListAll 获取所有设备模板 +func (r *gormDeviceTemplateRepository) ListAll() ([]*models.DeviceTemplate, error) { + var deviceTemplates []*models.DeviceTemplate + if err := r.db.Find(&deviceTemplates).Error; err != nil { + return nil, fmt.Errorf("获取设备模板列表失败: %w", err) + } + return deviceTemplates, nil +} + +// Update 更新数据库中的设备模板 +func (r *gormDeviceTemplateRepository) Update(deviceTemplate *models.DeviceTemplate) error { + return r.db.Save(deviceTemplate).Error +} + +// IsInUse 检查设备模板是否正在被设备使用 +func (r *gormDeviceTemplateRepository) IsInUse(id uint) (bool, error) { + var count int64 + if err := r.db.Model(&models.Device{}).Where("device_template_id = ?", id).Count(&count).Error; err != nil { + return false, fmt.Errorf("检查设备模板使用情况失败: %w", err) + } + return count > 0, nil +} + +// Delete 软删除数据库中的设备模板 +func (r *gormDeviceTemplateRepository) Delete(id uint) error { + inUse, err := r.IsInUse(id) + if err != nil { + return err + } + if inUse { + return errors.New("设备模板正在被设备使用,无法删除") + } + return r.db.Delete(&models.DeviceTemplate{}, id).Error +}