联级,多选
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m52s

This commit is contained in:
2026-05-09 15:06:27 +08:00
parent 6a6672d0e4
commit 09cf0be86e
8 changed files with 124 additions and 24 deletions

View File

@@ -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:"设备类型"`

View File

@@ -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:"卡接入号(模糊查询)"`

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 != "" {

View File

@@ -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")
}