diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 8c7aeec..a0902cc 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -588,6 +588,42 @@ class HuyaHttpClient: return self.call_rpc("shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, uid, guid, cookie) + def query_user_order_list(self, uid, guid, cookie, offset=0, page_size=10, + order_type=1, status=0, timeout=15.0): + """查询用户订单列表,用于支付二维码后的付款状态轮询。""" + from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp + + req = QueryUserOrderListReq() + req.userId = self._build_user(uid, guid, cookie) + req.offset = int(offset or 0) + req.orderType = int(order_type or 1) + req.pageSize = int(page_size or 10) + req.status = int(status or 0) + + result = self.call_rpc( + "shopMiddleUI", + "queryUserOrderList", + req, + QueryUserOrderListRsp, + uid, + guid, + cookie, + timeout=timeout, + ) + if result is not None and getattr(result, "orders", None): + return result + fallback = self.call_rpc( + "revenueWebUI", + "queryUserOrderList", + req, + QueryUserOrderListRsp, + uid, + guid, + cookie, + timeout=timeout, + ) + return fallback if fallback is not None else result + def create_order(self, uid, guid, cookie, pid, spu_id, sku_id, item_count=1, source_id="yellowcarlist", game_id="", scene=7, order_type=None): diff --git a/core/huya/shop_structs.py b/core/huya/shop_structs.py index b107bcc..b95ad7d 100644 --- a/core/huya/shop_structs.py +++ b/core/huya/shop_structs.py @@ -469,6 +469,11 @@ class OrderListShopInfo(TafStruct): def write_to(self, os: TafOutputStream): pass + def to_dict(self) -> dict: + return { + "shop_name": self.shopName, + } + class OrderListGoodsDetail(TafStruct): """订单明细(queryUserOrderList 响应 tag16)""" @@ -494,6 +499,17 @@ class OrderListGoodsDetail(TafStruct): def write_to(self, os: TafOutputStream): pass + def to_dict(self) -> dict: + return { + "spu_id": self.spuId, + "sku_id": self.skuId, + "buyer_uid": self.buyerUid, + "virtual_type": self.virtualType, + "quantity": self.quantity, + "shop_info": self.shopInfo.to_dict() if self.shopInfo else None, + "points": self.points, + } + class OrderListItem(TafStruct): """订单列表项(queryUserOrderList 响应 tag3 的 list 元素)""" @@ -531,6 +547,23 @@ class OrderListItem(TafStruct): def write_to(self, os: TafOutputStream): pass + def to_dict(self) -> dict: + return { + "biz_order_id": self.bizOrderId, + "app_id": self.appId, + "order_id": self.orderId, + "pid": self.pid, + "shop_name": self.shopName, + "order_status": self.orderStatus, + "item_name": self.itemName, + "unit_price": self.unitPrice, + "quantity": self.quantity, + "total_price": self.totalPrice, + "create_time": self.createTime, + "pay_time": self.payTime, + "goods_detail": self.goodsDetail.to_dict() if self.goodsDetail else None, + } + class QueryUserOrderListReq(TafStruct): """查询购买历史订单 (revenueWebUI.queryUserOrderList)""" @@ -578,6 +611,14 @@ class QueryUserOrderListRsp(TafStruct): def write_to(self, os: TafOutputStream): pass + def to_dict(self) -> dict: + return { + "code": self.code, + "message": self.message, + "total_count": self.totalCount, + "orders": [item.to_dict() for item in self.orders], + } + # ============================================================ # 下单 diff --git a/web/backend/services/huya_runner.py b/web/backend/services/huya_runner.py index 5710ca9..1424c51 100644 --- a/web/backend/services/huya_runner.py +++ b/web/backend/services/huya_runner.py @@ -20,6 +20,8 @@ from .huya_service import HUYA_CONFIG_FIELDS, cookie_value, ensure_huya_config, HUYA_RECHARGE_ACT_ID = 25135 HUYA_RECHARGE_SOURCE_ID = "yellowcarlist" HUYA_RECHARGE_SCENE = 4 +HUYA_PAYMENT_POLL_SECONDS = 180 +HUYA_PAYMENT_POLL_INTERVAL = 3 HUYA_RECHARGE_EXTRA_PRODUCTS = [ { "spu_id": "hy-5879340", @@ -186,6 +188,110 @@ class HuyaBatchRunner: task.finished_at = datetime.now(timezone.utc) worker_db.commit() + def _update_task_progress( + self, + worker_db: Session, + task: HuyaTask, + status: str, + message: str, + result: Optional[dict] = None, + ): + task.status = status + task.message = message + if result is not None: + task.result = result + worker_db.commit() + + @staticmethod + def _huya_order_status_label(status: int) -> str: + from core.huya.shop_structs import OrderStatus + + labels = { + OrderStatus.DEPOSIT_WAIT_PAY: "待支付", + OrderStatus.DEPOSIT_PAID: "已支付", + OrderStatus.WAIT_DELIVER: "待发货", + OrderStatus.WAIT_RECEIVE: "待收货", + OrderStatus.FINISHED: "已完成", + OrderStatus.FINISHED_CLOSED: "已关闭", + OrderStatus.CANCELLED: "已取消", + OrderStatus.BALANCE_WAIT_PAY: "尾款待支付", + OrderStatus.CANCELLED_BALANCE_EXPIRED: "尾款超时取消", + } + return labels.get(int(status or 0), str(status or "未知")) + + @classmethod + def _is_huya_order_paid(cls, order) -> bool: + from core.huya.shop_structs import OrderStatus + + paid_statuses = { + OrderStatus.DEPOSIT_PAID, + OrderStatus.WAIT_DELIVER, + OrderStatus.WAIT_RECEIVE, + OrderStatus.FINISHED, + OrderStatus.FINISHED_CLOSED, + } + return int(getattr(order, "payTime", 0) or 0) > 0 or int(getattr(order, "orderStatus", 0) or 0) in paid_statuses + + def _wait_recharge_payment( + self, + client: HuyaHttpClient, + uid: int, + guid: str, + cookie: str, + order_id: int, + result: dict, + ) -> tuple[str, dict | None]: + deadline = time.time() + HUYA_PAYMENT_POLL_SECONDS + order_id_text = str(order_id) + last_order = None + while not self._stop.is_set() and time.time() < deadline: + resp = client.query_user_order_list( + uid=uid, + guid=guid, + cookie=cookie, + offset=0, + page_size=10, + order_type=1, + status=0, + timeout=10.0, + ) + checked_at = datetime.now(timezone.utc).isoformat() + if resp is not None and getattr(resp, "orders", None): + for order in resp.orders: + if str(getattr(order, "orderId", "")) != order_id_text: + continue + last_order = order.to_dict() + status = int(getattr(order, "orderStatus", 0) or 0) + result.update({ + "payment_checked_at": checked_at, + "payment_order": last_order, + "payment_order_status": status, + "payment_order_status_label": self._huya_order_status_label(status), + }) + if self._is_huya_order_paid(order): + result.update({ + "payment_status": "paid", + "payment_status_label": "已支付", + "payment_paid": True, + "payment_paid_at": checked_at, + }) + return "paid", last_order + break + else: + result["payment_checked_at"] = checked_at + if self._stop.wait(HUYA_PAYMENT_POLL_INTERVAL): + break + + result.update({ + "payment_status": "timeout" if not self._stop.is_set() else "stopped", + "payment_status_label": "等待支付超时" if not self._stop.is_set() else "已停止监听", + "payment_paid": False, + "payment_timeout_seconds": HUYA_PAYMENT_POLL_SECONDS, + }) + if last_order: + result["payment_order"] = last_order + return result["payment_status"], last_order + def _execute_query_points( self, worker_db: Session, @@ -745,13 +851,38 @@ class HuyaBatchRunner: "app_order_id": pay_resp.appOrderId, "pay_order_id": pay_resp.payOrderId, "pay_url": pay_resp.payUrl, + "payment_status": "pending", + "payment_status_label": "等待支付", + "payment_paid": False, "goods": detail, "order": order_result, } account.status = "recharge_order_created" account.updated_at = datetime.now(timezone.utc) message = f"{product_name} x{count} {self._pay_channel_label(pay_channel)} {result['amount_text']}" - self._mark_task(worker_db, task, "success", message, result) + self._update_task_progress(worker_db, task, "running", f"{message},等待扫码支付", result) + self._push_log("info", f"[{uid}] 已生成虎牙支付二维码,开始监听订单 {order_resp.orderId}") + + payment_status, payment_order = self._wait_recharge_payment( + client=client, + uid=uid, + guid="", + cookie=cookie, + order_id=order_resp.orderId, + result=result, + ) + account.updated_at = datetime.now(timezone.utc) + if payment_status == "paid": + account.status = "recharge_paid" + paid_message = f"支付成功: {product_name} x{count} {result['amount_text']}" + if payment_order and payment_order.get("pay_time"): + paid_message += f",支付时间 {self._format_local_time(int(payment_order['pay_time']) // 1000)}" + self._mark_task(worker_db, task, "success", paid_message, result) + return + + account.status = "recharge_order_created" + timeout_message = f"{message},{result['payment_status_label']}" + self._mark_task(worker_db, task, "success", timeout_message, result) def _execute_get_bind_qr( self, diff --git a/web/frontend/src/pages/HuyaTasksPage.tsx b/web/frontend/src/pages/HuyaTasksPage.tsx index 06b2181..1053ac0 100644 --- a/web/frontend/src/pages/HuyaTasksPage.tsx +++ b/web/frontend/src/pages/HuyaTasksPage.tsx @@ -132,6 +132,20 @@ function hasPaymentQrcode(task: HuyaTaskItem): boolean { return task.task_type === 'create_recharge_order' && Boolean(resultText(task.result, 'pay_url')); } +function paymentStatus(task: HuyaTaskItem | null | undefined): string { + const status = task?.result?.payment_status; + return typeof status === 'string' ? status : ''; +} + +function paymentStatusLabel(task: HuyaTaskItem | null | undefined): string { + const label = task?.result?.payment_status_label; + return typeof label === 'string' ? label : ''; +} + +function isPaymentFinished(task: HuyaTaskItem | null | undefined): boolean { + return task?.task_type === 'create_recharge_order' && paymentStatus(task) === 'paid'; +} + export default function HuyaTasksPage() { const { token } = theme.useToken(); const [form] = Form.useForm(); @@ -163,6 +177,7 @@ export default function HuyaTasksPage() { const autoOpenQrReady = useRef(false); const autoOpenedPayTaskIds = useRef>(new Set()); const autoOpenPayReady = useRef(false); + const notifiedPaidTaskIds = useRef>(new Set()); const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs(); const { can } = usePermissions(); @@ -265,11 +280,26 @@ export default function HuyaTasksPage() { useEffect(() => { if (!autoOpenPayReady.current || payTask) return; const nextPayTask = tasks - .filter((task) => hasPaymentQrcode(task) && !autoOpenedPayTaskIds.current.has(task.id)) + .filter((task) => hasPaymentQrcode(task) && !isPaymentFinished(task) && !autoOpenedPayTaskIds.current.has(task.id)) .sort((a, b) => b.id - a.id)[0]; if (nextPayTask) openPayTask(nextPayTask); }, [openPayTask, payTask, tasks]); + useEffect(() => { + if (!payTask) return; + const latest = tasks.find((task) => task.id === payTask.id); + if (!latest) return; + if (isPaymentFinished(latest)) { + if (!notifiedPaidTaskIds.current.has(latest.id)) { + notifiedPaidTaskIds.current.add(latest.id); + message.success('虎牙支付成功'); + } + setPayTask(null); + return; + } + if (latest !== payTask) setPayTask(latest); + }, [payTask, tasks]); + const accountOptions = useMemo(() => { return accounts.map((account) => ({ value: account.id, label: accountLabel(account) })); }, [accounts]); @@ -456,6 +486,8 @@ export default function HuyaTasksPage() { const payProductName = resultText(payResult, 'product_name'); const payAmountText = resultText(payResult, 'amount_text'); const payChannelLabel = resultText(payResult, 'pay_channel_label'); + const payStatus = paymentStatus(payTask); + const payStatusText = paymentStatusLabel(payTask); const payOrderId = payResult?.order_id; const payAccountName = payTask ? payTask.account_nickname || payTask.account_uid || `#${payTask.account_id}` @@ -555,10 +587,15 @@ export default function HuyaTasksPage() { } if (record.task_type === 'create_recharge_order') { if (resultText(value, 'pay_url')) { + if (isPaymentFinished(record)) return 已支付; + const status = paymentStatus(record); return ( - + + {status === 'timeout' ? 待支付 : 等待支付} + + ); } return -; @@ -1166,6 +1203,11 @@ export default function HuyaTasksPage() { {payChannelLabel || '支付'}{payAmountText ? ` / ${payAmountText}` : ''}{payAccountName ? ` / ${payAccountName}` : ''} + {payStatusText ? ( + + {payStatusText} + + ) : null} {typeof payOrderId === 'number' || typeof payOrderId === 'string' ? ( 订单号 {String(payOrderId)} ) : null}