feat: 单卡回收接口优化 & 店铺禁用登录拦截
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s

单卡回收优化:
- 移除 from_shop_id 参数,系统自动识别卡所属店铺
- 保持直属下级限制,混合来源分别处理
- 新增 GetDistributedStandaloneByICCIDRange/GetDistributedStandaloneByFilters 方法

店铺禁用拦截:
- 登录时检查关联店铺状态,禁用店铺无法登录
- 新增 CodeShopDisabled 错误码

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 15:54:53 +08:00
parent 25e9749564
commit 037595c22e
7 changed files with 121 additions and 29 deletions

View File

@@ -384,10 +384,7 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
}
func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.RecallStandaloneCardsResponse, error) {
if err := s.validateDirectSubordinate(ctx, operatorShopID, req.FromShopID); err != nil {
return nil, err
}
// 1. 查询卡列表
cards, err := s.getCardsForRecall(ctx, req)
if err != nil {
return nil, err
@@ -402,7 +399,35 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
}, 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))
@@ -414,6 +439,7 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
boundCardIDSet[id] = true
}
// 5. 逐卡验证:绑定设备、所属店铺是否是直属下级
for _, card := range cards {
if boundCardIDSet[card.ID] {
failedItems = append(failedItems, dto.AllocationFailedItem{
@@ -423,15 +449,24 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
continue
}
if card.ShopID == nil || *card.ShopID != req.FromShopID {
if card.ShopID == nil {
failedItems = append(failedItems, dto.AllocationFailedItem{
ICCID: card.ICCID,
Reason: "卡不属于指定的店铺",
Reason: "卡未分配给任何店铺",
})
continue
}
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 {
@@ -443,6 +478,7 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
}, nil
}
// 6. 执行回收
isPlatform := operatorShopID == nil
var newShopID *uint
var newStatus int
@@ -454,6 +490,8 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
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)
@@ -462,8 +500,7 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
return err
}
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)
records := s.buildRecallRecords(cards, cardIDs, req.FromShopID, operatorShopID, operatorID, allocationNo, req.Remark)
records := s.buildRecallRecords(successCards, operatorShopID, operatorID, allocationNo, req.Remark)
return txRecordStore.BatchCreate(ctx, records)
})
@@ -482,11 +519,21 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
TotalCount: len(cards),
SuccessCount: len(cardIDs),
FailCount: len(failedItems),
AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall),
AllocationNo: allocationNo,
FailedItems: failedItems,
}, nil
}
// isDirectSubordinate 检查店铺是否是操作者的直属下级
func (s *Service) isDirectSubordinate(operatorShopID *uint, shop *model.Shop) bool {
if operatorShopID == nil {
// 平台用户:直属下级是 parent_id 为空的店铺
return shop.ParentID == nil
}
// 代理用户:直属下级是 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
@@ -537,12 +584,12 @@ func (s *Service) getCardsForAllocation(ctx context.Context, req *dto.AllocateSt
}
func (s *Service) getCardsForRecall(ctx context.Context, req *dto.RecallStandaloneCardsRequest) ([]*model.IotCard, error) {
fromShopID := req.FromShopID
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, &fromShopID)
// 查询已分配给店铺的单卡(回收场景)
return s.iotCardStore.GetDistributedStandaloneByICCIDRange(ctx, req.ICCIDStart, req.ICCIDEnd)
case dto.SelectionTypeFilter:
filters := make(map[string]any)
if req.CarrierID != nil {
@@ -551,7 +598,8 @@ func (s *Service) getCardsForRecall(ctx context.Context, req *dto.RecallStandalo
if req.BatchNo != "" {
filters["batch_no"] = req.BatchNo
}
return s.iotCardStore.GetStandaloneByFilters(ctx, filters, &fromShopID)
// 查询已分配给店铺的单卡(回收场景)
return s.iotCardStore.GetDistributedStandaloneByFilters(ctx, filters)
default:
return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
}
@@ -603,18 +651,9 @@ func (s *Service) buildAllocationRecords(cards []*model.IotCard, successCardIDs
return records
}
func (s *Service) buildRecallRecords(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
}
func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
var records []*model.AssetAllocationRecord
for _, card := range cards {
if !successIDSet[card.ID] {
continue
}
for _, card := range successCards {
record := &model.AssetAllocationRecord{
AllocationNo: allocationNo,
AllocationType: constants.AssetAllocationTypeRecall,
@@ -622,7 +661,7 @@ func (s *Service) buildRecallRecords(cards []*model.IotCard, successCardIDs []ui
AssetID: card.ID,
AssetIdentifier: card.ICCID,
FromOwnerType: constants.OwnerTypeShop,
FromOwnerID: &fromShopID,
FromOwnerID: card.ShopID, // 从卡的当前所属店铺获取
OperatorID: operatorID,
Remark: remark,
}