All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
**变更说明**: - 删除所有 *_test.go 文件(单元测试、集成测试、验收测试、流程测试) - 删除整个 tests/ 目录 - 更新 CLAUDE.md:用"测试禁令"章节替换所有测试要求 - 删除测试生成 Skill (openspec-generate-acceptance-tests) - 删除测试生成命令 (opsx:gen-tests) - 更新 tasks.md:删除所有测试相关任务 **新规范**: - ❌ 禁止编写任何形式的自动化测试 - ❌ 禁止创建 *_test.go 文件 - ❌ 禁止在任务中包含测试相关工作 - ✅ 仅当用户明确要求时才编写测试 **原因**: 业务系统的正确性通过人工验证和生产环境监控保证,测试代码维护成本高于价值。 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
243 lines
7.1 KiB
Go
243 lines
7.1 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
|
||
}
|
||
|
||
// 按套餐分组处理(因为需要区分联通27号 vs 其他1号)
|
||
for _, pkg := range packages {
|
||
// 查询运营商信息以确定计费日
|
||
// 只有单卡套餐才根据运营商判断,设备级套餐统一使用1号计费
|
||
billingDay := 1
|
||
if pkg.IotCardID != 0 {
|
||
var card model.IotCard
|
||
if err := tx.First(&card, pkg.IotCardID).Error; err == nil {
|
||
var carrier model.Carrier
|
||
if err := tx.First(&carrier, card.CarrierID).Error; err == nil {
|
||
if carrier.CarrierType == "CUCC" {
|
||
billingDay = 27
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 设备级套餐默认使用1号计费(已在 billingDay := 1 初始化)
|
||
|
||
// 计算下次重置时间
|
||
nextReset := calculateNextMonthlyResetTime(now, billingDay)
|
||
|
||
// 更新套餐
|
||
updates := map[string]interface{}{
|
||
"data_usage_mb": 0,
|
||
"last_reset_at": now,
|
||
"next_reset_at": nextReset,
|
||
"status": constants.PackageUsageStatusActive, // 重置后恢复为生效中
|
||
}
|
||
|
||
if err := tx.Model(pkg).Updates(updates).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "重置月流量失败")
|
||
}
|
||
|
||
s.logger.Info("月流量已重置",
|
||
zap.Uint("usage_id", pkg.ID),
|
||
zap.Int("billing_day", billingDay),
|
||
zap.Time("next_reset_at", nextReset))
|
||
}
|
||
|
||
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
|
||
})
|
||
}
|
||
|
||
// calculateNextMonthlyResetTime 计算下次月重置时间
|
||
func calculateNextMonthlyResetTime(now time.Time, billingDay int) time.Time {
|
||
currentDay := now.Day()
|
||
targetMonth := now.Month()
|
||
targetYear := now.Year()
|
||
|
||
// 如果当前日期 >= 计费日,下次重置是下月计费日
|
||
if currentDay >= billingDay {
|
||
targetMonth++
|
||
if targetMonth > 12 {
|
||
targetMonth = 1
|
||
targetYear++
|
||
}
|
||
}
|
||
|
||
// 处理月末天数不足的情况(例如2月没有27日)
|
||
maxDay := time.Date(targetYear, targetMonth+1, 0, 0, 0, 0, 0, now.Location()).Day()
|
||
if billingDay > maxDay {
|
||
billingDay = maxDay
|
||
}
|
||
|
||
return time.Date(targetYear, targetMonth, billingDay, 0, 0, 0, 0, now.Location())
|
||
}
|