Files
junhong_cmp_fiber/internal/service/device/service.go
huang cb75b5668b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
操作日志
2026-04-27 12:16:38 +08:00

1804 lines
46 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 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.ShopID != nil {
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)
bindingCounts, err := s.getBindingCounts(ctx, s.extractDeviceIDs(devices))
if err != nil {
return nil, err
}
list := make([]*dto.DeviceResponse, 0, len(devices))
for _, device := range devices {
item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts)
list = append(list, item)
}
return &dto.ListDeviceResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// 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
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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
}
return s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts), 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
}
beforeData := make([]map[string]any, 0, len(devices))
for _, device := range devices {
beforeData = append(beforeData, deviceSnapshot(device))
}
newStatus := constants.DeviceStatusDistributed
targetShopID := req.TargetShopID
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,
"failed_items": failedItems,
},
len(devices),
len(deviceIDs),
len(failedItems),
err,
)
return nil, err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceAllocate,
"设备分配",
constants.AssetAuditResultSuccess,
nil,
map[string]any{"devices": beforeData},
map[string]any{
"target_shop_id": req.TargetShopID,
"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
}
beforeData := make([]map[string]any, 0, len(devices))
for _, device := range devices {
beforeData = append(beforeData, deviceSnapshot(device))
}
var newShopID *uint
var newStatus int
if isPlatform {
newShopID = nil
newStatus = constants.DeviceStatusInStock
} else {
newShopID = operatorShopID
newStatus = constants.DeviceStatusDistributed
}
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,
},
len(devices),
len(deviceIDs),
len(failedItems),
err,
)
return nil, err
}
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,
},
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) 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) *dto.DeviceResponse {
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: s.getDeviceStatusName(device.Status),
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) getDeviceStatusName(status int) string {
switch status {
case constants.DeviceStatusInStock:
return "在库"
case constants.DeviceStatusDistributed:
return "已分销"
case constants.DeviceStatusActivated:
return "已激活"
case constants.DeviceStatusSuspended:
return "已停用"
default:
return "未知"
}
}
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) {
devices, err := s.deviceStore.GetByIDs(ctx, req.DeviceIDs)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
"operator_shop_id": operatorShopID,
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
err,
)
return nil, err
}
if len(devices) == 0 {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定被拒绝",
constants.AssetAuditResultDenied,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
"reason": "设备不存在",
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
errors.New(errors.CodeNotFound, "设备不存在"),
)
return &dto.BatchSetDeviceSeriesBindngResponse{
SuccessCount: 0,
FailCount: len(req.DeviceIDs),
FailedItems: s.buildDeviceNotFoundFailedItems(req.DeviceIDs),
}, 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,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
appErr,
)
return nil, appErr
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
err,
)
return nil, err
}
if packageSeries.Status != 1 {
appErr := errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定被拒绝",
constants.AssetAuditResultDenied,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
appErr,
)
return nil, appErr
}
}
var successDeviceIDs []uint
var failedItems []dto.DeviceSeriesBindngFailedItem
for _, deviceID := range req.DeviceIDs {
device, exists := deviceMap[deviceID]
if !exists {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: deviceID,
VirtualNo: "",
Reason: "设备不存在",
})
continue
}
// 验证操作者权限(仅代理用户)- 检查是否有该系列的套餐分配
if operatorShopID != nil && req.SeriesID > 0 {
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSeriesBinding,
"设备系列绑定失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"series_id": req.SeriesID,
"device_ids": req.DeviceIDs,
},
len(req.DeviceIDs),
0,
len(req.DeviceIDs),
err,
)
return nil, err
}
hasSeriesAllocation := false
for _, alloc := range seriesAllocations {
if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
hasSeriesAllocation = true
break
}
}
if !hasSeriesAllocation {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: deviceID,
VirtualNo: device.VirtualNo,
Reason: "您没有权限分配该套餐系列",
})
continue
}
}
// 验证设备权限(基于 device.ShopID
if operatorShopID != nil {
if device.ShopID == nil || *device.ShopID != *operatorShopID {
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
DeviceID: device.ID,
VirtualNo: device.VirtualNo,
Reason: "无权操作此设备",
})
continue
}
}
successDeviceIDs = append(successDeviceIDs, 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,
"success_device_ids": successDeviceIDs,
"failed_items": failedItems,
},
len(req.DeviceIDs),
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{
"series_id": req.SeriesID,
"success_device_ids": successDeviceIDs,
"failed_items": failedItems,
},
len(req.DeviceIDs),
len(successDeviceIDs),
len(failedItems),
resultErr,
)
return &dto.BatchSetDeviceSeriesBindngResponse{
SuccessCount: len(successDeviceIDs),
FailCount: len(failedItems),
FailedItems: failedItems,
}, nil
}
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()
}