Files
junhong_cmp_fiber/internal/service/iot_card/stop_resume_audit.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

240 lines
8.5 KiB
Go

package iot_card
import (
"context"
"strconv"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"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/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
func (s *StopResumeService) appendCardCommandAudit(
ctx context.Context,
tx *gorm.DB,
card *model.IotCard,
actionCode, summary, result, integrationID string,
beforeData, afterData map[string]any,
businessErr error,
) error {
if s.auditWriter == nil || card == nil || card.ID == 0 {
return errors.New(errors.CodeInvalidStatus, "IoT 卡停复机统一审计接缝未配置或资源不完整")
}
resourcesByCard, err := loadCardDeviceAuditReferences(ctx, tx, []*model.IotCard{card})
if err != nil {
return err
}
cardID := strconv.FormatUint(uint64(card.ID), 10)
resources := []audit.ResourceInput{{
Type: constants.AuditResourceIotCard, ID: &cardID,
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData,
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
}}
resources = append(resources, resourcesByCard[card.ID]...)
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
input := audit.AppendInput{
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
Metadata: map[string]any{"integration_id": integrationID}, Resources: resources,
}
if actionCode == constants.AuditActionIotCardAutoStopped || actionCode == constants.AuditActionIotCardAutoStarted ||
actionCode == constants.AuditActionIotCardAutoStopReasonUpdated {
input.Actor = audit.ActorInput{Kind: constants.AuditActorSystemTask, ID: "iot-card-stop-resume", Name: "IoT 卡停复机服务"}
input.Source = constants.AuditSourceWorker
}
return s.auditWriter.Append(ctx, tx, input)
}
func (s *StopResumeService) recordCardCommandAudit(
ctx context.Context,
card *model.IotCard,
actionCode, summary, result, integrationID string,
beforeData, afterData map[string]any,
businessErr error,
) {
if s.db == nil || s.auditWriter == nil || card == nil || card.ID == 0 {
recordCardAuditSecondaryFailure(ctx, actionCode, cardID(card), businessErr,
errors.New(errors.CodeInvalidStatus, "IoT 卡停复机统一审计接缝未配置"))
return
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.appendCardCommandAudit(ctx, tx, card, actionCode, summary, result, integrationID, beforeData, afterData, businessErr)
}); err != nil {
recordCardAuditSecondaryFailure(ctx, actionCode, card.ID, businessErr, err)
}
}
func cardID(card *model.IotCard) uint {
if card == nil {
return 0
}
return card.ID
}
func stopAuditAction(ctx context.Context, stopReason string) (string, string) {
if stopReason == constants.StopReasonManual && auditcontext.From(ctx).ActorKind == constants.AuditActorAccount {
return constants.AuditActionIotCardManualStopped, "人工停用 IoT 卡网络"
}
return constants.AuditActionIotCardAutoStopped, "自动停用 IoT 卡网络"
}
func cardCommandSeriesKey(ctx context.Context) string {
linkage := auditcontext.From(ctx)
if linkage.CorrelationID != "" {
return linkage.CorrelationID
}
if linkage.RequestID != "" {
return linkage.RequestID
}
return uuid.NewString()
}
func cardCommandAuditResult(err error) string {
if err == nil {
return constants.AuditResultSuccess
}
if isGatewayTimeout(err) {
return constants.AuditResultUnknown
}
return constants.AuditResultFailed
}
func (s *StopResumeService) updateCardStopReasonWithAudit(ctx context.Context, card *model.IotCard, stopReason string) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Update("stop_reason", stopReason).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡停机原因失败")
}
return s.appendCardCommandAudit(ctx, tx, card, constants.AuditActionIotCardAutoStopReasonUpdated,
"自动更新 IoT 卡停机原因", constants.AuditResultSuccess, "",
map[string]any{"stop_reason": card.StopReason}, map[string]any{"stop_reason": stopReason}, nil)
})
}
func (s *StopResumeService) startCardCommandAttempt(
ctx context.Context,
card *model.IotCard,
operation, scene, seriesKey string,
attempt int,
) (*gatewayAttempt, error) {
if s.integration == nil {
return nil, errors.New(errors.CodeInvalidStatus, "停复机 Integration Log 接缝未配置")
}
resourceID := strconv.FormatUint(uint64(card.ID), 10)
triggerSource := auditcontext.From(ctx).Source
if triggerSource == "" {
triggerSource = constants.AuditSourceWorker
}
triggerScene := scene
triggerSeries := uuid.NewSHA1(uuid.NameSpaceOID, []byte("gateway-card-command:"+seriesKey+":"+operation)).String()
requestID := requestIDFromContext(ctx)
correlationID := auditcontext.From(ctx).CorrelationID
if correlationID == "" {
correlationID = requestID
}
var requestIDPtr, correlationIDPtr *string
if requestID != "" {
requestIDPtr = &requestID
}
if correlationID != "" {
correlationIDPtr = &correlationID
}
log, err := s.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound,
Operation: operation, ExternalID: &card.ICCID,
ResourceType: constants.AssetTypeIotCard, ResourceID: &resourceID, ResourceKey: &card.ICCID,
TriggerSource: &triggerSource, TriggerScene: &triggerScene, TriggerSeries: &triggerSeries,
Attempt: attempt, RequestID: requestIDPtr, CorrelationID: correlationIDPtr,
RequestSummary: map[string]any{"iot_card_id": card.ID, "iccid": card.ICCID},
})
if err != nil {
return nil, err
}
return &gatewayAttempt{log: log, startedAt: time.Now()}, nil
}
func (s *StopResumeService) completeCardCommandAttempt(ctx context.Context, attempt *gatewayAttempt, callErr error, stateChanged bool) error {
if attempt == nil || attempt.log == nil {
return nil
}
completion := integrationlog.Completion{
Result: constants.IntegrationResultSuccess, DurationMS: time.Since(attempt.startedAt).Milliseconds(),
StateChanged: stateChanged, ResponseSummary: map[string]any{"result": "success"},
}
if callErr != nil {
completion.Result = constants.IntegrationResultFailed
completion.SafeProviderMessage = "Gateway 停复机请求失败"
completion.ResponseSummary = map[string]any{"result": "failed"}
if isGatewayTimeout(callErr) {
completion.Result = constants.IntegrationResultUnknown
completion.SafeProviderMessage = "Gateway 停复机请求结果未知"
completion.ResponseSummary = map[string]any{"result": "unknown"}
completion.RecoveryStrategy = constants.GatewayCardCommandUnknownRecoveryStrategy
}
}
_, err := s.integration.Complete(ctx, attempt.log.IntegrationID, completion)
return err
}
type cardCommandAttemptObserver struct {
service *StopResumeService
card *model.IotCard
operation string
scene string
seriesKey string
nextAttempt int
current *gatewayAttempt
successful *gatewayAttempt
lastIntegrationID string
lastCallErr error
recordingErr error
unknown bool
}
func (o *cardCommandAttemptObserver) BeforeAttempt(ctx context.Context, _ int) error {
o.nextAttempt++
o.lastCallErr = nil
attempt, err := o.service.startCardCommandAttempt(ctx, o.card, o.operation, o.scene, o.seriesKey, o.nextAttempt)
if err != nil {
o.recordingErr = err
return err
}
o.current = attempt
o.lastIntegrationID = attempt.log.IntegrationID
return nil
}
func (o *cardCommandAttemptObserver) AfterAttempt(ctx context.Context, _ int, callErr error) error {
o.lastCallErr = callErr
if isGatewayTimeout(callErr) {
o.unknown = true
}
if callErr == nil {
o.successful = o.current
o.current = nil
return nil
}
err := o.service.completeCardCommandAttempt(ctx, o.current, callErr, false)
o.current = nil
if err != nil {
o.recordingErr = err
}
return err
}
func (o *cardCommandAttemptObserver) auditResult(lastErr error) string {
if o.unknown {
return constants.AuditResultUnknown
}
return cardCommandAuditResult(lastErr)
}