diff --git a/internal/gateway/device.go b/internal/gateway/device.go index 17d7a96..45f60b0 100644 --- a/internal/gateway/device.go +++ b/internal/gateway/device.go @@ -70,9 +70,16 @@ func (c *Client) RebootDevice(ctx context.Context, req *DeviceOperationReq) erro } // SwitchMode 设置设备切卡模式 +// Gateway 实际要求 cardNo 传设备 IMEI,switch_mode 传字符串,并且必须指定目标 ICCID // 切换设备卡槽的自动/手动模式,0=自动切卡,1=手动切卡 // POST /device/SwitchMode func (c *Client) SwitchMode(ctx context.Context, req *SwitchModeReq) error { + if req.CardNo == "" { + return errors.New(errors.CodeInvalidParam, "cardNo 不能为空") + } + if req.ICCID == "" { + return errors.New(errors.CodeInvalidParam, "iccid 不能为空") + } _, err := c.doRequest(ctx, "/device/SwitchMode", req) return err } diff --git a/internal/gateway/models.go b/internal/gateway/models.go index b987694..b9e6638 100644 --- a/internal/gateway/models.go +++ b/internal/gateway/models.go @@ -193,7 +193,8 @@ type DeviceOperationReq struct { // SwitchModeReq 是设置设备切卡模式的请求 type SwitchModeReq struct { CardNo string `json:"cardNo" validate:"required" required:"true" description:"设备编号(IMEI)"` - SwitchMode int `json:"switch_mode" description:"切卡模式(0:自动切卡, 1:手动切卡)"` + SwitchMode string `json:"switch_mode" description:"切卡模式(0:自动切卡, 1:手动切卡)"` + ICCID string `json:"iccid" validate:"required" required:"true" description:"目标卡 ICCID"` CallbackURL string `json:"callbackUrl,omitempty" description:"异步回调通知地址"` } diff --git a/internal/handler/admin/device.go b/internal/handler/admin/device.go index 5da08a6..6eac714 100644 --- a/internal/handler/admin/device.go +++ b/internal/handler/admin/device.go @@ -298,6 +298,15 @@ func (h *DeviceHandler) SetSwitchMode(c *fiber.Ctx) error { if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } + if req.SwitchMode == nil { + return errors.New(errors.CodeInvalidParam, "切卡模式不能为空") + } + if *req.SwitchMode != 0 && *req.SwitchMode != 1 { + return errors.New(errors.CodeInvalidParam, "切卡模式仅支持0或1") + } + if req.IotCardID == 0 { + return errors.New(errors.CodeInvalidParam, "目标卡资产ID不能为空") + } if err := h.service.GatewaySwitchMode(c.UserContext(), identifier, &req); err != nil { return err diff --git a/internal/model/dto/device_dto.go b/internal/model/dto/device_dto.go index 606ff7e..dc8c5be 100644 --- a/internal/model/dto/device_dto.go +++ b/internal/model/dto/device_dto.go @@ -183,7 +183,8 @@ type SwitchCardRequest struct { // SetSwitchModeRequest 设置切卡模式请求 type SetSwitchModeRequest struct { Identifier string `path:"identifier" description:"设备标识符(支持虚拟号/IMEI/SN)" required:"true"` - SwitchMode int `json:"switch_mode" validate:"oneof=0 1" required:"true" description:"切卡模式 (0:自动切卡, 1:手动切卡)"` + SwitchMode *int `json:"switch_mode" validate:"required,oneof=0 1" required:"true" description:"切卡模式 (0:自动切卡, 1:手动切卡)"` + IotCardID uint `json:"iot_card_id" validate:"required,min=1" required:"true" minimum:"1" description:"目标卡资产ID(必须是系统内状态正常且已实名的卡)"` } type EmptyResponse struct { diff --git a/internal/routes/device.go b/internal/routes/device.go index 9c56334..4853207 100644 --- a/internal/routes/device.go +++ b/internal/routes/device.go @@ -167,7 +167,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp Register(devices, doc, groupPath, "POST", "/by-identifier/:identifier/switch-mode", handler.SetSwitchMode, RouteSpec{ Summary: "设置切卡模式", - Description: "通过虚拟号/IMEI/SN 设置设备切卡模式(0:自动切卡, 1:手动切卡)。设备必须已配置 IMEI。", + Description: "通过虚拟号/IMEI/SN 设置设备切卡模式(0:自动切卡, 1:手动切卡)。设备必须已配置 IMEI,且必须指定一张系统内状态正常并已实名的目标卡资产ID。", Tags: []string{"设备管理"}, Input: new(dto.SetSwitchModeRequest), Output: new(dto.EmptyResponse), diff --git a/internal/service/device/gateway_service.go b/internal/service/device/gateway_service.go index 0077742..0a82fb1 100644 --- a/internal/service/device/gateway_service.go +++ b/internal/service/device/gateway_service.go @@ -2,12 +2,14 @@ package device import ( "context" + "strconv" "github.com/break/junhong_cmp_fiber/internal/gateway" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" + "gorm.io/gorm" ) // getGatewayDevice 通过标识符获取设备并验证 IMEI 存在 @@ -344,6 +346,11 @@ func (s *Service) GatewayResetDevice(ctx context.Context, identifier string) err // GatewaySwitchMode 通过标识符设置设备切卡模式 func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req *dto.SetSwitchModeRequest) error { + switchMode := 0 + if req.SwitchMode != nil { + switchMode = *req.SwitchMode + } + device, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { s.logDeviceOperation( @@ -355,7 +362,8 @@ func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req nil, map[string]any{ "identifier": identifier, - "switch_mode": req.SwitchMode, + "switch_mode": switchMode, + "iot_card_id": req.IotCardID, }, 0, 0, @@ -364,9 +372,194 @@ func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req ) return err } + if req.SwitchMode == nil { + appErr := errors.New(errors.CodeInvalidParam, "切卡模式不能为空") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if switchMode != 0 && switchMode != 1 { + appErr := errors.New(errors.CodeInvalidParam, "切卡模式仅支持0或1") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if req.IotCardID == 0 { + appErr := errors.New(errors.CodeInvalidParam, "目标卡资产ID不能为空") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + + targetCard, err := s.iotCardStore.GetByID(ctx, req.IotCardID) + if err != nil { + if err == gorm.ErrRecordNotFound { + appErr := errors.New(errors.CodeNotFound, "目标卡资产不存在或无权限访问") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询目标卡资产失败") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换失败", + constants.AssetAuditResultFailed, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if _, err = s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, targetCard.ID); err != nil { + if err == gorm.ErrRecordNotFound { + appErr := errors.New(errors.CodeForbidden, "目标卡未绑定到当前设备,禁止设置切卡模式") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备卡绑定关系失败") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换失败", + constants.AssetAuditResultFailed, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if targetCard.ICCID == "" { + appErr := errors.New(errors.CodeConflict, "目标卡资产缺少ICCID,无法设置切卡模式") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID}, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if targetCard.NetworkStatus != constants.NetworkStatusOnline { + appErr := errors.New(errors.CodeForbidden, "目标卡状态异常,仅正常状态的卡允许设置切卡模式") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{ + "switch_mode": switchMode, + "iot_card_id": req.IotCardID, + "iccid": targetCard.ICCID, + "network_status": targetCard.NetworkStatus, + "real_name_status": targetCard.RealNameStatus, + }, + 0, + 0, + 0, + appErr, + ) + return appErr + } + if targetCard.RealNameStatus != constants.RealNameStatusVerified { + appErr := errors.New(errors.CodeForbidden, "目标卡未实名,禁止设置切卡模式") + s.logDeviceOperation( + ctx, + constants.AssetAuditOpDeviceSwitchMode, + "设备切卡模式切换被拒绝", + constants.AssetAuditResultDenied, + device, + map[string]any{"device": deviceSnapshot(device)}, + map[string]any{ + "switch_mode": switchMode, + "iot_card_id": req.IotCardID, + "iccid": targetCard.ICCID, + "network_status": targetCard.NetworkStatus, + "real_name_status": targetCard.RealNameStatus, + }, + 0, + 0, + 0, + appErr, + ) + return appErr + } if err = s.gatewayClient.SwitchMode(ctx, &gateway.SwitchModeReq{ CardNo: imei, - SwitchMode: req.SwitchMode, + SwitchMode: strconv.Itoa(switchMode), + ICCID: targetCard.ICCID, }); err != nil { s.logDeviceOperation( ctx, @@ -375,7 +568,7 @@ func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req constants.AssetAuditResultFailed, device, map[string]any{"device": deviceSnapshot(device)}, - map[string]any{"switch_mode": req.SwitchMode}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID}, 0, 0, 0, @@ -391,7 +584,7 @@ func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req constants.AssetAuditResultSuccess, device, map[string]any{"device": deviceSnapshot(device)}, - map[string]any{"switch_mode": req.SwitchMode}, + map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID}, 0, 0, 0,