package task_test import ( "context" "testing" "time" "google.golang.org/grpc" walletv1 "hyapp.local/api/proto/wallet/v1" taskdomain "hyapp/services/activity-service/internal/domain/task" taskservice "hyapp/services/activity-service/internal/service/task" "hyapp/services/activity-service/internal/testutil/mysqltest" ) // TestTaskEventQueryAndClaimFlow 覆盖任务定义、事件进度、App 查询、领奖和领奖幂等的主闭环。 func TestTaskEventQueryAndClaimFlow(t *testing.T) { repository := mysqltest.NewRepository(t) wallet := &fakeWalletClient{} svc := taskservice.New(repository, wallet) now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60)) svc.SetClock(func() time.Time { return now }) definition, created, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{ TaskType: taskdomain.TypeDaily, Category: "gift", MetricType: "gift_spend_coin", Title: "send gift", TargetValue: 100, TargetUnit: "coin", RewardCoinAmount: 10, Status: taskdomain.StatusActive, SortOrder: 1, OperatorAdminID: 90001, }) if err != nil || !created { t.Fatalf("create task definition failed: created=%v err=%v", created, err) } result, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{ EventID: "evt-gift-1", EventType: "WalletGiftDebited", SourceService: "wallet", UserID: 10001, MetricType: "gift_spend_coin", Value: 120, OccurredAtMS: now.UnixMilli(), }) if err != nil { t.Fatalf("ConsumeTaskEvent failed: %v", err) } if result.Status != taskdomain.EventStatusConsumed || result.MatchedTaskCount != 1 { t.Fatalf("event result mismatch: %+v", result) } duplicate, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{ EventID: "evt-gift-1", EventType: "WalletGiftDebited", SourceService: "wallet", UserID: 10001, MetricType: "gift_spend_coin", Value: 120, OccurredAtMS: now.UnixMilli(), }) if err != nil || duplicate.Status != taskdomain.EventStatusConsumed { t.Fatalf("duplicate event should be acknowledged: result=%+v err=%v", duplicate, err) } list, err := svc.ListUserTasks(context.Background(), 10001) if err != nil { t.Fatalf("ListUserTasks failed: %v", err) } daily := list.Sections[0] if len(daily.Items) != 1 || daily.Items[0].TaskID != definition.TaskID || daily.Items[0].ProgressValue != 120 || daily.Items[0].UserStatus != taskdomain.StatusCompleted || !daily.Items[0].Claimable { t.Fatalf("daily task item mismatch: %+v", daily.Items) } claim, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{ UserID: 10001, TaskID: definition.TaskID, TaskType: taskdomain.TypeDaily, CycleKey: "2026-05-09", CommandID: "cmd-claim-1", }) if err != nil { t.Fatalf("ClaimTaskReward failed: %v", err) } if claim.Status != taskdomain.ClaimStatusGranted || claim.WalletTransactionID != "wtx-task-1" || wallet.calls != 1 { t.Fatalf("claim result mismatch: claim=%+v wallet_calls=%d", claim, wallet.calls) } again, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{ UserID: 10001, TaskID: definition.TaskID, TaskType: taskdomain.TypeDaily, CycleKey: "2026-05-09", CommandID: "cmd-claim-1", }) if err != nil { t.Fatalf("ClaimTaskReward retry failed: %v", err) } if again.ClaimID != claim.ClaimID || wallet.calls != 1 { t.Fatalf("claim idempotency mismatch: first=%+v second=%+v calls=%d", claim, again, wallet.calls) } } type fakeWalletClient struct { calls int } func (f *fakeWalletClient) CreditTaskReward(context.Context, *walletv1.CreditTaskRewardRequest, ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) { f.calls++ return &walletv1.CreditTaskRewardResponse{ TransactionId: "wtx-task-1", Amount: 10, GrantedAtMs: 1778292000000, Balance: &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 10}, }, nil }