package packagepkg import ( "context" "sort" "strconv" "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/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) type packageUsageAuditChange struct { Usage *model.PackageUsage BeforeData map[string]any AfterData map[string]any } func appendPackageUsageAudit(ctx context.Context, tx *gorm.DB, writer *audit.Writer, actionCode, summary string, changes []packageUsageAuditChange, refund *model.RefundRequest, metadata map[string]any) error { if writer == nil { return errors.New(errors.CodeInvalidStatus, "套餐权益统一审计接缝未配置") } resources, err := packageUsageAuditResources(ctx, tx, changes, refund, summary) if err != nil { return err } return writer.Append(ctx, tx, audit.AppendInput{ ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, Metadata: metadata, Resources: resources, }) } func (s *ActivationService) appendExpirationAudit(ctx context.Context, tx *gorm.DB, main *model.PackageUsage, mainBeforeData map[string]any, addons []packageUsageAuditChange) error { changes := make([]packageUsageAuditChange, 0, 1+len(addons)) changes = append(changes, packageUsageAuditChange{Usage: main, BeforeData: mainBeforeData, AfterData: packageUsageStateData(main)}) changes = append(changes, addons...) return appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageExpired, "套餐权益到期并处理关联加油包", changes, nil, map[string]any{ "invalidated_addon_count": len(addons), }) } // AppendExpirationAudit 在调度器的既有过期事务内记录实际权益变化。 func (s *ActivationService) AppendExpirationAudit(ctx context.Context, tx *gorm.DB, main *model.PackageUsage, addons []*model.PackageUsage) error { if main == nil { return errors.New(errors.CodeInvalidParam, "过期套餐权益审计资源不完整") } mainAfter := *main mainAfter.Status = constants.PackageUsageStatusExpired addonChanges := make([]packageUsageAuditChange, 0, len(addons)) for _, addon := range addons { if addon == nil { continue } beforeData := packageUsageStateData(addon) after := *addon after.Status = constants.PackageUsageStatusInvalidated addonChanges = append(addonChanges, packageUsageAuditChange{Usage: &after, BeforeData: beforeData, AfterData: packageUsageStateData(&after)}) } return s.appendExpirationAudit(ctx, tx, &mainAfter, packageUsageStateData(main), addonChanges) } // RecordUsageFailure 在权益已定位且业务事务回滚后记录失败事实。 func (s *ActivationService) RecordUsageFailure(ctx context.Context, actionCode, summary string, usage *model.PackageUsage, businessErr error) { recordPackageUsageFailure(ctx, s.db, s.auditWriter, actionCode, summary, usage, businessErr) } func packageUsageAuditResources(ctx context.Context, tx *gorm.DB, changes []packageUsageAuditChange, refund *model.RefundRequest, subjectSummary string) ([]audit.ResourceInput, error) { if len(changes) == 0 || changes[0].Usage == nil || changes[0].Usage.ID == 0 { return nil, errors.New(errors.CodeInvalidParam, "套餐权益审计资源不完整") } orderIDs, packageIDs, cardIDs, deviceIDs := packageUsageReferenceIDs(changes) orders, packages, cards, devices, err := loadPackageUsageReferences(ctx, tx, orderIDs, packageIDs, cardIDs, deviceIDs) if err != nil { return nil, err } resources := make([]audit.ResourceInput, 0, len(changes)+len(orders)+len(packages)+len(cards)+len(devices)+1) for index, change := range changes { if change.Usage == nil || change.Usage.ID == 0 { continue } relation := constants.AuditResourceRelationAffected if index == 0 { relation = constants.AuditResourceRelationPrimary } resource := audit.PackageUsageResource(change.Usage, relation, constants.AuditResourceRolePackageUsageTarget, change.BeforeData, change.AfterData) resource.SubjectVisibility = constants.AuditSubjectResult resource.SubjectSummary = subjectSummary resources = append(resources, resource) } for i := range orders { resources = append(resources, audit.OrderResource(&orders[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsageOrder)) } for i := range packages { resources = append(resources, audit.PackageResource(&packages[i], constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsagePackage, nil, nil)) } for i := range cards { id := strconv.FormatUint(uint64(cards[i].ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceIotCard, ID: &id, Key: audit.IotCardResourceKey(&cards[i]), DisplayName: cards[i].ICCID, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePackageUsageAsset, IdentitySnapshot: audit.IotCardIdentitySnapshot(&cards[i]), SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subjectSummary, }) } for i := range devices { id := strconv.FormatUint(uint64(devices[i].ID), 10) resources = append(resources, audit.ResourceInput{ Type: constants.AuditResourceDevice, ID: &id, Key: audit.DeviceResourceKey(&devices[i]), DisplayName: devices[i].VirtualNo, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePackageUsageAsset, IdentitySnapshot: audit.DeviceIdentitySnapshot(&devices[i]), SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subjectSummary, }) } if refund != nil && refund.ID > 0 { resources = append(resources, audit.RefundResource(refund, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageUsageRefund)) } return resources, nil } func packageUsageReferenceIDs(changes []packageUsageAuditChange) ([]uint, []uint, []uint, []uint) { orders, packages, cards, devices := map[uint]struct{}{}, map[uint]struct{}{}, map[uint]struct{}{}, map[uint]struct{}{} for _, change := range changes { if change.Usage == nil { continue } orders[change.Usage.OrderID] = struct{}{} packages[change.Usage.PackageID] = struct{}{} if change.Usage.IotCardID > 0 { cards[change.Usage.IotCardID] = struct{}{} } if change.Usage.DeviceID > 0 { devices[change.Usage.DeviceID] = struct{}{} } } return mapUintKeys(orders), mapUintKeys(packages), mapUintKeys(cards), mapUintKeys(devices) } func loadPackageUsageReferences(ctx context.Context, tx *gorm.DB, orderIDs, packageIDs, cardIDs, deviceIDs []uint) ([]model.Order, []model.Package, []model.IotCard, []model.Device, error) { var orders []model.Order if len(orderIDs) > 0 { if err := tx.WithContext(ctx).Where("id IN ?", orderIDs).Order("id ASC").Find(&orders).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐权益关联订单审计快照失败") } } var packages []model.Package if len(packageIDs) > 0 { if err := tx.WithContext(ctx).Where("id IN ?", packageIDs).Order("id ASC").Find(&packages).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐权益关联套餐审计快照失败") } } var cards []model.IotCard if len(cardIDs) > 0 { if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Order("id ASC").Find(&cards).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐权益关联卡审计快照失败") } } var devices []model.Device if len(deviceIDs) > 0 { if err := tx.WithContext(ctx).Where("id IN ?", deviceIDs).Order("id ASC").Find(&devices).Error; err != nil { return nil, nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐权益关联设备审计快照失败") } } return orders, packages, cards, devices, nil } func mapUintKeys(values map[uint]struct{}) []uint { result := make([]uint, 0, len(values)) for value := range values { if value > 0 { result = append(result, value) } } sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) return result } func packageUsageStateData(usage *model.PackageUsage) map[string]any { if usage == nil { return nil } return map[string]any{ "status": usage.Status, "data_usage_mb": usage.DataUsageMB, "pending_realname_activation": usage.PendingRealnameActivation, "activated_at": usage.ActivatedAt, "expires_at": usage.ExpiresAt, "last_reset_at": usage.LastResetAt, "next_reset_at": usage.NextResetAt, "refund_id": usage.RefundID, "refund_no": usage.RefundNo, "iot_card_id": usage.IotCardID, "device_id": usage.DeviceID, } } func normalizePackageUsageAuditChanges(changes []packageUsageAuditChange) []packageUsageAuditChange { result := make([]packageUsageAuditChange, 0, len(changes)) positions := make(map[uint]int, len(changes)) for _, change := range changes { if change.Usage == nil || change.Usage.ID == 0 { continue } if position, ok := positions[change.Usage.ID]; ok { result[position].Usage = change.Usage result[position].AfterData = change.AfterData continue } positions[change.Usage.ID] = len(result) result = append(result, change) } return result } func recordPackageUsageFailure(ctx context.Context, db *gorm.DB, writer *audit.Writer, actionCode, summary string, usage *model.PackageUsage, businessErr error) { if usage == nil || usage.ID == 0 || writer == nil || db == nil { return } writer.RecordFailure(ctx, db, audit.AppendInput{ ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform, Resources: []audit.ResourceInput{audit.PackageUsageResource( usage, constants.AuditResourceRelationPrimary, constants.AuditResourceRolePackageUsageTarget, packageUsageStateData(usage), nil, )}, }, businessErr) }