Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
307 lines
14 KiB
Go
307 lines
14 KiB
Go
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
|
||
}
|