All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m2s
主要变更: - 在 StandaloneIotCardResponse 和 DeviceResponse 中添加 series_name 字段 - 在 iot_card 和 device service 中添加 loadSeriesNames 方法批量加载系列名称 - 更新相关方法以支持 series_name 的填充 其他变更: - 新增 OpenSpec 测试生成和共识锁定 skill - 新增 MCP 配置文件 - 更新 CLAUDE.md 项目规范文档 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
749 lines
21 KiB
Go
749 lines
21 KiB
Go
package iot_card
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||
"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"
|
||
"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"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
iotCardStore *postgres.IotCardStore
|
||
shopStore *postgres.ShopStore
|
||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
packageSeriesStore *postgres.PackageSeriesStore
|
||
gatewayClient *gateway.Client
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
iotCardStore *postgres.IotCardStore,
|
||
shopStore *postgres.ShopStore,
|
||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
packageSeriesStore *postgres.PackageSeriesStore,
|
||
gatewayClient *gateway.Client,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
iotCardStore: iotCardStore,
|
||
shopStore: shopStore,
|
||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
packageSeriesStore: packageSeriesStore,
|
||
gatewayClient: gatewayClient,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) {
|
||
page := req.Page
|
||
pageSize := req.PageSize
|
||
if page == 0 {
|
||
page = 1
|
||
}
|
||
if pageSize == 0 {
|
||
pageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
opts := &store.QueryOptions{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
filters := make(map[string]interface{})
|
||
if req.Status != nil {
|
||
filters["status"] = *req.Status
|
||
}
|
||
if req.CarrierID != nil {
|
||
filters["carrier_id"] = *req.CarrierID
|
||
}
|
||
if req.ShopID != nil {
|
||
filters["shop_id"] = *req.ShopID
|
||
}
|
||
if req.ICCID != "" {
|
||
filters["iccid"] = req.ICCID
|
||
}
|
||
if req.MSISDN != "" {
|
||
filters["msisdn"] = req.MSISDN
|
||
}
|
||
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.SeriesID != nil {
|
||
filters["series_id"] = *req.SeriesID
|
||
}
|
||
|
||
cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopNames(ctx, cards)
|
||
seriesMap := s.loadSeriesNames(ctx, cards)
|
||
|
||
list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
|
||
for _, card := range cards {
|
||
item := s.toStandaloneResponse(card, shopMap, seriesMap)
|
||
list = append(list, item)
|
||
}
|
||
|
||
totalPages := int(total) / pageSize
|
||
if int(total)%pageSize > 0 {
|
||
totalPages++
|
||
}
|
||
|
||
return &dto.ListStandaloneIotCardResponse{
|
||
List: list,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
TotalPages: totalPages,
|
||
}, nil
|
||
}
|
||
|
||
// GetByICCID 通过 ICCID 获取单卡详情
|
||
func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDetailResponse, error) {
|
||
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "IoT卡不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopNames(ctx, []*model.IotCard{card})
|
||
seriesMap := s.loadSeriesNames(ctx, []*model.IotCard{card})
|
||
standaloneResp := s.toStandaloneResponse(card, shopMap, seriesMap)
|
||
|
||
return &dto.IotCardDetailResponse{
|
||
StandaloneIotCardResponse: *standaloneResp,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) loadShopNames(ctx context.Context, cards []*model.IotCard) map[uint]string {
|
||
shopIDs := make([]uint, 0)
|
||
shopIDSet := make(map[uint]bool)
|
||
|
||
for _, card := range cards {
|
||
if card.ShopID != nil && *card.ShopID > 0 && !shopIDSet[*card.ShopID] {
|
||
shopIDs = append(shopIDs, *card.ShopID)
|
||
shopIDSet[*card.ShopID] = true
|
||
}
|
||
}
|
||
|
||
shopMap := make(map[uint]string)
|
||
if len(shopIDs) > 0 {
|
||
var shops []model.Shop
|
||
s.db.WithContext(ctx).Where("id IN ?", shopIDs).Find(&shops)
|
||
for _, shop := range shops {
|
||
shopMap[shop.ID] = shop.ShopName
|
||
}
|
||
}
|
||
|
||
return shopMap
|
||
}
|
||
|
||
func (s *Service) loadSeriesNames(ctx context.Context, cards []*model.IotCard) map[uint]string {
|
||
seriesIDs := make([]uint, 0)
|
||
seriesIDSet := make(map[uint]bool)
|
||
|
||
for _, card := range cards {
|
||
if card.SeriesID != nil && *card.SeriesID > 0 && !seriesIDSet[*card.SeriesID] {
|
||
seriesIDs = append(seriesIDs, *card.SeriesID)
|
||
seriesIDSet[*card.SeriesID] = true
|
||
}
|
||
}
|
||
|
||
seriesMap := make(map[uint]string)
|
||
if len(seriesIDs) > 0 {
|
||
var seriesList []model.PackageSeries
|
||
s.db.WithContext(ctx).Where("id IN ?", seriesIDs).Find(&seriesList)
|
||
for _, series := range seriesList {
|
||
seriesMap[series.ID] = series.SeriesName
|
||
}
|
||
}
|
||
|
||
return seriesMap
|
||
}
|
||
|
||
func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]string, seriesMap map[uint]string) *dto.StandaloneIotCardResponse {
|
||
resp := &dto.StandaloneIotCardResponse{
|
||
ID: card.ID,
|
||
ICCID: card.ICCID,
|
||
CardCategory: card.CardCategory,
|
||
CarrierID: card.CarrierID,
|
||
CarrierType: card.CarrierType,
|
||
CarrierName: card.CarrierName,
|
||
IMSI: card.IMSI,
|
||
MSISDN: card.MSISDN,
|
||
BatchNo: card.BatchNo,
|
||
Supplier: card.Supplier,
|
||
CostPrice: card.CostPrice,
|
||
DistributePrice: card.DistributePrice,
|
||
Status: card.Status,
|
||
ShopID: card.ShopID,
|
||
ActivatedAt: card.ActivatedAt,
|
||
ActivationStatus: card.ActivationStatus,
|
||
RealNameStatus: card.RealNameStatus,
|
||
NetworkStatus: card.NetworkStatus,
|
||
DataUsageMB: card.DataUsageMB,
|
||
SeriesID: card.SeriesID,
|
||
FirstCommissionPaid: card.FirstCommissionPaid,
|
||
AccumulatedRecharge: card.AccumulatedRecharge,
|
||
CreatedAt: card.CreatedAt,
|
||
UpdatedAt: card.UpdatedAt,
|
||
}
|
||
|
||
if card.ShopID != nil && *card.ShopID > 0 {
|
||
resp.ShopName = shopMap[*card.ShopID]
|
||
}
|
||
|
||
if card.SeriesID != nil && *card.SeriesID > 0 {
|
||
resp.SeriesName = seriesMap[*card.SeriesID]
|
||
}
|
||
|
||
return resp
|
||
}
|
||
|
||
func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateStandaloneCardsResponse, error) {
|
||
if err := s.validateDirectSubordinate(ctx, operatorShopID, req.ToShopID); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
cards, err := s.getCardsForAllocation(ctx, req, operatorShopID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(cards) == 0 {
|
||
return &dto.AllocateStandaloneCardsResponse{
|
||
TotalCount: 0,
|
||
SuccessCount: 0,
|
||
FailCount: 0,
|
||
FailedItems: []dto.AllocationFailedItem{},
|
||
}, nil
|
||
}
|
||
|
||
var cardIDs []uint
|
||
var failedItems []dto.AllocationFailedItem
|
||
|
||
boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
boundCardIDSet := make(map[uint]bool)
|
||
for _, id := range boundCardIDs {
|
||
boundCardIDSet[id] = true
|
||
}
|
||
|
||
isPlatform := operatorShopID == nil
|
||
|
||
for _, card := range cards {
|
||
if boundCardIDSet[card.ID] {
|
||
failedItems = append(failedItems, dto.AllocationFailedItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "已绑定设备的卡不能单独分配",
|
||
})
|
||
continue
|
||
}
|
||
|
||
if isPlatform && card.Status != constants.IotCardStatusInStock {
|
||
failedItems = append(failedItems, dto.AllocationFailedItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "平台只能分配在库状态的卡",
|
||
})
|
||
continue
|
||
}
|
||
|
||
if !isPlatform && card.Status != constants.IotCardStatusDistributed {
|
||
failedItems = append(failedItems, dto.AllocationFailedItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "代理只能分配已分销状态的卡",
|
||
})
|
||
continue
|
||
}
|
||
|
||
cardIDs = append(cardIDs, card.ID)
|
||
}
|
||
|
||
if len(cardIDs) == 0 {
|
||
return &dto.AllocateStandaloneCardsResponse{
|
||
TotalCount: len(cards),
|
||
SuccessCount: 0,
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
newStatus := constants.IotCardStatusDistributed
|
||
toShopID := req.ToShopID
|
||
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
txIotCardStore := postgres.NewIotCardStore(tx, nil)
|
||
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
|
||
|
||
if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, &toShopID, newStatus); err != nil {
|
||
return err
|
||
}
|
||
|
||
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
|
||
records := s.buildAllocationRecords(cards, cardIDs, operatorShopID, toShopID, operatorID, allocationNo, req.Remark)
|
||
return txRecordStore.BatchCreate(ctx, records)
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &dto.AllocateStandaloneCardsResponse{
|
||
TotalCount: len(cards),
|
||
SuccessCount: len(cardIDs),
|
||
FailCount: len(failedItems),
|
||
AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
cards, err := s.getCardsForRecall(ctx, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(cards) == 0 {
|
||
return &dto.RecallStandaloneCardsResponse{
|
||
TotalCount: 0,
|
||
SuccessCount: 0,
|
||
FailCount: 0,
|
||
FailedItems: []dto.AllocationFailedItem{},
|
||
}, nil
|
||
}
|
||
|
||
var cardIDs []uint
|
||
var failedItems []dto.AllocationFailedItem
|
||
|
||
boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, s.extractCardIDs(cards))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
boundCardIDSet := make(map[uint]bool)
|
||
for _, id := range boundCardIDs {
|
||
boundCardIDSet[id] = true
|
||
}
|
||
|
||
for _, card := range cards {
|
||
if boundCardIDSet[card.ID] {
|
||
failedItems = append(failedItems, dto.AllocationFailedItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "已绑定设备的卡不能单独回收",
|
||
})
|
||
continue
|
||
}
|
||
|
||
if card.ShopID == nil || *card.ShopID != req.FromShopID {
|
||
failedItems = append(failedItems, dto.AllocationFailedItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "卡不属于指定的店铺",
|
||
})
|
||
continue
|
||
}
|
||
|
||
cardIDs = append(cardIDs, card.ID)
|
||
}
|
||
|
||
if len(cardIDs) == 0 {
|
||
return &dto.RecallStandaloneCardsResponse{
|
||
TotalCount: len(cards),
|
||
SuccessCount: 0,
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
isPlatform := operatorShopID == nil
|
||
var newShopID *uint
|
||
var newStatus int
|
||
if isPlatform {
|
||
newShopID = nil
|
||
newStatus = constants.IotCardStatusInStock
|
||
} else {
|
||
newShopID = operatorShopID
|
||
newStatus = constants.IotCardStatusDistributed
|
||
}
|
||
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
txIotCardStore := postgres.NewIotCardStore(tx, nil)
|
||
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
|
||
|
||
if err := txIotCardStore.BatchUpdateShopIDAndStatus(ctx, cardIDs, newShopID, newStatus); err != nil {
|
||
return err
|
||
}
|
||
|
||
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)
|
||
records := s.buildRecallRecords(cards, cardIDs, req.FromShopID, operatorShopID, operatorID, allocationNo, req.Remark)
|
||
return txRecordStore.BatchCreate(ctx, records)
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &dto.RecallStandaloneCardsResponse{
|
||
TotalCount: len(cards),
|
||
SuccessCount: len(cardIDs),
|
||
FailCount: len(failedItems),
|
||
AllocationNo: s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error {
|
||
if operatorShopID != nil && *operatorShopID == targetShopID {
|
||
return errors.ErrCannotAllocateToSelf
|
||
}
|
||
|
||
targetShop, err := s.shopStore.GetByID(ctx, targetShopID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeShopNotFound)
|
||
}
|
||
return err
|
||
}
|
||
|
||
if operatorShopID == nil {
|
||
if targetShop.ParentID != nil {
|
||
return errors.ErrNotDirectSubordinate
|
||
}
|
||
} else {
|
||
if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
|
||
return errors.ErrNotDirectSubordinate
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) getCardsForAllocation(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorShopID *uint) ([]*model.IotCard, error) {
|
||
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, operatorShopID)
|
||
case dto.SelectionTypeFilter:
|
||
filters := make(map[string]any)
|
||
if req.CarrierID != nil {
|
||
filters["carrier_id"] = *req.CarrierID
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
if req.Status != nil {
|
||
filters["status"] = *req.Status
|
||
}
|
||
return s.iotCardStore.GetStandaloneByFilters(ctx, filters, operatorShopID)
|
||
default:
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
|
||
}
|
||
}
|
||
|
||
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)
|
||
case dto.SelectionTypeFilter:
|
||
filters := make(map[string]any)
|
||
if req.CarrierID != nil {
|
||
filters["carrier_id"] = *req.CarrierID
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
return s.iotCardStore.GetStandaloneByFilters(ctx, filters, &fromShopID)
|
||
default:
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的选卡方式")
|
||
}
|
||
}
|
||
|
||
func (s *Service) extractCardIDs(cards []*model.IotCard) []uint {
|
||
ids := make([]uint, len(cards))
|
||
for i, card := range cards {
|
||
ids[i] = card.ID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func (s *Service) buildAllocationRecords(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
|
||
}
|
||
|
||
var records []*model.AssetAllocationRecord
|
||
for _, card := range cards {
|
||
if !successIDSet[card.ID] {
|
||
continue
|
||
}
|
||
|
||
record := &model.AssetAllocationRecord{
|
||
AllocationNo: allocationNo,
|
||
AllocationType: constants.AssetAllocationTypeAllocate,
|
||
AssetType: constants.AssetTypeIotCard,
|
||
AssetID: card.ID,
|
||
AssetIdentifier: card.ICCID,
|
||
ToOwnerType: constants.OwnerTypeShop,
|
||
ToOwnerID: toShopID,
|
||
OperatorID: operatorID,
|
||
Remark: remark,
|
||
}
|
||
|
||
if fromShopID == nil {
|
||
record.FromOwnerType = constants.OwnerTypePlatform
|
||
record.FromOwnerID = nil
|
||
} else {
|
||
record.FromOwnerType = constants.OwnerTypeShop
|
||
record.FromOwnerID = fromShopID
|
||
}
|
||
|
||
records = append(records, record)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
var records []*model.AssetAllocationRecord
|
||
for _, card := range cards {
|
||
if !successIDSet[card.ID] {
|
||
continue
|
||
}
|
||
|
||
record := &model.AssetAllocationRecord{
|
||
AllocationNo: allocationNo,
|
||
AllocationType: constants.AssetAllocationTypeRecall,
|
||
AssetType: constants.AssetTypeIotCard,
|
||
AssetID: card.ID,
|
||
AssetIdentifier: card.ICCID,
|
||
FromOwnerType: constants.OwnerTypeShop,
|
||
FromOwnerID: &fromShopID,
|
||
OperatorID: operatorID,
|
||
Remark: remark,
|
||
}
|
||
|
||
if toShopID == nil {
|
||
record.ToOwnerType = constants.OwnerTypePlatform
|
||
record.ToOwnerID = 0
|
||
} else {
|
||
record.ToOwnerType = constants.OwnerTypeShop
|
||
record.ToOwnerID = *toShopID
|
||
}
|
||
|
||
records = append(records, record)
|
||
}
|
||
|
||
return records
|
||
}
|
||
|
||
// BatchSetSeriesBinding 批量设置卡的套餐系列绑定
|
||
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
|
||
cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(cards) == 0 {
|
||
return &dto.BatchSetCardSeriesBindngResponse{
|
||
SuccessCount: 0,
|
||
FailCount: len(req.ICCIDs),
|
||
FailedItems: s.buildCardNotFoundFailedItems(req.ICCIDs),
|
||
}, nil
|
||
}
|
||
|
||
cardMap := make(map[string]*model.IotCard)
|
||
for _, card := range cards {
|
||
cardMap[card.ICCID] = card
|
||
}
|
||
|
||
// 验证系列存在(仅当 SeriesID > 0 时)
|
||
var packageSeries *model.PackageSeries
|
||
if req.SeriesID > 0 {
|
||
packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
|
||
}
|
||
return nil, err
|
||
}
|
||
if packageSeries.Status != 1 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
|
||
}
|
||
}
|
||
|
||
var successCardIDs []uint
|
||
var failedItems []dto.CardSeriesBindngFailedItem
|
||
|
||
for _, iccid := range req.ICCIDs {
|
||
card, exists := cardMap[iccid]
|
||
if !exists {
|
||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||
ICCID: iccid,
|
||
Reason: "卡不存在",
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 验证操作者权限(仅代理用户)
|
||
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
|
||
}
|
||
}
|
||
|
||
// 验证卡权限(基于 card.ShopID)
|
||
if operatorShopID != nil {
|
||
if card.ShopID == nil || *card.ShopID != *operatorShopID {
|
||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||
ICCID: iccid,
|
||
Reason: "无权操作此卡",
|
||
})
|
||
continue
|
||
}
|
||
}
|
||
|
||
successCardIDs = append(successCardIDs, card.ID)
|
||
}
|
||
|
||
if len(successCardIDs) > 0 {
|
||
var seriesIDPtr *uint
|
||
if req.SeriesID > 0 {
|
||
seriesIDPtr = &req.SeriesID
|
||
}
|
||
if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return &dto.BatchSetCardSeriesBindngResponse{
|
||
SuccessCount: len(successCardIDs),
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeriesBindngFailedItem {
|
||
items := make([]dto.CardSeriesBindngFailedItem, len(iccids))
|
||
for i, iccid := range iccids {
|
||
items[i] = dto.CardSeriesBindngFailedItem{
|
||
ICCID: iccid,
|
||
Reason: "卡不存在",
|
||
}
|
||
}
|
||
return items
|
||
}
|
||
|
||
// SyncCardStatusFromGateway 从 Gateway 同步卡状态(示例方法)
|
||
func (s *Service) SyncCardStatusFromGateway(ctx context.Context, iccid string) error {
|
||
if s.gatewayClient == nil {
|
||
return errors.New(errors.CodeGatewayError, "Gateway 客户端未配置")
|
||
}
|
||
|
||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||
CardNo: iccid,
|
||
})
|
||
if err != nil {
|
||
s.logger.Error("查询卡状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||
return errors.Wrap(errors.CodeGatewayError, err, "查询卡状态失败")
|
||
}
|
||
|
||
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeNotFound, "IoT卡不存在")
|
||
}
|
||
return err
|
||
}
|
||
|
||
var newStatus int
|
||
switch resp.CardStatus {
|
||
case "准备":
|
||
newStatus = constants.IotCardStatusInStock
|
||
case "正常":
|
||
newStatus = constants.IotCardStatusDistributed
|
||
case "停机":
|
||
newStatus = constants.IotCardStatusSuspended
|
||
default:
|
||
s.logger.Warn("未知的卡状态", zap.String("cardStatus", resp.CardStatus))
|
||
return nil
|
||
}
|
||
|
||
if card.Status != newStatus {
|
||
card.Status = newStatus
|
||
if err := s.iotCardStore.Update(ctx, card); err != nil {
|
||
return err
|
||
}
|
||
s.logger.Info("同步卡状态成功",
|
||
zap.String("iccid", iccid),
|
||
zap.Int("oldStatus", card.Status),
|
||
zap.Int("newStatus", newStatus),
|
||
)
|
||
}
|
||
|
||
return nil
|
||
}
|