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:
@@ -363,8 +363,8 @@ func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
||||
VirtualUsedMB: float64(usage.DataUsageMB) / ratio,
|
||||
VirtualRemainMB: float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
ActivatedAt: nonZeroTimePtr(usage.ActivatedAt),
|
||||
ExpiresAt: nonZeroTimePtr(usage.ExpiresAt),
|
||||
ActivatedAt: usage.ActivatedAt,
|
||||
ExpiresAt: usage.ExpiresAt,
|
||||
MasterUsageID: usage.MasterUsageID,
|
||||
Priority: usage.Priority,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
|
||||
@@ -69,8 +69,8 @@ type PackageUsage struct {
|
||||
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:已使用流量(MB)" json:"data_usage_mb"`
|
||||
RealDataUsageMB int64 `gorm:"column:real_data_usage_mb;type:bigint;default:0;comment:真流量使用(MB)" json:"real_data_usage_mb"`
|
||||
VirtualDataUsageMB int64 `gorm:"column:virtual_data_usage_mb;type:bigint;default:0;comment:虚流量使用(MB)" json:"virtual_data_usage_mb"`
|
||||
ActivatedAt time.Time `gorm:"column:activated_at;not null;comment:套餐生效时间" json:"activated_at"`
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;not null;comment:套餐过期时间" json:"expires_at"`
|
||||
ActivatedAt *time.Time `gorm:"column:activated_at;comment:套餐生效时间" json:"activated_at"`
|
||||
ExpiresAt *time.Time `gorm:"column:expires_at;comment:套餐过期时间" json:"expires_at"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 0-待生效 1-生效中 2-已用完 3-已过期 4-已失效" json:"status"`
|
||||
LastPackageCheckAt *time.Time `gorm:"column:last_package_check_at;comment:最后一次套餐流量检查时间" json:"last_package_check_at"`
|
||||
Priority int `gorm:"column:priority;type:int;default:1;index:idx_package_usage_priority;comment:优先级(主套餐和加油包按此字段排队,数字越小优先级越高)" json:"priority"`
|
||||
|
||||
@@ -13,7 +13,7 @@ type PersonalCustomerDevice struct {
|
||||
CustomerID uint `gorm:"column:customer_id;type:bigint;not null;comment:关联个人客户ID" json:"customer_id"`
|
||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);not null;comment:设备虚拟号/IMEI" json:"virtual_no"`
|
||||
BindAt time.Time `gorm:"column:bind_at;type:timestamp;not null;comment:绑定时间" json:"bind_at"`
|
||||
LastUsedAt time.Time `gorm:"column:last_used_at;type:timestamp;comment:最后使用时间" json:"last_used_at"`
|
||||
LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp;comment:最后使用时间" json:"last_used_at"`
|
||||
Status int `gorm:"column:status;type:int;not null;default:1;comment:状态 0=禁用 1=启用" json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ type PersonalCustomerICCID struct {
|
||||
CustomerID uint `gorm:"column:customer_id;type:bigint;not null;comment:关联个人客户ID" json:"customer_id"`
|
||||
ICCID string `gorm:"column:iccid;type:varchar(20);not null;comment:ICCID(20位数字)" json:"iccid"`
|
||||
BindAt time.Time `gorm:"column:bind_at;type:timestamp;not null;comment:绑定时间" json:"bind_at"`
|
||||
LastUsedAt time.Time `gorm:"column:last_used_at;type:timestamp;comment:最后使用时间" json:"last_used_at"`
|
||||
LastUsedAt *time.Time `gorm:"column:last_used_at;type:timestamp;comment:最后使用时间" json:"last_used_at"`
|
||||
Status int `gorm:"column:status;type:int;not null;default:1;comment:状态 0=禁用 1=启用" json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ type PersonalCustomerPhone struct {
|
||||
CustomerID uint `gorm:"column:customer_id;type:bigint;not null;comment:关联个人客户ID" json:"customer_id"`
|
||||
Phone string `gorm:"column:phone;type:varchar(20);not null;comment:手机号" json:"phone"`
|
||||
IsPrimary bool `gorm:"column:is_primary;type:boolean;not null;default:false;comment:是否主手机号" json:"is_primary"`
|
||||
VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp;comment:验证通过时间" json:"verified_at"`
|
||||
VerifiedAt *time.Time `gorm:"column:verified_at;type:timestamp;comment:验证通过时间" json:"verified_at"`
|
||||
Status int `gorm:"column:status;type:int;not null;default:1;comment:状态 0=禁用 1=启用" json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -262,9 +262,12 @@ func (p *PollingInitializer) initBatch(ctx context.Context, cards []*model.IotCa
|
||||
|
||||
cacheKey := constants.RedisPollingCardInfoKey(card.ID)
|
||||
cacheData := map[string]interface{}{
|
||||
"id": card.ID, "iccid": card.ICCID,
|
||||
"card_category": card.CardCategory, "real_name_status": card.RealNameStatus,
|
||||
"network_status": card.NetworkStatus, "carrier_id": card.CarrierID,
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"card_category": card.CardCategory,
|
||||
"real_name_status": card.RealNameStatus,
|
||||
"network_status": card.NetworkStatus,
|
||||
"carrier_id": card.CarrierID,
|
||||
"current_month_usage_mb": card.CurrentMonthUsageMB,
|
||||
"last_gateway_reading_mb": card.LastGatewayReadingMB,
|
||||
"data_usage_mb": card.DataUsageMB,
|
||||
|
||||
@@ -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("套餐激活成功",
|
||||
|
||||
@@ -687,8 +687,8 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa
|
||||
VirtualRemainMB: float64(u.DataLimitMB-u.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
EnableVirtualData: enableVirtualData,
|
||||
ActivatedAt: nonZeroTimePtr(u.ActivatedAt),
|
||||
ExpiresAt: nonZeroTimePtr(u.ExpiresAt),
|
||||
ActivatedAt: u.ActivatedAt,
|
||||
ExpiresAt: u.ExpiresAt,
|
||||
MasterUsageID: u.MasterUsageID,
|
||||
Priority: u.Priority,
|
||||
CreatedAt: u.CreatedAt,
|
||||
@@ -764,8 +764,8 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui
|
||||
VirtualRemainMB: float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
EnableVirtualData: enableVirtualData,
|
||||
ActivatedAt: nonZeroTimePtr(usage.ActivatedAt),
|
||||
ExpiresAt: nonZeroTimePtr(usage.ExpiresAt),
|
||||
ActivatedAt: usage.ActivatedAt,
|
||||
ExpiresAt: usage.ExpiresAt,
|
||||
MasterUsageID: usage.MasterUsageID,
|
||||
Priority: usage.Priority,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
|
||||
@@ -1829,8 +1829,8 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
}
|
||||
|
||||
if status == constants.PackageUsageStatusActive {
|
||||
usage.ActivatedAt = activatedAt
|
||||
usage.ExpiresAt = expiresAt
|
||||
usage.ActivatedAt = &activatedAt
|
||||
usage.ExpiresAt = &expiresAt
|
||||
usage.NextResetAt = nextResetAt
|
||||
}
|
||||
|
||||
@@ -1883,7 +1883,11 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
var expiresAt time.Time
|
||||
// 注意:has_independent_expiry 字段在 Package 模型中,暂时使用默认行为
|
||||
// 默认加油包跟随主套餐过期
|
||||
expiresAt = mainPackage.ExpiresAt
|
||||
if mainPackage.ExpiresAt != nil {
|
||||
expiresAt = *mainPackage.ExpiresAt
|
||||
} else {
|
||||
expiresAt = now
|
||||
}
|
||||
|
||||
usage := &model.PackageUsage{
|
||||
BaseModel: model.BaseModel{
|
||||
@@ -1898,8 +1902,8 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
MasterUsageID: &mainPackage.ID,
|
||||
ActivatedAt: activatedAt,
|
||||
ExpiresAt: expiresAt,
|
||||
ActivatedAt: &activatedAt,
|
||||
ExpiresAt: &expiresAt,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
}
|
||||
|
||||
|
||||
@@ -202,9 +202,13 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新过期主套餐状态失败")
|
||||
}
|
||||
|
||||
expiresAt := now
|
||||
if expiredMain.ExpiresAt != nil {
|
||||
expiresAt = *expiredMain.ExpiresAt
|
||||
}
|
||||
s.logger.Info("主套餐已过期",
|
||||
zap.Uint("usage_id", expiredMain.ID),
|
||||
zap.Time("expires_at", expiredMain.ExpiresAt))
|
||||
zap.Time("expires_at", expiresAt))
|
||||
|
||||
// 任务 9.7: 加油包级联失效
|
||||
if err := s.invalidateAddons(ctx, tx, expiredMain.ID); err != nil {
|
||||
@@ -221,6 +225,120 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
})
|
||||
}
|
||||
|
||||
// ActivateSpecificPackage 任务 4: 激活指定的套餐使用记录
|
||||
// 根据 PackageUsageID 精准激活目标套餐,而非重跑"查找过期包"流程
|
||||
func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, packageUsageID uint) error {
|
||||
// 加载 PackageUsage
|
||||
var usage model.PackageUsage
|
||||
if err := s.db.WithContext(ctx).First(&usage, packageUsageID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "套餐使用记录不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
// 幂等检查:非待生效状态直接返回
|
||||
if usage.Status != constants.PackageUsageStatusPending {
|
||||
s.logger.Info("套餐无需激活(非待生效状态)",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Int("status", usage.Status))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 确定载体类型和 ID
|
||||
var carrierType string
|
||||
var carrierID uint
|
||||
if usage.IotCardID > 0 {
|
||||
carrierType = "iot_card"
|
||||
carrierID = usage.IotCardID
|
||||
} else if usage.DeviceID > 0 {
|
||||
carrierType = "device"
|
||||
carrierID = usage.DeviceID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐使用记录缺少载体信息")
|
||||
}
|
||||
|
||||
// 获取分布式锁
|
||||
lockKey := constants.RedisPackageActivationLockKey(carrierType, carrierID)
|
||||
lockValue := time.Now().String()
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, lockValue, 30*time.Second).Result()
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeRedisError, err, "获取分布式锁失败")
|
||||
}
|
||||
if !locked {
|
||||
s.logger.Warn("套餐激活正在进行中,跳过",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
}
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
// 加载关联 Package
|
||||
var pkg model.Package
|
||||
if err := s.db.WithContext(ctx).First(&pkg, usage.PackageID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
// 事务内激活
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 再次幂等检查(防止锁释放期间被其他操作激活)
|
||||
var currentUsage model.PackageUsage
|
||||
if err := tx.First(¤tUsage, packageUsageID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "重新查询套餐使用记录失败")
|
||||
}
|
||||
if currentUsage.Status != constants.PackageUsageStatusPending {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按 ExpiryBase 选择激活时间基准
|
||||
now := time.Now()
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
|
||||
// 更新套餐状态
|
||||
updates := map[string]interface{}{
|
||||
"status": constants.PackageUsageStatusActive,
|
||||
"activated_at": activatedAt,
|
||||
"expires_at": expiresAt,
|
||||
}
|
||||
if nextResetAt != nil {
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
if err := tx.Model(&usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
|
||||
}
|
||||
|
||||
// 同步载体状态
|
||||
s.syncCarrierStatusActivated(ctx, tx, &usage, carrierType, carrierID)
|
||||
|
||||
s.logger.Info("指定套餐已激活",
|
||||
zap.Uint("usage_id", usage.ID),
|
||||
zap.Uint("package_id", usage.PackageID),
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
|
||||
// 异步复机
|
||||
if s.resumeCallback != nil {
|
||||
go func(ct string, cid uint) {
|
||||
if err := s.resumeCallback.ResumeCardIfStopped(context.Background(), ct, cid); err != nil {
|
||||
s.logger.Error("激活后自动复机失败",
|
||||
zap.String("carrier_type", ct),
|
||||
zap.Uint("carrier_id", cid),
|
||||
zap.Error(err))
|
||||
}
|
||||
}(carrierType, carrierID)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// invalidateAddons 任务 9.7: 加油包级联失效
|
||||
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) error {
|
||||
var addons []*model.PackageUsage
|
||||
@@ -280,7 +398,13 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
}
|
||||
|
||||
// 激活套餐
|
||||
activatedAt := now
|
||||
// 按 ExpiryBase 选择激活计时基准:from_purchase-购买时起算用 CreatedAt,否则用当前时刻
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
activatedAt = nextMain.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
|
||||
// 计算下次重置时间
|
||||
|
||||
@@ -81,12 +81,12 @@ func (s *CustomerViewService) GetMyUsage(ctx context.Context, carrierType string
|
||||
|
||||
// 格式化时间
|
||||
activatedAtStr := ""
|
||||
if pkg.ActivatedAt.Year() > 1 {
|
||||
if pkg.ActivatedAt != nil {
|
||||
activatedAtStr = pkg.ActivatedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
expiresAtStr := ""
|
||||
if pkg.ExpiresAt.Year() > 1 {
|
||||
if pkg.ExpiresAt != nil {
|
||||
expiresAtStr = pkg.ExpiresAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ func (s *ResetService) resetMonthlyUsageWithDB(ctx context.Context, db *gorm.DB)
|
||||
continue
|
||||
}
|
||||
|
||||
activatedAt := usage.ActivatedAt
|
||||
if activatedAt.IsZero() {
|
||||
activatedAt = now
|
||||
activatedAt := now
|
||||
if usage.ActivatedAt != nil && !usage.ActivatedAt.IsZero() {
|
||||
activatedAt = *usage.ActivatedAt
|
||||
}
|
||||
nextResetAt := CalculateNextResetTime(constants.PackageDataResetMonthly, pkg.CalendarType, now, activatedAt)
|
||||
if nextResetAt == nil {
|
||||
|
||||
@@ -22,9 +22,8 @@ func NewPersonalCustomerDeviceStore(db *gorm.DB) *PersonalCustomerDeviceStore {
|
||||
|
||||
// Create 创建设备号绑定记录
|
||||
func (s *PersonalCustomerDeviceStore) Create(ctx context.Context, record *model.PersonalCustomerDevice) error {
|
||||
now := time.Now()
|
||||
record.BindAt = now
|
||||
record.LastUsedAt = now
|
||||
record.BindAt = time.Now()
|
||||
// LastUsedAt 在首次使用时更新,创建时保持 nil
|
||||
return s.db.WithContext(ctx).Create(record).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,8 @@ func NewPersonalCustomerICCIDStore(db *gorm.DB) *PersonalCustomerICCIDStore {
|
||||
|
||||
// Create 创建 ICCID 绑定记录
|
||||
func (s *PersonalCustomerICCIDStore) Create(ctx context.Context, record *model.PersonalCustomerICCID) error {
|
||||
now := time.Now()
|
||||
record.BindAt = now
|
||||
record.LastUsedAt = now
|
||||
record.BindAt = time.Now()
|
||||
// LastUsedAt 在首次使用时更新,创建时保持 nil
|
||||
return s.db.WithContext(ctx).Create(record).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"gorm.io/gorm"
|
||||
@@ -22,7 +21,7 @@ func NewPersonalCustomerPhoneStore(db *gorm.DB) *PersonalCustomerPhoneStore {
|
||||
|
||||
// Create 创建手机号绑定记录
|
||||
func (s *PersonalCustomerPhoneStore) Create(ctx context.Context, phone *model.PersonalCustomerPhone) error {
|
||||
phone.VerifiedAt = time.Now()
|
||||
// VerifiedAt 在验证通过后更新,创建时保持 nil
|
||||
return s.db.WithContext(ctx).Create(phone).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -528,8 +528,8 @@ func (h *AutoPurchaseHandler) activateMainPackage(
|
||||
}
|
||||
|
||||
if status == constants.PackageUsageStatusActive {
|
||||
usage.ActivatedAt = activatedAt
|
||||
usage.ExpiresAt = expiresAt
|
||||
usage.ActivatedAt = &activatedAt
|
||||
usage.ExpiresAt = &expiresAt
|
||||
usage.NextResetAt = nextResetAt
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
|
||||
Status: constants.PackageUsageStatusActive,
|
||||
Priority: priority,
|
||||
MasterUsageID: &mainPackage.ID,
|
||||
ActivatedAt: now,
|
||||
ActivatedAt: &now,
|
||||
ExpiresAt: expiresAt,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
Generation: order.Generation,
|
||||
|
||||
Reference in New Issue
Block a user