fix(douyu): 确认绑定回查按已生效绑定判定并支持同步延迟等待 & 失败提示优化为待绑定账号换绑限制文案 & 前端居中弹窗提示
This commit is contained in:
@@ -33,6 +33,8 @@ DOUYU_PAYMENT_POLL_SECONDS = 600
|
|||||||
DOUYU_PAYMENT_POLL_INTERVAL = 5
|
DOUYU_PAYMENT_POLL_INTERVAL = 5
|
||||||
DOUYU_GIFT_POINTS_REFRESH_TIMES = 3
|
DOUYU_GIFT_POINTS_REFRESH_TIMES = 3
|
||||||
DOUYU_GIFT_POINTS_REFRESH_INTERVAL = 2
|
DOUYU_GIFT_POINTS_REFRESH_INTERVAL = 2
|
||||||
|
DOUYU_CONFIRM_EFFECT_POLL_TIMES = 3
|
||||||
|
DOUYU_CONFIRM_EFFECT_POLL_INTERVAL = 3
|
||||||
|
|
||||||
|
|
||||||
class DouyuBatchRunner:
|
class DouyuBatchRunner:
|
||||||
@@ -1189,30 +1191,98 @@ class DouyuBatchRunner:
|
|||||||
|
|
||||||
# 确认接口优先用配置的确认 alias;没有则回退到命中查询的 alias
|
# 确认接口优先用配置的确认 alias;没有则回退到命中查询的 alias
|
||||||
use_confirm_alias = confirm_alias or query_alias
|
use_confirm_alias = confirm_alias or query_alias
|
||||||
|
try:
|
||||||
confirm_result = client.confirm_bind(use_confirm_alias)
|
confirm_result = client.confirm_bind(use_confirm_alias)
|
||||||
|
except DouyuActivityError as exc:
|
||||||
|
confirm_msg = str(exc)
|
||||||
|
self._push_log("warning", f"确认绑定接口失败: {confirm_msg}")
|
||||||
|
# 待绑定游戏账号侧换绑限制(未到换绑时间等)导致确认失败:保留原绑定并给出明确提示
|
||||||
|
self._apply_bind_info_to_account(account, before_bound or before, "game_queried")
|
||||||
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
f"待绑定游戏账号({role_name or '-'})未到换绑时间(不是斗鱼/虎牙账号),请重新换账号扫码绑定",
|
||||||
|
{
|
||||||
|
"act_alias": use_confirm_alias,
|
||||||
|
"query_act_alias": query_alias,
|
||||||
|
"query_act_aliases": query_aliases,
|
||||||
|
"before_bind_info": before,
|
||||||
|
**before_snapshot,
|
||||||
|
"confirm_error": confirm_msg,
|
||||||
|
"bind_ready_for_confirm": True,
|
||||||
|
"bind_confirmed": False,
|
||||||
|
"bind_phase": "confirm_failed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
confirm_raw = confirm_result.get("raw") or {}
|
confirm_raw = confirm_result.get("raw") or {}
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"info",
|
||||||
f"确认绑定接口返回 act={use_confirm_alias} "
|
f"确认绑定接口返回 act={use_confirm_alias} "
|
||||||
f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}",
|
f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}",
|
||||||
)
|
)
|
||||||
|
def _pick_bound_after(candidates: list[dict]) -> dict | None:
|
||||||
|
"""确认后回查:只认已生效绑定(bound_act=1)。
|
||||||
|
|
||||||
|
cjm 的角色名恒有且 bound_act 恒为 0,若按 alias 顺序挑选会永远
|
||||||
|
判定“确认未生效”,必须按 bound_act=1 判定确认结果。
|
||||||
|
"""
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
info
|
||||||
|
for info in candidates
|
||||||
|
if self._is_bound_act(info) and str(info.get("role_name") or "").strip()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
after_candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
after_candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
||||||
after = self._pick_bind_info(
|
if not after_candidates:
|
||||||
after_candidates,
|
|
||||||
baseline_role_name="",
|
|
||||||
baseline_is_bound_act=False,
|
|
||||||
prefer_pending=False,
|
|
||||||
) if after_candidates else None
|
|
||||||
if after is None:
|
|
||||||
raise DouyuActivityError("确认后回查绑定信息失败")
|
raise DouyuActivityError("确认后回查绑定信息失败")
|
||||||
|
after = _pick_bound_after(after_candidates)
|
||||||
|
# 已生效绑定存在同步延迟:确认接口已成功但未生效时短轮询等待
|
||||||
|
if after is None:
|
||||||
|
for _ in range(DOUYU_CONFIRM_EFFECT_POLL_TIMES):
|
||||||
|
if self._stop.wait(DOUYU_CONFIRM_EFFECT_POLL_INTERVAL):
|
||||||
|
break
|
||||||
|
after_candidates = self._fetch_bind_info_candidates(client, query_aliases)
|
||||||
|
if not after_candidates:
|
||||||
|
break
|
||||||
|
after = _pick_bound_after(after_candidates)
|
||||||
|
if after is not None:
|
||||||
|
break
|
||||||
|
if after is None:
|
||||||
|
# 已生效绑定始终未出现:确认未生效(或同步延迟超时),保留原绑定
|
||||||
|
self._apply_bind_info_to_account(account, before_bound or before, "game_queried")
|
||||||
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
f"待绑定游戏账号({role_name or '-'})未到换绑时间(不是斗鱼/虎牙账号),请重新换账号扫码绑定",
|
||||||
|
{
|
||||||
|
"act_alias": use_confirm_alias,
|
||||||
|
"query_act_alias": query_alias,
|
||||||
|
"query_act_aliases": query_aliases,
|
||||||
|
"before_bind_info": before,
|
||||||
|
"confirm": confirm_result,
|
||||||
|
"after_bind_info": None,
|
||||||
|
**before_snapshot,
|
||||||
|
"bind_ready_for_confirm": True,
|
||||||
|
"bind_confirmed": False,
|
||||||
|
"bind_phase": "confirm_failed",
|
||||||
|
"confirm_wait_error": "确认后短轮询未等到已生效绑定",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"info",
|
||||||
f"确认后回查 hit={after.get('act_alias') or '-'} "
|
f"确认后回查 hit={after.get('act_alias') or '-'} "
|
||||||
f"{self._format_bind_summary(after)}",
|
f"{self._format_bind_summary(after)}",
|
||||||
)
|
)
|
||||||
except DouyuActivityError as exc:
|
except DouyuActivityError as exc:
|
||||||
# 确认接口已成功时,回查失败仍按确认成功处理,但保留错误信息。
|
# 查询接口异常:确认接口已成功时按成功处理,但保留错误信息。
|
||||||
# 写库优先确认前已生效绑定,避免待确认角色被误写入。
|
# 写库优先确认前已生效绑定,避免待确认角色被误写入。
|
||||||
self._apply_bind_info_to_account(account, before_bound or before, "bind_confirmed")
|
self._apply_bind_info_to_account(account, before_bound or before, "bind_confirmed")
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
@@ -1236,33 +1306,8 @@ class DouyuBatchRunner:
|
|||||||
return
|
return
|
||||||
|
|
||||||
after_snapshot = self._bind_snapshot(after)
|
after_snapshot = self._bind_snapshot(after)
|
||||||
final_info = after if after_snapshot["role_name"] else before
|
final_role_name = after_snapshot["role_name"] or role_name
|
||||||
final_snapshot = after_snapshot if after_snapshot["role_name"] else before_snapshot
|
self._apply_bind_info_to_account(account, after, "bind_confirmed")
|
||||||
final_role_name = final_snapshot["role_name"] or role_name
|
|
||||||
if not after_snapshot["is_bound_act"]:
|
|
||||||
# 确认未生效:保留确认前已生效绑定,不把待确认新角色写入账号表
|
|
||||||
self._apply_bind_info_to_account(account, before_bound or before, "game_queried")
|
|
||||||
self._mark_task(
|
|
||||||
db,
|
|
||||||
task,
|
|
||||||
"failed",
|
|
||||||
f"确认绑定未生效: {final_role_name}",
|
|
||||||
{
|
|
||||||
"act_alias": use_confirm_alias,
|
|
||||||
"query_act_alias": after.get("act_alias") or query_alias,
|
|
||||||
"query_act_aliases": query_aliases,
|
|
||||||
"before_bind_info": before,
|
|
||||||
"confirm": confirm_result,
|
|
||||||
**final_snapshot,
|
|
||||||
"bind_ready_for_confirm": True,
|
|
||||||
"bind_confirmed": False,
|
|
||||||
"bind_phase": "confirm_failed",
|
|
||||||
"after_bind_info": after,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._apply_bind_info_to_account(account, final_info, "bind_confirmed")
|
|
||||||
self._mark_task(
|
self._mark_task(
|
||||||
db,
|
db,
|
||||||
task,
|
task,
|
||||||
@@ -1275,7 +1320,7 @@ class DouyuBatchRunner:
|
|||||||
"before_bind_info": before,
|
"before_bind_info": before,
|
||||||
"confirm": confirm_result,
|
"confirm": confirm_result,
|
||||||
"after_bind_info": after,
|
"after_bind_info": after,
|
||||||
**final_snapshot,
|
**after_snapshot,
|
||||||
"bind_ready_for_confirm": True,
|
"bind_ready_for_confirm": True,
|
||||||
"bind_confirmed": True,
|
"bind_confirmed": True,
|
||||||
"bind_phase": "confirmed",
|
"bind_phase": "confirmed",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
CheckCircleOutlined, CopyOutlined, CreditCardOutlined, FieldTimeOutlined, GiftOutlined,
|
CheckCircleOutlined, CopyOutlined, CreditCardOutlined, ExclamationCircleOutlined, FieldTimeOutlined, GiftOutlined,
|
||||||
ImportOutlined, QrcodeOutlined, ReloadOutlined,
|
ImportOutlined, QrcodeOutlined, ReloadOutlined,
|
||||||
SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
|
SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -116,6 +116,7 @@ const ESPORTS_TASK_TYPES = new Set([
|
|||||||
]);
|
]);
|
||||||
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
||||||
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
||||||
|
const CONFIRM_FAIL_TIP_SECONDS = 6;
|
||||||
|
|
||||||
function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
|
function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
|
||||||
const value = result?.[key];
|
const value = result?.[key];
|
||||||
@@ -308,6 +309,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const autoOpenedEsportsBindTaskIds = useRef<Set<number>>(new Set());
|
const autoOpenedEsportsBindTaskIds = useRef<Set<number>>(new Set());
|
||||||
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
|
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
|
||||||
const autoCopiedExchangeTaskIds = useRef<Set<number>>(new Set());
|
const autoCopiedExchangeTaskIds = useRef<Set<number>>(new Set());
|
||||||
|
const autoNotifiedConfirmFailedTaskIds = useRef<Set<number>>(new Set());
|
||||||
|
const [confirmFailTip, setConfirmFailTip] = useState<{ taskId: number; message: string } | null>(null);
|
||||||
|
const [confirmFailCountdown, setConfirmFailCountdown] = useState(0);
|
||||||
const [exchangePreview, setExchangePreview] = useState<{ task: DouyuTaskItem; url: string } | null>(null);
|
const [exchangePreview, setExchangePreview] = useState<{ task: DouyuTaskItem; url: string } | null>(null);
|
||||||
const autoOpenQrReady = useRef(false);
|
const autoOpenQrReady = useRef(false);
|
||||||
const autoOpenEsportsBindReady = useRef(false);
|
const autoOpenEsportsBindReady = useRef(false);
|
||||||
@@ -608,6 +612,40 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
);
|
);
|
||||||
}, [visibleTasks]);
|
}, [visibleTasks]);
|
||||||
|
|
||||||
|
// 确认绑定失败自动弹提示(页面中央弹窗,自动消失,一次性去重)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoOpenQrReady.current) return;
|
||||||
|
const failedTasks = visibleTasks
|
||||||
|
.filter((task) => (
|
||||||
|
task.task_type === 'confirm_bind'
|
||||||
|
&& ['failed', 'error'].includes(task.status)
|
||||||
|
&& (task.message || '').includes('待绑定游戏账号')
|
||||||
|
&& !autoNotifiedConfirmFailedTaskIds.current.has(task.id)
|
||||||
|
))
|
||||||
|
.sort((a, b) => b.id - a.id);
|
||||||
|
if (failedTasks.length === 0) return;
|
||||||
|
for (const task of failedTasks) autoNotifiedConfirmFailedTaskIds.current.add(task.id);
|
||||||
|
const latest = failedTasks[0];
|
||||||
|
setConfirmFailTip({ taskId: latest.id, message: latest.message });
|
||||||
|
}, [visibleTasks]);
|
||||||
|
|
||||||
|
// 弹窗倒计时自动关闭
|
||||||
|
useEffect(() => {
|
||||||
|
if (!confirmFailTip) return;
|
||||||
|
setConfirmFailCountdown(CONFIRM_FAIL_TIP_SECONDS);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setConfirmFailCountdown((cur) => {
|
||||||
|
if (cur <= 1) {
|
||||||
|
clearInterval(timer);
|
||||||
|
setConfirmFailTip(null);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return cur - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [confirmFailTip]);
|
||||||
|
|
||||||
const copyExchangePreview = async () => {
|
const copyExchangePreview = async () => {
|
||||||
if (!exchangePreview) return;
|
if (!exchangePreview) return;
|
||||||
try {
|
try {
|
||||||
@@ -1891,6 +1929,27 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 确认绑定失败提示 Modal(居中,自动消失) */}
|
||||||
|
<Modal
|
||||||
|
title={<Space><ExclamationCircleOutlined style={{ color: '#faad14' }} /><span>绑定失败提示</span></Space>}
|
||||||
|
open={Boolean(confirmFailTip)}
|
||||||
|
onCancel={() => setConfirmFailTip(null)}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
closable={false}
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', padding: '16px 8px 8px' }}>
|
||||||
|
<ExclamationCircleOutlined style={{ fontSize: 42, color: '#faad14' }} />
|
||||||
|
<div style={{ fontSize: 14, marginTop: 14, lineHeight: 1.7 }}>
|
||||||
|
{confirmFailTip?.message || ''}
|
||||||
|
</div>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, marginTop: 12, display: 'inline-block' }}>
|
||||||
|
{confirmFailCountdown} 秒后自动关闭
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user