hi-server/.codex-tmp/ua999_cleanup.go
shanshanzhong 62cf68b49b
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m35s
x
2026-04-04 11:46:06 -07:00

580 lines
18 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"sort"
"strings"
"time"
"github.com/redis/go-redis/v9"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v3"
)
type appConfig struct {
MySQL struct {
Addr string `yaml:"Addr"`
Username string `yaml:"Username"`
Password string `yaml:"Password"`
Dbname string `yaml:"Dbname"`
Config string `yaml:"Config"`
} `yaml:"MySQL"`
Redis struct {
Host string `yaml:"Host"`
Pass string `yaml:"Pass"`
DB int `yaml:"DB"`
} `yaml:"Redis"`
}
type userRow struct {
ID int64 `json:"id"`
ReferCode string `json:"refer_code"`
Balance int64 `json:"balance"`
Commission int64 `json:"commission"`
GiftAmount int64 `json:"gift_amount"`
Enable bool `json:"enable"`
IsAdmin bool `json:"is_admin"`
ValidEmail bool `json:"valid_email"`
MemberStatus string `json:"member_status"`
CreatedAt time.Time `json:"created_at"`
DeletedAt sql.NullTime `json:"-"`
}
type authMethod struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
AuthType string `json:"auth_type"`
Identifier string `json:"identifier"`
Verified bool `json:"verified"`
CreatedAt time.Time `json:"created_at"`
}
type deviceInfo struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Identifier string `json:"identifier"`
ShortCode string `json:"short_code"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
type subscribeInfo struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
OrderID int64 `json:"order_id"`
SubscribeID int64 `json:"subscribe_id"`
Token string `json:"token"`
UUID string `json:"uuid"`
Status uint8 `json:"status"`
StartTime time.Time `json:"start_time"`
ExpireTime time.Time `json:"expire_time"`
}
type familyInfo struct {
FamilyID int64 `json:"family_id"`
OwnerUserID int64 `json:"owner_user_id"`
IsOwner bool `json:"is_owner"`
MemberCount int64 `json:"member_count"`
}
type userSummary struct {
User userRow `json:"user"`
AuthMethods []authMethod `json:"auth_methods"`
Devices []deviceInfo `json:"devices"`
Subscriptions []subscribeInfo `json:"subscriptions"`
Family *familyInfo `json:"family,omitempty"`
OrderCount int64 `json:"order_count"`
TicketCount int64 `json:"ticket_count"`
TrafficLogCount int64 `json:"traffic_log_count"`
SystemLogCount int64 `json:"system_log_count"`
WithdrawalCount int64 `json:"withdrawal_count"`
IAPTransactionCount int64 `json:"iap_transaction_count"`
LogMessageCount int64 `json:"log_message_count"`
OnlineRecordCount int64 `json:"online_record_count"`
}
type deleteResult struct {
UserID int64 `json:"user_id"`
DeletedDBRows []string `json:"deleted_db_rows"`
DeletedRedisKeys int `json:"deleted_redis_keys"`
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
ctx := context.Background()
cfg := loadConfig("/Users/Apple/code_vpn/vpn/ppanel-server/etc/ppanel.yaml")
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", cfg.MySQL.Username, cfg.MySQL.Password, cfg.MySQL.Addr, cfg.MySQL.Dbname, cfg.MySQL.Config)
db, err := sql.Open("mysql", dsn)
must(err)
defer db.Close()
must(db.PingContext(ctx))
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Redis.Host,
Password: cfg.Redis.Pass,
DB: cfg.Redis.DB,
})
defer rdb.Close()
must(rdb.Ping(ctx).Err())
targetUserIDs, err := findTargetUsers(ctx, db)
must(err)
if len(targetUserIDs) == 0 {
fmt.Println(`{"matched_users":[],"deleted":[]}`)
return
}
summaries := make([]userSummary, 0, len(targetUserIDs))
for _, userID := range targetUserIDs {
summary, sumErr := collectSummary(ctx, db, userID)
must(sumErr)
summaries = append(summaries, summary)
}
before, err := json.MarshalIndent(map[string]interface{}{
"matched_users": summaries,
}, "", " ")
must(err)
fmt.Println(string(before))
results := make([]deleteResult, 0, len(targetUserIDs))
for _, summary := range summaries {
result, delErr := deleteUser(ctx, db, rdb, summary)
must(delErr)
results = append(results, result)
}
after, err := json.MarshalIndent(map[string]interface{}{
"deleted": results,
}, "", " ")
must(err)
fmt.Println(string(after))
}
func loadConfig(path string) appConfig {
content, err := os.ReadFile(path)
must(err)
var cfg appConfig
must(yaml.Unmarshal(content, &cfg))
return cfg
}
func findTargetUsers(ctx context.Context, db *sql.DB) ([]int64, error) {
rows, err := db.QueryContext(ctx, `
SELECT DISTINCT user_id
FROM user_device
WHERE user_agent LIKE ?
ORDER BY user_id ASC
`, "%999%")
if err != nil {
return nil, err
}
defer rows.Close()
var ids []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func collectSummary(ctx context.Context, db *sql.DB, userID int64) (userSummary, error) {
var summary userSummary
summary.User.ID = userID
err := db.QueryRowContext(ctx, `
SELECT id, refer_code, balance, commission, gift_amount, enable, is_admin, valid_email, member_status, created_at, deleted_at
FROM user
WHERE id = ?
`, userID).Scan(
&summary.User.ID,
&summary.User.ReferCode,
&summary.User.Balance,
&summary.User.Commission,
&summary.User.GiftAmount,
&summary.User.Enable,
&summary.User.IsAdmin,
&summary.User.ValidEmail,
&summary.User.MemberStatus,
&summary.User.CreatedAt,
&summary.User.DeletedAt,
)
if err != nil {
return summary, err
}
summary.AuthMethods, err = queryAuthMethods(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Devices, err = queryDevices(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Subscriptions, err = querySubscriptions(ctx, db, userID)
if err != nil {
return summary, err
}
summary.Family, err = queryFamily(ctx, db, userID)
if err != nil {
return summary, err
}
if summary.OrderCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM `order` WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.TicketCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM ticket WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.TrafficLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM traffic_log WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.SystemLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM system_logs WHERE object_id = ?", userID); err != nil {
return summary, err
}
if summary.WithdrawalCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_withdrawal WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.IAPTransactionCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM apple_iap_transactions WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.LogMessageCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM log_message WHERE user_id = ?", userID); err != nil {
return summary, err
}
if summary.OnlineRecordCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_device_online_record WHERE user_id = ?", userID); err != nil {
return summary, err
}
return summary, nil
}
func queryAuthMethods(ctx context.Context, db *sql.DB, userID int64) ([]authMethod, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, auth_type, auth_identifier, verified, created_at
FROM user_auth_methods
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []authMethod
for rows.Next() {
var item authMethod
if err := rows.Scan(&item.ID, &item.UserID, &item.AuthType, &item.Identifier, &item.Verified, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func queryDevices(ctx context.Context, db *sql.DB, userID int64) ([]deviceInfo, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, ip, user_agent, identifier, short_code, online, enabled, created_at
FROM user_device
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []deviceInfo
for rows.Next() {
var item deviceInfo
if err := rows.Scan(&item.ID, &item.UserID, &item.IP, &item.UserAgent, &item.Identifier, &item.ShortCode, &item.Online, &item.Enabled, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func querySubscriptions(ctx context.Context, db *sql.DB, userID int64) ([]subscribeInfo, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, user_id, order_id, subscribe_id, token, uuid, status, start_time, expire_time
FROM user_subscribe
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []subscribeInfo
for rows.Next() {
var item subscribeInfo
if err := rows.Scan(&item.ID, &item.UserID, &item.OrderID, &item.SubscribeID, &item.Token, &item.UUID, &item.Status, &item.StartTime, &item.ExpireTime); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func queryFamily(ctx context.Context, db *sql.DB, userID int64) (*familyInfo, error) {
var info familyInfo
err := db.QueryRowContext(ctx, `
SELECT ufm.family_id, uf.owner_user_id
FROM user_family_member ufm
JOIN user_family uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL
WHERE ufm.user_id = ? AND ufm.deleted_at IS NULL
LIMIT 1
`, userID).Scan(&info.FamilyID, &info.OwnerUserID)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
info.IsOwner = info.OwnerUserID == userID
memberCount, err := queryCount(ctx, db, `
SELECT COUNT(*)
FROM user_family_member
WHERE family_id = ? AND deleted_at IS NULL
`, info.FamilyID)
if err != nil {
return nil, err
}
info.MemberCount = memberCount
return &info, nil
}
func queryCount(ctx context.Context, db *sql.DB, q string, arg interface{}) (int64, error) {
var count int64
err := db.QueryRowContext(ctx, q, arg).Scan(&count)
return count, err
}
func deleteUser(ctx context.Context, db *sql.DB, rdb *redis.Client, summary userSummary) (deleteResult, error) {
result := deleteResult{UserID: summary.User.ID}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return result, err
}
defer tx.Rollback()
if summary.Family != nil {
if summary.Family.IsOwner {
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE family_id = ?`, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
}
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family WHERE id = ?`, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family=%d", rowsAffected(res)))
}
} else {
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE user_id = ? AND family_id = ?`, summary.User.ID, summary.Family.FamilyID); err != nil {
return result, err
} else {
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
}
}
}
if res, err := tx.ExecContext(ctx, `DELETE FROM user_auth_methods WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_auth_methods=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_subscribe WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_subscribe=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device_online_record WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device_online_record=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user_withdrawal WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_withdrawal=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, "DELETE FROM `order` WHERE user_id = ?", summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("order=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM traffic_log WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("traffic_log=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM system_logs WHERE object_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("system_logs=%d", rowsAffected(res))) }
var ticketIDs []int64
ticketRows, err := tx.QueryContext(ctx, `SELECT id FROM ticket WHERE user_id = ?`, summary.User.ID)
if err != nil {
return result, err
}
for ticketRows.Next() {
var id int64
if err := ticketRows.Scan(&id); err != nil {
ticketRows.Close()
return result, err
}
ticketIDs = append(ticketIDs, id)
}
ticketRows.Close()
if len(ticketIDs) > 0 {
holders := strings.TrimSuffix(strings.Repeat("?,", len(ticketIDs)), ",")
args := make([]interface{}, 0, len(ticketIDs))
for _, id := range ticketIDs {
args = append(args, id)
}
if res, err := tx.ExecContext(ctx, "DELETE FROM ticket_follow WHERE ticket_id IN ("+holders+")", args...); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket_follow=%d", rowsAffected(res))) }
}
if res, err := tx.ExecContext(ctx, `DELETE FROM ticket WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM apple_iap_transactions WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("apple_iap_transactions=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM log_message WHERE user_id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("log_message=%d", rowsAffected(res))) }
if res, err := tx.ExecContext(ctx, `DELETE FROM user WHERE id = ?`, summary.User.ID); err != nil {
return result, err
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user=%d", rowsAffected(res))) }
if err := tx.Commit(); err != nil {
return result, err
}
redisKeys, err := cleanupRedis(ctx, rdb, summary)
if err != nil {
return result, err
}
result.DeletedRedisKeys = len(redisKeys)
sort.Strings(result.DeletedDBRows)
return result, nil
}
func cleanupRedis(ctx context.Context, rdb *redis.Client, summary userSummary) ([]string, error) {
keySet := map[string]struct{}{
fmt.Sprintf("cache:user:id:%d", summary.User.ID): {},
fmt.Sprintf("cache:user:subscribe:user:%d", summary.User.ID): {},
fmt.Sprintf("cache:user:subscribe:user:%d:all", summary.User.ID): {},
fmt.Sprintf("auth:user_sessions:%d", summary.User.ID): {},
}
for _, am := range summary.AuthMethods {
if am.AuthType == "email" && am.Identifier != "" {
keySet[fmt.Sprintf("cache:user:email:%s", am.Identifier)] = struct{}{}
}
}
for _, sub := range summary.Subscriptions {
keySet[fmt.Sprintf("cache:user:subscribe:id:%d", sub.ID)] = struct{}{}
if sub.Token != "" {
keySet[fmt.Sprintf("cache:user:subscribe:token:%s", sub.Token)] = struct{}{}
}
}
for _, device := range summary.Devices {
keySet[fmt.Sprintf("cache:user:device:id:%d", device.ID)] = struct{}{}
if device.Identifier != "" {
keySet[fmt.Sprintf("cache:user:device:number:%s", device.Identifier)] = struct{}{}
keySet[fmt.Sprintf("auth:device_identifier:%s", device.Identifier)] = struct{}{}
}
}
sessionsKey := fmt.Sprintf("auth:user_sessions:%d", summary.User.ID)
sessionIDs, err := rdb.ZRange(ctx, sessionsKey, 0, -1).Result()
if err != nil && err != redis.Nil {
return nil, err
}
for _, sessionID := range sessionIDs {
if sessionID == "" {
continue
}
keySet[fmt.Sprintf("auth:session_id:%s", sessionID)] = struct{}{}
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
}
var cursor uint64
for {
keys, nextCursor, scanErr := rdb.Scan(ctx, cursor, "auth:session_id:*", 200).Result()
if scanErr != nil {
return nil, scanErr
}
for _, key := range keys {
if strings.Contains(key, ":detail:") {
continue
}
value, getErr := rdb.Get(ctx, key).Result()
if getErr != nil {
continue
}
if value == fmt.Sprintf("%d", summary.User.ID) {
keySet[key] = struct{}{}
sessionID := strings.TrimPrefix(key, "auth:session_id:")
if sessionID != "" {
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
}
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
keys := make([]string, 0, len(keySet))
for key := range keySet {
keys = append(keys, key)
}
sort.Strings(keys)
if len(keys) == 0 {
return keys, nil
}
if err := rdb.Del(ctx, keys...).Err(); err != nil {
return nil, err
}
return keys, nil
}
func rowsAffected(res sql.Result) int64 {
if res == nil {
return 0
}
n, err := res.RowsAffected()
if err != nil {
return 0
}
return n
}