feat(weibo): 展示 CK 导入重复账号明细
This commit is contained in:
@@ -170,6 +170,9 @@ UNIQUE(account_id) -- 同一账号同时只能属于一个工作台
|
||||
|
||||
- 默认重复导入:不覆盖现有 CK,返回 `skipped=1`。
|
||||
- 勾选「重复账号时更新 CK」:覆盖现有账号 CK 和最新资料,但**不新建账号**。
|
||||
- 重复结果不是只有数量:`duplicates[]` 逐条返回输入行号、UID、昵称和原因,
|
||||
并区分「账号池已存在」与「本次粘贴内容中 UID 重复」;导入弹窗直接列出,
|
||||
顶部提示只预览前三个,完整明细保留在弹窗。
|
||||
- 客服不能更新已分配给其他客服的账号 CK,也不能通过重复导入改变账号归属。
|
||||
- 客服导入新账号时,账号会自动分配给当前客服。
|
||||
- 若账号已被其他人的工作台占用,其他用户不能更新 CK,必须由当前使用人操作。
|
||||
|
||||
@@ -164,6 +164,16 @@ def test_import_creates_once_and_updates_existing_only_when_requested(db, curren
|
||||
assert duplicate.count == 0
|
||||
assert duplicate.skipped == 1
|
||||
assert duplicate.accounts == []
|
||||
assert [item.model_dump() for item in duplicate.duplicates] == [
|
||||
{
|
||||
"line": 1,
|
||||
"uid": "4014000001",
|
||||
"nickname": "测试微博",
|
||||
"message": "账号池中已存在,未勾选更新 CK",
|
||||
}
|
||||
]
|
||||
assert "4014000001" in duplicate.message
|
||||
assert "测试微博" in duplicate.message
|
||||
assert db.query(WeiboAccount).count() == 1
|
||||
account = db.query(WeiboAccount).one()
|
||||
assert account.uid == "4014000001"
|
||||
@@ -198,6 +208,20 @@ def test_import_creates_once_and_updates_existing_only_when_requested(db, curren
|
||||
assert "cookie" not in dumped
|
||||
|
||||
|
||||
def test_import_reports_duplicate_uid_inside_same_paste(db, current_user):
|
||||
result = import_cookies(
|
||||
WeiboCookieImport(text="SCF=a; SUB=first\nSCF=b; SUB=second"),
|
||||
db=db,
|
||||
current=current_user,
|
||||
)
|
||||
|
||||
assert result.count == 1
|
||||
assert result.skipped == 1
|
||||
assert result.duplicates[0].line == 2
|
||||
assert result.duplicates[0].uid == "4014000001"
|
||||
assert result.duplicates[0].message == "本次粘贴内容中该 UID 重复"
|
||||
|
||||
|
||||
def test_import_reports_invalid_ck_per_line_and_keeps_valid_account(db, current_user):
|
||||
result = import_cookies(
|
||||
WeiboCookieImport(text="SCF=a; SUB=expired\nSCF=b; SUB=valid", tag=""),
|
||||
|
||||
@@ -32,6 +32,7 @@ from ..schemas import (
|
||||
WeiboExchangeRecordsOut,
|
||||
WeiboExchangeRequest,
|
||||
WeiboGoodsOut,
|
||||
WeiboImportDuplicateOut,
|
||||
WeiboImportError,
|
||||
WeiboImportResult,
|
||||
WeiboMallListOut,
|
||||
@@ -980,7 +981,7 @@ def import_cookies(
|
||||
"""导入微博 CK;每个非空行一条完整 Cookie。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:import")
|
||||
accounts, errors, skipped = import_weibo_cookies(
|
||||
accounts, errors, skipped, duplicates = import_weibo_cookies(
|
||||
db,
|
||||
req.text,
|
||||
req.tag,
|
||||
@@ -992,6 +993,15 @@ def import_cookies(
|
||||
typed_errors = [
|
||||
WeiboImportError(line=item.line, message=item.message) for item in errors
|
||||
]
|
||||
typed_duplicates = [
|
||||
WeiboImportDuplicateOut(
|
||||
line=item.line,
|
||||
uid=item.uid,
|
||||
nickname=item.nickname,
|
||||
message=item.message,
|
||||
)
|
||||
for item in duplicates
|
||||
]
|
||||
if accounts and not _can_view_all_weibo_accounts(current):
|
||||
changed = False
|
||||
for account in accounts:
|
||||
@@ -1012,6 +1022,7 @@ def import_cookies(
|
||||
"uids": [account.uid for account in accounts],
|
||||
"count": len(accounts),
|
||||
"skipped": skipped,
|
||||
"duplicate_uids": [item.uid for item in duplicates],
|
||||
"errors": len(errors),
|
||||
},
|
||||
)
|
||||
@@ -1019,13 +1030,21 @@ def import_cookies(
|
||||
if errors:
|
||||
message += f",失败 {len(errors)} 条"
|
||||
if skipped:
|
||||
preview = "、".join(
|
||||
f"{item.uid}({item.nickname or '无昵称'})" for item in duplicates[:3]
|
||||
)
|
||||
message += f",跳过 {skipped} 条重复"
|
||||
if preview:
|
||||
message += f":{preview}"
|
||||
if len(duplicates) > 3:
|
||||
message += f" 等 {len(duplicates)} 个"
|
||||
return WeiboImportResult(
|
||||
success=bool(accounts) or (skipped > 0 and not errors),
|
||||
message=message,
|
||||
count=len(accounts),
|
||||
skipped=skipped,
|
||||
errors=typed_errors,
|
||||
duplicates=typed_duplicates,
|
||||
accounts=[_account_out(account) for account in accounts],
|
||||
)
|
||||
|
||||
|
||||
@@ -480,6 +480,13 @@ class WeiboImportError(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class WeiboImportDuplicateOut(BaseModel):
|
||||
line: int
|
||||
uid: str = ""
|
||||
nickname: str = ""
|
||||
message: str
|
||||
|
||||
|
||||
class WeiboAccountOut(BaseModel):
|
||||
id: int
|
||||
uid: str
|
||||
@@ -576,6 +583,7 @@ class WeiboImportResult(BaseModel):
|
||||
count: int
|
||||
skipped: int
|
||||
errors: list[WeiboImportError] = Field(default_factory=list)
|
||||
duplicates: list[WeiboImportDuplicateOut] = Field(default_factory=list)
|
||||
accounts: list[WeiboAccountOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@ class WeiboImportIssue:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WeiboImportDuplicate:
|
||||
line: int
|
||||
uid: str
|
||||
nickname: str
|
||||
message: str
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
|
||||
@@ -64,15 +72,21 @@ def import_weibo_cookies(
|
||||
update_existing: bool = False,
|
||||
current_user_id: int | None = None,
|
||||
can_update_all: bool = False,
|
||||
) -> tuple[list[WeiboAccount], list[WeiboImportIssue], int]:
|
||||
) -> tuple[
|
||||
list[WeiboAccount],
|
||||
list[WeiboImportIssue],
|
||||
int,
|
||||
list[WeiboImportDuplicate],
|
||||
]:
|
||||
"""逐行验证并导入微博 CK。
|
||||
|
||||
返回 ``(成功账号, 错误列表, 跳过数)``。单行 CK 失效或网络失败只记录该行,
|
||||
不阻断其余账号导入。
|
||||
返回 ``(成功账号, 错误列表, 跳过数, 重复明细)``。单行 CK 失效或网络失败
|
||||
只记录该行,不阻断其余账号导入。
|
||||
"""
|
||||
|
||||
accounts: list[WeiboAccount] = []
|
||||
errors: list[WeiboImportIssue] = []
|
||||
duplicates: list[WeiboImportDuplicate] = []
|
||||
skipped = 0
|
||||
seen_uids: set[str] = set()
|
||||
clean_tag = (tag or "").strip()
|
||||
@@ -92,6 +106,14 @@ def import_weibo_cookies(
|
||||
|
||||
if snapshot.uid in seen_uids:
|
||||
skipped += 1
|
||||
duplicates.append(
|
||||
WeiboImportDuplicate(
|
||||
line=line_no,
|
||||
uid=snapshot.uid,
|
||||
nickname=snapshot.nickname,
|
||||
message="本次粘贴内容中该 UID 重复",
|
||||
)
|
||||
)
|
||||
continue
|
||||
seen_uids.add(snapshot.uid)
|
||||
|
||||
@@ -126,6 +148,14 @@ def import_weibo_cookies(
|
||||
continue
|
||||
if account is not None and not update_existing:
|
||||
skipped += 1
|
||||
duplicates.append(
|
||||
WeiboImportDuplicate(
|
||||
line=line_no,
|
||||
uid=snapshot.uid,
|
||||
nickname=account.nickname or snapshot.nickname,
|
||||
message="账号池中已存在,未勾选更新 CK",
|
||||
)
|
||||
)
|
||||
continue
|
||||
if account is None:
|
||||
account = WeiboAccount(uid=snapshot.uid, cookie=cookie)
|
||||
@@ -139,7 +169,7 @@ def import_weibo_cookies(
|
||||
db.commit()
|
||||
for account in accounts:
|
||||
db.refresh(account)
|
||||
return accounts, errors, skipped
|
||||
return accounts, errors, skipped, duplicates
|
||||
|
||||
|
||||
def refresh_weibo_account(
|
||||
|
||||
@@ -777,12 +777,20 @@ export interface WeiboImportError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WeiboImportDuplicate {
|
||||
line: number;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WeiboImportResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
count: number;
|
||||
skipped: number;
|
||||
errors: WeiboImportError[];
|
||||
duplicates: WeiboImportDuplicate[];
|
||||
accounts: WeiboAccountItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
type WeiboBindQrResult,
|
||||
type WeiboExchangeRecordsResult,
|
||||
type WeiboImportError,
|
||||
type WeiboImportDuplicate,
|
||||
type WeiboMallListResult,
|
||||
type WeiboPaymentStatusResult,
|
||||
type WeiboPointHistoryResult,
|
||||
@@ -212,6 +213,9 @@ export default function WeiboAccountsPage({
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importErrors, setImportErrors] = useState<WeiboImportError[]>([]);
|
||||
const [importDuplicates, setImportDuplicates] = useState<
|
||||
WeiboImportDuplicate[]
|
||||
>([]);
|
||||
const [form] = Form.useForm<{
|
||||
text: string;
|
||||
tag?: string;
|
||||
@@ -327,6 +331,7 @@ export default function WeiboAccountsPage({
|
||||
Boolean(values.update_existing),
|
||||
);
|
||||
setImportErrors(result.errors);
|
||||
setImportDuplicates(result.duplicates);
|
||||
let workbenchError = '';
|
||||
if (isWorkbench && result.accounts.length > 0) {
|
||||
try {
|
||||
@@ -1722,6 +1727,7 @@ export default function WeiboAccountsPage({
|
||||
disabled={!canImport}
|
||||
onClick={() => {
|
||||
setImportErrors([]);
|
||||
setImportDuplicates([]);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ update_existing: false });
|
||||
setImportOpen(true);
|
||||
@@ -2753,6 +2759,26 @@ export default function WeiboAccountsPage({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{importDuplicates.length > 0 && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: importErrors.length > 0 ? 12 : 0 }}
|
||||
title={`${importDuplicates.length} 条重复账号已跳过`}
|
||||
description={
|
||||
<div style={{ maxHeight: 220, overflowY: 'auto' }}>
|
||||
{importDuplicates.map((item) => (
|
||||
<div key={`${item.line}-${item.uid}`}>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
第 {item.line} 行 · UID {item.uid} ·{' '}
|
||||
{item.nickname || '无昵称'}:{item.message}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user