fix: 修正套餐激活和时间字段nullable问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
核心变更: 1. Model层时间字段改为*time.Time并设为nullable - PackageUsage.ActivatedAt/ExpiresAt - PersonalCustomerDevice/ICCID/Phone.LastUsedAt/VerifiedAt 2. 数据库迁移: - activated_at/expires_at列移除NOT NULL约束 - 清洗零值记录(status=0且activated_at<'2000-01-01') 3. 新增ActivateSpecificPackage方法:精准激活指定套餐, 修复HandlePackageQueueActivation从"查找过期包"改为直接激活payload指定套餐 4. 新增孤儿套餐恢复扫描:Worker启动或每次套餐检查时, 自动发现并恢复无status=1主套餐的孤儿载体 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// PackageActivationHandler 套餐激活检查处理器
|
||||
@@ -94,9 +95,116 @@ func (h *PackageActivationHandler) HandlePackageActivationCheck(ctx context.Cont
|
||||
zap.Int("processed", len(expiredPackages)),
|
||||
zap.Duration("total_duration", time.Since(startTime)))
|
||||
|
||||
// 任务 6: 孤儿套餐扫描
|
||||
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))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findAndActivateOrphanPackages 任务 6.1-6.2: 查找孤儿载体并触发激活
|
||||
// 孤儿定义:存在 status=0 的主套餐(pending_realname_activation=false),但不存在 status=1 的主套餐
|
||||
func (h *PackageActivationHandler) findAndActivateOrphanPackages(ctx context.Context) (int, error) {
|
||||
// 查询孤儿待生效主套餐(无生效主套餐但有待生效主套餐)
|
||||
var orphanUsages []*model.PackageUsage
|
||||
err := h.db.WithContext(ctx).
|
||||
Where("status = ?", constants.PackageUsageStatusPending).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where("pending_realname_activation = ?", false).
|
||||
Where("deleted_at IS NULL").
|
||||
Limit(100).
|
||||
Find(&orphanUsages).Error
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询孤儿套餐失败")
|
||||
}
|
||||
|
||||
if len(orphanUsages) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 按载体分组,去重
|
||||
type carrierKey struct {
|
||||
carrierType string
|
||||
carrierID uint
|
||||
}
|
||||
carrierMap := make(map[carrierKey]*model.PackageUsage) // 保留 priority 最低的套餐
|
||||
|
||||
for _, usage := range orphanUsages {
|
||||
key := carrierKey{}
|
||||
if usage.IotCardID > 0 {
|
||||
key.carrierType = "iot_card"
|
||||
key.carrierID = usage.IotCardID
|
||||
} else if usage.DeviceID > 0 {
|
||||
key.carrierType = "device"
|
||||
key.carrierID = usage.DeviceID
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查该载体是否已有生效主套餐
|
||||
var activeCount int64
|
||||
var countErr error
|
||||
if key.carrierType == "iot_card" {
|
||||
countErr = h.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status = ?", constants.PackageUsageStatusActive).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where("iot_card_id = ?", key.carrierID).
|
||||
Count(&activeCount).Error
|
||||
} else {
|
||||
countErr = h.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status = ?", constants.PackageUsageStatusActive).
|
||||
Where("master_usage_id IS NULL").
|
||||
Where("device_id = ?", key.carrierID).
|
||||
Count(&activeCount).Error
|
||||
}
|
||||
if countErr != nil {
|
||||
h.logger.Warn("检查载体生效套餐失败",
|
||||
zap.String("carrier_type", key.carrierType),
|
||||
zap.Uint("carrier_id", key.carrierID),
|
||||
zap.Error(countErr))
|
||||
continue
|
||||
}
|
||||
|
||||
// 已有生效套餐,跳过
|
||||
if activeCount > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留 priority 最低的套餐
|
||||
if existing, ok := carrierMap[key]; ok {
|
||||
if usage.Priority < existing.Priority {
|
||||
carrierMap[key] = usage
|
||||
}
|
||||
} else {
|
||||
carrierMap[key] = usage
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个孤儿载体提交激活任务
|
||||
count := 0
|
||||
for key, usage := range carrierMap {
|
||||
if err := h.enqueueActivationTask(ctx, usage.ID, key.carrierType, key.carrierID, "orphan_recovery"); err != nil {
|
||||
h.logger.Warn("提交孤儿套餐激活任务失败",
|
||||
zap.Uint("package_usage_id", usage.ID),
|
||||
zap.String("carrier_type", key.carrierType),
|
||||
zap.Uint("carrier_id", key.carrierID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// findExpiredMainPackages 任务 19.2: 查询已过期的主套餐
|
||||
func (h *PackageActivationHandler) findExpiredMainPackages(ctx context.Context) ([]*model.PackageUsage, error) {
|
||||
var packages []*model.PackageUsage
|
||||
@@ -123,9 +231,13 @@ func (h *PackageActivationHandler) processExpiredPackage(ctx context.Context, pk
|
||||
return err
|
||||
}
|
||||
|
||||
expiresAt := time.Now()
|
||||
if pkg.ExpiresAt != nil {
|
||||
expiresAt = *pkg.ExpiresAt
|
||||
}
|
||||
h.logger.Info("主套餐已过期",
|
||||
zap.Uint("package_usage_id", pkg.ID),
|
||||
zap.Time("expires_at", pkg.ExpiresAt))
|
||||
zap.Time("expires_at", expiresAt))
|
||||
|
||||
// 任务 19.4: 加油包级联失效
|
||||
if err := h.invalidateAddons(ctx, tx, pkg.ID); err != nil {
|
||||
@@ -422,23 +534,17 @@ func (h *PackageActivationHandler) HandlePackageQueueActivation(ctx context.Cont
|
||||
|
||||
// 调用 ActivationService 执行激活
|
||||
if h.activationService != nil {
|
||||
if err := h.activationService.ActivateQueuedPackage(ctx, payload.CarrierType, payload.CarrierID); err != nil {
|
||||
if err := h.activationService.ActivateSpecificPackage(ctx, payload.PackageUsageID); err != nil {
|
||||
h.logger.Error("套餐激活失败",
|
||||
zap.Uint("package_usage_id", payload.PackageUsageID),
|
||||
zap.String("carrier_type", payload.CarrierType),
|
||||
zap.Uint("carrier_id", payload.CarrierID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// ActivationService 未注入,直接更新状态
|
||||
now := time.Now()
|
||||
if err := h.db.Model(&pkg).Updates(map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// ActivationService 未注入,无法安全激活(缺少 expires_at 计算)
|
||||
h.logger.Error("激活服务未注入,无法执行套餐激活",
|
||||
zap.Uint("package_usage_id", payload.PackageUsageID))
|
||||
return errors.New(errors.CodeInternalError, "激活服务未注入,无法执行套餐激活")
|
||||
}
|
||||
|
||||
h.logger.Info("套餐激活成功",
|
||||
|
||||
Reference in New Issue
Block a user