- 逆向并实现虎牙活动绑角色接口 webActUI.checkUserBindGameAccount,支持精准解析游戏角色名、区服和平台 - 增加本地持久化 roles.json 存储,启动即回显角色信息无需重复请求 - 前端账号作战表与 CK 管理列表整合角色展示([微信-安卓] 角色名),直观区分账号归属 - 工具栏增加「查询角色」按钮,并以兑换记录中的历史账号作为双重兜底 - 升级版本至 v0.5.5 并成功交叉编译 Windows 产物
289 lines
7.3 KiB
Go
289 lines
7.3 KiB
Go
package gui
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"huya-go/internal/huya"
|
||
"huya-go/internal/sniper"
|
||
)
|
||
|
||
const RoleStoreFileName = "roles.json"
|
||
|
||
// RoleInfo 描述一个账号绑定的角色详情,供本地持久化及前端展示。
|
||
type RoleInfo struct {
|
||
UID int64 `json:"uid"`
|
||
RoleName string `json:"role_name"`
|
||
AreaName string `json:"area_name"`
|
||
PlatName string `json:"plat_name"`
|
||
DisplayName string `json:"display_name"`
|
||
GameName string `json:"game_name"`
|
||
IsBound bool `json:"is_bound"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
}
|
||
|
||
// RoleResult 是批量查询角色时的单账号结果。
|
||
type RoleResult struct {
|
||
UID int64 `json:"uid"`
|
||
RoleName string `json:"role_name"`
|
||
AreaName string `json:"area_name"`
|
||
PlatName string `json:"plat_name"`
|
||
DisplayName string `json:"display_name"`
|
||
GameName string `json:"game_name"`
|
||
IsBound bool `json:"is_bound"`
|
||
Resp *huya.CheckUserBindGameAccountResp `json:"resp,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// RoleStore 在各实例数据目录下持久化保存各账号的角色信息。
|
||
type RoleStore struct {
|
||
path string
|
||
mu sync.RWMutex
|
||
roles map[int64]RoleInfo
|
||
}
|
||
|
||
func NewRoleStore(path string) *RoleStore {
|
||
return &RoleStore{
|
||
path: path,
|
||
roles: make(map[int64]RoleInfo),
|
||
}
|
||
}
|
||
|
||
func (s *RoleStore) Load() error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
if s.path == "" {
|
||
return nil
|
||
}
|
||
data, err := os.ReadFile(s.path)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
var raw map[string]RoleInfo
|
||
if err := json.Unmarshal(data, &raw); err != nil {
|
||
return err
|
||
}
|
||
s.roles = make(map[int64]RoleInfo, len(raw))
|
||
for k, v := range raw {
|
||
if uid, err := strconv.ParseInt(k, 10, 64); err == nil {
|
||
s.roles[uid] = v
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *RoleStore) Save() error {
|
||
s.mu.RLock()
|
||
raw := make(map[string]RoleInfo, len(s.roles))
|
||
for k, v := range s.roles {
|
||
raw[strconv.FormatInt(k, 10)] = v
|
||
}
|
||
s.mu.RUnlock()
|
||
|
||
if s.path == "" {
|
||
return nil
|
||
}
|
||
data, err := json.MarshalIndent(raw, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
dir := filepath.Dir(s.path)
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return err
|
||
}
|
||
|
||
tmpFile := fmt.Sprintf("%s.tmp.%d", s.path, time.Now().UnixNano())
|
||
if err := os.WriteFile(tmpFile, data, 0o644); err != nil {
|
||
return err
|
||
}
|
||
return os.Rename(tmpFile, s.path)
|
||
}
|
||
|
||
func (s *RoleStore) Get(uid int64) (RoleInfo, bool) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
info, ok := s.roles[uid]
|
||
return info, ok
|
||
}
|
||
|
||
func (s *RoleStore) Set(info RoleInfo) {
|
||
s.mu.Lock()
|
||
if info.UpdatedAt == 0 {
|
||
info.UpdatedAt = time.Now().Unix()
|
||
}
|
||
s.roles[info.UID] = info
|
||
s.mu.Unlock()
|
||
}
|
||
|
||
func (s *RoleStore) SetMany(list []RoleInfo) {
|
||
s.mu.Lock()
|
||
now := time.Now().Unix()
|
||
for _, info := range list {
|
||
if info.UpdatedAt == 0 {
|
||
info.UpdatedAt = now
|
||
}
|
||
s.roles[info.UID] = info
|
||
}
|
||
s.mu.Unlock()
|
||
}
|
||
|
||
func (s *RoleStore) GetAll() map[string]RoleInfo {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make(map[string]RoleInfo, len(s.roles))
|
||
for k, v := range s.roles {
|
||
out[strconv.FormatInt(k, 10)] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// GetCachedRoles 获取已持久化的角色缓存字典,供前端初始化加载。
|
||
func (a *App) GetCachedRoles() (map[string]RoleInfo, error) {
|
||
a.ensureRoleStore()
|
||
if a.roleStore == nil {
|
||
return map[string]RoleInfo{}, nil
|
||
}
|
||
return a.roleStore.GetAll(), nil
|
||
}
|
||
|
||
func (a *App) ensureRoleStore() {
|
||
if a.roleStore == nil && a.dataDir != "" {
|
||
a.roleStore = NewRoleStore(filepath.Join(a.dataDir, RoleStoreFileName))
|
||
_ = a.roleStore.Load()
|
||
}
|
||
}
|
||
|
||
// QueryAccountRoles 并发查询当前账号绑定的角色信息。
|
||
// 优先调用虎牙 webActUI.checkUserBindGameAccount;
|
||
// QueryRoles 并发查询当前账号绑定的角色信息。
|
||
// 优先调用虎牙 webActUI.checkUserBindGameAccount;
|
||
// 若未绑定角色但历史兑换记录中存在 UserPrizeInfo.Account,以此作为兜底展示;
|
||
// 查询成功后自动持久化存盘到 roles.json。
|
||
func (a *App) QueryRoles(cfg Config) ([]RoleResult, error) {
|
||
return a.QueryAccountRoles(cfg)
|
||
}
|
||
|
||
func (a *App) QueryAccountRoles(cfg Config) ([]RoleResult, error) {
|
||
cfg = normalizeConfig(cfg)
|
||
accounts, err := parseAccounts(cfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(accounts) == 0 {
|
||
return nil, errors.New("没有有效的账号凭据")
|
||
}
|
||
|
||
client, closeClient, err := a.newClient(cfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer closeClient()
|
||
|
||
a.ensureRoleStore()
|
||
|
||
ctx, cancel := requestContext(cfg.TimeoutSec)
|
||
defer cancel()
|
||
|
||
concurrency := cfg.Concurrency
|
||
if concurrency <= 0 {
|
||
concurrency = 3
|
||
}
|
||
if concurrency > len(accounts) {
|
||
concurrency = len(accounts)
|
||
}
|
||
|
||
results := make([]RoleResult, len(accounts))
|
||
sem := make(chan struct{}, concurrency)
|
||
var wg sync.WaitGroup
|
||
|
||
a.addLog("info", "开始查询绑定角色: accounts=%d 并发=%d", len(accounts), concurrency)
|
||
|
||
for i, acct := range accounts {
|
||
wg.Add(1)
|
||
go func(idx int, targetAcct sniper.Account) {
|
||
defer wg.Done()
|
||
sem <- struct{}{}
|
||
defer func() { <-sem }()
|
||
|
||
res := RoleResult{UID: targetAcct.UID}
|
||
// 1. 发起 checkUserBindGameAccount 查询
|
||
// bActID: 和平精英腾讯互联活动 ID,抓包确认为 9271
|
||
resp, callErr := client.CheckUserBindGameAccount(ctx, targetAcct.UID, targetAcct.Cookie, 9271)
|
||
if callErr != nil {
|
||
res.Error = callErr.Error()
|
||
} else if resp != nil {
|
||
res.Resp = resp
|
||
res.DisplayName = resp.RoleDisplayName()
|
||
res.RoleName = resp.AccountData.GameRole.RoleName
|
||
res.AreaName = resp.AccountData.GameRole.AreaName
|
||
res.PlatName = resp.AccountData.GameRole.PlatName
|
||
res.GameName = resp.GameName
|
||
res.IsBound = resp.AccountData.IsBindRole == 1
|
||
}
|
||
|
||
// 2. 兜底策略:如果主接口未查到角色名,尝试用 getUserPrizeRecords 里的历史记录兜底
|
||
if res.RoleName == "" && cfg.SID > 0 {
|
||
if recResp, recErr := client.GetUserPrizeRecords(ctx, targetAcct.UID, targetAcct.Cookie, cfg.SID); recErr == nil && recResp != nil {
|
||
for _, rec := range recResp.Records {
|
||
if strings.TrimSpace(rec.Account) != "" {
|
||
res.RoleName = strings.TrimSpace(rec.Account)
|
||
res.DisplayName = res.RoleName + " (历史)"
|
||
res.IsBound = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
results[idx] = res
|
||
}(i, acct)
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
// 更新到 RoleStore 并持久化存盘
|
||
if a.roleStore != nil {
|
||
toSave := make([]RoleInfo, 0, len(results))
|
||
for _, r := range results {
|
||
if r.DisplayName != "" || r.RoleName != "" {
|
||
toSave = append(toSave, RoleInfo{
|
||
UID: r.UID,
|
||
RoleName: r.RoleName,
|
||
AreaName: r.AreaName,
|
||
PlatName: r.PlatName,
|
||
DisplayName: r.DisplayName,
|
||
GameName: r.GameName,
|
||
IsBound: r.IsBound,
|
||
})
|
||
}
|
||
}
|
||
if len(toSave) > 0 {
|
||
a.roleStore.SetMany(toSave)
|
||
if saveErr := a.roleStore.Save(); saveErr != nil {
|
||
a.addLog("warn", "保存 roles.json 失败: %v", saveErr)
|
||
}
|
||
}
|
||
}
|
||
|
||
boundCount := 0
|
||
for _, r := range results {
|
||
if r.IsBound && r.DisplayName != "" {
|
||
boundCount++
|
||
}
|
||
}
|
||
a.addLog("info", "查询绑定角色完成: accounts=%d 已绑定=%d", len(accounts), boundCount)
|
||
return results, nil
|
||
}
|