From 1f2d54d53e36f331bd6b77afc110282aaedaf36f Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 20 Sep 2025 17:11:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swaggo | 2 - docs/docs.go | 18 +-- docs/swagger.json | 18 +-- docs/swagger.yaml | 17 ++- .../controller/device/device_controller.go | 125 +++++++++++++----- internal/app/controller/plan/converter.go | 77 +++++++---- .../app/controller/plan/plan_controller.go | 53 +++++--- internal/app/controller/response.go | 4 - internal/infra/models/device.go | 2 +- 9 files changed, 212 insertions(+), 104 deletions(-) diff --git a/.swaggo b/.swaggo index ac502ac..e69de29 100644 --- a/.swaggo +++ b/.swaggo @@ -1,2 +0,0 @@ -replace encoding/json.RawMessage object -replace git_huangwc_com_pig_pig-farm-controller_internal_app_controller_device.DeviceResponse device.DeviceResponse \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 884d006..68e498f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -560,9 +560,6 @@ const docTemplate = `{ } }, "definitions": { - "controller.Properties": { - "type": "object" - }, "controller.Response": { "type": "object", "properties": { @@ -596,7 +593,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -623,7 +621,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -652,7 +651,8 @@ const docTemplate = `{ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -952,7 +952,8 @@ const docTemplate = `{ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "type": { "allOf": [ @@ -984,7 +985,8 @@ const docTemplate = `{ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "plan_id": { "type": "integer", diff --git a/docs/swagger.json b/docs/swagger.json index 7fe31ca..4adabab 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -549,9 +549,6 @@ } }, "definitions": { - "controller.Properties": { - "type": "object" - }, "controller.Response": { "type": "object", "properties": { @@ -585,7 +582,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -612,7 +610,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -641,7 +640,8 @@ "type": "integer" }, "properties": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "sub_type": { "$ref": "#/definitions/models.DeviceSubType" @@ -941,7 +941,8 @@ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "type": { "allOf": [ @@ -973,7 +974,8 @@ "example": "打开风扇" }, "parameters": { - "$ref": "#/definitions/controller.Properties" + "type": "object", + "additionalProperties": true }, "plan_id": { "type": "integer", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5f87c17..dcb61c8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,6 +1,4 @@ definitions: - controller.Properties: - type: object controller.Response: properties: code: @@ -21,7 +19,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -39,7 +38,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -61,7 +61,8 @@ definitions: parent_id: type: integer properties: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object sub_type: $ref: '#/definitions/models.DeviceSubType' type: @@ -271,7 +272,8 @@ definitions: example: 打开风扇 type: string parameters: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object type: allOf: - $ref: '#/definitions/models.TaskType' @@ -292,7 +294,8 @@ definitions: example: 打开风扇 type: string parameters: - $ref: '#/definitions/controller.Properties' + additionalProperties: true + type: object plan_id: example: 1 type: integer diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go index 1b854a7..3eac4b5 100644 --- a/internal/app/controller/device/device_controller.go +++ b/internal/app/controller/device/device_controller.go @@ -1,7 +1,9 @@ package device import ( + "encoding/json" "errors" + "fmt" "strconv" "strings" "time" @@ -11,7 +13,6 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" "github.com/gin-gonic/gin" - "gorm.io/datatypes" "gorm.io/gorm" ) @@ -33,46 +34,54 @@ func NewController(repo repository.DeviceRepository, logger *logs.Logger) *Contr // CreateDeviceRequest 定义了创建设备时需要传入的参数 type CreateDeviceRequest struct { - Name string `json:"name" binding:"required"` - Type models.DeviceType `json:"type" binding:"required"` - SubType models.DeviceSubType `json:"sub_type,omitempty"` - ParentID *uint `json:"parent_id,omitempty"` - Location string `json:"location,omitempty"` - Properties controller.Properties `json:"properties,omitempty"` + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` } // UpdateDeviceRequest 定义了更新设备时需要传入的参数 type UpdateDeviceRequest struct { - Name string `json:"name" binding:"required"` - Type models.DeviceType `json:"type" binding:"required"` - SubType models.DeviceSubType `json:"sub_type,omitempty"` - ParentID *uint `json:"parent_id,omitempty"` - Location string `json:"location,omitempty"` - Properties controller.Properties `json:"properties,omitempty"` + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` } // --- Response DTOs --- // DeviceResponse 定义了返回给客户端的单个设备信息的结构 type DeviceResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Type models.DeviceType `json:"type"` - SubType models.DeviceSubType `json:"sub_type"` - ParentID *uint `json:"parent_id"` - Location string `json:"location"` - Properties controller.Properties `json:"properties"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID uint `json:"id"` + Name string `json:"name"` + Type models.DeviceType `json:"type"` + SubType models.DeviceSubType `json:"sub_type"` + ParentID *uint `json:"parent_id"` + Location string `json:"location"` + Properties map[string]interface{} `json:"properties"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // --- DTO 转换函数 --- // newDeviceResponse 从数据库模型创建一个新的设备响应 DTO -func newDeviceResponse(device *models.Device) *DeviceResponse { +func newDeviceResponse(device *models.Device) (*DeviceResponse, error) { if device == nil { - return nil + return nil, nil } + + var props map[string]interface{} + if len(device.Properties) > 0 && string(device.Properties) != "null" { + if err := json.Unmarshal(device.Properties, &props); err != nil { + return nil, fmt.Errorf("解析设备属性失败 (ID: %d): %w", device.ID, err) + } + } + return &DeviceResponse{ ID: device.ID, Name: device.Name, @@ -80,19 +89,23 @@ func newDeviceResponse(device *models.Device) *DeviceResponse { SubType: device.SubType, ParentID: device.ParentID, Location: device.Location, - Properties: controller.Properties(device.Properties), + Properties: props, CreatedAt: device.CreatedAt.Format(time.RFC3339), UpdatedAt: device.UpdatedAt.Format(time.RFC3339), - } + }, nil } // newListDeviceResponse 从数据库模型切片创建一个新的设备列表响应 DTO 切片 -func newListDeviceResponse(devices []*models.Device) []*DeviceResponse { +func newListDeviceResponse(devices []*models.Device) ([]*DeviceResponse, error) { list := make([]*DeviceResponse, 0, len(devices)) for _, device := range devices { - list = append(list, newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + return nil, err + } + list = append(list, resp) } - return list + return list, nil } // --- Controller Methods --- @@ -114,13 +127,20 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { return } + propertiesJSON, err := json.Marshal(req.Properties) + if err != nil { + c.logger.Errorf("创建设备: 序列化属性失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "属性字段格式错误") + return + } + device := &models.Device{ Name: req.Name, Type: req.Type, SubType: req.SubType, ParentID: req.ParentID, Location: req.Location, - Properties: datatypes.JSON(req.Properties), + Properties: propertiesJSON, } if err := c.repo.Create(device); err != nil { @@ -129,7 +149,14 @@ func (c *Controller) CreateDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeCreated, "设备创建成功", newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + c.logger.Errorf("创建设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "设备创建成功,但响应生成失败") + return + } + + controller.SendResponse(ctx, controller.CodeCreated, "设备创建成功", resp) } // GetDevice godoc @@ -158,7 +185,14 @@ func (c *Controller) GetDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "获取设备信息成功", newDeviceResponse(device)) + resp, err := newDeviceResponse(device) + if err != nil { + c.logger.Errorf("获取设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取设备信息失败: 内部数据格式错误") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "获取设备信息成功", resp) } // ListDevices godoc @@ -176,7 +210,14 @@ func (c *Controller) ListDevices(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "获取设备列表成功", newListDeviceResponse(devices)) + resp, err := newListDeviceResponse(devices) + if err != nil { + c.logger.Errorf("获取设备列表: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取设备列表失败: 内部数据格式错误") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "获取设备列表成功", resp) } // UpdateDevice godoc @@ -216,13 +257,20 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } + propertiesJSON, err := json.Marshal(req.Properties) + if err != nil { + c.logger.Errorf("更新设备: 序列化属性失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeBadRequest, "属性字段格式错误") + return + } + // 3. 更新从数据库中查出的现有设备对象的字段 existingDevice.Name = req.Name existingDevice.Type = req.Type existingDevice.SubType = req.SubType existingDevice.ParentID = req.ParentID existingDevice.Location = req.Location - existingDevice.Properties = datatypes.JSON(req.Properties) + existingDevice.Properties = propertiesJSON // 4. 将修改后的 existingDevice 对象保存回数据库 if err := c.repo.Update(existingDevice); err != nil { @@ -231,7 +279,14 @@ func (c *Controller) UpdateDevice(ctx *gin.Context) { return } - controller.SendResponse(ctx, controller.CodeSuccess, "设备更新成功", newDeviceResponse(existingDevice)) + resp, err := newDeviceResponse(existingDevice) + if err != nil { + c.logger.Errorf("更新设备: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "设备更新成功,但响应生成失败") + return + } + + controller.SendResponse(ctx, controller.CodeSuccess, "设备更新成功", resp) } // DeleteDevice godoc diff --git a/internal/app/controller/plan/converter.go b/internal/app/controller/plan/converter.go index 8410c14..6ace996 100644 --- a/internal/app/controller/plan/converter.go +++ b/internal/app/controller/plan/converter.go @@ -1,15 +1,16 @@ package plan import ( - "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "encoding/json" + "fmt" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" - "gorm.io/datatypes" ) // PlanToResponse 将Plan模型转换为PlanResponse -func PlanToResponse(plan *models.Plan) *PlanResponse { +func PlanToResponse(plan *models.Plan) (*PlanResponse, error) { if plan == nil { - return nil + return nil, nil } response := &PlanResponse{ @@ -28,7 +29,11 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { if plan.ContentType == models.PlanContentTypeSubPlans { response.SubPlans = make([]SubPlanResponse, len(plan.SubPlans)) for i, subPlan := range plan.SubPlans { - response.SubPlans[i] = SubPlanToResponse(&subPlan) + subPlanResp, err := SubPlanToResponse(&subPlan) + if err != nil { + return nil, err + } + response.SubPlans[i] = subPlanResp } } @@ -36,11 +41,15 @@ func PlanToResponse(plan *models.Plan) *PlanResponse { if plan.ContentType == models.PlanContentTypeTasks { response.Tasks = make([]TaskResponse, len(plan.Tasks)) for i, task := range plan.Tasks { - response.Tasks[i] = TaskToResponse(&task) + taskResp, err := TaskToResponse(&task) + if err != nil { + return nil, err + } + response.Tasks[i] = taskResp } } - return response + return response, nil } // PlanFromCreateRequest 将CreatePlanRequest转换为Plan模型,并进行业务规则验证 @@ -73,8 +82,11 @@ func PlanFromCreateRequest(req *CreatePlanRequest) (*models.Plan, error) { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { - // 使用来自请求的ExecutionOrder - plan.Tasks[i] = TaskFromRequest(&taskReq) + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task } } @@ -120,8 +132,11 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { if req.ContentType == models.PlanContentTypeTasks && req.Tasks != nil { plan.Tasks = make([]models.Task, len(req.Tasks)) for i, taskReq := range req.Tasks { - // 使用来自请求的ExecutionOrder - plan.Tasks[i] = TaskFromRequest(&taskReq) + task, err := TaskFromRequest(&taskReq) + if err != nil { + return nil, err + } + plan.Tasks[i] = task } } @@ -138,9 +153,9 @@ func PlanFromUpdateRequest(req *UpdatePlanRequest) (*models.Plan, error) { } // SubPlanToResponse 将SubPlan模型转换为SubPlanResponse -func SubPlanToResponse(subPlan *models.SubPlan) SubPlanResponse { +func SubPlanToResponse(subPlan *models.SubPlan) (SubPlanResponse, error) { if subPlan == nil { - return SubPlanResponse{} + return SubPlanResponse{}, nil } response := SubPlanResponse{ @@ -152,16 +167,27 @@ func SubPlanToResponse(subPlan *models.SubPlan) SubPlanResponse { // 如果有完整的子计划数据,也进行转换 if subPlan.ChildPlan != nil { - response.ChildPlan = PlanToResponse(subPlan.ChildPlan) + childPlanResp, err := PlanToResponse(subPlan.ChildPlan) + if err != nil { + return SubPlanResponse{}, err + } + response.ChildPlan = childPlanResp } - return response + return response, nil } // TaskToResponse 将Task模型转换为TaskResponse -func TaskToResponse(task *models.Task) TaskResponse { +func TaskToResponse(task *models.Task) (TaskResponse, error) { if task == nil { - return TaskResponse{} + return TaskResponse{}, nil + } + + var params map[string]interface{} + if len(task.Parameters) > 0 && string(task.Parameters) != "null" { + if err := json.Unmarshal(task.Parameters, ¶ms); err != nil { + return TaskResponse{}, fmt.Errorf("parsing task parameters failed (ID: %d): %w", task.ID, err) + } } return TaskResponse{ @@ -171,14 +197,19 @@ func TaskToResponse(task *models.Task) TaskResponse { Description: task.Description, ExecutionOrder: task.ExecutionOrder, Type: task.Type, - Parameters: controller.Properties(task.Parameters), - } + Parameters: params, + }, nil } // TaskFromRequest 将TaskRequest转换为Task模型 -func TaskFromRequest(req *TaskRequest) models.Task { +func TaskFromRequest(req *TaskRequest) (models.Task, error) { if req == nil { - return models.Task{} + return models.Task{}, nil + } + + paramsJSON, err := json.Marshal(req.Parameters) + if err != nil { + return models.Task{}, fmt.Errorf("serializing task parameters failed: %w", err) } return models.Task{ @@ -186,6 +217,6 @@ func TaskFromRequest(req *TaskRequest) models.Task { Description: req.Description, ExecutionOrder: req.ExecutionOrder, Type: req.Type, - Parameters: datatypes.JSON(req.Parameters), - } + Parameters: paramsJSON, + }, nil } diff --git a/internal/app/controller/plan/plan_controller.go b/internal/app/controller/plan/plan_controller.go index 1f6ca10..fc5bb08 100644 --- a/internal/app/controller/plan/plan_controller.go +++ b/internal/app/controller/plan/plan_controller.go @@ -71,22 +71,22 @@ type SubPlanResponse struct { // TaskRequest 定义任务请求结构体 type TaskRequest struct { - Name string `json:"name" example:"打开风扇"` - Description string `json:"description" example:"打开1号风扇"` - ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"waiting"` - Parameters controller.Properties `json:"parameters,omitempty"` + Name string `json:"name" example:"打开风扇"` + Description string `json:"description" example:"打开1号风扇"` + ExecutionOrder int `json:"execution_order" example:"1"` + Type models.TaskType `json:"type" example:"waiting"` + Parameters map[string]interface{} `json:"parameters,omitempty"` } // TaskResponse 定义任务响应结构体 type TaskResponse struct { - ID int `json:"id" example:"1"` - PlanID uint `json:"plan_id" example:"1"` - Name string `json:"name" example:"打开风扇"` - Description string `json:"description" example:"打开1号风扇"` - ExecutionOrder int `json:"execution_order" example:"1"` - Type models.TaskType `json:"type" example:"waiting"` - Parameters controller.Properties `json:"parameters,omitempty"` + ID int `json:"id" example:"1"` + PlanID uint `json:"plan_id" example:"1"` + Name string `json:"name" example:"打开风扇"` + Description string `json:"description" example:"打开1号风扇"` + ExecutionOrder int `json:"execution_order" example:"1"` + Type models.TaskType `json:"type" example:"waiting"` + Parameters map[string]interface{} `json:"parameters,omitempty"` } // --- Controller 定义 --- @@ -145,7 +145,12 @@ func (c *Controller) CreatePlan(ctx *gin.Context) { } // 使用已有的转换函数将创建后的模型转换为响应对象 - resp := PlanToResponse(planToCreate) + resp, err := PlanToResponse(planToCreate) + if err != nil { + c.logger.Errorf("创建计划: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "计划创建成功,但响应生成失败") + return + } // 使用统一的成功响应函数 controller.SendResponse(ctx, controller.CodeCreated, "计划创建成功", resp) @@ -183,7 +188,12 @@ func (c *Controller) GetPlan(ctx *gin.Context) { } // 3. 将模型转换为响应 DTO - resp := PlanToResponse(plan) + resp, err := PlanToResponse(plan) + if err != nil { + c.logger.Errorf("获取计划详情: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取计划详情失败: 内部数据格式错误") + return + } // 4. 发送成功响应 controller.SendResponse(ctx, controller.CodeSuccess, "获取计划详情成功", resp) @@ -208,7 +218,13 @@ func (c *Controller) ListPlans(ctx *gin.Context) { // 2. 将模型转换为响应 DTO planResponses := make([]PlanResponse, 0, len(plans)) for _, p := range plans { - planResponses = append(planResponses, *PlanToResponse(&p)) + resp, err := PlanToResponse(&p) + if err != nil { + c.logger.Errorf("获取计划列表: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "获取计划列表失败: 内部数据格式错误") + return + } + planResponses = append(planResponses, *resp) } // 3. 构造并发送成功响应 @@ -286,7 +302,12 @@ func (c *Controller) UpdatePlan(ctx *gin.Context) { } // 7. 将模型转换为响应 DTO - resp := PlanToResponse(updatedPlan) + resp, err := PlanToResponse(updatedPlan) + if err != nil { + c.logger.Errorf("更新计划: 序列化响应失败: %v", err) + controller.SendErrorResponse(ctx, controller.CodeInternalError, "计划更新成功,但响应生成失败") + return + } // 8. 发送成功响应 controller.SendResponse(ctx, controller.CodeSuccess, "计划更新成功", resp) diff --git a/internal/app/controller/response.go b/internal/app/controller/response.go index b08afd5..136356c 100644 --- a/internal/app/controller/response.go +++ b/internal/app/controller/response.go @@ -1,7 +1,6 @@ package controller import ( - "encoding/json" "net/http" "github.com/gin-gonic/gin" @@ -46,6 +45,3 @@ func SendResponse(ctx *gin.Context, code int, message string, data interface{}) func SendErrorResponse(ctx *gin.Context, code int, message string) { SendResponse(ctx, code, message, nil) } - -// Properties 是一个自定义类型,用于在 Swagger 中正确表示 JSON 对象 -type Properties json.RawMessage diff --git a/internal/infra/models/device.go b/internal/infra/models/device.go index cff3295..469129e 100644 --- a/internal/infra/models/device.go +++ b/internal/infra/models/device.go @@ -70,7 +70,7 @@ type Device struct { gorm.Model // Name 是设备的业务名称,应清晰可读,例如 "1号猪舍温度传感器" 或 "做料车间主控" - Name string `gorm:"unique;not null" json:"name"` + Name string `gorm:"not null" json:"name"` // Type 是设备的高级类别,用于区分区域主控和普通设备。建立索引以优化按类型查询。 Type DeviceType `gorm:"not null;index" json:"type"`