feat: 实现企业设备授权功能并归档 OpenSpec 变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m39s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m39s
- 新增企业设备授权模块(Model、DTO、Service、Handler、Store) - 实现设备授权的创建、查询、更新、删除等完整业务逻辑 - 添加企业卡授权与设备授权的关联关系 - 新增 2 个数据库迁移脚本 - 同步 OpenSpec delta specs 到 main specs - 归档 add-enterprise-device-authorization 变更 - 更新 API 文档和路由配置 - 新增完整的集成测试和单元测试覆盖
This commit is contained in:
161
internal/service/enterprise_card/authorization_service_test.go
Normal file
161
internal/service/enterprise_card/authorization_service_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package enterprise_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestAuthorizationService_BatchAuthorize_BoundCardRejected(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := NewAuthorizationService(enterpriseStore, iotCardStore, authStore, logger)
|
||||
|
||||
shop := &model.Shop{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "TEST_SHOP_001",
|
||||
Level: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(shop).Error)
|
||||
|
||||
enterprise := &model.Enterprise{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
EnterpriseName: "测试企业",
|
||||
EnterpriseCode: "TEST_ENT_001",
|
||||
OwnerShopID: &shop.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(enterprise).Error)
|
||||
|
||||
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
|
||||
require.NoError(t, tx.Create(carrier).Error)
|
||||
|
||||
unboundCard := &model.IotCard{
|
||||
ICCID: "UNBOUND_CARD_001",
|
||||
CardType: "normal",
|
||||
CarrierID: carrier.ID,
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, tx.Create(unboundCard).Error)
|
||||
|
||||
boundCard := &model.IotCard{
|
||||
ICCID: "BOUND_CARD_001",
|
||||
CardType: "normal",
|
||||
CarrierID: carrier.ID,
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, tx.Create(boundCard).Error)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_DEVICE_001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, tx.Create(device).Error)
|
||||
|
||||
now := time.Now()
|
||||
binding := &model.DeviceSimBinding{
|
||||
DeviceID: device.ID,
|
||||
IotCardID: boundCard.ID,
|
||||
SlotPosition: 1,
|
||||
BindStatus: 1,
|
||||
BindTime: &now,
|
||||
}
|
||||
require.NoError(t, tx.Create(binding).Error)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
ShopID: shop.ID,
|
||||
})
|
||||
|
||||
t.Run("绑定设备的卡被拒绝授权", func(t *testing.T) {
|
||||
req := BatchAuthorizeRequest{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardIDs: []uint{boundCard.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "测试授权",
|
||||
}
|
||||
|
||||
err := service.BatchAuthorize(ctx, req)
|
||||
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok, "应返回 AppError 类型")
|
||||
assert.Equal(t, errors.CodeCannotAuthorizeBoundCard, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "已绑定设备")
|
||||
})
|
||||
|
||||
t.Run("未绑定设备的卡可以授权", func(t *testing.T) {
|
||||
req := BatchAuthorizeRequest{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardIDs: []uint{unboundCard.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "测试授权",
|
||||
}
|
||||
|
||||
err := service.BatchAuthorize(ctx, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
auths, err := authStore.ListByCards(ctx, []uint{unboundCard.ID}, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, auths, 1)
|
||||
assert.Equal(t, enterprise.ID, auths[0].EnterpriseID)
|
||||
})
|
||||
|
||||
t.Run("混合卡列表中有绑定卡时整体拒绝", func(t *testing.T) {
|
||||
unboundCard2 := &model.IotCard{
|
||||
ICCID: "UNBOUND_CARD_002",
|
||||
CardType: "normal",
|
||||
CarrierID: carrier.ID,
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, tx.Create(unboundCard2).Error)
|
||||
|
||||
req := BatchAuthorizeRequest{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardIDs: []uint{unboundCard2.ID, boundCard.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "测试授权",
|
||||
}
|
||||
|
||||
err := service.BatchAuthorize(ctx, req)
|
||||
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok, "应返回 AppError 类型")
|
||||
assert.Equal(t, errors.CodeCannotAuthorizeBoundCard, appErr.Code)
|
||||
|
||||
auths, err := authStore.ListByCards(ctx, []uint{unboundCard2.ID}, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, auths, 0, "混合列表中的未绑定卡也不应被授权")
|
||||
})
|
||||
}
|
||||
@@ -65,34 +65,16 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
|
||||
s.db.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = 1", cardIDs).Find(&bindings)
|
||||
}
|
||||
|
||||
cardToDevice := make(map[uint]uint)
|
||||
deviceCards := make(map[uint][]uint)
|
||||
cardToDevice := make(map[uint]bool)
|
||||
for _, binding := range bindings {
|
||||
cardToDevice[binding.IotCardID] = binding.DeviceID
|
||||
deviceCards[binding.DeviceID] = append(deviceCards[binding.DeviceID], binding.IotCardID)
|
||||
}
|
||||
|
||||
deviceIDs := make([]uint, 0, len(deviceCards))
|
||||
for deviceID := range deviceCards {
|
||||
deviceIDs = append(deviceIDs, deviceID)
|
||||
}
|
||||
var devices []model.Device
|
||||
deviceMap := make(map[uint]*model.Device)
|
||||
if len(deviceIDs) > 0 {
|
||||
s.db.WithContext(ctx).Where("id IN ?", deviceIDs).Find(&devices)
|
||||
for i := range devices {
|
||||
deviceMap[devices[i].ID] = &devices[i]
|
||||
}
|
||||
cardToDevice[binding.IotCardID] = true
|
||||
}
|
||||
|
||||
resp := &dto.AllocateCardsPreviewResp{
|
||||
StandaloneCards: make([]dto.StandaloneCard, 0),
|
||||
DeviceBundles: make([]dto.DeviceBundle, 0),
|
||||
FailedItems: make([]dto.FailedItem, 0),
|
||||
}
|
||||
|
||||
processedDevices := make(map[uint]bool)
|
||||
|
||||
for _, iccid := range req.ICCIDs {
|
||||
card, exists := cardMap[iccid]
|
||||
if !exists {
|
||||
@@ -103,67 +85,28 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
|
||||
continue
|
||||
}
|
||||
|
||||
deviceID, hasDevice := cardToDevice[card.ID]
|
||||
if !hasDevice {
|
||||
resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{
|
||||
ICCID: card.ICCID,
|
||||
IotCardID: card.ID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierID: card.CarrierID,
|
||||
StatusName: getCardStatusName(card.Status),
|
||||
if cardToDevice[card.ID] {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "该卡已绑定设备,请使用设备授权功能",
|
||||
})
|
||||
} else {
|
||||
if processedDevices[deviceID] {
|
||||
continue
|
||||
}
|
||||
processedDevices[deviceID] = true
|
||||
|
||||
device := deviceMap[deviceID]
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bundleCardIDs := deviceCards[deviceID]
|
||||
bundle := dto.DeviceBundle{
|
||||
DeviceID: deviceID,
|
||||
DeviceNo: device.DeviceNo,
|
||||
BundleCards: make([]dto.DeviceBundleCard, 0),
|
||||
}
|
||||
|
||||
for _, bundleCardID := range bundleCardIDs {
|
||||
bundleCard := cardIDMap[bundleCardID]
|
||||
if bundleCard == nil {
|
||||
continue
|
||||
}
|
||||
if bundleCard.ID == card.ID {
|
||||
bundle.TriggerCard = dto.DeviceBundleCard{
|
||||
ICCID: bundleCard.ICCID,
|
||||
IotCardID: bundleCard.ID,
|
||||
MSISDN: bundleCard.MSISDN,
|
||||
}
|
||||
} else {
|
||||
bundle.BundleCards = append(bundle.BundleCards, dto.DeviceBundleCard{
|
||||
ICCID: bundleCard.ICCID,
|
||||
IotCardID: bundleCard.ID,
|
||||
MSISDN: bundleCard.MSISDN,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resp.DeviceBundles = append(resp.DeviceBundles, bundle)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
deviceCardCount := 0
|
||||
for _, bundle := range resp.DeviceBundles {
|
||||
deviceCardCount += 1 + len(bundle.BundleCards)
|
||||
resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{
|
||||
ICCID: card.ICCID,
|
||||
IotCardID: card.ID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierID: card.CarrierID,
|
||||
StatusName: getCardStatusName(card.Status),
|
||||
})
|
||||
}
|
||||
|
||||
resp.Summary = dto.AllocatePreviewSummary{
|
||||
StandaloneCardCount: len(resp.StandaloneCards),
|
||||
DeviceCount: len(resp.DeviceBundles),
|
||||
DeviceCardCount: deviceCardCount,
|
||||
TotalCardCount: len(resp.StandaloneCards) + deviceCardCount,
|
||||
DeviceCount: 0,
|
||||
DeviceCardCount: 0,
|
||||
TotalCardCount: len(resp.StandaloneCards),
|
||||
FailedCount: len(resp.FailedItems),
|
||||
}
|
||||
|
||||
@@ -186,36 +129,15 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(preview.DeviceBundles) > 0 && !req.ConfirmDeviceBundles {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "存在设备包,请确认整体授权设备下所有卡")
|
||||
}
|
||||
|
||||
resp := &dto.AllocateCardsResp{
|
||||
FailedItems: preview.FailedItems,
|
||||
FailCount: len(preview.FailedItems),
|
||||
AllocatedDevices: make([]dto.AllocatedDevice, 0),
|
||||
FailedItems: preview.FailedItems,
|
||||
FailCount: len(preview.FailedItems),
|
||||
}
|
||||
|
||||
cardIDsToAllocate := make([]uint, 0)
|
||||
for _, card := range preview.StandaloneCards {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||||
}
|
||||
for _, bundle := range preview.DeviceBundles {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, bundle.TriggerCard.IotCardID)
|
||||
for _, card := range bundle.BundleCards {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||||
}
|
||||
iccids := []string{bundle.TriggerCard.ICCID}
|
||||
for _, card := range bundle.BundleCards {
|
||||
iccids = append(iccids, card.ICCID)
|
||||
}
|
||||
resp.AllocatedDevices = append(resp.AllocatedDevices, dto.AllocatedDevice{
|
||||
DeviceID: bundle.DeviceID,
|
||||
DeviceNo: bundle.DeviceNo,
|
||||
CardCount: 1 + len(bundle.BundleCards),
|
||||
ICCIDs: iccids,
|
||||
})
|
||||
}
|
||||
|
||||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate)
|
||||
if err != nil {
|
||||
@@ -235,6 +157,7 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
AuthorizedBy: currentUserID,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: userType,
|
||||
Remark: req.Remark,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user