diff --git a/core/huya/activity_structs.py b/core/huya/activity_structs.py index 870d9c1..13edace 100644 --- a/core/huya/activity_structs.py +++ b/core/huya/activity_structs.py @@ -120,6 +120,124 @@ class GetActPrizeListReq(TafStruct): 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): """webActUI.getActTaskDetail 请求。""" diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 8b67a44..416ec21 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -286,6 +286,22 @@ class HuyaHttpClient: 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): """查询活动任务详情,用于发现充值商品 SPU。""" from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp diff --git a/web/backend/services/huya_runner.py b/web/backend/services/huya_runner.py index c8b1c21..aebb0fd 100644 --- a/web/backend/services/huya_runner.py +++ b/web/backend/services/huya_runner.py @@ -211,6 +211,65 @@ class HuyaBatchRunner: account.updated_at = datetime.now(timezone.utc) 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( self, worker_db: Session, @@ -955,6 +1014,7 @@ class HuyaBatchRunner: "get_bind_qr", "confirm_bind", "query_game_name", + "query_exchange_records", "refresh_goods", "refresh_recharge_goods", "create_recharge_order", @@ -978,6 +1038,8 @@ class HuyaBatchRunner: self._execute_confirm_bind(worker_db, task, account, account_info, config_info) elif self.task_type == "query_game_name": 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) if task.status == "success": self._push_log("success", f"[{current}] {name} {task.message}") diff --git a/web/frontend/src/pages/HuyaTasksPage.tsx b/web/frontend/src/pages/HuyaTasksPage.tsx index 30b62b8..6851faf 100644 --- a/web/frontend/src/pages/HuyaTasksPage.tsx +++ b/web/frontend/src/pages/HuyaTasksPage.tsx @@ -153,6 +153,7 @@ export default function HuyaTasksPage() { const [batchId, setBatchId] = useState(null); const [qrTask, setQrTask] = useState(null); const [payTask, setPayTask] = useState(null); + const [exchangeRecordsTask, setExchangeRecordsTask] = useState(null); const autoOpenedQrTaskIds = useRef>(new Set()); const autoOpenQrReady = useRef(false); const autoOpenedPayTaskIds = useRef>(new Set()); @@ -422,6 +423,14 @@ export default function HuyaTasksPage() { const payAccountName = payTask ? 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[] + : []; + const exchangeRecordsAccountName = exchangeRecordsTask + ? exchangeRecordsTask.account_nickname || exchangeRecordsTask.account_uid || `#${exchangeRecordsTask.account_id}` + : ''; const renderTaskResult = (value: Record | null, record: HuyaTaskItem) => { const bindQrImage = resultText(value, 'mini_qrcode_image'); @@ -463,6 +472,18 @@ export default function HuyaTasksPage() { if (value?.is_bound === false) return 未绑定; return -; } + if (record.task_type === 'query_exchange_records') { + const recordCount = value?.record_count; + if (typeof recordCount === 'number' && recordCount > 0) { + return ( + + ); + } + if (typeof recordCount === 'number') return 记录 {recordCount} 条; + return -; + } if (record.task_type === 'refresh_goods') { const goodsCount = value?.goods_count; if (typeof goodsCount === 'number') return 商品 {goodsCount} 个; @@ -603,6 +624,49 @@ export default function HuyaTasksPage() { }, ]; + const exchangeRecordColumns: TableProps>['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}积分` : -; + }, + }, + { + title: '奖品名称', + dataIndex: 'prize_name', + ellipsis: true, + render: (value: unknown) => typeof value === 'string' && value ? value : -, + }, + { + 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 ? {label} : -; + }, + }, + { + title: '兑换时间', + width: 170, + render: (_: unknown, record) => { + const text = record.exchange_time_text; + return typeof text === 'string' && text ? text : -; + }, + }, + ]; + return (
@@ -838,7 +902,7 @@ export default function HuyaTasksPage() { ))}
- 当前已接入查询积分、绑定、兑换商品、充值商品列表和扫码支付。 + 当前已接入查询积分、绑定、兑换记录、兑换商品、充值商品列表和扫码支付。
@@ -872,6 +936,29 @@ export default function HuyaTasksPage() { style={{ marginTop: 4 }} /> + setExchangeRecordsTask(null)} + footer={null} + width={760} + > + + + {exchangeRecordsAccountName}{exchangeRecordsResult?.sid ? ` / SID ${String(exchangeRecordsResult.sid)}` : ''} + + String(record.record_id || record.order_id || record.index)} + size="small" + pagination={false} + scroll={{ y: 360 }} + locale={{ emptyText: '暂无兑换记录' }} + /> + + +