diff --git a/tests/test_huya_workbench_scopes.py b/tests/test_huya_workbench_scopes.py index dbb129e..f026308 100644 --- a/tests/test_huya_workbench_scopes.py +++ b/tests/test_huya_workbench_scopes.py @@ -1,7 +1,13 @@ import pytest from tests.support.db import get_test_engine, make_test_session -from web.backend.models import HuyaAccount, HuyaWorkbench, HuyaWorkbenchAccount, User +from web.backend.models import ( + HuyaAccount, + HuyaTask, + HuyaWorkbench, + HuyaWorkbenchAccount, + User, +) from web.backend.routers.huya import ( delete_account, list_huya_account_ids, @@ -108,6 +114,47 @@ class TestHuyaWorkbenchScope: ) assert searched["account_ids"] == [no_cookie.id] + def test_recharge_order_tasks_created_for_every_account(self): + """多选账号生成支付码:每个账号一个任务(一个账号一张码,各自监听支付)。 + + 商品快照类任务(refresh_*)仍然是全局的,多选也只建一个。 + """ + other = HuyaAccount(uid="300", yyuid="300", cookie="yyuid=300; udb_uid=300") + self.session.add(other) + self.session.commit() + account_ids = [self.account.id, other.id] + + batch_id, count = create_planned_tasks( + self.session, + account_ids, + "create_recharge_order", + self.user.id, + payload={"spu_id": "hy-8328015"}, + handbook_scope="elite", + ) + + assert count == 2 + tasks = self.session.query(HuyaTask).filter(HuyaTask.batch_id == batch_id).all() + assert len(tasks) == 2 + # 每个选中的账号都拿到一个码,而不是只有第一个 + assert sorted(task.account_id for task in tasks) == sorted(account_ids) + assert {task.task_type for task in tasks} == {"create_recharge_order"} + + snapshot_batch, snapshot_count = create_planned_tasks( + self.session, + account_ids, + "refresh_recharge_goods", + self.user.id, + handbook_scope="elite", + ) + assert snapshot_count == 1 + assert ( + self.session.query(HuyaTask) + .filter(HuyaTask.batch_id == snapshot_batch) + .count() + == 1 + ) + def test_account_delete_cleans_workbench_membership(self): self.session.add( HuyaWorkbenchAccount( diff --git a/web/backend/services/huya_service.py b/web/backend/services/huya_service.py index d885c4b..447f885 100644 --- a/web/backend/services/huya_service.py +++ b/web/backend/services/huya_service.py @@ -523,12 +523,10 @@ def create_planned_tasks( batch_id = uuid.uuid4().hex[:12] payload = payload or {} accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all() - if ( - task_type - in {"refresh_goods", "refresh_recharge_goods", "create_recharge_order"} - and accounts - ): - # 全局快照和单笔支付二维码使用一个选中的 CK 即可;兑换商品需要保留多账号批量任务。 + if task_type in {"refresh_goods", "refresh_recharge_goods"} and accounts: + # 商品快照是全局的(不分账号),一个选中的 CK 就够了。 + # 注意:create_recharge_order 不能截断 —— 一个账号一张支付码, + # 多选时要同时生成多个码各自监听支付(前端弹窗并排展示)。 accounts = accounts[:1] for account in accounts: db.add( diff --git a/web/frontend/src/pages/HuyaElitePage.tsx b/web/frontend/src/pages/HuyaElitePage.tsx index 0128c5c..72272ba 100644 --- a/web/frontend/src/pages/HuyaElitePage.tsx +++ b/web/frontend/src/pages/HuyaElitePage.tsx @@ -232,7 +232,11 @@ export default function HuyaElitePage() { const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu); return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' }; })() : {}; - const created = await huyaApi.createTasks({ account_ids: accountIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload }); + // 支付码要「同时弹出多个二维码」:并发必须 >= 账号数,否则排在后面的码要等前面的码付完/超时才开始生成 + const batchConcurrency = taskType === 'create_recharge_order' + ? Math.min(10, Math.max(concurrency, accountIds.length)) + : concurrency; + const created = await huyaApi.createTasks({ account_ids: accountIds, task_type: taskType, handbook_scope: SCOPE, concurrency: batchConcurrency, payload }); setActiveBatches((prev) => [...new Set([...prev, created.batch_id])]); if (taskType === 'create_recharge_order') { // 多选账号时每个账号一个支付码:收进同一列表,弹窗里一起展示 @@ -245,7 +249,8 @@ export default function HuyaElitePage() { const next = raw as unknown as HuyaTaskItem; setTasks((prev) => [next, ...prev.filter((item) => item.id !== next.id)].slice(0, 300)); const result = next.result || {}; - if (next.task_type === 'create_recharge_order' && result.pay_url) setPayOpen(true); + // 支付弹窗只在「生成开通支付码」和点账号行时打开:多选时每个码都会回包, + // 这里再自动弹会把用户刚关掉的弹窗反复顶出来(码会在弹窗里自动补齐)。 if (next.task_type === 'get_bind_qr' && (result.mini_qrcode_image || result.bind_redirect_url)) setQrTask(next); }, onResult: () => { setActiveBatches((prev) => prev.filter((id) => id !== created.batch_id)); void load(); }, @@ -322,6 +327,15 @@ export default function HuyaElitePage() { }, [tasks, payBatchIds]); const payPendingTasks = payTasks.filter(needsScanPay); const payDoneTasks = payTasks.filter((task) => !needsScanPay(task)); + // 多选账号时一次生成多个码:个别账号失败(限购、CK 失效…)要单独点名,否则少一个码看不出原因 + const payFailedTasks = useMemo(() => tasks.filter((task) => task.task_type === 'create_recharge_order' + && payBatchIds.includes(task.batch_id) + && ['failed', 'error', 'stopped'].includes(task.status)), [tasks, payBatchIds]); + // 还在排队/生成中的码(并发不够时,后面的码要等前面的码付完或超时才轮到) + const payQueuedCount = useMemo(() => tasks.filter((task) => task.task_type === 'create_recharge_order' + && payBatchIds.includes(task.batch_id) + && ['planned', 'pending', 'running'].includes(task.status) + && !resultText(task, 'pay_url')).length, [tasks, payBatchIds]); const openPayTasks = (batchId?: string) => { if (batchId) setPayBatchIds((prev) => (prev.includes(batchId) ? prev : [...prev, batchId])); @@ -379,35 +393,37 @@ export default function HuyaElitePage() { setQrTask(null)} centered>{qrTask &&
{resultText(qrTask, 'mini_qrcode_image') ? 绑定二维码 : }

{qrTask.message || qrTask.status}

}
setPayOpen(false)} - footer={支付后会自动确认开通状态;已开通的码自动收起} + footer={多选账号会同时生成多个码并排展示,各自独立等待支付;支付后自动确认开通状态} > {payTasks.length === 0 ?
正在为选中的账号生成支付码…
:
- {payPendingTasks.length > 0 && + {payQueuedCount > 0 &&
还有 {payQueuedCount} 个账号在排队生成支付码(并发不够时会等已出的码付完/超时,可调大右侧「并发」)
} + {payPendingTasks.length > 0 &&
{payPendingTasks.map((task) => { const qrValue = payQrValue(task); return ( -
-
{resultText(task, 'mini_qrcode_image') ? 支付码 : }
- - {payTaskLabel(task)} - UID {task.account_uid || '-'} - {[resultText(task, 'product_name') || '精英宝典', resultText(task, 'amount_text')].filter(Boolean).join(' ')} - {resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status} - {task.message || '-'} - {isLoopbackUrl(qrValue) && 二维码是本机地址(localhost),手机扫不开;请用公网域名打开本页后重新生成} - - - - - +
+
{resultText(task, 'mini_qrcode_image') ? 支付码 : }
+
{payTaskLabel(task)}
+ UID {task.account_uid || '-'} +
{[resultText(task, 'product_name') || '精英宝典', resultText(task, 'amount_text')].filter(Boolean).join(' ')}
+
{resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status}
+ {task.message || '-'} + {isLoopbackUrl(qrValue) &&
二维码是本机地址(localhost),手机扫不开
} +
); })} - } +
} + {payFailedTasks.length > 0 &&
+ 生成失败(没有二维码) +
+ {payFailedTasks.map((task) => {payTaskLabel(task)} · {task.message || '生成失败'})} +
+
} {payDoneTasks.length > 0 &&
已完成(二维码已收起)