All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入 - 实现套餐流量重置服务(日/月/年周期重置) - 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑 - 新增订单创建幂等性防重(Redis 业务键 + 分布式锁) - 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求 - 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南) - 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录 - 新增 queue types 抽象和 Redis 常量定义
225 lines
6.6 KiB
Go
225 lines
6.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
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
// 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()
|
||
|
||
return 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
|
||
}
|
||
|
||
// 计算下次重置时间(明天 00:00:00)
|
||
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))
|
||
|
||
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()
|
||
|
||
return 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 {
|
||
// 查询套餐信息,获取 calendar_type
|
||
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
|
||
}
|
||
|
||
// 计算下次重置时间(基于套餐周期类型)
|
||
// 自然月套餐:每月1号重置
|
||
// 按天套餐:每30天重置
|
||
activatedAt := usage.ActivatedAt
|
||
if activatedAt.IsZero() {
|
||
activatedAt = now // 兜底处理
|
||
}
|
||
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))
|
||
}
|
||
|
||
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()
|
||
|
||
return 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
|
||
}
|
||
|
||
// 计算下次重置时间(明年 1月1日 00:00:00)
|
||
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))
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|