Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
685 lines
23 KiB
Go
685 lines
23 KiB
Go
package enterprise_card
|
||
|
||
import (
|
||
"context"
|
||
stderrors "errors"
|
||
"time"
|
||
|
||
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
|
||
"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/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
enterpriseStore *postgres.EnterpriseStore
|
||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||
iotCardStore *postgres.IotCardStore
|
||
accessAudit accessauditapp.Writer
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
enterpriseStore *postgres.EnterpriseStore,
|
||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
accessAudit accessauditapp.Writer,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
enterpriseStore: enterpriseStore,
|
||
enterpriseCardAuthStore: enterpriseCardAuthStore,
|
||
iotCardStore: iotCardStore,
|
||
accessAudit: accessAudit,
|
||
}
|
||
}
|
||
|
||
func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
|
||
iotCardPtrs, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||
}
|
||
|
||
cardMap := make(map[string]*model.IotCard)
|
||
cardIDMap := make(map[uint]*model.IotCard)
|
||
cardIDs := make([]uint, 0, len(iotCardPtrs))
|
||
for _, c := range iotCardPtrs {
|
||
cardMap[c.ICCID] = c
|
||
cardIDMap[c.ID] = c
|
||
cardIDs = append(cardIDs, c.ID)
|
||
}
|
||
|
||
var bindings []model.DeviceSimBinding
|
||
if len(cardIDs) > 0 {
|
||
s.db.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = 1", cardIDs).Find(&bindings)
|
||
}
|
||
|
||
cardToDevice := make(map[uint]bool)
|
||
for _, binding := range bindings {
|
||
cardToDevice[binding.IotCardID] = true
|
||
}
|
||
|
||
resp := &dto.AllocateCardsPreviewResp{
|
||
StandaloneCards: make([]dto.StandaloneCard, 0),
|
||
FailedItems: make([]dto.FailedItem, 0),
|
||
}
|
||
|
||
for _, iccid := range req.ICCIDs {
|
||
card, exists := cardMap[iccid]
|
||
if !exists {
|
||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||
ICCID: iccid,
|
||
Reason: "卡不存在",
|
||
})
|
||
continue
|
||
}
|
||
|
||
if cardToDevice[card.ID] {
|
||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||
ICCID: iccid,
|
||
Reason: "该卡已绑定设备,请使用设备授权功能",
|
||
})
|
||
continue
|
||
}
|
||
|
||
resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{
|
||
ICCID: card.ICCID,
|
||
IotCardID: card.ID,
|
||
MSISDN: card.MSISDN,
|
||
CarrierID: card.CarrierID,
|
||
StatusName: constants.GetIotCardStatusName(card.Status),
|
||
})
|
||
}
|
||
|
||
resp.Summary = dto.AllocatePreviewSummary{
|
||
StandaloneCardCount: len(resp.StandaloneCards),
|
||
DeviceCount: 0,
|
||
DeviceCardCount: 0,
|
||
TotalCardCount: len(resp.StandaloneCards),
|
||
FailedCount: len(resp.FailedItems),
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
|
||
// resolveICCIDsForAllocate 根据选取模式解析待授权的 ICCID 列表
|
||
func (s *Service) resolveICCIDsForAllocate(ctx context.Context, req *dto.AllocateCardsReq) ([]string, error) {
|
||
switch req.SelectionType {
|
||
case "list":
|
||
return req.ICCIDs, nil
|
||
case "range":
|
||
cards, err := s.iotCardStore.GetStandaloneByICCIDRangeForAuth(ctx, req.ICCIDStart, req.ICCIDEnd)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "按号段查询卡失败")
|
||
}
|
||
iccids := make([]string, 0, len(cards))
|
||
for _, c := range cards {
|
||
iccids = append(iccids, c.ICCID)
|
||
}
|
||
return iccids, nil
|
||
case "filter":
|
||
filters := map[string]any{}
|
||
if req.ICCID != "" {
|
||
filters["iccid"] = req.ICCID
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
if req.CarrierID != nil {
|
||
filters["carrier_id"] = *req.CarrierID
|
||
}
|
||
if req.ShopID != nil {
|
||
filters["shop_id"] = *req.ShopID
|
||
}
|
||
if len(req.ShopIDs) > 0 {
|
||
filters["shop_ids"] = req.ShopIDs
|
||
}
|
||
cards, err := s.iotCardStore.GetStandaloneByAuthFilters(ctx, filters)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询卡失败")
|
||
}
|
||
iccids := make([]string, 0, len(cards))
|
||
for _, c := range cards {
|
||
iccids = append(iccids, c.ICCID)
|
||
}
|
||
return iccids, nil
|
||
default:
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的选取模式")
|
||
}
|
||
}
|
||
|
||
// resolveICCIDsForRecall 根据选取模式解析待回收的 ICCID 列表
|
||
func (s *Service) resolveICCIDsForRecall(ctx context.Context, enterpriseID uint, req *dto.RecallCardsReq) ([]string, error) {
|
||
switch req.SelectionType {
|
||
case "list":
|
||
return req.ICCIDs, nil
|
||
case "range":
|
||
cards, err := s.iotCardStore.GetAuthorizedStandaloneByICCIDRange(ctx, enterpriseID, req.ICCIDStart, req.ICCIDEnd)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "按号段查询已授权卡失败")
|
||
}
|
||
iccids := make([]string, 0, len(cards))
|
||
for _, c := range cards {
|
||
iccids = append(iccids, c.ICCID)
|
||
}
|
||
return iccids, nil
|
||
case "filter":
|
||
filters := map[string]any{}
|
||
if req.ICCID != "" {
|
||
filters["iccid"] = req.ICCID
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
if req.CarrierID != nil {
|
||
filters["carrier_id"] = *req.CarrierID
|
||
}
|
||
cards, err := s.iotCardStore.GetAuthorizedStandaloneByFilters(ctx, enterpriseID, filters)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询已授权卡失败")
|
||
}
|
||
iccids := make([]string, 0, len(cards))
|
||
for _, c := range cards {
|
||
iccids = append(iccids, c.ICCID)
|
||
}
|
||
return iccids, nil
|
||
default:
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的选取模式")
|
||
}
|
||
}
|
||
|
||
func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsReq) (*dto.AllocateCardsResp, error) {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
if s.db == nil || s.accessAudit == nil {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "企业卡授权审计接缝未配置")
|
||
}
|
||
if err := validateEnterpriseCardActor(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
ownerShop := s.loadOwnerShop(ctx, enterprise.OwnerShopID)
|
||
|
||
iccids, err := s.resolveICCIDsForAllocate(ctx, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: iccids})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
resp := &dto.AllocateCardsResp{
|
||
FailedItems: preview.FailedItems,
|
||
FailCount: len(preview.FailedItems),
|
||
}
|
||
|
||
// 构建 cardID -> ICCID 映射,用于组装失败原因
|
||
cardIDToICCID := make(map[uint]string, len(preview.StandaloneCards))
|
||
allCandidateIDs := make([]uint, 0, len(preview.StandaloneCards))
|
||
for _, card := range preview.StandaloneCards {
|
||
cardIDToICCID[card.IotCardID] = card.ICCID
|
||
allCandidateIDs = append(allCandidateIDs, card.IotCardID)
|
||
}
|
||
auditCardList, err := s.iotCardStore.GetByIDs(ctx, allCandidateIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡审计快照失败")
|
||
}
|
||
cardIDMap := make(map[uint]*model.IotCard, len(auditCardList))
|
||
for _, card := range auditCardList {
|
||
cardIDMap[card.ID] = card
|
||
}
|
||
|
||
// 检测已被其他企业授权的卡,阻止重复授权
|
||
conflictAuths, err := s.enterpriseCardAuthStore.GetConflictingAuthsByCardIDs(ctx, enterpriseID, allCandidateIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询冲突授权失败")
|
||
}
|
||
|
||
cardIDsToAllocate := make([]uint, 0, len(allCandidateIDs))
|
||
seenAllocate := make(map[uint]struct{}, len(allCandidateIDs))
|
||
for _, cardID := range allCandidateIDs {
|
||
if _, seen := seenAllocate[cardID]; seen {
|
||
continue
|
||
}
|
||
seenAllocate[cardID] = struct{}{}
|
||
if _, conflict := conflictAuths[cardID]; conflict {
|
||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||
ICCID: cardIDToICCID[cardID],
|
||
Reason: "卡已授权给其他企业,请先收回",
|
||
})
|
||
resp.FailCount++
|
||
continue
|
||
}
|
||
cardIDsToAllocate = append(cardIDsToAllocate, cardID)
|
||
}
|
||
|
||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败")
|
||
}
|
||
|
||
now := time.Now()
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
auths := make([]*model.EnterpriseCardAuthorization, 0)
|
||
for _, cardID := range cardIDsToAllocate {
|
||
if existingAuths[cardID] {
|
||
continue
|
||
}
|
||
auths = append(auths, &model.EnterpriseCardAuthorization{
|
||
EnterpriseID: enterpriseID,
|
||
CardID: cardID,
|
||
AuthorizedBy: currentUserID,
|
||
AuthorizedAt: now,
|
||
AuthorizerType: userType,
|
||
Remark: req.Remark,
|
||
})
|
||
}
|
||
|
||
if len(auths) > 0 {
|
||
auditResult := constants.AuditResultSuccess
|
||
if resp.FailCount > 0 {
|
||
auditResult = constants.AuditResultPartial
|
||
}
|
||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.CreateInBatches(auths, 100).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "创建授权记录失败")
|
||
}
|
||
cards := make([]accessauditapp.IotCardChange, 0, len(auths))
|
||
authorizations := make([]accessauditapp.EnterpriseCardAuthorizationChange, 0, len(auths))
|
||
for _, auth := range auths {
|
||
card := cardIDMap[auth.CardID]
|
||
cards = append(cards, accessauditapp.IotCardChange{
|
||
Card: card, BeforeData: map[string]any{"enterprise_id": nil, "authorized": false},
|
||
AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true},
|
||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡已授权给企业",
|
||
})
|
||
authorizations = append(authorizations, accessauditapp.EnterpriseCardAuthorizationChange{
|
||
Authorization: auth, AfterData: map[string]any{"authorized": true, "remark": auth.Remark},
|
||
})
|
||
}
|
||
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||
ActionCode: constants.AuditActionEnterpriseCardsAllocated, Summary: "向企业授权卡",
|
||
Result: auditResult, OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop,
|
||
Cards: cards, CardAuthorizations: authorizations,
|
||
BeforeData: map[string]any{"authorized_card_count": 0},
|
||
AfterData: map[string]any{"authorized_card_count": len(auths)},
|
||
})
|
||
}); err != nil {
|
||
s.recordFailure(ctx, constants.AuditActionEnterpriseCardsAllocated, "向企业授权卡失败", enterprise, ownerShop, cardChanges(cardIDMap, allCandidateIDs), err)
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
resp.SuccessCount = len(auths)
|
||
return resp, nil
|
||
}
|
||
|
||
func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.RecallCardsReq) (*dto.RecallCardsResp, error) {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
if s.db == nil || s.accessAudit == nil {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "企业卡授权审计接缝未配置")
|
||
}
|
||
if err := validateEnterpriseCardActor(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
ownerShop := s.loadOwnerShop(ctx, enterprise.OwnerShopID)
|
||
|
||
iccids, err := s.resolveICCIDsForRecall(ctx, enterpriseID, req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
iotCardPtrs, err := s.iotCardStore.GetByICCIDs(ctx, iccids)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||
}
|
||
|
||
cardMap := make(map[string]*model.IotCard)
|
||
cardIDMap := make(map[uint]*model.IotCard)
|
||
cardIDs := make([]uint, 0, len(iotCardPtrs))
|
||
for _, c := range iotCardPtrs {
|
||
cardMap[c.ICCID] = c
|
||
cardIDMap[c.ID] = c
|
||
cardIDs = append(cardIDs, c.ID)
|
||
}
|
||
|
||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDs)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败")
|
||
}
|
||
|
||
resp := &dto.RecallCardsResp{
|
||
FailedItems: make([]dto.FailedItem, 0),
|
||
RecalledDevices: make([]dto.RecalledDevice, 0),
|
||
}
|
||
|
||
cardIDsToRecall := make([]uint, 0)
|
||
seenRecall := make(map[uint]struct{}, len(iccids))
|
||
for _, iccid := range iccids {
|
||
card, exists := cardMap[iccid]
|
||
if !exists {
|
||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||
ICCID: iccid,
|
||
Reason: "卡不存在",
|
||
})
|
||
continue
|
||
}
|
||
if !existingAuths[card.ID] {
|
||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||
ICCID: iccid,
|
||
Reason: "该卡未授权给此企业",
|
||
})
|
||
continue
|
||
}
|
||
if _, seen := seenRecall[card.ID]; seen {
|
||
continue
|
||
}
|
||
seenRecall[card.ID] = struct{}{}
|
||
cardIDsToRecall = append(cardIDsToRecall, card.ID)
|
||
}
|
||
|
||
if len(cardIDsToRecall) > 0 {
|
||
var recalled []*model.EnterpriseCardAuthorization
|
||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Where("enterprise_id = ? AND card_id IN ? AND revoked_at IS NULL", enterpriseID, cardIDsToRecall).
|
||
Find(&recalled).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询有效卡授权失败")
|
||
}
|
||
if len(recalled) == 0 {
|
||
return nil
|
||
}
|
||
now := time.Now()
|
||
ids := make([]uint, 0, len(recalled))
|
||
cards := make([]accessauditapp.IotCardChange, 0, len(recalled))
|
||
authorizations := make([]accessauditapp.EnterpriseCardAuthorizationChange, 0, len(recalled))
|
||
for _, auth := range recalled {
|
||
ids = append(ids, auth.ID)
|
||
before := *auth
|
||
auth.RevokedBy = ¤tUserID
|
||
auth.RevokedAt = &now
|
||
cards = append(cards, accessauditapp.IotCardChange{
|
||
Card: cardIDMap[auth.CardID],
|
||
BeforeData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true},
|
||
AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": false},
|
||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡授权已回收",
|
||
})
|
||
authorizations = append(authorizations, accessauditapp.EnterpriseCardAuthorizationChange{
|
||
Authorization: auth,
|
||
BeforeData: map[string]any{"revoked_by": before.RevokedBy, "revoked_at": before.RevokedAt},
|
||
AfterData: map[string]any{"revoked_by": currentUserID, "revoked_at": now},
|
||
})
|
||
}
|
||
if err := tx.Model(&model.EnterpriseCardAuthorization{}).Where("id IN ? AND revoked_at IS NULL", ids).
|
||
Updates(map[string]any{"revoked_by": currentUserID, "revoked_at": now}).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "回收卡授权失败")
|
||
}
|
||
result := constants.AuditResultSuccess
|
||
if len(resp.FailedItems) > 0 {
|
||
result = constants.AuditResultPartial
|
||
}
|
||
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||
ActionCode: constants.AuditActionEnterpriseCardsRecalled, Summary: "回收企业卡授权",
|
||
Result: result, OperatorID: currentUserID, Enterprise: enterprise, Shop: ownerShop,
|
||
Cards: cards, CardAuthorizations: authorizations,
|
||
BeforeData: map[string]any{"authorized_card_count": len(recalled)},
|
||
AfterData: map[string]any{"authorized_card_count": 0},
|
||
})
|
||
}); err != nil {
|
||
s.recordFailure(ctx, constants.AuditActionEnterpriseCardsRecalled, "回收企业卡授权失败", enterprise, ownerShop, cardChanges(cardIDMap, cardIDsToRecall), err)
|
||
return nil, err
|
||
}
|
||
resp.SuccessCount = len(recalled)
|
||
}
|
||
|
||
resp.FailCount = len(resp.FailedItems)
|
||
return resp, nil
|
||
}
|
||
|
||
func validateEnterpriseCardActor(ctx context.Context) error {
|
||
switch middleware.GetUserTypeFromContext(ctx) {
|
||
case constants.UserTypeSuperAdmin, constants.UserTypePlatform, constants.UserTypeAgent:
|
||
return nil
|
||
default:
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
func (s *Service) loadOwnerShop(ctx context.Context, ownerShopID *uint) *model.Shop {
|
||
if ownerShopID == nil {
|
||
return nil
|
||
}
|
||
shop, err := postgres.NewShopStore(s.db, nil).GetByID(ctx, *ownerShopID)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return shop
|
||
}
|
||
|
||
func cardChanges(cardMap map[uint]*model.IotCard, ids []uint) []accessauditapp.IotCardChange {
|
||
changes := make([]accessauditapp.IotCardChange, 0, len(ids))
|
||
for _, id := range ids {
|
||
if card := cardMap[id]; card != nil {
|
||
changes = append(changes, accessauditapp.IotCardChange{Card: card})
|
||
}
|
||
}
|
||
return changes
|
||
}
|
||
|
||
func (s *Service) recordFailure(
|
||
ctx context.Context,
|
||
actionCode, summary string,
|
||
enterprise *model.Enterprise,
|
||
ownerShop *model.Shop,
|
||
cards []accessauditapp.IotCardChange,
|
||
originalErr error,
|
||
) {
|
||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{
|
||
ActionCode: actionCode, Summary: summary, Result: enterpriseCardFailureResult(originalErr),
|
||
OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Shop: ownerShop,
|
||
Cards: cards, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||
}, originalErr)
|
||
}
|
||
|
||
func enterpriseCardFailureResult(err error) string {
|
||
var appErr *errors.AppError
|
||
if stderrors.As(err, &appErr) {
|
||
switch appErr.Code {
|
||
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeEnterpriseNotFound,
|
||
errors.CodeIotCardNotFound, errors.CodeIotCardStatusNotAllowed, errors.CodeCannotAuthorizeToOthersEnterprise,
|
||
errors.CodeCannotAuthorizeOthersCard, errors.CodeCannotAuthorizeBoundCard, errors.CodeCardAlreadyAuthorized,
|
||
errors.CodeCardNotAuthorized, errors.CodeCannotRevokeOthersAuthorization:
|
||
return constants.AuditResultDenied
|
||
}
|
||
}
|
||
return constants.AuditResultFailed
|
||
}
|
||
|
||
func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.EnterpriseCardListReq) (*dto.EnterpriseCardPageResult, error) {
|
||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
|
||
cardIDs, err := s.enterpriseCardAuthStore.ListCardIDsByEnterprise(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询授权卡ID失败")
|
||
}
|
||
|
||
if len(cardIDs) == 0 {
|
||
return &dto.EnterpriseCardPageResult{
|
||
Items: make([]dto.EnterpriseCardItem, 0),
|
||
Total: 0,
|
||
Page: req.Page,
|
||
Size: req.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
page := req.Page
|
||
pageSize := req.PageSize
|
||
if page == 0 {
|
||
page = 1
|
||
}
|
||
if pageSize == 0 {
|
||
pageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).Where("id IN ?", cardIDs)
|
||
|
||
if req.Status != nil {
|
||
query = query.Where("status = ?", *req.Status)
|
||
}
|
||
if req.CarrierID != nil {
|
||
query = query.Where("carrier_id = ?", *req.CarrierID)
|
||
}
|
||
if req.ICCID != "" {
|
||
query = query.Where("iccid LIKE ?", "%"+req.ICCID+"%")
|
||
}
|
||
if req.VirtualNo != "" {
|
||
query = query.Where("virtual_no LIKE ?", "%"+req.VirtualNo+"%")
|
||
}
|
||
if req.MSISDN != "" {
|
||
query = query.Where("msisdn LIKE ?", "%"+req.MSISDN+"%")
|
||
}
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "统计卡数量失败")
|
||
}
|
||
|
||
var cards []model.IotCard
|
||
offset := (page - 1) * pageSize
|
||
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&cards).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡列表失败")
|
||
}
|
||
|
||
// 批量查询各卡当前生效的主套餐名称(取 package_name 快照,避免 JOIN tb_package)
|
||
pageCardIDs := make([]uint, 0, len(cards))
|
||
for _, card := range cards {
|
||
pageCardIDs = append(pageCardIDs, card.ID)
|
||
}
|
||
packageNameMap := s.buildPackageNameMap(ctx, pageCardIDs)
|
||
|
||
items := make([]dto.EnterpriseCardItem, 0, len(cards))
|
||
for _, card := range cards {
|
||
items = append(items, dto.EnterpriseCardItem{
|
||
ID: card.ID,
|
||
ICCID: card.ICCID,
|
||
MSISDN: card.MSISDN,
|
||
VirtualNo: card.VirtualNo,
|
||
CarrierID: card.CarrierID,
|
||
CarrierName: card.CarrierName,
|
||
PackageName: packageNameMap[card.ID],
|
||
Status: card.Status,
|
||
StatusName: constants.GetIotCardStatusName(card.Status),
|
||
NetworkStatus: card.NetworkStatus,
|
||
NetworkStatusName: constants.GetNetworkStatusName(card.NetworkStatus),
|
||
GatewayExtend: card.GatewayExtend,
|
||
})
|
||
}
|
||
|
||
return &dto.EnterpriseCardPageResult{
|
||
Items: items,
|
||
Total: total,
|
||
Page: page,
|
||
Size: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// buildPackageNameMap 批量查询卡列表当前生效主套餐名称,返回 map[iotCardID]packageName
|
||
func (s *Service) buildPackageNameMap(ctx context.Context, cardIDs []uint) map[uint]string {
|
||
result := make(map[uint]string, len(cardIDs))
|
||
if len(cardIDs) == 0 {
|
||
return result
|
||
}
|
||
|
||
type usageRow struct {
|
||
IotCardID uint `gorm:"column:iot_card_id"`
|
||
PackageName string `gorm:"column:package_name"`
|
||
}
|
||
|
||
// 每张卡取优先级最低(数字最小)的生效中主套餐,使用 DISTINCT ON 保证每卡仅一条
|
||
var rows []usageRow
|
||
s.db.WithContext(ctx).Raw(`
|
||
SELECT DISTINCT ON (iot_card_id) iot_card_id, package_name
|
||
FROM tb_package_usage
|
||
WHERE iot_card_id IN ?
|
||
AND status = 1
|
||
AND master_usage_id IS NULL
|
||
AND deleted_at IS NULL
|
||
ORDER BY iot_card_id, priority ASC
|
||
`, cardIDs).Scan(&rows)
|
||
|
||
for _, row := range rows {
|
||
result[row.IotCardID] = row.PackageName
|
||
}
|
||
return result
|
||
}
|
||
|
||
func (s *Service) SuspendCard(ctx context.Context, enterpriseID, cardID uint) error {
|
||
return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 0)
|
||
}
|
||
|
||
func (s *Service) ResumeCard(ctx context.Context, enterpriseID, cardID uint) error {
|
||
return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 1)
|
||
}
|
||
|
||
func (s *Service) updateCardNetworkStatus(ctx context.Context, enterpriseID, cardID uint, networkStatus int) error {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
|
||
auth, err := s.enterpriseCardAuthStore.GetByEnterpriseAndCard(ctx, enterpriseID, cardID)
|
||
if err != nil || auth.RevokedAt != nil {
|
||
return errors.New(errors.CodeForbidden, "无权限操作此卡")
|
||
}
|
||
|
||
return s.iotCardStore.UpdateFields(ctx, cardID, map[string]any{
|
||
"network_status": networkStatus,
|
||
})
|
||
}
|