feat(douyu): 精英宝典支持查询开通状态,开通前先校验且不再误判到账
线上两个问题:工作台看不到账号有没有开通精英宝典;点开通时二维码会「闪一下 就变小」——大图弹窗 2~4 秒后自动关闭,只剩列表里 56px 的小二维码。 根因: - getUserInfo 的 manualType/expireTime 才是开通状态的权威口径,但只有兑换 流程用过一次,没有任务类型对外暴露;前端只看积分列,而「开通后积分花完」 和「从未开通」都是 0 分,区分不了(实测 13456 manualType=1/score=0); - 开通前不查状态,且到账判定写成「积分 >= 300」:账上本来就有 680 分的账号 在出码后第一轮轮询就被判成功,前端按 paymentArrived 自动关掉了弹窗。 改动: - 新增任务类型 query_elite_status:查 manualType/manualScore/expireTime 并 落库到 accounts.elite_opened / elite_expire_time(迁移 20261001_0036), 精英工作台加查询按钮与「宝典状态」列(已开通+到期 / 未开通); - create_elite_qr 开通前先查状态:已开通直接判成功、不再生成支付码(方案 A), 既消除二维码闪现,也避免给已开通账号开出真实支付单; - 到账判定改为「积分比开通前增长」,基线用与轮询同源的 credit/balance 取, 取不到基线才退回旧口径; - 支付弹窗不再自动关闭:到账/失败/停止都在弹窗里给结论 + 手动关闭;列表小 二维码在任务进终态后隐藏。 新增 9 条测试覆盖上述行为,回退 runner 时其中 8 条失败。
This commit is contained in:
@@ -90,6 +90,7 @@ class DouyuBatchRunner(
|
||||
"get_bind_qr": self._execute_get_bind_qr,
|
||||
"confirm_bind": self._execute_confirm_bind,
|
||||
"create_elite_qr": self._execute_create_elite_qr,
|
||||
"query_elite_status": self._execute_query_elite_status,
|
||||
"prepare_esports_bind": self._execute_prepare_esports_bind,
|
||||
"get_esports_bind_qr": self._execute_get_esports_bind_qr,
|
||||
"query_esports_game_name": self._execute_query_esports_game_name,
|
||||
|
||||
@@ -57,6 +57,74 @@ class ManualMixin:
|
||||
account.updated_at = datetime.now(UTC)
|
||||
return {"points": points, "points_query": result}
|
||||
|
||||
@staticmethod
|
||||
def _format_expire_text(expire_time: int | None) -> str:
|
||||
"""把到期时间戳格式化成中文描述,缺失或为 0 时返回空串。"""
|
||||
if not expire_time or expire_time <= 0:
|
||||
return ""
|
||||
expire_at = datetime.fromtimestamp(expire_time, tz=UTC).astimezone()
|
||||
return f"(到期 {expire_at:%Y-%m-%d %H:%M})"
|
||||
|
||||
def _refresh_elite_handbook(
|
||||
self,
|
||||
client: DouyuActivityClient,
|
||||
account: Account,
|
||||
*,
|
||||
manual_id: str,
|
||||
rid: str = "",
|
||||
) -> dict:
|
||||
"""查询精英宝典开通状态(manualType/expireTime)并写回账号。
|
||||
|
||||
积分区分不了「开通后积分花完」和「从未开通」,manualType 才是权威口径:
|
||||
0=未开通,>=1=已开通。
|
||||
"""
|
||||
result = client.elite_user_info(manual_id=manual_id, rid=rid)
|
||||
manual_type = self._to_int(result.get("manual_type")) or 0
|
||||
manual_score = self._to_int(result.get("manual_score"))
|
||||
expire_time = self._to_int(result.get("expire_time")) or 0
|
||||
opened = manual_type >= 1
|
||||
account.elite_opened = opened
|
||||
account.elite_expire_time = expire_time or None
|
||||
if manual_score is not None:
|
||||
account.points = manual_score
|
||||
account.updated_at = datetime.now(UTC)
|
||||
return {
|
||||
"elite_manual_type": manual_type,
|
||||
"elite_manual_score": manual_score,
|
||||
"elite_expire_time": expire_time,
|
||||
"elite_opened": opened,
|
||||
"elite_user_info": result,
|
||||
"points": manual_score,
|
||||
}
|
||||
|
||||
def _execute_query_elite_status(
|
||||
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||
):
|
||||
"""查询精英宝典是否已开通、到期时间与积分。"""
|
||||
manual_id = str(config.get("manual_id") or "").strip()
|
||||
if not manual_id:
|
||||
self._mark_task(db, task, "failed", "请先配置精英宝典 manualID")
|
||||
return
|
||||
client = self._client(cookie)
|
||||
result = self._refresh_elite_handbook(
|
||||
client,
|
||||
account,
|
||||
manual_id=manual_id,
|
||||
rid=str(config.get("rid") or ""),
|
||||
)
|
||||
account.bind_status = "elite_status_queried"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
state = "已开通" if result["elite_opened"] else "未开通"
|
||||
expire_text = self._format_expire_text(result["elite_expire_time"])
|
||||
points = result["elite_manual_score"]
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"精英宝典{state}{expire_text},积分: {points if points is not None else '-'}",
|
||||
result,
|
||||
)
|
||||
|
||||
def _wait_points_after_payment(
|
||||
self,
|
||||
db: Session,
|
||||
@@ -66,11 +134,19 @@ class ManualMixin:
|
||||
cookie: str,
|
||||
ctn: str,
|
||||
result: dict,
|
||||
*,
|
||||
baseline_points: int | None = None,
|
||||
) -> bool:
|
||||
"""等待宝典支付到账;积分达到 300 视为开通成功。"""
|
||||
"""等待宝典支付到账。
|
||||
|
||||
判定口径是「积分比开通前有增长」:旧口径 `积分 >= 300` 会让账上本来就
|
||||
有 300+ 积分的账号在出码后第一轮轮询就被判成功,二维码刚弹出来就消失
|
||||
(线上 2~4 秒 success 的案例都是这种)。拿不到基线时才退回旧口径。
|
||||
"""
|
||||
deadline = time.monotonic() + DOUYU_PAYMENT_POLL_SECONDS
|
||||
result["payment_polling"] = True
|
||||
result["payment_target_points"] = 300
|
||||
result["elite_points_baseline"] = baseline_points
|
||||
poll_count = 0
|
||||
last_points = None
|
||||
while not self._stop.is_set() and time.monotonic() <= deadline:
|
||||
@@ -84,10 +160,16 @@ class ManualMixin:
|
||||
result.update(points_result)
|
||||
result["payment_poll_count"] = poll_count
|
||||
result["payment_polling"] = True
|
||||
if last_points is not None and last_points >= 300:
|
||||
result["payment_polling"] = False
|
||||
result["elite_opened"] = True
|
||||
return True
|
||||
if last_points is not None:
|
||||
arrived = (
|
||||
last_points > baseline_points
|
||||
if baseline_points is not None
|
||||
else last_points >= 300
|
||||
)
|
||||
if arrived:
|
||||
result["payment_polling"] = False
|
||||
result["elite_opened"] = True
|
||||
return True
|
||||
self._update_task_progress(
|
||||
db,
|
||||
task,
|
||||
@@ -213,6 +295,59 @@ class ManualMixin:
|
||||
if not act_alias:
|
||||
self._mark_task(db, task, "failed", "请先配置开通宝典活动 actAlias")
|
||||
return
|
||||
|
||||
# 开通前先查状态(对齐电竞手册):已开通的账号直接给出结论,不再生成
|
||||
# 支付码 —— 既避免二维码刚弹出来就因「积分本来就够」被判成功而消失,
|
||||
# 也避免给已开通账号生成一张无意义的真实支付单。
|
||||
manual_id = str(config.get("manual_id") or "").strip()
|
||||
baseline_points: int | None = None
|
||||
if manual_id:
|
||||
try:
|
||||
baseline = self._refresh_elite_handbook(
|
||||
client,
|
||||
account,
|
||||
manual_id=manual_id,
|
||||
rid=str(config.get("rid") or ""),
|
||||
)
|
||||
db.commit()
|
||||
except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
|
||||
self._push_log(
|
||||
"warning", f"开通前查询精英宝典状态失败,按未开通流程继续: {exc}"
|
||||
)
|
||||
else:
|
||||
if baseline["elite_opened"]:
|
||||
expire_text = self._format_expire_text(
|
||||
baseline["elite_expire_time"]
|
||||
)
|
||||
points = baseline["elite_manual_score"]
|
||||
account.bind_status = "elite_opened"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"精英宝典已开通{expire_text},积分: "
|
||||
f"{points if points is not None else '-'}",
|
||||
{
|
||||
**baseline,
|
||||
"elite_already_opened": True,
|
||||
"payment_polling": False,
|
||||
},
|
||||
)
|
||||
return
|
||||
baseline_points = baseline["elite_manual_score"]
|
||||
# 到账轮询读的是 credit/balance,基线也用同一个接口取,
|
||||
# 避免两个口径不一致时把「余额本来就高」误判成刚到账。
|
||||
try:
|
||||
baseline_points = self._refresh_account_points(
|
||||
client, account, cookie, ctn=ctn
|
||||
)["points"]
|
||||
db.commit()
|
||||
except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
|
||||
self._push_log(
|
||||
"warning", f"开通前查询积分基线失败,改用手册积分: {exc}"
|
||||
)
|
||||
|
||||
result = client.create_elite_qr(
|
||||
ctn=ctn,
|
||||
act_alias=act_alias,
|
||||
@@ -225,13 +360,21 @@ class ManualMixin:
|
||||
db, task, "running", "精英宝典支付码已生成,等待开通到账", result
|
||||
)
|
||||
opened = self._wait_points_after_payment(
|
||||
db, task, account, client, cookie, ctn, result
|
||||
db,
|
||||
task,
|
||||
account,
|
||||
client,
|
||||
cookie,
|
||||
ctn,
|
||||
result,
|
||||
baseline_points=baseline_points,
|
||||
)
|
||||
if self._stop.is_set():
|
||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||
return
|
||||
if opened:
|
||||
account.bind_status = "elite_opened"
|
||||
account.elite_opened = True
|
||||
account.updated_at = datetime.now(UTC)
|
||||
self._mark_task(
|
||||
db, task, "success", f"精英宝典已开通,积分: {account.points}", result
|
||||
|
||||
@@ -17,6 +17,7 @@ SUPPORTED_DOUYU_TASK_TYPES = {
|
||||
"get_bind_qr": "获取绑定二维码",
|
||||
"confirm_bind": "确认绑定",
|
||||
"create_elite_qr": "开通精英宝典30",
|
||||
"query_elite_status": "查询精英宝典状态",
|
||||
"prepare_esports_bind": "绑定电竞手册角色",
|
||||
"get_esports_bind_qr": "切换电竞角色",
|
||||
"query_esports_game_name": "查询最新游戏角色",
|
||||
@@ -57,6 +58,7 @@ DOUYU_HANDBOOK_TASK_TYPES = {
|
||||
"get_bind_qr",
|
||||
"confirm_bind",
|
||||
"create_elite_qr",
|
||||
"query_elite_status",
|
||||
"create_gold_qr",
|
||||
"donate_elite_gift",
|
||||
"query_points",
|
||||
|
||||
Reference in New Issue
Block a user