package enterprise_card import ( "context" "time" "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/middleware" "gorm.io/gorm" ) type Service struct { db *gorm.DB enterpriseStore *postgres.EnterpriseStore enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore } func New( db *gorm.DB, enterpriseStore *postgres.EnterpriseStore, enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore, ) *Service { return &Service{ db: db, enterpriseStore: enterpriseStore, enterpriseCardAuthStore: enterpriseCardAuthStore, } } func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } var iotCards []model.IotCard if err := s.db.WithContext(ctx).Where("iccid IN ?", req.ICCIDs).Find(&iotCards).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败") } cardMap := make(map[string]*model.IotCard) cardIDMap := make(map[uint]*model.IotCard) for i := range iotCards { cardMap[iotCards[i].ICCID] = &iotCards[i] cardIDMap[iotCards[i].ID] = &iotCards[i] } cardIDs := make([]uint, 0, len(iotCards)) for _, card := range iotCards { cardIDs = append(cardIDs, card.ID) } var bindings []model.DeviceSimBinding if len(cardIDs) > 0 { s.db.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = 1", cardIDs).Find(&bindings) } cardToDevice := make(map[uint]bool) for _, binding := range bindings { cardToDevice[binding.IotCardID] = true } resp := &dto.AllocateCardsPreviewResp{ StandaloneCards: make([]dto.StandaloneCard, 0), FailedItems: make([]dto.FailedItem, 0), } for _, iccid := range req.ICCIDs { card, exists := cardMap[iccid] if !exists { resp.FailedItems = append(resp.FailedItems, dto.FailedItem{ ICCID: iccid, Reason: "卡不存在", }) continue } if cardToDevice[card.ID] { resp.FailedItems = append(resp.FailedItems, dto.FailedItem{ ICCID: iccid, Reason: "该卡已绑定设备,请使用设备授权功能", }) continue } resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{ ICCID: card.ICCID, IotCardID: card.ID, MSISDN: card.MSISDN, CarrierID: card.CarrierID, StatusName: constants.GetIotCardStatusName(card.Status), }) } resp.Summary = dto.AllocatePreviewSummary{ StandaloneCardCount: len(resp.StandaloneCards), DeviceCount: 0, DeviceCardCount: 0, TotalCardCount: len(resp.StandaloneCards), FailedCount: len(resp.FailedItems), } return resp, nil } func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsReq) (*dto.AllocateCardsResp, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs}) if err != nil { return nil, err } resp := &dto.AllocateCardsResp{ FailedItems: preview.FailedItems, FailCount: len(preview.FailedItems), } cardIDsToAllocate := make([]uint, 0) for _, card := range preview.StandaloneCards { cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID) } existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败") } now := time.Now() userType := middleware.GetUserTypeFromContext(ctx) auths := make([]*model.EnterpriseCardAuthorization, 0) for _, cardID := range cardIDsToAllocate { if existingAuths[cardID] { continue } auths = append(auths, &model.EnterpriseCardAuthorization{ EnterpriseID: enterpriseID, CardID: cardID, AuthorizedBy: currentUserID, AuthorizedAt: now, AuthorizerType: userType, Remark: req.Remark, }) } if len(auths) > 0 { if err := s.enterpriseCardAuthStore.BatchCreate(ctx, auths); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "创建授权记录失败") } } resp.SuccessCount = len(cardIDsToAllocate) return resp, nil } func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.RecallCardsReq) (*dto.RecallCardsResp, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } var iotCards []model.IotCard if err := s.db.WithContext(ctx).Where("iccid IN ?", req.ICCIDs).Find(&iotCards).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败") } cardMap := make(map[string]*model.IotCard) cardIDMap := make(map[uint]*model.IotCard) cardIDs := make([]uint, 0, len(iotCards)) for i := range iotCards { cardMap[iotCards[i].ICCID] = &iotCards[i] cardIDMap[iotCards[i].ID] = &iotCards[i] cardIDs = append(cardIDs, iotCards[i].ID) } existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败") } resp := &dto.RecallCardsResp{ FailedItems: make([]dto.FailedItem, 0), RecalledDevices: make([]dto.RecalledDevice, 0), } cardIDsToRecall := make([]uint, 0) for _, iccid := range req.ICCIDs { card, exists := cardMap[iccid] if !exists { resp.FailedItems = append(resp.FailedItems, dto.FailedItem{ ICCID: iccid, Reason: "卡不存在", }) continue } if !existingAuths[card.ID] { resp.FailedItems = append(resp.FailedItems, dto.FailedItem{ ICCID: iccid, Reason: "该卡未授权给此企业", }) continue } cardIDsToRecall = append(cardIDsToRecall, card.ID) } if len(cardIDsToRecall) > 0 { if err := s.enterpriseCardAuthStore.BatchUpdateStatus(ctx, enterpriseID, cardIDsToRecall, 0); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "回收授权失败") } } resp.SuccessCount = len(cardIDsToRecall) resp.FailCount = len(resp.FailedItems) return resp, nil } func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.EnterpriseCardListReq) (*dto.EnterpriseCardPageResult, error) { _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } cardIDs, err := s.enterpriseCardAuthStore.ListCardIDsByEnterprise(ctx, enterpriseID) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询授权卡ID失败") } if len(cardIDs) == 0 { return &dto.EnterpriseCardPageResult{ Items: make([]dto.EnterpriseCardItem, 0), Total: 0, Page: req.Page, Size: req.PageSize, }, nil } page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } query := s.db.WithContext(ctx).Model(&model.IotCard{}).Where("id IN ?", cardIDs) if req.Status != nil { query = query.Where("status = ?", *req.Status) } if req.CarrierID != nil { query = query.Where("carrier_id = ?", *req.CarrierID) } if req.ICCID != "" { query = query.Where("iccid LIKE ?", "%"+req.ICCID+"%") } var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "统计卡数量失败") } var cards []model.IotCard offset := (page - 1) * pageSize if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&cards).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡列表失败") } // 批量查询各卡当前生效的主套餐名称(取 package_name 快照,避免 JOIN tb_package) pageCardIDs := make([]uint, 0, len(cards)) for _, card := range cards { pageCardIDs = append(pageCardIDs, card.ID) } packageNameMap := s.buildPackageNameMap(ctx, pageCardIDs) items := make([]dto.EnterpriseCardItem, 0, len(cards)) for _, card := range cards { items = append(items, dto.EnterpriseCardItem{ ID: card.ID, ICCID: card.ICCID, MSISDN: card.MSISDN, CarrierID: card.CarrierID, CarrierName: card.CarrierName, PackageName: packageNameMap[card.ID], Status: card.Status, StatusName: constants.GetIotCardStatusName(card.Status), NetworkStatus: card.NetworkStatus, NetworkStatusName: constants.GetNetworkStatusName(card.NetworkStatus), }) } return &dto.EnterpriseCardPageResult{ Items: items, Total: total, Page: page, Size: pageSize, }, nil } // buildPackageNameMap 批量查询卡列表当前生效主套餐名称,返回 map[iotCardID]packageName func (s *Service) buildPackageNameMap(ctx context.Context, cardIDs []uint) map[uint]string { result := make(map[uint]string, len(cardIDs)) if len(cardIDs) == 0 { return result } type usageRow struct { IotCardID uint `gorm:"column:iot_card_id"` PackageName string `gorm:"column:package_name"` } // 每张卡取优先级最低(数字最小)的生效中主套餐,使用 DISTINCT ON 保证每卡仅一条 var rows []usageRow s.db.WithContext(ctx).Raw(` SELECT DISTINCT ON (iot_card_id) iot_card_id, package_name FROM tb_package_usage WHERE iot_card_id IN ? AND status = 1 AND master_usage_id IS NULL AND deleted_at IS NULL ORDER BY iot_card_id, priority ASC `, cardIDs).Scan(&rows) for _, row := range rows { result[row.IotCardID] = row.PackageName } return result } func (s *Service) SuspendCard(ctx context.Context, enterpriseID, cardID uint) error { return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 0) } func (s *Service) ResumeCard(ctx context.Context, enterpriseID, cardID uint) error { return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 1) } func (s *Service) updateCardNetworkStatus(ctx context.Context, enterpriseID, cardID uint, networkStatus int) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } auth, err := s.enterpriseCardAuthStore.GetByEnterpriseAndCard(ctx, enterpriseID, cardID) if err != nil || auth.RevokedAt != nil { return errors.New(errors.CodeForbidden, "无权限操作此卡") } return s.db.WithContext(ctx).Model(&model.IotCard{}). Where("id = ?", cardID). Update("network_status", networkStatus).Error }