收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
239
internal/service/iot_card/stop_resume_audit.go
Normal file
239
internal/service/iot_card/stop_resume_audit.go
Normal file
@@ -0,0 +1,239 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user