收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"time"
"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/constants"
@@ -38,6 +39,12 @@ type UsageService struct {
deviceSimBindingStore *postgres.DeviceSimBindingStore
logger *zap.Logger
stopResumeCallback StopResumeCallback // 停复机回调,可选
auditWriter *audit.Writer
}
// SetLifecycleAudit 注入套餐权益流量扣减统一审计 Writer。
func (s *UsageService) SetLifecycleAudit(writer *audit.Writer) {
s.auditWriter = writer
}
func NewUsageService(
@@ -81,6 +88,7 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
shouldSuspend := false
suspendCarrierType := ""
var suspendCarrierID uint
var auditChanges []packageUsageAuditChange
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
targetCarrierType, targetCarrierID, packages, err := s.resolveActivePackages(ctx, tx, carrierType, carrierID)
@@ -116,9 +124,12 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
isLastPackage := index == len(packages)-1
if remainingQuota <= 0 && !isLastPackage {
// 套餐已用完,标记为已用完
beforeData := packageUsageStateData(pkg)
if err := tx.Model(pkg).Update("status", constants.PackageUsageStatusDepleted).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新套餐状态失败")
}
pkg.Status = constants.PackageUsageStatusDepleted
auditChanges = append(auditChanges, packageUsageAuditChange{Usage: pkg, BeforeData: beforeData, AfterData: packageUsageStateData(pkg)})
continue
}
@@ -132,9 +143,12 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
deductFromPkg = remainingQuota
}
if deductFromPkg <= 0 {
beforeData := packageUsageStateData(pkg)
if err := tx.Model(pkg).Update("status", constants.PackageUsageStatusDepleted).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新套餐状态失败")
}
pkg.Status = constants.PackageUsageStatusDepleted
auditChanges = append(auditChanges, packageUsageAuditChange{Usage: pkg, BeforeData: beforeData, AfterData: packageUsageStateData(pkg)})
continue
}
@@ -149,14 +163,20 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
updates["status"] = constants.PackageUsageStatusDepleted
}
beforeData := packageUsageStateData(pkg)
if err := tx.Model(pkg).Updates(updates).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新套餐使用量失败")
}
pkg.DataUsageMB = newUsage
if status, ok := updates["status"].(int); ok {
pkg.Status = status
}
// 任务 10.6: 写入日记录
if err := s.updateDailyRecord(ctx, tx, pkg.ID, today, deductFromPkg, newUsage); err != nil {
return err
}
auditChanges = append(auditChanges, packageUsageAuditChange{Usage: pkg, BeforeData: beforeData, AfterData: packageUsageStateData(pkg)})
remainingUsage -= deductFromPkg
@@ -171,17 +191,28 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
}
// 任务 10.5: 检查是否所有套餐都用完(触发停机)
shouldSuspendCurrent, err := s.checkAndTriggerSuspension(ctx, tx, targetCarrierType, targetCarrierID)
shouldSuspendCurrent, suspensionChanges, err := s.checkAndTriggerSuspension(ctx, tx, targetCarrierType, targetCarrierID)
if err != nil {
return err
}
auditChanges = append(auditChanges, suspensionChanges...)
shouldSuspend = shouldSuspendCurrent
suspendCarrierType = targetCarrierType
suspendCarrierID = targetCarrierID
if len(auditChanges) > 0 {
if err := appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageTrafficDeducted, "扣减套餐权益流量", normalizePackageUsageAuditChanges(auditChanges), nil, map[string]any{
"carrier_type": targetCarrierType, "carrier_id": targetCarrierID, "usage_mb": deductUsageMB,
}); err != nil {
return err
}
}
return nil
})
if err != nil {
if len(auditChanges) > 0 {
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageTrafficDeducted, "扣减套餐权益流量失败", auditChanges[0].Usage, err)
}
return err
}
@@ -360,7 +391,7 @@ func (s *UsageService) updateDailyRecord(ctx context.Context, tx *gorm.DB, packa
}
// checkAndTriggerSuspension 任务 10.5: 检查停机条件
func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, []packageUsageAuditChange, error) {
query := tx.Model(&model.PackageUsage{}).
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted})
@@ -369,24 +400,28 @@ func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.D
} else if carrierType == constants.AssetTypeDevice {
query = query.Where("device_id = ?", carrierID)
} else {
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
return false, nil, errors.New(errors.CodeInvalidParam, "无效的载体类型")
}
var packages []*model.PackageUsage
if err := query.Find(&packages).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐状态失败")
return false, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐状态失败")
}
hasAvailablePackage := false
var changes []packageUsageAuditChange
for _, pkg := range packages {
if pkg == nil {
continue
}
if pkg.IsTrafficDepleted() {
if pkg.Status != constants.PackageUsageStatusDepleted {
beforeData := packageUsageStateData(pkg)
if err := tx.Model(pkg).Update("status", constants.PackageUsageStatusDepleted).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐耗尽状态失败")
return false, nil, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐耗尽状态失败")
}
pkg.Status = constants.PackageUsageStatusDepleted
changes = append(changes, packageUsageAuditChange{Usage: pkg, BeforeData: beforeData, AfterData: packageUsageStateData(pkg)})
}
continue
}
@@ -398,10 +433,10 @@ func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.D
s.logger.Warn("所有套餐已用完,触发停机",
zap.String("carrier_type", carrierType),
zap.Uint("carrier_id", carrierID))
return true, nil
return true, changes, nil
}
return false, nil
return false, changes, nil
}
// triggerSuspensionAfterCommit 在事务提交后触发停机检查,避免回调读到未提交状态。