Files
junhong_cmp_fiber/internal/service/device/gateway_service.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

307 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}