Files
huya-go/internal/gui/role.go
T
yml2213 7dadcd1644 feat(role): 增加用户绑定角色信息查询与作战表展示
- 逆向并实现虎牙活动绑角色接口 webActUI.checkUserBindGameAccount,支持精准解析游戏角色名、区服和平台
- 增加本地持久化 roles.json 存储,启动即回显角色信息无需重复请求
- 前端账号作战表与 CK 管理列表整合角色展示([微信-安卓] 角色名),直观区分账号归属
- 工具栏增加「查询角色」按钮,并以兑换记录中的历史账号作为双重兜底
- 升级版本至 v0.5.5 并成功交叉编译 Windows 产物
2026-10-19 12:09:24 +08:00

289 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}