236 lines
7.2 KiB
Go
236 lines
7.2 KiB
Go
package listingstale
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/database"
|
|
"hfb_sys/backend/internal/model"
|
|
"hfb_sys/backend/internal/modules/sms"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type fakeConfigReader struct {
|
|
value string
|
|
}
|
|
|
|
func (r *fakeConfigReader) FindValue(ctx context.Context, key string) (string, error) {
|
|
if key != sms.ConfigKey {
|
|
return "", gorm.ErrRecordNotFound
|
|
}
|
|
return r.value, nil
|
|
}
|
|
|
|
func staleConfig(t *testing.T, enabled bool, staleDays int) string {
|
|
t.Helper()
|
|
raw, err := sms.MarshalConfig(sms.Config{
|
|
Enabled: true,
|
|
Nodes: map[string]sms.NodeConfig{
|
|
sms.NodeListingStalePriceSuggestion: {
|
|
Enabled: enabled,
|
|
TemplateCode: "SMS_512620985",
|
|
StaleDays: staleDays,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("MarshalConfig() error = %v", err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func newStaleTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db := database.NewTestDB()
|
|
if err := database.AutoMigrateIfMissing(db, &model.SmsMessage{}, &model.RentalListing{}, &model.User{}); err != nil {
|
|
t.Fatalf("AutoMigrate() error = %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func seedListing(t *testing.T, db *gorm.DB, listing model.RentalListing) model.RentalListing {
|
|
t.Helper()
|
|
if err := db.Create(&listing).Error; err != nil {
|
|
t.Fatalf("创建商品失败: %v", err)
|
|
}
|
|
return listing
|
|
}
|
|
|
|
func TestRunOnceEnqueuesOnlyListingsJustCrossingThreshold(t *testing.T) {
|
|
db := newStaleTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
owner := model.User{Phone: "13900000001", Nickname: "号主"}
|
|
if err := db.Create(&owner).Error; err != nil {
|
|
t.Fatalf("创建用户失败: %v", err)
|
|
}
|
|
|
|
now := time.Now()
|
|
justStale := now.Add(-3*24*time.Hour - 6*time.Hour) // 刚跨过 3 天阈值,窗口内
|
|
longStale := now.Add(-10 * 24 * time.Hour) // 窗口外,功能上线前的历史滞销账号
|
|
fresh := now.Add(-24 * time.Hour) // 还没到阈值
|
|
staleAt := now.Add(-3*24*time.Hour - 6*time.Hour)
|
|
|
|
// 命中:上架满 3 天、仍在架、未成交。
|
|
hit := seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-HIT", OwnerID: owner.ID, Status: "published", PublishedAt: &justStale,
|
|
})
|
|
// 窗口外:上架过久,避免功能上线时批量轰炸。
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-OLD", OwnerID: owner.ID, Status: "published", PublishedAt: &longStale,
|
|
})
|
|
// 未到阈值。
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-FRESH", OwnerID: owner.ID, Status: "published", PublishedAt: &fresh,
|
|
})
|
|
// 已成交(在租)。
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-RENTED", OwnerID: owner.ID, Status: "rented", PublishedAt: &staleAt,
|
|
})
|
|
// 交易中。
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-INCHARGE", OwnerID: owner.ID, Status: "published", InTransaction: true, PublishedAt: &staleAt,
|
|
})
|
|
|
|
job := New(db, nil, zap.NewNop(), &fakeConfigReader{value: staleConfig(t, true, 3)})
|
|
|
|
count, err := job.RunOnce(ctx)
|
|
if err != nil {
|
|
t.Fatalf("RunOnce() error = %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("RunOnce() = %d, want 1", count)
|
|
}
|
|
|
|
var rows []model.SmsMessage
|
|
if err := db.Where("node = ?", sms.NodeListingStalePriceSuggestion).Find(&rows).Error; err != nil {
|
|
t.Fatalf("查询短信记录失败: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("短信记录数 = %d, want 1", len(rows))
|
|
}
|
|
if rows[0].BizID != hit.ID || rows[0].BizType != "listing" {
|
|
t.Fatalf("业务对象 = %s/%d, want listing/%d", rows[0].BizType, rows[0].BizID, hit.ID)
|
|
}
|
|
if rows[0].UserID != owner.ID || rows[0].Phone != owner.Phone {
|
|
t.Fatalf("接收人 = %d/%s, want %d/%s", rows[0].UserID, rows[0].Phone, owner.ID, owner.Phone)
|
|
}
|
|
|
|
// 再次执行应因唯一索引去重而不重复入队。
|
|
count, err = job.RunOnce(ctx)
|
|
if err != nil {
|
|
t.Fatalf("重复 RunOnce() error = %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("重复 RunOnce() = %d, want 0(同一账号只提醒一次)", count)
|
|
}
|
|
var total int64
|
|
if err := db.Model(&model.SmsMessage{}).Count(&total).Error; err != nil {
|
|
t.Fatalf("统计失败: %v", err)
|
|
}
|
|
if total != 1 {
|
|
t.Fatalf("短信记录总数 = %d, want 1", total)
|
|
}
|
|
}
|
|
|
|
// 同一候选窗口内的账号数超过批次上限时,已提醒过的账号不能继续占用批次名额,
|
|
// 否则排在后面的账号会在窗口结束后被永久漏发。
|
|
func TestRunOnceDoesNotStarveListingsBeyondBatchSize(t *testing.T) {
|
|
db := newStaleTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
owner := model.User{Phone: "13900000004"}
|
|
if err := db.Create(&owner).Error; err != nil {
|
|
t.Fatalf("创建用户失败: %v", err)
|
|
}
|
|
|
|
staleAt := time.Now().Add(-3*24*time.Hour - 6*time.Hour)
|
|
expected := map[uint64]bool{}
|
|
for _, no := range []string{"L-BATCH-1", "L-BATCH-2", "L-BATCH-3"} {
|
|
listing := seedListing(t, db, model.RentalListing{
|
|
ListingNo: no, OwnerID: owner.ID, Status: "published", PublishedAt: &staleAt,
|
|
})
|
|
expected[listing.ID] = true
|
|
}
|
|
|
|
job := New(db, nil, zap.NewNop(), &fakeConfigReader{value: staleConfig(t, true, 3)})
|
|
job.batchSize = 1
|
|
|
|
for round := 1; round <= len(expected); round++ {
|
|
count, err := job.RunOnce(ctx)
|
|
if err != nil {
|
|
t.Fatalf("第 %d 轮 RunOnce() error = %v", round, err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("第 %d 轮 RunOnce() = %d, want 1(已提醒的账号不应占满批次名额)", round, count)
|
|
}
|
|
}
|
|
|
|
var rows []model.SmsMessage
|
|
if err := db.Where("node = ?", sms.NodeListingStalePriceSuggestion).Find(&rows).Error; err != nil {
|
|
t.Fatalf("查询短信记录失败: %v", err)
|
|
}
|
|
if len(rows) != len(expected) {
|
|
t.Fatalf("短信记录数 = %d, want %d", len(rows), len(expected))
|
|
}
|
|
for _, row := range rows {
|
|
if !expected[row.BizID] {
|
|
t.Fatalf("出现未预期的业务对象 %d", row.BizID)
|
|
}
|
|
delete(expected, row.BizID)
|
|
}
|
|
if len(expected) != 0 {
|
|
t.Fatalf("仍有账号未被提醒: %v", expected)
|
|
}
|
|
}
|
|
|
|
func TestRunOnceSkipsWhenNodeInactive(t *testing.T) {
|
|
db := newStaleTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
owner := model.User{Phone: "13900000002"}
|
|
if err := db.Create(&owner).Error; err != nil {
|
|
t.Fatalf("创建用户失败: %v", err)
|
|
}
|
|
staleAt := time.Now().Add(-3*24*time.Hour - 6*time.Hour)
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-OFF", OwnerID: owner.ID, Status: "published", PublishedAt: &staleAt,
|
|
})
|
|
|
|
job := New(db, nil, zap.NewNop(), &fakeConfigReader{value: staleConfig(t, false, 3)})
|
|
count, err := job.RunOnce(ctx)
|
|
if err != nil {
|
|
t.Fatalf("RunOnce() error = %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("节点关闭时 RunOnce() = %d, want 0", count)
|
|
}
|
|
}
|
|
|
|
func TestRunOnceRespectsConfiguredStaleDays(t *testing.T) {
|
|
db := newStaleTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
owner := model.User{Phone: "13900000003"}
|
|
if err := db.Create(&owner).Error; err != nil {
|
|
t.Fatalf("创建用户失败: %v", err)
|
|
}
|
|
// 上架 3.5 天:阈值为 3 天时命中,阈值调成 5 天则不应命中。
|
|
staleAt := time.Now().Add(-3*24*time.Hour - 12*time.Hour)
|
|
seedListing(t, db, model.RentalListing{
|
|
ListingNo: "L-DAYS", OwnerID: owner.ID, Status: "published", PublishedAt: &staleAt,
|
|
})
|
|
|
|
job := New(db, nil, zap.NewNop(), &fakeConfigReader{value: staleConfig(t, true, 5)})
|
|
count, err := job.RunOnce(ctx)
|
|
if err != nil {
|
|
t.Fatalf("RunOnce() error = %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("阈值 5 天时 RunOnce() = %d, want 0", count)
|
|
}
|
|
}
|