package iot_card import ( "context" "time" "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/internal/store" "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/middleware" "go.uber.org/zap" "gorm.io/gorm" ) // PollingCallback 轮询回调接口 // 用于在卡生命周期事件发生时通知轮询调度器 type PollingCallback interface { // OnCardCreated 单卡创建时的回调 OnCardCreated(ctx context.Context, card *model.IotCard) // OnCardStatusChanged 卡状态变化时的回调 OnCardStatusChanged(ctx context.Context, cardID uint) // OnCardDeleted 卡删除时的回调 OnCardDeleted(ctx context.Context, cardID uint) // OnCardEnabled 卡启用轮询时的回调 OnCardEnabled(ctx context.Context, cardID uint) // OnCardDisabled 卡禁用轮询时的回调 OnCardDisabled(ctx context.Context, cardID uint) } type Service struct { db *gorm.DB iotCardStore *postgres.IotCardStore shopStore *postgres.ShopStore assetAllocationRecordStore *postgres.AssetAllocationRecordStore shopPackageAllocationStore *postgres.ShopPackageAllocationStore shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore packageSeriesStore *postgres.PackageSeriesStore gatewayClient *gateway.Client logger *zap.Logger pollingCallback PollingCallback // 轮询回调,可选 } func New( db *gorm.DB, iotCardStore *postgres.IotCardStore, shopStore *postgres.ShopStore, assetAllocationRecordStore *postgres.AssetAllocationRecordStore, shopPackageAllocationStore *postgres.ShopPackageAllocationStore, shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore, packageSeriesStore *postgres.PackageSeriesStore, gatewayClient *gateway.Client, logger *zap.Logger, ) *Service { return &Service{ db: db, iotCardStore: iotCardStore, shopStore: shopStore, assetAllocationRecordStore: assetAllocationRecordStore, shopPackageAllocationStore: shopPackageAllocationStore, shopSeriesAllocationStore: shopSeriesAllocationStore, packageSeriesStore: packageSeriesStore, gatewayClient: gatewayClient, logger: logger, } } // SetPollingCallback 设置轮询回调 // 在应用启动时由 bootstrap 调用,注入轮询调度器 func (s *Service) SetPollingCallback(callback PollingCallback) { s.pollingCallback = callback } func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) { page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } opts := &store.QueryOptions{ Page: page, PageSize: pageSize, } filters := make(map[string]interface{}) if req.Status != nil { filters["status"] = *req.Status } if req.CarrierID != nil { filters["carrier_id"] = *req.CarrierID } if req.ShopID != nil { filters["shop_id"] = *req.ShopID } if req.ICCID != "" { filters["iccid"] = req.ICCID } if req.MSISDN != "" { filters["msisdn"] = req.MSISDN } if req.VirtualNo != "" { filters["virtual_no"] = req.VirtualNo } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo } if req.PackageID != nil { filters["package_id"] = *req.PackageID } if req.IsDistributed != nil { filters["is_distributed"] = *req.IsDistributed } if req.ICCIDStart != "" { filters["iccid_start"] = req.ICCIDStart } if req.ICCIDEnd != "" { filters["iccid_end"] = req.ICCIDEnd } if req.IsReplaced != nil { filters["is_replaced"] = *req.IsReplaced } if req.SeriesID != nil { filters["series_id"] = *req.SeriesID } // 代理用户注入 subordinate_shop_ids,让 Store 层走并行查询路径 // 避免 PG 对 shop_id IN (...) + ORDER BY 选择全表扫描 userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { shopID := middleware.GetShopIDFromContext(ctx) if shopID > 0 { subordinateIDs, err := s.shopStore.GetSubordinateShopIDs(ctx, shopID) if err == nil && len(subordinateIDs) > 1 { filters["subordinate_shop_ids"] = subordinateIDs } } } cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters) if err != nil { return nil, err } shopMap := s.loadShopNames(ctx, cards) seriesMap := s.loadSeriesNames(ctx, cards) list := make([]*dto.StandaloneIotCardResponse, 0, len(cards)) for _, card := range cards { item := s.toStandaloneResponse(card, shopMap, seriesMap) list = append(list, item) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &dto.ListStandaloneIotCardResponse{ List: list, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // GetByICCID 通过 ICCID 获取单卡详情 func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDetailResponse, error) { card, err := s.iotCardStore.GetByICCID(ctx, iccid) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "IoT卡不存在") } return nil, err } shopMap := s.loadShopNames(ctx, []*model.IotCard{card}) seriesMap := s.loadSeriesNames(ctx, []*model.IotCard{card}) standaloneResp := s.toStandaloneResponse(card, shopMap, seriesMap) return &dto.IotCardDetailResponse{ StandaloneIotCardResponse: *standaloneResp, }, nil } func (s *Service) loadShopNames(ctx context.Context, cards []*model.IotCard) map[uint]string { shopIDs := make([]uint, 0) shopIDSet := make(map[uint]bool) for _, card := range cards { if card.ShopID != nil && *card.ShopID > 0 && !shopIDSet[*card.ShopID] { shopIDs = append(shopIDs, *card.ShopID) shopIDSet[*card.ShopID] = true } } shopMap := make(map[uint]string) if len(shopIDs) > 0 { var shops []model.Shop // 使用 Unscoped() 包含已删除的店铺,确保能显示店铺名称 s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops) for _, shop := range shops { shopMap[shop.ID] = shop.ShopName } } return shopMap } func (s *Service) loadSeriesNames(ctx context.Context, cards []*model.IotCard) map[uint]string { seriesIDs := make([]uint, 0) seriesIDSet := make(map[uint]bool) for _, card := range cards { if card.SeriesID != nil && *card.SeriesID > 0 && !seriesIDSet[*card.SeriesID] { seriesIDs = append(seriesIDs, *card.SeriesID) seriesIDSet[*card.SeriesID] = true } } seriesMap := make(map[uint]string) if len(seriesIDs) > 0 { var seriesList []model.PackageSeries s.db.WithContext(ctx).Where("id IN ?", seriesIDs).Find(&seriesList) for _, series := range seriesList { seriesMap[series.ID] = series.SeriesName } } return seriesMap } func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]string, seriesMap map[uint]string) *dto.StandaloneIotCardResponse { resp := &dto.StandaloneIotCardResponse{ ID: card.ID, ICCID: card.ICCID, VirtualNo: card.VirtualNo, CardCategory: card.CardCategory, CarrierID: card.CarrierID, CarrierType: card.CarrierType, CarrierName: card.CarrierName, IMSI: card.IMSI, MSISDN: card.MSISDN, BatchNo: card.BatchNo, Supplier: card.Supplier, Status: card.Status, ShopID: card.ShopID, ActivatedAt: card.ActivatedAt, ActivationStatus: card.ActivationStatus, RealNameStatus: card.RealNameStatus, NetworkStatus: card.NetworkStatus, DataUsageMB: card.DataUsageMB, CurrentMonthUsageMB: card.CurrentMonthUsageMB, CurrentMonthStartDate: card.CurrentMonthStartDate, LastMonthTotalMB: card.LastMonthTotalMB, LastDataCheckAt: card.LastDataCheckAt, LastRealNameCheckAt: card.LastRealNameCheckAt, EnablePolling: card.EnablePolling, SeriesID: card.SeriesID, FirstCommissionPaid: card.FirstCommissionPaid, AccumulatedRecharge: card.AccumulatedRecharge, CreatedAt: card.CreatedAt, UpdatedAt: card.UpdatedAt, } if card.ShopID != nil && *card.ShopID > 0 { resp.ShopName = shopMap[*card.ShopID] } if card.SeriesID != nil && *card.SeriesID > 0 { resp.SeriesName = seriesMap[*card.SeriesID] } return resp } func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateStandaloneCardsResponse, error) { if err := s.validateDirectSubordinate(ctx, operatorShopID, req.ToShopID); err != nil { return nil, err } cards, err := s.getCardsForAllocation(ctx, req, operatorShopID) if err != nil { return nil, err } if len(cards) == 0 { return &dto.AllocateStandaloneCardsResponse{ TotalCount: 0, SuccessCount: 0, FailCount: 0, FailedItems: []dto.AllocationFailedItem{}, }, nil } var cardIDs []uint var failedItems []dto.AllocationFailedItem boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards)) if err != nil { return nil, err } boundCardIDSet := make(map[uint]bool) for _, id := range boundCardIDs { boundCardIDSet[id] = true } isPlatform := operatorShopID == nil for _, card := range cards { if boundCardIDSet[card.ID] { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "已绑定设备的卡不能单独分配", }) continue } if isPlatform && card.Status != constants.IotCardStatusInStock { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "平台只能分配在库状态的卡", }) continue } if !isPlatform && card.Status != constants.IotCardStatusDistributed { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "代理只能分配已分销状态的卡", }) continue } cardIDs = append(cardIDs, card.ID) } if len(cardIDs) == 0 { return &dto.AllocateStandaloneCardsResponse{ TotalCount: len(cards), SuccessCount: 0, FailCount: len(failedItems), FailedItems: failedItems, }, nil } newStatus := constants.IotCardStatusDistributed toShopID := req.ToShopID err = s.db.Transaction(func(tx *gorm.DB) error { txIotCardStore := postgres.NewIotCardStore(tx, nil) txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil) if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, &toShopID, newStatus); err != nil { return err } allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate) records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark) return txRecordStore.BatchCreate(ctx, records) }) if err != nil { return nil, err } // 通知轮询调度器状态变化(卡被分配后可能需要重新匹配配置) if s.pollingCallback != nil && len(cardIDs) > 0 { for _, cardID := range cardIDs { s.pollingCallback.OnCardStatusChanged(ctx, cardID) } } return &dto.AllocateStandaloneCardsResponse{ TotalCount: len(cards), SuccessCount: len(cardIDs), FailCount: len(failedItems), AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate), FailedItems: failedItems, }, nil } func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.RecallStandaloneCardsResponse, error) { // 1. 查询卡列表 cards, err := s.getCardsForRecall(ctx, req) if err != nil { return nil, err } if len(cards) == 0 { return &dto.RecallStandaloneCardsResponse{ TotalCount: 0, SuccessCount: 0, FailCount: 0, FailedItems: []dto.AllocationFailedItem{}, }, nil } // 2. 收集所有卡的店铺 ID,批量查询店铺信息以验证直属下级关系 shopIDSet := make(map[uint]bool) for _, card := range cards { if card.ShopID != nil { shopIDSet[*card.ShopID] = true } } shopIDs := make([]uint, 0, len(shopIDSet)) for shopID := range shopIDSet { shopIDs = append(shopIDs, shopID) } // 3. 批量查询店铺,验证哪些是直属下级 directSubordinateSet := make(map[uint]bool) if len(shopIDs) > 0 { shops, err := s.shopStore.GetByIDs(ctx, shopIDs) if err != nil { return nil, err } for _, shop := range shops { if s.isDirectSubordinate(operatorShopID, shop) { directSubordinateSet[shop.ID] = true } } } // 4. 检查绑定设备的卡 var cardIDs []uint var successCards []*model.IotCard var failedItems []dto.AllocationFailedItem boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards)) if err != nil { return nil, err } boundCardIDSet := make(map[uint]bool) for _, id := range boundCardIDs { boundCardIDSet[id] = true } // 5. 逐卡验证:绑定设备、所属店铺是否是直属下级 for _, card := range cards { if boundCardIDSet[card.ID] { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "已绑定设备的卡不能单独回收", }) continue } if card.ShopID == nil { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "卡未分配给任何店铺", }) continue } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { if !directSubordinateSet[*card.ShopID] { failedItems = append(failedItems, dto.AllocationFailedItem{ ICCID: card.ICCID, Reason: "卡所属店铺不是您的直属下级", }) continue } } cardIDs = append(cardIDs, card.ID) successCards = append(successCards, card) } if len(cardIDs) == 0 { return &dto.RecallStandaloneCardsResponse{ TotalCount: len(cards), SuccessCount: 0, FailCount: len(failedItems), FailedItems: failedItems, }, nil } // 6. 执行回收 isPlatform := operatorShopID == nil var newShopID *uint var newStatus int if isPlatform { newShopID = nil newStatus = constants.IotCardStatusInStock } else { newShopID = operatorShopID newStatus = constants.IotCardStatusDistributed } allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall) err = s.db.Transaction(func(tx *gorm.DB) error { txIotCardStore := postgres.NewIotCardStore(tx, nil) txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil) if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, newShopID, newStatus); err != nil { return err } records := s.buildRecallRecords(successCards, operatorShopID, operatorID, allocationNo, req.Remark) return txRecordStore.BatchCreate(ctx, records) }) if err != nil { return nil, err } // 通知轮询调度器状态变化(卡被回收后可能需要重新匹配配置) if s.pollingCallback != nil && len(cardIDs) > 0 { for _, cardID := range cardIDs { s.pollingCallback.OnCardStatusChanged(ctx, cardID) } } return &dto.RecallStandaloneCardsResponse{ TotalCount: len(cards), SuccessCount: len(cardIDs), FailCount: len(failedItems), AllocationNo: allocationNo, FailedItems: failedItems, }, nil } // isDirectSubordinate 检查店铺是否是操作者的可回收范围 // 平台用户可以回收所有店铺的卡,代理用户只能回收直属下级店铺的卡 func (s *Service) isDirectSubordinate(operatorShopID *uint, shop *model.Shop) bool { if operatorShopID == nil { // 平台用户:可以回收所有店铺的卡 return true } // 代理用户:直属下级是 parent_id 等于自己的店铺 return shop.ParentID != nil && *shop.ParentID == *operatorShopID } func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error { if operatorShopID != nil && *operatorShopID == targetShopID { return errors.ErrCannotAllocateToSelf } targetShop, err := s.shopStore.GetByID(ctx, targetShopID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeShopNotFound) } return err } if operatorShopID == nil { if targetShop.ParentID != nil { return errors.ErrNotDirectSubordinate } } else { if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID { return errors.ErrNotDirectSubordinate } } return nil } func (s *Service) getCardsForAllocation(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorShopID *uint) ([]*model.IotCard, error) { switch req.SelectionType { case dto.SelectionTypeList: return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs) case dto.SelectionTypeRange: return s.iotCardStore.GetStandaloneByICCIDRange(ctx, req.ICCIDStart, req.ICCIDEnd, operatorShopID) case dto.SelectionTypeFilter: filters := make(map[string]any) if req.CarrierID != nil { filters["carrier_id"] = *req.CarrierID } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo } if req.Status != nil { filters["status"] = *req.Status } return s.iotCardStore.GetStandaloneByFilters(ctx, filters, operatorShopID) default: return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式") } } func (s *Service) getCardsForRecall(ctx context.Context, req *dto.RecallStandaloneCardsRequest) ([]*model.IotCard, error) { switch req.SelectionType { case dto.SelectionTypeList: return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs) case dto.SelectionTypeRange: // 查询已分配给店铺的单卡(回收场景) return s.iotCardStore.GetDistributedStandaloneByICCIDRange(ctx, req.ICCIDStart, req.ICCIDEnd) case dto.SelectionTypeFilter: filters := make(map[string]any) if req.CarrierID != nil { filters["carrier_id"] = *req.CarrierID } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo } // 查询已分配给店铺的单卡(回收场景) return s.iotCardStore.GetDistributedStandaloneByFilters(ctx, filters) default: return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式") } } func (s *Service) extractCardIDs(cards []*model.IotCard) []uint { ids := make([]uint, len(cards)) for i, card := range cards { ids[i] = card.ID } return ids } func (s *Service) buildAllocationRecords(cards []*model.IotCard, successCardIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord { successIDSet := make(map[uint]bool) for _, id := range successCardIDs { successIDSet[id] = true } var records []*model.AssetAllocationRecord for _, card := range cards { if !successIDSet[card.ID] { continue } record := &model.AssetAllocationRecord{ AllocationNo: allocationNo, AllocationType: constants.AssetAllocationTypeAllocate, AssetType: constants.AssetTypeIotCard, AssetID: card.ID, AssetIdentifier: card.ICCID, ToOwnerType: constants.OwnerTypeShop, ToOwnerID: toShopID, OperatorID: operatorID, Remark: remark, } if fromShopID == nil { record.FromOwnerType = constants.OwnerTypePlatform record.FromOwnerID = nil } else { record.FromOwnerType = constants.OwnerTypeShop record.FromOwnerID = fromShopID } records = append(records, record) } return records } func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord { var records []*model.AssetAllocationRecord for _, card := range successCards { record := &model.AssetAllocationRecord{ AllocationNo: allocationNo, AllocationType: constants.AssetAllocationTypeRecall, AssetType: constants.AssetTypeIotCard, AssetID: card.ID, AssetIdentifier: card.ICCID, FromOwnerType: constants.OwnerTypeShop, FromOwnerID: card.ShopID, // 从卡的当前所属店铺获取 OperatorID: operatorID, Remark: remark, } if toShopID == nil { record.ToOwnerType = constants.OwnerTypePlatform record.ToOwnerID = 0 } else { record.ToOwnerType = constants.OwnerTypeShop record.ToOwnerID = *toShopID } records = append(records, record) } return records } // BatchSetSeriesBinding 批量设置卡的套餐系列绑定 func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) { cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs) if err != nil { return nil, err } if len(cards) == 0 { return &dto.BatchSetCardSeriesBindngResponse{ SuccessCount: 0, FailCount: len(req.ICCIDs), FailedItems: s.buildCardNotFoundFailedItems(req.ICCIDs), }, nil } cardMap := make(map[string]*model.IotCard) for _, card := range cards { cardMap[card.ICCID] = card } // 验证系列存在(仅当 SeriesID > 0 时) var packageSeries *model.PackageSeries if req.SeriesID > 0 { packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用") } return nil, err } if packageSeries.Status != 1 { return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用") } } var successCardIDs []uint var failedItems []dto.CardSeriesBindngFailedItem for _, iccid := range req.ICCIDs { card, exists := cardMap[iccid] if !exists { failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ ICCID: iccid, Reason: "卡不存在", }) continue } // 验证操作者权限(仅代理用户) if operatorShopID != nil && req.SeriesID > 0 { seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID) if err != nil { return nil, err } hasSeriesAllocation := false for _, alloc := range seriesAllocations { if alloc.SeriesID == req.SeriesID && alloc.Status == 1 { hasSeriesAllocation = true break } } if !hasSeriesAllocation { failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ ICCID: iccid, Reason: "您没有权限分配该套餐系列", }) continue } } // 验证卡权限(基于 card.ShopID) if operatorShopID != nil { if card.ShopID == nil || *card.ShopID != *operatorShopID { failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ ICCID: iccid, Reason: "无权操作此卡", }) continue } } successCardIDs = append(successCardIDs, card.ID) } if len(successCardIDs) > 0 { var seriesIDPtr *uint if req.SeriesID > 0 { seriesIDPtr = &req.SeriesID } if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil { return nil, err } } return &dto.BatchSetCardSeriesBindngResponse{ SuccessCount: len(successCardIDs), FailCount: len(failedItems), FailedItems: failedItems, }, nil } func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem { items := make([]dto.CardSeriesBindngFailedItem, len(iccids)) for i, iccid := range iccids { items[i] = dto.CardSeriesBindngFailedItem{ ICCID: iccid, Reason: "卡不存在", } } return items } // RefreshCardDataFromGateway 从 Gateway 完整同步卡数据 // 调用网关查询网络状态、实名状态、本月流量,并写回数据库 func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string) error { card, err := s.iotCardStore.GetByICCID(ctx, iccid) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "IoT卡不存在") } return err } syncTime := time.Now() updates := map[string]any{ "last_sync_time": syncTime, } if s.gatewayClient != nil { // 1. 查询网络状态(卡的开/停机状态) statusResp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{ CardNo: iccid, }) if err != nil { s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err)) } else { networkStatus := parseNetworkStatus(statusResp.CardStatus) updates["network_status"] = networkStatus } // 2. 查询实名状态 realnameResp, err := s.gatewayClient.QueryRealnameStatus(ctx, &gateway.CardStatusReq{ CardNo: iccid, }) if err != nil { s.logger.Warn("刷新卡数据:查询实名状态失败", zap.String("iccid", iccid), zap.Error(err)) } else { realNameStatus := parseGatewayRealnameStatus(realnameResp.RealStatus) updates["real_name_status"] = realNameStatus } // 3. 查询本月流量用量 flowResp, err := s.gatewayClient.QueryFlow(ctx, &gateway.FlowQueryReq{ CardNo: iccid, }) if err != nil { s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err)) } else { updates["current_month_usage_mb"] = flowResp.Used } } if err := s.db.WithContext(ctx).Model(&model.IotCard{}). Where("id = ?", card.ID). Updates(updates).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "更新卡数据失败") } s.logger.Info("刷新卡数据成功", zap.String("iccid", iccid), zap.Uint("card_id", card.ID)) return nil } // parseNetworkStatus 将网关返回的卡状态字符串转换为 network_status 数值 // 停机→0,其他(准备/正常)→1 func parseNetworkStatus(cardStatus string) int { if cardStatus == "停机" { return 0 } return 1 } // parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值 // true=已实名(2),false=未实名(0) func parseGatewayRealnameStatus(realStatus bool) int { if realStatus { return 2 } return 0 } // UpdatePollingStatus 更新卡的轮询状态 // 启用或禁用卡的轮询功能 func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error { card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "IoT卡不存在") } return err } // 检查是否需要更新 if card.EnablePolling == enablePolling { return nil // 状态未变化 } // 更新数据库 card.EnablePolling = enablePolling if err := s.iotCardStore.Update(ctx, card); err != nil { return err } s.logger.Info("更新卡轮询状态", zap.Uint("card_id", cardID), zap.Bool("enable_polling", enablePolling), ) // 通知轮询调度器 if s.pollingCallback != nil { if enablePolling { s.pollingCallback.OnCardEnabled(ctx, cardID) } else { s.pollingCallback.OnCardDisabled(ctx, cardID) } } return nil } // BatchUpdatePollingStatus 批量更新卡的轮询状态 func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, enablePolling bool) error { if len(cardIDs) == 0 { return nil } // 批量更新数据库 if err := s.iotCardStore.BatchUpdatePollingStatus(ctx, cardIDs, enablePolling); err != nil { return err } s.logger.Info("批量更新卡轮询状态", zap.Int("count", len(cardIDs)), zap.Bool("enable_polling", enablePolling), ) // 通知轮询调度器 if s.pollingCallback != nil { for _, cardID := range cardIDs { if enablePolling { s.pollingCallback.OnCardEnabled(ctx, cardID) } else { s.pollingCallback.OnCardDisabled(ctx, cardID) } } } return nil } // DeleteCard 删除卡(软删除) func (s *Service) DeleteCard(ctx context.Context, cardID uint) error { card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "IoT卡不存在") } return err } // 执行软删除 if err := s.iotCardStore.Delete(ctx, cardID); err != nil { return err } s.logger.Info("删除卡", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID)) // 通知轮询调度器 if s.pollingCallback != nil { s.pollingCallback.OnCardDeleted(ctx, cardID) } return nil } // BatchDeleteCards 批量删除卡(软删除) func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error { if len(cardIDs) == 0 { return nil } // 批量软删除 if err := s.iotCardStore.BatchDelete(ctx, cardIDs); err != nil { return err } s.logger.Info("批量删除卡", zap.Int("count", len(cardIDs))) // 通知轮询调度器 if s.pollingCallback != nil { for _, cardID := range cardIDs { s.pollingCallback.OnCardDeleted(ctx, cardID) } } return nil }