实现虎牙充值商品列表与支付二维码
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""新增虎牙充值商品快照表
|
||||
|
||||
Revision ID: 20260704_0005
|
||||
Revises: 20260704_0004
|
||||
Create Date: 2026-07-04
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260704_0005"
|
||||
down_revision: Union[str, None] = "20260704_0004"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
return sa.inspect(bind).has_table(table_name)
|
||||
|
||||
|
||||
def _indexes(bind, table_name: str) -> set[str]:
|
||||
if not _has_table(bind, table_name):
|
||||
return set()
|
||||
return {index["name"] for index in sa.inspect(bind).get_indexes(table_name)}
|
||||
|
||||
|
||||
def _create_index_if_missing(bind, name: str, table_name: str, columns: list[str], unique: bool = False) -> None:
|
||||
if name not in _indexes(bind, table_name):
|
||||
op.create_index(name, table_name, columns, unique=unique)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if not _has_table(bind, "huya_recharge_goods_snapshot"):
|
||||
op.create_table(
|
||||
"huya_recharge_goods_snapshot",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("spu_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("sku_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("name", sa.String(length=256), nullable=True),
|
||||
sa.Column("price", sa.Integer(), nullable=True),
|
||||
sa.Column("stock", sa.Integer(), nullable=True),
|
||||
sa.Column("buy_limit", sa.Integer(), nullable=True),
|
||||
sa.Column("icon", sa.String(length=512), nullable=True),
|
||||
sa.Column("description", sa.String(length=512), nullable=True),
|
||||
sa.Column("task_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("task_name", sa.String(length=256), nullable=True),
|
||||
sa.Column("raw", sa.JSON(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
_create_index_if_missing(
|
||||
bind,
|
||||
"ix_huya_recharge_goods_snapshot_spu_id",
|
||||
"huya_recharge_goods_snapshot",
|
||||
["spu_id"],
|
||||
)
|
||||
_create_index_if_missing(
|
||||
bind,
|
||||
"ix_huya_recharge_goods_snapshot_sku_id",
|
||||
"huya_recharge_goods_snapshot",
|
||||
["sku_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if _has_table(bind, "huya_recharge_goods_snapshot"):
|
||||
indexes = _indexes(bind, "huya_recharge_goods_snapshot")
|
||||
if "ix_huya_recharge_goods_snapshot_sku_id" in indexes:
|
||||
op.drop_index("ix_huya_recharge_goods_snapshot_sku_id", table_name="huya_recharge_goods_snapshot")
|
||||
if "ix_huya_recharge_goods_snapshot_spu_id" in indexes:
|
||||
op.drop_index("ix_huya_recharge_goods_snapshot_spu_id", table_name="huya_recharge_goods_snapshot")
|
||||
op.drop_table("huya_recharge_goods_snapshot")
|
||||
@@ -147,6 +147,25 @@ class HuyaGoodsSnapshot(Base):
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
|
||||
class HuyaRechargeGoodsSnapshot(Base):
|
||||
"""虎牙充值商品快照"""
|
||||
__tablename__ = "huya_recharge_goods_snapshot"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
spu_id = Column(String(64), nullable=False, index=True)
|
||||
sku_id = Column(String(64), default="", index=True)
|
||||
name = Column(String(256), default="")
|
||||
price = Column(Integer, nullable=True)
|
||||
stock = Column(Integer, nullable=True)
|
||||
buy_limit = Column(Integer, nullable=True)
|
||||
icon = Column(String(512), default="")
|
||||
description = Column(String(512), default="")
|
||||
task_id = Column(String(64), default="")
|
||||
task_name = Column(String(256), default="")
|
||||
raw = Column(JSON, nullable=True)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
|
||||
class ProxyConfig(Base):
|
||||
"""代理配置(全局单条记录)"""
|
||||
__tablename__ = "proxy_config"
|
||||
|
||||
@@ -9,13 +9,14 @@ from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from ..database import SessionLocal, get_db
|
||||
from ..deps import authenticate_websocket, require_permission
|
||||
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaTask, User
|
||||
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User
|
||||
from ..schemas import (
|
||||
HuyaAccountOut,
|
||||
HuyaConfigOut,
|
||||
HuyaConfigUpdate,
|
||||
HuyaCookieImport,
|
||||
HuyaGoodsOut,
|
||||
HuyaRechargeGoodsOut,
|
||||
HuyaTaskBatchRequest,
|
||||
HuyaTaskOut,
|
||||
)
|
||||
@@ -200,6 +201,16 @@ def list_goods(
|
||||
return rows
|
||||
|
||||
|
||||
@router.get("/recharge-goods", response_model=list[HuyaRechargeGoodsOut])
|
||||
def list_recharge_goods(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:task")),
|
||||
):
|
||||
"""查看已缓存的虎牙充值商品快照。"""
|
||||
rows = db.query(HuyaRechargeGoodsSnapshot).order_by(HuyaRechargeGoodsSnapshot.id.asc()).all()
|
||||
return rows
|
||||
|
||||
|
||||
@router.post("/tasks/batch")
|
||||
async def create_task_batch(
|
||||
req: HuyaTaskBatchRequest,
|
||||
|
||||
@@ -314,6 +314,42 @@ class HuyaGoodsOut(BaseModel):
|
||||
}
|
||||
|
||||
|
||||
class HuyaRechargeGoodsOut(BaseModel):
|
||||
id: int
|
||||
spu_id: str
|
||||
sku_id: str = ""
|
||||
name: str = ""
|
||||
price: Optional[int] = None
|
||||
stock: Optional[int] = None
|
||||
buy_limit: Optional[int] = None
|
||||
icon: str = ""
|
||||
description: str = ""
|
||||
task_id: str = ""
|
||||
task_name: str = ""
|
||||
raw: Optional[dict[str, Any]] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_serializer
|
||||
def _serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"spu_id": self.spu_id,
|
||||
"sku_id": self.sku_id,
|
||||
"name": self.name,
|
||||
"price": self.price,
|
||||
"stock": self.stock,
|
||||
"buy_limit": self.buy_limit,
|
||||
"icon": self.icon,
|
||||
"description": self.description,
|
||||
"task_id": self.task_id,
|
||||
"task_name": self.task_name,
|
||||
"raw": self.raw,
|
||||
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ---- 代理配置 ----
|
||||
class ProxyConfigOut(BaseModel):
|
||||
enabled: bool = False
|
||||
|
||||
@@ -11,10 +11,24 @@ from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from core.huya import HuyaHttpClient
|
||||
from ..database import SessionLocal
|
||||
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
|
||||
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask
|
||||
from .huya_service import HUYA_CONFIG_FIELDS, cookie_value, ensure_huya_config, huya_config_value
|
||||
|
||||
|
||||
HUYA_RECHARGE_ACT_ID = 25135
|
||||
HUYA_RECHARGE_SOURCE_ID = "yellowcarlist"
|
||||
HUYA_RECHARGE_SCENE = 4
|
||||
HUYA_RECHARGE_EXTRA_PRODUCTS = [
|
||||
{
|
||||
"spu_id": "hy-5879340",
|
||||
"name": "精英宝典",
|
||||
"task_name": "开通精英宝典",
|
||||
"description": "得300积分丨解锁道具兑换权益",
|
||||
"sort": 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class HuyaBatchRunner:
|
||||
"""批量执行虎牙任务,通过队列推送实时日志。"""
|
||||
|
||||
@@ -264,6 +278,319 @@ class HuyaBatchRunner:
|
||||
message = f"已刷新商品 {len(goods)} 个"
|
||||
self._mark_task(worker_db, task, "success", message, {**result, "goods": goods})
|
||||
|
||||
@staticmethod
|
||||
def _normalize_pay_channel(value) -> str:
|
||||
text = str(value or "").strip()
|
||||
lowered = text.lower()
|
||||
if lowered in {"weixin", "wx", "wechat", "微信"}:
|
||||
return "Weixin"
|
||||
return "Zfb"
|
||||
|
||||
@staticmethod
|
||||
def _pay_channel_label(value: str) -> str:
|
||||
return "微信" if value == "Weixin" else "支付宝"
|
||||
|
||||
@staticmethod
|
||||
def _recharge_price_text(price: int | None) -> str:
|
||||
if not price:
|
||||
return ""
|
||||
return f"{price / 100:.2f}元"
|
||||
|
||||
def _execute_refresh_recharge_goods(
|
||||
self,
|
||||
worker_db: Session,
|
||||
task: HuyaTask,
|
||||
account: HuyaAccount,
|
||||
account_info: dict,
|
||||
config_info: dict,
|
||||
):
|
||||
pid = self._to_int(config_info.get("room_pid"))
|
||||
if not pid:
|
||||
self._mark_task(worker_db, task, "failed", "请先配置虎牙直播间 ID")
|
||||
return
|
||||
|
||||
uid = self._resolve_uid(account_info)
|
||||
if not uid:
|
||||
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
|
||||
return
|
||||
|
||||
cookie = account_info.get("cookie") or ""
|
||||
if not cookie:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
return
|
||||
|
||||
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}"))
|
||||
task_resp = client.get_act_task_detail(uid=uid, cookie=cookie, act_id=HUYA_RECHARGE_ACT_ID)
|
||||
if task_resp is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙充值任务详情接口无响应")
|
||||
return
|
||||
|
||||
task_result = task_resp.to_dict()
|
||||
if task_resp.status != 200:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
task_resp.msg or f"虎牙充值任务详情获取失败: {task_resp.status}",
|
||||
task_result,
|
||||
)
|
||||
return
|
||||
|
||||
candidates: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def add_candidate(item: dict):
|
||||
spu_id = str(item.get("spu_id") or "").strip()
|
||||
if not spu_id or spu_id in seen:
|
||||
return
|
||||
seen.add(spu_id)
|
||||
candidates.append(item)
|
||||
|
||||
for item in HUYA_RECHARGE_EXTRA_PRODUCTS:
|
||||
add_candidate(dict(item))
|
||||
|
||||
for index, item in enumerate(task_result.get("tasks", []), start=1):
|
||||
if int(item.get("task_type") or 0) != 67:
|
||||
continue
|
||||
add_candidate({
|
||||
"spu_id": item.get("spu_id") or "",
|
||||
"name": item.get("name") or "",
|
||||
"task_id": str(item.get("task_id") or ""),
|
||||
"task_name": item.get("name") or "",
|
||||
"description": item.get("description") or "",
|
||||
"icon": item.get("icon") or "",
|
||||
"task_url": item.get("task_url") or "",
|
||||
"prizes": item.get("prizes") or [],
|
||||
"sort": index,
|
||||
})
|
||||
|
||||
if not candidates:
|
||||
self._mark_task(worker_db, task, "failed", "未从活动任务中发现充值商品", task_result)
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
goods: list[dict] = []
|
||||
failed: list[dict] = []
|
||||
|
||||
for candidate in candidates:
|
||||
spu_id = candidate["spu_id"]
|
||||
detail_resp = client.get_goods_info(
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
pid=pid,
|
||||
spu_id=spu_id,
|
||||
sku_id=0,
|
||||
game_id="0",
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
)
|
||||
if detail_resp is None:
|
||||
failed.append({"spu_id": spu_id, "message": "商品详情接口无响应"})
|
||||
continue
|
||||
detail = detail_resp.to_dict()
|
||||
if detail_resp.code != 200 or not detail.get("sku_id"):
|
||||
failed.append({
|
||||
"spu_id": spu_id,
|
||||
"message": detail_resp.message or f"商品详情获取失败: {detail_resp.code}",
|
||||
"detail": detail,
|
||||
})
|
||||
continue
|
||||
|
||||
item = {
|
||||
**candidate,
|
||||
**detail,
|
||||
"spu_id": detail.get("spu_id") or spu_id,
|
||||
"sku_id": str(detail.get("sku_id") or ""),
|
||||
"name": detail.get("name") or candidate.get("name") or spu_id,
|
||||
"description": detail.get("description") or candidate.get("description") or "",
|
||||
"icon": detail.get("icon") or candidate.get("icon") or "",
|
||||
"task_id": candidate.get("task_id") or "",
|
||||
"task_name": candidate.get("task_name") or candidate.get("name") or "",
|
||||
"raw_order": int(candidate.get("sort") or 0),
|
||||
}
|
||||
goods.append(item)
|
||||
|
||||
worker_db.query(HuyaRechargeGoodsSnapshot).delete(synchronize_session=False)
|
||||
for item in goods:
|
||||
worker_db.add(HuyaRechargeGoodsSnapshot(
|
||||
spu_id=item["spu_id"],
|
||||
sku_id=item["sku_id"],
|
||||
name=item["name"],
|
||||
price=item.get("price") or None,
|
||||
stock=item.get("stock") or None,
|
||||
buy_limit=item.get("buy_limit") or None,
|
||||
icon=item.get("icon") or "",
|
||||
description=item.get("description") or "",
|
||||
task_id=item.get("task_id") or "",
|
||||
task_name=item.get("task_name") or "",
|
||||
raw=item,
|
||||
updated_at=now,
|
||||
))
|
||||
|
||||
account.status = "recharge_goods_refreshed"
|
||||
account.updated_at = now
|
||||
message = f"已刷新充值商品 {len(goods)} 个"
|
||||
if failed:
|
||||
message += f",失败 {len(failed)} 个"
|
||||
result = {
|
||||
"act_id": HUYA_RECHARGE_ACT_ID,
|
||||
"goods_count": len(goods),
|
||||
"failed_count": len(failed),
|
||||
"goods": goods,
|
||||
"failed": failed,
|
||||
"task_detail": task_result,
|
||||
}
|
||||
self._mark_task(worker_db, task, "success" if goods else "failed", message, result)
|
||||
|
||||
def _execute_create_recharge_order(
|
||||
self,
|
||||
worker_db: Session,
|
||||
task: HuyaTask,
|
||||
account: HuyaAccount,
|
||||
account_info: dict,
|
||||
config_info: dict,
|
||||
):
|
||||
pid = self._to_int(config_info.get("room_pid"))
|
||||
if not pid:
|
||||
self._mark_task(worker_db, task, "failed", "请先配置虎牙直播间 ID")
|
||||
return
|
||||
|
||||
spu_id = str(self.payload.get("spu_id") or "").strip()
|
||||
if not spu_id:
|
||||
self._mark_task(worker_db, task, "failed", "请选择充值商品")
|
||||
return
|
||||
|
||||
count = self._to_int(self.payload.get("count")) or 1
|
||||
count = max(1, min(count, 999))
|
||||
pay_channel = self._normalize_pay_channel(self.payload.get("pay_channel") or config_info.get("pay_channel"))
|
||||
|
||||
uid = self._resolve_uid(account_info)
|
||||
if not uid:
|
||||
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
|
||||
return
|
||||
|
||||
cookie = account_info.get("cookie") or ""
|
||||
if not cookie:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
return
|
||||
|
||||
snapshot = worker_db.query(HuyaRechargeGoodsSnapshot).filter(
|
||||
HuyaRechargeGoodsSnapshot.spu_id == spu_id
|
||||
).first()
|
||||
payload_sku_id = self._to_int(self.payload.get("sku_id"))
|
||||
sku_id = payload_sku_id or self._to_int(snapshot.sku_id if snapshot else "")
|
||||
product_name = str(self.payload.get("product_name") or (snapshot.name if snapshot else "") or spu_id)
|
||||
unit_price = int(snapshot.price or 0) if snapshot else 0
|
||||
|
||||
client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}"))
|
||||
detail_resp = client.get_goods_info(
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
pid=pid,
|
||||
spu_id=spu_id,
|
||||
sku_id=sku_id or 0,
|
||||
game_id="0",
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
)
|
||||
if detail_resp is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙充值商品详情接口无响应")
|
||||
return
|
||||
detail = detail_resp.to_dict()
|
||||
if detail_resp.code != 200:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
detail_resp.message or f"虎牙充值商品详情获取失败: {detail_resp.code}",
|
||||
detail,
|
||||
)
|
||||
return
|
||||
|
||||
sku_id = int(detail.get("sku_id") or sku_id or 0)
|
||||
product_name = detail.get("name") or product_name
|
||||
unit_price = int(detail.get("price") or unit_price or 0)
|
||||
if not sku_id:
|
||||
self._mark_task(worker_db, task, "failed", "充值商品缺少 SKU,请先刷新充值商品列表", detail)
|
||||
return
|
||||
|
||||
order_resp = client.create_order(
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
pid=pid,
|
||||
spu_id=spu_id,
|
||||
sku_id=sku_id,
|
||||
item_count=count,
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
game_id="0",
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
order_type=6,
|
||||
)
|
||||
if order_resp is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙下单接口无响应")
|
||||
return
|
||||
order_result = order_resp.to_dict()
|
||||
if order_resp.code != 200 or not order_resp.orderId:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
order_resp.message or f"虎牙下单失败: {order_resp.code}",
|
||||
{"goods": detail, "order": order_result},
|
||||
)
|
||||
return
|
||||
|
||||
pay_resp = client.pay_order_submit(
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
order_id=order_resp.orderId,
|
||||
pay_type=pay_channel,
|
||||
pid=pid,
|
||||
source_id=HUYA_RECHARGE_SOURCE_ID,
|
||||
scene=HUYA_RECHARGE_SCENE,
|
||||
item_count=count,
|
||||
)
|
||||
if pay_resp is None:
|
||||
self._mark_task(worker_db, task, "error", "虎牙支付接口无响应", {"goods": detail, "order": order_result})
|
||||
return
|
||||
pay_result = pay_resp.to_dict()
|
||||
if pay_resp.code != 200 or not pay_resp.payUrl:
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
pay_resp.message or f"虎牙支付二维码生成失败: {pay_resp.code}",
|
||||
{"goods": detail, "order": order_result, "pay": pay_result},
|
||||
)
|
||||
return
|
||||
|
||||
amount = int(pay_resp.amount or unit_price * count or 0)
|
||||
result = {
|
||||
"spu_id": spu_id,
|
||||
"sku_id": sku_id,
|
||||
"product_name": product_name,
|
||||
"count": count,
|
||||
"unit_price": unit_price,
|
||||
"amount": amount,
|
||||
"amount_text": self._recharge_price_text(amount),
|
||||
"pay_channel": pay_channel,
|
||||
"pay_channel_label": self._pay_channel_label(pay_channel),
|
||||
"order_id": order_resp.orderId,
|
||||
"app_order_id": pay_resp.appOrderId,
|
||||
"pay_order_id": pay_resp.payOrderId,
|
||||
"pay_url": pay_resp.payUrl,
|
||||
"goods": detail,
|
||||
"order": order_result,
|
||||
}
|
||||
account.status = "recharge_order_created"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
message = f"{product_name} x{count} {self._pay_channel_label(pay_channel)} {result['amount_text']}"
|
||||
self._mark_task(worker_db, task, "success", message, result)
|
||||
|
||||
def _execute_get_bind_qr(
|
||||
self,
|
||||
worker_db: Session,
|
||||
@@ -629,6 +956,8 @@ class HuyaBatchRunner:
|
||||
"confirm_bind",
|
||||
"query_game_name",
|
||||
"refresh_goods",
|
||||
"refresh_recharge_goods",
|
||||
"create_recharge_order",
|
||||
}:
|
||||
self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现")
|
||||
self._push_log("warning", f"[{current}] {name} 暂未实现: {self.task_type}")
|
||||
@@ -639,6 +968,10 @@ class HuyaBatchRunner:
|
||||
self._execute_query_points(worker_db, task, account, account_info, config_info)
|
||||
elif self.task_type == "refresh_goods":
|
||||
self._execute_refresh_goods(worker_db, task, account, account_info, config_info)
|
||||
elif self.task_type == "refresh_recharge_goods":
|
||||
self._execute_refresh_recharge_goods(worker_db, task, account, account_info, config_info)
|
||||
elif self.task_type == "create_recharge_order":
|
||||
self._execute_create_recharge_order(worker_db, task, account, account_info, config_info)
|
||||
elif self.task_type == "get_bind_qr":
|
||||
self._execute_get_bind_qr(worker_db, task, account, account_info, config_info)
|
||||
elif self.task_type == "confirm_bind":
|
||||
|
||||
@@ -13,12 +13,12 @@ from ..models import HuyaAccount, HuyaConfig, HuyaTask
|
||||
SUPPORTED_TASK_TYPES = {
|
||||
"get_bind_qr": "获取绑定二维码",
|
||||
"query_points": "一键查询积分",
|
||||
"open_elite_book": "开通精英宝典",
|
||||
"recharge_points": "充值积分",
|
||||
"query_game_name": "一键查询游戏名",
|
||||
"query_exchange_records": "一键查询兑换记录",
|
||||
"confirm_bind": "确认绑定",
|
||||
"refresh_goods": "刷新商品列表",
|
||||
"refresh_recharge_goods": "刷新充值商品列表",
|
||||
"create_recharge_order": "生成支付二维码",
|
||||
}
|
||||
|
||||
|
||||
@@ -172,8 +172,8 @@ def create_planned_tasks(
|
||||
batch_id = uuid.uuid4().hex[:12]
|
||||
payload = payload or {}
|
||||
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
|
||||
if task_type == "refresh_goods" and accounts:
|
||||
# 商品列表是全局快照,使用一个可用 CK 刷新即可。
|
||||
if task_type in {"refresh_goods", "refresh_recharge_goods", "create_recharge_order"} and accounts:
|
||||
# 全局快照和单笔支付二维码都使用一个选中的 CK 即可。
|
||||
accounts = accounts[:1]
|
||||
for account in accounts:
|
||||
db.add(HuyaTask(
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
HuyaConfig,
|
||||
HuyaCookieImportResult,
|
||||
HuyaGoodsItem,
|
||||
HuyaRechargeGoodsItem,
|
||||
HuyaTaskBatchRequest,
|
||||
HuyaTaskBatchResult,
|
||||
HuyaTaskItem,
|
||||
@@ -23,6 +24,7 @@ export const huyaApi = {
|
||||
getConfig: () => api.get<HuyaConfig, HuyaConfig>('/huya/config'),
|
||||
updateConfig: (data: Partial<HuyaConfig>) => api.put<HuyaConfig, HuyaConfig>('/huya/config', data),
|
||||
listGoods: () => api.get<HuyaGoodsItem[], HuyaGoodsItem[]>('/huya/goods'),
|
||||
listRechargeGoods: () => api.get<HuyaRechargeGoodsItem[], HuyaRechargeGoodsItem[]>('/huya/recharge-goods'),
|
||||
createTasks: (data: HuyaTaskBatchRequest) =>
|
||||
api.post<HuyaTaskBatchResult, HuyaTaskBatchResult>('/huya/tasks/batch', data),
|
||||
listTasks: (batchId?: string) =>
|
||||
|
||||
@@ -183,6 +183,22 @@ export interface HuyaGoodsItem {
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface HuyaRechargeGoodsItem {
|
||||
id: number;
|
||||
spu_id: string;
|
||||
sku_id: string;
|
||||
name: string;
|
||||
price: number | null;
|
||||
stock: number | null;
|
||||
buy_limit: number | null;
|
||||
icon: string;
|
||||
description: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
raw: Record<string, unknown> | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
// ==================== Proxy ====================
|
||||
|
||||
export interface ProxyConfig {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Form, Input, InputNumber, message, Modal, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
|
||||
Button, Card, Col, Form, Input, InputNumber, message, Modal, QRCode, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined, GiftOutlined,
|
||||
AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined,
|
||||
LinkOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type HuyaAccountItem,
|
||||
type HuyaConfig,
|
||||
type HuyaGoodsItem,
|
||||
type HuyaRechargeGoodsItem,
|
||||
type HuyaTaskItem,
|
||||
} from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
@@ -26,22 +27,22 @@ const FALLBACK_TASK_TYPES: Record<string, string> = {
|
||||
get_bind_qr: '获取绑定二维码',
|
||||
confirm_bind: '确认绑定',
|
||||
query_points: '一键查询积分',
|
||||
open_elite_book: '开通精英宝典',
|
||||
recharge_points: '充值积分',
|
||||
query_game_name: '一键查询游戏名',
|
||||
query_exchange_records: '一键查询兑换记录',
|
||||
refresh_goods: '刷新商品列表',
|
||||
refresh_recharge_goods: '刷新充值商品列表',
|
||||
create_recharge_order: '生成支付二维码',
|
||||
};
|
||||
|
||||
const QUICK_ACTIONS = [
|
||||
{ key: 'get_bind_qr', icon: <LinkOutlined /> },
|
||||
{ key: 'confirm_bind', icon: <CheckCircleOutlined /> },
|
||||
{ key: 'query_points', icon: <SearchOutlined /> },
|
||||
{ key: 'open_elite_book', icon: <GiftOutlined /> },
|
||||
{ key: 'recharge_points', icon: <CreditCardOutlined /> },
|
||||
{ key: 'query_game_name', icon: <AppstoreOutlined /> },
|
||||
{ key: 'query_exchange_records', icon: <FieldTimeOutlined /> },
|
||||
{ key: 'refresh_goods', icon: <ReloadOutlined /> },
|
||||
{ key: 'refresh_recharge_goods', icon: <ShoppingOutlined /> },
|
||||
{ key: 'create_recharge_order', icon: <CreditCardOutlined /> },
|
||||
];
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
@@ -115,22 +116,33 @@ function formatRemainText(value: string): string {
|
||||
return /^-?\d+(\.\d+)?$/.test(text) ? `${text}%` : text;
|
||||
}
|
||||
|
||||
function formatPriceText(value: number | null | undefined): string {
|
||||
if (!value) return '';
|
||||
return `¥${(value / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
function hasMiniQrcode(task: HuyaTaskItem): boolean {
|
||||
return task.task_type === 'get_bind_qr' && Boolean(resultText(task.result, 'mini_qrcode_image'));
|
||||
}
|
||||
|
||||
function hasPaymentQrcode(task: HuyaTaskItem): boolean {
|
||||
return task.task_type === 'create_recharge_order' && Boolean(resultText(task.result, 'pay_url'));
|
||||
}
|
||||
|
||||
export default function HuyaTasksPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [form] = Form.useForm<HuyaConfig>();
|
||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||
const [tasks, setTasks] = useState<HuyaTaskItem[]>([]);
|
||||
const [goods, setGoods] = useState<HuyaGoodsItem[]>([]);
|
||||
const [rechargeGoods, setRechargeGoods] = useState<HuyaRechargeGoodsItem[]>([]);
|
||||
const [taskTypes, setTaskTypes] = useState<Record<string, string>>(FALLBACK_TASK_TYPES);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
|
||||
const [selectedGoodsId, setSelectedGoodsId] = useState<string>('');
|
||||
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
|
||||
const [selectedRechargeGoodsId, setSelectedRechargeGoodsId] = useState<string>('');
|
||||
const [rechargeCount, setRechargeCount] = useState(1);
|
||||
const [rechargePayChannel, setRechargePayChannel] = useState('Weixin');
|
||||
const [concurrency, setConcurrency] = useState(() => {
|
||||
const v = localStorage.getItem('huya_task_concurrency');
|
||||
return v ? Math.max(1, Math.min(10, Number(v) || 3)) : 3;
|
||||
@@ -140,8 +152,11 @@ export default function HuyaTasksPage() {
|
||||
const [savingConfig, setSavingConfig] = useState(false);
|
||||
const [batchId, setBatchId] = useState<string | null>(null);
|
||||
const [qrTask, setQrTask] = useState<HuyaTaskItem | null>(null);
|
||||
const [payTask, setPayTask] = useState<HuyaTaskItem | null>(null);
|
||||
const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
|
||||
const autoOpenQrReady = useRef(false);
|
||||
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
|
||||
const autoOpenPayReady = useRef(false);
|
||||
const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs();
|
||||
const { can } = usePermissions();
|
||||
|
||||
@@ -158,18 +173,30 @@ export default function HuyaTasksPage() {
|
||||
autoOpenQrReady.current = true;
|
||||
}, []);
|
||||
|
||||
const rememberExistingPaymentQrcodes = useCallback((items: HuyaTaskItem[]) => {
|
||||
if (autoOpenPayReady.current) return;
|
||||
items.filter(hasPaymentQrcode).forEach((task) => autoOpenedPayTaskIds.current.add(task.id));
|
||||
autoOpenPayReady.current = true;
|
||||
}, []);
|
||||
|
||||
const openQrTask = useCallback((task: HuyaTaskItem) => {
|
||||
autoOpenedQrTaskIds.current.add(task.id);
|
||||
setQrTask(task);
|
||||
}, []);
|
||||
|
||||
const openPayTask = useCallback((task: HuyaTaskItem) => {
|
||||
autoOpenedPayTaskIds.current.add(task.id);
|
||||
setPayTask(task);
|
||||
}, []);
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [accountResult, taskResult, goodsResult, configResult, taskTypeResult] = await Promise.allSettled([
|
||||
const [accountResult, taskResult, goodsResult, rechargeGoodsResult, configResult, taskTypeResult] = await Promise.allSettled([
|
||||
huyaApi.listAccounts(),
|
||||
huyaApi.listTasks(),
|
||||
huyaApi.listGoods(),
|
||||
huyaApi.listRechargeGoods(),
|
||||
canConfig ? huyaApi.getConfig() : Promise.resolve(null),
|
||||
huyaApi.taskTypes(),
|
||||
]);
|
||||
@@ -177,16 +204,19 @@ export default function HuyaTasksPage() {
|
||||
if (accountResult.status === 'fulfilled') setAccounts(accountResult.value);
|
||||
if (taskResult.status === 'fulfilled') {
|
||||
rememberExistingQrcodes(taskResult.value);
|
||||
rememberExistingPaymentQrcodes(taskResult.value);
|
||||
setTasks(taskResult.value);
|
||||
}
|
||||
if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
|
||||
if (rechargeGoodsResult.status === 'fulfilled') setRechargeGoods(rechargeGoodsResult.value);
|
||||
if (configResult.status === 'fulfilled' && configResult.value) form.setFieldsValue(configResult.value);
|
||||
if (taskTypeResult.status === 'fulfilled') setTaskTypes({ ...FALLBACK_TASK_TYPES, ...taskTypeResult.value });
|
||||
|
||||
const failedLabels = [
|
||||
accountResult.status === 'rejected' ? `CK 列表: ${getErrorMessage(accountResult.reason)}` : '',
|
||||
taskResult.status === 'rejected' ? `任务记录: ${getErrorMessage(taskResult.reason)}` : '',
|
||||
goodsResult.status === 'rejected' ? `商品快照: ${getErrorMessage(goodsResult.reason)}` : '',
|
||||
goodsResult.status === 'rejected' ? `兑换商品: ${getErrorMessage(goodsResult.reason)}` : '',
|
||||
rechargeGoodsResult.status === 'rejected' ? `充值商品: ${getErrorMessage(rechargeGoodsResult.reason)}` : '',
|
||||
configResult.status === 'rejected' ? `虎牙配置: ${getErrorMessage(configResult.reason)}` : '',
|
||||
taskTypeResult.status === 'rejected' ? `任务类型: ${getErrorMessage(taskTypeResult.reason)}` : '',
|
||||
].filter(Boolean);
|
||||
@@ -196,17 +226,18 @@ export default function HuyaTasksPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canConfig, form, rememberExistingQrcodes]);
|
||||
}, [canConfig, form, rememberExistingPaymentQrcodes, rememberExistingQrcodes]);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
try {
|
||||
const data = await huyaApi.listTasks();
|
||||
rememberExistingQrcodes(data);
|
||||
rememberExistingPaymentQrcodes(data);
|
||||
setTasks(data);
|
||||
} catch {
|
||||
// 轮询失败不打扰操作,下一轮继续刷新。
|
||||
}
|
||||
}, [rememberExistingQrcodes]);
|
||||
}, [rememberExistingPaymentQrcodes, rememberExistingQrcodes]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
@@ -225,6 +256,14 @@ export default function HuyaTasksPage() {
|
||||
if (nextQrTask) openQrTask(nextQrTask);
|
||||
}, [openQrTask, qrTask, tasks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoOpenPayReady.current || payTask) return;
|
||||
const nextPayTask = tasks
|
||||
.filter((task) => hasPaymentQrcode(task) && !autoOpenedPayTaskIds.current.has(task.id))
|
||||
.sort((a, b) => b.id - a.id)[0];
|
||||
if (nextPayTask) openPayTask(nextPayTask);
|
||||
}, [openPayTask, payTask, tasks]);
|
||||
|
||||
const accountOptions = useMemo(() => {
|
||||
return accounts.map((account) => ({ value: account.id, label: accountLabel(account) }));
|
||||
}, [accounts]);
|
||||
@@ -237,16 +276,34 @@ export default function HuyaTasksPage() {
|
||||
));
|
||||
}, [goods]);
|
||||
|
||||
const goodsOptions = useMemo(() => {
|
||||
return sortedGoods.map((item) => ({
|
||||
value: item.product_id,
|
||||
label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`,
|
||||
}));
|
||||
}, [sortedGoods]);
|
||||
const sortedRechargeGoods = useMemo(() => {
|
||||
return [...rechargeGoods].sort((a, b) => {
|
||||
const aOrder = goodsRawNumber({ raw: a.raw } as HuyaGoodsItem, 'raw_order');
|
||||
const bOrder = goodsRawNumber({ raw: b.raw } as HuyaGoodsItem, 'raw_order');
|
||||
return aOrder - bOrder || a.id - b.id;
|
||||
});
|
||||
}, [rechargeGoods]);
|
||||
|
||||
const selectedGoods = useMemo(() => {
|
||||
return sortedGoods.find((item) => item.product_id === selectedGoodsId) || null;
|
||||
}, [sortedGoods, selectedGoodsId]);
|
||||
const rechargeGoodsOptions = useMemo(() => {
|
||||
return sortedRechargeGoods.map((item) => ({
|
||||
value: item.spu_id,
|
||||
label: `${item.name || item.spu_id}${item.price ? ` / ${formatPriceText(item.price)}` : ''}`,
|
||||
}));
|
||||
}, [sortedRechargeGoods]);
|
||||
|
||||
const selectedRechargeGoods = useMemo(() => {
|
||||
return sortedRechargeGoods.find((item) => item.spu_id === selectedRechargeGoodsId) || null;
|
||||
}, [sortedRechargeGoods, selectedRechargeGoodsId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortedRechargeGoods.length === 0) {
|
||||
if (selectedRechargeGoodsId) setSelectedRechargeGoodsId('');
|
||||
return;
|
||||
}
|
||||
if (!sortedRechargeGoods.some((item) => item.spu_id === selectedRechargeGoodsId)) {
|
||||
setSelectedRechargeGoodsId(sortedRechargeGoods[0].spu_id);
|
||||
}
|
||||
}, [selectedRechargeGoodsId, sortedRechargeGoods]);
|
||||
|
||||
const goodsCategories = useMemo(() => {
|
||||
const map = new Map<string, { key: string; label: string; sort: number; count: number }>();
|
||||
@@ -283,11 +340,13 @@ export default function HuyaTasksPage() {
|
||||
}, [sortedGoods, selectedGoodsCategory]);
|
||||
|
||||
const createPayload = (taskType: string) => {
|
||||
if (taskType !== 'recharge_points') return {};
|
||||
if (taskType !== 'create_recharge_order') return {};
|
||||
return {
|
||||
product_id: selectedGoods?.product_id || selectedGoodsId,
|
||||
product_name: selectedGoods?.name || '',
|
||||
spu_id: selectedRechargeGoods?.spu_id || selectedRechargeGoodsId,
|
||||
sku_id: selectedRechargeGoods?.sku_id || '',
|
||||
product_name: selectedRechargeGoods?.name || '',
|
||||
count: rechargeCount,
|
||||
pay_channel: rechargePayChannel,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -296,7 +355,7 @@ export default function HuyaTasksPage() {
|
||||
message.warning('请先选择虎牙 CK');
|
||||
return;
|
||||
}
|
||||
if (taskType === 'recharge_points' && !selectedGoodsId) {
|
||||
if (taskType === 'create_recharge_order' && !selectedRechargeGoodsId) {
|
||||
message.warning('请先选择充值商品');
|
||||
return;
|
||||
}
|
||||
@@ -312,7 +371,7 @@ export default function HuyaTasksPage() {
|
||||
const finishTask = () => {
|
||||
setBatchId(null);
|
||||
setStarting(false);
|
||||
if (taskType === 'refresh_goods') {
|
||||
if (taskType === 'refresh_goods' || taskType === 'refresh_recharge_goods') {
|
||||
void loadAll();
|
||||
} else {
|
||||
void loadTasks();
|
||||
@@ -354,6 +413,15 @@ export default function HuyaTasksPage() {
|
||||
const qrAccountName = qrTask
|
||||
? resultProfileNick(qrResult) || qrTask.account_nickname || qrTask.account_uid || `#${qrTask.account_id}`
|
||||
: '';
|
||||
const payResult = payTask?.result || null;
|
||||
const payUrl = resultText(payResult, 'pay_url');
|
||||
const payProductName = resultText(payResult, 'product_name');
|
||||
const payAmountText = resultText(payResult, 'amount_text');
|
||||
const payChannelLabel = resultText(payResult, 'pay_channel_label');
|
||||
const payOrderId = payResult?.order_id;
|
||||
const payAccountName = payTask
|
||||
? payTask.account_nickname || payTask.account_uid || `#${payTask.account_id}`
|
||||
: '';
|
||||
|
||||
const renderTaskResult = (value: Record<string, unknown> | null, record: HuyaTaskItem) => {
|
||||
const bindQrImage = resultText(value, 'mini_qrcode_image');
|
||||
@@ -400,6 +468,29 @@ export default function HuyaTasksPage() {
|
||||
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
if (record.task_type === 'refresh_recharge_goods') {
|
||||
const goodsCount = value?.goods_count;
|
||||
const failedCount = value?.failed_count;
|
||||
if (typeof goodsCount === 'number') {
|
||||
return (
|
||||
<Space size={6}>
|
||||
<Tag color="green">充值商品 {goodsCount} 个</Tag>
|
||||
{typeof failedCount === 'number' && failedCount > 0 ? <Tag color="orange">失败 {failedCount}</Tag> : null}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
if (record.task_type === 'create_recharge_order') {
|
||||
if (resultText(value, 'pay_url')) {
|
||||
return (
|
||||
<Button size="small" icon={<QrcodeOutlined />} onClick={() => openPayTask(record)}>
|
||||
查看支付码
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
|
||||
const availableScore = value?.available_score;
|
||||
if (typeof availableScore === 'number') {
|
||||
@@ -479,6 +570,39 @@ export default function HuyaTasksPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const rechargeGoodsColumns: TableProps<HuyaRechargeGoodsItem>['columns'] = [
|
||||
{ title: 'SPU', dataIndex: 'spu_id', width: 120, ellipsis: true },
|
||||
{ title: 'SKU', dataIndex: 'sku_id', width: 100, ellipsis: true },
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'price',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (value: number | null) => formatPriceText(value) || <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (value: number | null) => value ?? <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'task_name',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (value: string) => value || <Text type="secondary">商品详情</Text>,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
width: 160,
|
||||
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
@@ -530,8 +654,8 @@ export default function HuyaTasksPage() {
|
||||
<Form.Item label="支付渠道" name="pay_channel">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
{ value: 'Wx', label: '微信' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -540,27 +664,7 @@ export default function HuyaTasksPage() {
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title={<Space><ShoppingOutlined />商品快照</Space>} style={{ marginBottom: 12 }}>
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择充值商品"
|
||||
value={selectedGoodsId || undefined}
|
||||
onChange={(value) => setSelectedGoodsId(value || '')}
|
||||
options={goodsOptions}
|
||||
style={{ minWidth: 240 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={99}
|
||||
value={rechargeCount}
|
||||
onChange={(value) => setRechargeCount(value || 1)}
|
||||
addonAfter="份"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</Space>
|
||||
<Card size="small" title={<Space><ShoppingOutlined />兑换商品列表</Space>} style={{ marginBottom: 12 }}>
|
||||
{goodsCategories.length > 0 && (
|
||||
<Tabs
|
||||
size="small"
|
||||
@@ -580,8 +684,78 @@ export default function HuyaTasksPage() {
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 760, y: 220 }}
|
||||
locale={{ emptyText: '暂无商品快照,请先刷新商品列表' }}
|
||||
scroll={{ y: 220 }}
|
||||
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title={<Space><CreditCardOutlined />充值商品列表</Space>}
|
||||
extra={(
|
||||
<Space size={6}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => startTask('refresh_recharge_goods')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<QrcodeOutlined />}
|
||||
onClick={() => startTask('create_recharge_order')}
|
||||
disabled={!canTask || selectedIds.length === 0 || wsConnected}
|
||||
>
|
||||
支付码
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Space style={{ marginBottom: 8 }} wrap>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="选择充值商品"
|
||||
value={selectedRechargeGoodsId || undefined}
|
||||
onChange={(value) => setSelectedRechargeGoodsId(value || '')}
|
||||
options={rechargeGoodsOptions}
|
||||
style={{ minWidth: 220 }}
|
||||
filterOption={(input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase())}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={999}
|
||||
value={rechargeCount}
|
||||
onChange={(value) => setRechargeCount(value || 1)}
|
||||
addonAfter="份"
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Select
|
||||
value={rechargePayChannel}
|
||||
onChange={setRechargePayChannel}
|
||||
options={[
|
||||
{ value: 'Weixin', label: '微信' },
|
||||
{ value: 'Zfb', label: '支付宝' },
|
||||
]}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
columns={rechargeGoodsColumns}
|
||||
dataSource={sortedRechargeGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 220 }}
|
||||
locale={{ emptyText: '暂无充值商品,请先刷新充值商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedRechargeGoodsId(record.spu_id),
|
||||
})}
|
||||
rowClassName={(record) => record.spu_id === selectedRechargeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -640,7 +814,18 @@ export default function HuyaTasksPage() {
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{QUICK_ACTIONS.map((item) => (
|
||||
<Tooltip key={item.key} title={item.key === 'refresh_goods' ? '使用一个选中的 CK 刷新当前 SID 商品快照' : undefined}>
|
||||
<Tooltip
|
||||
key={item.key}
|
||||
title={
|
||||
item.key === 'refresh_goods'
|
||||
? '使用一个选中的 CK 刷新当前 SID 兑换商品'
|
||||
: item.key === 'refresh_recharge_goods'
|
||||
? '使用一个选中的 CK 刷新充值商品列表'
|
||||
: item.key === 'create_recharge_order'
|
||||
? '按左侧选择生成扫码支付二维码'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={item.icon}
|
||||
size="small"
|
||||
@@ -653,7 +838,7 @@ export default function HuyaTasksPage() {
|
||||
))}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
当前已接入查询积分、获取绑定二维码、确认绑定、查询游戏名和刷新商品列表。
|
||||
当前已接入查询积分、绑定、兑换商品、充值商品列表和扫码支付。
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -725,6 +910,35 @@ export default function HuyaTasksPage() {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="支付二维码"
|
||||
open={!!payTask}
|
||||
onCancel={() => setPayTask(null)}
|
||||
footer={null}
|
||||
width={380}
|
||||
>
|
||||
{payTask && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, padding: '8px 0 12px' }}>
|
||||
<Space direction="vertical" size={2} style={{ width: '100%', textAlign: 'center' }}>
|
||||
<Text strong>{payProductName || '充值商品'}</Text>
|
||||
<Text type="secondary">
|
||||
{payChannelLabel || '支付'}{payAmountText ? ` / ${payAmountText}` : ''}{payAccountName ? ` / ${payAccountName}` : ''}
|
||||
</Text>
|
||||
{typeof payOrderId === 'number' || typeof payOrderId === 'string' ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>订单号 {String(payOrderId)}</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
{payUrl ? (
|
||||
<div style={{ padding: 14, borderRadius: 10, background: '#fff', lineHeight: 0 }}>
|
||||
<QRCode value={payUrl} size={260} bordered={false} color="#000" bgColor="#fff" />
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">暂无支付二维码</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user