All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2033 lines
53 KiB
Go
2033 lines
53 KiB
Go
package device
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||
"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"
|
||
"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/logger"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
redis *redis.Client
|
||
deviceStore *postgres.DeviceStore
|
||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||
iotCardStore *postgres.IotCardStore
|
||
shopStore *postgres.ShopStore
|
||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
packageSeriesStore *postgres.PackageSeriesStore
|
||
gatewayClient *gateway.Client
|
||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||
assetAuditService AssetAuditService
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
rds *redis.Client,
|
||
deviceStore *postgres.DeviceStore,
|
||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
shopStore *postgres.ShopStore,
|
||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
packageSeriesStore *postgres.PackageSeriesStore,
|
||
gatewayClient *gateway.Client,
|
||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||
assetAuditService AssetAuditService,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
redis: rds,
|
||
deviceStore: deviceStore,
|
||
deviceSimBindingStore: deviceSimBindingStore,
|
||
iotCardStore: iotCardStore,
|
||
shopStore: shopStore,
|
||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
packageSeriesStore: packageSeriesStore,
|
||
gatewayClient: gatewayClient,
|
||
assetIdentifierStore: assetIdentifierStore,
|
||
assetAuditService: assetAuditService,
|
||
}
|
||
}
|
||
|
||
// List 获取设备列表
|
||
func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.ListDeviceResponse, error) {
|
||
page := req.Page
|
||
pageSize := req.PageSize
|
||
if page == 0 {
|
||
page = 1
|
||
}
|
||
if pageSize == 0 {
|
||
pageSize = constants.DefaultPageSize
|
||
}
|
||
|
||
opts := &store.QueryOptions{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
filters := make(map[string]interface{})
|
||
if req.VirtualNo != "" {
|
||
filters["virtual_no"] = req.VirtualNo
|
||
}
|
||
if req.DeviceName != "" {
|
||
filters["device_name"] = req.DeviceName
|
||
}
|
||
if req.Status != nil {
|
||
filters["status"] = *req.Status
|
||
}
|
||
if req.ActivationStatus != nil {
|
||
filters["activation_status"] = *req.ActivationStatus
|
||
}
|
||
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
|
||
if hasShopIDs {
|
||
filters["shop_ids"] = shopIDs
|
||
} else if req.ShopID != nil {
|
||
if *req.ShopID == 0 {
|
||
filters["shop_ids"] = []uint{}
|
||
} else {
|
||
filters["shop_id"] = req.ShopID
|
||
}
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
if req.DeviceType != "" {
|
||
filters["device_type"] = req.DeviceType
|
||
}
|
||
if req.Manufacturer != "" {
|
||
filters["manufacturer"] = req.Manufacturer
|
||
}
|
||
if req.CreatedAtStart != nil {
|
||
filters["created_at_start"] = *req.CreatedAtStart
|
||
}
|
||
if req.CreatedAtEnd != nil {
|
||
filters["created_at_end"] = *req.CreatedAtEnd
|
||
}
|
||
if req.SeriesID != nil {
|
||
filters["series_id"] = *req.SeriesID
|
||
}
|
||
|
||
devices, total, err := s.deviceStore.List(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopData(ctx, devices)
|
||
seriesMap := s.loadSeriesNames(ctx, devices)
|
||
deviceIDs := s.extractDeviceIDs(devices)
|
||
bindingCounts, err := s.getBindingCounts(ctx, deviceIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, deviceIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
list := make([]*dto.DeviceResponse, 0, len(devices))
|
||
for _, device := range devices {
|
||
item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses)
|
||
list = append(list, item)
|
||
}
|
||
|
||
return &dto.ListDeviceResponse{
|
||
List: list,
|
||
Total: total,
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
func normalizeShopIDs(ids []uint) ([]uint, bool) {
|
||
if len(ids) == 0 {
|
||
return nil, false
|
||
}
|
||
seen := make(map[uint]struct{}, len(ids))
|
||
result := make([]uint, 0, len(ids))
|
||
for _, id := range ids {
|
||
if id == 0 {
|
||
continue
|
||
}
|
||
if _, exists := seen[id]; exists {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
result = append(result, id)
|
||
}
|
||
return result, true
|
||
}
|
||
|
||
// Get 获取设备详情
|
||
func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error) {
|
||
device, err := s.deviceStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopData(ctx, []*model.Device{device})
|
||
seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
|
||
bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
|
||
}
|
||
|
||
// GetByDeviceNo 通过设备号获取设备详情
|
||
func (s *Service) GetByDeviceNo(ctx context.Context, deviceNo string) (*dto.DeviceResponse, error) {
|
||
device, err := s.deviceStore.GetByDeviceNo(ctx, deviceNo)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopData(ctx, []*model.Device{device})
|
||
seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
|
||
bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
|
||
}
|
||
|
||
// GetByIdentifier 通过任意标识符获取设备详情
|
||
// 支持 device_no(虚拟号)、imei、sn 三个字段的自动匹配
|
||
func (s *Service) GetByIdentifier(ctx context.Context, identifier string) (*dto.DeviceResponse, error) {
|
||
device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
shopMap := s.loadShopData(ctx, []*model.Device{device})
|
||
seriesMap := s.loadSeriesNames(ctx, []*model.Device{device})
|
||
bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
activationStatuses, err := s.deviceStore.GetActivationStatusMap(ctx, []uint{device.ID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses), nil
|
||
}
|
||
|
||
// GetDeviceByIdentifier 通过任意标识符获取设备模型(内部使用,不转为 DTO)
|
||
// 用于 Handler 层获取设备后提取 IMEI 调用 Gateway API
|
||
func (s *Service) GetDeviceByIdentifier(ctx context.Context, identifier string) (*model.Device, error) {
|
||
device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "设备不存在或无权限访问")
|
||
}
|
||
return nil, err
|
||
}
|
||
return device, nil
|
||
}
|
||
|
||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||
auditDevice := &model.Device{}
|
||
auditDevice.ID = id
|
||
device, err := s.deviceStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceDelete,
|
||
"删除设备失败",
|
||
constants.AssetAuditResultFailed,
|
||
auditDevice,
|
||
nil,
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceDelete,
|
||
"删除设备失败",
|
||
constants.AssetAuditResultFailed,
|
||
auditDevice,
|
||
nil,
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
err,
|
||
)
|
||
return err
|
||
}
|
||
beforeData := deviceSnapshot(device)
|
||
|
||
if err := s.deviceSimBindingStore.UnbindByDeviceID(ctx, device.ID); err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceDelete,
|
||
"删除设备失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
beforeData,
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
err,
|
||
)
|
||
return err
|
||
}
|
||
|
||
if err := s.deviceStore.Delete(ctx, id); err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceDelete,
|
||
"删除设备失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
beforeData,
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
err,
|
||
)
|
||
return err
|
||
}
|
||
|
||
if s.assetIdentifierStore != nil {
|
||
_ = s.assetIdentifierStore.DeleteByAsset(ctx, model.AssetTypeDevice, id)
|
||
}
|
||
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceDelete,
|
||
"删除设备",
|
||
constants.AssetAuditResultSuccess,
|
||
device,
|
||
beforeData,
|
||
map[string]any{
|
||
"deleted": true,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
nil,
|
||
)
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetByVirtualNo 通过虚拟号获取设备
|
||
func (s *Service) GetByVirtualNo(ctx context.Context, virtualNo string) (*model.Device, error) {
|
||
return s.deviceStore.GetByIdentifier(ctx, virtualNo)
|
||
}
|
||
|
||
// GetCardByICCID 通过 ICCID 获取 IoT 卡
|
||
func (s *Service) GetCardByICCID(ctx context.Context, iccid string) (*model.IotCard, error) {
|
||
cards, err := s.iotCardStore.GetByICCIDs(ctx, []string{iccid})
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡失败")
|
||
}
|
||
if len(cards) == 0 {
|
||
return nil, errors.New(errors.CodeNotFound, "卡不存在")
|
||
}
|
||
return cards[0], nil
|
||
}
|
||
|
||
// AllocateDevices 批量分配设备
|
||
func (s *Service) AllocateDevices(ctx context.Context, req *dto.AllocateDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateDevicesResponse, error) {
|
||
// 代理仅可分配给直属下级;平台/超级管理员可跨级分配
|
||
if err := s.validateDirectSubordinate(ctx, operatorShopID, req.TargetShopID); err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"device_ids": req.DeviceIDs,
|
||
},
|
||
len(req.DeviceIDs),
|
||
0,
|
||
len(req.DeviceIDs),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
|
||
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"device_ids": req.DeviceIDs,
|
||
},
|
||
len(req.DeviceIDs),
|
||
0,
|
||
len(req.DeviceIDs),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
|
||
if len(devices) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"device_ids": req.DeviceIDs,
|
||
"reason": "未找到可分配设备",
|
||
},
|
||
len(req.DeviceIDs),
|
||
0,
|
||
len(req.DeviceIDs),
|
||
errors.New(errors.CodeNotFound, "未找到可分配设备"),
|
||
)
|
||
return &dto.AllocateDevicesResponse{
|
||
SuccessCount: 0,
|
||
FailCount: 0,
|
||
FailedItems: []dto.AllocationDeviceFailedItem{},
|
||
}, nil
|
||
}
|
||
|
||
var deviceIDs []uint
|
||
var failedItems []dto.AllocationDeviceFailedItem
|
||
|
||
isPlatform := operatorShopID == nil
|
||
|
||
for _, device := range devices {
|
||
// 平台只能分配 shop_id=NULL 的设备
|
||
if isPlatform && device.ShopID != nil {
|
||
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "平台只能分配库存设备",
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 代理只能分配自己店铺的设备
|
||
if !isPlatform && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
|
||
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "设备不属于当前店铺",
|
||
})
|
||
continue
|
||
}
|
||
|
||
deviceIDs = append(deviceIDs, device.ID)
|
||
}
|
||
|
||
if len(deviceIDs) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"failed_items": failedItems,
|
||
},
|
||
len(devices),
|
||
0,
|
||
len(failedItems),
|
||
errors.New(errors.CodeForbidden, "无可分配设备"),
|
||
)
|
||
return &dto.AllocateDevicesResponse{
|
||
SuccessCount: 0,
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
newStatus := constants.DeviceStatusDistributed
|
||
targetShopID := req.TargetShopID
|
||
targetShopName := s.getAuditShopName(ctx, &targetShopID)
|
||
shopMap := s.loadShopData(ctx, devices)
|
||
beforeData := make([]map[string]any, 0, len(devices))
|
||
for _, device := range devices {
|
||
snapshot := deviceSnapshot(device)
|
||
snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID)
|
||
beforeData = append(beforeData, snapshot)
|
||
}
|
||
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
txDeviceStore := postgres.NewDeviceStore(tx, nil)
|
||
txCardStore := postgres.NewIotCardStore(tx, nil)
|
||
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
|
||
|
||
if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, &targetShopID, newStatus); err != nil {
|
||
return err
|
||
}
|
||
|
||
boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(boundCardIDs) > 0 {
|
||
if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, &targetShopID, constants.IotCardStatusDistributed); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeAllocate)
|
||
records := s.buildAllocationRecords(devices, deviceIDs, operatorShopID, targetShopID, operatorID, allocationNo, req.Remark)
|
||
return txRecordStore.BatchCreate(ctx, records)
|
||
})
|
||
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
map[string]any{"devices": beforeData},
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"target_shop_name": targetShopName,
|
||
"failed_items": failedItems,
|
||
},
|
||
len(devices),
|
||
len(deviceIDs),
|
||
len(failedItems),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
s.iotCardStore.InvalidateListCountCache(ctx)
|
||
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceAllocate,
|
||
"设备分配",
|
||
constants.AssetAuditResultSuccess,
|
||
nil,
|
||
map[string]any{"devices": beforeData},
|
||
map[string]any{
|
||
"target_shop_id": req.TargetShopID,
|
||
"target_shop_name": targetShopName,
|
||
"device_ids": deviceIDs,
|
||
"failed_items": failedItems,
|
||
},
|
||
len(devices),
|
||
len(deviceIDs),
|
||
len(failedItems),
|
||
nil,
|
||
)
|
||
|
||
return &dto.AllocateDevicesResponse{
|
||
SuccessCount: len(deviceIDs),
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
// RecallDevices 批量回收设备
|
||
func (s *Service) RecallDevices(ctx context.Context, req *dto.RecallDevicesRequest, operatorID uint, operatorShopID *uint) (*dto.RecallDevicesResponse, error) {
|
||
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceRecall,
|
||
"设备回收失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"device_ids": req.DeviceIDs,
|
||
},
|
||
len(req.DeviceIDs),
|
||
0,
|
||
len(req.DeviceIDs),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
|
||
if len(devices) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceRecall,
|
||
"设备回收被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"device_ids": req.DeviceIDs,
|
||
"reason": "未找到可回收设备",
|
||
},
|
||
len(req.DeviceIDs),
|
||
0,
|
||
len(req.DeviceIDs),
|
||
errors.New(errors.CodeNotFound, "未找到可回收设备"),
|
||
)
|
||
return &dto.RecallDevicesResponse{
|
||
SuccessCount: 0,
|
||
FailCount: 0,
|
||
FailedItems: []dto.AllocationDeviceFailedItem{},
|
||
}, nil
|
||
}
|
||
|
||
var deviceIDs []uint
|
||
var failedItems []dto.AllocationDeviceFailedItem
|
||
|
||
isPlatform := operatorShopID == nil
|
||
|
||
for _, device := range devices {
|
||
// 验证设备所属店铺是否为直属下级
|
||
if device.ShopID == nil {
|
||
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "设备已在平台库存中",
|
||
})
|
||
continue
|
||
}
|
||
|
||
// 验证直属下级关系(平台用户可以回收所有店铺的设备)
|
||
if !isPlatform {
|
||
if err := s.validateDirectSubordinate(ctx, operatorShopID, *device.ShopID); err != nil {
|
||
failedItems = append(failedItems, dto.AllocationDeviceFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "只能回收直属下级店铺的设备",
|
||
})
|
||
continue
|
||
}
|
||
}
|
||
|
||
deviceIDs = append(deviceIDs, device.ID)
|
||
}
|
||
|
||
if len(deviceIDs) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceRecall,
|
||
"设备回收被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"device_ids": req.DeviceIDs,
|
||
"failed_items": failedItems,
|
||
},
|
||
len(devices),
|
||
0,
|
||
len(failedItems),
|
||
errors.New(errors.CodeForbidden, "无可回收设备"),
|
||
)
|
||
return &dto.RecallDevicesResponse{
|
||
SuccessCount: 0,
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
var newShopID *uint
|
||
var newStatus int
|
||
if isPlatform {
|
||
newShopID = nil
|
||
newStatus = constants.DeviceStatusInStock
|
||
} else {
|
||
newShopID = operatorShopID
|
||
newStatus = constants.DeviceStatusDistributed
|
||
}
|
||
targetShopName := s.getAuditRecallTargetShopName(ctx, newShopID)
|
||
shopMap := s.loadShopData(ctx, devices)
|
||
beforeData := make([]map[string]any, 0, len(devices))
|
||
for _, device := range devices {
|
||
snapshot := deviceSnapshot(device)
|
||
snapshot["shop_name"] = deviceShopMapValue(shopMap, device.ShopID)
|
||
beforeData = append(beforeData, snapshot)
|
||
}
|
||
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
txDeviceStore := postgres.NewDeviceStore(tx, nil)
|
||
txCardStore := postgres.NewIotCardStore(tx, nil)
|
||
txRecordStore := postgres.NewAssetAllocationRecordStore(tx, nil)
|
||
|
||
if err := txDeviceStore.BatchUpdateShopIDAndStatus(ctx, deviceIDs, newShopID, newStatus); err != nil {
|
||
return err
|
||
}
|
||
|
||
boundCardIDs, err := s.deviceSimBindingStore.GetBoundCardIDsByDeviceIDs(ctx, deviceIDs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(boundCardIDs) > 0 {
|
||
var cardStatus int
|
||
if isPlatform {
|
||
cardStatus = constants.IotCardStatusInStock
|
||
} else {
|
||
cardStatus = constants.IotCardStatusDistributed
|
||
}
|
||
if err := txCardStore.BatchUpdateShopIDAndStatus(ctx, boundCardIDs, newShopID, cardStatus); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
allocationNo := s.assetAllocationRecordStore.GenerateAllocationNo(ctx, constants.AssetAllocationTypeRecall)
|
||
records := s.buildRecallRecords(devices, deviceIDs, operatorShopID, newShopID, operatorID, allocationNo, req.Remark)
|
||
return txRecordStore.BatchCreate(ctx, records)
|
||
})
|
||
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceRecall,
|
||
"设备回收失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
map[string]any{"devices": beforeData},
|
||
map[string]any{
|
||
"device_ids": deviceIDs,
|
||
"failed_items": failedItems,
|
||
"target_shop_name": targetShopName,
|
||
},
|
||
len(devices),
|
||
len(deviceIDs),
|
||
len(failedItems),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
s.iotCardStore.InvalidateListCountCache(ctx)
|
||
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceRecall,
|
||
"设备回收",
|
||
constants.AssetAuditResultSuccess,
|
||
nil,
|
||
map[string]any{"devices": beforeData},
|
||
map[string]any{
|
||
"device_ids": deviceIDs,
|
||
"failed_items": failedItems,
|
||
"target_shop": newShopID,
|
||
"target_shop_name": targetShopName,
|
||
},
|
||
len(devices),
|
||
len(deviceIDs),
|
||
len(failedItems),
|
||
nil,
|
||
)
|
||
|
||
return &dto.RecallDevicesResponse{
|
||
SuccessCount: len(deviceIDs),
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
// 辅助方法
|
||
|
||
func (s *Service) validateDirectSubordinate(ctx context.Context, operatorShopID *uint, targetShopID uint) error {
|
||
if operatorShopID != nil && *operatorShopID == targetShopID {
|
||
return errors.ErrCannotAllocateToSelf
|
||
}
|
||
|
||
targetShop, err := s.shopStore.GetByID(ctx, targetShopID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeShopNotFound)
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 平台/超级管理员可跨级分配到任意店铺(前置已校验目标店铺存在)
|
||
if operatorShopID == nil {
|
||
return nil
|
||
}
|
||
|
||
// 代理仅允许分配给直属下级店铺
|
||
if targetShop.ParentID == nil || *targetShop.ParentID != *operatorShopID {
|
||
return errors.ErrNotDirectSubordinate
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) getAuditShopName(ctx context.Context, shopID *uint) string {
|
||
if shopID == nil || *shopID == 0 {
|
||
return ""
|
||
}
|
||
var shop model.Shop
|
||
if err := s.db.WithContext(ctx).Unscoped().First(&shop, *shopID).Error; err != nil {
|
||
return ""
|
||
}
|
||
return shop.ShopName
|
||
}
|
||
|
||
func (s *Service) getAuditRecallTargetShopName(ctx context.Context, shopID *uint) string {
|
||
if shopID == nil {
|
||
return "平台库存"
|
||
}
|
||
return s.getAuditShopName(ctx, shopID)
|
||
}
|
||
|
||
func deviceShopMapValue(shopMap map[uint]string, shopID *uint) string {
|
||
if shopID == nil {
|
||
return ""
|
||
}
|
||
return shopMap[*shopID]
|
||
}
|
||
|
||
func (s *Service) loadShopData(ctx context.Context, devices []*model.Device) map[uint]string {
|
||
shopIDs := make([]uint, 0)
|
||
shopIDSet := make(map[uint]bool)
|
||
|
||
for _, device := range devices {
|
||
if device.ShopID != nil && *device.ShopID > 0 && !shopIDSet[*device.ShopID] {
|
||
shopIDs = append(shopIDs, *device.ShopID)
|
||
shopIDSet[*device.ShopID] = true
|
||
}
|
||
}
|
||
|
||
shopMap := make(map[uint]string)
|
||
if len(shopIDs) > 0 {
|
||
var shops []model.Shop
|
||
// 使用 Unscoped() 包含已删除的店铺,确保能显示店铺名称
|
||
s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops)
|
||
for _, shop := range shops {
|
||
shopMap[shop.ID] = shop.ShopName
|
||
}
|
||
}
|
||
|
||
return shopMap
|
||
}
|
||
|
||
func (s *Service) loadSeriesNames(ctx context.Context, devices []*model.Device) map[uint]string {
|
||
seriesIDs := make([]uint, 0)
|
||
seriesIDSet := make(map[uint]bool)
|
||
|
||
for _, device := range devices {
|
||
if device.SeriesID != nil && *device.SeriesID > 0 && !seriesIDSet[*device.SeriesID] {
|
||
seriesIDs = append(seriesIDs, *device.SeriesID)
|
||
seriesIDSet[*device.SeriesID] = true
|
||
}
|
||
}
|
||
|
||
seriesMap := make(map[uint]string)
|
||
if len(seriesIDs) > 0 {
|
||
var seriesList []model.PackageSeries
|
||
s.db.WithContext(ctx).Where("id IN ?", seriesIDs).Find(&seriesList)
|
||
for _, series := range seriesList {
|
||
seriesMap[series.ID] = series.SeriesName
|
||
}
|
||
}
|
||
|
||
return seriesMap
|
||
}
|
||
|
||
func (s *Service) getBindingCounts(ctx context.Context, deviceIDs []uint) (map[uint]int64, error) {
|
||
result := make(map[uint]int64)
|
||
if len(deviceIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
|
||
bindings, err := s.deviceSimBindingStore.ListByDeviceIDs(ctx, deviceIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, binding := range bindings {
|
||
result[binding.DeviceID]++
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func (s *Service) extractDeviceIDs(devices []*model.Device) []uint {
|
||
ids := make([]uint, len(devices))
|
||
for i, device := range devices {
|
||
ids[i] = device.ID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string, seriesMap map[uint]string, bindingCounts map[uint]int64, activationStatuses map[uint]int) *dto.DeviceResponse {
|
||
activationStatus := activationStatuses[device.ID]
|
||
resp := &dto.DeviceResponse{
|
||
ID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
IMEI: device.IMEI,
|
||
SN: device.SN,
|
||
DeviceName: device.DeviceName,
|
||
DeviceModel: device.DeviceModel,
|
||
DeviceType: device.DeviceType,
|
||
MaxSimSlots: device.MaxSimSlots,
|
||
Manufacturer: device.Manufacturer,
|
||
BatchNo: device.BatchNo,
|
||
ShopID: device.ShopID,
|
||
Status: device.Status,
|
||
StatusName: constants.GetDeviceStatusName(device.Status),
|
||
ActivationStatus: activationStatus,
|
||
ActivationStatusName: constants.GetActivationStatusName(activationStatus),
|
||
BoundCardCount: int(bindingCounts[device.ID]),
|
||
SeriesID: device.SeriesID,
|
||
ActivatedAt: device.ActivatedAt,
|
||
CreatedAt: device.CreatedAt,
|
||
UpdatedAt: device.UpdatedAt,
|
||
OnlineStatus: device.OnlineStatus,
|
||
LastOnlineTime: device.LastOnlineTime,
|
||
SoftwareVersion: device.SoftwareVersion,
|
||
SwitchMode: device.SwitchMode,
|
||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||
}
|
||
|
||
if device.ShopID != nil && *device.ShopID > 0 {
|
||
resp.ShopName = shopMap[*device.ShopID]
|
||
}
|
||
|
||
if device.SeriesID != nil && *device.SeriesID > 0 {
|
||
resp.SeriesName = seriesMap[*device.SeriesID]
|
||
}
|
||
|
||
return resp
|
||
}
|
||
|
||
func (s *Service) buildAllocationRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
|
||
successIDSet := make(map[uint]bool)
|
||
for _, id := range successDeviceIDs {
|
||
successIDSet[id] = true
|
||
}
|
||
|
||
var records []*model.AssetAllocationRecord
|
||
for _, device := range devices {
|
||
if !successIDSet[device.ID] {
|
||
continue
|
||
}
|
||
|
||
record := &model.AssetAllocationRecord{
|
||
AllocationNo: allocationNo,
|
||
AllocationType: constants.AssetAllocationTypeAllocate,
|
||
AssetType: constants.AssetTypeDevice,
|
||
AssetID: device.ID,
|
||
AssetIdentifier: device.VirtualNo,
|
||
ToOwnerType: constants.OwnerTypeShop,
|
||
ToOwnerID: toShopID,
|
||
OperatorID: operatorID,
|
||
Remark: remark,
|
||
}
|
||
|
||
if fromShopID == nil {
|
||
record.FromOwnerType = constants.OwnerTypePlatform
|
||
record.FromOwnerID = nil
|
||
} else {
|
||
record.FromOwnerType = constants.OwnerTypeShop
|
||
record.FromOwnerID = fromShopID
|
||
}
|
||
|
||
records = append(records, record)
|
||
}
|
||
|
||
return records
|
||
}
|
||
|
||
func (s *Service) buildRecallRecords(devices []*model.Device, successDeviceIDs []uint, fromShopID *uint, toShopID *uint, operatorID uint, allocationNo, remark string) []*model.AssetAllocationRecord {
|
||
successIDSet := make(map[uint]bool)
|
||
for _, id := range successDeviceIDs {
|
||
successIDSet[id] = true
|
||
}
|
||
|
||
var records []*model.AssetAllocationRecord
|
||
for _, device := range devices {
|
||
if !successIDSet[device.ID] {
|
||
continue
|
||
}
|
||
|
||
record := &model.AssetAllocationRecord{
|
||
AllocationNo: allocationNo,
|
||
AllocationType: constants.AssetAllocationTypeRecall,
|
||
AssetType: constants.AssetTypeDevice,
|
||
AssetID: device.ID,
|
||
AssetIdentifier: device.VirtualNo,
|
||
OperatorID: operatorID,
|
||
Remark: remark,
|
||
}
|
||
|
||
if fromShopID == nil {
|
||
record.FromOwnerType = constants.OwnerTypePlatform
|
||
record.FromOwnerID = nil
|
||
} else {
|
||
record.FromOwnerType = constants.OwnerTypeShop
|
||
record.FromOwnerID = fromShopID
|
||
}
|
||
|
||
if toShopID == nil {
|
||
record.ToOwnerType = constants.OwnerTypePlatform
|
||
record.ToOwnerID = 0
|
||
} else {
|
||
record.ToOwnerType = constants.OwnerTypeShop
|
||
record.ToOwnerID = *toShopID
|
||
}
|
||
|
||
records = append(records, record)
|
||
}
|
||
|
||
return records
|
||
}
|
||
|
||
// BatchSetSeriesBinding 批量设置设备的套餐系列绑定
|
||
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetDeviceSeriesBindngResponse, error) {
|
||
selectionType, err := normalizeDeviceSeriesBindingSelection(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
devices, err := s.getDevicesForSeriesBinding(ctx, req, selectionType)
|
||
batchTotal := deviceSeriesBindingBatchTotal(req, selectionType, devices)
|
||
auditData := deviceSeriesBindingAuditData(req, selectionType, operatorShopID)
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
batchTotal,
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
|
||
if len(devices) == 0 {
|
||
failedItems := []dto.DeviceSeriesBindngFailedItem{}
|
||
if selectionType == dto.SelectionTypeList {
|
||
failedItems = s.buildDeviceNotFoundFailedItems(req.DeviceIDs)
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
len(failedItems),
|
||
errors.New(errors.CodeNotFound, "设备不存在"),
|
||
)
|
||
}
|
||
return &dto.BatchSetDeviceSeriesBindngResponse{
|
||
SuccessCount: 0,
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
deviceMap := make(map[uint]*model.Device)
|
||
for _, device := range devices {
|
||
deviceMap[device.ID] = device
|
||
}
|
||
|
||
// 验证系列存在(仅当 SeriesID > 0 时)
|
||
var packageSeries *model.PackageSeries
|
||
if req.SeriesID > 0 {
|
||
packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
appErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
batchTotal,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
batchTotal,
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
if packageSeries.Status != 1 {
|
||
appErr := errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
batchTotal,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
}
|
||
|
||
var successDeviceIDs []uint
|
||
var failedItems []dto.DeviceSeriesBindngFailedItem
|
||
successDeviceIDSet := make(map[uint]struct{})
|
||
|
||
hasSeriesAllocation := true
|
||
if operatorShopID != nil && req.SeriesID > 0 {
|
||
hasSeriesAllocation, err = s.hasAvailableSeriesAllocation(ctx, *operatorShopID, req.SeriesID)
|
||
if err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
auditData,
|
||
batchTotal,
|
||
0,
|
||
batchTotal,
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
addDevice := func(device *model.Device, requestedDeviceID uint) {
|
||
if device == nil {
|
||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||
DeviceID: requestedDeviceID,
|
||
VirtualNo: "",
|
||
Reason: "设备不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
if !hasSeriesAllocation {
|
||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "您没有权限分配该套餐系列",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 代理只能操作自己店铺名下的设备,保持旧接口权限语义不变。
|
||
if operatorShopID != nil && (device.ShopID == nil || *device.ShopID != *operatorShopID) {
|
||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||
DeviceID: device.ID,
|
||
VirtualNo: device.VirtualNo,
|
||
Reason: "无权操作此设备",
|
||
})
|
||
return
|
||
}
|
||
|
||
if _, exists := successDeviceIDSet[device.ID]; exists {
|
||
return
|
||
}
|
||
successDeviceIDSet[device.ID] = struct{}{}
|
||
successDeviceIDs = append(successDeviceIDs, device.ID)
|
||
}
|
||
|
||
if selectionType == dto.SelectionTypeList {
|
||
for _, deviceID := range req.DeviceIDs {
|
||
addDevice(deviceMap[deviceID], deviceID)
|
||
}
|
||
} else {
|
||
for _, device := range devices {
|
||
addDevice(device, device.ID)
|
||
}
|
||
}
|
||
|
||
if len(successDeviceIDs) > 0 {
|
||
var seriesIDPtr *uint
|
||
if req.SeriesID > 0 {
|
||
seriesIDPtr = &req.SeriesID
|
||
}
|
||
if err := s.deviceStore.BatchUpdateSeriesID(ctx, successDeviceIDs, seriesIDPtr); err != nil {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"series_id": req.SeriesID,
|
||
"selection_type": selectionType,
|
||
"success_device_ids": successDeviceIDs,
|
||
"failed_items": failedItems,
|
||
},
|
||
batchTotal,
|
||
len(successDeviceIDs),
|
||
len(failedItems),
|
||
err,
|
||
)
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
beforeData := make([]map[string]any, 0, len(devices))
|
||
for _, device := range devices {
|
||
beforeData = append(beforeData, deviceSnapshot(device))
|
||
}
|
||
resultStatus := constants.AssetAuditResultSuccess
|
||
var resultErr error
|
||
if len(successDeviceIDs) == 0 && len(failedItems) > 0 {
|
||
resultStatus = constants.AssetAuditResultDenied
|
||
resultErr = errors.New(errors.CodeForbidden, "无可绑定设备")
|
||
}
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceSeriesBinding,
|
||
"设备系列绑定",
|
||
resultStatus,
|
||
nil,
|
||
map[string]any{
|
||
"devices": beforeData,
|
||
},
|
||
map[string]any{
|
||
"selection_type": selectionType,
|
||
"series_id": req.SeriesID,
|
||
"success_device_ids": successDeviceIDs,
|
||
"failed_items": failedItems,
|
||
},
|
||
batchTotal,
|
||
len(successDeviceIDs),
|
||
len(failedItems),
|
||
resultErr,
|
||
)
|
||
|
||
return &dto.BatchSetDeviceSeriesBindngResponse{
|
||
SuccessCount: len(successDeviceIDs),
|
||
FailCount: len(failedItems),
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
func normalizeDeviceSeriesBindingSelection(req *dto.BatchSetDeviceSeriesBindngRequest) (string, error) {
|
||
switch req.SelectionType {
|
||
case dto.SelectionTypeList:
|
||
if len(req.DeviceIDs) == 0 {
|
||
return "", errors.New(errors.CodeInvalidParam, "selection_type=list时device_ids不能为空")
|
||
}
|
||
return dto.SelectionTypeList, nil
|
||
case dto.SelectionTypeRange:
|
||
if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
|
||
return "", errors.New(errors.CodeInvalidParam, "selection_type=range时virtual_no_start和virtual_no_end不能为空")
|
||
}
|
||
if req.VirtualNoStart > req.VirtualNoEnd {
|
||
return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
|
||
}
|
||
return dto.SelectionTypeRange, nil
|
||
case dto.SelectionTypeFilter:
|
||
return dto.SelectionTypeFilter, nil
|
||
case "":
|
||
if len(req.DeviceIDs) > 0 {
|
||
return dto.SelectionTypeList, nil
|
||
}
|
||
if req.VirtualNoStart != "" || req.VirtualNoEnd != "" {
|
||
if req.VirtualNoStart == "" || req.VirtualNoEnd == "" {
|
||
return "", errors.New(errors.CodeInvalidParam, "virtual_no_start和virtual_no_end必须同时传入")
|
||
}
|
||
if req.VirtualNoStart > req.VirtualNoEnd {
|
||
return "", errors.New(errors.CodeInvalidParam, "设备虚拟号起始值不能大于结束值")
|
||
}
|
||
return dto.SelectionTypeRange, nil
|
||
}
|
||
if hasDeviceSeriesBindingFilters(req) {
|
||
return dto.SelectionTypeFilter, nil
|
||
}
|
||
return "", errors.New(errors.CodeInvalidParam, "请选择要设置套餐系列的设备")
|
||
default:
|
||
return "", errors.New(errors.CodeInvalidParam, "无效的选设备方式")
|
||
}
|
||
}
|
||
|
||
func hasDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) bool {
|
||
return req.VirtualNo != "" ||
|
||
req.DeviceName != "" ||
|
||
req.Status != nil ||
|
||
req.ActivationStatus != nil ||
|
||
req.ShopID != nil ||
|
||
len(req.ShopIDs) > 0 ||
|
||
req.FilterSeriesID != nil ||
|
||
req.BatchNo != "" ||
|
||
req.DeviceType != "" ||
|
||
req.Manufacturer != "" ||
|
||
req.CreatedAtStart != nil ||
|
||
req.CreatedAtEnd != nil
|
||
}
|
||
|
||
func (s *Service) getDevicesForSeriesBinding(ctx context.Context, req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string) ([]*model.Device, error) {
|
||
switch selectionType {
|
||
case dto.SelectionTypeList:
|
||
return s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
|
||
case dto.SelectionTypeRange:
|
||
return s.deviceStore.GetByFilters(ctx, map[string]any{
|
||
"virtual_no_start": req.VirtualNoStart,
|
||
"virtual_no_end": req.VirtualNoEnd,
|
||
})
|
||
case dto.SelectionTypeFilter:
|
||
return s.deviceStore.GetByFilters(ctx, buildDeviceSeriesBindingFilters(req))
|
||
default:
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的选设备方式")
|
||
}
|
||
}
|
||
|
||
func buildDeviceSeriesBindingFilters(req *dto.BatchSetDeviceSeriesBindngRequest) map[string]any {
|
||
filters := make(map[string]any)
|
||
if req.VirtualNo != "" {
|
||
filters["virtual_no"] = req.VirtualNo
|
||
}
|
||
if req.VirtualNoStart != "" {
|
||
filters["virtual_no_start"] = req.VirtualNoStart
|
||
}
|
||
if req.VirtualNoEnd != "" {
|
||
filters["virtual_no_end"] = req.VirtualNoEnd
|
||
}
|
||
if req.DeviceName != "" {
|
||
filters["device_name"] = req.DeviceName
|
||
}
|
||
if req.Status != nil {
|
||
filters["status"] = *req.Status
|
||
}
|
||
if req.ActivationStatus != nil {
|
||
filters["activation_status"] = *req.ActivationStatus
|
||
}
|
||
shopIDs, hasShopIDs := normalizeShopIDs(req.ShopIDs)
|
||
if hasShopIDs {
|
||
filters["shop_ids"] = shopIDs
|
||
} else if req.ShopID != nil {
|
||
if *req.ShopID == 0 {
|
||
filters["shop_ids"] = []uint{}
|
||
} else {
|
||
filters["shop_id"] = req.ShopID
|
||
}
|
||
}
|
||
if req.FilterSeriesID != nil {
|
||
filters["series_id"] = *req.FilterSeriesID
|
||
}
|
||
if req.BatchNo != "" {
|
||
filters["batch_no"] = req.BatchNo
|
||
}
|
||
if req.DeviceType != "" {
|
||
filters["device_type"] = req.DeviceType
|
||
}
|
||
if req.Manufacturer != "" {
|
||
filters["manufacturer"] = req.Manufacturer
|
||
}
|
||
if req.CreatedAtStart != nil {
|
||
filters["created_at_start"] = *req.CreatedAtStart
|
||
}
|
||
if req.CreatedAtEnd != nil {
|
||
filters["created_at_end"] = *req.CreatedAtEnd
|
||
}
|
||
return filters
|
||
}
|
||
|
||
func (s *Service) hasAvailableSeriesAllocation(ctx context.Context, shopID uint, seriesID uint) (bool, error) {
|
||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
for _, alloc := range seriesAllocations {
|
||
if alloc.SeriesID == seriesID && alloc.Status == 1 {
|
||
return true, nil
|
||
}
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
func deviceSeriesBindingBatchTotal(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, devices []*model.Device) int {
|
||
if selectionType == dto.SelectionTypeList {
|
||
return len(req.DeviceIDs)
|
||
}
|
||
return len(devices)
|
||
}
|
||
|
||
func deviceSeriesBindingAuditData(req *dto.BatchSetDeviceSeriesBindngRequest, selectionType string, operatorShopID *uint) map[string]any {
|
||
data := map[string]any{
|
||
"selection_type": selectionType,
|
||
"series_id": req.SeriesID,
|
||
"operator_shop_id": operatorShopID,
|
||
}
|
||
switch selectionType {
|
||
case dto.SelectionTypeList:
|
||
data["device_ids"] = req.DeviceIDs
|
||
case dto.SelectionTypeRange:
|
||
data["virtual_no_start"] = req.VirtualNoStart
|
||
data["virtual_no_end"] = req.VirtualNoEnd
|
||
case dto.SelectionTypeFilter:
|
||
data["filters"] = buildDeviceSeriesBindingFilters(req)
|
||
}
|
||
return data
|
||
}
|
||
|
||
func (s *Service) buildDeviceNotFoundFailedItems(deviceIDs []uint) []dto.DeviceSeriesBindngFailedItem {
|
||
items := make([]dto.DeviceSeriesBindngFailedItem, len(deviceIDs))
|
||
for i, id := range deviceIDs {
|
||
items[i] = dto.DeviceSeriesBindngFailedItem{
|
||
DeviceID: id,
|
||
VirtualNo: "",
|
||
Reason: "设备不存在",
|
||
}
|
||
}
|
||
return items
|
||
}
|
||
|
||
// StopDevice 设备停机
|
||
// POST /api/admin/assets/device/:device_id/stop
|
||
// 查找设备绑定的所有已实名且已开机的卡,逐一调网关停机
|
||
func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSuspendResponse, error) {
|
||
log := logger.GetAppLogger()
|
||
|
||
userID := middleware.GetUserIDFromContext(ctx)
|
||
if userID == 0 {
|
||
appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{"device_id": deviceID},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
|
||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||
if err != nil {
|
||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{"device_id": deviceID},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
|
||
// 复机保护期内禁止停机
|
||
if s.redis != nil {
|
||
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Result()
|
||
if exists > 0 {
|
||
appErr := errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
}
|
||
|
||
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||
if err != nil {
|
||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
|
||
if len(bindings) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机",
|
||
constants.AssetAuditResultSuccess,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
map[string]any{
|
||
"success_count": 0,
|
||
"fail_count": 0,
|
||
"skip_count": 0,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
nil,
|
||
)
|
||
return &dto.DeviceSuspendResponse{}, nil
|
||
}
|
||
|
||
cardIDs := make([]uint, 0, len(bindings))
|
||
for _, b := range bindings {
|
||
cardIDs = append(cardIDs, b.IotCardID)
|
||
}
|
||
|
||
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
|
||
if err != nil {
|
||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
len(cardIDs),
|
||
0,
|
||
len(cardIDs),
|
||
appErr,
|
||
)
|
||
return nil, appErr
|
||
}
|
||
|
||
beforeCards := make([]map[string]any, 0, len(cards))
|
||
for _, card := range cards {
|
||
beforeCards = append(beforeCards, map[string]any{
|
||
"id": card.ID,
|
||
"iccid": card.ICCID,
|
||
"network_status": card.NetworkStatus,
|
||
"real_name_status": card.RealNameStatus,
|
||
"stop_reason": card.StopReason,
|
||
})
|
||
}
|
||
|
||
var successCount, skipCount int
|
||
var failedItems []dto.DeviceSuspendFailItem
|
||
|
||
for _, card := range cards {
|
||
if card.RealNameStatus != constants.RealNameStatusVerified || card.NetworkStatus != constants.NetworkStatusOnline {
|
||
skipCount++
|
||
continue
|
||
}
|
||
|
||
if s.gatewayClient != nil {
|
||
log.Info("调用网关停机(设备)",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID))
|
||
if gwErr := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); gwErr != nil {
|
||
log.Error("设备停机-调网关停机失败",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID),
|
||
zap.Error(gwErr))
|
||
failedItems = append(failedItems, dto.DeviceSuspendFailItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "网关停机失败",
|
||
})
|
||
continue
|
||
}
|
||
log.Info("网关停机成功(设备)",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID))
|
||
}
|
||
|
||
now := time.Now()
|
||
if dbErr := s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{
|
||
"network_status": constants.NetworkStatusOffline,
|
||
"stopped_at": now,
|
||
"stop_reason": constants.StopReasonManual,
|
||
}); dbErr != nil {
|
||
log.Error("设备停机-更新卡状态失败",
|
||
zap.Uint("card_id", card.ID),
|
||
zap.Error(dbErr))
|
||
failedItems = append(failedItems, dto.DeviceSuspendFailItem{
|
||
ICCID: card.ICCID,
|
||
Reason: "更新卡状态失败",
|
||
})
|
||
continue
|
||
}
|
||
|
||
s.invalidatePollingCardCache(card.ID)
|
||
successCount++
|
||
}
|
||
|
||
// 成功停机至少一张卡后设置保护期
|
||
if successCount > 0 && s.redis != nil {
|
||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"), 1, constants.DeviceProtectPeriodDuration)
|
||
s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "start"))
|
||
}
|
||
|
||
resultStatus := constants.AssetAuditResultSuccess
|
||
var resultErr error
|
||
if successCount == 0 && len(failedItems) > 0 {
|
||
resultStatus = constants.AssetAuditResultFailed
|
||
resultErr = errors.New(errors.CodeGatewayError, "设备停机失败,未成功处理任何卡")
|
||
}
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStop,
|
||
"设备停机",
|
||
resultStatus,
|
||
device,
|
||
map[string]any{
|
||
"device": deviceSnapshot(device),
|
||
"cards": beforeCards,
|
||
},
|
||
map[string]any{
|
||
"success_count": successCount,
|
||
"fail_count": len(failedItems),
|
||
"skip_count": skipCount,
|
||
"failed_items": failedItems,
|
||
},
|
||
len(cards),
|
||
successCount,
|
||
len(failedItems),
|
||
resultErr,
|
||
)
|
||
|
||
return &dto.DeviceSuspendResponse{
|
||
SuccessCount: successCount,
|
||
FailCount: len(failedItems),
|
||
SkipCount: skipCount,
|
||
FailedItems: failedItems,
|
||
}, nil
|
||
}
|
||
|
||
// StartDevice 设备复机
|
||
// POST /api/admin/assets/device/:device_id/start
|
||
// 查找设备绑定的所有已实名且已停机的卡,逐一调网关复机
|
||
func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
|
||
log := logger.GetAppLogger()
|
||
|
||
userID := middleware.GetUserIDFromContext(ctx)
|
||
if userID == 0 {
|
||
appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
nil,
|
||
nil,
|
||
map[string]any{"device_id": deviceID},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||
if err != nil {
|
||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{"device_id": deviceID},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
// 停机保护期内禁止复机
|
||
if s.redis != nil {
|
||
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Result()
|
||
if exists > 0 {
|
||
appErr := errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
}
|
||
|
||
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||
if err != nil {
|
||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
if len(bindings) == 0 {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机",
|
||
constants.AssetAuditResultSuccess,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
map[string]any{
|
||
"success_count": 0,
|
||
"fail_count": 0,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
nil,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
cardIDs := make([]uint, 0, len(bindings))
|
||
for _, b := range bindings {
|
||
cardIDs = append(cardIDs, b.IotCardID)
|
||
}
|
||
|
||
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
|
||
if err != nil {
|
||
appErr := errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
map[string]any{"device": deviceSnapshot(device)},
|
||
nil,
|
||
len(cardIDs),
|
||
0,
|
||
len(cardIDs),
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
beforeCards := make([]map[string]any, 0, len(cards))
|
||
for _, card := range cards {
|
||
beforeCards = append(beforeCards, map[string]any{
|
||
"id": card.ID,
|
||
"iccid": card.ICCID,
|
||
"network_status": card.NetworkStatus,
|
||
"real_name_status": card.RealNameStatus,
|
||
"stop_reason": card.StopReason,
|
||
})
|
||
}
|
||
|
||
var successCount int
|
||
var failCount int
|
||
var lastErr error
|
||
|
||
for _, card := range cards {
|
||
if card.RealNameStatus != constants.RealNameStatusVerified || card.NetworkStatus != constants.NetworkStatusOffline {
|
||
continue
|
||
}
|
||
|
||
if s.gatewayClient != nil {
|
||
log.Info("调用网关复机(设备)",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID))
|
||
if gwErr := s.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); gwErr != nil {
|
||
log.Error("设备复机-调网关复机失败",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID),
|
||
zap.Error(gwErr))
|
||
lastErr = gwErr
|
||
failCount++
|
||
continue
|
||
}
|
||
log.Info("网关复机成功(设备)",
|
||
zap.Uint("device_id", deviceID),
|
||
zap.String("iccid", card.ICCID))
|
||
}
|
||
|
||
now := time.Now()
|
||
if dbErr := s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{
|
||
"network_status": constants.NetworkStatusOnline,
|
||
"resumed_at": now,
|
||
"stop_reason": "",
|
||
}); dbErr != nil {
|
||
log.Error("设备复机-更新卡状态失败",
|
||
zap.Uint("card_id", card.ID),
|
||
zap.Error(dbErr))
|
||
lastErr = dbErr
|
||
failCount++
|
||
continue
|
||
}
|
||
|
||
s.invalidatePollingCardCache(card.ID)
|
||
successCount++
|
||
}
|
||
|
||
// 成功复机至少一张卡后设置保护期
|
||
if successCount > 0 && s.redis != nil {
|
||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "start"), 1, constants.DeviceProtectPeriodDuration)
|
||
s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"))
|
||
}
|
||
|
||
// 全部失败时返回 error
|
||
if successCount == 0 && lastErr != nil {
|
||
appErr := errors.Wrap(errors.CodeGatewayError, lastErr, "设备复机失败,所有卡均复机失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
map[string]any{
|
||
"device": deviceSnapshot(device),
|
||
"cards": beforeCards,
|
||
},
|
||
map[string]any{
|
||
"success_count": successCount,
|
||
"fail_count": failCount,
|
||
},
|
||
len(cards),
|
||
successCount,
|
||
failCount,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpDeviceStart,
|
||
"设备复机",
|
||
constants.AssetAuditResultSuccess,
|
||
device,
|
||
map[string]any{
|
||
"device": deviceSnapshot(device),
|
||
"cards": beforeCards,
|
||
},
|
||
map[string]any{
|
||
"success_count": successCount,
|
||
"fail_count": failCount,
|
||
},
|
||
len(cards),
|
||
successCount,
|
||
failCount,
|
||
nil,
|
||
)
|
||
|
||
return nil
|
||
}
|
||
|
||
// UpdateRealnamePolicy 更新设备的实名认证策略
|
||
func (s *Service) UpdateRealnamePolicy(ctx context.Context, deviceID uint, realnamePolicy string) error {
|
||
// 检查设备是否存在
|
||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpAssetRealnamePolicy,
|
||
"更新设备实名策略失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"device_id": deviceID,
|
||
"realname_policy": realnamePolicy,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpAssetRealnamePolicy,
|
||
"更新设备实名策略失败",
|
||
constants.AssetAuditResultFailed,
|
||
nil,
|
||
nil,
|
||
map[string]any{
|
||
"device_id": deviceID,
|
||
"realname_policy": realnamePolicy,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
beforeData := map[string]any{
|
||
"realname_policy": device.RealnamePolicy,
|
||
"device": deviceSnapshot(device),
|
||
}
|
||
|
||
// 幂等检查
|
||
if device.RealnamePolicy == realnamePolicy {
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpAssetRealnamePolicy,
|
||
"更新设备实名策略被拒绝",
|
||
constants.AssetAuditResultDenied,
|
||
device,
|
||
beforeData,
|
||
map[string]any{"realname_policy": realnamePolicy},
|
||
0,
|
||
0,
|
||
0,
|
||
errors.New(errors.CodeConflict, "实名认证策略未变化"),
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// 更新数据库
|
||
if err := s.deviceStore.UpdateRealnamePolicy(ctx, deviceID, realnamePolicy); err != nil {
|
||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpAssetRealnamePolicy,
|
||
"更新设备实名策略失败",
|
||
constants.AssetAuditResultFailed,
|
||
device,
|
||
beforeData,
|
||
map[string]any{"realname_policy": realnamePolicy},
|
||
0,
|
||
0,
|
||
0,
|
||
appErr,
|
||
)
|
||
return appErr
|
||
}
|
||
|
||
s.logDeviceOperation(
|
||
ctx,
|
||
constants.AssetAuditOpAssetRealnamePolicy,
|
||
"更新设备实名策略",
|
||
constants.AssetAuditResultSuccess,
|
||
device,
|
||
beforeData,
|
||
map[string]any{
|
||
"realname_policy": realnamePolicy,
|
||
},
|
||
0,
|
||
0,
|
||
0,
|
||
nil,
|
||
)
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) invalidatePollingCardCache(cardID uint) {
|
||
if s.redis == nil {
|
||
return
|
||
}
|
||
_ = s.redis.Del(context.Background(), constants.RedisPollingCardInfoKey(cardID)).Err()
|
||
}
|