批量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s

This commit is contained in:
2026-05-11 14:54:23 +08:00
parent 93200a9074
commit 1350a9ed79
8 changed files with 613 additions and 210 deletions

View File

@@ -982,7 +982,14 @@ func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *ui
// BatchSetSeriesBinding 批量设置卡的套餐系列绑定
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
selectionType, err := normalizeCardSeriesBindingSelection(req)
if err != nil {
return nil, err
}
cards, err := s.getCardsForSeriesBinding(ctx, req, selectionType)
batchTotal := cardSeriesBindingBatchTotal(req, selectionType, cards)
auditData := cardSeriesBindingAuditData(req, selectionType)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
@@ -991,44 +998,38 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
BatchTotal: batchTotal,
AfterData: auditData,
})
return nil, err
}
if len(cards) == 0 {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorMsg: "卡不存在",
BatchTotal: len(req.ICCIDs),
FailCount: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
failedItems := []dto.CardSeriesBindngFailedItem{}
if selectionType == dto.SelectionTypeList {
failedItems = s.buildCardNotFoundFailedItems(req.ICCIDs)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorMsg: "卡不存在",
BatchTotal: batchTotal,
FailCount: len(failedItems),
AfterData: auditData,
})
}
return &dto.BatchSetCardSeriesBindngResponse{
SuccessCount: 0,
FailCount: len(req.ICCIDs),
FailedItems: s.buildCardNotFoundFailedItems(req.ICCIDs),
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
cardMap := make(map[string]*model.IotCard)
for _, card := range cards {
cardMap[card.ICCID] = card
}
cardMap := indexCardsByICCID(cards)
// 验证系列存在(仅当 SeriesID > 0 时)
var packageSeries *model.PackageSeries
if req.SeriesID > 0 {
packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
packageSeries, err := s.packageSeriesStore.GetByID(ctx, req.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
denyErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
@@ -1039,11 +1040,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
BatchTotal: batchTotal,
AfterData: auditData,
})
return nil, denyErr
}
@@ -1054,11 +1052,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
BatchTotal: batchTotal,
AfterData: auditData,
})
return nil, err
}
@@ -1071,11 +1066,8 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
BatchTotal: batchTotal,
AfterData: auditData,
})
return nil, denyErr
}
@@ -1083,53 +1075,59 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
var successCardIDs []uint
var failedItems []dto.CardSeriesBindngFailedItem
successCardIDSet := make(map[uint]struct{})
for _, iccid := range req.ICCIDs {
card, exists := cardMap[iccid]
if !exists {
hasSeriesAllocation := true
if operatorShopID != nil && req.SeriesID > 0 {
hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
if err != nil {
return nil, err
}
}
addCard := func(card *model.IotCard, requestedICCID string) {
if card == nil {
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
ICCID: iccid,
ICCID: requestedICCID,
Reason: "卡不存在",
})
continue
return
}
// 验证操作者权限(仅代理用户)
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
}
if !hasSeriesAllocation {
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
ICCID: card.ICCID,
Reason: "您没有权限分配该套餐系列",
})
return
}
// 验证卡权限(基于 card.ShopID
if operatorShopID != nil {
if card.ShopID == nil || *card.ShopID != *operatorShopID {
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
ICCID: iccid,
Reason: "无权操作此卡",
})
continue
}
// 代理只能操作自己店铺名下的卡,保持旧接口权限语义不变。
if operatorShopID != nil && (card.ShopID == nil || *card.ShopID != *operatorShopID) {
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
ICCID: card.ICCID,
Reason: "无权操作此卡",
})
return
}
if _, exists := successCardIDSet[card.ID]; exists {
return
}
successCardIDSet[card.ID] = struct{}{}
successCardIDs = append(successCardIDs, card.ID)
}
if selectionType == dto.SelectionTypeList {
for _, iccid := range req.ICCIDs {
addCard(cardMap[iccid], iccid)
}
} else {
for _, card := range cards {
addCard(card, card.ICCID)
}
}
if len(successCardIDs) > 0 {
var seriesIDPtr *uint
if req.SeriesID > 0 {
@@ -1143,13 +1141,14 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
BatchTotal: batchTotal,
SuccessCount: len(successCardIDs),
FailCount: len(failedItems),
AfterData: map[string]any{
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
"selection_type": selectionType,
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
},
})
return nil, err
@@ -1184,16 +1183,17 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
OperationDesc: operationDesc,
ResultStatus: resultStatus,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
BatchTotal: batchTotal,
SuccessCount: len(successCardIDs),
FailCount: len(failedItems),
BeforeData: map[string]any{
"cards": beforeCards,
},
AfterData: map[string]any{
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
"selection_type": selectionType,
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
},
})
@@ -1204,6 +1204,185 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
}, nil
}
func normalizeCardSeriesBindingSelection(req *dto.BatchSetCardSeriesBindngRequest) (string, error) {
switch req.SelectionType {
case dto.SelectionTypeList:
if len(req.ICCIDs) == 0 {
return "", errors.New(errors.CodeInvalidParam, "selection_type=list时iccids不能为空")
}
return dto.SelectionTypeList, nil
case dto.SelectionTypeRange:
if req.ICCIDStart == "" || req.ICCIDEnd == "" {
return "", errors.New(errors.CodeInvalidParam, "selection_type=range时iccid_start和iccid_end不能为空")
}
if req.ICCIDStart > req.ICCIDEnd {
return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
}
return dto.SelectionTypeRange, nil
case dto.SelectionTypeFilter:
return dto.SelectionTypeFilter, nil
case "":
if len(req.ICCIDs) > 0 {
return dto.SelectionTypeList, nil
}
if req.ICCIDStart != "" || req.ICCIDEnd != "" {
if req.ICCIDStart == "" || req.ICCIDEnd == "" {
return "", errors.New(errors.CodeInvalidParam, "iccid_start和iccid_end必须同时传入")
}
if req.ICCIDStart > req.ICCIDEnd {
return "", errors.New(errors.CodeInvalidParam, "ICCID起始号不能大于结束号")
}
return dto.SelectionTypeRange, nil
}
if hasCardSeriesBindingFilters(req) {
return dto.SelectionTypeFilter, nil
}
return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的卡")
default:
return "", errors.New(errors.CodeInvalidParam, "无效的选卡方式")
}
}
func hasCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) bool {
return req.Status != nil ||
req.CarrierID != nil ||
req.ShopID != nil ||
len(req.ShopIDs) > 0 ||
req.FilterSeriesID != nil ||
req.ICCID != "" ||
req.MSISDN != "" ||
req.IsStandalone != nil ||
req.BatchNo != "" ||
req.PackageID != nil ||
req.IsDistributed != nil ||
req.IsReplaced != nil ||
req.CarrierName != ""
}
func (s *Service) getCardsForSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, selectionType string) ([]*model.IotCard, error) {
switch selectionType {
case dto.SelectionTypeList:
return s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
case dto.SelectionTypeRange:
filters := map[string]any{
"iccid_start": req.ICCIDStart,
"iccid_end": req.ICCIDEnd,
}
return s.iotCardStore.GetBySeriesBindingFilters(ctx, filters)
case dto.SelectionTypeFilter:
return s.iotCardStore.GetBySeriesBindingFilters(ctx, buildCardSeriesBindingFilters(req))
default:
return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
}
}
func buildCardSeriesBindingFilters(req *dto.BatchSetCardSeriesBindngRequest) map[string]any {
filters := make(map[string]any)
if req.Status != nil {
filters["status"] = *req.Status
}
if req.CarrierID != nil {
filters["carrier_id"] = *req.CarrierID
}
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.FilterSeriesID != nil {
filters["series_id"] = *req.FilterSeriesID
}
if req.ICCID != "" {
filters["iccid"] = req.ICCID
}
if req.MSISDN != "" {
filters["msisdn"] = req.MSISDN
}
if req.IsStandalone != nil {
filters["is_standalone"] = req.IsStandalone
}
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.CarrierName != "" {
filters["carrier_name"] = req.CarrierName
}
return filters
}
func (s *Service) hasAvailableSeriesAllocation(ctx context.Context, shopID uint, seriesID uint) (bool, error) {
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
if err != nil {
return false, err
}
for _, alloc := range seriesAllocations {
if alloc.SeriesID == seriesID && alloc.Status == 1 {
return true, nil
}
}
return false, nil
}
func indexCardsByICCID(cards []*model.IotCard) map[string]*model.IotCard {
cardMap := make(map[string]*model.IotCard, len(cards))
for _, card := range cards {
if card.ICCID != "" {
cardMap[card.ICCID] = card
}
if card.ICCID19 != "" {
cardMap[card.ICCID19] = card
}
if card.ICCID20 != nil && *card.ICCID20 != "" {
cardMap[*card.ICCID20] = card
}
}
return cardMap
}
func cardSeriesBindingBatchTotal(req *dto.BatchSetCardSeriesBindngRequest, selectionType string, cards []*model.IotCard) int {
if selectionType == dto.SelectionTypeList {
return len(req.ICCIDs)
}
return len(cards)
}
func cardSeriesBindingAuditData(req *dto.BatchSetCardSeriesBindngRequest, selectionType string) map[string]any {
data := map[string]any{
"selection_type": selectionType,
"series_id": req.SeriesID,
}
switch selectionType {
case dto.SelectionTypeList:
data["iccids"] = req.ICCIDs
case dto.SelectionTypeRange:
data["iccid_start"] = req.ICCIDStart
data["iccid_end"] = req.ICCIDEnd
case dto.SelectionTypeFilter:
data["filters"] = buildCardSeriesBindingFilters(req)
}
return data
}
func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem {
items := make([]dto.CardSeriesBindngFailedItem, len(iccids))
for i, iccid := range iccids {