feat(huya): 精英宝典账号支持转入外部单协作
This commit is contained in:
@@ -50,6 +50,7 @@ from .workbench import (
|
||||
stop_batch,
|
||||
task_types,
|
||||
tasks_summary,
|
||||
transfer_workbench_to_collab,
|
||||
update_config,
|
||||
update_huya_workbench_accounts,
|
||||
ws_huya_logs,
|
||||
@@ -105,6 +106,7 @@ __all__ = [
|
||||
"task_types",
|
||||
"tasks_summary",
|
||||
"test_sms_url_endpoint",
|
||||
"transfer_workbench_to_collab",
|
||||
"update_config",
|
||||
"update_huya_workbench_accounts",
|
||||
"ws_huya_logs",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from ...schemas import HuyaWorkbenchTransferToCollab
|
||||
from ._common import *
|
||||
|
||||
router = APIRouter()
|
||||
@@ -114,6 +115,51 @@ def update_huya_workbench_accounts(
|
||||
return {"account_ids": ids, "success": True}
|
||||
|
||||
|
||||
@router.post("/workbench-accounts/transfer-to-collab")
|
||||
def transfer_workbench_to_collab(
|
||||
req: HuyaWorkbenchTransferToCollab,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:task")),
|
||||
):
|
||||
"""把单个精英宝典账号移出工作台,并返回其网页 CK 供外部单新建表单使用。"""
|
||||
_require_huya_perm(current, "huya:collab:edit")
|
||||
_require_huya_task_account_access(db, current, [req.account_id])
|
||||
|
||||
membership = (
|
||||
db.query(HuyaWorkbenchAccount)
|
||||
.filter(
|
||||
HuyaWorkbenchAccount.user_id == current.id,
|
||||
HuyaWorkbenchAccount.handbook_scope == "elite",
|
||||
HuyaWorkbenchAccount.account_id == req.account_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if membership is None:
|
||||
raise HTTPException(status_code=404, detail="该账号不在精英宝典工作台")
|
||||
|
||||
account = db.query(HuyaAccount).filter(HuyaAccount.id == req.account_id).first()
|
||||
ck_text = str(account.cookie or "").strip() if account else ""
|
||||
if not ck_text:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="该账号没有可用 Cookie,无法转入外部单协作"
|
||||
)
|
||||
|
||||
db.delete(membership)
|
||||
record_audit(
|
||||
db,
|
||||
current,
|
||||
action="huya:workbench:transfer_collab",
|
||||
target=f"huya_account:{account.id}",
|
||||
detail={"account_id": account.id, "uid": account.uid},
|
||||
)
|
||||
db.commit()
|
||||
return {
|
||||
"account_id": account.id,
|
||||
"ck_text": ck_text,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/goods", response_model=list[HuyaGoodsOut])
|
||||
def list_goods(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -1199,6 +1199,12 @@ class HuyaWorkbenchCookieImport(BaseModel):
|
||||
handbook_scope: str = Field("elite", pattern="^elite$|^browser_exchange$")
|
||||
|
||||
|
||||
class HuyaWorkbenchTransferToCollab(BaseModel):
|
||||
"""精英宝典工作台把单个账号转入外部单协作。"""
|
||||
|
||||
account_id: int = Field(..., gt=0)
|
||||
|
||||
|
||||
class HuyaBrowserExchangeRequest(BaseModel):
|
||||
"""浏览器兑换的目标页选择(常量见 services/huya_exchange_targets.py)。"""
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ import type {
|
||||
HuyaExchangeTargetKey,
|
||||
HuyaExternalCkImportRequest,
|
||||
HuyaExternalCkImportResult,
|
||||
HuyaWorkbenchTransferToCollabRequest,
|
||||
HuyaWorkbenchTransferToCollabResult,
|
||||
HuyaWorkbenchScope,
|
||||
MessageCountResponse,
|
||||
MessageDeletedResponse,
|
||||
@@ -179,6 +181,12 @@ export const huyaApi = {
|
||||
/** 工作台导入外部 CK: 入库并把账号加入自己的工作台 (text 每行一条 CK) */
|
||||
importWorkbenchCookies: (data: HuyaExternalCkImportRequest) =>
|
||||
api.post<HuyaExternalCkImportResult, HuyaExternalCkImportResult>('/huya/workbench-accounts/import-cookies', data),
|
||||
/** 精英宝典账号转入外部单协作:移出工作台并取回该账号网页 CK */
|
||||
transferWorkbenchToCollab: (accountId: number) =>
|
||||
api.post<HuyaWorkbenchTransferToCollabResult, HuyaWorkbenchTransferToCollabResult>(
|
||||
'/huya/workbench-accounts/transfer-to-collab',
|
||||
{ account_id: accountId } satisfies HuyaWorkbenchTransferToCollabRequest,
|
||||
),
|
||||
listWorkbenchAccounts: (scope: HuyaWorkbenchScope = 'elite') =>
|
||||
api.get<{ account_ids: number[]; configured: boolean }, { account_ids: number[]; configured: boolean }>(
|
||||
'/huya/workbench-accounts', { params: { handbook_scope: scope } },
|
||||
|
||||
@@ -1274,6 +1274,17 @@ export interface HuyaExternalCkImportResult extends MessageCountResponse {
|
||||
account_ids: number[];
|
||||
}
|
||||
|
||||
/** 精英宝典工作台「转入外部单协作」:请求体 */
|
||||
export interface HuyaWorkbenchTransferToCollabRequest {
|
||||
account_id: number;
|
||||
}
|
||||
|
||||
/** 精英宝典工作台「转入外部单协作」:结果里的 ck_text 用于外部单新建表单预填 */
|
||||
export interface HuyaWorkbenchTransferToCollabResult extends MessageResponse {
|
||||
account_id: number;
|
||||
ck_text: string;
|
||||
}
|
||||
|
||||
// ==================== Proxy ====================
|
||||
|
||||
export interface ProxyConfig {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. **内联编辑必须带 `expected_version`**:别人先改过会 409,绝不静默覆盖(Excel 最痛的点)。
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Alert, App, Badge, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
Empty, Form, Image, Input, InputNumber, Modal, Popconfirm, Popover, Progress, Radio, Row, Select, Space, Statistic,
|
||||
@@ -499,6 +500,8 @@ function ImageSlotEditor({
|
||||
export default function HuyaCollabPage() {
|
||||
const { modal } = App.useApp();
|
||||
const { can, user } = usePermissions();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const canEdit = can('huya:collab:edit');
|
||||
const canManage = can('huya:collab:manage');
|
||||
// 单行复制与批量导出共用这个权限点(默认运营和客服都有)
|
||||
@@ -819,6 +822,23 @@ export default function HuyaCollabPage() {
|
||||
[runCkPreview],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const state = location.state as { transferCk?: string } | null;
|
||||
const transferCk = state?.transferCk?.trim();
|
||||
if (!transferCk) return;
|
||||
|
||||
setCreateOpen(true);
|
||||
setCkPreview(null);
|
||||
setPreviewing(false);
|
||||
setGiftHint('');
|
||||
setDraftRestoredAt(null);
|
||||
previewSeq.current += 1;
|
||||
createForm.resetFields();
|
||||
createForm.setFieldsValue({ ck_text: transferCk });
|
||||
scheduleCkPreview(transferCk, true);
|
||||
navigate(location.pathname, { replace: true, state: null });
|
||||
}, [createForm, location.pathname, location.state, navigate, scheduleCkPreview]);
|
||||
|
||||
const isArchiveView = filters.archive_scope === 'archived';
|
||||
const selectedRows = useMemo(
|
||||
() => rows.filter((row) => selectedIds.includes(row.id)),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, GiftOutlined, ImportOutlined, QrcodeOutlined,
|
||||
ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined, ZoomInOutlined,
|
||||
ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined, SwapOutlined, ZoomInOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAccountItem, type HuyaConfig, type HuyaGoodsItem, type HuyaRechargeGoodsItem, type HuyaTaskItem } from '../api/modules';
|
||||
import HuyaExternalCkImportModal from '../components/HuyaExternalCkImportModal';
|
||||
@@ -169,9 +170,11 @@ function taskQrUrl(task: HuyaTaskItem | undefined): string {
|
||||
}
|
||||
|
||||
export default function HuyaElitePage() {
|
||||
const navigate = useNavigate();
|
||||
const { can } = usePermissions();
|
||||
const canConfig = can('huya:config');
|
||||
const canImport = can('huya:import') || can('huya:elite:import_ck');
|
||||
const canTransferCollab = can('huya:collab:edit');
|
||||
const [pool, setPool] = useState<HuyaAccountItem[]>([]);
|
||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
@@ -188,6 +191,7 @@ export default function HuyaElitePage() {
|
||||
const [pageSize, setPageSize] = useState(savedPageSize);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [transferring, setTransferring] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [concurrency, setConcurrency] = useState(3);
|
||||
const [refreshInterval, setRefreshInterval] = useState(savedInterval);
|
||||
@@ -272,6 +276,31 @@ export default function HuyaElitePage() {
|
||||
setSelectedIds((prev) => prev.filter((id) => saved.account_ids.includes(id)));
|
||||
};
|
||||
|
||||
const transferToCollab = async () => {
|
||||
if (selectedIds.length !== 1) {
|
||||
message.warning('一次只能选择一个账号转入外部单协作');
|
||||
return;
|
||||
}
|
||||
const accountId = selectedIds[0];
|
||||
const account = accounts.find((item) => item.id === accountId);
|
||||
if (account && account.has_cookie === false) {
|
||||
message.warning('该账号没有可用 Cookie,不能转入外部单协作');
|
||||
return;
|
||||
}
|
||||
setTransferring(true);
|
||||
try {
|
||||
const result = await huyaApi.transferWorkbenchToCollab(accountId);
|
||||
message.success('已从精英宝典工作台移除,正在打开外部单协作');
|
||||
navigate('/huya/collab', {
|
||||
replace: true,
|
||||
state: { transferCk: result.ck_text },
|
||||
});
|
||||
} catch (error) {
|
||||
setTransferring(false);
|
||||
message.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
// 开通商品分组:精英宝典(限购主商品,单独一块)与其他商品(币/徽章/令…,带数量那一块)
|
||||
const rechargeGoodsSorted = [...rechargeGoods].sort(byRechargeOrder);
|
||||
const eliteRechargeGoods = rechargeGoodsSorted.filter(isEliteHandbookGood);
|
||||
@@ -478,7 +507,7 @@ export default function HuyaElitePage() {
|
||||
</Space>
|
||||
);
|
||||
|
||||
const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名/手机号" value={search} onChange={(event) => { setSearch(event.target.value); setCurrentPage(1); }} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>{canImport && <Button size="small" icon={<ImportOutlined />} onClick={() => setCkImportOpen(true)}>导入外部 CK</Button>}{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}>移出选中</Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} tableLayout="fixed" rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)), preserveSelectedRowKeys: true }} columns={accountColumns} dataSource={filteredAccounts} locale={{ emptyText: '工作台暂无账号,点击右上角「导入账号」添加' }} pagination={{ current: currentPage, pageSize, total: filteredAccounts.length, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); localStorage.setItem(PAGE_SIZE_KEY, String(size)); setCurrentPage(1); } } }} scroll={{ y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (!task?.result) return; if (task.task_type === 'get_bind_qr') void openQrTask(task); else if (task.task_type === 'create_recharge_order') openPayTasks(task.batch_id); }, onContextMenu: (event) => handleRowContextMenu(row, event), style: { cursor: 'context-menu' }, title: '右键打开账号动作' })} /></div></>;
|
||||
const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名/手机号" value={search} onChange={(event) => { setSearch(event.target.value); setCurrentPage(1); }} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>{canImport && <Button size="small" icon={<ImportOutlined />} onClick={() => setCkImportOpen(true)}>导入外部 CK</Button>}{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}>移出选中</Button>}{canTransferCollab && selectedIds.length === 1 && <Button size="small" icon={<SwapOutlined />} loading={transferring} onClick={() => void transferToCollab()}>转入外部单协作</Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} tableLayout="fixed" rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)), preserveSelectedRowKeys: true }} columns={accountColumns} dataSource={filteredAccounts} locale={{ emptyText: '工作台暂无账号,点击右上角「导入账号」添加' }} pagination={{ current: currentPage, pageSize, total: filteredAccounts.length, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); localStorage.setItem(PAGE_SIZE_KEY, String(size)); setCurrentPage(1); } } }} scroll={{ y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (!task?.result) return; if (task.task_type === 'get_bind_qr') void openQrTask(task); else if (task.task_type === 'create_recharge_order') openPayTasks(task.batch_id); }, onContextMenu: (event) => handleRowContextMenu(row, event), style: { cursor: 'context-menu' }, title: '右键打开账号动作' })} /></div></>;
|
||||
|
||||
const configField = (key: Exclude<keyof HuyaConfig, 'updated_at'>, label: string) => configDraft && <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}><Text>{label}</Text><Input value={configDraft[key] || ''} onChange={(event) => setConfigDraft({ ...configDraft, [key]: event.target.value })} style={{ width: 300 }} /></Space>;
|
||||
const contextMenuStyle = contextMenu ? {
|
||||
|
||||
Reference in New Issue
Block a user