168 lines
6.8 KiB
Go
168 lines
6.8 KiB
Go
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 = ¤tExpiry
|
||
return []*model.PackageUsage{u}
|
||
}(), wantStatus: constants.PackageExpiryEstimateStatusExact, wantDate: ¤tExpiry, wantDays: intPointer(8),
|
||
},
|
||
{
|
||
name: "多段队列按下一时刻接续", usages: func() []*model.PackageUsage {
|
||
current := newUsage(1, constants.PackageUsageStatusActive, 1)
|
||
current.ExpiresAt = ¤tExpiry
|
||
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 = ¤tExpiry
|
||
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 = ¤tExpiry
|
||
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 = ¤tExpiry
|
||
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 }
|