package device import ( "context" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/logger" "go.uber.org/zap" "gorm.io/gorm" ) func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDeviceCardsResponse, error) { device, err := s.deviceStore.GetByID(ctx, deviceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, device.ID) if err != nil { return nil, err } cardIDs := make([]uint, 0, len(bindings)) for _, binding := range bindings { cardIDs = append(cardIDs, binding.IotCardID) } var cards []*model.IotCard if len(cardIDs) > 0 { cards, err = s.iotCardStore.GetByIDs(ctx, cardIDs) if err != nil { return nil, err } } cardMap := make(map[uint]*model.IotCard) for _, card := range cards { cardMap[card.ID] = card } responses := make([]*dto.DeviceCardBindingResponse, 0, len(bindings)) for _, binding := range bindings { card := cardMap[binding.IotCardID] if card == nil { continue } resp := &dto.DeviceCardBindingResponse{ ID: binding.ID, SlotPosition: binding.SlotPosition, IotCardID: binding.IotCardID, ICCID: card.ICCID, MSISDN: card.MSISDN, CarrierName: card.CarrierName, Status: card.Status, StatusName: constants.GetIotCardStatusName(card.Status), RealNameStatus: card.RealNameStatus, RealNameStatusName: constants.GetRealNameStatusName(card.RealNameStatus), BindTime: binding.BindTime, IsCurrent: binding.IsCurrent, RealnamePolicy: card.RealnamePolicy, NetworkStatus: card.NetworkStatus, GatewayExtend: card.GatewayExtend, } responses = append(responses, resp) } return &dto.ListDeviceCardsResponse{ Bindings: responses, }, nil } func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCardToDeviceRequest) (*dto.BindCardToDeviceResponse, error) { device, err := s.deviceStore.GetByID(ctx, deviceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } metadata := map[string]any{"iot_card_id": req.IotCardID, "slot_position": req.SlotPosition} if req.SlotPosition > device.MaxSimSlots { appErr := errors.New(errors.CodeInvalidParam, "插槽位置超出设备最大插槽数") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied, device, nil, nil, metadata, appErr) return nil, appErr } existingBinding, err := s.deviceSimBindingStore.GetByDeviceAndSlot(ctx, device.ID, req.SlotPosition) if err != nil && err != gorm.ErrRecordNotFound { s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed, device, nil, nil, metadata, err) return nil, err } if existingBinding != nil { appErr := errors.New(errors.CodeConflict, "该插槽已有绑定的卡") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied, device, nil, nil, metadata, appErr) return nil, appErr } card, err := s.iotCardStore.GetByID(ctx, req.IotCardID) if err != nil { if err == gorm.ErrRecordNotFound { appErr := errors.New(errors.CodeIotCardNotFound) s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed, device, nil, nil, metadata, appErr) return nil, appErr } s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed, device, nil, nil, metadata, err) return nil, err } item := deviceBindingAuditItem{ Card: card, CardRole: constants.AuditResourceRoleDeviceBindingTargetCard, CardBefore: map[string]any{"device_id": nil, "slot_position": nil}, CardAfter: map[string]any{"device_id": device.ID, "slot_position": req.SlotPosition}, } activeBinding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID) if err != nil && err != gorm.ErrRecordNotFound { s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed, device, nil, []deviceBindingAuditItem{item}, metadata, err) return nil, err } if activeBinding != nil { appErr := errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied, device, nil, []deviceBindingAuditItem{item}, metadata, appErr) return nil, appErr } binding := &model.DeviceSimBinding{ DeviceID: device.ID, IotCardID: card.ID, SlotPosition: req.SlotPosition, BindStatus: 1, } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := postgres.NewDeviceSimBindingStore(tx, nil).Create(ctx, binding); err != nil { return err } item.Binding = binding item.BindingRole = constants.AuditResourceRoleDeviceCreatedBinding item.BindingAfter = bindingStateData(binding, constants.BindStatusBound, false) return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCardBound, "设备绑定 IoT 卡", constants.AuditResultSuccess, device, map[string]any{"slot_position": req.SlotPosition, "iot_card_id": nil}, map[string]any{"slot_position": req.SlotPosition, "iot_card_id": card.ID}, []deviceBindingAuditItem{item}, metadata, nil) }) if err != nil { result := constants.AuditResultFailed if appErr, ok := err.(*errors.AppError); ok && (appErr.Code == errors.CodeConflict || appErr.Code == errors.CodeIotCardBoundToDevice) { result = constants.AuditResultDenied } s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", result, device, nil, []deviceBindingAuditItem{{Card: card, CardRole: constants.AuditResourceRoleDeviceBindingTargetCard}}, metadata, err) return nil, err } // 异步快照维护:更新卡上的设备虚拟号,失败不阻断主流程 if updateErr := s.iotCardStore.UpdateDeviceVirtualNo(ctx, card.ID, device.VirtualNo); updateErr != nil { logger.GetAppLogger().Warn("更新卡的设备虚拟号快照失败", zap.Uint("card_id", card.ID), zap.Uint("device_id", device.ID), zap.Error(updateErr), ) } return &dto.BindCardToDeviceResponse{ BindingID: binding.ID, Message: "绑定成功", }, nil } func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*dto.UnbindCardFromDeviceResponse, error) { device, err := s.deviceStore.GetByID(ctx, deviceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } metadata := map[string]any{"iot_card_id": cardID} binding, err := s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, cardID) if err != nil { if err == gorm.ErrRecordNotFound { appErr := errors.New(errors.CodeNotFound, "该卡未绑定到此设备") s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡被拒绝", constants.AuditResultDenied, device, nil, nil, metadata, appErr) return nil, appErr } s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡失败", constants.AuditResultFailed, device, nil, nil, metadata, err) return nil, err } card, cardErr := s.iotCardStore.GetByID(ctx, binding.IotCardID) if cardErr != nil { card = &model.IotCard{} card.ID = binding.IotCardID } item := deviceBindingAuditItem{ Card: card, Binding: binding, CardRole: constants.AuditResourceRoleDeviceBindingTargetCard, BindingRole: constants.AuditResourceRoleDeviceRemovedBinding, CardBefore: map[string]any{"device_id": device.ID, "slot_position": binding.SlotPosition}, CardAfter: map[string]any{"device_id": nil, "slot_position": nil}, BindingBefore: bindingStateData(binding, constants.BindStatusBound, binding.IsCurrent), BindingAfter: bindingStateData(binding, constants.BindStatusUnbound, false), } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := postgres.NewDeviceSimBindingStore(tx, nil).Unbind(ctx, binding.ID); err != nil { return err } if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}).Where("id = ?", binding.ID).Update("is_current", false).Error; err != nil { return err } return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCardUnbound, "设备解绑 IoT 卡", constants.AuditResultSuccess, device, map[string]any{"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID}, map[string]any{"slot_position": binding.SlotPosition, "iot_card_id": nil}, []deviceBindingAuditItem{item}, metadata, nil) }) if err != nil { s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡失败", constants.AuditResultFailed, device, nil, []deviceBindingAuditItem{item}, metadata, err) return nil, err } // 解绑后清空卡的设备虚拟号快照,失败不阻断主流程 if updateErr := s.iotCardStore.UpdateDeviceVirtualNo(ctx, cardID, ""); updateErr != nil { logger.GetAppLogger().Warn("清空卡的设备虚拟号快照失败", zap.Uint("card_id", cardID), zap.Uint("device_id", deviceID), zap.Error(updateErr), ) } return &dto.UnbindCardFromDeviceResponse{ Message: "解绑成功", }, nil }