2026-06-11 18:14:04 +08:00

313 lines
14 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"flag"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
activityv1 "hyapp.local/api/proto/activity/v1"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/inviteactivity"
inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
)
const (
appCode = "lalu"
operatorAdminID = int64(900100100)
tierStatus = "active"
)
type walletEntrySummary struct {
Count int64 `json:"count"`
Coins int64 `json:"coins"`
}
func main() {
var (
activityAddr = flag.String("activity_addr", "127.0.0.1:14006", "activity-service gRPC address")
walletAddr = flag.String("wallet_addr", "127.0.0.1:13004", "wallet-service gRPC address")
activityDSN = flag.String("activity_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC", "activity MySQL DSN")
walletDSN = flag.String("wallet_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC", "wallet MySQL DSN")
)
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ctx = appcode.WithContext(ctx, appCode)
conn, err := grpc.DialContext(ctx, *activityAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
must("dial activity-service", err)
defer conn.Close()
adminClient := activityv1.NewAdminInviteActivityRewardServiceClient(conn)
rewardClient := activityv1.NewInviteActivityRewardServiceClient(conn)
walletConn, err := grpc.DialContext(ctx, *walletAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
must("dial wallet-service", err)
defer walletConn.Close()
walletClient := walletv1.NewWalletServiceClient(walletConn)
activityRepo, err := mysqlstorage.Open(ctx, *activityDSN)
must("open activity mysql", err)
defer activityRepo.Close()
must("migrate activity mysql", activityRepo.Migrate(ctx))
walletDB, err := sql.Open("mysql", *walletDSN)
must("open wallet mysql", err)
defer walletDB.Close()
must("ping wallet mysql", walletDB.PingContext(ctx))
runID := fmt.Sprintf("iar_%d", time.Now().UnixMilli())
inviterUserID := int64(880000000000) + time.Now().Unix()%10000000
invitedUserID := inviterUserID + 9001
now := time.Now().UTC()
cycle := inviteactivityservice.CycleForTime(now)
config := updateConfig(ctx, adminClient, runID)
rechargeTier := findTier(config.GetTiers(), domain.RewardTypeRecharge, runID+"_recharge_160k")
validTier := findTier(config.GetTiers(), domain.RewardTypeValidInvite, runID+"_valid_1")
service := inviteactivityservice.New(activityRepo, walletClient, fakeInviteAttributionSource{inviterUserID: inviterUserID})
consumeInvite(ctx, service, runID, inviterUserID, invitedUserID, now)
afterInviteInviterStatus := getStatus(ctx, rewardClient, inviterUserID)
afterInviteInviteeStatus := getStatus(ctx, rewardClient, invitedUserID)
inviterInviteClaim := findClaim(afterInviteInviterStatus.GetClaims(), domain.RewardTypeInviteInviter)
inviteeInviteClaim := findClaim(afterInviteInviteeStatus.GetClaims(), domain.RewardTypeInvitee)
assert(inviterInviteClaim.GetRewardCoinAmount() == 700, "inviter per invite reward should be 700 coins")
assert(inviteeInviteClaim.GetRewardCoinAmount() == 200, "invitee per invite reward should be 200 coins")
consumeRecharge(ctx, service, runID, inviterUserID, 1, "google_play", 80000, now.Add(time.Minute))
consumeRecharge(ctx, service, runID, inviterUserID, 2, "mifapay", 40000, now.Add(2*time.Minute))
consumeRecharge(ctx, service, runID, inviterUserID, 3, "coin_seller_transfer", 40000, now.Add(3*time.Minute))
consumeValidInvite(ctx, service, runID, inviterUserID, 1, now.Add(4*time.Minute))
beforeClaim := getStatus(ctx, rewardClient, inviterUserID)
assert(beforeClaim.GetProgress().GetTotalRechargeCoinAmount() == 160000, "invite recharge progress should be 160000 coins")
assert(beforeClaim.GetProgress().GetValidInviteCount() == 1, "valid invite progress should be 1")
leaderboard := listLeaderboard(ctx, rewardClient)
assert(len(leaderboard.GetEntries()) > 0, "leaderboard should include at least one entry")
leaderboardEntry := findLeaderboardEntry(leaderboard.GetEntries(), inviterUserID)
assert(leaderboardEntry != nil, "leaderboard should include inviter")
assert(leaderboardEntry.GetValidInviteCount() == 1, "leaderboard valid invite count should be 1")
rechargeClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeRecharge, rechargeTier.GetTierId(), runID+"_claim_recharge")
validClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeValidInvite, validTier.GetTierId(), runID+"_claim_valid")
afterClaim := getStatus(ctx, rewardClient, inviterUserID)
assert(afterClaim.GetProgress().GetClaimedRewardCoinAmount() == 3500, "claimed reward progress should be 3500 coins")
walletInviteInviter := queryWalletEntry(ctx, walletDB, inviterInviteClaim.GetWalletTransactionId())
walletInvitee := queryWalletEntry(ctx, walletDB, inviteeInviteClaim.GetWalletTransactionId())
walletRecharge := queryWalletEntry(ctx, walletDB, rechargeClaim.GetWalletTransactionId())
walletValid := queryWalletEntry(ctx, walletDB, validClaim.GetWalletTransactionId())
assert(walletInviteInviter.Coins == 700, "wallet inviter per invite reward should credit 700 coins")
assert(walletInvitee.Coins == 200, "wallet invitee per invite reward should credit 200 coins")
assert(walletRecharge.Coins == 2500, "wallet recharge tier reward should credit 2500 coins")
assert(walletValid.Coins == 300, "wallet valid invite reward should credit 300 coins")
out := map[string]any{
"run_id": runID,
"inviter_user_id": inviterUserID,
"invited_user_id": invitedUserID,
"cycle": cycle.Key,
"progress_before_claim": map[string]int64{
"total_recharge_coin_amount": beforeClaim.GetProgress().GetTotalRechargeCoinAmount(),
"valid_invite_count": beforeClaim.GetProgress().GetValidInviteCount(),
},
"leaderboard_entry": map[string]any{
"rank_no": leaderboardEntry.GetRankNo(),
"user_id": leaderboardEntry.GetUserId(),
"valid_invite_count": leaderboardEntry.GetValidInviteCount(),
"total_recharge_coin_amount": leaderboardEntry.GetTotalRechargeCoinAmount(),
},
"claims": []map[string]any{
{"reward_type": inviterInviteClaim.GetRewardType(), "tier_code": inviterInviteClaim.GetTierCode(), "reward_coin_amount": inviterInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviterInviteClaim.GetWalletTransactionId()},
{"reward_type": inviteeInviteClaim.GetRewardType(), "tier_code": inviteeInviteClaim.GetTierCode(), "reward_coin_amount": inviteeInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviteeInviteClaim.GetWalletTransactionId()},
{"reward_type": rechargeClaim.GetRewardType(), "tier_code": rechargeClaim.GetTierCode(), "reward_coin_amount": rechargeClaim.GetRewardCoinAmount(), "wallet_transaction_id": rechargeClaim.GetWalletTransactionId()},
{"reward_type": validClaim.GetRewardType(), "tier_code": validClaim.GetTierCode(), "reward_coin_amount": validClaim.GetRewardCoinAmount(), "wallet_transaction_id": validClaim.GetWalletTransactionId()},
},
"wallet_entries": map[string]walletEntrySummary{
"invite_inviter": walletInviteInviter,
"invite_invitee": walletInvitee,
"recharge": walletRecharge,
"valid_invite": walletValid,
},
"progress_after_claim": map[string]int64{
"claimed_reward_coin_amount": afterClaim.GetProgress().GetClaimedRewardCoinAmount(),
},
}
encoded, _ := json.MarshalIndent(out, "", " ")
fmt.Println(string(encoded))
}
func updateConfig(ctx context.Context, client activityv1.AdminInviteActivityRewardServiceClient, runID string) *activityv1.InviteActivityRewardConfig {
resp, err := client.UpdateInviteActivityRewardConfig(ctx, &activityv1.UpdateInviteActivityRewardConfigRequest{
Meta: meta(runID + "_update_config"),
Enabled: true,
PerInviteInviterRewardCoinAmount: 700,
PerInviteInviteeRewardCoinAmount: 200,
OperatorAdminId: operatorAdminID,
Tiers: []*activityv1.InviteActivityRewardTier{
{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_80k", TierName: "Local recharge 80k", ThresholdCoinAmount: 80000, RewardCoinAmount: 1000, Status: tierStatus, SortOrder: 1},
{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_160k", TierName: "Local recharge 160k", ThresholdCoinAmount: 160000, RewardCoinAmount: 2500, Status: tierStatus, SortOrder: 2},
{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_1", TierName: "Local valid invite 1", ThresholdValidInviteCount: 1, RewardCoinAmount: 300, Status: tierStatus, SortOrder: 1},
{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_2", TierName: "Local valid invite 2", ThresholdValidInviteCount: 2, RewardCoinAmount: 900, Status: tierStatus, SortOrder: 2},
},
})
must("update invite activity config", err)
return resp.GetConfig()
}
func consumeInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, invitedUserID int64, occurredAt time.Time) {
err := service.ConsumeInviteEvent(ctx, domain.InviteEvent{
AppCode: appCode,
EventID: runID + "_invite_bound_1",
EventType: domain.EventTypeUserInvited,
InvitedUserID: invitedUserID,
InviterUserID: inviterUserID,
InviteCode: runID,
Source: "invite_h5",
OccurredAtMS: occurredAt.UnixMilli(),
})
must("consume invite bound event", err)
}
func consumeRecharge(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, rechargeType string, coins int64, occurredAt time.Time) {
err := service.ConsumeWalletRechargeEvent(ctx, domain.RechargeEvent{
AppCode: appCode,
EventID: fmt.Sprintf("%s_recharge_%d", runID, index),
EventType: domain.EventTypeWalletRechargeRecorded,
TransactionID: fmt.Sprintf("%s_tx_%d", runID, index),
CommandID: fmt.Sprintf("%s_cmd_%d", runID, index),
InvitedUserID: inviterUserID + 100 + index,
InviterUserID: inviterUserID,
RechargeCoinAmount: coins,
RechargeType: rechargeType,
OccurredAtMS: occurredAt.UnixMilli(),
})
must("consume invite recharge "+rechargeType, err)
}
func consumeValidInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, occurredAt time.Time) {
err := service.ConsumeValidInviteEvent(ctx, domain.ValidInviteEvent{
AppCode: appCode,
EventID: fmt.Sprintf("%s_valid_%d", runID, index),
EventType: domain.EventTypeUserInviteBecameValid,
InvitedUserID: inviterUserID + 200 + index,
InviterUserID: inviterUserID,
OccurredAtMS: occurredAt.UnixMilli(),
})
must("consume valid invite", err)
}
func getStatus(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64) *activityv1.InviteActivityRewardStatus {
resp, err := client.GetInviteActivityRewardStatus(ctx, &activityv1.GetInviteActivityRewardStatusRequest{
Meta: meta("get_status"),
UserId: userID,
})
must("get invite activity status", err)
return resp.GetStatus()
}
func listLeaderboard(ctx context.Context, client activityv1.InviteActivityRewardServiceClient) *activityv1.ListInviteActivityLeaderboardResponse {
resp, err := client.ListInviteActivityLeaderboard(ctx, &activityv1.ListInviteActivityLeaderboardRequest{
Meta: meta("list_leaderboard"),
Page: 1,
PageSize: 50,
})
must("list invite activity leaderboard", err)
return resp
}
func claim(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64, rewardType string, tierID int64, commandID string) *activityv1.InviteActivityRewardClaim {
resp, err := client.ClaimInviteActivityReward(ctx, &activityv1.ClaimInviteActivityRewardRequest{
Meta: meta(commandID),
UserId: userID,
RewardType: rewardType,
TierId: tierID,
CommandId: commandID,
})
must("claim invite activity reward "+rewardType, err)
return resp.GetClaim()
}
func queryWalletEntry(ctx context.Context, db *sql.DB, transactionID string) walletEntrySummary {
var summary walletEntrySummary
err := db.QueryRowContext(ctx, `
SELECT COUNT(*), COALESCE(SUM(available_delta), 0)
FROM wallet_entries
WHERE app_code = ? AND transaction_id = ? AND asset_type = 'COIN'`,
appCode, transactionID,
).Scan(&summary.Count, &summary.Coins)
must("query wallet entry "+transactionID, err)
return summary
}
func findTier(tiers []*activityv1.InviteActivityRewardTier, rewardType string, tierCode string) *activityv1.InviteActivityRewardTier {
for _, tier := range tiers {
if tier.GetRewardType() == rewardType && tier.GetTierCode() == tierCode {
return tier
}
}
log.Fatalf("tier not found: %s %s", rewardType, tierCode)
return nil
}
func findClaim(claims []*activityv1.InviteActivityRewardClaim, rewardType string) *activityv1.InviteActivityRewardClaim {
for _, claim := range claims {
if claim.GetRewardType() == rewardType {
return claim
}
}
log.Fatalf("claim not found: %s", rewardType)
return nil
}
func findLeaderboardEntry(entries []*activityv1.InviteActivityLeaderboardEntry, userID int64) *activityv1.InviteActivityLeaderboardEntry {
for _, entry := range entries {
if entry.GetUserId() == userID {
return entry
}
}
return nil
}
func meta(requestID string) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: requestID,
Caller: "invite-activity-local-flow",
AppCode: appCode,
SentAtMs: time.Now().UTC().UnixMilli(),
}
}
func assert(ok bool, message string) {
if !ok {
log.Fatal(message)
}
}
func must(step string, err error) {
if err != nil {
log.Fatalf("%s: %v", step, err)
}
}
type fakeInviteAttributionSource struct {
inviterUserID int64
}
func (s fakeInviteAttributionSource) GetInviteAttribution(_ context.Context, invitedUserID int64) (*userv1.InviteAttribution, error) {
return &userv1.InviteAttribution{
Found: true,
AppCode: appCode,
InvitedUserId: invitedUserID,
InviterUserId: s.inviterUserID,
}, nil
}