feat(role): 增加用户绑定角色信息查询与作战表展示
- 逆向并实现虎牙活动绑角色接口 webActUI.checkUserBindGameAccount,支持精准解析游戏角色名、区服和平台 - 增加本地持久化 roles.json 存储,启动即回显角色信息无需重复请求 - 前端账号作战表与 CK 管理列表整合角色展示([微信-安卓] 角色名),直观区分账号归属 - 工具栏增加「查询角色」按钮,并以兑换记录中的历史账号作为双重兜底 - 升级版本至 v0.5.5 并成功交叉编译 Windows 产物
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "huya-go-frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "huya-go-frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"dependencies": {
|
||||
"antd": "^5.26.2",
|
||||
"lucide-react": "^0.468.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "huya-go-frontend",
|
||||
"private": true,
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
e0e233c05b00ef1f530e32412926f966
|
||||
a9d40923be4354a749fbbe0c2b9f924e
|
||||
+176
-25
@@ -36,12 +36,14 @@ import {
|
||||
Square,
|
||||
Settings,
|
||||
Trash2,
|
||||
UserCheck,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
CheckCookies,
|
||||
DefaultConfig,
|
||||
Exchange,
|
||||
GetCachedRoles,
|
||||
GetInstanceInfo,
|
||||
GetLogFilePath,
|
||||
GetLogs,
|
||||
@@ -52,6 +54,7 @@ import {
|
||||
ListPrizes,
|
||||
OpenLogDir,
|
||||
PreviewAccounts,
|
||||
QueryRoles,
|
||||
QueryScores,
|
||||
ReconcileScores,
|
||||
SetLogLevel,
|
||||
@@ -144,6 +147,17 @@ type CookieCheckResult = {
|
||||
checkedAt: number
|
||||
}
|
||||
|
||||
type RoleItem = {
|
||||
uid: number
|
||||
role_name?: string
|
||||
area_name?: string
|
||||
plat_name?: string
|
||||
display_name?: string
|
||||
game_name?: string
|
||||
is_bound?: boolean
|
||||
updated_at?: number
|
||||
}
|
||||
|
||||
type AccountPreview = {
|
||||
line: number
|
||||
uid: number
|
||||
@@ -153,6 +167,7 @@ type AccountPreview = {
|
||||
checkStatus?: CookieCheckStatus
|
||||
checkMessage?: string
|
||||
checkedAt?: number
|
||||
role?: RoleItem
|
||||
}
|
||||
|
||||
type CookieEntry = {
|
||||
@@ -165,6 +180,7 @@ type CookieEntry = {
|
||||
checkMessage?: string
|
||||
checkedAt?: number
|
||||
updatedAt: number
|
||||
role?: RoleItem
|
||||
}
|
||||
|
||||
type LogEntry = {
|
||||
@@ -752,6 +768,7 @@ export default function Dashboard() {
|
||||
const [productSearch, setProductSearch] = useState('')
|
||||
const [scores, setScores] = useState<ScoreResult[]>([])
|
||||
const [records, setRecords] = useState<RecordResult[]>([])
|
||||
const [rolesByUID, setRolesByUID] = useState<Map<number, RoleItem>>(new Map())
|
||||
const [panelTab, setPanelTab] = useState<string>('logs')
|
||||
const [recordModalData, setRecordModalData] = useState<{
|
||||
open: boolean
|
||||
@@ -922,6 +939,20 @@ export default function Dashboard() {
|
||||
if (savedCookies.length > 0) {
|
||||
applyCookieEntries(savedCookies, true, true).catch((err: unknown) => message.error(String(err)))
|
||||
}
|
||||
|
||||
GetCachedRoles()
|
||||
.then((cachedRoles) => {
|
||||
if (!cachedRoles) return
|
||||
const m = new Map<number, RoleItem>()
|
||||
for (const [k, v] of Object.entries(cachedRoles)) {
|
||||
const uid = Number(k)
|
||||
if (uid) m.set(uid, v as RoleItem)
|
||||
}
|
||||
setRolesByUID(m)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.warn('读取本地角色缓存失败:', err)
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => message.error(String(err)))
|
||||
|
||||
@@ -1336,6 +1367,56 @@ export default function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
const queryAccountRoles = async () => {
|
||||
if (running) {
|
||||
message.warning('抢购运行中,请先停止再查询绑定角色')
|
||||
return
|
||||
}
|
||||
const targetAccounts =
|
||||
selectedAccountLines.length > 0
|
||||
? accounts.filter((item) => selectedAccountLines.includes(item.line))
|
||||
: accounts
|
||||
if (targetAccounts.length === 0) {
|
||||
message.warning('没有可查询的账号凭据')
|
||||
return
|
||||
}
|
||||
setLoading('roles')
|
||||
try {
|
||||
const values = config()
|
||||
if (selectedAccountLines.length > 0) {
|
||||
const lines = targetAccounts
|
||||
.map((a) => cookieEntries[a.line - 1]?.text || '')
|
||||
.filter(Boolean)
|
||||
values.cookieText = lines.join('\n')
|
||||
}
|
||||
const results = (await QueryRoles(values)) as RoleItem[]
|
||||
setRolesByUID((prev) => {
|
||||
const next = new Map(prev)
|
||||
results.forEach((r) => {
|
||||
if (r.uid) {
|
||||
next.set(Number(r.uid), {
|
||||
uid: Number(r.uid),
|
||||
role_name: r.role_name,
|
||||
area_name: r.area_name,
|
||||
plat_name: r.plat_name,
|
||||
display_name: r.display_name,
|
||||
game_name: r.game_name,
|
||||
is_bound: r.is_bound,
|
||||
})
|
||||
}
|
||||
})
|
||||
return next
|
||||
})
|
||||
const boundCount = results.filter((r) => r.is_bound && r.display_name).length
|
||||
message.success(`角色查询完成:${results.length} 个账号,${boundCount} 个已绑定角色`)
|
||||
} catch (err: unknown) {
|
||||
message.error(`查询角色失败: ${String(err)}`)
|
||||
} finally {
|
||||
setLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const clearExpiredCookies = () => {
|
||||
if (failedCookieCount === 0) return
|
||||
modal.confirm({
|
||||
@@ -1607,22 +1688,42 @@ export default function Dashboard() {
|
||||
const accountColumns: ColumnsType<AccountPreview> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '账号',
|
||||
width: 130,
|
||||
render: (_, row) => (
|
||||
<div>
|
||||
<strong>{row.uid || `第${row.line}行`}</strong>
|
||||
<div className="muted">
|
||||
{isCookieCheckUnusable(row.checkStatus)
|
||||
? row.checkMessage || 'CK 已失效'
|
||||
: row.valid
|
||||
? row.checkStatus === 'valid'
|
||||
? 'CK 可用'
|
||||
: '已加入任务'
|
||||
: row.error || '未选择'}
|
||||
title: '账号 / 角色',
|
||||
width: 155,
|
||||
render: (_, row) => {
|
||||
const role = rolesByUID.get(row.uid)
|
||||
return (
|
||||
<div>
|
||||
<strong>{row.uid || `第${row.line}行`}</strong>
|
||||
{role?.display_name ? (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<Tag
|
||||
color="cyan"
|
||||
style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}
|
||||
title={`游戏: ${role.game_name || '和平精英'}\n角色: ${role.role_name || ''}\n区服: ${role.area_name || ''} ${role.plat_name || ''}`}
|
||||
>
|
||||
{role.display_name}
|
||||
</Tag>
|
||||
</div>
|
||||
) : role?.is_bound === false ? (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<Tag color="default" style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}>
|
||||
未绑定角色
|
||||
</Tag>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="muted" style={{ marginTop: 2 }}>
|
||||
{isCookieCheckUnusable(row.checkStatus)
|
||||
? row.checkMessage || 'CK 已失效'
|
||||
: row.valid
|
||||
? row.checkStatus === 'valid'
|
||||
? 'CK 可用'
|
||||
: '已加入任务'
|
||||
: row.error || '未选择'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '凭据',
|
||||
@@ -1748,22 +1849,43 @@ export default function Dashboard() {
|
||||
running,
|
||||
scoreMap,
|
||||
watchedName,
|
||||
rolesByUID,
|
||||
],
|
||||
)
|
||||
|
||||
const cookieColumns: ColumnsType<CookieEntry> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '账号',
|
||||
width: 140,
|
||||
render: (_, row) => (
|
||||
<div>
|
||||
<strong>{row.uid || '-'}</strong>
|
||||
<div className="muted">
|
||||
{isCookieCheckUnusable(row.checkStatus) ? 'CK 已失效' : row.valid ? '可解析 UID' : row.error || '待校验'}
|
||||
title: '账号 / 角色',
|
||||
width: 155,
|
||||
render: (_, row) => {
|
||||
const role = rolesByUID.get(row.uid)
|
||||
return (
|
||||
<div>
|
||||
<strong>{row.uid || '-'}</strong>
|
||||
{role?.display_name ? (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<Tag
|
||||
color="cyan"
|
||||
style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}
|
||||
title={`游戏: ${role.game_name || '和平精英'}\n角色: ${role.role_name || ''}\n区服: ${role.area_name || ''} ${role.plat_name || ''}`}
|
||||
>
|
||||
{role.display_name}
|
||||
</Tag>
|
||||
</div>
|
||||
) : role?.is_bound === false ? (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<Tag color="default" style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}>
|
||||
未绑定角色
|
||||
</Tag>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="muted" style={{ marginTop: 2 }}>
|
||||
{isCookieCheckUnusable(row.checkStatus) ? 'CK 已失效' : row.valid ? '可解析 UID' : row.error || '待校验'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '格式',
|
||||
@@ -1812,7 +1934,7 @@ export default function Dashboard() {
|
||||
),
|
||||
},
|
||||
],
|
||||
[cookieEntries],
|
||||
[cookieEntries, rolesByUID],
|
||||
)
|
||||
|
||||
const prizeColumns: ColumnsType<PrizeInfo> = useMemo(
|
||||
@@ -2266,6 +2388,14 @@ export default function Dashboard() {
|
||||
>
|
||||
CK 有效性
|
||||
</Button>
|
||||
<Button
|
||||
icon={<UserCheck size={16} />}
|
||||
loading={loading === 'roles'}
|
||||
disabled={running || cookieEntries.length === 0}
|
||||
onClick={queryAccountRoles}
|
||||
>
|
||||
查询角色
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ClipboardList size={16} />}
|
||||
loading={loading === 'records'}
|
||||
@@ -2280,6 +2410,27 @@ export default function Dashboard() {
|
||||
const list = (data || []) as RecordResult[]
|
||||
setRecords(list)
|
||||
setPanelTab('records')
|
||||
setRolesByUID((prev) => {
|
||||
const next = new Map(prev)
|
||||
let changed = false
|
||||
list.forEach((item) => {
|
||||
if (!next.get(item.uid)?.role_name && item.records?.length) {
|
||||
for (const rec of item.records) {
|
||||
if (rec.account && rec.account.trim()) {
|
||||
next.set(item.uid, {
|
||||
uid: item.uid,
|
||||
role_name: rec.account.trim(),
|
||||
display_name: rec.account.trim() + ' (历史)',
|
||||
is_bound: true,
|
||||
})
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return changed ? next : prev
|
||||
})
|
||||
const total = list.reduce((sum, item) => sum + (item.records?.length || 0), 0)
|
||||
message.success(`查询完成:${list.length} 个账号,共 ${total} 条兑换记录`)
|
||||
})
|
||||
|
||||
Vendored
+6
@@ -10,6 +10,8 @@ export function DefaultConfig():Promise<gui.Config>;
|
||||
|
||||
export function Exchange(arg1:gui.Config):Promise<Array<gui.ExchangeResult>>;
|
||||
|
||||
export function GetCachedRoles():Promise<Record<string, gui.RoleInfo>>;
|
||||
|
||||
export function GetInstanceInfo():Promise<gui.InstanceInfo>;
|
||||
|
||||
export function GetLogFilePath():Promise<string>;
|
||||
@@ -30,6 +32,10 @@ export function OpenLogDir():Promise<void>;
|
||||
|
||||
export function PreviewAccounts(arg1:string):Promise<Array<gui.AccountPreview>>;
|
||||
|
||||
export function QueryAccountRoles(arg1:gui.Config):Promise<Array<gui.RoleResult>>;
|
||||
|
||||
export function QueryRoles(arg1:gui.Config):Promise<Array<gui.RoleResult>>;
|
||||
|
||||
export function QueryScores(arg1:gui.Config):Promise<Array<gui.ScoreResult>>;
|
||||
|
||||
export function ReconcileScores(arg1:gui.Config):Promise<Array<score.ReconcileItem>>;
|
||||
|
||||
@@ -14,6 +14,10 @@ export function Exchange(arg1) {
|
||||
return window['go']['gui']['App']['Exchange'](arg1);
|
||||
}
|
||||
|
||||
export function GetCachedRoles() {
|
||||
return window['go']['gui']['App']['GetCachedRoles']();
|
||||
}
|
||||
|
||||
export function GetInstanceInfo() {
|
||||
return window['go']['gui']['App']['GetInstanceInfo']();
|
||||
}
|
||||
@@ -54,6 +58,14 @@ export function PreviewAccounts(arg1) {
|
||||
return window['go']['gui']['App']['PreviewAccounts'](arg1);
|
||||
}
|
||||
|
||||
export function QueryAccountRoles(arg1) {
|
||||
return window['go']['gui']['App']['QueryAccountRoles'](arg1);
|
||||
}
|
||||
|
||||
export function QueryRoles(arg1) {
|
||||
return window['go']['gui']['App']['QueryRoles'](arg1);
|
||||
}
|
||||
|
||||
export function QueryScores(arg1) {
|
||||
return window['go']['gui']['App']['QueryScores'](arg1);
|
||||
}
|
||||
|
||||
@@ -260,6 +260,52 @@ export namespace gui {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class RoleResult {
|
||||
uid: number;
|
||||
role_name: string;
|
||||
area_name: string;
|
||||
plat_name: string;
|
||||
display_name: string;
|
||||
game_name: string;
|
||||
is_bound: boolean;
|
||||
resp?: huya.CheckUserBindGameAccountResp;
|
||||
error?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RoleResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.uid = source["uid"];
|
||||
this.role_name = source["role_name"];
|
||||
this.area_name = source["area_name"];
|
||||
this.plat_name = source["plat_name"];
|
||||
this.display_name = source["display_name"];
|
||||
this.game_name = source["game_name"];
|
||||
this.is_bound = source["is_bound"];
|
||||
this.resp = this.convertValues(source["resp"], huya.CheckUserBindGameAccountResp);
|
||||
this.error = source["error"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class ScoreResult {
|
||||
uid: number;
|
||||
score: number;
|
||||
@@ -341,6 +387,128 @@ export namespace huya {
|
||||
this.total = source["total"];
|
||||
}
|
||||
}
|
||||
export class GameRole {
|
||||
role_id: string;
|
||||
role_name: string;
|
||||
area_name: string;
|
||||
plat_name: string;
|
||||
partition_name: string;
|
||||
game_serial: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GameRole(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.role_id = source["role_id"];
|
||||
this.role_name = source["role_name"];
|
||||
this.area_name = source["area_name"];
|
||||
this.plat_name = source["plat_name"];
|
||||
this.partition_name = source["partition_name"];
|
||||
this.game_serial = source["game_serial"];
|
||||
}
|
||||
}
|
||||
export class GameAccount {
|
||||
open_id: string;
|
||||
app_id: string;
|
||||
type: string;
|
||||
face_url: string;
|
||||
nick: string;
|
||||
access_token: string;
|
||||
extend_info: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GameAccount(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.open_id = source["open_id"];
|
||||
this.app_id = source["app_id"];
|
||||
this.type = source["type"];
|
||||
this.face_url = source["face_url"];
|
||||
this.nick = source["nick"];
|
||||
this.access_token = source["access_token"];
|
||||
this.extend_info = source["extend_info"];
|
||||
}
|
||||
}
|
||||
export class GameAccountData {
|
||||
is_bind_account: number;
|
||||
is_bind_role: number;
|
||||
is_need_act_check: number;
|
||||
game_account: GameAccount;
|
||||
game_role: GameRole;
|
||||
change_bind_time: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GameAccountData(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.is_bind_account = source["is_bind_account"];
|
||||
this.is_bind_role = source["is_bind_role"];
|
||||
this.is_need_act_check = source["is_need_act_check"];
|
||||
this.game_account = this.convertValues(source["game_account"], GameAccount);
|
||||
this.game_role = this.convertValues(source["game_role"], GameRole);
|
||||
this.change_bind_time = source["change_bind_time"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class CheckUserBindGameAccountResp {
|
||||
status: number;
|
||||
msg: string;
|
||||
account_data: GameAccountData;
|
||||
game_name: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new CheckUserBindGameAccountResp(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.status = source["status"];
|
||||
this.msg = source["msg"];
|
||||
this.account_data = this.convertValues(source["account_data"], GameAccountData);
|
||||
this.game_name = source["game_name"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class ExchangeInfo {
|
||||
pid: number;
|
||||
exchange_score: number;
|
||||
@@ -357,6 +525,9 @@ export namespace huya {
|
||||
this.desc = source["desc"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class PrizeTag {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -52,6 +52,9 @@ type App struct {
|
||||
|
||||
// scoreBaselines 记录每个账号抢购前的积分基线,抢购后用于刚性扣分对账。
|
||||
scoreBaselines *score.BaselineStore
|
||||
// roleStore 记录各账号绑定的角色信息,并在本地持久化保存。
|
||||
roleStore *RoleStore
|
||||
|
||||
|
||||
runMu sync.Mutex
|
||||
monitorCancel context.CancelFunc
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoleStoreCRUDAndSave(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "roles.json")
|
||||
|
||||
store := NewRoleStore(path)
|
||||
store.Set(RoleInfo{
|
||||
UID: 1199668605121,
|
||||
RoleName: "白菜被拱了呀",
|
||||
AreaName: "微信",
|
||||
PlatName: "安卓",
|
||||
DisplayName: "白菜被拱了呀 [微信-安卓]",
|
||||
GameName: "和平精英",
|
||||
IsBound: true,
|
||||
})
|
||||
|
||||
if err := store.Save(); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
// 重新 Load
|
||||
store2 := NewRoleStore(path)
|
||||
if err := store2.Load(); err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
|
||||
info, ok := store2.Get(1199668605121)
|
||||
if !ok {
|
||||
t.Fatalf("expected role for 1199668605121")
|
||||
}
|
||||
if info.RoleName != "白菜被拱了呀" {
|
||||
t.Errorf("expected roleName 白菜被拱了呀, got %s", info.RoleName)
|
||||
}
|
||||
if info.DisplayName != "白菜被拱了呀 [微信-安卓]" {
|
||||
t.Errorf("expected displayName 白菜被拱了呀 [微信-安卓], got %s", info.DisplayName)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
FuncPrizeList = "getActPrizeList"
|
||||
FuncPrizeRecords = "getUserPrizeRecords"
|
||||
FuncExchangePrize = "scoreExchangePrize"
|
||||
FuncCheckUserBindGameAccount = "checkUserBindGameAccount"
|
||||
)
|
||||
|
||||
type ActivityUserID struct {
|
||||
@@ -842,3 +843,216 @@ func exchangeResponseMessage(resp *ScoreExchangePrizeResp) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CheckUserBindGameAccountReq struct {
|
||||
User ActivityUserID
|
||||
GID int32
|
||||
OuterActID string
|
||||
IsInnerBind int32
|
||||
Scene string
|
||||
BActID int32
|
||||
IsUseOuterActID int32
|
||||
}
|
||||
|
||||
func (q *CheckUserBindGameAccountReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteStruct(0, &q.User); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(1, int64(q.GID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(2, q.OuterActID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(3, int64(q.IsInnerBind)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(4, q.Scene); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(5, int64(q.BActID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteInt32(6, int64(q.IsUseOuterActID))
|
||||
}
|
||||
|
||||
func (q *CheckUserBindGameAccountReq) ReadFrom(r *taf.Reader) error {
|
||||
_, err := r.ReadStruct(0, false, &q.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if q.GID, err = r.ReadInt32(1, false, q.GID); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.OuterActID, err = r.ReadString(2, false, q.OuterActID); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.IsInnerBind, err = r.ReadInt32(3, false, q.IsInnerBind); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.Scene, err = r.ReadString(4, false, q.Scene); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.BActID, err = r.ReadInt32(5, false, q.BActID); err != nil {
|
||||
return err
|
||||
}
|
||||
q.IsUseOuterActID, err = r.ReadInt32(6, false, q.IsUseOuterActID)
|
||||
return err
|
||||
}
|
||||
|
||||
type GameRole struct {
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AreaName string `json:"area_name"`
|
||||
PlatName string `json:"plat_name"`
|
||||
PartitionName string `json:"partition_name"`
|
||||
GameSerial string `json:"game_serial"`
|
||||
}
|
||||
|
||||
func (g *GameRole) WriteTo(w *taf.Writer) error { return nil }
|
||||
|
||||
func (g *GameRole) ReadFrom(r *taf.Reader) error {
|
||||
var err error
|
||||
if g.RoleID, err = r.ReadString(0, false, g.RoleID); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.RoleName, err = r.ReadString(1, false, g.RoleName); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.AreaName, err = r.ReadString(2, false, g.AreaName); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.PlatName, err = r.ReadString(3, false, g.PlatName); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.PartitionName, err = r.ReadString(4, false, g.PartitionName); err != nil {
|
||||
return err
|
||||
}
|
||||
g.GameSerial, err = r.ReadString(5, false, g.GameSerial)
|
||||
return err
|
||||
}
|
||||
|
||||
type GameAccount struct {
|
||||
OpenID string `json:"open_id"`
|
||||
AppID string `json:"app_id"`
|
||||
Type string `json:"type"`
|
||||
FaceURL string `json:"face_url"`
|
||||
Nick string `json:"nick"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExtendInfo string `json:"extend_info"`
|
||||
}
|
||||
|
||||
func (g *GameAccount) WriteTo(w *taf.Writer) error { return nil }
|
||||
|
||||
func (g *GameAccount) ReadFrom(r *taf.Reader) error {
|
||||
var err error
|
||||
if g.OpenID, err = r.ReadString(0, false, g.OpenID); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.AppID, err = r.ReadString(1, false, g.AppID); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.Type, err = r.ReadString(2, false, g.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.FaceURL, err = r.ReadString(3, false, g.FaceURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.Nick, err = r.ReadString(4, false, g.Nick); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.AccessToken, err = r.ReadString(5, false, g.AccessToken); err != nil {
|
||||
return err
|
||||
}
|
||||
g.ExtendInfo, err = r.ReadString(6, false, g.ExtendInfo)
|
||||
return err
|
||||
}
|
||||
|
||||
type GameAccountData struct {
|
||||
IsBindAccount int32 `json:"is_bind_account"`
|
||||
IsBindRole int32 `json:"is_bind_role"`
|
||||
IsNeedActCheck int32 `json:"is_need_act_check"`
|
||||
GameAccount GameAccount `json:"game_account"`
|
||||
GameRole GameRole `json:"game_role"`
|
||||
ChangeBindTime int32 `json:"change_bind_time"`
|
||||
}
|
||||
|
||||
func (d *GameAccountData) WriteTo(w *taf.Writer) error { return nil }
|
||||
|
||||
func (d *GameAccountData) ReadFrom(r *taf.Reader) error {
|
||||
var err error
|
||||
if d.IsBindAccount, err = r.ReadInt32(0, false, d.IsBindAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsBindRole, err = r.ReadInt32(1, false, d.IsBindRole); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsNeedActCheck, err = r.ReadInt32(2, false, d.IsNeedActCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = r.ReadStruct(3, false, &d.GameAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = r.ReadStruct(4, false, &d.GameRole); err != nil {
|
||||
return err
|
||||
}
|
||||
d.ChangeBindTime, err = r.ReadInt32(5, false, d.ChangeBindTime)
|
||||
return err
|
||||
}
|
||||
|
||||
type CheckUserBindGameAccountResp struct {
|
||||
Status int32 `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
AccountData GameAccountData `json:"account_data"`
|
||||
GameName string `json:"game_name"`
|
||||
}
|
||||
|
||||
func (r *CheckUserBindGameAccountResp) WriteTo(w *taf.Writer) error { return nil }
|
||||
|
||||
func (r *CheckUserBindGameAccountResp) ReadFrom(rd *taf.Reader) error {
|
||||
var err error
|
||||
if r.Status, err = rd.ReadInt32(0, false, r.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Msg, err = rd.ReadString(1, false, r.Msg); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = rd.ReadStruct(2, false, &r.AccountData); err != nil {
|
||||
return err
|
||||
}
|
||||
r.GameName, err = rd.ReadString(3, false, r.GameName)
|
||||
return err
|
||||
}
|
||||
|
||||
// RoleDisplayName 格式化角色展示名称,如 "白菜被拱了呀 [微信-安卓]",未绑定时返回空串或未绑定。
|
||||
func (r *CheckUserBindGameAccountResp) RoleDisplayName() string {
|
||||
if r == nil || r.Status != 200 {
|
||||
return ""
|
||||
}
|
||||
role := r.AccountData.GameRole
|
||||
if strings.TrimSpace(role.RoleName) == "" {
|
||||
if r.AccountData.IsBindRole == 0 && r.AccountData.IsBindAccount == 0 {
|
||||
return "未绑定角色"
|
||||
}
|
||||
if r.AccountData.GameAccount.Nick != "" {
|
||||
return r.AccountData.GameAccount.Nick
|
||||
}
|
||||
return ""
|
||||
}
|
||||
var tagParts []string
|
||||
if role.AreaName != "" {
|
||||
tagParts = append(tagParts, role.AreaName)
|
||||
}
|
||||
if role.PlatName != "" {
|
||||
plat := strings.TrimSuffix(strings.TrimSuffix(role.PlatName, "(android)"), "(iOS)")
|
||||
plat = strings.TrimSpace(plat)
|
||||
if plat != "" {
|
||||
tagParts = append(tagParts, plat)
|
||||
}
|
||||
}
|
||||
if len(tagParts) > 0 {
|
||||
return fmt.Sprintf("%s [%s]", role.RoleName, strings.Join(tagParts, "-"))
|
||||
}
|
||||
return role.RoleName
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package huya
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"huya-go/internal/wup"
|
||||
)
|
||||
|
||||
var sampleBindRespHex = "000000be10032c3c4001560877656241637455496618636865636b5573657242696e6447616d654163636f756e747d0001008408000206001d0000010c0604745273701d0000700a0100c816002a0001100120013a0600160026027171360046085151e794a8e688b7560066000b4a0600160fe5bfa7e98381e5988ee5b7b4e8b1862604e6898b51360fe5ae89e58d9328616e64726f696429460056000b526a4fc5800b360ce5928ce5b9b3e7b2bee88bb1460050070b811388980ca80c"
|
||||
|
||||
func TestDecodeCheckUserBindGameAccountResp(t *testing.T) {
|
||||
respBytes, err := hex.DecodeString(sampleBindRespHex)
|
||||
if err != nil {
|
||||
t.Fatalf("decode hex: %v", err)
|
||||
}
|
||||
|
||||
var wResp wup.Response
|
||||
if err := wResp.Decode(respBytes); err != nil {
|
||||
t.Fatalf("wup decode: %v", err)
|
||||
}
|
||||
|
||||
var resp CheckUserBindGameAccountResp
|
||||
ok, err := wResp.ReadStruct("tRsp", &resp)
|
||||
if err != nil {
|
||||
t.Fatalf("readStruct tRsp: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("tRsp not found in response")
|
||||
}
|
||||
|
||||
t.Logf("Status: %d, Msg: %s, GameName: %s", resp.Status, resp.Msg, resp.GameName)
|
||||
t.Logf("RoleName: %s, AreaName: %s, PlatName: %s", resp.AccountData.GameRole.RoleName, resp.AccountData.GameRole.AreaName, resp.AccountData.GameRole.PlatName)
|
||||
t.Logf("DisplayName: %s", resp.RoleDisplayName())
|
||||
|
||||
if resp.Status != 200 {
|
||||
t.Errorf("expected status 200, got %d", resp.Status)
|
||||
}
|
||||
if resp.AccountData.GameRole.RoleName != "忧郁嘎巴豆" {
|
||||
t.Errorf("expected roleName 忧郁嘎巴豆, got %s", resp.AccountData.GameRole.RoleName)
|
||||
}
|
||||
if resp.AccountData.GameRole.AreaName != "手Q" {
|
||||
t.Errorf("expected areaName 手Q, got %s", resp.AccountData.GameRole.AreaName)
|
||||
}
|
||||
if resp.AccountData.GameRole.PlatName != "安卓(android)" {
|
||||
t.Errorf("expected platName 安卓(android), got %s", resp.AccountData.GameRole.PlatName)
|
||||
}
|
||||
if resp.RoleDisplayName() != "忧郁嘎巴豆 [手Q-安卓]" {
|
||||
t.Errorf("expected roleDisplayName 忧郁嘎巴豆 [手Q-安卓], got %s", resp.RoleDisplayName())
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type ActivityClient interface {
|
||||
QueryUserScore(ctx context.Context, uid int64, cookie string, sid int32) (*GetUserScoreResp, error)
|
||||
GetUserPrizeRecords(ctx context.Context, uid int64, cookie string, sid int32) (*GetUserPrizeRecordsResp, error)
|
||||
ScoreExchangePrize(ctx context.Context, uid int64, cookie string, sid, pid int32) (*ScoreExchangePrizeResp, error)
|
||||
CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*CheckUserBindGameAccountResp, error)
|
||||
}
|
||||
|
||||
// NewActivityClient 按传输层创建活动客户端,供 CLI 和 GUI 共用。
|
||||
|
||||
@@ -276,3 +276,21 @@ func (c *HTTPClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie s
|
||||
}
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *HTTPClient) CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*CheckUserBindGameAccountResp, error) {
|
||||
if bActID <= 0 {
|
||||
bActID = 9271
|
||||
}
|
||||
req := &CheckUserBindGameAccountReq{
|
||||
User: c.buildUser(uid, cookie, ActivityWebUA),
|
||||
BActID: bActID,
|
||||
IsUseOuterActID: 1,
|
||||
}
|
||||
var resp CheckUserBindGameAccountResp
|
||||
_, err := c.CallRPC(ctx, ServantWebActUI, FuncCheckUserBindGameAccount, req, &resp, CallOptions{UID: uid, Cookie: cookie, RequestID: 1})
|
||||
if err == nil {
|
||||
c.logf("[HTTP] 绑定角色已解码 uid=%d status=%d role=%s game=%s msg=%s", uid, resp.Status, resp.RoleDisplayName(), resp.GameName, resp.Msg)
|
||||
}
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
|
||||
@@ -793,6 +793,27 @@ func (c *WSSClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie st
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func (c *WSSClient) CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*CheckUserBindGameAccountResp, error) {
|
||||
session, err := c.session(ctx, uid, cookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bActID <= 0 {
|
||||
bActID = 9271
|
||||
}
|
||||
req := &CheckUserBindGameAccountReq{
|
||||
User: c.buildUser(uid, cookie),
|
||||
BActID: bActID,
|
||||
IsUseOuterActID: 1,
|
||||
}
|
||||
var resp CheckUserBindGameAccountResp
|
||||
_, err = session.callRPC(ctx, ServantWebActUI, FuncCheckUserBindGameAccount, req, &resp)
|
||||
if err == nil {
|
||||
c.logf("[WSS] 绑定角色已解码 uid=%d status=%d role=%s game=%s msg=%s", uid, resp.Status, resp.RoleDisplayName(), resp.GameName, resp.Msg)
|
||||
}
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func NormalizeWSSCookie(cookie string) string {
|
||||
cookie = NormalizeCookie(cookie)
|
||||
pairs := CookiePairs(cookie)
|
||||
|
||||
@@ -32,6 +32,10 @@ func (m *mockScoreClient) GetUserPrizeRecords(ctx context.Context, uid int64, co
|
||||
func (m *mockScoreClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie string, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockScoreClient) CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*huya.CheckUserBindGameAccountResp, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
func TestBaselineStoreCRUDAndFlush(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -60,6 +60,24 @@ func (f *fakeActivityClient) ScoreExchangePrize(ctx context.Context, uid int64,
|
||||
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeActivityClient) CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*huya.CheckUserBindGameAccountResp, error) {
|
||||
return &huya.CheckUserBindGameAccountResp{
|
||||
Status: 200,
|
||||
Msg: "请求成功",
|
||||
AccountData: huya.GameAccountData{
|
||||
IsBindAccount: 1,
|
||||
IsBindRole: 1,
|
||||
GameRole: huya.GameRole{
|
||||
RoleName: "测试角色",
|
||||
AreaName: "微信",
|
||||
PlatName: "安卓",
|
||||
},
|
||||
},
|
||||
GameName: "和平精英",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
func (f *fakeActivityClient) PrewarmActivity(ctx context.Context, uid int64, cookie string, sid int32) error {
|
||||
f.mu.Lock()
|
||||
f.prewarmCount++
|
||||
|
||||
@@ -6,7 +6,7 @@ package version
|
||||
// 构建时可用 ldflags 覆盖(构建脚本就是这么做的):
|
||||
//
|
||||
// go build -ldflags "-X huya-go/internal/version.Version=0.5.4"
|
||||
var Version = "0.5.4"
|
||||
var Version = "0.5.5"
|
||||
|
||||
// Display 返回带 v 前缀的展示版本号,例如 v0.5.4。
|
||||
func Display() string {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"info": {
|
||||
"companyName": "huya-go",
|
||||
"productName": "虎牙抢购助手",
|
||||
"productVersion": "0.5.4",
|
||||
"productVersion": "0.5.5",
|
||||
"copyright": "",
|
||||
"comments": "虎牙活动积分兑换/抢购工具"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user