package device import ( "context" "strconv" "github.com/break/junhong_cmp_fiber/internal/gateway" "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "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 存在 // 提供统一的设备查找 + IMEI 校验逻辑,供所有 Gateway 代理方法复用 func (s *Service) getGatewayDevice(ctx context.Context, identifier string) (*model.Device, string, error) { device, err := s.deviceStore.GetByIdentifier(ctx, identifier) if err != nil { return nil, "", errors.New(errors.CodeNotFound, "设备不存在或无权限访问") } if device.IMEI == "" { return nil, "", errors.New(errors.CodeInvalidParam, "该设备未配置 IMEI,无法调用网关接口") } return device, device.IMEI, nil } // GatewayGetDeviceInfo 通过标识符查询设备网关信息 func (s *Service) GatewayGetDeviceInfo(ctx context.Context, identifier string) (*gateway.DeviceInfoResp, error) { _, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return nil, err } return s.gatewayClient.GetDeviceInfo(ctx, &gateway.DeviceInfoReq{ DeviceID: imei, }) } // GatewayGetSlotInfo 通过标识符查询设备卡槽信息 func (s *Service) GatewayGetSlotInfo(ctx context.Context, identifier string) (*gateway.SlotInfoResp, error) { _, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return nil, err } return s.gatewayClient.GetSlotInfo(ctx, &gateway.DeviceInfoReq{ DeviceID: imei, }) } // GatewaySetWiFi 通过标识符设置设备 WiFi func (s *Service) GatewaySetWiFi(ctx context.Context, identifier string, req *dto.SetWiFiRequest) error { device, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return err } observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "") err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{ ActionCode: constants.AuditActionDeviceWiFiSet, Summary: "设置设备 Wi-Fi", Operation: constants.IntegrationOperationGatewaySetWiFi, Scene: constants.CardObservationSceneDeviceSetWiFi, RequestSummary: map[string]any{ "device_id": device.ID, "imei": imei, "ssid": req.SSID, "enabled_requested": req.Enabled, "credentials_configured": req.Password != "", }, Metadata: map[string]any{ "ssid": req.SSID, "enabled_requested": req.Enabled, "credentials_configured": req.Password != "", }, Call: func(callCtx context.Context) error { return s.gatewayClient.SetWiFi(callCtx, &gateway.WiFiReq{ CardNo: imei, Params: gateway.WiFiParams{SSIDName: req.SSID, SSIDPassword: req.Password}, }) }, }) if err != nil { return err } s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSetWiFi, observation, false) return nil } // GatewaySwitchCard 通过标识符切换设备使用的卡 func (s *Service) GatewaySwitchCard(ctx context.Context, identifier string, req *dto.SwitchCardRequest) error { device, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return err } state, err := loadDeviceBindingState(ctx, s.db, device.ID, req.TargetICCID, false) metadata := map[string]any{"target_iccid": req.TargetICCID} if err != nil { s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡失败", constants.AuditResultFailed, device, nil, nil, metadata, err) return err } if state.target == nil { appErr := errors.New(errors.CodeForbidden, "目标卡未绑定到当前设备") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡被拒绝", constants.AuditResultDenied, device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr) return appErr } targetCard := state.cards[state.target.IotCardID] if targetCard == nil { appErr := errors.New(errors.CodeNotFound, "目标卡资产不存在或无权限访问") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡失败", constants.AuditResultFailed, device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr) return appErr } metadata["target_iot_card_id"] = targetCard.ID metadata["target_slot_position"] = state.target.SlotPosition observation := s.captureDeviceControlObservation(ctx, device.ID, 0, req.TargetICCID) deviceID := strconv.FormatUint(uint64(device.ID), 10) observer := &deviceGatewayAttemptObserver{ service: s, operation: constants.IntegrationOperationGatewaySwitchCard, scene: constants.CardObservationSceneDeviceSwitchCard, seriesKey: deviceCommandSeriesKey(ctx), resource: deviceGatewayResource{ Type: constants.AuditResourceDevice, ID: deviceID, Key: audit.DeviceResourceKey(device), ExternalID: imei, RequestSummary: map[string]any{ "device_id": device.ID, "imei": imei, "target_iot_card_id": targetCard.ID, "target_iccid": targetCard.ICCID, "target_slot_position": state.target.SlotPosition, }, }, } if err = s.gatewayClient.SwitchCard(gateway.WithAttemptObserver(ctx, observer), &gateway.SwitchCardReq{ CardNo: imei, ICCID: req.TargetICCID, }); err != nil { result, summary := constants.AuditResultFailed, "设备切卡失败" if observer.unknown { result, summary = constants.AuditResultUnknown, "设备切卡结果未知" } metadata["integration_id"] = observer.integration s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, summary, result, device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, err) return err } metadata["integration_id"] = observer.integration if err := observer.completeSuccess(ctx, false); err != nil { appErr := errors.Wrap(errors.CodeDatabaseError, err, "终结设备切卡 Integration Log 失败") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡结果未知", constants.AuditResultUnknown, device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr) return appErr } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { lockedState, err := loadDeviceBindingState(ctx, tx, device.ID, targetCard.ICCID, true) if err != nil { return err } if lockedState.target == nil { return errors.New(errors.CodeConflict, "切卡期间目标卡绑定关系已变化") } if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}). Where("device_id = ? AND bind_status = ?", device.ID, constants.BindStatusBound). Update("is_current", false).Error; err != nil { return err } if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}). Where("id = ? AND bind_status = ?", lockedState.target.ID, constants.BindStatusBound). Update("is_current", true).Error; err != nil { return err } return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCurrentCardSwitched, "切换设备当前卡", constants.AuditResultSuccess, device, map[string]any{"current_iot_card_id": currentCardID(lockedState)}, map[string]any{"current_iot_card_id": lockedState.target.IotCardID}, switchCardAuditItems(lockedState), metadata, nil) }) if err != nil { s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡结果未知", constants.AuditResultUnknown, device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, err) return err } s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSwitchCard, observation, true) return nil } // GatewayRebootDevice 通过标识符重启设备 func (s *Service) GatewayRebootDevice(ctx context.Context, identifier string) error { device, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return err } observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "") err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{ ActionCode: constants.AuditActionDeviceRebooted, Summary: "重启设备", Operation: constants.IntegrationOperationGatewayReboot, Scene: constants.CardObservationSceneDeviceReboot, RequestSummary: map[string]any{"device_id": device.ID, "imei": imei}, Metadata: map[string]any{"requested_action": "reboot"}, Call: func(callCtx context.Context) error { return s.gatewayClient.RebootDevice(callCtx, &gateway.DeviceOperationReq{DeviceID: imei}) }, }) if err != nil { return err } s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceReboot, observation, false) return nil } // GatewayResetDevice 通过标识符恢复设备出厂设置 func (s *Service) GatewayResetDevice(ctx context.Context, identifier string) error { device, imei, err := s.getGatewayDevice(ctx, identifier) if err != nil { return err } observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "") err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{ ActionCode: constants.AuditActionDeviceReset, Summary: "恢复设备出厂设置", Operation: constants.IntegrationOperationGatewayReset, Scene: constants.CardObservationSceneDeviceReset, RequestSummary: map[string]any{"device_id": device.ID, "imei": imei}, Metadata: map[string]any{"requested_action": "factory_reset"}, Call: func(callCtx context.Context) error { return s.gatewayClient.ResetDevice(callCtx, &gateway.DeviceOperationReq{DeviceID: imei}) }, }) if err != nil { return err } s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceReset, observation, false) return nil } // 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 { return err } recordRejected := func(summary, result string, businessErr error, targetCard *model.IotCard) error { s.recordDeviceCommandAudit(ctx, constants.AuditActionDeviceSwitchModeSet, summary, result, device, targetCard, nil, nil, map[string]any{"requested_switch_mode": switchMode, "iot_card_id": req.IotCardID}, businessErr) return businessErr } if req.SwitchMode == nil { appErr := errors.New(errors.CodeInvalidParam, "切卡模式不能为空") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil) } if switchMode != 0 && switchMode != 1 { appErr := errors.New(errors.CodeInvalidParam, "切卡模式仅支持0或1") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil) } if req.IotCardID == 0 { appErr := errors.New(errors.CodeInvalidParam, "目标卡资产ID不能为空") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil) } targetCard, err := s.iotCardStore.GetByID(ctx, req.IotCardID) if err != nil { if err == gorm.ErrRecordNotFound { appErr := errors.New(errors.CodeNotFound, "目标卡资产不存在或无权限访问") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil) } appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询目标卡资产失败") return recordRejected("设置设备切卡模式失败", constants.AuditResultFailed, appErr, nil) } if _, err = s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, targetCard.ID); err != nil { if err == gorm.ErrRecordNotFound { appErr := errors.New(errors.CodeForbidden, "目标卡未绑定到当前设备,禁止设置切卡模式") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard) } appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备卡绑定关系失败") return recordRejected("设置设备切卡模式失败", constants.AuditResultFailed, appErr, targetCard) } if targetCard.ICCID == "" { appErr := errors.New(errors.CodeConflict, "目标卡资产缺少ICCID,无法设置切卡模式") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard) } if targetCard.NetworkStatus != constants.NetworkStatusOnline { appErr := errors.New(errors.CodeForbidden, "目标卡状态异常,仅正常状态的卡允许设置切卡模式") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard) } if targetCard.RealNameStatus != constants.RealNameStatusVerified { appErr := errors.New(errors.CodeForbidden, "目标卡未实名,禁止设置切卡模式") return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard) } observation := s.captureDeviceControlObservation(ctx, device.ID, targetCard.ID, targetCard.ICCID) err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{ ActionCode: constants.AuditActionDeviceSwitchModeSet, Summary: "设置设备切卡模式", Operation: constants.IntegrationOperationGatewaySwitchMode, Scene: constants.CardObservationSceneDeviceSwitchMode, RequestSummary: map[string]any{ "device_id": device.ID, "imei": imei, "switch_mode": switchMode, "iot_card_id": targetCard.ID, "iccid": targetCard.ICCID, }, Metadata: map[string]any{ "requested_switch_mode": switchMode, "iot_card_id": targetCard.ID, "iccid": targetCard.ICCID, }, TargetCard: targetCard, Call: func(callCtx context.Context) error { return s.gatewayClient.SwitchMode(callCtx, &gateway.SwitchModeReq{ CardNo: imei, SwitchMode: strconv.Itoa(switchMode), ICCID: targetCard.ICCID, }) }, }) if err != nil { return err } s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSwitchMode, observation, true) return nil }