修复活动门槛统计使用自定义时间范围
This commit is contained in:
parent
f8c4e17ccc
commit
9f96f235e7
@ -204,14 +204,14 @@ func (s *service) buildActivityDetail(ctx context.Context, item Activity, userID
|
|||||||
detail.Winners = winners.List
|
detail.Winners = winners.List
|
||||||
}
|
}
|
||||||
if userID > 0 {
|
if userID > 0 {
|
||||||
progress, period, err := s.evaluateQualification(ctx, item, userID, time.Now())
|
progress, _, err := s.evaluateQualification(ctx, item, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
detail.QualificationProgress = progress
|
detail.QualificationProgress = progress
|
||||||
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && qualificationSatisfied(item, progress)
|
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && qualificationSatisfied(item, progress)
|
||||||
var count int64
|
var count int64
|
||||||
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ? AND period_key = ?", item.ID, userID, period).Count(&count)
|
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ?", item.ID, userID).Count(&count)
|
||||||
detail.Joined = count > 0
|
detail.Joined = count > 0
|
||||||
if detail.Joined {
|
if detail.Joined {
|
||||||
detail.CanJoin = false
|
detail.CanJoin = false
|
||||||
|
|||||||
@ -22,7 +22,14 @@ func (s *service) Join(ctx context.Context, activityID int64, userID int64) erro
|
|||||||
}
|
}
|
||||||
return errors.New("当前不在活动参与时间内")
|
return errors.New("当前不在活动参与时间内")
|
||||||
}
|
}
|
||||||
progress, period, err := s.evaluateQualification(ctx, activity, userID, now)
|
var existing int64
|
||||||
|
if err := s.repo.GetDbW().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ?", activityID, userID).Count(&existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existing > 0 {
|
||||||
|
return errors.New("已参与该活动")
|
||||||
|
}
|
||||||
|
progress, period, err := s.evaluateQualification(ctx, activity, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -78,12 +85,12 @@ func (s *service) ListJoinableActivitiesForUser(ctx context.Context, userID int6
|
|||||||
}
|
}
|
||||||
items := make([]JoinableActivityItem, 0, len(activities))
|
items := make([]JoinableActivityItem, 0, len(activities))
|
||||||
for _, activity := range activities {
|
for _, activity := range activities {
|
||||||
progress, period, err := s.evaluateQualification(ctx, activity, userID, now)
|
progress, _, err := s.evaluateQualification(ctx, activity, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var count int64
|
var count int64
|
||||||
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ? AND period_key = ?", activity.ID, userID, period).Count(&count)
|
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ?", activity.ID, userID).Count(&count)
|
||||||
joined := count > 0
|
joined := count > 0
|
||||||
canJoin := !joined && qualificationSatisfied(activity, progress)
|
canJoin := !joined && qualificationSatisfied(activity, progress)
|
||||||
items = append(items, JoinableActivityItem{
|
items = append(items, JoinableActivityItem{
|
||||||
@ -106,8 +113,8 @@ func (s *service) ListJoinableActivitiesForUser(ctx context.Context, userID int6
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) evaluateQualification(ctx context.Context, activity Activity, userID int64, now time.Time) (QualificationProgress, string, error) {
|
func (s *service) evaluateQualification(ctx context.Context, activity Activity, userID int64) (QualificationProgress, string, error) {
|
||||||
start, end, period := periodRange(activity.Type, now)
|
start, end, period := activityStatsRange(activity)
|
||||||
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return QualificationProgress{}, "", err
|
return QualificationProgress{}, "", err
|
||||||
@ -196,24 +203,16 @@ func (s *service) countEffectiveInvites(ctx context.Context, inviterID int64, ac
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func periodRange(activityType string, now time.Time) (time.Time, time.Time, string) {
|
func activityStatsRange(activity Activity) (time.Time, time.Time, string) {
|
||||||
loc := now.Location()
|
return activity.StartTime, exclusiveEndTime(activity.EndTime), activityPeriodKey(activity)
|
||||||
switch activityType {
|
}
|
||||||
case TypeWeekly:
|
|
||||||
dayOffset := (int(now.Weekday()) + 6) % 7
|
func exclusiveEndTime(end time.Time) time.Time {
|
||||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -dayOffset)
|
return end.Add(time.Second)
|
||||||
end := start.AddDate(0, 0, 7)
|
}
|
||||||
y, w := start.ISOWeek()
|
|
||||||
return start, end, fmt.Sprintf("%04d-W%02d", y, w)
|
func activityPeriodKey(activity Activity) string {
|
||||||
case TypeMonthly:
|
return fmt.Sprintf("%016x", uint64(activity.ID))
|
||||||
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
|
|
||||||
end := start.AddDate(0, 1, 0)
|
|
||||||
return start, end, start.Format("2006-01")
|
|
||||||
default:
|
|
||||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
|
||||||
end := start.AddDate(0, 0, 1)
|
|
||||||
return start, end, start.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func maxInt64(a, b int64) int64 {
|
func maxInt64(a, b int64) int64 {
|
||||||
|
|||||||
@ -11,7 +11,8 @@ func TestCountEffectiveInvites_OnlyCountsInvitesCreatedAfterActivityStart(t *tes
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
activityStart := now.Add(-2 * time.Hour)
|
activityStart := now.Add(-2 * time.Hour)
|
||||||
periodStart, periodEnd, _ := periodRange(TypeDaily, now)
|
periodStart := activityStart
|
||||||
|
periodEnd := now.Add(time.Hour)
|
||||||
|
|
||||||
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 2, 'INV1', ?, ?)`, activityStart.Add(-time.Hour), activityStart.Add(-time.Hour))
|
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 2, 'INV1', ?, ?)`, activityStart.Add(-time.Hour), activityStart.Add(-time.Hour))
|
||||||
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 3, 'INV1', ?, ?)`, activityStart.Add(10*time.Minute), activityStart.Add(10*time.Minute))
|
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 3, 'INV1', ?, ?)`, activityStart.Add(10*time.Minute), activityStart.Add(10*time.Minute))
|
||||||
@ -27,6 +28,73 @@ func TestCountEffectiveInvites_OnlyCountsInvitesCreatedAfterActivityStart(t *tes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEvaluateQualification_UsesConfiguredActivityTimeRangeForSpend(t *testing.T) {
|
||||||
|
svc, _, db := newThresholdTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Date(2026, 6, 10, 12, 0, 0, 0, time.Local)
|
||||||
|
activity := Activity{
|
||||||
|
ID: 4,
|
||||||
|
Title: "自定义消费时间",
|
||||||
|
Type: TypeMonthly,
|
||||||
|
QualificationMode: QualificationModeSpendOnly,
|
||||||
|
SpendThresholdAmount: 1000,
|
||||||
|
MinParticipants: 1,
|
||||||
|
StartTime: now.Add(-time.Hour),
|
||||||
|
EndTime: now.Add(time.Hour),
|
||||||
|
DrawTime: now.Add(2 * time.Hour),
|
||||||
|
Status: StatusActive,
|
||||||
|
}
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_BEFORE', 1, 5000, 5000, 2, ?, ?, ?, ?)`, activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), time.Unix(0, 0))
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_INSIDE', 1, 1000, 1000, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0))
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_AFTER', 1, 7000, 7000, 2, ?, ?, ?, ?)`, activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), time.Unix(0, 0))
|
||||||
|
|
||||||
|
progress, _, err := svc.evaluateQualification(ctx, activity, 40)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("evaluateQualification failed: %v", err)
|
||||||
|
}
|
||||||
|
if progress.CurrentPaid != 1000 {
|
||||||
|
t.Fatalf("expected only configured-range spend 1000, got %d", progress.CurrentPaid)
|
||||||
|
}
|
||||||
|
if !progress.SpendQualified {
|
||||||
|
t.Fatalf("expected spend to be qualified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluateQualification_UsesConfiguredActivityTimeRangeForInvites(t *testing.T) {
|
||||||
|
svc, _, db := newThresholdTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Date(2026, 6, 10, 12, 0, 0, 0, time.Local)
|
||||||
|
activity := Activity{
|
||||||
|
ID: 5,
|
||||||
|
Title: "自定义邀请时间",
|
||||||
|
Type: TypeMonthly,
|
||||||
|
QualificationMode: QualificationModeInviteOnly,
|
||||||
|
InviteThresholdCount: 1,
|
||||||
|
InviteEffectiveAmount: 1000,
|
||||||
|
MinParticipants: 1,
|
||||||
|
StartTime: now.Add(-time.Hour),
|
||||||
|
EndTime: now.Add(time.Hour),
|
||||||
|
DrawTime: now.Add(2 * time.Hour),
|
||||||
|
Status: StatusActive,
|
||||||
|
}
|
||||||
|
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (50, 51, 'INV50', ?, ?)`, activity.StartTime.Add(time.Minute), activity.StartTime.Add(time.Minute))
|
||||||
|
mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (50, 52, 'INV50', ?, ?)`, activity.StartTime.Add(time.Minute), activity.StartTime.Add(time.Minute))
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (51, 'INV_BEFORE', 1, 1500, 1500, 2, ?, ?, ?, ?)`, activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), time.Unix(0, 0))
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (51, 'INV_AFTER', 1, 1500, 1500, 2, ?, ?, ?, ?)`, activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), time.Unix(0, 0))
|
||||||
|
mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (52, 'INV_INSIDE', 1, 1200, 1200, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0))
|
||||||
|
|
||||||
|
progress, _, err := svc.evaluateQualification(ctx, activity, 50)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("evaluateQualification failed: %v", err)
|
||||||
|
}
|
||||||
|
if progress.EffectiveInviteCount != 1 {
|
||||||
|
t.Fatalf("expected only one effective invite in configured range, got %d", progress.EffectiveInviteCount)
|
||||||
|
}
|
||||||
|
if !progress.InviteQualified {
|
||||||
|
t.Fatalf("expected invite to be qualified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestJoin_SpendOnlyQualifiedWritesParticipant(t *testing.T) {
|
func TestJoin_SpendOnlyQualifiedWritesParticipant(t *testing.T) {
|
||||||
svc, _, db := newThresholdTestService(t)
|
svc, _, db := newThresholdTestService(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@ -189,12 +189,15 @@ func (s *service) buildActivityDetail(ctx context.Context, item Activity, userID
|
|||||||
detail.Winners = winners.List
|
detail.Winners = winners.List
|
||||||
}
|
}
|
||||||
if userID > 0 {
|
if userID > 0 {
|
||||||
start, end, period := periodRange(item.Type, time.Now())
|
start, end, _ := activityStatsRange(item)
|
||||||
detail.CurrentPaid, _ = s.sumPaidAmount(ctx, userID, start, end)
|
detail.CurrentPaid, _ = s.sumPaidAmount(ctx, userID, start, end)
|
||||||
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && detail.CurrentPaid >= item.ThresholdAmount
|
detail.CanJoin = isJoinWindowOpen(item, time.Now()) && detail.CurrentPaid >= item.ThresholdAmount
|
||||||
var count int64
|
var count int64
|
||||||
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ? AND period_key = ?", item.ID, userID, period).Count(&count)
|
s.repo.GetDbR().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ?", item.ID, userID).Count(&count)
|
||||||
detail.Joined = count > 0
|
detail.Joined = count > 0
|
||||||
|
if detail.Joined {
|
||||||
|
detail.CanJoin = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return detail, nil
|
return detail, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,14 @@ func (s *service) Join(ctx context.Context, activityID int64, userID int64) erro
|
|||||||
}
|
}
|
||||||
return errors.New("当前不在活动参与时间内")
|
return errors.New("当前不在活动参与时间内")
|
||||||
}
|
}
|
||||||
start, end, period := periodRange(activity.Type, now)
|
var existing int64
|
||||||
|
if err := s.repo.GetDbW().WithContext(ctx).Model(&Participant{}).Where("activity_id = ? AND user_id = ?", activityID, userID).Count(&existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existing > 0 {
|
||||||
|
return errors.New("已参与该活动")
|
||||||
|
}
|
||||||
|
start, end, period := activityStatsRange(activity)
|
||||||
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -72,22 +79,14 @@ func (s *service) sumPaidAmount(ctx context.Context, userID int64, start time.Ti
|
|||||||
return total, err
|
return total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func periodRange(activityType string, now time.Time) (time.Time, time.Time, string) {
|
func activityStatsRange(activity Activity) (time.Time, time.Time, string) {
|
||||||
loc := now.Location()
|
return activity.StartTime, exclusiveEndTime(activity.EndTime), activityPeriodKey(activity)
|
||||||
switch activityType {
|
}
|
||||||
case TypeWeekly:
|
|
||||||
dayOffset := (int(now.Weekday()) + 6) % 7
|
func exclusiveEndTime(end time.Time) time.Time {
|
||||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -dayOffset)
|
return end.Add(time.Second)
|
||||||
end := start.AddDate(0, 0, 7)
|
}
|
||||||
y, w := start.ISOWeek()
|
|
||||||
return start, end, fmt.Sprintf("%04d-W%02d", y, w)
|
func activityPeriodKey(activity Activity) string {
|
||||||
case TypeMonthly:
|
return fmt.Sprintf("%016x", uint64(activity.ID))
|
||||||
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
|
|
||||||
end := start.AddDate(0, 1, 0)
|
|
||||||
return start, end, start.Format("2006-01")
|
|
||||||
default:
|
|
||||||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
|
||||||
end := start.AddDate(0, 0, 1)
|
|
||||||
return start, end, start.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
132
internal/service/welfare_activity/participant_test.go
Normal file
132
internal/service/welfare_activity/participant_test.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package welfare_activity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJoin_UsesConfiguredActivityTimeRangeForSpend(t *testing.T) {
|
||||||
|
svc, _, db := newWelfareTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Now().Truncate(time.Second)
|
||||||
|
activity := Activity{
|
||||||
|
ID: 1,
|
||||||
|
Title: "自定义福利时间",
|
||||||
|
Type: TypeMonthly,
|
||||||
|
ThresholdAmount: 1000,
|
||||||
|
StartTime: now.Add(-time.Hour),
|
||||||
|
EndTime: now.Add(time.Hour),
|
||||||
|
DrawTime: now.Add(2 * time.Hour),
|
||||||
|
Status: StatusActive,
|
||||||
|
}
|
||||||
|
mustInsertWelfareActivity(t, db, activity)
|
||||||
|
mustExecWelfare(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (10, 'WELFARE_BEFORE', 1, 5000, 5000, 2, ?, ?, ?, ?)`, activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), time.Unix(0, 0))
|
||||||
|
mustExecWelfare(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (10, 'WELFARE_INSIDE', 1, 1000, 1000, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0))
|
||||||
|
mustExecWelfare(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (10, 'WELFARE_AFTER', 1, 7000, 7000, 2, ?, ?, ?, ?)`, activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), time.Unix(0, 0))
|
||||||
|
|
||||||
|
if err := svc.Join(ctx, activity.ID, 10); err != nil {
|
||||||
|
t.Fatalf("Join failed: %v", err)
|
||||||
|
}
|
||||||
|
var participant Participant
|
||||||
|
if err := db.Where("activity_id = ? AND user_id = ?", activity.ID, 10).First(&participant).Error; err != nil {
|
||||||
|
t.Fatalf("query participant failed: %v", err)
|
||||||
|
}
|
||||||
|
if participant.PaidAmountSnapshot != 1000 {
|
||||||
|
t.Fatalf("expected only configured-range spend 1000, got %d", participant.PaidAmountSnapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWelfareTestService(t *testing.T) (*service, mysql.Repo, *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
repo, err := mysql.NewSQLiteRepoForTest()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sqlite repo failed: %v", err)
|
||||||
|
}
|
||||||
|
db := repo.GetDbW()
|
||||||
|
initWelfareTestTables(t, db)
|
||||||
|
return &service{repo: repo}, repo, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func initWelfareTestTables(t *testing.T, db *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE welfare_activities (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
threshold_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME NOT NULL,
|
||||||
|
draw_time DATETIME NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
description TEXT,
|
||||||
|
cover_image TEXT,
|
||||||
|
draw_batch TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME
|
||||||
|
);`,
|
||||||
|
`CREATE TABLE welfare_activity_participants (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
activity_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
period_key TEXT NOT NULL,
|
||||||
|
paid_amount_snapshot INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(activity_id, user_id, period_key)
|
||||||
|
);`,
|
||||||
|
`CREATE TABLE orders (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
order_no TEXT NOT NULL,
|
||||||
|
source_type INTEGER NOT NULL DEFAULT 1,
|
||||||
|
total_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
discount_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
points_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
actual_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
status INTEGER NOT NULL DEFAULT 1,
|
||||||
|
pay_preorder_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
paid_at DATETIME,
|
||||||
|
cancelled_at DATETIME,
|
||||||
|
user_address_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_consumed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
points_ledger_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
coupon_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
item_card_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
remark TEXT,
|
||||||
|
ext_order_id TEXT NOT NULL DEFAULT ''
|
||||||
|
);`,
|
||||||
|
}
|
||||||
|
for _, stmt := range stmts {
|
||||||
|
if err := db.Exec(stmt).Error; err != nil {
|
||||||
|
t.Fatalf("create table failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustExecWelfare(t *testing.T, db *gorm.DB, sql string, args ...any) {
|
||||||
|
t.Helper()
|
||||||
|
if err := db.Exec(sql, args...).Error; err != nil {
|
||||||
|
t.Fatalf("exec failed: %v, sql=%s", err, sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustInsertWelfareActivity(t *testing.T, db *gorm.DB, activity Activity) {
|
||||||
|
t.Helper()
|
||||||
|
if activity.CreatedAt.IsZero() {
|
||||||
|
activity.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if activity.UpdatedAt.IsZero() {
|
||||||
|
activity.UpdatedAt = activity.CreatedAt
|
||||||
|
}
|
||||||
|
if err := db.Create(&activity).Error; err != nil {
|
||||||
|
t.Fatalf("insert activity failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,21 +15,6 @@ func (s *service) ListJoinableActivitiesForUser(ctx context.Context, userID int6
|
|||||||
return []JoinableActivityItem{}, nil
|
return []JoinableActivityItem{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
paidByType := map[string]int64{}
|
|
||||||
periodByType := map[string]string{}
|
|
||||||
for _, activity := range activities {
|
|
||||||
if _, ok := paidByType[activity.Type]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
start, end, period := periodRange(activity.Type, now)
|
|
||||||
paid, err := s.sumPaidAmount(ctx, userID, start, end)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
paidByType[activity.Type] = paid
|
|
||||||
periodByType[activity.Type] = period
|
|
||||||
}
|
|
||||||
|
|
||||||
activityIDs := make([]int64, 0, len(activities))
|
activityIDs := make([]int64, 0, len(activities))
|
||||||
for _, activity := range activities {
|
for _, activity := range activities {
|
||||||
activityIDs = append(activityIDs, activity.ID)
|
activityIDs = append(activityIDs, activity.ID)
|
||||||
@ -38,19 +23,19 @@ func (s *service) ListJoinableActivitiesForUser(ctx context.Context, userID int6
|
|||||||
if err := s.repo.GetDbR().WithContext(ctx).Where("user_id = ? AND activity_id IN ?", userID, activityIDs).Find(&participants).Error; err != nil {
|
if err := s.repo.GetDbR().WithContext(ctx).Where("user_id = ? AND activity_id IN ?", userID, activityIDs).Find(&participants).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
joinedMap := map[int64]map[string]bool{}
|
joinedMap := map[int64]bool{}
|
||||||
for _, participant := range participants {
|
for _, participant := range participants {
|
||||||
if _, ok := joinedMap[participant.ActivityID]; !ok {
|
joinedMap[participant.ActivityID] = true
|
||||||
joinedMap[participant.ActivityID] = map[string]bool{}
|
|
||||||
}
|
|
||||||
joinedMap[participant.ActivityID][participant.PeriodKey] = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]JoinableActivityItem, 0, len(activities))
|
items := make([]JoinableActivityItem, 0, len(activities))
|
||||||
for _, activity := range activities {
|
for _, activity := range activities {
|
||||||
currentPaid := paidByType[activity.Type]
|
start, end, _ := activityStatsRange(activity)
|
||||||
periodKey := periodByType[activity.Type]
|
currentPaid, err := s.sumPaidAmount(ctx, userID, start, end)
|
||||||
joined := joinedMap[activity.ID][periodKey]
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
joined := joinedMap[activity.ID]
|
||||||
canJoin := !joined && currentPaid >= activity.ThresholdAmount
|
canJoin := !joined && currentPaid >= activity.ThresholdAmount
|
||||||
items = append(items, JoinableActivityItem{
|
items = append(items, JoinableActivityItem{
|
||||||
ActivityID: activity.ID,
|
ActivityID: activity.ID,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user