package packagesilence import ( "context" "time" "github.com/bytedance/sonic" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // recordInput 是一条沉默期流水与对应审计。 type recordInput struct { Usage *model.PackageUsage // 变更后的套餐记录 EventType string Trigger string BeforeStatus *string BeforeDeadline *time.Time ChangeDays int DefaultDays *int IsAdjusted bool Reason string ApprovalID *uint BatchID *uint ExtensionID *uint ExchangeID *uint ExchangeNo string RealnameAt *time.Time Extra map[string]any AuditAction string AuditSummary string } // writeRecord 在调用方事务内写不可变流水与审计。审计写入失败不回滚业务(统一审计 Writer 自行留痕)。 func writeRecord(ctx context.Context, tx *gorm.DB, writer *audit.Writer, input recordInput) error { usage := input.Usage if usage == nil || usage.ID == 0 { return errors.New(errors.CodeInvalidParam, "沉默期流水缺少套餐记录") } assetType, assetID := usageAsset(usage) operatorID, operatorType := operatorFromContext(ctx) extra := []byte("{}") if len(input.Extra) > 0 { encoded, err := sonic.Marshal(input.Extra) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "序列化沉默期流水扩展信息失败") } extra = encoded } record := &model.PackageSilenceRecord{ PackageUsageID: usage.ID, EventType: input.EventType, TriggerSource: input.Trigger, AssetType: assetType, AssetID: assetID, BeforeStatus: input.BeforeStatus, AfterStatus: usage.SilenceStatus, BeforeDeadlineAt: input.BeforeDeadline, AfterDeadlineAt: usage.SilenceDeadlineAt, PeriodDays: usage.SilencePeriodDays, DefaultDaysSnapshot: input.DefaultDays, IsAdjusted: input.IsAdjusted, ChangeDays: input.ChangeDays, OperatorID: operatorID, OperatorType: operatorType, Reason: truncate(input.Reason, 500), ApprovalInstanceID: input.ApprovalID, BatchID: input.BatchID, ExtensionID: input.ExtensionID, ExchangeOrderID: input.ExchangeID, ExchangeNo: input.ExchangeNo, RealnameAt: input.RealnameAt, Extra: extra, CreatedAt: time.Now(), } if err := postgres.NewPackageSilenceRecordStore(tx).Create(ctx, record); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入沉默期流水失败") } if input.AuditAction == "" || writer == nil { return nil } metadata := map[string]any{ "event_type": input.EventType, "trigger": input.Trigger, "record_id": record.ID, "change_days": input.ChangeDays, "reason": truncate(input.Reason, 200), } for key, value := range input.Extra { metadata[key] = value } return writer.AppendSilenceUsage(ctx, tx, audit.SilenceAuditInput{ ActionCode: input.AuditAction, Summary: input.AuditSummary, Usage: usage, BeforeData: map[string]any{"silence_status": input.BeforeStatus, "silence_deadline_at": input.BeforeDeadline}, AfterData: silenceStateData(usage), Metadata: metadata, }) } func silenceStateData(usage *model.PackageUsage) map[string]any { return map[string]any{ "silence_status": usage.SilenceStatus, "silence_deadline_at": usage.SilenceDeadlineAt, "silence_period_days": usage.SilencePeriodDays, "silence_extended_days": usage.SilenceExtendedDays, "silence_paid_extended_days": usage.SilencePaidExtendedDays, "silence_extension_count": usage.SilenceExtensionCount, "silence_free_extension_used": usage.SilenceFreeExtensionUsed, "silence_expired_at": usage.SilenceExpiredAt, "silence_adjust_pending": usage.SilenceAdjustPending, } } // usageAsset 返回套餐记录当前载体。 func usageAsset(usage *model.PackageUsage) (string, uint) { if usage.DeviceID > 0 { return constants.SilenceAssetTypeDevice, usage.DeviceID } return constants.SilenceAssetTypeIotCard, usage.IotCardID } func operatorFromContext(ctx context.Context) (uint, string) { value := auditcontext.From(ctx) if value.ActorID == "" { return 0, constants.SilenceOperatorSystem } var id uint for _, r := range value.ActorID { if r < '0' || r > '9' { return 0, value.ActorKind } id = id*10 + uint(r-'0') } switch value.ActorKind { case constants.AuditActorAccount: return id, constants.SilenceOperatorAccount case constants.AuditActorPersonalCustomer: return id, constants.SilenceOperatorCustomer default: return id, value.ActorKind } } func truncate(value string, max int) string { runes := []rune(value) if len(runes) <= max { return value } return string(runes[:max]) } func stringPtr(value string) *string { return &value } func copyStringPtr(value *string) *string { if value == nil { return nil } copied := *value return &copied } func copyTimePtr(value *time.Time) *time.Time { if value == nil { return nil } copied := *value return &copied }