type: 收窄斗鱼鱼翅充值执行器类型
This commit is contained in:
@@ -5,16 +5,41 @@ import re
|
|||||||
import time
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, TYPE_CHECKING, cast
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.douyu import DouyuActivityClient, FishFinRechargeClient, FishFinRechargeConfig, FishFinRechargeError
|
from core.douyu import (
|
||||||
|
DouyuActivityClient,
|
||||||
|
FishFinRechargeClient,
|
||||||
|
FishFinRechargeConfig,
|
||||||
|
FishFinRechargeError,
|
||||||
|
)
|
||||||
from ..models import Account, DouyuTask
|
from ..models import Account, DouyuTask
|
||||||
from .douyu_service import update_account_profile_from_cookie
|
from .douyu_service import update_account_profile_from_cookie
|
||||||
from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS
|
from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .douyu_runner import DouyuBatchRunner
|
||||||
|
|
||||||
|
|
||||||
class GoldMixin:
|
class GoldMixin:
|
||||||
"""鱼翅充值域:扫码充值、供应商直充与到账轮询。"""
|
"""鱼翅充值域:扫码充值、供应商直充与到账轮询。"""
|
||||||
def _refresh_account_gold_balance(self, client: DouyuActivityClient, account: Account) -> dict:
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_stop: Any
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_int(value: Any) -> int | None: ...
|
||||||
|
|
||||||
|
def _client(self, cookie: str) -> DouyuActivityClient: ...
|
||||||
|
def _task_payload(self, task: DouyuTask) -> dict: ...
|
||||||
|
def _push_log(self, level: str, message: str) -> None: ...
|
||||||
|
def _mark_task(self, *args: Any, **kwargs: Any) -> None: ...
|
||||||
|
def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ...
|
||||||
|
|
||||||
|
def _refresh_account_gold_balance(
|
||||||
|
self, client: DouyuActivityClient, account: Account
|
||||||
|
) -> dict:
|
||||||
"""刷新鱼翅和钱包兑换余额并写回账号表。"""
|
"""刷新鱼翅和钱包兑换余额并写回账号表。"""
|
||||||
gold = client.gold_account()
|
gold = client.gold_account()
|
||||||
exchange = client.exchange_balance()
|
exchange = client.exchange_balance()
|
||||||
@@ -67,7 +92,9 @@ class GoldMixin:
|
|||||||
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
||||||
break
|
break
|
||||||
continue
|
continue
|
||||||
changed = last_gold is not None and (baseline_gold is None or last_gold != baseline_gold)
|
changed = last_gold is not None and (
|
||||||
|
baseline_gold is None or last_gold != baseline_gold
|
||||||
|
)
|
||||||
if changed:
|
if changed:
|
||||||
result["payment_polling"] = False
|
result["payment_polling"] = False
|
||||||
result["gold_recharged"] = True
|
result["gold_recharged"] = True
|
||||||
@@ -83,7 +110,9 @@ class GoldMixin:
|
|||||||
poll_count += 1
|
poll_count += 1
|
||||||
result["payment_poll_count"] = poll_count
|
result["payment_poll_count"] = poll_count
|
||||||
result["payment_poll_error"] = str(exc)
|
result["payment_poll_error"] = str(exc)
|
||||||
self._update_task_progress(db, task, "running", f"等待鱼翅到账: {exc}", result)
|
self._update_task_progress(
|
||||||
|
db, task, "running", f"等待鱼翅到账: {exc}", result
|
||||||
|
)
|
||||||
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
||||||
break
|
break
|
||||||
result["payment_polling"] = False
|
result["payment_polling"] = False
|
||||||
@@ -94,8 +123,12 @@ class GoldMixin:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _supplier_value(payload: dict, *keys: str):
|
def _supplier_value(payload: dict, *keys: str):
|
||||||
"""兼容供应商将订单字段放在响应根节点、data 或 result 节点。"""
|
"""兼容供应商将订单字段放在响应根节点、data 或 result 节点。"""
|
||||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
data_raw = payload.get("data")
|
||||||
result = payload.get("result") if isinstance(payload.get("result"), dict) else {}
|
data = cast(dict[str, Any], data_raw) if isinstance(data_raw, dict) else {}
|
||||||
|
result_raw = payload.get("result")
|
||||||
|
result = (
|
||||||
|
cast(dict[str, Any], result_raw) if isinstance(result_raw, dict) else {}
|
||||||
|
)
|
||||||
for source in (payload, data, result):
|
for source in (payload, data, result):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if source.get(key) is not None:
|
if source.get(key) is not None:
|
||||||
@@ -105,7 +138,11 @@ class GoldMixin:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _supplier_order_status(cls, payload: dict) -> int | None:
|
def _supplier_order_status(cls, payload: dict) -> int | None:
|
||||||
"""提取供应商订单状态,文档约定 0-4。"""
|
"""提取供应商订单状态,文档约定 0-4。"""
|
||||||
return cls._to_int(cls._supplier_value(payload, "order_status", "orderStatus", "supplier_order_status"))
|
return cls._to_int(
|
||||||
|
cls._supplier_value(
|
||||||
|
payload, "order_status", "orderStatus", "supplier_order_status"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _supplier_message(cls, payload: dict) -> str:
|
def _supplier_message(cls, payload: dict) -> str:
|
||||||
@@ -116,8 +153,12 @@ class GoldMixin:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _supplier_result(payload: dict) -> dict:
|
def _supplier_result(payload: dict) -> dict:
|
||||||
"""保存必要订单状态,避免把完整供应商响应或签名暴露到任务结果。"""
|
"""保存必要订单状态,避免把完整供应商响应或签名暴露到任务结果。"""
|
||||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
data_raw = payload.get("data")
|
||||||
response_result = payload.get("result") if isinstance(payload.get("result"), dict) else {}
|
data = cast(dict[str, Any], data_raw) if isinstance(data_raw, dict) else {}
|
||||||
|
result_raw = payload.get("result")
|
||||||
|
response_result = (
|
||||||
|
cast(dict[str, Any], result_raw) if isinstance(result_raw, dict) else {}
|
||||||
|
)
|
||||||
result = {
|
result = {
|
||||||
key: value
|
key: value
|
||||||
for key, value in {**payload, **data, **response_result}.items()
|
for key, value in {**payload, **data, **response_result}.items()
|
||||||
@@ -131,7 +172,10 @@ class GoldMixin:
|
|||||||
existing = str(task.supplier_out_order_id or "").strip()
|
existing = str(task.supplier_out_order_id or "").strip()
|
||||||
if existing:
|
if existing:
|
||||||
return existing
|
return existing
|
||||||
batch_token = re.sub(r"[^A-Za-z0-9]", "", str(task.batch_id or "")).upper()[:16] or "LOCAL"
|
batch_token = (
|
||||||
|
re.sub(r"[^A-Za-z0-9]", "", str(task.batch_id or "")).upper()[:16]
|
||||||
|
or "LOCAL"
|
||||||
|
)
|
||||||
return f"DYGF{batch_token}T{task.id}"
|
return f"DYGF{batch_token}T{task.id}"
|
||||||
|
|
||||||
def _wait_supplier_gold_order(
|
def _wait_supplier_gold_order(
|
||||||
@@ -151,7 +195,9 @@ class GoldMixin:
|
|||||||
# 回调可能已在另一个数据库会话中结束订单,刷新后直接使用其结果。
|
# 回调可能已在另一个数据库会话中结束订单,刷新后直接使用其结果。
|
||||||
db.refresh(task)
|
db.refresh(task)
|
||||||
if task.status in {"success", "failed"}:
|
if task.status in {"success", "failed"}:
|
||||||
callback_result = task.result if isinstance(task.result, dict) else result
|
callback_result = (
|
||||||
|
task.result if isinstance(task.result, dict) else result
|
||||||
|
)
|
||||||
result.update(callback_result)
|
result.update(callback_result)
|
||||||
result["payment_polling"] = False
|
result["payment_polling"] = False
|
||||||
return self._supplier_order_status(callback_result)
|
return self._supplier_order_status(callback_result)
|
||||||
@@ -159,12 +205,14 @@ class GoldMixin:
|
|||||||
code = self._to_int(self._supplier_value(payload, "code"))
|
code = self._to_int(self._supplier_value(payload, "code"))
|
||||||
status = self._supplier_order_status(payload)
|
status = self._supplier_order_status(payload)
|
||||||
poll_count += 1
|
poll_count += 1
|
||||||
result.update({
|
result.update(
|
||||||
|
{
|
||||||
"payment_poll_count": poll_count,
|
"payment_poll_count": poll_count,
|
||||||
"supplier_code": code,
|
"supplier_code": code,
|
||||||
"supplier_order_status": status,
|
"supplier_order_status": status,
|
||||||
"supplier_order": self._supplier_result(payload),
|
"supplier_order": self._supplier_result(payload),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
if code != 200:
|
if code != 200:
|
||||||
result["payment_polling"] = False
|
result["payment_polling"] = False
|
||||||
return status if status in {2, 3, 4} else 4
|
return status if status in {2, 3, 4} else 4
|
||||||
@@ -182,21 +230,29 @@ class GoldMixin:
|
|||||||
poll_count += 1
|
poll_count += 1
|
||||||
result["payment_poll_count"] = poll_count
|
result["payment_poll_count"] = poll_count
|
||||||
result["payment_poll_error"] = str(exc)
|
result["payment_poll_error"] = str(exc)
|
||||||
self._update_task_progress(db, task, "running", f"查询供应商订单失败: {exc}", result)
|
self._update_task_progress(
|
||||||
|
db, task, "running", f"查询供应商订单失败: {exc}", result
|
||||||
|
)
|
||||||
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
||||||
break
|
break
|
||||||
result["payment_polling"] = False
|
result["payment_polling"] = False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _execute_create_gold_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_create_gold_qr(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
payload = self._task_payload(task)
|
payload = self._task_payload(task)
|
||||||
amount = int(payload.get("amount") or payload.get("gold_amount") or 1)
|
amount = int(payload.get("amount") or payload.get("gold_amount") or 1)
|
||||||
channel = str(config.get("gold_recharge_channel") or "wechat_qr")
|
channel = str(config.get("gold_recharge_channel") or "wechat_qr")
|
||||||
if channel == "supplier_api":
|
if channel == "supplier_api":
|
||||||
try:
|
try:
|
||||||
self._execute_create_gold_supplier_order(db, task, account, cookie, config, amount)
|
self._execute_create_gold_supplier_order(
|
||||||
|
db, task, account, cookie, config, amount
|
||||||
|
)
|
||||||
except FishFinRechargeError as exc:
|
except FishFinRechargeError as exc:
|
||||||
self._mark_task(db, task, "failed", str(exc), {"recharge_channel": "supplier_api"})
|
self._mark_task(
|
||||||
|
db, task, "failed", str(exc), {"recharge_channel": "supplier_api"}
|
||||||
|
)
|
||||||
return
|
return
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
baseline_gold = account.gold_balance
|
baseline_gold = account.gold_balance
|
||||||
@@ -206,11 +262,17 @@ class GoldMixin:
|
|||||||
db.commit()
|
db.commit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._push_log("warning", f"生成鱼翅码前刷新余额失败: {exc}")
|
self._push_log("warning", f"生成鱼翅码前刷新余额失败: {exc}")
|
||||||
result = client.create_gold_qr(amount=amount, pay_type=int(config["gold_pay_type"]))
|
result = client.create_gold_qr(
|
||||||
|
amount=amount, pay_type=int(config["gold_pay_type"])
|
||||||
|
)
|
||||||
account.bind_status = "gold_qr_created"
|
account.bind_status = "gold_qr_created"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._update_task_progress(db, task, "running", f"鱼翅 {amount} 元支付码已生成,等待到账", result)
|
self._update_task_progress(
|
||||||
recharged = self._wait_gold_balance_after_payment(db, task, account, client, result, baseline_gold)
|
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():
|
if self._stop.is_set():
|
||||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||||
return
|
return
|
||||||
@@ -244,7 +306,9 @@ class GoldMixin:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""创建供应商鱼翅直充订单并轮询订单状态。"""
|
"""创建供应商鱼翅直充订单并轮询订单状态。"""
|
||||||
product_id = str(config.get("gold_api_product_id") or "").strip()
|
product_id = str(config.get("gold_api_product_id") or "").strip()
|
||||||
template_name = str(config.get("gold_api_account_template_name") or "斗鱼昵称").strip()
|
template_name = str(
|
||||||
|
config.get("gold_api_account_template_name") or "斗鱼昵称"
|
||||||
|
).strip()
|
||||||
if not product_id:
|
if not product_id:
|
||||||
raise FishFinRechargeError("请先在配置中填写供应商直充商品 ID")
|
raise FishFinRechargeError("请先在配置中填写供应商直充商品 ID")
|
||||||
# 充值商品按斗鱼昵称识别账号,UID 只能作为审计信息,不能作为充值值。
|
# 充值商品按斗鱼昵称识别账号,UID 只能作为审计信息,不能作为充值值。
|
||||||
@@ -283,8 +347,16 @@ class GoldMixin:
|
|||||||
)
|
)
|
||||||
elif stage == "response":
|
elif stage == "response":
|
||||||
status = self._to_int(event.get("order_status"))
|
status = self._to_int(event.get("order_status"))
|
||||||
status_labels = {0: "待处理", 1: "处理中", 2: "成功", 3: "失败", 4: "异常"}
|
status_labels = {
|
||||||
status_text = status_labels.get(status, "-")
|
0: "待处理",
|
||||||
|
1: "处理中",
|
||||||
|
2: "成功",
|
||||||
|
3: "失败",
|
||||||
|
4: "异常",
|
||||||
|
}
|
||||||
|
status_text = status_labels.get(
|
||||||
|
status if status is not None else -1, "-"
|
||||||
|
)
|
||||||
reason = str(event.get("fail_reason") or event.get("message") or "-")
|
reason = str(event.get("fail_reason") or event.get("message") or "-")
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"info",
|
||||||
@@ -310,7 +382,9 @@ class GoldMixin:
|
|||||||
pay_amount=pay_amount,
|
pay_amount=pay_amount,
|
||||||
out_order_id=order_no,
|
out_order_id=order_no,
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
recharge_arg=[{"templateName": template_name, "templateVal": recharge_account}],
|
recharge_arg=[
|
||||||
|
{"templateName": template_name, "templateVal": recharge_account}
|
||||||
|
],
|
||||||
order_type=0,
|
order_type=0,
|
||||||
notify_url=client.config.notify_url,
|
notify_url=client.config.notify_url,
|
||||||
)
|
)
|
||||||
@@ -331,11 +405,19 @@ class GoldMixin:
|
|||||||
"supplier_order": self._supplier_result(order_payload),
|
"supplier_order": self._supplier_result(order_payload),
|
||||||
}
|
}
|
||||||
if code != 200:
|
if code != 200:
|
||||||
self._mark_task(db, task, "failed", self._supplier_message(order_payload) or "供应商创建直充订单失败", result)
|
self._mark_task(
|
||||||
|
db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
self._supplier_message(order_payload) or "供应商创建直充订单失败",
|
||||||
|
result,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
account.bind_status = "gold_api_order_created"
|
account.bind_status = "gold_api_order_created"
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._update_task_progress(db, task, "running", "供应商直充订单已创建,等待到账", result)
|
self._update_task_progress(
|
||||||
|
db, task, "running", "供应商直充订单已创建,等待到账", result
|
||||||
|
)
|
||||||
if status not in {2, 3, 4}:
|
if status not in {2, 3, 4}:
|
||||||
status = self._wait_supplier_gold_order(db, task, client, result)
|
status = self._wait_supplier_gold_order(db, task, client, result)
|
||||||
if self._stop.is_set():
|
if self._stop.is_set():
|
||||||
@@ -351,7 +433,9 @@ class GoldMixin:
|
|||||||
return
|
return
|
||||||
self._mark_task(db, task, "failed", "供应商直充订单查询超时", result)
|
self._mark_task(db, task, "failed", "供应商直充订单查询超时", result)
|
||||||
|
|
||||||
def _execute_query_gold_balance(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
def _execute_query_gold_balance(
|
||||||
|
self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict
|
||||||
|
):
|
||||||
client = self._client(cookie)
|
client = self._client(cookie)
|
||||||
result = self._refresh_account_gold_balance(client, account)
|
result = self._refresh_account_gold_balance(client, account)
|
||||||
account.bind_status = "gold_balance_queried"
|
account.bind_status = "gold_balance_queried"
|
||||||
@@ -363,4 +447,3 @@ class GoldMixin:
|
|||||||
f"鱼翅余额: {account.gold_balance if account.gold_balance is not None else '-'}",
|
f"鱼翅余额: {account.gold_balance if account.gold_balance is not None else '-'}",
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user