This commit is contained in:
@@ -2,13 +2,19 @@ package polling
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
@@ -17,46 +23,36 @@ import (
|
||||
// 管理 IoT 卡和设备的轮询启用状态
|
||||
// S2 修复:card 类型委托给 IotCardService(含 DB 写入 + callback 通知),避免绕过生命周期
|
||||
type AssetPollingService struct {
|
||||
db *gorm.DB
|
||||
deviceStore *postgres.DeviceStore
|
||||
deviceBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardService *iotCardSvc.Service
|
||||
queueMgr *polling.PollingQueueManager
|
||||
logger *zap.Logger
|
||||
assetAuditService assetAuditSvc.OperationLogger
|
||||
auditWriter *auditinfra.Writer
|
||||
}
|
||||
|
||||
// NewAssetPollingService 创建资产轮询管控服务
|
||||
func NewAssetPollingService(
|
||||
db *gorm.DB,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
deviceBindingStore *postgres.DeviceSimBindingStore,
|
||||
iotCardService *iotCardSvc.Service,
|
||||
queueMgr *polling.PollingQueueManager,
|
||||
logger *zap.Logger,
|
||||
assetAuditService assetAuditSvc.OperationLogger,
|
||||
auditWriter *auditinfra.Writer,
|
||||
) *AssetPollingService {
|
||||
return &AssetPollingService{
|
||||
db: db,
|
||||
deviceStore: deviceStore,
|
||||
deviceBindingStore: deviceBindingStore,
|
||||
iotCardService: iotCardService,
|
||||
queueMgr: queueMgr,
|
||||
logger: logger,
|
||||
assetAuditService: assetAuditService,
|
||||
auditWriter: auditWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AssetPollingService) logAssetPollingAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
if p.OperationType == "" {
|
||||
p.OperationType = constants.AssetAuditOpAssetPollingStatus
|
||||
}
|
||||
if p.Operator.Type == "" {
|
||||
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
|
||||
}
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
// UpdatePollingStatus 更新资产轮询状态
|
||||
// assetType: "card" 或 "device"
|
||||
// assetID: 资产ID
|
||||
@@ -64,87 +60,23 @@ func (s *AssetPollingService) logAssetPollingAudit(ctx context.Context, p assetA
|
||||
func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType string, assetID uint, enablePolling bool) error {
|
||||
switch assetType {
|
||||
case constants.AssetTypeIotCard:
|
||||
beforeData := map[string]any{
|
||||
"asset_type": constants.AssetTypeIotCard,
|
||||
"asset_id": assetID,
|
||||
"enable_polling": "unknown",
|
||||
"source_service": "asset_polling",
|
||||
}
|
||||
afterData := map[string]any{
|
||||
"asset_type": constants.AssetTypeIotCard,
|
||||
"asset_id": assetID,
|
||||
"enable_polling": enablePolling,
|
||||
}
|
||||
// S2 修复:委托给 IotCardService,确保 DB 写入 + PollingCallback 回调一并触发
|
||||
if err := s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: assetID,
|
||||
OperationDesc: "统一入口更新轮询状态失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
AfterData: afterData,
|
||||
})
|
||||
return err
|
||||
}
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: assetID,
|
||||
OperationDesc: "统一入口更新轮询状态",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BeforeData: beforeData,
|
||||
AfterData: afterData,
|
||||
})
|
||||
return nil
|
||||
return s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling)
|
||||
|
||||
case constants.AssetTypeDevice:
|
||||
device, getErr := s.deviceStore.GetByID(ctx, assetID)
|
||||
if getErr != nil {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(getErr)
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: assetID,
|
||||
OperationDesc: "统一入口更新轮询状态失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
AfterData: map[string]any{
|
||||
"asset_type": constants.AssetTypeDevice,
|
||||
"asset_id": assetID,
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
return getErr
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, getErr, "查询设备失败")
|
||||
result := constants.AuditResultFailed
|
||||
if stderrors.Is(getErr, gorm.ErrRecordNotFound) {
|
||||
appErr = errors.New(errors.CodeNotFound, "设备不存在")
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordDevicePollingFailure(ctx, &model.Device{Model: gorm.Model{ID: assetID}}, enablePolling, result, appErr)
|
||||
return appErr
|
||||
}
|
||||
beforeData := map[string]any{
|
||||
"asset_type": constants.AssetTypeDevice,
|
||||
"asset_id": device.ID,
|
||||
"asset_identifier": device.VirtualNo,
|
||||
"enable_polling": device.EnablePolling,
|
||||
}
|
||||
afterData := map[string]any{
|
||||
"asset_type": constants.AssetTypeDevice,
|
||||
"asset_id": device.ID,
|
||||
"asset_identifier": device.VirtualNo,
|
||||
"enable_polling": enablePolling,
|
||||
}
|
||||
// 1. 更新设备的 enable_polling 字段
|
||||
if err := s.deviceStore.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口更新轮询状态失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
AfterData: afterData,
|
||||
})
|
||||
if err := s.updateDevicePollingStatus(ctx, device, enablePolling); err != nil {
|
||||
s.recordDevicePollingFailure(ctx, device, enablePolling, constants.AuditResultFailed, err)
|
||||
return err
|
||||
}
|
||||
bindings, err := s.deviceBindingStore.ListByDeviceID(ctx, assetID)
|
||||
@@ -173,33 +105,76 @@ func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口更新轮询状态",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BeforeData: beforeData,
|
||||
AfterData: afterData,
|
||||
})
|
||||
return nil
|
||||
|
||||
default:
|
||||
err := errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: assetType,
|
||||
AssetID: assetID,
|
||||
OperationDesc: "统一入口更新轮询状态被拒绝",
|
||||
ResultStatus: constants.AssetAuditResultDenied,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
AfterData: map[string]any{
|
||||
"asset_type": assetType,
|
||||
"asset_id": assetID,
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
return err
|
||||
return errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AssetPollingService) updateDevicePollingStatus(ctx context.Context, device *model.Device, enablePolling bool) error {
|
||||
if s.db == nil || s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备统一审计接缝未配置")
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if device.EnablePolling != enablePolling {
|
||||
result := tx.Model(&model.Device{}).Where("id = ? AND enable_polling = ?", device.ID, device.EnablePolling).Update("enable_polling", enablePolling)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备轮询状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "轮询状态已变更,请刷新后重试")
|
||||
}
|
||||
}
|
||||
return s.appendDevicePollingAudit(ctx, tx, device, enablePolling, constants.AuditResultSuccess, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AssetPollingService) appendDevicePollingAudit(ctx context.Context, tx *gorm.DB, device *model.Device, enablePolling bool, result string, businessErr error) error {
|
||||
if device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备审计资源不完整")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(device.ID), 10)
|
||||
errorCode, errorSummary := auditErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, auditinfra.AppendInput{
|
||||
ActionCode: constants.AuditActionDevicePollingStatusUpdated,
|
||||
Summary: "更新设备轮询开关",
|
||||
Result: result,
|
||||
ErrorCode: errorCode,
|
||||
ErrorSummary: errorSummary,
|
||||
ScopeType: constants.AuditScopePlatform,
|
||||
Resources: []auditinfra.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &id, Key: auditinfra.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: auditinfra.DeviceIdentitySnapshot(device),
|
||||
BeforeData: map[string]any{"enable_polling": device.EnablePolling}, AfterData: map[string]any{"enable_polling": enablePolling},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备轮询开关已更新",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AssetPollingService) recordDevicePollingFailure(ctx context.Context, device *model.Device, enablePolling bool, result string, businessErr error) {
|
||||
if s == nil || s.db == nil || s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return
|
||||
}
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDevicePollingAudit(ctx, tx, device, enablePolling, result, businessErr)
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
errorCode, _ := auditErrorInfo(businessErr)
|
||||
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionDevicePollingStatusUpdated, strconv.FormatUint(uint64(device.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err)
|
||||
}
|
||||
|
||||
func auditErrorInfo(err error) (string, string) {
|
||||
if err == nil {
|
||||
return "", ""
|
||||
}
|
||||
var appErr *errors.AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
return strconv.Itoa(appErr.Code), appErr.Error()
|
||||
}
|
||||
return "", err.Error()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user