全局审计完成
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s

This commit is contained in:
2026-08-07 11:02:52 +08:00
parent 88cc5e96ec
commit c64f3d8b80
94 changed files with 8641 additions and 6714 deletions

View File

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