package threshold_activity import ( "context" "testing" "time" ) func TestDraw_AbortWhenParticipantsBelowMin(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activity := Activity{ ID: 11, Title: "开奖流产", Type: TypeDaily, QualificationMode: QualificationModeSpendOnly, MinParticipants: 2, StartTime: now.Add(-2 * time.Hour), EndTime: now.Add(2 * time.Hour), DrawTime: now.Add(-time.Minute), Status: StatusActive, } mustInsertActivity(t, db, activity) mustExec(t, db, `INSERT INTO threshold_activity_participants (activity_id, user_id, period_key, qualification_source, paid_amount_snapshot, effective_invite_count_snapshot, created_at) VALUES (11, 101, '2026-06-09', 'spend', 1000, 0, ?)`, now) if err := svc.Draw(ctx, activity.ID); err != nil { t.Fatalf("Draw failed: %v", err) } var updated Activity if err := db.Where("id = ?", activity.ID).First(&updated).Error; err != nil { t.Fatalf("query activity failed: %v", err) } if updated.Status != StatusAborted { t.Fatalf("expected status aborted, got %s", updated.Status) } if updated.AbortReason != "min_participants_not_met" { t.Fatalf("expected abort reason min_participants_not_met, got %s", updated.AbortReason) } var winnerCount int64 if err := db.Table("threshold_activity_winners").Where("activity_id = ?", activity.ID).Count(&winnerCount).Error; err != nil { t.Fatalf("count winners failed: %v", err) } if winnerCount != 0 { t.Fatalf("expected no winners when aborted, got %d", winnerCount) } } func TestDraw_FinishWhenParticipantsReachMinCreatesWinner(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activity := Activity{ ID: 12, Title: "正常开奖", Type: TypeDaily, QualificationMode: QualificationModeSpendOnly, MinParticipants: 1, StartTime: now.Add(-2 * time.Hour), EndTime: now.Add(2 * time.Hour), DrawTime: now.Add(-time.Minute), Status: StatusActive, } mustInsertActivity(t, db, activity) mustExec(t, db, `INSERT INTO threshold_activity_participants (activity_id, user_id, period_key, qualification_source, paid_amount_snapshot, effective_invite_count_snapshot, created_at) VALUES (12, 201, '2026-06-09', 'spend', 2000, 0, ?)`, now) mustExec(t, db, `INSERT INTO products (id, name, price, cost_price, stock, images_json) VALUES (501, '测试商品', 1999, 888, 5, '[]')`) mustExec(t, db, `INSERT INTO threshold_activity_prizes (id, activity_id, reward_type, reward_ref_id, reward_name_snapshot, reward_image_snapshot, reward_value_snapshot_cents, cost_snapshot_cents, quantity, remaining_quantity, sort, created_at, updated_at) VALUES (601, 12, 'product', 501, '测试商品', '', 1999, 888, 1, 1, 1, ?, ?)`, now, now) if err := svc.Draw(ctx, activity.ID); err != nil { t.Fatalf("Draw failed: %v", err) } var updated Activity if err := db.Where("id = ?", activity.ID).First(&updated).Error; err != nil { t.Fatalf("query activity failed: %v", err) } if updated.Status != StatusFinished { t.Fatalf("expected status finished, got %s", updated.Status) } if updated.DrawBatch == "" { t.Fatalf("expected draw batch to be set") } var winnerCount int64 if err := db.Table("threshold_activity_winners").Where("activity_id = ?", activity.ID).Count(&winnerCount).Error; err != nil { t.Fatalf("count winners failed: %v", err) } if winnerCount != 1 { t.Fatalf("expected 1 winner, got %d", winnerCount) } var prize Prize if err := db.Where("id = ?", 601).First(&prize).Error; err != nil { t.Fatalf("query prize failed: %v", err) } if prize.RemainingQuantity != 0 { t.Fatalf("expected remaining quantity 0, got %d", prize.RemainingQuantity) } var stock int64 if err := db.Table("products").Select("stock").Where("id = ?", 501).Scan(&stock).Error; err != nil { t.Fatalf("query stock failed: %v", err) } if stock != 4 { t.Fatalf("expected product stock 4, got %d", stock) } } func TestDrawDueActivities_ProcessesOnlyDueActivities(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() due := Activity{ ID: 21, Title: "到期活动", Type: TypeDaily, QualificationMode: QualificationModeSpendOnly, MinParticipants: 2, StartTime: now.Add(-2 * time.Hour), EndTime: now.Add(2 * time.Hour), DrawTime: now.Add(-time.Minute), Status: StatusActive, } future := Activity{ ID: 22, Title: "未到期活动", Type: TypeDaily, QualificationMode: QualificationModeSpendOnly, MinParticipants: 1, StartTime: now.Add(-2 * time.Hour), EndTime: now.Add(2 * time.Hour), DrawTime: now.Add(time.Hour), Status: StatusActive, } mustInsertActivity(t, db, due) mustInsertActivity(t, db, future) mustExec(t, db, `INSERT INTO threshold_activity_participants (activity_id, user_id, period_key, qualification_source, paid_amount_snapshot, effective_invite_count_snapshot, created_at) VALUES (21, 301, '2026-06-09', 'spend', 1000, 0, ?)`, now) if err := svc.DrawDueActivities(ctx); err != nil { t.Fatalf("DrawDueActivities failed: %v", err) } var dueUpdated, futureUpdated Activity if err := db.Where("id = ?", 21).First(&dueUpdated).Error; err != nil { t.Fatalf("query due activity failed: %v", err) } if err := db.Where("id = ?", 22).First(&futureUpdated).Error; err != nil { t.Fatalf("query future activity failed: %v", err) } if dueUpdated.Status != StatusAborted { t.Fatalf("expected due activity aborted, got %s", dueUpdated.Status) } if futureUpdated.Status != StatusActive { t.Fatalf("expected future activity remain active, got %s", futureUpdated.Status) } }