全局审计完成
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

@@ -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,
}
}

View File

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

View File

@@ -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,

View File

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

View 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)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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]

View File

@@ -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,
}
}

View 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,
}
}

View 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)
}

View File

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

View File

@@ -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,
}

View File

@@ -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),

View File

@@ -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]

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