diff --git a/cmd/api/main.go b/cmd/api/main.go index a75eb2e..b001114 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -204,15 +204,16 @@ func createFiberApp(cfg *config.Config, appLogger *zap.Logger) *fiber.App { registerTimeParserCompat() return fiber.New(fiber.Config{ - AppName: "君鸿卡管系统 v1.0.0", - StrictRouting: true, - CaseSensitive: true, - JSONEncoder: sonic.Marshal, - JSONDecoder: sonic.Unmarshal, - Prefork: cfg.Server.Prefork, - ReadTimeout: cfg.Server.ReadTimeout, - WriteTimeout: cfg.Server.WriteTimeout, - ErrorHandler: internalMiddleware.ErrorHandler(appLogger), + AppName: "君鸿卡管系统 v1.0.0", + StrictRouting: true, + CaseSensitive: true, + JSONEncoder: sonic.Marshal, + JSONDecoder: sonic.Unmarshal, + EnableSplittingOnParsers: true, + Prefork: cfg.Server.Prefork, + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + ErrorHandler: internalMiddleware.ErrorHandler(appLogger), }) } diff --git a/internal/model/dto/device_dto.go b/internal/model/dto/device_dto.go index 02eb880..0b42bb4 100644 --- a/internal/model/dto/device_dto.go +++ b/internal/model/dto/device_dto.go @@ -8,7 +8,8 @@ type ListDeviceRequest struct { VirtualNo string `json:"virtual_no" query:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"虚拟号(模糊查询)"` DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"` Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` - ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID (NULL表示平台库存)"` + ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID(兼容旧参数,单选;NULL表示平台库存)"` + ShopIDs []uint `json:"shop_ids" query:"shop_ids" validate:"omitempty,dive,min=1" description:"店铺ID列表(多选)"` SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"` BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"` DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"` diff --git a/internal/model/dto/iot_card_dto.go b/internal/model/dto/iot_card_dto.go index 92dd8a9..0b3a7ad 100644 --- a/internal/model/dto/iot_card_dto.go +++ b/internal/model/dto/iot_card_dto.go @@ -7,7 +7,8 @@ type ListStandaloneIotCardRequest struct { PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"` Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` CarrierID *uint `json:"carrier_id" query:"carrier_id" description:"运营商ID"` - ShopID *uint `json:"shop_id" query:"shop_id" description:"分销商ID"` + ShopID *uint `json:"shop_id" query:"shop_id" description:"分销商ID(兼容旧参数,单选)"` + ShopIDs []uint `json:"shop_ids" query:"shop_ids" validate:"omitempty,dive,min=1" description:"分销商ID列表(多选)"` SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"` ICCID string `json:"iccid" query:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"ICCID(模糊查询)"` MSISDN string `json:"msisdn" query:"msisdn" validate:"omitempty,max=20" maxLength:"20" description:"卡接入号(模糊查询)"` diff --git a/internal/service/device/service.go b/internal/service/device/service.go index 4f6b70c..dc27647 100644 --- a/internal/service/device/service.go +++ b/internal/service/device/service.go @@ -93,8 +93,15 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li if req.Status != nil { filters["status"] = *req.Status } - if req.ShopID != nil { - filters["shop_id"] = req.ShopID + shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs) + if hasShopIDs { + filters["shop_ids"] = shopIDs + } else if req.ShopID != nil { + if *req.ShopID == 0 { + filters["shop_ids"] = []uint{} + } else { + filters["shop_id"] = req.ShopID + } } if req.BatchNo != "" { filters["batch_no"] = req.BatchNo @@ -141,6 +148,25 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li }, nil } +func normalizeShopIDs(ids []uint) ([]uint, bool) { + if len(ids) == 0 { + return nil, false + } + seen := make(map[uint]struct{}, len(ids)) + result := make([]uint, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, exists := seen[id]; exists { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result, true +} + // Get 获取设备详情 func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error) { device, err := s.deviceStore.GetByID(ctx, id) diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index 7de259b..bbbd174 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -156,8 +156,15 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot if req.CarrierID != nil { filters["carrier_id"] = *req.CarrierID } - if req.ShopID != nil { - filters["shop_id"] = *req.ShopID + shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs) + if hasShopIDs { + filters["shop_ids"] = shopIDs + } else if req.ShopID != nil { + if *req.ShopID == 0 { + filters["shop_ids"] = []uint{} + } else { + filters["shop_id"] = *req.ShopID + } } if req.ICCID != "" { filters["iccid"] = req.ICCID @@ -200,8 +207,16 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot 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 + if err == nil { + if hasShopIDs { + scopedShopIDs := intersectShopIDs(shopIDs, subordinateIDs) + filters["shop_ids"] = scopedShopIDs + if len(scopedShopIDs) > 1 { + filters["subordinate_shop_ids"] = scopedShopIDs + } + } else if len(subordinateIDs) > 1 { + filters["subordinate_shop_ids"] = subordinateIDs + } } } } @@ -229,6 +244,42 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot }, nil } +func normalizeShopIDs(ids []uint) ([]uint, bool) { + if len(ids) == 0 { + return nil, false + } + seen := make(map[uint]struct{}, len(ids)) + result := make([]uint, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, exists := seen[id]; exists { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result, true +} + +func intersectShopIDs(selectedIDs, allowedIDs []uint) []uint { + if len(selectedIDs) == 0 || len(allowedIDs) == 0 { + return []uint{} + } + allowed := make(map[uint]struct{}, len(allowedIDs)) + for _, id := range allowedIDs { + allowed[id] = struct{}{} + } + result := make([]uint, 0, len(selectedIDs)) + for _, id := range selectedIDs { + if _, ok := allowed[id]; ok { + result = append(result, id) + } + } + return result +} + // GetByICCID 通过 ICCID 获取单卡详情 func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDetailResponse, error) { card, err := s.iotCardStore.GetByICCID(ctx, iccid) diff --git a/internal/store/postgres/device_store.go b/internal/store/postgres/device_store.go index c911414..7885ac0 100644 --- a/internal/store/postgres/device_store.go +++ b/internal/store/postgres/device_store.go @@ -109,7 +109,13 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter if status, ok := filters["status"].(int); ok && status > 0 { query = query.Where("status = ?", status) } - if shopID, ok := filters["shop_id"].(*uint); ok { + if shopIDs, ok := filters["shop_ids"].([]uint); ok { + if len(shopIDs) == 0 { + query = query.Where("1 = 0") + } else { + query = query.Where("shop_id IN ?", shopIDs) + } + } else if shopID, ok := filters["shop_id"].(*uint); ok { if shopID == nil { query = query.Where("shop_id IS NULL") } else { diff --git a/internal/store/postgres/iot_card_store.go b/internal/store/postgres/iot_card_store.go index e7ee5d1..1c415fb 100644 --- a/internal/store/postgres/iot_card_store.go +++ b/internal/store/postgres/iot_card_store.go @@ -677,7 +677,7 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts defer wg.Done() q := s.db.WithContext(ctx).Model(&model.IotCard{}). - Where("is_standalone = true AND deleted_at IS NULL AND shop_id = ?", sid) + Where("deleted_at IS NULL AND shop_id = ?", sid) q = s.applyStandaloneFilters(ctx, q, filters) var ids []cardIDWithTime @@ -692,7 +692,7 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts var count int64 if !hasCachedTotal { countQ := s.db.WithContext(ctx).Model(&model.IotCard{}). - Where("is_standalone = true AND deleted_at IS NULL AND shop_id = ?", sid) + Where("deleted_at IS NULL AND shop_id = ?", sid) countQ = s.applyStandaloneFilters(ctx, countQ, filters) if err := countQ.Count(&count).Error; err != nil { results[idx] = shopResult{err: err} @@ -781,8 +781,8 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts } // applyStandaloneFilters 应用独立卡列表的通用过滤条件 -// 注意:不包含 shop_id、deleted_at 条件(由调用方控制) -// 也不包含 subordinate_shop_ids(仅用于路由选择,不作为查询条件) +// 注意:不包含 deleted_at 条件(由调用方控制) +// subordinate_shop_ids 仅用于路由选择,不作为查询条件 // is_standalone 仅在 filters["is_standalone"] 非 nil 时拼接(不传则不过滤) func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.DB, filters map[string]any) *gorm.DB { // is_standalone 为可选过滤条件,仅在明确传入时应用 @@ -797,8 +797,14 @@ func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.D if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 { query = query.Where("carrier_id = ?", carrierID) } - // 并行路径下 shop_id 已由调用方设置,此处仅处理显式的 shop_id 过滤 - if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 { + // 并行路径下 shop_id 已由调用方设置,此处仅处理显式店铺筛选。 + if shopIDs, ok := filters["shop_ids"].([]uint); ok { + if len(shopIDs) == 0 { + query = query.Where("1 = 0") + } else { + query = query.Where("shop_id IN ?", shopIDs) + } + } else if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 { query = query.Where("shop_id = ?", shopID) } if iccid, ok := filters["iccid"].(string); ok && iccid != "" { diff --git a/internal/store/postgres/shop_store.go b/internal/store/postgres/shop_store.go index 165f438..0a0ded9 100644 --- a/internal/store/postgres/shop_store.go +++ b/internal/store/postgres/shop_store.go @@ -236,6 +236,14 @@ func (s *ShopStore) ListForCascade(ctx context.Context, shopName string, parentI if parentID != nil { query = query.Where("parent_id = ?", *parentID) + } else if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent { + shopID := middleware.GetShopIDFromContext(ctx) + if shopID == 0 { + query = query.Where("1 = 0") + } else { + // 代理账号的级联根节点是自己的店铺,避免默认根查询落到平台顶级店铺。 + query = query.Where("id = ?", shopID) + } } else { query = query.Where("parent_id IS NULL") }