Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
240 lines
8.5 KiB
Go
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)
|
|
}
|