This commit is contained in:
@@ -123,49 +123,43 @@ func (h *PackageActivationHandler) syncTrafficBeforeExpiry(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// HandlePackageActivationCheck 任务 19.2-19.5: 处理套餐激活检查
|
||||
// 每 10 秒调度一次,检查过期主套餐并激活下一个待生效主套餐
|
||||
// HandlePackageActivationCheck 任务 19.2-19.6: 处理套餐激活检查
|
||||
// 每 10 秒调度一次,检查主套餐和拥有独立有效期的加油包。
|
||||
func (h *PackageActivationHandler) HandlePackageActivationCheck(ctx context.Context) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// 任务 19.2: 查询已过期的主套餐(status IN (1,2) AND expires_at <= NOW)
|
||||
// 任务 19.2-19.5:处理过期主套餐及其关联加油包。
|
||||
expiredPackages, err := h.findExpiredMainPackages(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("查询过期主套餐失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(expiredPackages) > 0 {
|
||||
h.logger.Info("发现过期主套餐",
|
||||
zap.Int("count", len(expiredPackages)),
|
||||
zap.Duration("check_duration", time.Since(startTime)))
|
||||
|
||||
// 处理每个过期的主套餐
|
||||
for _, pkg := range expiredPackages {
|
||||
if err := h.processExpiredPackage(ctx, pkg); err != nil {
|
||||
h.logger.Error("处理过期套餐失败",
|
||||
zap.Uint("package_usage_id", pkg.ID),
|
||||
zap.Error(err))
|
||||
// 继续处理下一个,不中断
|
||||
continue
|
||||
}
|
||||
for _, pkg := range expiredPackages {
|
||||
if err := h.processExpiredPackage(ctx, pkg); err != nil {
|
||||
h.logger.Error("处理过期套餐失败", zap.Uint("package_usage_id", pkg.ID), zap.Error(err))
|
||||
}
|
||||
|
||||
h.logger.Info("套餐激活检查完成",
|
||||
zap.Int("processed", len(expiredPackages)),
|
||||
zap.Duration("total_duration", time.Since(startTime)))
|
||||
}
|
||||
|
||||
// 任务 6: 孤儿套餐扫描
|
||||
// 任务 19.6:独立有效期加油包单独到期,不等待主套餐到期。
|
||||
expiredAddons, err := h.findExpiredIndependentAddons(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("查询独立有效期加油包失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
for _, addon := range expiredAddons {
|
||||
if err := h.processExpiredIndependentAddon(ctx, addon); err != nil {
|
||||
h.logger.Error("处理独立有效期加油包失败", zap.Uint("package_usage_id", addon.ID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 孤儿套餐扫描仍照常执行。
|
||||
orphanCount, err := h.findAndActivateOrphanPackages(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("孤儿套餐扫描失败", zap.Error(err))
|
||||
// 不中断主流程
|
||||
} else if orphanCount > 0 {
|
||||
h.logger.Info("孤儿套餐扫描完成",
|
||||
zap.Int("orphan_count", orphanCount))
|
||||
h.logger.Info("孤儿套餐扫描完成", zap.Int("orphan_count", orphanCount))
|
||||
}
|
||||
|
||||
h.logger.Debug("套餐激活检查完成", zap.Int("expired_main_count", len(expiredPackages)), zap.Int("expired_addon_count", len(expiredAddons)), zap.Duration("total_duration", time.Since(startTime)))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -252,6 +246,39 @@ func (h *PackageActivationHandler) findExpiredMainPackages(ctx context.Context)
|
||||
return packages, err
|
||||
}
|
||||
|
||||
// findExpiredIndependentAddons 查询拥有独立有效期且已到期的加油包。
|
||||
func (h *PackageActivationHandler) findExpiredIndependentAddons(ctx context.Context) ([]*model.PackageUsage, error) {
|
||||
var addons []*model.PackageUsage
|
||||
err := h.db.WithContext(ctx).
|
||||
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
||||
Where("master_usage_id IS NOT NULL AND has_independent_expiry = ?", true).
|
||||
Where("expires_at IS NOT NULL AND expires_at <= ?", time.Now()).
|
||||
Limit(1000).
|
||||
Find(&addons).Error
|
||||
return addons, err
|
||||
}
|
||||
|
||||
// processExpiredIndependentAddon 将独立有效期加油包标记为已过期,不影响主套餐和其他加油包。
|
||||
func (h *PackageActivationHandler) processExpiredIndependentAddon(ctx context.Context, addon *model.PackageUsage) error {
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(addon).
|
||||
Where("status IN ? AND has_independent_expiry = ? AND expires_at IS NOT NULL AND expires_at <= ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}, true, time.Now()).
|
||||
Update("status", constants.PackageUsageStatusExpired)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
if h.activationService == nil {
|
||||
return errors.New(errors.CodeInternalError, "套餐激活服务未注入")
|
||||
}
|
||||
return h.activationService.AppendAddonExpirationAudit(ctx, tx, addon)
|
||||
})
|
||||
}
|
||||
|
||||
// findAndActivateOrphanPackages 任务 6.1-6.2: 查找孤儿载体并触发激活
|
||||
|
||||
// processExpiredPackage 处理单个过期套餐
|
||||
// 流程:先同步最新流量 → 事务内标记过期和失效加油包 → 提交后投递下一套餐;
|
||||
// 仅明确没有后续套餐时才触发停机检查,避免与异步激活任务竞态。
|
||||
|
||||
@@ -2644,15 +2644,15 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
status := constants.PackageUsageStatusActive
|
||||
activatedAt := now
|
||||
|
||||
// 计算过期时间(根据 has_independent_expiry)
|
||||
var expiresAt time.Time
|
||||
// 注意:has_independent_expiry 字段在 Package 模型中,暂时使用默认行为
|
||||
// 默认加油包跟随主套餐过期
|
||||
if mainPackage.ExpiresAt != nil {
|
||||
expiresAt = *mainPackage.ExpiresAt
|
||||
} else {
|
||||
expiresAt = now
|
||||
}
|
||||
// 加油包按自身有效期计算;仅当主套餐更早到期时截断到主套餐到期时间。
|
||||
expiresAt, hasIndependentExpiry := packagepkg.CalculateAddonExpiryTime(
|
||||
activatedAt,
|
||||
terms.CalendarType,
|
||||
terms.DurationMonths,
|
||||
terms.DurationDays,
|
||||
mainPackage.ExpiresAt,
|
||||
)
|
||||
nextResetAt := packagepkg.CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)
|
||||
|
||||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||||
addonRetailAmount := order.TotalAmount
|
||||
@@ -2673,8 +2673,10 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
MasterUsageID: &mainPackage.ID,
|
||||
HasIndependentExpiry: hasIndependentExpiry,
|
||||
ActivatedAt: &activatedAt,
|
||||
ExpiresAt: &expiresAt,
|
||||
NextResetAt: nextResetAt,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
PaidAmount: &order.SellerCostPrice,
|
||||
RetailAmount: &addonRetailAmount,
|
||||
|
||||
@@ -62,6 +62,18 @@ func (s *ActivationService) AppendExpirationAudit(ctx context.Context, tx *gorm.
|
||||
return s.appendExpirationAudit(ctx, tx, &mainAfter, packageUsageStateData(main), addonChanges)
|
||||
}
|
||||
|
||||
// AppendAddonExpirationAudit 记录独立有效期加油包到期事实。
|
||||
func (s *ActivationService) AppendAddonExpirationAudit(ctx context.Context, tx *gorm.DB, addon *model.PackageUsage) error {
|
||||
if addon == nil {
|
||||
return errors.New(errors.CodeInvalidParam, "加油包权益资源不完整")
|
||||
}
|
||||
after := *addon
|
||||
after.Status = constants.PackageUsageStatusExpired
|
||||
return appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageExpired, "加油包权益到期", []packageUsageAuditChange{{
|
||||
Usage: &after, BeforeData: packageUsageStateData(addon), AfterData: packageUsageStateData(&after),
|
||||
}}, nil, map[string]any{"addon_usage_id": addon.ID})
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -39,6 +39,16 @@ func CalculateExpiryTime(calendarType string, activatedAt time.Time, durationMon
|
||||
return expiryDate
|
||||
}
|
||||
|
||||
// CalculateAddonExpiryTime 计算加油包到期时间,并在主套餐更早到期时截断。
|
||||
// 返回值中的 independent 表示加油包自身有效期未晚于主套餐,因而按自身规则到期。
|
||||
func CalculateAddonExpiryTime(activatedAt time.Time, calendarType string, durationMonths, durationDays int, mainExpiresAt *time.Time) (expiresAt time.Time, independent bool) {
|
||||
addonExpiresAt := CalculateExpiryTime(calendarType, activatedAt, durationMonths, durationDays)
|
||||
if mainExpiresAt != nil && mainExpiresAt.Before(addonExpiresAt) {
|
||||
return *mainExpiresAt, false
|
||||
}
|
||||
return addonExpiresAt, true
|
||||
}
|
||||
|
||||
// CalculateValidityDays 计算套餐展示有效天数。
|
||||
// 按天套餐优先返回折算后的总天数;自然月套餐按 baseTime 推算到目标月月末。
|
||||
func CalculateValidityDays(calendarType string, baseTime time.Time, durationMonths, durationDays int) int {
|
||||
|
||||
Reference in New Issue
Block a user