修复斗鱼绑定主链路:双 actAlias 分工与任务驱动扫码确认

扫码/确认走活动 alias,角色与换绑最新态优先查 cjm;生成二维码前强制校验换绑冷却,前端以任务结果为单一数据源展示二维码、待确认角色与换绑时间。
This commit is contained in:
yml2213
2026-07-24 22:36:25 +08:00
parent 781ad74762
commit 9471a5b4ff
4 changed files with 1770 additions and 331 deletions
+876 -26
View File
@@ -26,6 +26,11 @@ from .douyu_service import (
)
DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX"
DOUYU_BIND_ROLE_POLL_SECONDS = 65
DOUYU_BIND_ROLE_POLL_INTERVAL = 5
class DouyuBatchRunner:
"""批量执行斗鱼活动任务,通过队列推送实时日志。"""
@@ -95,6 +100,179 @@ class DouyuBatchRunner:
return f"{hours}小时{minutes}{sec}"
return f"{minutes}{sec}"
@staticmethod
def _bind_qr_act_alias(config: dict) -> str:
"""生成绑定二维码用的活动 alias。"""
bind_alias = str(config.get("bind_act_alias") or "").strip()
confirm_alias = str(config.get("confirm_act_alias") or "").strip()
if not bind_alias or bind_alias == DOUYU_LEGACY_BIND_ACT_ALIAS:
return confirm_alias or bind_alias
return bind_alias
@staticmethod
def _query_bind_act_aliases(config: dict) -> list[str]:
"""查询/轮询角色用的 alias 列表。
现网最新绑定信息在 legacy(cjm);活动 alias 可能仍用于扫码/确认,
所以按优先级去重返回多个,轮询时取“更像新扫码结果”的那个。
"""
ordered = [
str(config.get("legacy_act_alias") or "").strip(),
str(config.get("confirm_act_alias") or "").strip(),
str(config.get("bind_act_alias") or "").strip(),
]
aliases: list[str] = []
for alias in ordered:
if alias and alias not in aliases:
aliases.append(alias)
return aliases
@staticmethod
def _confirm_act_alias(config: dict) -> str:
"""确认绑定接口用的活动 alias。"""
confirm_alias = str(config.get("confirm_act_alias") or "").strip()
if confirm_alias:
return confirm_alias
bind_alias = str(config.get("bind_act_alias") or "").strip()
if bind_alias and bind_alias != DOUYU_LEGACY_BIND_ACT_ALIAS:
return bind_alias
return str(config.get("legacy_act_alias") or "").strip()
# 兼容旧调用名
@classmethod
def _current_bind_act_alias(cls, config: dict) -> str:
aliases = cls._query_bind_act_aliases(config)
return aliases[0] if aliases else cls._bind_qr_act_alias(config)
@staticmethod
def _role_channel(bind_info: dict) -> str:
return " / ".join(
part for part in [bind_info.get("area_name"), bind_info.get("plat_name")] if part
)
@staticmethod
def _is_truthy_flag(value) -> bool:
if value is True:
return True
if value is False or value is None:
return False
text = str(value).strip().lower()
return text in {"1", "true", "yes", "y"}
@classmethod
def _is_bound_act(cls, bind_info: dict | None) -> bool:
if not bind_info:
return False
return cls._is_truthy_flag(bind_info.get("is_bound_act"))
@classmethod
def _can_change_role(cls, bind_info: dict | None) -> bool:
"""综合 can_change_role 与换绑倒计时判断是否允许换绑。"""
if not bind_info:
return True
wait_time = cls._to_int(bind_info.get("change_role_wait_time"))
if wait_time is not None and wait_time > 0:
return False
if bind_info.get("can_change_role") is not None:
return cls._is_truthy_flag(bind_info.get("can_change_role"))
# 已绑定但接口没给倒计时/开关时,默认允许(避免误杀首次绑定)
return True
@classmethod
def _is_change_cooling(cls, bind_info: dict | None) -> bool:
"""是否处于换绑冷却:已有绑定角色且当前不可换绑。"""
if not bind_info:
return False
role_name = str(bind_info.get("role_name") or "").strip()
is_bound = cls._is_bound_act(bind_info) or bool(role_name)
if not is_bound:
return False
return not cls._can_change_role(bind_info)
@classmethod
def _is_pending_role(
cls,
bind_info: dict | None,
*,
baseline_role_name: str = "",
baseline_is_bound_act: bool = False,
) -> bool:
"""判断是否出现了可确认的扫码角色。
规则:
1. 必须有角色名
2. 已绑定同一角色(或无 baseline 的已绑定)不算 pending
3. 角色名相对 baseline 变化,或从已绑定变成待确认,算 pending
4. need_bind_act / need_bind_role 且角色相对 baseline 有变化,算 pending
5. 无 baseline 且未绑定但有角色,视为待确认残留
"""
if not bind_info:
return False
role_name = str(bind_info.get("role_name") or "").strip()
if not role_name:
return False
is_bound_act = cls._is_bound_act(bind_info)
need_bind_act = cls._is_truthy_flag(bind_info.get("need_bind_act"))
need_bind_role = cls._is_truthy_flag(bind_info.get("need_bind_role"))
role_changed = bool(baseline_role_name) and role_name != baseline_role_name
if is_bound_act:
# 已绑定:仅当相对 baseline 角色发生变化时才视为新扫码结果
return role_changed
if need_bind_act or need_bind_role:
if not baseline_role_name:
return True
return role_changed or baseline_is_bound_act
if not baseline_role_name:
return True
if role_changed:
return True
# 同一角色从已绑定变为未绑定
return baseline_is_bound_act
def _bind_snapshot(self, bind_info: dict | None) -> dict:
info = bind_info or {}
role_name = str(info.get("role_name") or "")
return {
"role_name": role_name,
"area_name": info.get("area_name") or "",
"plat_name": info.get("plat_name") or "",
"nickname": info.get("nickname") or "",
"is_bound_act": self._is_bound_act(info),
"is_bound_role": self._is_truthy_flag(info.get("is_bound_role")),
"is_bound_account": self._is_truthy_flag(info.get("is_bound_account")),
"need_bind_act": self._is_truthy_flag(info.get("need_bind_act")),
"need_bind_role": self._is_truthy_flag(info.get("need_bind_role")),
"change_role_wait_time": self._to_int(info.get("change_role_wait_time")),
"can_change_role": self._can_change_role(info),
"bind_info": info,
}
def _format_bind_summary(self, bind_info: dict | None, *, pending: bool | None = None) -> str:
snap = self._bind_snapshot(bind_info)
role = snap["role_name"] or "-"
area = snap["area_name"] or "-"
plat = snap["plat_name"] or "-"
pending_text = ""
if pending is not None:
pending_text = f" pending={1 if pending else 0}"
return (
f"role={role} area={area} plat={plat}"
f" bound_act={1 if snap['is_bound_act'] else 0}"
f" bound_role={1 if snap['is_bound_role'] else 0}"
f" need_act={1 if snap['need_bind_act'] else 0}"
f" need_role={1 if snap['need_bind_role'] else 0}"
f" wait={snap['change_role_wait_time'] if snap['change_role_wait_time'] is not None else '-'}"
f"{pending_text}"
)
def _apply_bind_info_to_account(self, account: Account, bind_info: dict, status: str) -> None:
role_name = str(bind_info.get("role_name") or "")
account.game_name = role_name or account.game_name
account.game_channel = self._role_channel(bind_info) or account.game_channel
account.change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time"))
account.bind_status = status
account.updated_at = datetime.now(timezone.utc)
def _mark_task(
self,
db: Session,
@@ -110,6 +288,20 @@ class DouyuBatchRunner:
task.finished_at = datetime.now(timezone.utc)
db.commit()
def _update_task_progress(
self,
db: Session,
task: DouyuTask,
status: str,
message: str,
result: dict | None = None,
) -> None:
task.status = status
task.message = message[:512]
if result is not None:
task.result = result
db.commit()
def _upsert_goods(self, db: Session, goods: list[dict]) -> None:
now = datetime.now(timezone.utc)
for raw in goods:
@@ -144,6 +336,239 @@ class DouyuBatchRunner:
def _client(self, cookie: str) -> DouyuActivityClient:
return DouyuActivityClient(cookie, logger=lambda msg: self._push_log("debug", msg))
def _fetch_bind_info_candidates(
self,
client: DouyuActivityClient,
aliases: list[str],
) -> list[dict]:
"""按多个 actAlias 查询绑定信息,保留成功结果。"""
results: list[dict] = []
for alias in aliases:
if not alias:
continue
try:
info = client.bind_info(alias, v2=True)
except DouyuActivityError as exc:
self._push_log("warning", f"查询绑定信息失败 act={alias}: {exc}")
continue
info = {**info, "act_alias": alias}
results.append(info)
return results
def _pick_bind_info(
self,
candidates: list[dict],
*,
baseline_role_name: str = "",
baseline_is_bound_act: bool = False,
prefer_pending: bool = True,
prefer_aliases: list[str] | None = None,
) -> dict | None:
"""从多个 alias 结果里挑最有用的绑定信息。
- prefer_pending=True:优先选“待确认/新扫码角色”(通常来自 cjm)
- prefer_aliases:在同等条件下优先指定 alias(如活动当前绑定)
"""
if not candidates:
return None
def _alias_rank(info: dict) -> int:
alias = str(info.get("act_alias") or "")
if not prefer_aliases:
return 0
try:
return prefer_aliases.index(alias)
except ValueError:
return len(prefer_aliases) + 1
ranked = sorted(enumerate(candidates), key=lambda item: (_alias_rank(item[1]), item[0]))
ordered = [item[1] for item in ranked]
if prefer_pending:
for info in ordered:
if self._is_pending_role(
info,
baseline_role_name=baseline_role_name,
baseline_is_bound_act=baseline_is_bound_act,
):
return info
for info in ordered:
if str(info.get("role_name") or "").strip():
return info
return ordered[0]
def _pick_baseline_bind_info(
self,
candidates: list[dict],
config: dict,
) -> dict | None:
"""选“扫码前当前已绑定角色”作为 baseline。
活动 alias20260120QYOOB)只反映当前已绑定;
cjm 才是换绑最新态。baseline 应优先活动 alias 的已绑定结果,
避免把 cjm 上的待确认新角色误当成扫码前旧角色。
"""
if not candidates:
return None
prefer = []
for key in ("confirm_act_alias", "bind_act_alias", "legacy_act_alias"):
alias = str(config.get(key) or "").strip()
if alias and alias not in prefer:
prefer.append(alias)
# 先找“已绑定且有角色”的活动结果
for alias in prefer:
for info in candidates:
if str(info.get("act_alias") or "") != alias:
continue
if self._is_bound_act(info) and str(info.get("role_name") or "").strip():
return info
# 再退回任意有角色的结果(仍按活动 alias 优先)
return self._pick_bind_info(
candidates,
prefer_pending=False,
prefer_aliases=prefer,
)
def _wait_bind_role_result(
self,
client: DouyuActivityClient,
db: Session,
task: DouyuTask,
account: Account,
query_aliases: list[str],
result: dict,
baseline_role_name: str = "",
baseline_is_bound_act: bool = False,
) -> tuple[str, dict]:
"""生成二维码后轮询绑定信息,直到识别到待确认角色、超时或停止。"""
deadline = time.monotonic() + DOUYU_BIND_ROLE_POLL_SECONDS
result["bind_polling"] = True
result["bind_phase"] = result.get("bind_phase") or "waiting_scan"
result["bind_ready_for_confirm"] = False
result["bind_confirmed"] = False
result["baseline_role_name"] = baseline_role_name
result["baseline_is_bound_act"] = baseline_is_bound_act
result["query_act_aliases"] = query_aliases
self._push_log(
"info",
"开始轮询绑定角色 "
f"aliases={','.join(query_aliases) or '-'} "
f"baseline={baseline_role_name or '-'} bound={1 if baseline_is_bound_act else 0}",
)
self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result)
poll_count = 0
last_summary = ""
while not self._stop.is_set() and time.monotonic() < deadline:
if self._stop.wait(DOUYU_BIND_ROLE_POLL_INTERVAL):
break
candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates:
result["bind_poll_error"] = "所有 actAlias 查询绑定信息失败"
self._update_task_progress(db, task, "running", "等待绑定角色同步: 查询失败", result)
continue
poll_count += 1
bind_info = self._pick_bind_info(
candidates,
baseline_role_name=baseline_role_name,
baseline_is_bound_act=baseline_is_bound_act,
prefer_pending=True,
prefer_aliases=query_aliases,
) or candidates[0]
snapshot = self._bind_snapshot(bind_info)
is_pending_role = self._is_pending_role(
bind_info,
baseline_role_name=baseline_role_name,
baseline_is_bound_act=baseline_is_bound_act,
)
query_alias = str(bind_info.get("act_alias") or "")
summary = (
f"act={query_alias or '-'} "
f"{self._format_bind_summary(bind_info, pending=is_pending_role)}"
)
# 字段变化或每 3 次打印一次,避免刷屏但仍能看到过程
if summary != last_summary or poll_count == 1 or poll_count % 3 == 0:
self._push_log("info", f"轮询绑定#{poll_count}: {summary}")
last_summary = summary
# 注意:未识别到新角色时,不要把当前已绑定角色写进 role_name,
# 否则前端会把旧角色误当成“待确认角色/查询结果”。
if is_pending_role:
result.update({
**snapshot,
"act_alias": query_alias,
"query_act_alias": query_alias,
"bind_ready_for_confirm": True,
"bind_confirmed": False,
"bind_phase": "role_ready",
"bind_polling": False,
"poll_count": poll_count,
"bind_summary": summary,
"bind_candidates": [
{
"act_alias": item.get("act_alias"),
"role_name": item.get("role_name"),
"is_bound_act": self._is_bound_act(item),
}
for item in candidates
],
})
role_name = snapshot["role_name"]
self._apply_bind_info_to_account(account, bind_info, "game_queried")
self._push_log("success", f"识别到待确认角色: {role_name} (act={query_alias})")
self._update_task_progress(
db,
task,
"running",
f"已识别角色: {role_name},待确认绑定",
result,
)
return role_name, result
if snapshot["role_name"] and snapshot["is_bound_act"]:
result["current_role_name"] = snapshot["role_name"]
result["current_area_name"] = snapshot["area_name"]
result["current_plat_name"] = snapshot["plat_name"]
result.update({
"bind_info": bind_info,
"act_alias": query_alias,
"query_act_alias": query_alias,
"is_bound_act": snapshot["is_bound_act"],
"is_bound_role": snapshot["is_bound_role"],
"is_bound_account": snapshot["is_bound_account"],
"need_bind_act": snapshot["need_bind_act"],
"need_bind_role": snapshot["need_bind_role"],
"change_role_wait_time": snapshot["change_role_wait_time"],
"can_change_role": snapshot["can_change_role"],
"role_name": "",
"area_name": "",
"plat_name": "",
"bind_ready_for_confirm": False,
"bind_confirmed": False,
"bind_phase": "waiting_scan",
"bind_polling": True,
"poll_count": poll_count,
"bind_summary": summary,
})
self._update_task_progress(db, task, "running", f"等待扫码绑定 ({summary})", result)
result["bind_polling"] = False
result["bind_ready_for_confirm"] = False
result["bind_phase"] = "stopped" if self._stop.is_set() else "role_timeout"
result["poll_count"] = poll_count
if last_summary:
result["bind_summary"] = last_summary
self._push_log(
"warning",
f"轮询结束 phase={result['bind_phase']} polls={poll_count} last={last_summary or '-'}",
)
return "", result
def _execute_refresh_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
result = client.list_goods(manual_id=config["manual_id"], rid=config["rid"])
@@ -155,22 +580,319 @@ class DouyuBatchRunner:
def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
result = client.get_bind_qr(str(config["bind_act_alias"]))
qr_act_alias = self._bind_qr_act_alias(config)
query_aliases = self._query_bind_act_aliases(config)
if not qr_act_alias and not query_aliases:
self._mark_task(db, task, "failed", "请先配置绑定活动 actAlias")
return
if qr_act_alias and qr_act_alias not in query_aliases:
query_aliases = [qr_act_alias, *query_aliases]
before_candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not before_candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
# baseline 用活动 alias 的“当前已绑定”;pending 检测优先 cjm 的换绑最新态
before = self._pick_baseline_bind_info(before_candidates, config) or before_candidates[0]
# 换绑冷却必须看活动当前绑定(QYOOB),不要用 cjm
cooldown_info = self._pick_change_wait_bind_info(before_candidates, config) or before
pending_before = self._pick_bind_info(
before_candidates,
baseline_role_name=str(before.get("role_name") or ""),
baseline_is_bound_act=self._is_bound_act(before),
prefer_pending=True,
prefer_aliases=query_aliases,
) or before
before_snapshot = self._bind_snapshot(before)
cooldown_snapshot = self._bind_snapshot(cooldown_info)
current_role_name = before_snapshot["role_name"] or cooldown_snapshot["role_name"]
wait_time = cooldown_snapshot["change_role_wait_time"]
self._push_log(
"info",
"绑定前状态 "
f"qr_act={qr_act_alias or '-'} query={','.join(query_aliases)} "
f"baseline_hit={before.get('act_alias') or '-'} {self._format_bind_summary(before)} "
f"cooldown_hit={cooldown_info.get('act_alias') or '-'} "
f"{self._format_bind_summary(cooldown_info)} "
f"pending_hit={pending_before.get('act_alias') or '-'} "
f"{self._format_bind_summary(pending_before)}",
)
# 生成二维码前强制检查换绑冷却:冷却中直接失败,绝不发码
if self._is_change_cooling(cooldown_info):
role_label = current_role_name or cooldown_snapshot["role_name"] or "当前角色"
wait_text = self._format_wait_time(wait_time) or "冷却中"
self._push_log(
"warning",
f"换绑冷却中,跳过生成二维码 role={role_label} wait={wait_time if wait_time is not None else '-'} "
f"can={cooldown_info.get('can_change_role')}",
)
result = {
"act_alias": qr_act_alias or cooldown_info.get("act_alias") or before.get("act_alias"),
"query_act_alias": cooldown_info.get("act_alias") or before.get("act_alias"),
"query_act_aliases": query_aliases,
"before_bind_info": before,
"cooldown_bind_info": cooldown_info,
**cooldown_snapshot,
"current_role_name": role_label if role_label != "当前角色" else current_role_name,
"current_area_name": cooldown_snapshot["area_name"] or before_snapshot["area_name"],
"current_plat_name": cooldown_snapshot["plat_name"] or before_snapshot["plat_name"],
"change_role_wait_time": wait_time,
"change_role_wait_text": self._format_wait_time(wait_time),
"bind_ready_for_confirm": False,
"bind_confirmed": bool(cooldown_snapshot["is_bound_act"] or before_snapshot["is_bound_act"]),
"bind_phase": "change_waiting",
"bind_polling": False,
}
self._apply_bind_info_to_account(
account,
cooldown_info if cooldown_snapshot["role_name"] else before,
"bind_confirmed" if result["bind_confirmed"] else "game_queried",
)
self._mark_task(
db,
task,
"failed",
f"{role_label} 暂不能换绑,剩余 {wait_text}",
result,
)
return
# 生成码前若 cjm 已有待确认新角色,直接返回,无需重复扫码。
pending_snapshot = self._bind_snapshot(pending_before)
if self._is_pending_role(
pending_before,
baseline_role_name=current_role_name,
baseline_is_bound_act=before_snapshot["is_bound_act"],
) and pending_snapshot["role_name"]:
result = {
"act_alias": qr_act_alias or pending_before.get("act_alias"),
"query_act_alias": pending_before.get("act_alias"),
"query_act_aliases": query_aliases,
"before_bind_info": before,
**pending_snapshot,
"current_role_name": current_role_name,
"current_area_name": before_snapshot["area_name"],
"current_plat_name": before_snapshot["plat_name"],
"bind_ready_for_confirm": True,
"bind_confirmed": False,
"bind_phase": "role_ready",
"bind_polling": False,
}
self._apply_bind_info_to_account(account, pending_before, "game_queried")
self._mark_task(
db,
task,
"success",
f"已识别角色: {pending_snapshot['role_name']},待确认绑定",
result,
)
return
if not qr_act_alias:
self._mark_task(db, task, "failed", "请先配置绑定二维码活动 actAlias")
return
self._push_log(
"info",
f"换绑校验通过,开始生成二维码 act={qr_act_alias} "
f"role={current_role_name or '-'} wait={wait_time if wait_time is not None else 0}",
)
qr_result = client.get_bind_qr(qr_act_alias)
result = {
**qr_result,
"act_alias": qr_act_alias,
"query_act_aliases": query_aliases,
"before_bind_info": before,
"current_role_name": current_role_name,
"current_area_name": before_snapshot["area_name"],
"current_plat_name": before_snapshot["plat_name"],
"role_name": "",
"area_name": "",
"plat_name": "",
"bind_ready_for_confirm": False,
"bind_confirmed": False,
"bind_phase": "waiting_scan",
"bind_polling": True,
}
account.bind_status = "bind_qr_generated"
account.updated_at = datetime.now(timezone.utc)
self._mark_task(db, task, "success", "绑定二维码已生成", result)
# 关键:先把二维码 progress 出去,前端 running 期间即可弹窗扫码。
self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result)
role_name, result = self._wait_bind_role_result(
client,
db,
task,
account,
query_aliases,
result,
baseline_role_name=current_role_name,
baseline_is_bound_act=before_snapshot["is_bound_act"],
)
if role_name:
self._mark_task(db, task, "success", f"已识别角色: {role_name},待确认绑定", result)
return
if result.get("bind_phase") == "stopped":
self._mark_task(db, task, "stopped", "任务已停止", result)
return
last_summary = str(result.get("bind_summary") or "")
timeout_msg = "已生成绑定二维码,未检测到新扫码角色"
if current_role_name:
timeout_msg = f"{timeout_msg}(当前仍是 {current_role_name}"
if last_summary:
timeout_msg = f"{timeout_msg} | {last_summary}"
self._mark_task(db, task, "success", timeout_msg, result)
def _execute_confirm_bind(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
before = client.bind_info(str(config["legacy_act_alias"]), v2=True)
result = client.confirm_bind(str(config["confirm_act_alias"]))
after = client.bind_info(str(config["confirm_act_alias"]), v2=False)
role_name = after.get("role_name") or before.get("role_name") or ""
account.game_name = role_name or account.game_name
account.game_channel = " / ".join(part for part in [after.get("area_name"), after.get("plat_name")] if part)
account.bind_status = "bind_confirmed"
account.updated_at = datetime.now(timezone.utc)
self._mark_task(db, task, "success", f"绑定成功: {role_name or '已确认'}", {"before": before, "confirm": result, "bind_info": after})
confirm_alias = self._confirm_act_alias(config)
query_aliases = self._query_bind_act_aliases(config)
if confirm_alias and confirm_alias not in query_aliases:
query_aliases = [confirm_alias, *query_aliases]
if not query_aliases:
self._mark_task(db, task, "failed", "请先配置绑定活动 actAlias")
return
before_candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not before_candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
before = self._pick_bind_info(
before_candidates,
baseline_role_name="",
baseline_is_bound_act=False,
prefer_pending=True,
prefer_aliases=query_aliases,
) or before_candidates[0]
before_snapshot = self._bind_snapshot(before)
role_name = before_snapshot["role_name"]
query_alias = str(before.get("act_alias") or "")
self._push_log(
"info",
f"确认前状态 confirm_act={confirm_alias or '-'} hit={query_alias or '-'} "
f"{self._format_bind_summary(before)}",
)
if not role_name:
self._mark_task(
db,
task,
"failed",
"尚未识别到待确认角色,请先扫码完成绑定",
{
"act_alias": confirm_alias or query_alias,
"query_act_alias": query_alias,
"query_act_aliases": query_aliases,
"before_bind_info": before,
**before_snapshot,
"bind_ready_for_confirm": False,
"bind_confirmed": False,
"bind_phase": "waiting_role",
},
)
return
if before_snapshot["is_bound_act"]:
self._apply_bind_info_to_account(account, before, "bind_confirmed")
self._mark_task(
db,
task,
"success",
f"已绑定: {role_name}",
{
"act_alias": confirm_alias or query_alias,
"query_act_alias": query_alias,
"query_act_aliases": query_aliases,
"before_bind_info": before,
**before_snapshot,
"bind_ready_for_confirm": True,
"bind_confirmed": True,
"bind_phase": "confirmed",
},
)
return
# 确认接口优先用配置的确认 alias;没有则回退到命中查询的 alias
use_confirm_alias = confirm_alias or query_alias
confirm_result = client.confirm_bind(use_confirm_alias)
try:
after_candidates = self._fetch_bind_info_candidates(client, query_aliases)
after = self._pick_bind_info(
after_candidates,
baseline_role_name="",
baseline_is_bound_act=False,
prefer_pending=False,
) if after_candidates else None
if after is None:
raise DouyuActivityError("确认后回查绑定信息失败")
except DouyuActivityError as exc:
# 确认接口已成功时,回查失败仍按确认成功处理,但保留错误信息。
self._apply_bind_info_to_account(account, before, "bind_confirmed")
self._mark_task(
db,
task,
"success",
f"绑定成功: {role_name}",
{
"act_alias": use_confirm_alias,
"query_act_alias": query_alias,
"query_act_aliases": query_aliases,
"before_bind_info": before,
"confirm": confirm_result,
**before_snapshot,
"bind_ready_for_confirm": True,
"bind_confirmed": True,
"bind_phase": "confirmed",
"refresh_error": str(exc),
},
)
return
after_snapshot = self._bind_snapshot(after)
final_info = after if after_snapshot["role_name"] else before
final_snapshot = after_snapshot if after_snapshot["role_name"] else before_snapshot
final_role_name = final_snapshot["role_name"] or role_name
if not after_snapshot["is_bound_act"]:
self._apply_bind_info_to_account(account, final_info, "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(
db,
task,
"success",
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,
"after_bind_info": after,
**final_snapshot,
"bind_ready_for_confirm": True,
"bind_confirmed": True,
"bind_phase": "confirmed",
},
)
def _execute_create_elite_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
@@ -179,7 +901,7 @@ class DouyuBatchRunner:
ctn = client.acf_ccn(refresh_subscribe=True)
result = client.create_elite_qr(
ctn=ctn,
act_alias=str(config["confirm_act_alias"]),
act_alias=self._current_bind_act_alias(config) or str(config["confirm_act_alias"]),
amount=int(config["elite_amount"]),
room_id=str(config["room_id"]),
)
@@ -285,27 +1007,155 @@ class DouyuBatchRunner:
def _execute_query_game_name(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
result = client.bind_info(str(config["confirm_act_alias"]), v2=False)
if not result.get("role_name"):
result = client.bind_info(str(config["legacy_act_alias"]), v2=True)
role_name = result.get("role_name") or ""
account.game_name = role_name
account.game_channel = " / ".join(part for part in [result.get("area_name"), result.get("plat_name")] if part)
account.bind_status = "game_queried" if role_name else "game_not_bound"
account.change_role_wait_time = self._to_int(result.get("change_role_wait_time"))
account.updated_at = datetime.now(timezone.utc)
message = f"游戏名: {role_name}" if role_name else "未获取到游戏名"
query_aliases = self._query_bind_act_aliases(config)
if not query_aliases:
self._mark_task(db, task, "failed", "请先配置绑定活动 actAlias")
return
candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
bind_info = self._pick_bind_info(
candidates,
prefer_pending=True,
prefer_aliases=query_aliases,
) or candidates[0]
snapshot = self._bind_snapshot(bind_info)
role_name = snapshot["role_name"]
source_alias = str(bind_info.get("act_alias") or "")
summary = f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}"
self._push_log(
"info",
"查询角色 "
f"aliases={','.join(query_aliases)} hit={source_alias or '-'} "
f"{self._format_bind_summary(bind_info)}",
)
self._apply_bind_info_to_account(account, bind_info, "game_queried" if role_name else "game_not_bound")
pending = bool(role_name) and not snapshot["is_bound_act"]
result = {
"act_alias": source_alias,
"query_act_alias": source_alias,
"query_act_aliases": query_aliases,
**snapshot,
"bind_ready_for_confirm": pending,
"bind_confirmed": snapshot["is_bound_act"],
"bind_phase": (
"confirmed" if snapshot["is_bound_act"]
else ("role_ready" if role_name else "waiting_role")
),
"bind_summary": summary,
"bind_candidates": [
{
"act_alias": item.get("act_alias"),
"role_name": item.get("role_name"),
"is_bound_act": self._is_bound_act(item),
}
for item in candidates
],
}
if not role_name:
message = f"未获取到游戏名 | {summary}"
elif snapshot["is_bound_act"]:
message = f"当前已绑定: {role_name} | {summary}"
else:
message = f"待确认角色: {role_name} | {summary}"
self._mark_task(db, task, "success" if role_name else "failed", message, result)
def _pick_change_wait_bind_info(self, candidates: list[dict], config: dict) -> dict | None:
"""换绑倒计时优先看活动当前绑定(QYOOB),不是 cjm 换绑最新态。"""
if not candidates:
return None
# 1) 优先活动 alias 上有角色的结果
baseline = self._pick_baseline_bind_info(candidates, config)
if baseline is not None:
wait = self._to_int(baseline.get("change_role_wait_time"))
if wait is not None:
return baseline
# 2) 任意带 wait_time 的结果里取 wait 最大的
with_wait = []
for info in candidates:
wait = self._to_int(info.get("change_role_wait_time"))
if wait is not None:
with_wait.append((wait, info))
if with_wait:
with_wait.sort(key=lambda item: item[0], reverse=True)
return with_wait[0][1]
# 3) 回退 baseline / 首个有角色
return baseline or self._pick_bind_info(
candidates,
prefer_pending=False,
prefer_aliases=[
str(config.get("confirm_act_alias") or "").strip(),
str(config.get("bind_act_alias") or "").strip(),
str(config.get("legacy_act_alias") or "").strip(),
],
)
def _execute_query_change_bind_time(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
result = client.bind_info(str(config["confirm_act_alias"]), v2=True)
wait_time = self._to_int(result.get("change_role_wait_time"))
query_aliases = self._query_bind_act_aliases(config)
if not query_aliases:
self._mark_task(db, task, "failed", "请先配置绑定活动 actAlias")
return
candidates = self._fetch_bind_info_candidates(client, query_aliases)
if not candidates:
self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias")
return
# 换绑时间看“当前已绑定”活动态,不要优先 cjm(cjm 常无 changeRoleWaitTime
bind_info = self._pick_change_wait_bind_info(candidates, config) or candidates[0]
snapshot = self._bind_snapshot(bind_info)
wait_time = snapshot["change_role_wait_time"]
account.change_role_wait_time = wait_time
account.bind_status = "change_time_queried"
account.updated_at = datetime.now(timezone.utc)
result["change_role_wait_text"] = self._format_wait_time(wait_time)
self._mark_task(db, task, "success", f"换绑剩余: {result['change_role_wait_text'] or '-'}", result)
source_alias = str(bind_info.get("act_alias") or "")
wait_text = self._format_wait_time(wait_time)
can_change = snapshot["can_change_role"]
role_name = snapshot["role_name"] or "-"
if wait_time is None:
if can_change is False:
status_text = "不可换绑(接口未返回倒计时)"
elif can_change is True:
status_text = "可换绑"
else:
status_text = "未返回换绑倒计时"
elif wait_time <= 0:
status_text = "可换绑"
wait_text = "0"
else:
status_text = f"剩余 {wait_text}"
result = {
"act_alias": source_alias,
"query_act_alias": source_alias,
"query_act_aliases": query_aliases,
**snapshot,
"change_role_wait_text": wait_text,
"bind_ready_for_confirm": bool(snapshot["role_name"]) and not snapshot["is_bound_act"],
"bind_confirmed": snapshot["is_bound_act"],
"bind_summary": f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}",
"bind_candidates": [
{
"act_alias": item.get("act_alias"),
"role_name": item.get("role_name"),
"is_bound_act": self._is_bound_act(item),
"change_role_wait_time": self._to_int(item.get("change_role_wait_time")),
"can_change_role": item.get("can_change_role"),
}
for item in candidates
],
}
self._push_log(
"info",
f"查询换绑时间 hit={source_alias or '-'} role={role_name} "
f"wait={wait_time if wait_time is not None else '-'} can={can_change}",
)
self._mark_task(
db,
task,
"success",
f"{role_name} {status_text} | act={source_alias or '-'}",
result,
)
def _execute_query_limited_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie)
+5 -1
View File
@@ -34,7 +34,7 @@ SUPPORTED_DOUYU_TASK_TYPES = {
DOUYU_CONFIG_DEFAULTS = {
"manual_id": "G4KA4Qnz4LDp7",
"rid": "9263298",
"bind_act_alias": "20250213NQCYX",
"bind_act_alias": "20260120QYOOB",
"confirm_act_alias": "20260120QYOOB",
"legacy_act_alias": "cjm",
"room_id": "9263298",
@@ -64,6 +64,10 @@ def apply_douyu_config_defaults(config: DouyuConfig) -> bool:
"""补齐斗鱼配置默认值,返回是否发生变更。"""
changed = False
for field in DOUYU_CONFIG_FIELDS:
if field == "bind_act_alias" and str(getattr(config, field, "") or "").strip() == "20250213NQCYX":
setattr(config, field, DOUYU_CONFIG_DEFAULTS[field])
changed = True
continue
normalized = douyu_config_value(field, getattr(config, field, None))
if getattr(config, field, None) != normalized:
setattr(config, field, normalized)
File diff suppressed because it is too large Load Diff