实现虎牙刷新商品列表
This commit is contained in:
@@ -104,6 +104,191 @@ class GetUserScoreResp(TafStruct):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GetActPrizeListReq(TafStruct):
|
||||||
|
"""webActUI.getActPrizeList 请求。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.userId = ActivityUserId()
|
||||||
|
self.sid: int = 0
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_struct(0, self.userId)
|
||||||
|
os.write_int32(1, self.sid)
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.userId = ins.read_struct(0, ActivityUserId) or self.userId
|
||||||
|
self.sid = ins.read_int32(1, default=self.sid)
|
||||||
|
|
||||||
|
|
||||||
|
class ActPrizeItem(TafStruct):
|
||||||
|
"""活动商品项,字段位置来自 getActPrizeList 实测响应。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.prizeId: int = 0
|
||||||
|
self.prizeType: int = 0
|
||||||
|
self.name: str = ""
|
||||||
|
self.score: int = 0
|
||||||
|
self.icon: str = ""
|
||||||
|
self.stock: int = 0
|
||||||
|
self.remainText: str = ""
|
||||||
|
self.price: int = 0
|
||||||
|
self.categoryId: str = ""
|
||||||
|
self.totalRemain: int = 0
|
||||||
|
self.rawOrder: int = 0
|
||||||
|
self.startTime: int = 0
|
||||||
|
self.endTime: int = 0
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.prizeId = ins.read_int64(0, default=self.prizeId)
|
||||||
|
self.prizeType = ins.read_int32(1, default=self.prizeType)
|
||||||
|
self.name = ins.read_string(2, default=self.name)
|
||||||
|
self.score = ins.read_int64(3, default=self.score)
|
||||||
|
ins.read_int32(4, default=0)
|
||||||
|
self.icon = ins.read_string(5, default=self.icon)
|
||||||
|
self.stock = ins.read_int64(10, default=self.stock)
|
||||||
|
self.remainText = ins.read_string(12, default=self.remainText)
|
||||||
|
self.price = ins.read_int64(13, default=self.price)
|
||||||
|
self.categoryId = ins.read_string(14, default=self.categoryId)
|
||||||
|
self.totalRemain = ins.read_int64(17, default=self.totalRemain)
|
||||||
|
self.rawOrder = ins.read_int64(22, default=self.rawOrder)
|
||||||
|
self.startTime = ins.read_int64(25, default=self.startTime)
|
||||||
|
self.endTime = ins.read_int64(26, default=self.endTime)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int64(0, self.prizeId)
|
||||||
|
os.write_int32(1, self.prizeType)
|
||||||
|
os.write_string(2, self.name)
|
||||||
|
os.write_int64(3, self.score)
|
||||||
|
os.write_string(5, self.icon)
|
||||||
|
os.write_int64(10, self.stock)
|
||||||
|
os.write_string(12, self.remainText)
|
||||||
|
os.write_int64(13, self.price)
|
||||||
|
os.write_string(14, self.categoryId)
|
||||||
|
os.write_int64(17, self.totalRemain)
|
||||||
|
os.write_int64(22, self.rawOrder)
|
||||||
|
os.write_int64(25, self.startTime)
|
||||||
|
os.write_int64(26, self.endTime)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_list_item(ins: TafInputStream, _tag: int):
|
||||||
|
item = ActPrizeItem()
|
||||||
|
item.read_from(ins)
|
||||||
|
while True:
|
||||||
|
_end_tag, dtype = ins.read_head()
|
||||||
|
if dtype == TafType.STRUCT_END:
|
||||||
|
break
|
||||||
|
ins.skip_field(dtype)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remain_percent_text(value: str) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text or text.endswith("%"):
|
||||||
|
return text
|
||||||
|
try:
|
||||||
|
float(text)
|
||||||
|
except ValueError:
|
||||||
|
return text
|
||||||
|
return f"{text}%"
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"product_id": str(self.prizeId),
|
||||||
|
"name": self.name,
|
||||||
|
"price": self.price,
|
||||||
|
"remain_text": self._remain_percent_text(self.remainText),
|
||||||
|
"remain_percent": self.remainText,
|
||||||
|
"stock": self.stock,
|
||||||
|
"score": self.score,
|
||||||
|
"icon": self.icon,
|
||||||
|
"prize_type": self.prizeType,
|
||||||
|
"category_id": self.categoryId,
|
||||||
|
"total_remain": self.totalRemain,
|
||||||
|
"raw_order": self.rawOrder,
|
||||||
|
"start_time": self.startTime,
|
||||||
|
"end_time": self.endTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ActPrizeTag(TafStruct):
|
||||||
|
"""活动商品分类。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tagId: int = 0
|
||||||
|
self.name: str = ""
|
||||||
|
self.sort: int = 0
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.tagId = ins.read_int64(0, default=self.tagId)
|
||||||
|
self.name = ins.read_string(1, default=self.name)
|
||||||
|
self.sort = ins.read_int32(2, default=self.sort)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int64(0, self.tagId)
|
||||||
|
os.write_string(1, self.name)
|
||||||
|
os.write_int32(2, self.sort)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_list_item(ins: TafInputStream, _tag: int):
|
||||||
|
item = ActPrizeTag()
|
||||||
|
item.read_from(ins)
|
||||||
|
while True:
|
||||||
|
_end_tag, dtype = ins.read_head()
|
||||||
|
if dtype == TafType.STRUCT_END:
|
||||||
|
break
|
||||||
|
ins.skip_field(dtype)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"category_id": str(self.tagId),
|
||||||
|
"category_name": self.name,
|
||||||
|
"sort": self.sort,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GetActPrizeListResp(TafStruct):
|
||||||
|
"""webActUI.getActPrizeList 响应。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.status: int = 0
|
||||||
|
self.msg: str = ""
|
||||||
|
self.prizes: list[ActPrizeItem] = []
|
||||||
|
self.tags: list[ActPrizeTag] = []
|
||||||
|
|
||||||
|
def read_from(self, ins: TafInputStream):
|
||||||
|
self.status = ins.read_int32(0, default=self.status)
|
||||||
|
self.msg = ins.read_string(1, default=self.msg)
|
||||||
|
self.prizes = ins.read_list(2, item_reader=ActPrizeItem.read_list_item)
|
||||||
|
self.tags = ins.read_list(4, item_reader=ActPrizeTag.read_list_item)
|
||||||
|
|
||||||
|
def write_to(self, os: TafOutputStream):
|
||||||
|
os.write_int32(0, self.status)
|
||||||
|
os.write_string(1, self.msg)
|
||||||
|
os.write_list(2, self.prizes)
|
||||||
|
os.write_list(4, self.tags)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
tag_info = {
|
||||||
|
str(item.tagId): {"name": item.name, "sort": index, "raw_sort": item.sort}
|
||||||
|
for index, item in enumerate(self.tags)
|
||||||
|
}
|
||||||
|
goods = []
|
||||||
|
for item in self.prizes:
|
||||||
|
data = item.to_dict()
|
||||||
|
category = tag_info.get(data["category_id"], {})
|
||||||
|
data["category_name"] = category.get("name", "")
|
||||||
|
data["category_sort"] = category.get("sort", 0)
|
||||||
|
goods.append(data)
|
||||||
|
return {
|
||||||
|
"status": self.status,
|
||||||
|
"msg": self.msg,
|
||||||
|
"goods_count": len(self.prizes),
|
||||||
|
"categories": [item.to_dict() for item in self.tags],
|
||||||
|
"goods": goods,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GameAccount(TafStruct):
|
class GameAccount(TafStruct):
|
||||||
"""游戏账号信息。"""
|
"""游戏账号信息。"""
|
||||||
|
|
||||||
@@ -532,6 +717,35 @@ def _self_check():
|
|||||||
assert parsed.status == 200
|
assert parsed.status == 200
|
||||||
assert parsed.available_score == 180
|
assert parsed.available_score == 180
|
||||||
|
|
||||||
|
prize_rsp = GetActPrizeListResp()
|
||||||
|
prize = ActPrizeItem()
|
||||||
|
prize.prizeId = 12885
|
||||||
|
prize.name = "赤蝎幽灵大礼包"
|
||||||
|
prize.price = 3600
|
||||||
|
prize.remainText = "39"
|
||||||
|
prize.categoryId = "85"
|
||||||
|
tag = ActPrizeTag()
|
||||||
|
tag.tagId = 85
|
||||||
|
tag.name = "限量返场"
|
||||||
|
tag.sort = 2
|
||||||
|
prize_rsp.status = 200
|
||||||
|
prize_rsp.msg = "请求成功"
|
||||||
|
prize_rsp.prizes = [prize]
|
||||||
|
prize_rsp.tags = [tag]
|
||||||
|
os = TafOutputStream()
|
||||||
|
os.write_struct(0, prize_rsp)
|
||||||
|
ins = TafInputStream(os.get_bytes())
|
||||||
|
_, dtype = ins.read_head()
|
||||||
|
assert dtype == TafType.STRUCT_BEGIN
|
||||||
|
parsed_prize = GetActPrizeListResp()
|
||||||
|
parsed_prize.read_from(ins)
|
||||||
|
assert parsed_prize.status == 200
|
||||||
|
assert parsed_prize.prizes[0].name == "赤蝎幽灵大礼包"
|
||||||
|
assert parsed_prize.prizes[0].price == 3600
|
||||||
|
parsed_prize_data = parsed_prize.to_dict()
|
||||||
|
assert parsed_prize_data["goods"][0]["category_name"] == "限量返场"
|
||||||
|
assert parsed_prize_data["goods"][0]["remain_text"] == "39%"
|
||||||
|
|
||||||
link_rsp = GetLiveLinkParamResp()
|
link_rsp = GetLiveLinkParamResp()
|
||||||
link_rsp.status = 200
|
link_rsp.status = 200
|
||||||
link_rsp.livelinkParam = {
|
link_rsp.livelinkParam = {
|
||||||
|
|||||||
@@ -270,6 +270,22 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_act_prize_list(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
||||||
|
"""查询活动可兑换商品列表。"""
|
||||||
|
from .activity_structs import GetActPrizeListReq, GetActPrizeListResp
|
||||||
|
req = GetActPrizeListReq()
|
||||||
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
|
req.sid = sid
|
||||||
|
return self.call_rpc(
|
||||||
|
"webActUI",
|
||||||
|
"getActPrizeList",
|
||||||
|
req,
|
||||||
|
GetActPrizeListResp,
|
||||||
|
uid=uid,
|
||||||
|
cookie=cookie,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
def check_user_bind_game_account(
|
def check_user_bind_game_account(
|
||||||
self,
|
self,
|
||||||
uid: int,
|
uid: int,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ def list_goods(
|
|||||||
current: User = Depends(require_permission("huya:task")),
|
current: User = Depends(require_permission("huya:task")),
|
||||||
):
|
):
|
||||||
"""查看已缓存的虎牙商品快照。"""
|
"""查看已缓存的虎牙商品快照。"""
|
||||||
rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.updated_at.desc()).all()
|
rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.id.asc()).all()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.orm import Session, joinedload
|
|||||||
|
|
||||||
from core.huya import HuyaHttpClient
|
from core.huya import HuyaHttpClient
|
||||||
from ..database import SessionLocal
|
from ..database import SessionLocal
|
||||||
from ..models import HuyaAccount, HuyaTask
|
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
|
||||||
from .huya_service import HUYA_CONFIG_FIELDS, cookie_value, ensure_huya_config, huya_config_value
|
from .huya_service import HUYA_CONFIG_FIELDS, cookie_value, ensure_huya_config, huya_config_value
|
||||||
|
|
||||||
|
|
||||||
@@ -197,6 +197,73 @@ class HuyaBatchRunner:
|
|||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
|
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
|
||||||
|
|
||||||
|
def _execute_refresh_goods(
|
||||||
|
self,
|
||||||
|
worker_db: Session,
|
||||||
|
task: HuyaTask,
|
||||||
|
account: HuyaAccount,
|
||||||
|
account_info: dict,
|
||||||
|
config_info: dict,
|
||||||
|
):
|
||||||
|
sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip()
|
||||||
|
if not sid:
|
||||||
|
self._mark_task(worker_db, task, "failed", "请先配置虎牙活动 SID")
|
||||||
|
return
|
||||||
|
|
||||||
|
sid_int = self._to_int(sid)
|
||||||
|
if not sid_int:
|
||||||
|
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
|
||||||
|
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}"))
|
||||||
|
response = client.get_act_prize_list(uid=uid, cookie=cookie, sid=sid_int)
|
||||||
|
if response is None:
|
||||||
|
self._mark_task(worker_db, task, "error", "虎牙商品列表接口无响应")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = response.to_dict()
|
||||||
|
result["sid"] = sid_int
|
||||||
|
if response.status != 200:
|
||||||
|
self._mark_task(
|
||||||
|
worker_db,
|
||||||
|
task,
|
||||||
|
"failed",
|
||||||
|
response.msg or f"虎牙商品列表刷新失败: {response.status}",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
goods = [
|
||||||
|
item for item in result.get("goods", [])
|
||||||
|
if item.get("product_id") and item.get("name")
|
||||||
|
]
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False)
|
||||||
|
for item in goods:
|
||||||
|
worker_db.add(HuyaGoodsSnapshot(
|
||||||
|
product_id=item["product_id"],
|
||||||
|
name=item["name"],
|
||||||
|
price=item["price"],
|
||||||
|
remain_text=item["remain_text"],
|
||||||
|
raw=item,
|
||||||
|
updated_at=now,
|
||||||
|
))
|
||||||
|
|
||||||
|
account.status = "goods_refreshed"
|
||||||
|
account.updated_at = now
|
||||||
|
message = f"已刷新商品 {len(goods)} 个"
|
||||||
|
self._mark_task(worker_db, task, "success", message, {**result, "goods": goods})
|
||||||
|
|
||||||
def _execute_get_bind_qr(
|
def _execute_get_bind_qr(
|
||||||
self,
|
self,
|
||||||
worker_db: Session,
|
worker_db: Session,
|
||||||
@@ -556,7 +623,13 @@ class HuyaBatchRunner:
|
|||||||
name = self._account_name(account_info)
|
name = self._account_name(account_info)
|
||||||
self._push_log("info", f"[{current}/{total}] 开始虎牙任务: {name}")
|
self._push_log("info", f"[{current}/{total}] 开始虎牙任务: {name}")
|
||||||
|
|
||||||
if self.task_type not in {"query_points", "get_bind_qr", "confirm_bind", "query_game_name"}:
|
if self.task_type not in {
|
||||||
|
"query_points",
|
||||||
|
"get_bind_qr",
|
||||||
|
"confirm_bind",
|
||||||
|
"query_game_name",
|
||||||
|
"refresh_goods",
|
||||||
|
}:
|
||||||
self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现")
|
self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现")
|
||||||
self._push_log("warning", f"[{current}] {name} 暂未实现: {self.task_type}")
|
self._push_log("warning", f"[{current}] {name} 暂未实现: {self.task_type}")
|
||||||
return
|
return
|
||||||
@@ -564,6 +637,8 @@ class HuyaBatchRunner:
|
|||||||
try:
|
try:
|
||||||
if self.task_type == "query_points":
|
if self.task_type == "query_points":
|
||||||
self._execute_query_points(worker_db, task, account, account_info, config_info)
|
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 == "get_bind_qr":
|
elif self.task_type == "get_bind_qr":
|
||||||
self._execute_get_bind_qr(worker_db, task, account, account_info, config_info)
|
self._execute_get_bind_qr(worker_db, task, account, account_info, config_info)
|
||||||
elif self.task_type == "confirm_bind":
|
elif self.task_type == "confirm_bind":
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ def create_planned_tasks(
|
|||||||
batch_id = uuid.uuid4().hex[:12]
|
batch_id = uuid.uuid4().hex[:12]
|
||||||
payload = payload or {}
|
payload = payload or {}
|
||||||
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
|
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
|
||||||
|
if task_type == "refresh_goods" and accounts:
|
||||||
|
# 商品列表是全局快照,使用一个可用 CK 刷新即可。
|
||||||
|
accounts = accounts[:1]
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
db.add(HuyaTask(
|
db.add(HuyaTask(
|
||||||
batch_id=batch_id,
|
batch_id=batch_id,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Card, Col, Form, Input, InputNumber, message, Modal, Row, Select, Space, Table, Tag, Tooltip, Typography, theme,
|
Button, Card, Col, Form, Input, InputNumber, message, Modal, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -87,6 +87,34 @@ function resultProfileNick(result: Record<string, unknown> | null | undefined):
|
|||||||
return typeof nick === 'string' ? nick : '';
|
return typeof nick === 'string' ? nick : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goodsRawString(item: HuyaGoodsItem, key: string): string {
|
||||||
|
const value = item.raw?.[key];
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value === 'number') return String(value);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goodsRawNumber(item: HuyaGoodsItem, key: string): number {
|
||||||
|
const value = item.raw?.[key];
|
||||||
|
if (typeof value === 'number') return value;
|
||||||
|
if (typeof value === 'string' && value.trim()) return Number(value) || 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goodsCategoryKey(item: HuyaGoodsItem): string {
|
||||||
|
return goodsRawString(item, 'category_id') || goodsRawString(item, 'category_name') || 'uncategorized';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goodsCategoryLabel(item: HuyaGoodsItem): string {
|
||||||
|
return goodsRawString(item, 'category_name') || goodsRawString(item, 'category_id') || '未分类';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRemainText(value: string): string {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text || text.endsWith('%')) return text;
|
||||||
|
return /^-?\d+(\.\d+)?$/.test(text) ? `${text}%` : text;
|
||||||
|
}
|
||||||
|
|
||||||
function hasMiniQrcode(task: HuyaTaskItem): boolean {
|
function hasMiniQrcode(task: HuyaTaskItem): boolean {
|
||||||
return task.task_type === 'get_bind_qr' && Boolean(resultText(task.result, 'mini_qrcode_image'));
|
return task.task_type === 'get_bind_qr' && Boolean(resultText(task.result, 'mini_qrcode_image'));
|
||||||
}
|
}
|
||||||
@@ -101,6 +129,7 @@ export default function HuyaTasksPage() {
|
|||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
|
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
|
||||||
const [selectedGoodsId, setSelectedGoodsId] = useState<string>('');
|
const [selectedGoodsId, setSelectedGoodsId] = useState<string>('');
|
||||||
|
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
|
||||||
const [rechargeCount, setRechargeCount] = useState(1);
|
const [rechargeCount, setRechargeCount] = useState(1);
|
||||||
const [concurrency, setConcurrency] = useState(() => {
|
const [concurrency, setConcurrency] = useState(() => {
|
||||||
const v = localStorage.getItem('huya_task_concurrency');
|
const v = localStorage.getItem('huya_task_concurrency');
|
||||||
@@ -200,16 +229,58 @@ export default function HuyaTasksPage() {
|
|||||||
return accounts.map((account) => ({ value: account.id, label: accountLabel(account) }));
|
return accounts.map((account) => ({ value: account.id, label: accountLabel(account) }));
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
|
||||||
|
const sortedGoods = useMemo(() => {
|
||||||
|
return [...goods].sort((a, b) => (
|
||||||
|
goodsRawNumber(a, 'category_sort') - goodsRawNumber(b, 'category_sort')
|
||||||
|
|| goodsRawNumber(a, 'raw_order') - goodsRawNumber(b, 'raw_order')
|
||||||
|
|| a.id - b.id
|
||||||
|
));
|
||||||
|
}, [goods]);
|
||||||
|
|
||||||
const goodsOptions = useMemo(() => {
|
const goodsOptions = useMemo(() => {
|
||||||
return goods.map((item) => ({
|
return sortedGoods.map((item) => ({
|
||||||
value: item.product_id,
|
value: item.product_id,
|
||||||
label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`,
|
label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`,
|
||||||
}));
|
}));
|
||||||
}, [goods]);
|
}, [sortedGoods]);
|
||||||
|
|
||||||
const selectedGoods = useMemo(() => {
|
const selectedGoods = useMemo(() => {
|
||||||
return goods.find((item) => item.product_id === selectedGoodsId) || null;
|
return sortedGoods.find((item) => item.product_id === selectedGoodsId) || null;
|
||||||
}, [goods, selectedGoodsId]);
|
}, [sortedGoods, selectedGoodsId]);
|
||||||
|
|
||||||
|
const goodsCategories = useMemo(() => {
|
||||||
|
const map = new Map<string, { key: string; label: string; sort: number; count: number }>();
|
||||||
|
sortedGoods.forEach((item) => {
|
||||||
|
const key = goodsCategoryKey(item);
|
||||||
|
const current = map.get(key);
|
||||||
|
if (current) {
|
||||||
|
current.count += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(key, {
|
||||||
|
key,
|
||||||
|
label: goodsCategoryLabel(item),
|
||||||
|
sort: goodsRawNumber(item, 'category_sort'),
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(map.values()).sort((a, b) => a.sort - b.sort || a.label.localeCompare(b.label));
|
||||||
|
}, [sortedGoods]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (goodsCategories.length === 0) {
|
||||||
|
if (selectedGoodsCategory) setSelectedGoodsCategory('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!goodsCategories.some((item) => item.key === selectedGoodsCategory)) {
|
||||||
|
setSelectedGoodsCategory(goodsCategories[0].key);
|
||||||
|
}
|
||||||
|
}, [goodsCategories, selectedGoodsCategory]);
|
||||||
|
|
||||||
|
const filteredGoods = useMemo(() => {
|
||||||
|
if (!selectedGoodsCategory) return sortedGoods;
|
||||||
|
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
|
||||||
|
}, [sortedGoods, selectedGoodsCategory]);
|
||||||
|
|
||||||
const createPayload = (taskType: string) => {
|
const createPayload = (taskType: string) => {
|
||||||
if (taskType !== 'recharge_points') return {};
|
if (taskType !== 'recharge_points') return {};
|
||||||
@@ -238,13 +309,22 @@ export default function HuyaTasksPage() {
|
|||||||
concurrency,
|
concurrency,
|
||||||
payload: createPayload(taskType),
|
payload: createPayload(taskType),
|
||||||
});
|
});
|
||||||
|
const finishTask = () => {
|
||||||
|
setBatchId(null);
|
||||||
|
setStarting(false);
|
||||||
|
if (taskType === 'refresh_goods') {
|
||||||
|
void loadAll();
|
||||||
|
} else {
|
||||||
|
void loadTasks();
|
||||||
|
}
|
||||||
|
};
|
||||||
setBatchId(result.batch_id);
|
setBatchId(result.batch_id);
|
||||||
message.success(`已创建 ${taskTypes[taskType] || taskType},共 ${result.count} 个账号`);
|
message.success(`已创建 ${taskTypes[taskType] || taskType},共 ${result.count} 个账号`);
|
||||||
await loadTasks();
|
await loadTasks();
|
||||||
connectLogs(`/api/huya/ws/${result.batch_id}`, {
|
connectLogs(`/api/huya/ws/${result.batch_id}`, {
|
||||||
onClose: () => { setBatchId(null); setStarting(false); loadTasks(); },
|
onClose: finishTask,
|
||||||
onResult: () => { setBatchId(null); setStarting(false); loadTasks(); },
|
onResult: finishTask,
|
||||||
onError: () => { setBatchId(null); setStarting(false); loadTasks(); },
|
onError: finishTask,
|
||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(getErrorMessage(e));
|
message.error(getErrorMessage(e));
|
||||||
@@ -315,6 +395,11 @@ export default function HuyaTasksPage() {
|
|||||||
if (value?.is_bound === false) return <Tag>未绑定</Tag>;
|
if (value?.is_bound === false) return <Tag>未绑定</Tag>;
|
||||||
return <Text type="secondary">-</Text>;
|
return <Text type="secondary">-</Text>;
|
||||||
}
|
}
|
||||||
|
if (record.task_type === 'refresh_goods') {
|
||||||
|
const goodsCount = value?.goods_count;
|
||||||
|
if (typeof goodsCount === 'number') return <Tag color="green">商品 {goodsCount} 个</Tag>;
|
||||||
|
return <Text type="secondary">-</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
const availableScore = value?.available_score;
|
const availableScore = value?.available_score;
|
||||||
if (typeof availableScore === 'number') {
|
if (typeof availableScore === 'number') {
|
||||||
@@ -365,6 +450,14 @@ export default function HuyaTasksPage() {
|
|||||||
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
|
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
|
||||||
{ title: '商品ID', dataIndex: 'product_id', width: 120, ellipsis: true },
|
{ title: '商品ID', dataIndex: 'product_id', width: 120, ellipsis: true },
|
||||||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
width: 110,
|
||||||
|
render: (_: unknown, record) => {
|
||||||
|
const label = goodsCategoryLabel(record);
|
||||||
|
return label ? <Tag>{label}</Tag> : <Text type="secondary">-</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
@@ -376,7 +469,7 @@ export default function HuyaTasksPage() {
|
|||||||
title: '库存',
|
title: '库存',
|
||||||
dataIndex: 'remain_text',
|
dataIndex: 'remain_text',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (value: string) => value || <Text type="secondary">-</Text>,
|
render: (value: string) => formatRemainText(value) || <Text type="secondary">-</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
@@ -398,7 +491,7 @@ export default function HuyaTasksPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', paddingRight: 2 }}>
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', paddingRight: 2 }}>
|
||||||
<Row gutter={12}>
|
<Row gutter={12}>
|
||||||
<Col xs={24} xl={10}>
|
<Col xs={24} xl={10}>
|
||||||
<Card
|
<Card
|
||||||
@@ -468,14 +561,27 @@ export default function HuyaTasksPage() {
|
|||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
{goodsCategories.length > 0 && (
|
||||||
|
<Tabs
|
||||||
|
size="small"
|
||||||
|
activeKey={selectedGoodsCategory}
|
||||||
|
onChange={setSelectedGoodsCategory}
|
||||||
|
items={
|
||||||
|
goodsCategories.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
label: `${item.label} (${item.count})`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
columns={goodsColumns}
|
columns={goodsColumns}
|
||||||
dataSource={goods}
|
dataSource={filteredGoods}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 620, y: 220 }}
|
scroll={{ x: 760, y: 220 }}
|
||||||
locale={{ emptyText: '暂无商品快照,后续接入刷新商品列表后写入' }}
|
locale={{ emptyText: '暂无商品快照,请先刷新商品列表' }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -534,7 +640,7 @@ export default function HuyaTasksPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
{QUICK_ACTIONS.map((item) => (
|
{QUICK_ACTIONS.map((item) => (
|
||||||
<Tooltip key={item.key} title={item.key === 'refresh_goods' ? '当前阶段创建计划任务,真实拉取逻辑后续接入' : undefined}>
|
<Tooltip key={item.key} title={item.key === 'refresh_goods' ? '使用一个选中的 CK 刷新当前 SID 商品快照' : undefined}>
|
||||||
<Button
|
<Button
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -547,7 +653,7 @@ export default function HuyaTasksPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
当前已接入查询积分和获取绑定二维码,其余任务会先保留计划记录。
|
当前已接入查询积分、获取绑定二维码、确认绑定、查询游戏名和刷新商品列表。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user