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