Files
junhong_cmp_fiber/internal/service/enterprise_card/service.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
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 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

685 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = &currentUserID
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,
})
}