账号导入--可以直接打标签
This commit is contained in:
@@ -186,8 +186,11 @@ def import_accounts(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current: User = Depends(require_permission("account:import")),
|
current: User = Depends(require_permission("account:import")),
|
||||||
):
|
):
|
||||||
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)"""
|
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||||||
accounts, skipped = parse_and_build_accounts(req.text)
|
|
||||||
|
req.tag 为统一标签兜底:某行未单独写标签时使用该值,行内标签优先。
|
||||||
|
"""
|
||||||
|
accounts, skipped = parse_and_build_accounts(req.text, req.tag)
|
||||||
|
|
||||||
if accounts:
|
if accounts:
|
||||||
db.add_all(accounts)
|
db.add_all(accounts)
|
||||||
|
|||||||
@@ -80,8 +80,13 @@ class UserUpdate(BaseModel):
|
|||||||
|
|
||||||
# ---- 账号 ----
|
# ---- 账号 ----
|
||||||
class AccountImport(BaseModel):
|
class AccountImport(BaseModel):
|
||||||
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码"""
|
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||||||
|
|
||||||
|
tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值,
|
||||||
|
行内第5列标签优先。
|
||||||
|
"""
|
||||||
text: str
|
text: str
|
||||||
|
tag: str = ""
|
||||||
|
|
||||||
|
|
||||||
class AccountAssign(BaseModel):
|
class AccountAssign(BaseModel):
|
||||||
|
|||||||
@@ -31,15 +31,21 @@ def cookie_account_ids_query(db: Session):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
def parse_and_build_accounts(text: str) -> tuple[list[Account], int]:
|
def parse_and_build_accounts(text: str, default_tag: str = "") -> tuple[list[Account], int]:
|
||||||
"""
|
"""
|
||||||
解析批量导入文本,构建 Account ORM 对象列表。
|
解析批量导入文本,构建 Account ORM 对象列表。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 批量导入文本,每行格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||||||
|
default_tag: 统一标签兜底。某行未单独写标签时使用该值,
|
||||||
|
行内第5列标签优先。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(accounts, skipped_count)
|
(accounts, skipped_count)
|
||||||
"""
|
"""
|
||||||
from core.douyu.email_verifier import get_email_config_for_account
|
from core.douyu.email_verifier import get_email_config_for_account
|
||||||
|
|
||||||
|
default_tag = (default_tag or "").strip()
|
||||||
accounts = []
|
accounts = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
for line in text.strip().split('\n'):
|
for line in text.strip().split('\n'):
|
||||||
@@ -52,7 +58,9 @@ def parse_and_build_accounts(text: str) -> tuple[list[Account], int]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
username, password, email, email_password = [p.strip() for p in parts[:4]]
|
username, password, email, email_password = [p.strip() for p in parts[:4]]
|
||||||
tag = parts[4].strip() if len(parts) > 4 else ""
|
line_tag = parts[4].strip() if len(parts) > 4 else ""
|
||||||
|
# 行内标签优先,未写则用统一兜底标签
|
||||||
|
tag = line_tag or default_tag
|
||||||
if not all([username, password, email, email_password]):
|
if not all([username, password, email, email_password]):
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export const accountApi = {
|
|||||||
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
|
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
|
||||||
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
|
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
|
||||||
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
|
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
|
||||||
import: (text: string) => api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text }),
|
import: (text: string, tag?: string) =>
|
||||||
|
api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text, tag: tag || '' }),
|
||||||
assign: (id: number, assigned_to: number | null) =>
|
assign: (id: number, assigned_to: number | null) =>
|
||||||
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
|
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
|
||||||
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function AccountsPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [importOpen, setImportOpen] = useState(false);
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
const [importText, setImportText] = useState('');
|
const [importText, setImportText] = useState('');
|
||||||
|
const [importTag, setImportTag] = useState('');
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [tagFilter, setTagFilter] = useState<string>('');
|
const [tagFilter, setTagFilter] = useState<string>('');
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
@@ -137,10 +138,11 @@ export default function AccountsPage() {
|
|||||||
}
|
}
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
try {
|
try {
|
||||||
const result = await accountApi.import(importText);
|
const result = await accountApi.import(importText, importTag);
|
||||||
message.success(result.message);
|
message.success(result.message);
|
||||||
setImportOpen(false);
|
setImportOpen(false);
|
||||||
setImportText('');
|
setImportText('');
|
||||||
|
setImportTag('');
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
loadTags();
|
loadTags();
|
||||||
loadSummary();
|
loadSummary();
|
||||||
@@ -375,7 +377,7 @@ export default function AccountsPage() {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
{canImport && (
|
{canImport && (
|
||||||
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
|
<Button type="primary" icon={<ImportOutlined />} onClick={() => { setImportTag(''); setImportOpen(true); }}>
|
||||||
批量导入
|
批量导入
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -480,6 +482,17 @@ export default function AccountsPage() {
|
|||||||
placeholder={`用户名|密码|邮箱|邮箱密码|标签\n用户名|密码|邮箱|邮箱密码|标签`}
|
placeholder={`用户名|密码|邮箱|邮箱密码|标签\n用户名|密码|邮箱|邮箱密码|标签`}
|
||||||
style={{ marginTop: 8 }}
|
style={{ marginTop: 8 }}
|
||||||
/>
|
/>
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<Text type="secondary">统一标签(可选):未在行内写标签的账号将使用此标签</Text>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ width: '100%', marginTop: 4 }}
|
||||||
|
placeholder="输入或选择标签"
|
||||||
|
value={importTag ? [importTag] : []}
|
||||||
|
onChange={(vals) => setImportTag(vals.length > 0 ? vals[vals.length - 1] : '')}
|
||||||
|
options={tags.map((t) => ({ value: t, label: t }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ const ESPORTS_QUICK_ACTIONS = [
|
|||||||
{ key: 'query_esports_game_name', icon: <SearchOutlined /> },
|
{ key: 'query_esports_game_name', icon: <SearchOutlined /> },
|
||||||
{ key: 'create_esports_qr', icon: <CreditCardOutlined /> },
|
{ key: 'create_esports_qr', icon: <CreditCardOutlined /> },
|
||||||
{ key: 'query_esports_points', icon: <SearchOutlined /> },
|
{ key: 'query_esports_points', icon: <SearchOutlined /> },
|
||||||
|
{ key: 'query_change_bind_time', icon: <FieldTimeOutlined /> },
|
||||||
|
{ key: 'query_limited_goods', icon: <SearchOutlined /> },
|
||||||
{ key: 'query_gold_balance', icon: <SearchOutlined /> },
|
{ key: 'query_gold_balance', icon: <SearchOutlined /> },
|
||||||
{ key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
|
{ key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
|
||||||
{ key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
|
{ key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
|
||||||
@@ -76,6 +78,8 @@ const ESPORTS_TASK_TYPES = new Set([
|
|||||||
'create_esports_qr',
|
'create_esports_qr',
|
||||||
'query_esports_points',
|
'query_esports_points',
|
||||||
'query_gold_balance',
|
'query_gold_balance',
|
||||||
|
'query_change_bind_time',
|
||||||
|
'query_limited_goods',
|
||||||
'refresh_esports_goods',
|
'refresh_esports_goods',
|
||||||
'exchange_esports_goods',
|
'exchange_esports_goods',
|
||||||
'create_gold_qr',
|
'create_gold_qr',
|
||||||
@@ -1308,7 +1312,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
dataSource={filteredAccounts}
|
dataSource={filteredAccounts}
|
||||||
pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }}
|
pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
scroll={{ y: Math.max(120, accountTableAreaHeight - 80) }}
|
scroll={{ x: 960, y: Math.max(120, accountTableAreaHeight - 80) }}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onContextMenu: (e) => handleRowContextMenu(record, e),
|
onContextMenu: (e) => handleRowContextMenu(record, e),
|
||||||
style: { cursor: 'context-menu' },
|
style: { cursor: 'context-menu' },
|
||||||
|
|||||||
Reference in New Issue
Block a user