diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index 9ede6e9..579b329 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -89,6 +89,14 @@ class DouyuActivityClient: ]) self._load_cookie(self.cookie) + def _set_cookie_value(self, key: str, value: str) -> None: + """写入单个 Cookie 值,确保手动 Cookie 请求头与 Session 一致。""" + self.cookie = normalize_cookie_pairs([ + *cookie_pairs(self.cookie), + (key, value), + ]) + self._load_cookie(self.cookie) + def _headers(self, referer: str = "https://www.douyu.com/") -> dict[str, str]: return { "User-Agent": PC_UA, @@ -300,6 +308,9 @@ class DouyuActivityClient: def create_elite_qr(self, *, ctn: str, act_alias: str, amount: int, room_id: str) -> dict[str, Any]: """生成精英宝典支付二维码。""" + # E 语言实现会在 peace/item 和 getQrCode 两次请求里显式拼上 acf_ccn=ctn。 + # 这里也同步 Cookie,避免接口只看 Cookie 时误判未绑定游戏账号。 + self._set_cookie_value("acf_ccn", ctn) item_payload = self._request_json( "post", self.PEACE_ITEM_API, @@ -324,7 +335,7 @@ class DouyuActivityClient: qr = (payload.get("data") or {}).get("qr") or "" if not qr: raise DouyuActivityError(payload.get("msg") or "生成精英宝典支付码失败") - return {"pay_url": qr, "ctn": ctn, "item": item_payload, "raw": payload} + return {"pay_url": qr, "ctn": ctn, "act_alias": act_alias, "item": item_payload, "raw": payload} def create_gold_qr(self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1") -> dict[str, Any]: """生成鱼翅充值支付二维码。""" @@ -498,7 +509,7 @@ class DouyuActivityClient: payload = self._request_json( "get", self.EXCHANGE_BALANCE_API, - "查询鱼刺余额", + "查询钱包兑换余额", params={"appCode": "YJTX"}, headers={ "Origin": "", diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index 5f7e69a..f744083 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -29,6 +29,8 @@ from .douyu_service import ( DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX" DOUYU_BIND_ROLE_POLL_SECONDS = 65 DOUYU_BIND_ROLE_POLL_INTERVAL = 5 +DOUYU_PAYMENT_POLL_SECONDS = 600 +DOUYU_PAYMENT_POLL_INTERVAL = 5 class DouyuBatchRunner: @@ -101,13 +103,20 @@ class DouyuBatchRunner: return f"{minutes}分{sec}秒" @staticmethod - def _bind_qr_act_alias(config: dict) -> str: + def _action_act_alias(config: dict, key: str) -> str: + """动作类接口用的活动 alias,排除只用于查询最新角色的 legacy alias。""" + alias = str(config.get(key) or "").strip() + query_only_alias = str(config.get("legacy_act_alias") or "").strip() + if not alias: + return "" + if alias in {query_only_alias, DOUYU_LEGACY_BIND_ACT_ALIAS}: + return "" + return alias + + @classmethod + def _bind_qr_act_alias(cls, 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 + return cls._action_act_alias(config, "bind_act_alias") or cls._action_act_alias(config, "confirm_act_alias") @staticmethod def _query_bind_act_aliases(config: dict) -> list[str]: @@ -127,22 +136,15 @@ class DouyuBatchRunner: aliases.append(alias) return aliases - @staticmethod - def _confirm_act_alias(config: dict) -> str: + @classmethod + def _confirm_act_alias(cls, 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() + return cls._action_act_alias(config, "confirm_act_alias") or cls._action_act_alias(config, "bind_act_alias") # 兼容旧调用名 @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) + return cls._confirm_act_alias(config) or cls._bind_qr_act_alias(config) @staticmethod def _role_channel(bind_info: dict) -> str: @@ -328,6 +330,152 @@ class DouyuBatchRunner: config = ensure_douyu_config(db) return {field: douyu_config_value(field, getattr(config, field, None)) for field in DOUYU_CONFIG_FIELDS} + def _refresh_account_points( + self, + client: DouyuActivityClient, + account: Account, + cookie: str, + *, + ctn: str | None = None, + ) -> dict: + """刷新账号积分并写回账号表。""" + uid = account_uid(account, cookie) + if not uid: + raise DouyuActivityError("Cookie 中没有 acf_uid,无法查询积分") + ctn_value = ctn or client.acf_ccn(refresh_subscribe=False) + result = client.query_points(uid=uid, ctn=ctn_value) + points = self._to_int(result.get("points")) + account.uid = uid + account.points = points + update_account_profile_from_cookie(account, cookie) + account.updated_at = datetime.now(timezone.utc) + return {"points": points, "points_query": result} + + def _refresh_account_gold_balance(self, client: DouyuActivityClient, account: Account) -> dict: + """刷新鱼翅和钱包兑换余额并写回账号表。""" + gold = client.gold_account() + exchange = client.exchange_balance() + account.gold_balance = self._to_int(gold.get("gold")) + account.exchange_balance = self._to_int(exchange.get("count")) + account.updated_at = datetime.now(timezone.utc) + return { + "gold_balance": account.gold_balance, + "exchange_balance": account.exchange_balance, + "gold": gold, + "exchange_balance_query": exchange, + } + + def _wait_points_after_payment( + self, + db: Session, + task: DouyuTask, + account: Account, + client: DouyuActivityClient, + cookie: str, + ctn: str, + result: dict, + ) -> bool: + """等待宝典支付到账;积分达到 300 视为开通成功。""" + deadline = time.monotonic() + DOUYU_PAYMENT_POLL_SECONDS + result["payment_polling"] = True + result["payment_target_points"] = 300 + poll_count = 0 + last_points = None + while not self._stop.is_set() and time.monotonic() <= deadline: + try: + points_result = self._refresh_account_points(client, account, cookie, ctn=ctn) + db.commit() + poll_count += 1 + last_points = points_result["points"] + 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 + self._update_task_progress( + db, + task, + "running", + f"精英宝典支付码已生成,等待开通到账(当前积分 {last_points if last_points is not None else '-'})", + result, + ) + except Exception as exc: + poll_count += 1 + result["payment_poll_count"] = poll_count + result["payment_poll_error"] = str(exc) + self._update_task_progress(db, task, "running", f"等待开通到账: {exc}", result) + if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL): + break + result["payment_polling"] = False + result["elite_opened"] = False + result["points"] = last_points + return False + + def _wait_gold_balance_after_payment( + self, + db: Session, + task: DouyuTask, + account: Account, + client: DouyuActivityClient, + result: dict, + baseline_gold: int | None, + ) -> bool: + """等待鱼翅充值到账;余额变化后写回账号表。""" + deadline = time.monotonic() + DOUYU_PAYMENT_POLL_SECONDS + result["payment_polling"] = True + result["baseline_gold_balance"] = baseline_gold + poll_count = 0 + last_gold = baseline_gold + baseline_ready = baseline_gold is not None + while not self._stop.is_set() and time.monotonic() <= deadline: + try: + balance_result = self._refresh_account_gold_balance(client, account) + db.commit() + poll_count += 1 + last_gold = balance_result["gold_balance"] + result.update(balance_result) + result["payment_poll_count"] = poll_count + result["payment_polling"] = True + if not baseline_ready and last_gold is not None: + baseline_gold = last_gold + result["baseline_gold_balance"] = baseline_gold + baseline_ready = True + self._update_task_progress( + db, + task, + "running", + f"鱼翅支付码已生成,已记录当前余额 {last_gold},等待到账", + result, + ) + if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL): + break + continue + changed = last_gold is not None and (baseline_gold is None or last_gold != baseline_gold) + if changed: + result["payment_polling"] = False + result["gold_recharged"] = True + return True + self._update_task_progress( + db, + task, + "running", + f"鱼翅支付码已生成,等待到账(当前鱼翅 {last_gold if last_gold is not None else '-'})", + result, + ) + except Exception as exc: + poll_count += 1 + result["payment_poll_count"] = poll_count + result["payment_poll_error"] = str(exc) + self._update_task_progress(db, task, "running", f"等待鱼翅到账: {exc}", result) + if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL): + break + result["payment_polling"] = False + result["gold_recharged"] = False + result["gold_balance"] = last_gold + return False + 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 {} @@ -868,24 +1016,73 @@ class DouyuBatchRunner: ctn = str(self._task_payload(task).get("ctn") or "") if not ctn: ctn = client.acf_ccn(refresh_subscribe=True) + act_alias = self._confirm_act_alias(config) or self._bind_qr_act_alias(config) + if not act_alias: + self._mark_task(db, task, "failed", "请先配置开通宝典活动 actAlias") + return result = client.create_elite_qr( ctn=ctn, - act_alias=self._current_bind_act_alias(config) or str(config["confirm_act_alias"]), + act_alias=act_alias, amount=int(config["elite_amount"]), room_id=str(config["room_id"]), ) account.bind_status = "elite_qr_created" account.updated_at = datetime.now(timezone.utc) - self._mark_task(db, task, "success", "精英宝典支付码已生成", result) + self._update_task_progress(db, task, "running", "精英宝典支付码已生成,等待开通到账", result) + opened = self._wait_points_after_payment(db, task, account, client, cookie, ctn, result) + if self._stop.is_set(): + self._mark_task(db, task, "stopped", "任务已停止", result) + return + if opened: + account.bind_status = "elite_opened" + account.updated_at = datetime.now(timezone.utc) + self._mark_task(db, task, "success", f"精英宝典已开通,积分: {account.points}", result) + return + self._mark_task( + db, + task, + "failed", + f"未检测到精英宝典开通到账,当前积分: {account.points if account.points is not None else '-'}", + result, + ) def _execute_create_gold_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): payload = self._task_payload(task) amount = int(payload.get("amount") or payload.get("gold_amount") or 1) client = self._client(cookie) + baseline_gold = account.gold_balance + try: + baseline = self._refresh_account_gold_balance(client, account) + baseline_gold = baseline["gold_balance"] + db.commit() + except Exception as exc: + self._push_log("warning", f"生成鱼翅码前刷新余额失败: {exc}") result = client.create_gold_qr(amount=amount, pay_type=int(config["gold_pay_type"])) account.bind_status = "gold_qr_created" account.updated_at = datetime.now(timezone.utc) - self._mark_task(db, task, "success", f"鱼翅 {amount} 元支付码已生成", result) + self._update_task_progress(db, task, "running", f"鱼翅 {amount} 元支付码已生成,等待到账", result) + recharged = self._wait_gold_balance_after_payment(db, task, account, client, result, baseline_gold) + if self._stop.is_set(): + self._mark_task(db, task, "stopped", "任务已停止", result) + return + if recharged: + account.bind_status = "gold_recharged" + account.updated_at = datetime.now(timezone.utc) + self._mark_task( + db, + task, + "success", + f"鱼翅已到账,当前余额: {account.gold_balance if account.gold_balance is not None else '-'}", + result, + ) + return + self._mark_task( + db, + task, + "failed", + f"未检测到鱼翅到账,当前余额: {account.gold_balance if account.gold_balance is not None else '-'}", + result, + ) def _execute_donate_elite_gift(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): payload = self._task_payload(task) @@ -897,24 +1094,34 @@ class DouyuBatchRunner: gift_id=str(payload.get("gift_id") or config["gift_id"]), skin_id=str(payload.get("skin_id") or config["skin_id"]), ) + 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)) + except Exception as exc: + refresh_errors.append(f"积分: {exc}") + if refresh_errors: + result["refresh_errors"] = refresh_errors account.bind_status = "gift_donated" account.updated_at = datetime.now(timezone.utc) - self._mark_task(db, task, "success", f"赠送精英令成功: {gift_count}", result) + message = f"赠送精英令成功: {gift_count}" + if account.gold_balance is not None: + message += f",鱼翅余额: {account.gold_balance}" + if account.points is not None: + message += f",积分: {account.points}" + self._mark_task(db, task, "success", message, result) def _execute_query_points(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): client = self._client(cookie) ctn = client.acf_ccn(refresh_subscribe=False) - uid = account_uid(account, cookie) - if not uid: - self._mark_task(db, task, "failed", "Cookie 中没有 acf_uid,无法查询积分") - return - result = client.query_points(uid=uid, ctn=ctn) - points = self._to_int(result.get("points")) - account.uid = uid - account.points = points - update_account_profile_from_cookie(account, cookie) + result = self._refresh_account_points(client, account, cookie, ctn=ctn) account.bind_status = "points_queried" account.updated_at = datetime.now(timezone.utc) + points = result["points"] self._mark_task(db, task, "success", f"积分: {points if points is not None else '-'}", result) def _execute_exchange_goods(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): @@ -1138,10 +1345,7 @@ class DouyuBatchRunner: def _execute_query_gold_balance(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): client = self._client(cookie) - gold = client.gold_account() - exchange = client.exchange_balance() - account.gold_balance = self._to_int(gold.get("gold")) - account.exchange_balance = self._to_int(exchange.get("count")) + result = self._refresh_account_gold_balance(client, account) account.bind_status = "gold_balance_queried" account.updated_at = datetime.now(timezone.utc) self._mark_task( @@ -1149,7 +1353,7 @@ class DouyuBatchRunner: task, "success", f"鱼翅余额: {account.gold_balance if account.gold_balance is not None else '-'}", - {"gold": gold, "exchange_balance": exchange}, + result, ) def _execute_query_exchange_records(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index f648586..e90c3d2 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -844,9 +844,9 @@ export default function DouyuTasksPage() { 查询 - +
{QUERY_ACTION_KEYS.map((key) => renderActionButton(key))} - +
@@ -878,6 +878,7 @@ export default function DouyuTasksPage() { 充值与送礼
+ {renderActionButton('create_elite_qr')}