feat: 实现企业设备授权功能并归档 OpenSpec 变更
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:
2026-01-29 13:18:49 +08:00
parent e87513541b
commit b02175271a
118 changed files with 14306 additions and 472 deletions

View File

@@ -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,
})
}