修复加油包独立有效期和流量重置
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 4m22s

This commit is contained in:
2026-09-22 16:36:07 +08:00
parent 3610b776ca
commit c1c78d5008
4 changed files with 87 additions and 36 deletions

View File

@@ -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 处理单个过期套餐
// 流程:先同步最新流量 → 事务内标记过期和失效加油包 → 提交后投递下一套餐;
// 仅明确没有后续套餐时才触发停机检查,避免与异步激活任务竞态。

View File

@@ -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,

View File

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

View File

@@ -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 {