Files
junhong_cmp_fiber/internal/query/packageexpiry/query_test.go

168 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package packageexpiry
import (
"context"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/testutil"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"gorm.io/gorm"
)
func TestCalculate(t *testing.T) {
location := time.FixedZone("Asia/Shanghai", 8*60*60)
now := time.Date(2026, 7, 23, 12, 0, 0, 0, location)
currentExpiry := time.Date(2026, 7, 31, 23, 59, 59, 0, location)
createdAt := time.Date(2026, 7, 1, 12, 0, 0, 0, location)
newUsage := func(id uint, status int, priority int) *model.PackageUsage {
return &model.PackageUsage{
Model: modelBase(id, createdAt.Add(time.Duration(id)*time.Second)),
PackageID: id,
Status: status,
Priority: priority,
ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation,
CalendarTypeSnapshot: constants.PackageCalendarTypeByDay,
DurationDaysSnapshot: 3,
}
}
tests := []struct {
name string
usages []*model.PackageUsage
packages map[uint]*model.Package
wantStatus string
wantDate *time.Time
wantDays *int
}{
{name: "无套餐", wantStatus: constants.PackageExpiryEstimateStatusNone},
{
name: "当前套餐", usages: func() []*model.PackageUsage {
u := newUsage(1, constants.PackageUsageStatusActive, 1)
u.ExpiresAt = &currentExpiry
return []*model.PackageUsage{u}
}(), wantStatus: constants.PackageExpiryEstimateStatusExact, wantDate: &currentExpiry, wantDays: intPointer(8),
},
{
name: "多段队列按下一时刻接续", usages: func() []*model.PackageUsage {
current := newUsage(1, constants.PackageUsageStatusActive, 1)
current.ExpiresAt = &currentExpiry
queued := newUsage(2, constants.PackageUsageStatusPending, 2)
queued.DurationDaysSnapshot = 3
return []*model.PackageUsage{current, queued}
}(), wantStatus: constants.PackageExpiryEstimateStatusExact,
wantDate: timePointer(time.Date(2026, 8, 4, 23, 59, 59, 0, location)), wantDays: intPointer(12),
},
{
name: "自然月月末", usages: func() []*model.PackageUsage {
current := newUsage(1, constants.PackageUsageStatusActive, 1)
current.ExpiresAt = &currentExpiry
queued := newUsage(2, constants.PackageUsageStatusPending, 2)
queued.CalendarTypeSnapshot = constants.PackageCalendarTypeNaturalMonth
queued.DurationDaysSnapshot = 0
queued.DurationMonthsSnapshot = 1
return []*model.PackageUsage{current, queued}
}(), wantStatus: constants.PackageExpiryEstimateStatusExact,
wantDate: timePointer(time.Date(2026, 9, 30, 23, 59, 59, 0, location)), wantDays: intPointer(69),
},
{
name: "等待实名激活", usages: func() []*model.PackageUsage {
u := newUsage(1, constants.PackageUsageStatusPending, 1)
u.PendingRealnameActivation = true
return []*model.PackageUsage{u}
}(), wantStatus: constants.PackageExpiryEstimateStatusWaitingActivation,
},
{
name: "历史空快照回退", usages: func() []*model.PackageUsage {
current := newUsage(1, constants.PackageUsageStatusActive, 1)
current.ExpiresAt = &currentExpiry
queued := newUsage(2, constants.PackageUsageStatusPending, 2)
queued.ExpiryBaseSnapshot, queued.CalendarTypeSnapshot, queued.DurationDaysSnapshot = "", "", 0
return []*model.PackageUsage{current, queued}
}(), packages: map[uint]*model.Package{2: {ExpiryBase: constants.PackageExpiryBaseFromActivation, CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 3}},
wantStatus: constants.PackageExpiryEstimateStatusExact,
},
{
name: "非法快照", usages: func() []*model.PackageUsage {
current := newUsage(1, constants.PackageUsageStatusActive, 1)
current.ExpiresAt = &currentExpiry
queued := newUsage(2, constants.PackageUsageStatusPending, 2)
queued.CalendarTypeSnapshot = "bad"
return []*model.PackageUsage{current, queued}
}(), wantStatus: constants.PackageExpiryEstimateStatusInvalidData,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Calculate(tt.usages, tt.packages, now)
if got.ExpiryEstimateStatus != tt.wantStatus {
t.Fatalf("状态错误want=%s got=%s", tt.wantStatus, got.ExpiryEstimateStatus)
}
if tt.wantDate != nil && (got.EstimatedFinalExpiresAt == nil || !got.EstimatedFinalExpiresAt.Equal(*tt.wantDate)) {
t.Fatalf("到期时间错误want=%v got=%v", tt.wantDate, got.EstimatedFinalExpiresAt)
}
if tt.wantDays != nil && (got.DaysUntilFinalExpiry == nil || *got.DaysUntilFinalExpiry != *tt.wantDays) {
t.Fatalf("剩余天数错误want=%v got=%v", *tt.wantDays, got.DaysUntilFinalExpiry)
}
if tt.wantStatus != constants.PackageExpiryEstimateStatusExact && (got.EstimatedFinalExpiresAt != nil || got.DaysUntilFinalExpiry != nil) {
t.Fatalf("非精确状态必须返回 null%+v", got)
}
})
}
}
// TestQueryResolveBatchUsesOneBatchLoad 验证一页资产只通过批量读取计算,不逐卡查询套餐。
func TestQueryResolveBatchUsesOneBatchLoad(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
location := time.FixedZone("Asia/Shanghai", 8*60*60)
now := time.Date(2026, 7, 23, 12, 0, 0, 0, location)
assetIDs := make([]uint, 0, 100)
for i := 1; i <= 100; i++ {
assetID := uint(970000 + i)
assetIDs = append(assetIDs, assetID)
expiresAt := now.AddDate(0, 0, i%16)
usage := &model.PackageUsage{
OrderID: assetID,
OrderNo: "UR46-BATCH",
PackageID: assetID,
UsageType: constants.AssetTypeIotCard,
IotCardID: assetID,
DataLimitMB: 1,
Status: constants.PackageUsageStatusActive,
Priority: 1,
ActivatedAt: &now,
ExpiresAt: &expiresAt,
ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation,
CalendarTypeSnapshot: constants.PackageCalendarTypeByDay,
DurationDaysSnapshot: 30,
DurationMonthsSnapshot: 0,
}
if err := tx.Create(usage).Error; err != nil {
t.Fatalf("创建批量套餐使用记录失败:%v", err)
}
}
query := NewQuery(tx)
query.now = func() time.Time { return now }
results, err := query.ResolveBatch(context.Background(), constants.AssetTypeIotCard, assetIDs)
if err != nil {
t.Fatalf("批量查询失败:%v", err)
}
if len(results) != len(assetIDs) {
t.Fatalf("批量结果数量错误want=%d got=%d", len(assetIDs), len(results))
}
for _, assetID := range assetIDs {
if results[assetID].ExpiryEstimateStatus != constants.PackageExpiryEstimateStatusExact {
t.Fatalf("资产 %d 应得到精确结果:%+v", assetID, results[assetID])
}
}
}
func modelBase(id uint, createdAt time.Time) gorm.Model {
return gorm.Model{ID: id, CreatedAt: createdAt}
}
func intPointer(value int) *int { return &value }
func timePointer(value time.Time) *time.Time { return &value }