2026-06-04 10:06:49 +08:00

159 lines
4.6 KiB
Go

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
}