feat(douyu): 支持和平小店商品兑换
This commit is contained in:
@@ -455,7 +455,10 @@ class DouyuBatchRunner:
|
||||
row.price = self._to_int(raw.get("price") or raw.get("iPrice"))
|
||||
row.org_price = self._to_int(raw.get("org_price") or raw.get("iOrgPrice"))
|
||||
row.category = str(raw.get("category") or raw.get("iCategoryId") or "")
|
||||
row.goods_left = self._to_int(raw.get("goods_left") or raw.get("iGoodsLeft"))
|
||||
goods_left = raw.get("goods_left")
|
||||
if goods_left is None:
|
||||
goods_left = raw.get("iGoodsLeft")
|
||||
row.goods_left = self._to_int(goods_left)
|
||||
row.raw = raw
|
||||
row.updated_at = now
|
||||
db.commit()
|
||||
@@ -1414,6 +1417,101 @@ class DouyuBatchRunner:
|
||||
{"fragments": fragments, "role": role, "area_id": int(areaid)},
|
||||
)
|
||||
|
||||
def _execute_exchange_xpd_goods(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""兑换和平小店商品,使用本次签发的道聚城短时授权。"""
|
||||
payload = self._task_payload(task)
|
||||
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
|
||||
if not commodity_id:
|
||||
self._mark_task(db, task, "failed", "请选择小店商品")
|
||||
return
|
||||
try:
|
||||
pay_type = int(payload.get("pay_type") or 1)
|
||||
except (TypeError, ValueError):
|
||||
self._mark_task(db, task, "failed", "兑换货币参数无效")
|
||||
return
|
||||
if pay_type not in (1, 5):
|
||||
self._mark_task(db, task, "failed", "小店兑换仅支持点券或扭蛋碎片")
|
||||
return
|
||||
|
||||
goods = (
|
||||
db.query(DouyuXpdGoodsSnapshot)
|
||||
.filter(DouyuXpdGoodsSnapshot.commodity_id == commodity_id)
|
||||
.first()
|
||||
)
|
||||
if not goods:
|
||||
self._mark_task(db, task, "failed", "未找到小店商品快照,请先刷新商品列表")
|
||||
return
|
||||
goods_snapshot = goods.raw if isinstance(goods.raw, dict) else {}
|
||||
goods_raw = goods_snapshot.get("raw") if isinstance(goods_snapshot.get("raw"), dict) else goods_snapshot
|
||||
price_key = "iPrice" if pay_type == 1 else "iJb2Price"
|
||||
price = self._to_int(goods_raw.get(price_key))
|
||||
if price is None:
|
||||
price = goods.price if pay_type == 1 else None
|
||||
if price is None or price <= 0:
|
||||
currency = "点券" if pay_type == 1 else "扭蛋碎片"
|
||||
self._mark_task(db, task, "failed", f"该商品不支持使用{currency}兑换")
|
||||
return
|
||||
if goods.goods_left is not None and goods.goods_left <= 0:
|
||||
self._mark_task(db, task, "failed", "该商品库存不足,请刷新商品列表后重试")
|
||||
return
|
||||
|
||||
client = self._client(cookie)
|
||||
embed = client.xpd_embed_query(
|
||||
act_alias=str(config["xpd_act_alias"]),
|
||||
rid=str(config["xpd_rid"]),
|
||||
)
|
||||
role: dict = {}
|
||||
try:
|
||||
role = client.xpd_get_role(
|
||||
embed_query=embed["query"],
|
||||
act_id=str(config["xpd_act_id"]),
|
||||
rid=str(config["xpd_rid"]),
|
||||
)
|
||||
if role.get("role_id"):
|
||||
self._apply_xpd_role_to_account(account, role, self._xpd_area_id(role, account))
|
||||
except DouyuActivityError as exc:
|
||||
self._push_log("warning", f"小店兑换前刷新角色失败,使用已保存角色: {exc}")
|
||||
if not role.get("role_id") and not account.xpd_role_id:
|
||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定")
|
||||
return
|
||||
|
||||
result = client.xpd_exchange_goods(
|
||||
embed_query=embed["query"],
|
||||
act_id=str(config["xpd_act_id"]),
|
||||
rid=str(config["xpd_rid"]),
|
||||
commodity_id=commodity_id,
|
||||
price=price,
|
||||
picture=str(goods_raw.get("sGoodsPic") or ""),
|
||||
pay_type=pay_type,
|
||||
action_id=str(goods_raw.get("iActionId") or ""),
|
||||
)
|
||||
if pay_type == 1 and result.get("new_balance") is not None:
|
||||
account.xpd_balance = result["new_balance"]
|
||||
if pay_type == 5 and result.get("new_balance") is not None:
|
||||
account.xpd_fragments = result["new_balance"]
|
||||
account.xpd_bind_status = "xpd_goods_exchanged"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
|
||||
currency = "点券" if pay_type == 1 else "扭蛋碎片"
|
||||
display_role = role.get("role_name") or account.xpd_game_name or ""
|
||||
channel = "微信" if (role.get("type") == "wx" or account.xpd_area_id == 1) else "手Q"
|
||||
result.update({
|
||||
"goods": goods_raw,
|
||||
"game_name": display_role,
|
||||
"game_channel": channel,
|
||||
"account_name": account.nickname or account.username or account.uid or f"#{account.id}",
|
||||
"currency": currency,
|
||||
})
|
||||
self._mark_task(db, task, "success", f"兑换小店商品成功: {goods.name or commodity_id}({price}{currency})", result)
|
||||
|
||||
def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
||||
client = self._client(cookie)
|
||||
qr_act_alias = self._bind_qr_act_alias(config)
|
||||
@@ -2710,6 +2808,7 @@ class DouyuBatchRunner:
|
||||
"refresh_xpd_goods": self._execute_refresh_xpd_goods,
|
||||
"query_xpd_balance": self._execute_query_xpd_balance,
|
||||
"query_xpd_fragments": self._execute_query_xpd_fragments,
|
||||
"exchange_xpd_goods": self._execute_exchange_xpd_goods,
|
||||
}.get(task.task_type)
|
||||
if handler is None:
|
||||
self._mark_task(worker_db, task, "failed", "不支持的任务类型")
|
||||
|
||||
@@ -44,6 +44,7 @@ SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
|
||||
"refresh_xpd_goods": "刷新小店商品列表",
|
||||
"query_xpd_balance": "查询小店点券余额",
|
||||
"query_xpd_fragments": "查询小店扭蛋碎片",
|
||||
"exchange_xpd_goods": "兑换小店商品",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ const PEACE_QUICK_ACTIONS = [
|
||||
{ key: 'query_xpd_balance', icon: <SearchOutlined /> },
|
||||
{ key: 'query_xpd_fragments', icon: <SearchOutlined /> },
|
||||
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
|
||||
{ key: 'exchange_xpd_goods', icon: <ShoppingOutlined /> },
|
||||
];
|
||||
|
||||
const ELITE_TASK_TYPES = new Set([
|
||||
@@ -133,6 +134,7 @@ const PEACE_TASK_TYPES = new Set([
|
||||
'refresh_xpd_goods',
|
||||
'query_xpd_balance',
|
||||
'query_xpd_fragments',
|
||||
'exchange_xpd_goods',
|
||||
]);
|
||||
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
||||
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
||||
@@ -230,9 +232,34 @@ function goodsLabel(item: DouyuGoodsItem): string {
|
||||
|
||||
function xpdGoodsLabel(item: DouyuGoodsItem): string {
|
||||
const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
|
||||
const price = xpd.price != null ? ` / ${xpd.price}点券` : '';
|
||||
const left = xpd.goods_left != null ? ` / 剩${xpd.goods_left}` : '';
|
||||
return `${item.name || item.commodity_id}${price}${left}`;
|
||||
const raw = item.raw?.raw as Record<string, unknown> | undefined;
|
||||
const pointPrice = Number(xpd.price ?? raw?.iPrice);
|
||||
const fragmentPrice = Number(raw?.iJb2Price);
|
||||
const prices = [
|
||||
Number.isFinite(pointPrice) && pointPrice > 0 ? `${pointPrice}点券` : '',
|
||||
Number.isFinite(fragmentPrice) && fragmentPrice > 0 ? `${fragmentPrice}碎片` : '',
|
||||
].filter(Boolean);
|
||||
const stock = xpd.goods_left;
|
||||
const stockText = stock != null && Number.isFinite(Number(stock))
|
||||
? `库存${stock}`
|
||||
: '库存未知';
|
||||
return `${item.name || item.commodity_id}${prices.length ? ` / ${prices.join(' 或 ')}` : ' / 暂不可兑换'} / ${stockText}`;
|
||||
}
|
||||
|
||||
function xpdGoodsPaymentOptions(item: DouyuGoodsItem | undefined): Array<{ value: '1' | '5'; label: string }> {
|
||||
if (!item) return [];
|
||||
const xpd = item as DouyuGoodsItem & { price?: number | null };
|
||||
const raw = item.raw?.raw as Record<string, unknown> | undefined;
|
||||
const pointPrice = Number(xpd.price ?? raw?.iPrice);
|
||||
const fragmentPrice = Number(raw?.iJb2Price);
|
||||
return [
|
||||
Number.isFinite(pointPrice) && pointPrice > 0
|
||||
? { value: '1' as const, label: `使用点券兑换(${pointPrice}点券)` }
|
||||
: null,
|
||||
Number.isFinite(fragmentPrice) && fragmentPrice > 0
|
||||
? { value: '5' as const, label: `使用扭蛋碎片兑换(${fragmentPrice}碎片)` }
|
||||
: null,
|
||||
].filter((option): option is { value: '1' | '5'; label: string } => option !== null);
|
||||
}
|
||||
|
||||
function formatWaitSeconds(seconds: number | null | undefined): string {
|
||||
@@ -336,6 +363,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
|
||||
const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
|
||||
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
||||
const [xpdPayType, setXpdPayType] = useState<'1' | '5'>('1');
|
||||
const [accountSearch, setAccountSearch] = useState('');
|
||||
const [layoutMode, setLayoutMode] = useState<'stack' | 'split'>(() => {
|
||||
const v = localStorage.getItem(DOUYU_LAYOUT_MODE_STORAGE_KEY);
|
||||
@@ -835,6 +863,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
|
||||
// 多批次并发:不再用全局 busy 标志阻塞 UI,刷新频率直接用各子条件判断
|
||||
|
||||
const selectedXpdGoods = useMemo(
|
||||
() => goods.find((item) => item.commodity_id === selectedGoodsId),
|
||||
[goods, selectedGoodsId],
|
||||
);
|
||||
const xpdPaymentOptions = useMemo(
|
||||
() => xpdGoodsPaymentOptions(selectedXpdGoods),
|
||||
[selectedXpdGoods],
|
||||
);
|
||||
const selectedXpdGoodsSoldOut = (() => {
|
||||
const stock = (selectedXpdGoods as (DouyuGoodsItem & { goods_left?: number | null }) | undefined)?.goods_left;
|
||||
return stock != null && Number.isFinite(Number(stock)) && Number(stock) <= 0;
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (xpdPaymentOptions.length === 0) return;
|
||||
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
|
||||
setXpdPayType(xpdPaymentOptions[0].value);
|
||||
}
|
||||
}, [xpdPayType, xpdPaymentOptions]);
|
||||
|
||||
const startTask = async (
|
||||
taskType: string,
|
||||
accountIds?: number[],
|
||||
@@ -846,12 +894,25 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
return;
|
||||
}
|
||||
// 多批次并发:不再拦截,允许在已有批次运行时继续创建新批次
|
||||
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
|
||||
if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType) && !selectedGoodsId) {
|
||||
message.warning('请先选择兑换商品');
|
||||
return;
|
||||
}
|
||||
if (taskType === 'exchange_xpd_goods') {
|
||||
if (selectedXpdGoodsSoldOut) {
|
||||
message.warning('该商品库存不足,请刷新商品列表后重试');
|
||||
return;
|
||||
}
|
||||
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
|
||||
message.warning('该商品不支持当前兑换方式');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const payload: Record<string, unknown> = { ...extraPayload };
|
||||
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) payload.commodity_id = selectedGoodsId;
|
||||
if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType)) {
|
||||
payload.commodity_id = selectedGoodsId;
|
||||
}
|
||||
if (taskType === 'exchange_xpd_goods') payload.pay_type = xpdPayType;
|
||||
if (taskType === 'create_gold_qr') payload.amount = goldAmount;
|
||||
if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
|
||||
payload.gift_count = giftCount;
|
||||
@@ -1451,6 +1512,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
|
||||
const actionDisabled = (taskType: string) => {
|
||||
if (selectedIds.length === 0) return true;
|
||||
if (taskType === 'exchange_xpd_goods') {
|
||||
return !selectedGoodsId
|
||||
|| selectedXpdGoodsSoldOut
|
||||
|| !xpdPaymentOptions.some((option) => option.value === xpdPayType);
|
||||
}
|
||||
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
|
||||
// 选中账号中有正在执行的任务时禁用,避免同账号并发不同类型任务(并发写状态字段)
|
||||
return selectedHasRunning;
|
||||
@@ -1545,7 +1611,19 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
optionFilterProp="label"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{renderActionButton('refresh_xpd_goods')}
|
||||
<Select
|
||||
size="small"
|
||||
value={xpdPaymentOptions.some((option) => option.value === xpdPayType) ? xpdPayType : undefined}
|
||||
onChange={setXpdPayType}
|
||||
options={xpdPaymentOptions}
|
||||
placeholder={selectedGoodsId ? '该商品暂无可用兑换方式' : '先选择商品'}
|
||||
disabled={!selectedGoodsId || xpdPaymentOptions.length === 0}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<div style={actionGridStyle}>
|
||||
{renderActionButton('refresh_xpd_goods')}
|
||||
{renderActionButton('exchange_xpd_goods', 'primary')}
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DouyuTaskItem } from '../api/types';
|
||||
|
||||
const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods']);
|
||||
const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods']);
|
||||
const CARD_W = 320;
|
||||
const CARD_H = 470;
|
||||
const FONT_FAMILY = '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif';
|
||||
@@ -28,9 +28,10 @@ function goodsOf(task: DouyuTaskItem): {
|
||||
const goods = (task.result?.goods ?? null) as Record<string, unknown> | null;
|
||||
const name = rawString(goods, 'commodityName')
|
||||
|| rawString(goods, 'name')
|
||||
|| rawString(goods, 'sGoodsName')
|
||||
|| task.message.replace(/^兑换[^::]*[::]?\s*/, '').trim()
|
||||
|| rawString(task.result, 'commodity_id');
|
||||
const pic = rawString(goods, 'webPic') || rawString(goods, 'pic');
|
||||
const pic = rawString(goods, 'webPic') || rawString(goods, 'pic') || rawString(goods, 'sGoodsPic');
|
||||
const roleName = rawString(task.result, 'game_name');
|
||||
const channel = rawString(task.result, 'game_channel');
|
||||
const accountName = rawString(task.result, 'account_name')
|
||||
|
||||
Reference in New Issue
Block a user