feat(weibo): 展示 CK 导入重复账号明细

This commit is contained in:
yml
2026-11-11 21:10:23 +08:00
parent 3f860c3272
commit 11b5a4d938
7 changed files with 123 additions and 5 deletions
@@ -170,6 +170,9 @@ UNIQUE(account_id) -- 同一账号同时只能属于一个工作台
- 默认重复导入:不覆盖现有 CK,返回 `skipped=1`。
- 勾选「重复账号时更新 CK」:覆盖现有账号 CK 和最新资料,但**不新建账号**。
- 重复结果不是只有数量:`duplicates[]` 逐条返回输入行号、UID、昵称和原因,
并区分「账号池已存在」与「本次粘贴内容中 UID 重复」;导入弹窗直接列出,
顶部提示只预览前三个,完整明细保留在弹窗。
- 客服不能更新已分配给其他客服的账号 CK,也不能通过重复导入改变账号归属。
- 客服导入新账号时,账号会自动分配给当前客服。
- 若账号已被其他人的工作台占用,其他用户不能更新 CK,必须由当前使用人操作。
+24
View File
@@ -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=""),
+20 -1
View File
@@ -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],
)
+8
View File
@@ -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)
+34 -4
View File
@@ -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(
+8
View File
@@ -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