All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m5s
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package enterprise_card
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"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"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
enterpriseStore *postgres.EnterpriseStore
|
||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||
iotCardStore *postgres.IotCardStore
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
enterpriseStore *postgres.EnterpriseStore,
|
||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
enterpriseStore: enterpriseStore,
|
||
enterpriseCardAuthStore: enterpriseCardAuthStore,
|
||
iotCardStore: iotCardStore,
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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, "未授权访问")
|
||
}
|
||
|
||
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
|
||
if err != nil {
|
||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||
}
|
||
|
||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
resp := &dto.AllocateCardsResp{
|
||
FailedItems: preview.FailedItems,
|
||
FailCount: len(preview.FailedItems),
|
||
}
|
||
|
||
cardIDsToAllocate := make([]uint, 0)
|
||
for _, card := range preview.StandaloneCards {
|
||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||
}
|
||
|
||
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 {
|
||
if err := s.enterpriseCardAuthStore.BatchCreate(ctx, auths); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建授权记录失败")
|
||
}
|
||
}
|
||
|
||
resp.SuccessCount = len(cardIDsToAllocate)
|
||
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, "未授权访问")
|
||
}
|
||
|
||
_, 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)
|
||
}
|
||
|
||
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)
|
||
for _, iccid := range req.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
|
||
}
|
||
cardIDsToRecall = append(cardIDsToRecall, card.ID)
|
||
}
|
||
|
||
if len(cardIDsToRecall) > 0 {
|
||
if err := s.enterpriseCardAuthStore.BatchUpdateStatus(ctx, enterpriseID, cardIDsToRecall, 0); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "回收授权失败")
|
||
}
|
||
}
|
||
|
||
resp.SuccessCount = len(cardIDsToRecall)
|
||
resp.FailCount = len(resp.FailedItems)
|
||
return resp, nil
|
||
}
|
||
|
||
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+"%")
|
||
}
|
||
|
||
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,
|
||
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,
|
||
})
|
||
}
|