159 lines
4.6 KiB
Go
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
|
|
}
|