实现虎牙查询兑换记录
This commit is contained in:
@@ -120,6 +120,124 @@ class GetActPrizeListReq(TafStruct):
|
|||||||
self.sid = ins.read_int32(1, default=self.sid)
|
self.sid = ins.read_int32(1, default=self.sid)
|
||||||
|
|
||||||
|
|
||||||
|
class GetUserPrizeRecordsReq(TafStruct):
|
||||||
|
"""webActUI.getUserPrizeRecords 请求。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.userId = ActivityUserId()
|
||||||
|
self.sid: int = 0
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_struct(0, self.userId)
|
||||||
|
os.write_int32(1, self.sid)
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.userId = ins.read_struct(0, ActivityUserId) or self.userId
|
||||||
|
self.sid = ins.read_int32(1, default=self.sid)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPrizeRecordItem(TafStruct):
|
||||||
|
"""用户兑换记录项,字段来自 getUserPrizeRecords 实测响应。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.recordId: int = 0
|
||||||
|
self.prizeName: str = ""
|
||||||
|
self.score: int = 0
|
||||||
|
self.exchangeTime: int = 0
|
||||||
|
self.prizeStatus: int = 0
|
||||||
|
self.prizeType: int = 0
|
||||||
|
self.orderId: str = ""
|
||||||
|
self.icon: str = ""
|
||||||
|
self.prizeId: int = 0
|
||||||
|
self.prizeItemId: int = 0
|
||||||
|
self.ext: dict = {}
|
||||||
|
self.exchangeDate: int = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _status_label(status: int) -> str:
|
||||||
|
if status == 0:
|
||||||
|
return "未发放"
|
||||||
|
if status == 1:
|
||||||
|
return "已发放"
|
||||||
|
return f"状态{status}"
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.recordId = ins.read_int64(0, default=self.recordId)
|
||||||
|
self.prizeName = ins.read_string(1, default=self.prizeName)
|
||||||
|
self.score = ins.read_int64(2, default=self.score)
|
||||||
|
self.exchangeTime = ins.read_int64(3, default=self.exchangeTime)
|
||||||
|
self.prizeStatus = ins.read_int32(4, default=self.prizeStatus)
|
||||||
|
self.prizeType = ins.read_int32(10, default=self.prizeType)
|
||||||
|
self.orderId = ins.read_string(12, default=self.orderId)
|
||||||
|
self.icon = ins.read_string(14, default=self.icon)
|
||||||
|
self.prizeId = ins.read_int64(16, default=self.prizeId)
|
||||||
|
self.prizeItemId = ins.read_int64(19, default=self.prizeItemId)
|
||||||
|
self.ext = ins.read_map(20)
|
||||||
|
self.exchangeDate = ins.read_int64(21, default=self.exchangeDate)
|
||||||
|
while True:
|
||||||
|
pos = ins.buf.tell()
|
||||||
|
tag, dtype = ins.read_head()
|
||||||
|
if dtype == TafType.STRUCT_END:
|
||||||
|
ins.buf.seek(pos)
|
||||||
|
return
|
||||||
|
ins.skip_field(dtype)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_list_item(ins: TafInputStream, _tag: int):
|
||||||
|
item = UserPrizeRecordItem()
|
||||||
|
item.read_from(ins)
|
||||||
|
_end_tag, dtype = ins.read_head()
|
||||||
|
if dtype != TafType.STRUCT_END:
|
||||||
|
raise ValueError(f"期望兑换记录 STRUCT_END,实际 0x{dtype:02x}")
|
||||||
|
return item
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"record_id": self.recordId,
|
||||||
|
"prize_name": self.prizeName,
|
||||||
|
"score": self.score,
|
||||||
|
"exchange_time": self.exchangeTime,
|
||||||
|
"exchange_date": self.exchangeDate,
|
||||||
|
"prize_status": self.prizeStatus,
|
||||||
|
"status_label": self._status_label(self.prizeStatus),
|
||||||
|
"order_id": self.orderId,
|
||||||
|
"icon": self.icon,
|
||||||
|
"prize_id": self.prizeId,
|
||||||
|
"prize_type": self.prizeType,
|
||||||
|
"prize_item_id": self.prizeItemId,
|
||||||
|
"ext": dict(self.ext),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GetUserPrizeRecordsResp(TafStruct):
|
||||||
|
"""webActUI.getUserPrizeRecords 响应。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.status: int = 0
|
||||||
|
self.msg: str = ""
|
||||||
|
self.records: list[UserPrizeRecordItem] = []
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.status = ins.read_int32(0, default=self.status)
|
||||||
|
self.msg = ins.read_string(1, default=self.msg)
|
||||||
|
self.records = ins.read_list(2, item_reader=UserPrizeRecordItem.read_list_item)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
records = [item.to_dict() for item in self.records]
|
||||||
|
return {
|
||||||
|
"status": self.status,
|
||||||
|
"msg": self.msg,
|
||||||
|
"record_count": len(records),
|
||||||
|
"records": records,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GetActTaskDetailReq(TafStruct):
|
class GetActTaskDetailReq(TafStruct):
|
||||||
"""webActUI.getActTaskDetail 请求。"""
|
"""webActUI.getActTaskDetail 请求。"""
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,22 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_user_prize_records(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
||||||
|
"""查询用户兑换记录。"""
|
||||||
|
from .activity_structs import GetUserPrizeRecordsReq, GetUserPrizeRecordsResp
|
||||||
|
req = GetUserPrizeRecordsReq()
|
||||||
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
|
req.sid = sid
|
||||||
|
return self.call_rpc(
|
||||||
|
"webActUI",
|
||||||
|
"getUserPrizeRecords",
|
||||||
|
req,
|
||||||
|
GetUserPrizeRecordsResp,
|
||||||
|
uid=uid,
|
||||||
|
cookie=cookie,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0):
|
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0):
|
||||||
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
||||||
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
||||||
|
|||||||
@@ -211,6 +211,65 @@ class HuyaBatchRunner:
|
|||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
|
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
|
||||||
|
|
||||||
|
def _execute_query_exchange_records(
|
||||||
|
self,
|
||||||
|
worker_db: Session,
|
||||||
|
task: HuyaTask,
|
||||||
|
account: HuyaAccount,
|
||||||
|
account_info: dict,
|
||||||
|
config_info: dict,
|
||||||
|
):
|
||||||
|
sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip()
|
||||||
|
if not sid:
|
||||||
|
self._mark_task(worker_db, task, "failed", "请先配置虎牙活动 SID")
|
||||||
|
return
|
||||||
|
|
||||||
|
sid_int = self._to_int(sid)
|
||||||
|
if not sid_int:
|
||||||
|
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
|
||||||
|
return
|
||||||
|
|
||||||
|
uid = self._resolve_uid(account_info)
|
||||||
|
if not uid:
|
||||||
|
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
|
||||||
|
return
|
||||||
|
|
||||||
|
cookie = account_info.get("cookie") or ""
|
||||||
|
if not cookie:
|
||||||
|
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||||
|
return
|
||||||
|
|
||||||
|
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}"))
|
||||||
|
response = client.get_user_prize_records(uid=uid, cookie=cookie, sid=sid_int)
|
||||||
|
if response is None:
|
||||||
|
self._mark_task(worker_db, task, "error", "虎牙兑换记录接口无响应")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = response.to_dict()
|
||||||
|
result["sid"] = sid_int
|
||||||
|
if response.status != 200:
|
||||||
|
self._mark_task(
|
||||||
|
worker_db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
response.msg or f"虎牙兑换记录查询失败: {response.status}",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
records = result.get("records", [])
|
||||||
|
for index, item in enumerate(records, start=1):
|
||||||
|
item["index"] = index
|
||||||
|
item["exchange_time_text"] = self._format_local_time(int(item.get("exchange_time") or 0))
|
||||||
|
if item.get("score") is not None:
|
||||||
|
item["score_text"] = f"{int(item.get('score') or 0)}积分"
|
||||||
|
|
||||||
|
account.status = "exchange_records_queried"
|
||||||
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
|
count = len(records)
|
||||||
|
message = f"兑换记录 {count} 条" if count else "暂无兑换记录"
|
||||||
|
self._mark_task(worker_db, task, "success", message, result)
|
||||||
|
|
||||||
def _execute_refresh_goods(
|
def _execute_refresh_goods(
|
||||||
self,
|
self,
|
||||||
worker_db: Session,
|
worker_db: Session,
|
||||||
@@ -955,6 +1014,7 @@ class HuyaBatchRunner:
|
|||||||
"get_bind_qr",
|
"get_bind_qr",
|
||||||
"confirm_bind",
|
"confirm_bind",
|
||||||
"query_game_name",
|
"query_game_name",
|
||||||
|
"query_exchange_records",
|
||||||
"refresh_goods",
|
"refresh_goods",
|
||||||
"refresh_recharge_goods",
|
"refresh_recharge_goods",
|
||||||
"create_recharge_order",
|
"create_recharge_order",
|
||||||
@@ -978,6 +1038,8 @@ class HuyaBatchRunner:
|
|||||||
self._execute_confirm_bind(worker_db, task, account, account_info, config_info)
|
self._execute_confirm_bind(worker_db, task, account, account_info, config_info)
|
||||||
elif self.task_type == "query_game_name":
|
elif self.task_type == "query_game_name":
|
||||||
self._execute_query_game_name(worker_db, task, account, account_info, config_info)
|
self._execute_query_game_name(worker_db, task, account, account_info, config_info)
|
||||||
|
elif self.task_type == "query_exchange_records":
|
||||||
|
self._execute_query_exchange_records(worker_db, task, account, account_info, config_info)
|
||||||
worker_db.refresh(task)
|
worker_db.refresh(task)
|
||||||
if task.status == "success":
|
if task.status == "success":
|
||||||
self._push_log("success", f"[{current}] {name} {task.message}")
|
self._push_log("success", f"[{current}] {name} {task.message}")
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export default function HuyaTasksPage() {
|
|||||||
const [batchId, setBatchId] = useState<string | null>(null);
|
const [batchId, setBatchId] = useState<string | null>(null);
|
||||||
const [qrTask, setQrTask] = useState<HuyaTaskItem | null>(null);
|
const [qrTask, setQrTask] = useState<HuyaTaskItem | null>(null);
|
||||||
const [payTask, setPayTask] = useState<HuyaTaskItem | null>(null);
|
const [payTask, setPayTask] = useState<HuyaTaskItem | null>(null);
|
||||||
|
const [exchangeRecordsTask, setExchangeRecordsTask] = useState<HuyaTaskItem | null>(null);
|
||||||
const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
|
const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
|
||||||
const autoOpenQrReady = useRef(false);
|
const autoOpenQrReady = useRef(false);
|
||||||
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
|
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
|
||||||
@@ -422,6 +423,14 @@ export default function HuyaTasksPage() {
|
|||||||
const payAccountName = payTask
|
const payAccountName = payTask
|
||||||
? payTask.account_nickname || payTask.account_uid || `#${payTask.account_id}`
|
? payTask.account_nickname || payTask.account_uid || `#${payTask.account_id}`
|
||||||
: '';
|
: '';
|
||||||
|
const exchangeRecordsResult = exchangeRecordsTask?.result || null;
|
||||||
|
const exchangeRecordsRaw = exchangeRecordsResult?.records;
|
||||||
|
const exchangeRecords = Array.isArray(exchangeRecordsRaw)
|
||||||
|
? exchangeRecordsRaw.filter((item) => item && typeof item === 'object') as Record<string, unknown>[]
|
||||||
|
: [];
|
||||||
|
const exchangeRecordsAccountName = exchangeRecordsTask
|
||||||
|
? exchangeRecordsTask.account_nickname || exchangeRecordsTask.account_uid || `#${exchangeRecordsTask.account_id}`
|
||||||
|
: '';
|
||||||
|
|
||||||
const renderTaskResult = (value: Record<string, unknown> | null, record: HuyaTaskItem) => {
|
const renderTaskResult = (value: Record<string, unknown> | null, record: HuyaTaskItem) => {
|
||||||
const bindQrImage = resultText(value, 'mini_qrcode_image');
|
const bindQrImage = resultText(value, 'mini_qrcode_image');
|
||||||
@@ -463,6 +472,18 @@ export default function HuyaTasksPage() {
|
|||||||
if (value?.is_bound === false) return <Tag>未绑定</Tag>;
|
if (value?.is_bound === false) return <Tag>未绑定</Tag>;
|
||||||
return <Text type="secondary">-</Text>;
|
return <Text type="secondary">-</Text>;
|
||||||
}
|
}
|
||||||
|
if (record.task_type === 'query_exchange_records') {
|
||||||
|
const recordCount = value?.record_count;
|
||||||
|
if (typeof recordCount === 'number' && recordCount > 0) {
|
||||||
|
return (
|
||||||
|
<Button size="small" icon={<FieldTimeOutlined />} onClick={() => setExchangeRecordsTask(record)}>
|
||||||
|
查看记录
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof recordCount === 'number') return <Tag>记录 {recordCount} 条</Tag>;
|
||||||
|
return <Text type="secondary">-</Text>;
|
||||||
|
}
|
||||||
if (record.task_type === 'refresh_goods') {
|
if (record.task_type === 'refresh_goods') {
|
||||||
const goodsCount = value?.goods_count;
|
const goodsCount = value?.goods_count;
|
||||||
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
||||||
@@ -603,6 +624,49 @@ export default function HuyaTasksPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const exchangeRecordColumns: TableProps<Record<string, unknown>>['columns'] = [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
width: 70,
|
||||||
|
align: 'center',
|
||||||
|
render: (_: unknown, record, index) => String(record.index || index + 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消耗',
|
||||||
|
width: 110,
|
||||||
|
render: (_: unknown, record) => {
|
||||||
|
const scoreText = record.score_text;
|
||||||
|
if (typeof scoreText === 'string' && scoreText) return scoreText;
|
||||||
|
const score = record.score;
|
||||||
|
return typeof score === 'number' ? `${score}积分` : <Text type="secondary">-</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '奖品名称',
|
||||||
|
dataIndex: 'prize_name',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value: unknown) => typeof value === 'string' && value ? value : <Text type="secondary">-</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '兑换状态',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
render: (_: unknown, record) => {
|
||||||
|
const label = typeof record.status_label === 'string' ? record.status_label : '';
|
||||||
|
const color = label === '已发放' ? 'green' : 'orange';
|
||||||
|
return label ? <Tag color={color}>{label}</Tag> : <Text type="secondary">-</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '兑换时间',
|
||||||
|
width: 170,
|
||||||
|
render: (_: unknown, record) => {
|
||||||
|
const text = record.exchange_time_text;
|
||||||
|
return typeof text === 'string' && text ? text : <Text type="secondary">-</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
<div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
<div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||||
@@ -838,7 +902,7 @@ export default function HuyaTasksPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
当前已接入查询积分、绑定、兑换商品、充值商品列表和扫码支付。
|
当前已接入查询积分、绑定、兑换记录、兑换商品、充值商品列表和扫码支付。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -872,6 +936,29 @@ export default function HuyaTasksPage() {
|
|||||||
style={{ marginTop: 4 }}
|
style={{ marginTop: 4 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="兑换记录"
|
||||||
|
open={!!exchangeRecordsTask}
|
||||||
|
onCancel={() => setExchangeRecordsTask(null)}
|
||||||
|
footer={null}
|
||||||
|
width={760}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
{exchangeRecordsAccountName}{exchangeRecordsResult?.sid ? ` / SID ${String(exchangeRecordsResult.sid)}` : ''}
|
||||||
|
</Text>
|
||||||
|
<Table
|
||||||
|
columns={exchangeRecordColumns}
|
||||||
|
dataSource={exchangeRecords}
|
||||||
|
rowKey={(record) => String(record.record_id || record.order_id || record.index)}
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ y: 360 }}
|
||||||
|
locale={{ emptyText: '暂无兑换记录' }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="绑定小程序码"
|
title="绑定小程序码"
|
||||||
open={!!qrTask}
|
open={!!qrTask}
|
||||||
|
|||||||
Reference in New Issue
Block a user