feat(douyu): 精英宝典工作台支持 CK 检测与失效重登

This commit is contained in:
yml2213
2026-10-23 10:21:15 +08:00
parent d81f0524a8
commit 84d7c8e0a4
7 changed files with 274 additions and 5 deletions
+11 -1
View File
@@ -19,8 +19,18 @@ export const douyuApi = {
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
listAccountsPaged: (params: PageParams & { ids?: string; tag?: string }) =>
api.get<PaginatedResponse<DouyuTaskAccountItem>, PaginatedResponse<DouyuTaskAccountItem>>('/douyu/accounts', { params }),
listAccountIds: (params?: { search?: string; tag?: string; account_names?: string }) =>
listAccountIds: (params: { search?: string; tag?: string; account_names?: string } = {}) =>
api.get<{ account_ids: number[]; total: number }, { account_ids: number[]; total: number }>('/douyu/accounts/ids', { params }),
checkAccountsCk: (ids: number[]) =>
api.post<
{ results: Array<{ id: number; valid: boolean; checked_at: string }>; success: boolean },
{ results: Array<{ id: number; valid: boolean; checked_at: string }>; success: boolean }
>('/douyu/accounts/ck-check', null, { params: { ids: ids.join(',') } }),
reloginInvalidAccounts: (ids: number[]) =>
api.post<
{ batch_id: string; count: number; skipped: number; success: boolean },
{ batch_id: string; count: number; skipped: number; success: boolean }
>('/douyu/accounts/relogin-invalid', { ids }),
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
getRechargeChannel: () => api.get<DouyuRechargeChannel, DouyuRechargeChannel>('/douyu/recharge-channel'),
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
+2
View File
@@ -613,6 +613,8 @@ export interface DouyuTaskAccountItem {
uid: string;
nickname: string;
tag: string;
ck_check_status: string;
ck_checked_at: string | null;
points: number | null;
game_name: string;
game_channel: string;
@@ -5,7 +5,7 @@ import {
import type { TableProps } from 'antd';
import {
CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, CreditCardOutlined, ExclamationCircleOutlined, FieldTimeOutlined, FilterOutlined, GiftOutlined,
ImportOutlined, QrcodeOutlined, ReloadOutlined,
ImportOutlined, LoadingOutlined, QrcodeOutlined, ReloadOutlined,
SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
} from '@ant-design/icons';
import {
@@ -87,7 +87,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const isEsportsHandbook = handbook === 'esports';
const isEliteHandbook = handbook === 'elite';
const { title: handbookTitle, description: handbookDescription, taskTypes: activeTaskTypes, quickActions } = HANDBOOK_META[handbook];
const { can } = usePermissions();
const { can, user } = usePermissions();
const isAdmin = user?.role === 'super_admin';
const { token } = theme.useToken();
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
// 工作台账号归属由服务端按用户保存;localStorage 仅用于首次升级迁移旧浏览器数据。
@@ -120,6 +121,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
});
const [accountPage, setAccountPage] = useState(1);
const [accountTotal, setAccountTotal] = useState(0);
const [ckCheckingIds, setCkCheckingIds] = useState<Set<number>>(new Set());
const tasksLoadingRef = useRef(false);
const [importOpen, setImportOpen] = useState(false);
@@ -814,6 +816,39 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return saved.account_ids;
};
const checkWorkbenchCk = async (ids: number[]) => {
if (ids.length === 0) return;
setCkCheckingIds((previous) => new Set([...previous, ...ids]));
message.info(`正在检测 ${ids.length} 个账号的 CK...`);
try {
const result = await douyuApi.checkAccountsCk(ids);
const byId = new Map(result.results.map((item) => [item.id, item]));
setAccounts((previous) => previous.map((account) => {
const checked = byId.get(account.id);
if (!checked) return account;
return {
...account,
ck_check_status: checked.valid ? 'valid' : 'invalid',
ck_checked_at: checked.checked_at,
};
}));
const invalid = result.results.filter((item) => !item.valid).length;
if (invalid > 0) {
message.warning(`CK 检测完成:${invalid} 个账号 CK 失效`);
} else {
message.success(`CK 检测完成:${result.results.length} 个账号全部有效`);
}
} catch (error) {
message.error(`CK 检测失败:${getErrorMessage(error)}`);
} finally {
setCkCheckingIds((previous) => {
const next = new Set(previous);
ids.forEach((id) => next.delete(id));
return next;
});
}
};
const importAccounts = async (ids: number[]) => {
const toImport = importPool.filter((a) => ids.includes(a.id));
const nextIds = [...new Set([...workbenchIds, ...toImport.map((a) => a.id)])];
@@ -830,6 +865,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
setImportSelectedIds([]);
closeImportModal();
message.success(`已导入 ${toImport.length} 个账号`);
void checkWorkbenchCk(toImport.map((a) => a.id));
};
const closeImportModal = () => {
@@ -873,6 +909,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
await saveWorkbenchIds([...new Set([...workbenchIds, ...imported])]);
closeImportModal();
message.success(`已一键导入 ${imported.length} 个账号`);
void checkWorkbenchCk(imported);
} catch (error) {
message.error(getErrorMessage(error));
} finally {
@@ -893,6 +930,30 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
message.success(`已移出 ${sel.size} 个账号`);
};
const reloginInvalidSelected = () => {
if (selectedIds.length === 0) {
message.warning('请先勾选账号');
return;
}
Modal.confirm({
title: `重登选中的 ${selectedIds.length} 个账号中的失效 CK?`,
content: '只会处理 CK 已失效的账号;新登录成功后才替换旧 CK,失败的账号会保留原 CK。',
okText: '开始重登',
cancelText: '取消',
onOk: async () => {
try {
const result = await douyuApi.reloginInvalidAccounts(selectedIds);
const skipped = result.skipped ? `,${result.skipped} 个因缺少登录凭据跳过` : '';
message.success(`已开始重登 ${result.count} 个失效账号${skipped}`);
setSelectedIds([]);
window.setTimeout(() => void loadData(), 2000);
} catch (error) {
message.error(getErrorMessage(error));
}
},
});
};
// ---- Context menu ----
const [contextMenu, setContextMenu] = useState<{
accountIds: number[]; x: number; y: number;
@@ -1142,6 +1203,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</Space>
),
},
{
title: 'CK状态', width: 100, align: 'center',
render: (_, record) => {
if (ckCheckingIds.has(record.id)) {
return <Tag color="processing" icon={<LoadingOutlined />}>检测中</Tag>;
}
if (record.ck_check_status === 'valid') {
return (
<Tooltip title={record.ck_checked_at ? `检测时间:${formatTime(record.ck_checked_at)}` : undefined}>
<Tag color="success">有效</Tag>
</Tooltip>
);
}
if (record.ck_check_status === 'invalid') {
return (
<Tooltip title={record.ck_checked_at ? `检测时间:${formatTime(record.ck_checked_at)}` : undefined}>
<Tag color="error">失效</Tag>
</Tooltip>
);
}
return <Text type="secondary">未检测</Text>;
},
},
{
title: '游戏名', dataIndex: 'game_name', width: 170,
render: (_, record) => {
@@ -1742,6 +1826,25 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</Space>
<Space>
<Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>
<Button
size="small"
icon={<CheckCircleOutlined />}
disabled={selectedIds.length === 0 || ckCheckingIds.size > 0}
onClick={() => void checkWorkbenchCk(selectedIds)}
>
检测 CK{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
</Button>
{isAdmin && (
<Button
size="small"
danger
icon={<ReloadOutlined />}
disabled={selectedIds.length === 0}
onClick={reloginInvalidSelected}
>
重登失效
</Button>
)}
{selectedIds.length > 0 && (
<Button size="small" danger onClick={removeSelected}>移出选中</Button>
)}
@@ -1778,7 +1881,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
},
}}
tableLayout="fixed"
scroll={{ x: 1280, y: Math.max(120, accountTableAreaHeight - 80) }}
scroll={{ x: 1380, y: Math.max(120, accountTableAreaHeight - 80) }}
onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' },