package main import ( "context" "database/sql" "flag" "fmt" "log" "sort" "strings" "time" _ "github.com/go-sql-driver/mysql" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" walletv1 "hyapp.local/api/proto/wallet/v1" ) const registrationLevelBadgeReason = "growth_level_registration_badge" func main() { var appCode string var activityDSN string var userDSN string var walletAddr string var dryRun bool var onlyUserID int64 flag.StringVar(&appCode, "app_code", "lalu", "app_code to backfill") flag.StringVar(&activityDSN, "activity_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC", "activity mysql dsn") flag.StringVar(&userDSN, "user_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC", "user mysql dsn") flag.StringVar(&walletAddr, "wallet_addr", "127.0.0.1:13004", "wallet-service grpc address") flag.BoolVar(&dryRun, "dry_run", true, "print planned grants without calling wallet-service") flag.Int64Var(&onlyUserID, "user_id", 0, "optional single user_id") flag.Parse() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() activityDB, err := sql.Open("mysql", activityDSN) if err != nil { log.Fatalf("open activity db: %v", err) } defer activityDB.Close() userDB, err := sql.Open("mysql", userDSN) if err != nil { log.Fatalf("open user db: %v", err) } defer userDB.Close() badges, err := levelOneBadgeResources(ctx, activityDB, appCode) if err != nil { log.Fatalf("query level badges: %v", err) } users, err := targetUsers(ctx, userDB, appCode, onlyUserID) if err != nil { log.Fatalf("query users: %v", err) } tracks := []string{"wealth", "game", "charm"} for _, track := range tracks { if badges[track] <= 0 { log.Printf("skip track=%s: level 1 long badge is not configured", track) } } if dryRun { log.Printf("dry_run=true app_code=%s users=%d badges=%v", appCode, len(users), badges) return } conn, err := grpc.NewClient(walletAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("dial wallet-service: %v", err) } defer conn.Close() wallet := walletv1.NewWalletServiceClient(conn) total := 0 for _, userID := range users { for _, track := range tracks { resourceID := badges[track] if resourceID <= 0 { continue } // wallet-service 用 command_id 做完整请求幂等;同一个用户同一轨道重复执行只会返回首次发放结果。 commandID := fmt.Sprintf("registration_level_badges:%s:%d:%s", appCode, userID, track) _, err := wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{ CommandId: commandID, AppCode: appCode, TargetUserId: userID, ResourceId: resourceID, Quantity: 1, DurationMs: 0, Reason: registrationLevelBadgeReason, OperatorUserId: userID, GrantSource: "growth_level", }) if err != nil { log.Fatalf("grant failed user_id=%d track=%s resource_id=%d: %v", userID, track, resourceID, err) } total++ } } log.Printf("done app_code=%s users=%d grant_calls=%d", appCode, len(users), total) } func levelOneBadgeResources(ctx context.Context, db *sql.DB, appCode string) (map[string]int64, error) { rows, err := db.QueryContext(ctx, ` SELECT track, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(display_config_json, '$.long_badge_resource_id')) AS SIGNED), 0) AS resource_id FROM growth_level_rules WHERE app_code = ? AND status = 'active' AND level = 1 ORDER BY track ASC`, appCode) if err != nil { return nil, err } defer rows.Close() result := map[string]int64{} for rows.Next() { var track string var resourceID int64 if err := rows.Scan(&track, &resourceID); err != nil { return nil, err } track = strings.ToLower(strings.TrimSpace(track)) if resourceID > 0 { result[track] = resourceID } } return result, rows.Err() } func targetUsers(ctx context.Context, db *sql.DB, appCode string, onlyUserID int64) ([]int64, error) { if onlyUserID > 0 { return []int64{onlyUserID}, nil } rows, err := db.QueryContext(ctx, ` SELECT user_id FROM users WHERE app_code = ? AND status = 'active' ORDER BY user_id ASC`, appCode) if err != nil { return nil, err } defer rows.Close() users := make([]int64, 0) for rows.Next() { var userID int64 if err := rows.Scan(&userID); err != nil { return nil, err } users = append(users, userID) } if err := rows.Err(); err != nil { return nil, err } sort.Slice(users, func(i, j int) bool { return users[i] < users[j] }) return users, nil }