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>
274 lines
7.6 KiB
Go
274 lines
7.6 KiB
Go
package packagepkg
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"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"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type ResetService struct {
|
|
db *gorm.DB
|
|
redis *redis.Client
|
|
packageUsageStore *postgres.PackageUsageStore
|
|
logger *zap.Logger
|
|
resumeCallback ResumeCallback
|
|
}
|
|
|
|
func NewResetService(
|
|
db *gorm.DB,
|
|
redis *redis.Client,
|
|
packageUsageStore *postgres.PackageUsageStore,
|
|
logger *zap.Logger,
|
|
) *ResetService {
|
|
return &ResetService{
|
|
db: db,
|
|
redis: redis,
|
|
packageUsageStore: packageUsageStore,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// SetResumeCallback 设置复机回调,流量重置后对满足条件的卡触发自动复机
|
|
func (s *ResetService) SetResumeCallback(callback ResumeCallback) {
|
|
s.resumeCallback = callback
|
|
}
|
|
|
|
// ResetDailyUsage 任务 11.2-11.3: 重置日流量
|
|
func (s *ResetService) ResetDailyUsage(ctx context.Context) error {
|
|
return s.resetDailyUsageWithDB(ctx, s.db)
|
|
}
|
|
|
|
// resetDailyUsageWithDB 内部方法,支持传入 DB/TX
|
|
func (s *ResetService) resetDailyUsageWithDB(ctx context.Context, db *gorm.DB) error {
|
|
now := time.Now()
|
|
|
|
var resetPackages []*model.PackageUsage
|
|
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var packages []*model.PackageUsage
|
|
err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetDaily).
|
|
Where("next_reset_at <= ?", now).
|
|
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
|
Find(&packages).Error
|
|
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败")
|
|
}
|
|
|
|
if len(packages) == 0 {
|
|
s.logger.Info("没有需要重置的日流量套餐")
|
|
return nil
|
|
}
|
|
|
|
packageIDs := make([]uint, len(packages))
|
|
for i, pkg := range packages {
|
|
packageIDs[i] = pkg.ID
|
|
}
|
|
|
|
nextReset := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
|
|
|
updates := map[string]interface{}{
|
|
"data_usage_mb": 0,
|
|
"last_reset_at": now,
|
|
"next_reset_at": nextReset,
|
|
"status": constants.PackageUsageStatusActive,
|
|
}
|
|
|
|
if err := tx.Model(&model.PackageUsage{}).
|
|
Where("id IN ?", packageIDs).
|
|
Updates(updates).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "批量重置日流量失败")
|
|
}
|
|
|
|
s.logger.Info("日流量重置完成",
|
|
zap.Int("count", len(packages)),
|
|
zap.Time("next_reset_at", nextReset))
|
|
|
|
resetPackages = packages
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.triggerResumeForPackages(ctx, resetPackages)
|
|
return nil
|
|
}
|
|
|
|
// ResetMonthlyUsage 任务 11.4-11.5: 重置月流量
|
|
func (s *ResetService) ResetMonthlyUsage(ctx context.Context) error {
|
|
return s.resetMonthlyUsageWithDB(ctx, s.db)
|
|
}
|
|
|
|
// resetMonthlyUsageWithDB 内部方法,支持传入 DB/TX
|
|
func (s *ResetService) resetMonthlyUsageWithDB(ctx context.Context, db *gorm.DB) error {
|
|
now := time.Now()
|
|
|
|
var resetPackages []*model.PackageUsage
|
|
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var packages []*model.PackageUsage
|
|
err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetMonthly).
|
|
Where("next_reset_at <= ?", now).
|
|
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
|
Find(&packages).Error
|
|
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败")
|
|
}
|
|
|
|
if len(packages) == 0 {
|
|
s.logger.Info("没有需要重置的月流量套餐")
|
|
return nil
|
|
}
|
|
|
|
for _, usage := range packages {
|
|
var pkg model.Package
|
|
if err := tx.First(&pkg, usage.PackageID).Error; err != nil {
|
|
s.logger.Error("查询套餐信息失败",
|
|
zap.Uint("usage_id", usage.ID),
|
|
zap.Uint("package_id", usage.PackageID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
activatedAt := now
|
|
if usage.ActivatedAt != nil && !usage.ActivatedAt.IsZero() {
|
|
activatedAt = *usage.ActivatedAt
|
|
}
|
|
nextResetAt := CalculateNextResetTime(constants.PackageDataResetMonthly, pkg.CalendarType, now, activatedAt)
|
|
if nextResetAt == nil {
|
|
s.logger.Warn("计算下次重置时间失败",
|
|
zap.Uint("usage_id", usage.ID))
|
|
continue
|
|
}
|
|
|
|
updates := map[string]interface{}{
|
|
"data_usage_mb": 0,
|
|
"last_reset_at": now,
|
|
"next_reset_at": *nextResetAt,
|
|
"status": constants.PackageUsageStatusActive,
|
|
}
|
|
|
|
if err := tx.Model(usage).Updates(updates).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "重置月流量失败")
|
|
}
|
|
|
|
s.logger.Info("月流量已重置",
|
|
zap.Uint("usage_id", usage.ID),
|
|
zap.String("calendar_type", pkg.CalendarType),
|
|
zap.Time("next_reset_at", *nextResetAt))
|
|
}
|
|
|
|
resetPackages = packages
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.triggerResumeForPackages(ctx, resetPackages)
|
|
return nil
|
|
}
|
|
|
|
// ResetYearlyUsage 任务 11.6-11.7: 重置年流量
|
|
func (s *ResetService) ResetYearlyUsage(ctx context.Context) error {
|
|
return s.resetYearlyUsageWithDB(ctx, s.db)
|
|
}
|
|
|
|
// resetYearlyUsageWithDB 内部方法,支持传入 DB/TX
|
|
func (s *ResetService) resetYearlyUsageWithDB(ctx context.Context, db *gorm.DB) error {
|
|
now := time.Now()
|
|
|
|
var resetPackages []*model.PackageUsage
|
|
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var packages []*model.PackageUsage
|
|
err := tx.Where("data_reset_cycle = ?", constants.PackageDataResetYearly).
|
|
Where("next_reset_at <= ?", now).
|
|
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted}).
|
|
Find(&packages).Error
|
|
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询待重置套餐失败")
|
|
}
|
|
|
|
if len(packages) == 0 {
|
|
s.logger.Info("没有需要重置的年流量套餐")
|
|
return nil
|
|
}
|
|
|
|
packageIDs := make([]uint, len(packages))
|
|
for i, pkg := range packages {
|
|
packageIDs[i] = pkg.ID
|
|
}
|
|
|
|
nextReset := time.Date(now.Year()+1, 1, 1, 0, 0, 0, 0, now.Location())
|
|
|
|
updates := map[string]interface{}{
|
|
"data_usage_mb": 0,
|
|
"last_reset_at": now,
|
|
"next_reset_at": nextReset,
|
|
"status": constants.PackageUsageStatusActive,
|
|
}
|
|
|
|
if err := tx.Model(&model.PackageUsage{}).
|
|
Where("id IN ?", packageIDs).
|
|
Updates(updates).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "批量重置年流量失败")
|
|
}
|
|
|
|
s.logger.Info("年流量重置完成",
|
|
zap.Int("count", len(packages)),
|
|
zap.Time("next_reset_at", nextReset))
|
|
|
|
resetPackages = packages
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.triggerResumeForPackages(ctx, resetPackages)
|
|
return nil
|
|
}
|
|
|
|
// triggerResumeForPackages 对重置后的套餐,遍历其载体并触发自动复机
|
|
// 流量重置后,因流量耗尽而停机的卡满足复机条件,此处异步触发避免阻塞重置流程
|
|
func (s *ResetService) triggerResumeForPackages(ctx context.Context, packages []*model.PackageUsage) {
|
|
if s.resumeCallback == nil || len(packages) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
var carrierType string
|
|
var carrierID uint
|
|
|
|
if pkg.DeviceID > 0 {
|
|
carrierType = "device"
|
|
carrierID = pkg.DeviceID
|
|
} else if pkg.IotCardID > 0 {
|
|
carrierType = "iot_card"
|
|
carrierID = pkg.IotCardID
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
go func(ct string, cid uint) {
|
|
if err := s.resumeCallback.ResumeCardIfStopped(context.Background(), ct, cid); err != nil {
|
|
s.logger.Warn("流量重置后自动复机失败",
|
|
zap.String("carrier_type", ct),
|
|
zap.Uint("carrier_id", cid),
|
|
zap.Error(err))
|
|
}
|
|
}(carrierType, carrierID)
|
|
}
|
|
}
|