This commit is contained in:
@@ -47,7 +47,6 @@ type Service struct {
|
||||
shopRoleStore *postgres.ShopRoleStore
|
||||
shopStore ShopStoreInterface
|
||||
enterpriseStore middleware.EnterpriseStoreInterface
|
||||
auditService AuditServiceInterface
|
||||
wecomMembers WeComMemberFinder
|
||||
tokenManager *pkgAuth.TokenManager
|
||||
}
|
||||
@@ -70,10 +69,6 @@ func (s *Service) SetTokenManager(tokenManager *pkgAuth.TokenManager) {
|
||||
s.tokenManager = tokenManager
|
||||
}
|
||||
|
||||
type AuditServiceInterface interface {
|
||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||
}
|
||||
|
||||
// WeComMemberFinder 定义账号绑定时校验应用可见成员的边界。
|
||||
type WeComMemberFinder interface {
|
||||
GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error)
|
||||
@@ -87,7 +82,6 @@ func New(
|
||||
shopRoleStore *postgres.ShopRoleStore,
|
||||
shopStore ShopStoreInterface,
|
||||
enterpriseStore middleware.EnterpriseStoreInterface,
|
||||
auditService AuditServiceInterface,
|
||||
) *Service {
|
||||
return &Service{
|
||||
accountStore: accountStore,
|
||||
@@ -96,7 +90,6 @@ func New(
|
||||
shopRoleStore: shopRoleStore,
|
||||
shopStore: shopStore,
|
||||
enterpriseStore: enterpriseStore,
|
||||
auditService: auditService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Package account_audit 提供账号操作审计日志服务
|
||||
// 负责记录所有账号管理操作,用于审计追踪和合规要求
|
||||
package account_audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AccountOperationLogStore 账号操作日志存储接口
|
||||
type AccountOperationLogStore interface {
|
||||
Create(ctx context.Context, log *model.AccountOperationLog) error
|
||||
}
|
||||
|
||||
// Service 账号审计服务
|
||||
type Service struct {
|
||||
store AccountOperationLogStore
|
||||
}
|
||||
|
||||
// NewService 创建账号审计服务实例
|
||||
func NewService(store AccountOperationLogStore) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// LogOperation 记录账号操作日志(异步写入,不阻塞主流程)
|
||||
func (s *Service) LogOperation(ctx context.Context, log *model.AccountOperationLog) {
|
||||
// 异步写入审计日志,不阻塞业务操作
|
||||
go func() {
|
||||
if err := s.store.Create(context.Background(), log); err != nil {
|
||||
// 写入失败只记录错误日志,不影响业务
|
||||
logger.GetAppLogger().Error("写入账号操作日志失败",
|
||||
zap.Uint("operator_id", log.OperatorID),
|
||||
zap.String("operation_type", log.OperationType),
|
||||
zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -24,11 +24,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// AuditServiceInterface 审计日志服务接口
|
||||
type AuditServiceInterface interface {
|
||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||
}
|
||||
|
||||
// OperationPasswordServiceInterface 全局操作密码服务接口
|
||||
type OperationPasswordServiceInterface interface {
|
||||
Verify(ctx context.Context, inputPassword string) error
|
||||
@@ -49,7 +44,6 @@ type Service struct {
|
||||
offlineCreation *agentrechargeapp.OfflineCreationService
|
||||
shopStore *postgres.ShopStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
auditService AuditServiceInterface
|
||||
operationPasswordService OperationPasswordServiceInterface
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
@@ -63,7 +57,6 @@ func New(
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
auditService AuditServiceInterface,
|
||||
operationPasswordService OperationPasswordServiceInterface,
|
||||
rdb *redis.Client,
|
||||
logger *zap.Logger,
|
||||
@@ -74,7 +67,6 @@ func New(
|
||||
agentWalletStore: agentWalletStore,
|
||||
shopStore: shopStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
auditService: auditService,
|
||||
operationPasswordService: operationPasswordService,
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
|
||||
@@ -3,10 +3,14 @@ package asset
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
|
||||
infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,255 +18,154 @@ import (
|
||||
|
||||
var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.AssetStatusSold}
|
||||
|
||||
// LifecycleService 资产生命周期服务
|
||||
// LifecycleService 资产生命周期服务。
|
||||
type LifecycleService struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
assetAuditService assetAuditSvc.OperationLogger
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
auditWriter *infraAudit.Writer
|
||||
}
|
||||
|
||||
// NewLifecycleService 创建资产生命周期服务
|
||||
func NewLifecycleService(
|
||||
db *gorm.DB,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
assetAuditService assetAuditSvc.OperationLogger,
|
||||
) *LifecycleService {
|
||||
return &LifecycleService{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
assetAuditService: assetAuditService,
|
||||
}
|
||||
// NewLifecycleService 创建资产生命周期服务。
|
||||
func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, auditWriter *infraAudit.Writer) *LifecycleService {
|
||||
return &LifecycleService{db: db, iotCardStore: iotCardStore, deviceStore: deviceStore, auditWriter: auditWriter}
|
||||
}
|
||||
|
||||
func (s *LifecycleService) logLifecycleAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
if p.Operator.Type == "" {
|
||||
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
|
||||
}
|
||||
if p.OperationType == "" {
|
||||
p.OperationType = constants.AssetAuditOpAssetDeactivate
|
||||
}
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
// DeactivateIotCard 手动停用 IoT 卡
|
||||
// DeactivateIotCard 手动停用 IoT 卡。
|
||||
func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error {
|
||||
card, err := s.iotCardStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
appErr := errors.New(errors.CodeIotCardNotFound)
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: id,
|
||||
OperationDesc: "统一入口停用IoT卡失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: id,
|
||||
OperationDesc: "统一入口停用IoT卡失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
})
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
appErr = errors.New(errors.CodeIotCardNotFound)
|
||||
}
|
||||
result := constants.AuditResultFailed
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", result, cardResourceStub(id), nil, appErr)
|
||||
return appErr
|
||||
}
|
||||
|
||||
beforeData := map[string]any{
|
||||
"asset_status": card.AssetStatus,
|
||||
"iccid": card.ICCID,
|
||||
"virtual_no": card.VirtualNo,
|
||||
}
|
||||
|
||||
beforeData := map[string]any{"asset_status": card.AssetStatus}
|
||||
if !canDeactivateAsset(card.AssetStatus) {
|
||||
appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
OperationDesc: "统一入口停用IoT卡被拒绝",
|
||||
ResultStatus: constants.AssetAuditResultDenied,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
})
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡被拒绝", constants.AuditResultDenied, card, beforeData, appErr)
|
||||
return appErr
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
OperationDesc: "统一入口停用IoT卡失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
AfterData: map[string]any{
|
||||
"asset_status": constants.AssetStatusDeactivated,
|
||||
"iccid": card.ICCID,
|
||||
"virtual_no": card.VirtualNo,
|
||||
},
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
OperationDesc: "统一入口停用IoT卡失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeIotCard,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
OperationDesc: "统一入口停用IoT卡",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BeforeData: beforeData,
|
||||
AfterData: map[string]any{
|
||||
"asset_status": constants.AssetStatusDeactivated,
|
||||
"iccid": card.ICCID,
|
||||
"virtual_no": card.VirtualNo,
|
||||
},
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.IotCard{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
return s.appendCardDeactivationAudit(ctx, tx, card, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil)
|
||||
})
|
||||
|
||||
return nil
|
||||
if err != nil {
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", constants.AuditResultFailed, card, beforeData, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeactivateDevice 手动停用设备
|
||||
// DeactivateDevice 手动停用设备。
|
||||
func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error {
|
||||
device, err := s.deviceStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: id,
|
||||
OperationDesc: "统一入口停用设备失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: id,
|
||||
OperationDesc: "统一入口停用设备失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
})
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
appErr = errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
result := constants.AuditResultFailed
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", result, deviceResourceStub(id), nil, appErr)
|
||||
return appErr
|
||||
}
|
||||
|
||||
beforeData := map[string]any{
|
||||
"asset_status": device.AssetStatus,
|
||||
"virtual_no": device.VirtualNo,
|
||||
}
|
||||
|
||||
beforeData := map[string]any{"asset_status": device.AssetStatus}
|
||||
if !canDeactivateAsset(device.AssetStatus) {
|
||||
appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口停用设备被拒绝",
|
||||
ResultStatus: constants.AssetAuditResultDenied,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
})
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备被拒绝", constants.AuditResultDenied, device, beforeData, appErr)
|
||||
return appErr
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.Device{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口停用设备失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
AfterData: map[string]any{
|
||||
"asset_status": constants.AssetStatusDeactivated,
|
||||
"virtual_no": device.VirtualNo,
|
||||
},
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口停用设备失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BeforeData: beforeData,
|
||||
})
|
||||
return appErr
|
||||
}
|
||||
|
||||
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
AssetType: constants.AssetTypeDevice,
|
||||
AssetID: device.ID,
|
||||
AssetIdentifier: device.VirtualNo,
|
||||
OperationDesc: "统一入口停用设备",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BeforeData: beforeData,
|
||||
AfterData: map[string]any{
|
||||
"asset_status": constants.AssetStatusDeactivated,
|
||||
"virtual_no": device.VirtualNo,
|
||||
},
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Device{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
return s.appendDeviceDeactivationAudit(ctx, tx, device, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil)
|
||||
})
|
||||
|
||||
return nil
|
||||
if err != nil {
|
||||
s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", constants.AuditResultFailed, device, beforeData, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *LifecycleService) appendCardDeactivationAudit(ctx context.Context, tx *gorm.DB, card *model.IotCard, beforeData, afterData map[string]any, result string, businessErr error) error {
|
||||
if s.auditWriter == nil || card == nil || card.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡资产统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(card.ID), 10)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{
|
||||
ActionCode: constants.AuditActionIotCardDeactivated, Summary: "停用 IoT 卡资产", Result: result,
|
||||
ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform,
|
||||
Resources: []infraAudit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &id, Key: infraAudit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: infraAudit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡资产已停用",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LifecycleService) appendDeviceDeactivationAudit(ctx context.Context, tx *gorm.DB, device *model.Device, beforeData, afterData map[string]any, result string, businessErr error) error {
|
||||
if s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备资产统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
id := strconv.FormatUint(uint64(device.ID), 10)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{
|
||||
ActionCode: constants.AuditActionDeviceDeactivated, Summary: "停用设备资产", Result: result,
|
||||
ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform,
|
||||
Resources: []infraAudit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &id, Key: infraAudit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: infraAudit.DeviceIdentitySnapshot(device), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备资产已停用",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LifecycleService) recordLifecycleFailure(ctx context.Context, actionCode, summary, result string, resource any, beforeData map[string]any, businessErr error) {
|
||||
if s == nil || s.db == nil || s.auditWriter == nil {
|
||||
return
|
||||
}
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
switch value := resource.(type) {
|
||||
case *model.IotCard:
|
||||
return s.appendCardDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr)
|
||||
case *model.Device:
|
||||
return s.appendDeviceDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr)
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidStatus, "资产审计资源类型无效")
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
auditfailure.RecordSecondaryWriteFailure(actionCode, summary, linkage.RequestID, linkage.CorrelationID, errorCode, err)
|
||||
}
|
||||
|
||||
func cardResourceStub(id uint) *model.IotCard { return &model.IotCard{Model: gorm.Model{ID: id}} }
|
||||
func deviceResourceStub(id uint) *model.Device { return &model.Device{Model: gorm.Model{ID: id}} }
|
||||
|
||||
func canDeactivateAsset(assetStatus int) bool {
|
||||
return assetStatus == constants.AssetStatusInStock || assetStatus == constants.AssetStatusSold
|
||||
}
|
||||
|
||||
94
internal/service/asset/manual_adjustment_audit.go
Normal file
94
internal/service/asset/manual_adjustment_audit.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *Service) appendPackageAdjustmentAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
usage *model.PackageUsage,
|
||||
assetType string,
|
||||
assetID uint,
|
||||
assetIdentifier string,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || usage == nil || usage.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "资产套餐统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
usageID := strconv.FormatUint(uint64(usage.ID), 10)
|
||||
assetResourceID := strconv.FormatUint(uint64(assetID), 10)
|
||||
var resourceType string
|
||||
switch assetType {
|
||||
case constants.AssetTypeIotCard:
|
||||
resourceType = constants.AuditResourceIotCard
|
||||
case constants.AssetTypeDevice:
|
||||
resourceType = constants.AuditResourceDevice
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "资产类型无效")
|
||||
}
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{
|
||||
ActionCode: actionCode,
|
||||
Summary: summary,
|
||||
Result: result,
|
||||
ErrorCode: errorCode,
|
||||
ErrorSummary: errorSummary,
|
||||
ScopeType: constants.AuditScopePlatform,
|
||||
Resources: []infraAudit.ResourceInput{
|
||||
{
|
||||
Type: resourceType, ID: &assetResourceID, Key: assetIdentifier, DisplayName: assetIdentifier,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePackageUsageAsset,
|
||||
IdentitySnapshot: map[string]any{"id": assetID, "asset_type": assetType, "identifier": assetIdentifier},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
},
|
||||
{
|
||||
Type: constants.AuditResourcePackageUsage, ID: &usageID,
|
||||
Key: "package_usage:" + usageID, DisplayName: "套餐权益#" + usageID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRolePackageUsageTarget,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": usage.ID, "order_id": usage.OrderID, "package_id": usage.PackageID,
|
||||
"iot_card_id": usage.IotCardID, "device_id": usage.DeviceID, "status": usage.Status,
|
||||
},
|
||||
BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordPackageAdjustmentFailure(
|
||||
ctx context.Context,
|
||||
actionCode, summary string,
|
||||
usage *model.PackageUsage,
|
||||
assetType string,
|
||||
assetID uint,
|
||||
assetIdentifier string,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
if s == nil || s.db == nil || usage == nil {
|
||||
return
|
||||
}
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendPackageAdjustmentAudit(ctx, tx, actionCode, summary, constants.AuditResultFailed, usage, assetType, assetID, assetIdentifier, beforeData, afterData, businessErr)
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
auditfailure.RecordSecondaryWriteFailure(actionCode, strconv.FormatUint(uint64(usage.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err)
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
@@ -52,7 +52,7 @@ type Service struct {
|
||||
iotCardService IotCardRefresher
|
||||
gatewayClient *gateway.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService assetAuditSvc.OperationLogger
|
||||
auditWriter *infraAudit.Writer
|
||||
packageExpiryQuery PackageExpiryResolver
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ func New(
|
||||
orderStore *postgres.OrderStore,
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
exchangeOrderStore *postgres.ExchangeOrderStore,
|
||||
assetAuditService assetAuditSvc.OperationLogger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -96,11 +95,15 @@ func New(
|
||||
iotCardService: iotCardService,
|
||||
gatewayClient: gatewayClient,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
assetAuditService: assetAuditService,
|
||||
packageExpiryQuery: packageexpiry.NewQuery(db),
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessAudit 注入资产人工调整的统一审计 Writer。
|
||||
func (s *Service) SetAccessAudit(writer *infraAudit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
// Resolve 通过任意标识符解析资产
|
||||
// 主路径:查注册表(精确匹配 ICCID 或 VirtualNo)
|
||||
// Fallback:原有跨表 OR 查询(处理 IMEI/SN/MSISDN 等非注册标识符)
|
||||
@@ -912,35 +915,26 @@ func (s *Service) UpdatePackageExpiresAt(ctx context.Context, assetType string,
|
||||
}
|
||||
|
||||
beforeData := packageUsageExpiresAtAuditData(before)
|
||||
rows, err := s.packageUsageStore.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt)
|
||||
var updated *model.PackageUsage
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
store := postgres.NewPackageUsageStore(tx, nil)
|
||||
rows, updateErr := store.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt)
|
||||
if updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "修改套餐过期时间失败")
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
updated, updateErr = store.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "查询套餐使用记录失败")
|
||||
}
|
||||
return s.appendPackageAdjustmentAudit(ctx, tx, constants.AuditActionPackageUsageExpiresAtUpdated, "修改资产套餐过期时间", constants.AuditResultSuccess, updated, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil)
|
||||
})
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐过期时间失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
s.recordPackageAdjustmentFailure(ctx, constants.AuditActionPackageUsageExpiresAtUpdated, "修改资产套餐过期时间失败", before, assetType, assetID, assetIdentifier, beforeData, map[string]any{"package_usage_id": packageUsageID, "expires_at": expiresAt}, err)
|
||||
return nil, err
|
||||
}
|
||||
if rows == 0 {
|
||||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil)
|
||||
return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
|
||||
}
|
||||
|
||||
@@ -962,35 +956,26 @@ func (s *Service) UpdatePackageUsage(ctx context.Context, assetType string, asse
|
||||
|
||||
beforeData := packageUsageTrafficAuditData(before)
|
||||
nextStatus := statusForManualDataUsage(before, dataUsageMB)
|
||||
rows, err := s.packageUsageStore.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus)
|
||||
var updated *model.PackageUsage
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
store := postgres.NewPackageUsageStore(tx, nil)
|
||||
rows, updateErr := store.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus)
|
||||
if updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "修改套餐已用量失败")
|
||||
}
|
||||
if rows == 0 {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
updated, updateErr = store.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "查询套餐使用记录失败")
|
||||
}
|
||||
return s.appendPackageAdjustmentAudit(ctx, tx, constants.AuditActionPackageUsageTrafficAdjusted, "修改资产套餐已用量", constants.AuditResultSuccess, updated, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil)
|
||||
})
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐已用量失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
s.recordPackageAdjustmentFailure(ctx, constants.AuditActionPackageUsageTrafficAdjusted, "修改资产套餐已用量失败", before, assetType, assetID, assetIdentifier, beforeData, map[string]any{"package_usage_id": packageUsageID, "data_usage_mb": dataUsageMB}, err)
|
||||
return nil, err
|
||||
}
|
||||
if rows == 0 {
|
||||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil)
|
||||
return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
|
||||
}
|
||||
|
||||
@@ -1210,31 +1195,6 @@ func packageUsageTrafficAuditData(usage *model.PackageUsage) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) logPackageAdjustmentAudit(ctx context.Context, operationType, operationDesc, resultStatus, assetType string, assetID uint, assetIdentifier string, beforeData, afterData map[string]any, err error) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
wrappedBefore, wrappedAfter := assetAuditSvc.WrapOperationContent(beforeData, afterData, map[string]any{
|
||||
"asset_type": assetAuditSvc.NormalizeAssetType(assetType),
|
||||
"asset_id": assetID,
|
||||
"asset_identifier": assetIdentifier,
|
||||
})
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, assetAuditSvc.BuildLogParams{
|
||||
Operator: assetAuditSvc.OperatorFromContext(ctx),
|
||||
AssetType: assetType,
|
||||
AssetID: assetID,
|
||||
AssetIdentifier: assetIdentifier,
|
||||
OperationType: operationType,
|
||||
OperationDesc: operationDesc,
|
||||
BeforeData: wrappedBefore,
|
||||
AfterData: wrappedAfter,
|
||||
ResultStatus: resultStatus,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
}))
|
||||
}
|
||||
|
||||
// tracePreviousGenerations 通过换货链逆向追溯前代资产的订单,最多追溯10代
|
||||
func (s *Service) tracePreviousGenerations(ctx context.Context, assetType string, assetID uint) ([]*dto.PreviousGenerationOrders, bool) {
|
||||
const maxDepth = 10
|
||||
|
||||
@@ -9,14 +9,11 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AssetOperationLogStore 资产操作日志存储接口。
|
||||
type AssetOperationLogStore interface {
|
||||
Create(ctx context.Context, log *model.AssetOperationLog) error
|
||||
ListByAssetPaged(
|
||||
ctx context.Context,
|
||||
assetType string,
|
||||
@@ -28,11 +25,6 @@ type AssetOperationLogStore interface {
|
||||
) ([]*model.AssetOperationLog, int64, error)
|
||||
}
|
||||
|
||||
// OperationLogger 资产审计记录接口。
|
||||
type OperationLogger interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
// Service 资产审计服务。
|
||||
type Service struct {
|
||||
store AssetOperationLogStore
|
||||
@@ -57,24 +49,6 @@ func NewService(store AssetOperationLogStore, db *gorm.DB) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
// LogOperation 记录资产操作日志(异步写入,不阻塞主流程)。
|
||||
func (s *Service) LogOperation(ctx context.Context, log *model.AssetOperationLog) {
|
||||
if s == nil || s.store == nil || log == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := s.store.Create(context.Background(), log); err != nil {
|
||||
logger.GetAppLogger().Error("写入资产操作日志失败",
|
||||
zap.String("asset_type", log.AssetType),
|
||||
zap.Uint("asset_id", log.AssetID),
|
||||
zap.String("operation_type", log.OperationType),
|
||||
zap.String("result_status", log.ResultStatus),
|
||||
zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ListByAsset 按资产分页查询操作日志。
|
||||
func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*dto.AssetOperationLogListResponse, error) {
|
||||
if s == nil || s.store == nil {
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// AssetAuditService 资产审计服务接口。
|
||||
type AssetAuditService interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
func (s *Service) logDeviceAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
if p.Operator.Type == "" {
|
||||
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
|
||||
}
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeDevice
|
||||
}
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
func (s *Service) logDeviceOperation(
|
||||
ctx context.Context,
|
||||
operationType string,
|
||||
operationDesc string,
|
||||
resultStatus string,
|
||||
device *model.Device,
|
||||
beforeData map[string]any,
|
||||
afterData map[string]any,
|
||||
batchTotal int,
|
||||
successCount int,
|
||||
failCount int,
|
||||
err error,
|
||||
) {
|
||||
if strings.TrimSpace(operationDesc) == "" {
|
||||
operationDesc = operationType
|
||||
}
|
||||
if strings.TrimSpace(resultStatus) == "" {
|
||||
if err != nil {
|
||||
resultStatus = constants.AssetAuditResultFailed
|
||||
} else {
|
||||
resultStatus = constants.AssetAuditResultSuccess
|
||||
}
|
||||
}
|
||||
|
||||
params := assetAuditSvc.BuildLogParams{
|
||||
OperationType: operationType,
|
||||
OperationDesc: operationDesc,
|
||||
ResultStatus: resultStatus,
|
||||
BatchTotal: batchTotal,
|
||||
SuccessCount: successCount,
|
||||
FailCount: failCount,
|
||||
}
|
||||
snapshot := deviceSnapshot(device)
|
||||
beforeContent := stripDeviceOperationMeta(beforeData)
|
||||
afterContent := stripDeviceOperationMeta(afterData)
|
||||
beforeContent = ensureDeviceOperationContentMap(beforeContent, beforeData)
|
||||
afterContent = ensureDeviceOperationContentMap(afterContent, afterData)
|
||||
enrichDeviceOperationContent(beforeContent, beforeData)
|
||||
enrichDeviceOperationContent(afterContent, afterData)
|
||||
params.BeforeData, params.AfterData = assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot)
|
||||
|
||||
if device != nil {
|
||||
params.AssetID = device.ID
|
||||
params.AssetIdentifier = device.VirtualNo
|
||||
}
|
||||
if err != nil {
|
||||
params.ErrorCode, params.ErrorMsg = assetAuditSvc.BuildErrorInfo(err)
|
||||
}
|
||||
|
||||
s.logDeviceAudit(ctx, params)
|
||||
}
|
||||
|
||||
func stripDeviceOperationMeta(data map[string]any) map[string]any {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(data))
|
||||
for k, v := range data {
|
||||
if k == "device" || k == "devices" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureDeviceOperationContentMap(content map[string]any, raw map[string]any) map[string]any {
|
||||
if content != nil || len(raw) == 0 {
|
||||
return content
|
||||
}
|
||||
if _, ok := raw["device"].(map[string]any); ok {
|
||||
return make(map[string]any)
|
||||
}
|
||||
if _, ok := raw["card"].(map[string]any); ok {
|
||||
return make(map[string]any)
|
||||
}
|
||||
switch raw["devices"].(type) {
|
||||
case []map[string]any, []any:
|
||||
return make(map[string]any)
|
||||
}
|
||||
switch raw["cards"].(type) {
|
||||
case []map[string]any, []any:
|
||||
return make(map[string]any)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func enrichDeviceOperationContent(content map[string]any, raw map[string]any) {
|
||||
if len(raw) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if deviceRaw, ok := raw["device"].(map[string]any); ok {
|
||||
mergeReadableDeviceFields(content, deviceRaw)
|
||||
}
|
||||
if cardRaw, ok := raw["card"].(map[string]any); ok {
|
||||
mergeReadableCardFields(content, cardRaw)
|
||||
}
|
||||
|
||||
switch devicesRaw := raw["devices"].(type) {
|
||||
case []map[string]any:
|
||||
deviceIDs, virtualNos := collectDeviceReadableLists(devicesRaw)
|
||||
if len(deviceIDs) > 0 && content["device_ids"] == nil {
|
||||
content["device_ids"] = deviceIDs
|
||||
}
|
||||
if len(virtualNos) > 0 && content["device_virtual_nos"] == nil {
|
||||
content["device_virtual_nos"] = virtualNos
|
||||
}
|
||||
case []any:
|
||||
devices := make([]map[string]any, 0, len(devicesRaw))
|
||||
for _, item := range devicesRaw {
|
||||
deviceMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
devices = append(devices, deviceMap)
|
||||
}
|
||||
deviceIDs, virtualNos := collectDeviceReadableLists(devices)
|
||||
if len(deviceIDs) > 0 && content["device_ids"] == nil {
|
||||
content["device_ids"] = deviceIDs
|
||||
}
|
||||
if len(virtualNos) > 0 && content["device_virtual_nos"] == nil {
|
||||
content["device_virtual_nos"] = virtualNos
|
||||
}
|
||||
}
|
||||
|
||||
switch cardsRaw := raw["cards"].(type) {
|
||||
case []map[string]any:
|
||||
cardIDs, iccids := collectDeviceAuditCardReadableLists(cardsRaw)
|
||||
if len(cardIDs) > 0 && content["card_ids"] == nil {
|
||||
content["card_ids"] = cardIDs
|
||||
}
|
||||
if len(iccids) > 0 && content["iccids"] == nil {
|
||||
content["iccids"] = iccids
|
||||
}
|
||||
case []any:
|
||||
cards := make([]map[string]any, 0, len(cardsRaw))
|
||||
for _, item := range cardsRaw {
|
||||
cardMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cards = append(cards, cardMap)
|
||||
}
|
||||
cardIDs, iccids := collectDeviceAuditCardReadableLists(cards)
|
||||
if len(cardIDs) > 0 && content["card_ids"] == nil {
|
||||
content["card_ids"] = cardIDs
|
||||
}
|
||||
if len(iccids) > 0 && content["iccids"] == nil {
|
||||
content["iccids"] = iccids
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeReadableDeviceFields(content map[string]any, device map[string]any) {
|
||||
if len(device) == 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := content["device_id"]; !ok {
|
||||
if v, exists := device["id"]; exists {
|
||||
content["device_id"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_virtual_no"]; !ok {
|
||||
if v := stringifyDeviceAuditValue(device["virtual_no"]); v != "" {
|
||||
content["device_virtual_no"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_imei"]; !ok {
|
||||
if v := stringifyDeviceAuditValue(device["imei"]); v != "" {
|
||||
content["device_imei"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_sn"]; !ok {
|
||||
if v := stringifyDeviceAuditValue(device["sn"]); v != "" {
|
||||
content["device_sn"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeReadableCardFields(content map[string]any, card map[string]any) {
|
||||
if len(card) == 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := content["iot_card_id"]; !ok {
|
||||
if v, exists := card["id"]; exists {
|
||||
content["iot_card_id"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["iccid"]; !ok {
|
||||
if v := stringifyDeviceAuditValue(card["iccid"]); v != "" {
|
||||
content["iccid"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectDeviceReadableLists(devices []map[string]any) ([]any, []string) {
|
||||
deviceIDs := make([]any, 0, len(devices))
|
||||
virtualNos := make([]string, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if id, ok := device["id"]; ok {
|
||||
deviceIDs = append(deviceIDs, id)
|
||||
}
|
||||
if virtualNo := stringifyDeviceAuditValue(device["virtual_no"]); virtualNo != "" {
|
||||
virtualNos = append(virtualNos, virtualNo)
|
||||
}
|
||||
}
|
||||
return deviceIDs, virtualNos
|
||||
}
|
||||
|
||||
func collectDeviceAuditCardReadableLists(cards []map[string]any) ([]any, []string) {
|
||||
cardIDs := make([]any, 0, len(cards))
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if id, ok := card["id"]; ok {
|
||||
cardIDs = append(cardIDs, id)
|
||||
}
|
||||
if iccid := stringifyDeviceAuditValue(card["iccid"]); iccid != "" {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
}
|
||||
return cardIDs, iccids
|
||||
}
|
||||
|
||||
func stringifyDeviceAuditValue(v any) string {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
return vv
|
||||
case fmt.Stringer:
|
||||
return vv.String()
|
||||
default:
|
||||
if vv == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(vv)
|
||||
}
|
||||
}
|
||||
|
||||
func deviceSnapshot(device *model.Device) map[string]any {
|
||||
if device == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": device.ID,
|
||||
"virtual_no": device.VirtualNo,
|
||||
"imei": device.IMEI,
|
||||
"sn": device.SN,
|
||||
"shop_id": device.ShopID,
|
||||
"status": device.Status,
|
||||
"series_id": device.SeriesID,
|
||||
"enable_polling": device.EnablePolling,
|
||||
"realname_policy": device.RealnamePolicy,
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ type Service struct {
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
gatewayClient *gateway.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService AssetAuditService
|
||||
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
packageExpiryQuery *packageexpiry.Query
|
||||
@@ -152,7 +151,6 @@ func New(
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
gatewayClient *gateway.Client,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
assetAuditService AssetAuditService,
|
||||
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore,
|
||||
enterpriseStore *postgres.EnterpriseStore,
|
||||
) *Service {
|
||||
@@ -169,7 +167,6 @@ func New(
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
gatewayClient: gatewayClient,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
assetAuditService: assetAuditService,
|
||||
enterpriseDeviceAuthStore: enterpriseDeviceAuthStore,
|
||||
enterpriseStore: enterpriseStore,
|
||||
packageExpiryQuery: packageexpiry.NewQuery(db),
|
||||
|
||||
@@ -15,11 +15,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// AssetAuditService 资产审计服务接口。
|
||||
type AssetAuditService interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
func (s *Service) writeDeviceImportTaskAudit(ctx context.Context, tx *gorm.DB, task *model.DeviceImportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error {
|
||||
scopeType, scopeID := constants.AuditScopePlatform, ""
|
||||
if task.OperatorShopID != nil {
|
||||
|
||||
@@ -23,7 +23,6 @@ type Service struct {
|
||||
db *gorm.DB
|
||||
importTaskStore *postgres.DeviceImportTaskStore
|
||||
queueClient *queue.Client
|
||||
assetAudit AssetAuditService
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
@@ -35,14 +34,12 @@ func New(
|
||||
db *gorm.DB,
|
||||
importTaskStore *postgres.DeviceImportTaskStore,
|
||||
queueClient *queue.Client,
|
||||
assetAudit AssetAuditService,
|
||||
auditWriters ...*audit.Writer,
|
||||
) *Service {
|
||||
service := &Service{
|
||||
db: db,
|
||||
importTaskStore: importTaskStore,
|
||||
queueClient: queueClient,
|
||||
assetAudit: assetAudit,
|
||||
}
|
||||
if len(auditWriters) > 0 {
|
||||
service.auditWriter = auditWriters[0]
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// AssetAuditService 资产审计服务接口。
|
||||
type AssetAuditService interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
func (s *Service) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
if p.Operator.Type == "" {
|
||||
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
|
||||
}
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeIotCard
|
||||
}
|
||||
p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData)
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
func (s *StopResumeService) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
if p.Operator.Type == "" {
|
||||
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
|
||||
}
|
||||
if p.AssetType == "" {
|
||||
p.AssetType = constants.AssetTypeIotCard
|
||||
}
|
||||
p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData)
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
|
||||
}
|
||||
|
||||
func normalizeCardAuditPayload(beforeData, afterData map[string]any) (map[string]any, map[string]any) {
|
||||
snapshot := map[string]any(nil)
|
||||
if raw, ok := beforeData["card"]; ok {
|
||||
if m, ok := raw.(map[string]any); ok {
|
||||
snapshot = m
|
||||
}
|
||||
}
|
||||
if snapshot == nil {
|
||||
if raw, ok := afterData["card"]; ok {
|
||||
if m, ok := raw.(map[string]any); ok {
|
||||
snapshot = m
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeContent := stripCardOperationMeta(beforeData)
|
||||
afterContent := stripCardOperationMeta(afterData)
|
||||
beforeContent = ensureCardOperationContentMap(beforeContent, beforeData)
|
||||
afterContent = ensureCardOperationContentMap(afterContent, afterData)
|
||||
enrichCardOperationContent(beforeContent, beforeData)
|
||||
enrichCardOperationContent(afterContent, afterData)
|
||||
return assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot)
|
||||
}
|
||||
|
||||
func stripCardOperationMeta(data map[string]any) map[string]any {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(data))
|
||||
for k, v := range data {
|
||||
if k == "device" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ensureCardOperationContentMap(content map[string]any, raw map[string]any) map[string]any {
|
||||
if content != nil || len(raw) == 0 {
|
||||
return content
|
||||
}
|
||||
if _, ok := raw["card"].(map[string]any); ok {
|
||||
return make(map[string]any)
|
||||
}
|
||||
if _, ok := raw["device"].(map[string]any); ok {
|
||||
return make(map[string]any)
|
||||
}
|
||||
switch raw["cards"].(type) {
|
||||
case []map[string]any, []any:
|
||||
return make(map[string]any)
|
||||
default:
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
func enrichCardOperationContent(content map[string]any, raw map[string]any) {
|
||||
if len(raw) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if cardRaw, ok := raw["card"].(map[string]any); ok {
|
||||
mergeReadableCardFields(content, cardRaw)
|
||||
}
|
||||
if deviceRaw, ok := raw["device"].(map[string]any); ok {
|
||||
mergeReadableDeviceFields(content, deviceRaw)
|
||||
}
|
||||
|
||||
switch cardsRaw := raw["cards"].(type) {
|
||||
case []map[string]any:
|
||||
cardIDs, iccids := collectCardReadableLists(cardsRaw)
|
||||
if len(cardIDs) > 0 && content["card_ids"] == nil {
|
||||
content["card_ids"] = cardIDs
|
||||
}
|
||||
if len(iccids) > 0 && content["iccids"] == nil {
|
||||
content["iccids"] = iccids
|
||||
}
|
||||
case []any:
|
||||
cards := make([]map[string]any, 0, len(cardsRaw))
|
||||
for _, item := range cardsRaw {
|
||||
cardMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cards = append(cards, cardMap)
|
||||
}
|
||||
cardIDs, iccids := collectCardReadableLists(cards)
|
||||
if len(cardIDs) > 0 && content["card_ids"] == nil {
|
||||
content["card_ids"] = cardIDs
|
||||
}
|
||||
if len(iccids) > 0 && content["iccids"] == nil {
|
||||
content["iccids"] = iccids
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeReadableCardFields(content map[string]any, card map[string]any) {
|
||||
if len(card) == 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := content["card_id"]; !ok {
|
||||
if v, exists := card["id"]; exists {
|
||||
content["card_id"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["iccid"]; !ok {
|
||||
if v := stringifyCardAuditValue(card["iccid"]); v != "" {
|
||||
content["iccid"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_virtual_no"]; !ok {
|
||||
if v := stringifyCardAuditValue(card["device_virtual_no"]); v != "" {
|
||||
content["device_virtual_no"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeReadableDeviceFields(content map[string]any, device map[string]any) {
|
||||
if len(device) == 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := content["device_id"]; !ok {
|
||||
if v, exists := device["id"]; exists {
|
||||
content["device_id"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_virtual_no"]; !ok {
|
||||
if v := stringifyCardAuditValue(device["virtual_no"]); v != "" {
|
||||
content["device_virtual_no"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_imei"]; !ok {
|
||||
if v := stringifyCardAuditValue(device["imei"]); v != "" {
|
||||
content["device_imei"] = v
|
||||
}
|
||||
}
|
||||
if _, ok := content["device_sn"]; !ok {
|
||||
if v := stringifyCardAuditValue(device["sn"]); v != "" {
|
||||
content["device_sn"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectCardReadableLists(cards []map[string]any) ([]any, []string) {
|
||||
cardIDs := make([]any, 0, len(cards))
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if id, ok := card["id"]; ok {
|
||||
cardIDs = append(cardIDs, id)
|
||||
}
|
||||
if iccid := stringifyCardAuditValue(card["iccid"]); iccid != "" {
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
}
|
||||
return cardIDs, iccids
|
||||
}
|
||||
|
||||
func stringifyCardAuditValue(v any) string {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
return vv
|
||||
case fmt.Stringer:
|
||||
return vv.String()
|
||||
default:
|
||||
if vv == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(vv)
|
||||
}
|
||||
}
|
||||
|
||||
func cardSnapshot(card *model.IotCard) map[string]any {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"device_virtual_no": card.DeviceVirtualNo,
|
||||
"shop_id": card.ShopID,
|
||||
"status": card.Status,
|
||||
"series_id": card.SeriesID,
|
||||
"enable_polling": card.EnablePolling,
|
||||
"realname_policy": card.RealnamePolicy,
|
||||
"real_name_status": card.RealNameStatus,
|
||||
"network_status": card.NetworkStatus,
|
||||
"stop_reason": card.StopReason,
|
||||
}
|
||||
}
|
||||
16
internal/service/iot_card/card_snapshot.go
Normal file
16
internal/service/iot_card/card_snapshot.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package iot_card
|
||||
|
||||
import "github.com/break/junhong_cmp_fiber/internal/model"
|
||||
|
||||
func cardSnapshot(card *model.IotCard) map[string]any {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": card.ID, "iccid": card.ICCID, "device_virtual_no": card.DeviceVirtualNo,
|
||||
"shop_id": card.ShopID, "status": card.Status, "series_id": card.SeriesID,
|
||||
"enable_polling": card.EnablePolling, "realname_policy": card.RealnamePolicy,
|
||||
"real_name_status": card.RealNameStatus, "network_status": card.NetworkStatus,
|
||||
"stop_reason": card.StopReason,
|
||||
}
|
||||
}
|
||||
85
internal/service/iot_card/polling_audit.go
Normal file
85
internal/service/iot_card/polling_audit.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (s *Service) appendPollingStatusAudit(ctx context.Context, tx *gorm.DB, card *model.IotCard, before, after bool) error {
|
||||
return s.appendCardLifecycleAudit(ctx, tx, constants.AuditActionIotCardPollingStatusUpdated, "更新 IoT 卡轮询开关", constants.AuditResultSuccess, card,
|
||||
map[string]any{"enable_polling": before}, map[string]any{"enable_polling": after}, nil)
|
||||
}
|
||||
|
||||
func (s *Service) appendBatchPollingStatusAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, enablePolling bool) error {
|
||||
if s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置")
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
batchKey := linkage.RequestID
|
||||
if batchKey == "" {
|
||||
batchKey = uuid.NewString()
|
||||
}
|
||||
rootEventID := "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("iot-card-polling-status:"+batchKey)).String()
|
||||
children := make([]infraAudit.AppendInput, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
continue
|
||||
}
|
||||
id := strconv.FormatUint(uint64(card.ID), 10)
|
||||
children = append(children, infraAudit.AppendInput{
|
||||
EventID: "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte(rootEventID+":"+id)).String(),
|
||||
ActionCode: constants.AuditActionIotCardPollingStatusUpdated,
|
||||
Summary: "批量更新 IoT 卡轮询开关",
|
||||
Result: constants.AuditResultSuccess,
|
||||
Resources: []infraAudit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &id, Key: infraAudit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: infraAudit.IotCardIdentitySnapshot(card),
|
||||
BeforeData: map[string]any{"enable_polling": card.EnablePolling}, AfterData: map[string]any{"enable_polling": enablePolling},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡轮询开关已更新",
|
||||
}},
|
||||
})
|
||||
}
|
||||
return s.auditWriter.AppendBatch(ctx, tx, infraAudit.BatchInput{
|
||||
Root: infraAudit.AppendInput{
|
||||
EventID: rootEventID,
|
||||
ActionCode: constants.AuditActionIotCardPollingStatusBatchUpdated,
|
||||
Summary: "批量更新 IoT 卡轮询开关", Result: constants.AuditResultSuccess,
|
||||
BatchTotal: len(children), SuccessCount: len(children),
|
||||
Resources: []infraAudit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCardBatch, Key: batchKey, DisplayName: batchKey,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
||||
IdentitySnapshot: map[string]any{"card_count": len(children), "enable_polling": enablePolling},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
},
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordPollingStatusFailure(ctx context.Context, actionCode, result string, card *model.IotCard, enablePolling bool, businessErr error) {
|
||||
if card == nil || card.ID == 0 || s.db == nil || s.auditWriter == nil {
|
||||
return
|
||||
}
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendCardLifecycleAudit(ctx, tx, actionCode, "更新 IoT 卡轮询开关失败", result, card, nil,
|
||||
map[string]any{"enable_polling": enablePolling}, businessErr)
|
||||
})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
auditfailure.RecordSecondaryWriteFailure(actionCode, strconv.FormatUint(uint64(card.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err)
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
@@ -68,7 +67,6 @@ type Service struct {
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
redis *redis.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService AssetAuditService
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
packageExpiryQuery *packageexpiry.Query
|
||||
@@ -109,7 +107,6 @@ func New(
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
assetAuditService AssetAuditService,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -121,7 +118,6 @@ func New(
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
assetAuditService: assetAuditService,
|
||||
packageExpiryQuery: packageexpiry.NewQuery(db),
|
||||
}
|
||||
}
|
||||
@@ -1696,77 +1692,33 @@ func parseGatewayRealnameStatus(realStatus bool) int {
|
||||
func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error {
|
||||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询 IoT 卡失败")
|
||||
result := constants.AuditResultFailed
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "更新卡轮询状态被拒绝",
|
||||
ResultStatus: constants.AssetAuditResultDenied,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
AssetID: cardID,
|
||||
AssetIdentifier: "",
|
||||
AfterData: map[string]any{
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
return denyErr
|
||||
appErr = errors.New(errors.CodeNotFound, "IoT卡不存在")
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "更新卡轮询状态执行失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
AssetID: cardID,
|
||||
AfterData: map[string]any{
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
s.recordPollingStatusFailure(ctx, constants.AuditActionIotCardPollingStatusUpdated, result, &model.IotCard{Model: gorm.Model{ID: cardID}}, enablePolling, appErr)
|
||||
return appErr
|
||||
}
|
||||
beforePolling := card.EnablePolling
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if beforePolling != enablePolling {
|
||||
result := tx.Model(&model.IotCard{}).Where("id = ? AND enable_polling = ?", card.ID, beforePolling).Update("enable_polling", enablePolling)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新 IoT 卡轮询状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "轮询状态已变更,请刷新后重试")
|
||||
}
|
||||
}
|
||||
return s.appendPollingStatusAudit(ctx, tx, card, beforePolling, enablePolling)
|
||||
})
|
||||
if err != nil {
|
||||
s.recordPollingStatusFailure(ctx, constants.AuditActionIotCardPollingStatusUpdated, constants.AuditResultFailed, card, enablePolling, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否需要更新
|
||||
if card.EnablePolling == enablePolling {
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "更新卡轮询状态",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
BeforeData: map[string]any{
|
||||
"enable_polling": card.EnablePolling,
|
||||
},
|
||||
AfterData: map[string]any{
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
return nil // 状态未变化
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
card.EnablePolling = enablePolling
|
||||
if err := s.iotCardStore.Update(ctx, card); err != nil {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "更新卡轮询状态执行失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
BeforeData: map[string]any{
|
||||
"enable_polling": !enablePolling,
|
||||
},
|
||||
AfterData: map[string]any{
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("更新卡轮询状态",
|
||||
zap.Uint("card_id", cardID),
|
||||
@@ -1782,20 +1734,6 @@ func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePo
|
||||
}
|
||||
}
|
||||
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "更新卡轮询状态",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
AssetID: card.ID,
|
||||
AssetIdentifier: card.ICCID,
|
||||
BeforeData: map[string]any{
|
||||
"enable_polling": !enablePolling,
|
||||
},
|
||||
AfterData: map[string]any{
|
||||
"enable_polling": enablePolling,
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1805,23 +1743,17 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint,
|
||||
return nil
|
||||
}
|
||||
|
||||
// 批量更新数据库
|
||||
if err := s.iotCardStore.BatchUpdatePollingStatus(ctx, cardIDs, enablePolling); err != nil {
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "批量更新卡轮询状态执行失败",
|
||||
ResultStatus: constants.AssetAuditResultFailed,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
BatchTotal: len(cardIDs),
|
||||
FailCount: len(cardIDs),
|
||||
AfterData: map[string]any{
|
||||
"card_ids": cardIDs,
|
||||
"enable_polling": enablePolling,
|
||||
"trigger_source": "batch",
|
||||
},
|
||||
})
|
||||
cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if updateErr := tx.Model(&model.IotCard{}).Where("id IN ?", cardIDs).Update("enable_polling", enablePolling).Error; updateErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, updateErr, "批量更新 IoT 卡轮询状态失败")
|
||||
}
|
||||
return s.appendBatchPollingStatusAudit(ctx, tx, cards, enablePolling)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1841,19 +1773,6 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint,
|
||||
}
|
||||
}
|
||||
|
||||
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
|
||||
OperationType: constants.AssetAuditOpCardPollingStatus,
|
||||
OperationDesc: "批量更新卡轮询状态",
|
||||
ResultStatus: constants.AssetAuditResultSuccess,
|
||||
BatchTotal: len(cardIDs),
|
||||
SuccessCount: len(cardIDs),
|
||||
AfterData: map[string]any{
|
||||
"card_ids": cardIDs,
|
||||
"enable_polling": enablePolling,
|
||||
"trigger_source": "batch",
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ type StopResumeService struct {
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
assetAuditService AssetAuditService
|
||||
pollingCallback PollingCallback
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter
|
||||
auditWriter *audit.Writer
|
||||
@@ -75,7 +74,6 @@ func NewStopResumeService(
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
assetAuditService AssetAuditService,
|
||||
) *StopResumeService {
|
||||
return &StopResumeService{
|
||||
redis: redis,
|
||||
@@ -84,7 +82,6 @@ func NewStopResumeService(
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
assetAuditService: assetAuditService,
|
||||
maxRetries: 3,
|
||||
retryInterval: 2 * time.Second,
|
||||
}
|
||||
|
||||
@@ -13,11 +13,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// AssetAuditService 资产审计服务接口。
|
||||
type AssetAuditService interface {
|
||||
LogOperation(ctx context.Context, log *model.AssetOperationLog)
|
||||
}
|
||||
|
||||
func (s *Service) writeImportTaskAudit(ctx context.Context, tx *gorm.DB, task *model.IotCardImportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error {
|
||||
return s.auditWriter.WriteTask(ctx, tx, infraAudit.TaskInput{
|
||||
EventID: infraAudit.TaskEventID(constants.AuditResourceIotCardImportTask, task.ID, phase),
|
||||
|
||||
@@ -24,7 +24,6 @@ type Service struct {
|
||||
importTaskStore *postgres.IotCardImportTaskStore
|
||||
carrierStore carrierGetter
|
||||
queueClient *queue.Client
|
||||
assetAudit AssetAuditService
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
@@ -52,7 +51,6 @@ func New(
|
||||
db *gorm.DB,
|
||||
importTaskStore *postgres.IotCardImportTaskStore,
|
||||
queueClient *queue.Client,
|
||||
assetAudit AssetAuditService,
|
||||
auditWriters ...*audit.Writer,
|
||||
) *Service {
|
||||
service := &Service{
|
||||
@@ -60,7 +58,6 @@ func New(
|
||||
importTaskStore: importTaskStore,
|
||||
carrierStore: NewCarrierStore(db),
|
||||
queueClient: queueClient,
|
||||
assetAudit: assetAudit,
|
||||
}
|
||||
if len(auditWriters) > 0 {
|
||||
service.auditWriter = auditWriters[0]
|
||||
|
||||
@@ -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