Files
junhong_cmp_fiber/internal/service/package/lifecycle_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

222 lines
9.7 KiB
Go

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)
}