优化斗鱼任务操作体验

This commit is contained in:
yml2213
2026-07-25 11:58:30 +08:00
parent 6ca7898520
commit 63962811f8
2 changed files with 113 additions and 13 deletions
+72 -2
View File
@@ -31,6 +31,8 @@ DOUYU_BIND_ROLE_POLL_SECONDS = 65
DOUYU_BIND_ROLE_POLL_INTERVAL = 5
DOUYU_PAYMENT_POLL_SECONDS = 600
DOUYU_PAYMENT_POLL_INTERVAL = 5
DOUYU_GIFT_POINTS_REFRESH_TIMES = 3
DOUYU_GIFT_POINTS_REFRESH_INTERVAL = 2
class DouyuBatchRunner:
@@ -476,6 +478,51 @@ class DouyuBatchRunner:
result["gold_balance"] = last_gold
return False
def _refresh_points_after_elite_gift(
self,
db: Session,
task: DouyuTask,
account: Account,
client: DouyuActivityClient,
cookie: str,
ctn: str | None,
result: dict,
baseline_points: int | None,
gift_count: int,
) -> dict:
"""赠送精英令后短轮询积分;1 个精英令约等于 10 积分。"""
expected_gain = max(0, gift_count) * 10
target_points = baseline_points + expected_gain if baseline_points is not None else None
result["gift_points_baseline"] = baseline_points
result["gift_points_expected_gain"] = expected_gain
result["gift_points_target"] = target_points
last_points = None
refresh_result: dict = {}
for index in range(1, DOUYU_GIFT_POINTS_REFRESH_TIMES + 1):
refresh_result = self._refresh_account_points(client, account, cookie, ctn=ctn)
db.commit()
last_points = refresh_result["points"]
result.update(refresh_result)
result["gift_points_refresh_count"] = index
if target_points is None or (last_points is not None and last_points >= target_points):
result["gift_points_confirmed"] = target_points is None or last_points is not None
return refresh_result
if index < DOUYU_GIFT_POINTS_REFRESH_TIMES:
self._update_task_progress(
db,
task,
"running",
f"赠送精英令成功,等待积分同步(当前 {last_points if last_points is not None else '-'},预期 {target_points}",
result,
)
if self._stop.wait(DOUYU_GIFT_POINTS_REFRESH_INTERVAL):
break
result["gift_points_confirmed"] = False
result["points"] = last_points
return refresh_result
def _task_payload(self, task: DouyuTask) -> dict:
result = task.result if isinstance(task.result, dict) else {}
payload = result.get("payload") if isinstance(result.get("payload"), dict) else {}
@@ -1088,20 +1135,41 @@ class DouyuBatchRunner:
payload = self._task_payload(task)
gift_count = int(payload.get("gift_count") or payload.get("count") or 1)
client = self._client(cookie)
ctn = None
baseline_points = account.points
try:
ctn = client.acf_ccn(refresh_subscribe=False)
baseline_result = self._refresh_account_points(client, account, cookie, ctn=ctn)
db.commit()
baseline_points = baseline_result["points"]
except Exception as exc:
self._push_log("warning", f"赠送精英令前刷新积分失败: {exc}")
result = client.donate_elite_gift(
gift_count=gift_count,
room_id=str(payload.get("room_id") or config["room_id"]),
gift_id=str(payload.get("gift_id") or config["gift_id"]),
skin_id=str(payload.get("skin_id") or config["skin_id"]),
)
result["gift_points_baseline"] = baseline_points
refresh_errors = []
try:
result.update(self._refresh_account_gold_balance(client, account))
except Exception as exc:
refresh_errors.append(f"鱼翅余额: {exc}")
try:
ctn = client.acf_ccn(refresh_subscribe=False)
result.update(self._refresh_account_points(client, account, cookie, ctn=ctn))
result.update(
self._refresh_points_after_elite_gift(
db,
task,
account,
client,
cookie,
ctn,
result,
baseline_points,
gift_count,
)
)
except Exception as exc:
refresh_errors.append(f"积分: {exc}")
if refresh_errors:
@@ -1113,6 +1181,8 @@ class DouyuBatchRunner:
message += f",鱼翅余额: {account.gold_balance}"
if account.points is not None:
message += f",积分: {account.points}"
if result.get("gift_points_target") is not None and not result.get("gift_points_confirmed"):
message += f"(未确认涨到 {result['gift_points_target']}"
self._mark_task(db, task, "success", message, result)
def _execute_query_points(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
+41 -11
View File
@@ -40,6 +40,8 @@ const QUICK_ACTIONS = [
const BIND_ACTION_KEYS = ['get_bind_qr', 'confirm_bind', 'query_game_name', 'query_change_bind_time'];
const QUERY_ACTION_KEYS = ['query_points', 'query_limited_goods', 'query_gold_balance'];
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
const STATUS_COLORS: Record<string, string> = {
planned: 'default', pending: 'default', running: 'processing',
@@ -98,6 +100,12 @@ function hasPaymentQrcode(task: DouyuTaskItem | null | undefined): boolean {
return Boolean(task && ['create_elite_qr', 'create_gold_qr'].includes(task.task_type) && resultText(task.result, 'pay_url'));
}
function paymentArrived(task: DouyuTaskItem | null | undefined): boolean {
if (!task || !['create_elite_qr', 'create_gold_qr'].includes(task.task_type)) return false;
if (task.status !== 'success') return false;
return task.result?.gold_recharged === true || task.result?.elite_opened === true;
}
function bindPhaseText(phase: string, polling: boolean): string {
if (phase === 'role_ready') return '已识别角色,待确认';
if (phase === 'change_waiting') return '换绑冷却中';
@@ -136,6 +144,11 @@ function resultNumber(result: Record<string, unknown> | null | undefined, key: s
return null;
}
function savedPositiveInteger(key: string, fallback = 1): number {
const value = Number(localStorage.getItem(key));
return Number.isInteger(value) && value >= 1 ? value : fallback;
}
export default function DouyuTasksPage() {
const { can } = usePermissions();
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
@@ -148,8 +161,8 @@ export default function DouyuTasksPage() {
const [runningBatchId, setRunningBatchId] = useState('');
const [configOpen, setConfigOpen] = useState(false);
const [concurrency, setConcurrency] = useState(3);
const [goldAmount, setGoldAmount] = useState(1);
const [giftCount, setGiftCount] = useState(1);
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
const [selectedGoodsId, setSelectedGoodsId] = useState('');
const [accountSearch, setAccountSearch] = useState('');
@@ -242,6 +255,14 @@ export default function DouyuTasksPage() {
useEffect(() => { loadData(); }, [loadData]);
useEffect(() => {
localStorage.setItem(DOUYU_GOLD_AMOUNT_STORAGE_KEY, String(goldAmount));
}, [goldAmount]);
useEffect(() => {
localStorage.setItem(DOUYU_GIFT_COUNT_STORAGE_KEY, String(giftCount));
}, [giftCount]);
const hasActiveTasks = useMemo(
() => tasks.some((task) => ['pending', 'running', 'planned'].includes(task.status)),
[tasks],
@@ -279,7 +300,7 @@ export default function DouyuTasksPage() {
useEffect(() => {
if (!autoOpenPayReady.current || payTask) return;
const nextPayTask = tasks
.filter((task) => hasPaymentQrcode(task) && !autoOpenedPayTaskIds.current.has(task.id))
.filter((task) => hasPaymentQrcode(task) && !paymentArrived(task) && !autoOpenedPayTaskIds.current.has(task.id))
.sort((a, b) => b.id - a.id)[0];
if (nextPayTask) openPayTask(nextPayTask);
}, [openPayTask, payTask, tasks]);
@@ -294,7 +315,12 @@ export default function DouyuTasksPage() {
useEffect(() => {
if (!payTask) return;
const latest = tasks.find((task) => task.id === payTask.id);
if (latest && latest !== payTask) setPayTask(latest);
if (!latest) return;
if (paymentArrived(latest)) {
setPayTask(null);
return;
}
if (latest !== payTask) setPayTask(latest);
}, [payTask, tasks]);
const batchBusy = Boolean(runningBatchId || logs.connected || hasActiveTasks);
@@ -749,13 +775,14 @@ export default function DouyuTasksPage() {
gap: 12, overflow: 'hidden',
}}>
{/* Left: accounts + tasks */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, overflow: 'hidden' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
<Card
size="small" title="账号"
extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>}
styles={{ body: { flex: 1, overflow: 'auto', padding: 8 } }}
style={{ flex: '1 1 0', minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }}
>
<Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap>
<Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
<Space>
<Input
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
@@ -773,6 +800,7 @@ export default function DouyuTasksPage() {
<Table
rowKey="id"
size="small"
style={{ flex: 1, minHeight: 0 }}
loading={loading}
rowSelection={{
selectedRowKeys: selectedIds,
@@ -781,7 +809,7 @@ export default function DouyuTasksPage() {
columns={accountColumns}
dataSource={filteredAccounts}
pagination={{ pageSize: 15, showSizeChanger: false }}
scroll={{ y: 'calc(100vh - 520px)' }}
scroll={{ y: 'max(180px, calc(100vh - 650px))' }}
onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' },
@@ -798,12 +826,14 @@ export default function DouyuTasksPage() {
<Tag color="error"> {tasks.filter((t) => ['failed', 'error'].includes(t.status)).length}</Tag>
</Space>
}
styles={{ body: { padding: 8 } }}
style={{ flex: '0 0 300px', minHeight: 260, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
styles={{ body: { flex: 1, minHeight: 0, padding: 8, overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
>
<Table
rowKey="id" size="small" columns={taskColumns} dataSource={tasks}
loading={loading} pagination={{ pageSize: 10, showSizeChanger: false }}
scroll={{ x: 700 }}
style={{ flex: 1, minHeight: 0 }}
loading={loading} pagination={{ pageSize: 4, showSizeChanger: false, size: 'small' }}
scroll={{ x: 700, y: 150 }}
/>
</Card>
</div>