feat(weibo): 支持管理员强制更新占用账号 CK

This commit is contained in:
yml
2026-11-14 14:46:38 +08:00
parent b4801336c5
commit 17a2b5c464
7 changed files with 229 additions and 23 deletions
+15 -1
View File
@@ -1175,6 +1175,8 @@ def import_cookies(
"""导入微博 CK;每个非空行一条完整 Cookie。"""
_require_weibo_perm(current, "weibo:import")
if req.force_update:
_require_weibo_perm(current, "weibo:assign:force")
accounts, errors, skipped, duplicates = import_weibo_cookies(
db,
req.text,
@@ -1183,9 +1185,20 @@ def import_cookies(
req.update_existing,
current.id,
can_update_all=_can_view_all_weibo_accounts(current),
force_update=req.force_update,
)
typed_errors = [
WeiboImportError(line=item.line, message=item.message) for item in errors
WeiboImportError(
line=item.line,
message=item.message,
account_id=item.account_id,
uid=item.uid,
assigned_user_id=item.assigned_user_id,
assigned_username=item.assigned_username,
workbench_owner_id=item.workbench_owner_id,
workbench_owner_username=item.workbench_owner_username,
)
for item in errors
]
typed_duplicates = [
WeiboImportDuplicateOut(
@@ -1218,6 +1231,7 @@ def import_cookies(
"skipped": skipped,
"duplicate_uids": [item.uid for item in duplicates],
"errors": len(errors),
"force_update": req.force_update,
},
)
message = f"成功导入 {len(accounts)} 个微博账号"
+13
View File
@@ -474,11 +474,24 @@ class WeiboCookieImport(BaseModel):
text: str = Field(..., min_length=1)
tag: str = Field("", max_length=64)
update_existing: bool = False
force_update: bool = False
@model_validator(mode="after")
def _validate_force_update(self):
if self.force_update and not self.update_existing:
raise ValueError("强制更新必须同时勾选“重复账号时更新 CK”")
return self
class WeiboImportError(BaseModel):
line: int
message: str
account_id: int | None = None
uid: str = ""
assigned_user_id: int | None = None
assigned_username: str = ""
workbench_owner_id: int | None = None
workbench_owner_username: str = ""
class WeiboImportDuplicateOut(BaseModel):
+70 -16
View File
@@ -19,7 +19,7 @@ from core.weibo import (
normalize_weibo_cookie,
)
from ..models import WeiboAccount, WeiboRechargeRecord, WeiboWorkbenchAccount
from ..models import User, WeiboAccount, WeiboRechargeRecord, WeiboWorkbenchAccount
from .weibo_image_service import embed_weibo_goods_images
@@ -27,6 +27,12 @@ from .weibo_image_service import embed_weibo_goods_images
class WeiboImportIssue:
line: int
message: str
account_id: int | None = None
uid: str = ""
assigned_user_id: int | None = None
assigned_username: str = ""
workbench_owner_id: int | None = None
workbench_owner_username: str = ""
@dataclass(frozen=True, slots=True)
@@ -74,6 +80,7 @@ def import_weibo_cookies(
update_existing: bool = False,
current_user_id: int | None = None,
can_update_all: bool = False,
force_update: bool = False,
) -> tuple[
list[WeiboAccount],
list[WeiboImportIssue],
@@ -123,31 +130,78 @@ def import_weibo_cookies(
db.query(WeiboAccount).filter(WeiboAccount.uid == snapshot.uid).first()
)
if account is not None and update_existing and current_user_id is not None:
if (
not can_update_all
and account.assigned_to is not None
and account.assigned_to != current_user_id
):
errors.append(
WeiboImportIssue(
line=line_no,
message="该账号已分配给其他客服,不能更新 CK",
)
assigned_user = (
db.query(User).filter(User.id == account.assigned_to).first()
if account.assigned_to is not None
else None
)
workbench_owner = (
db.query(User)
.join(
WeiboWorkbenchAccount,
WeiboWorkbenchAccount.user_id == User.id,
)
continue
owner_id = (
db.query(WeiboWorkbenchAccount.user_id)
.filter(
WeiboWorkbenchAccount.account_id == account.id,
WeiboWorkbenchAccount.exclusive_slot.is_not(None),
)
.first()
)
if owner_id is not None and owner_id[0] != current_user_id:
assigned_conflict = (
not can_update_all
and account.assigned_to is not None
and account.assigned_to != current_user_id
)
workbench_conflict = (
workbench_owner is not None and workbench_owner.id != current_user_id
)
if not force_update and (assigned_conflict or workbench_conflict):
assigned_label = (
assigned_user.username if assigned_user else account.assigned_to
)
workbench_label = workbench_owner.username if workbench_owner else ""
if (
assigned_conflict
and workbench_conflict
and workbench_owner is not None
):
conflict_message = (
"该账号已分配给其他客服:账号归属客服 "
f"{assigned_label}(ID {account.assigned_to}),"
f"且个人工作台排他槽位归 {workbench_label}"
f"(ID {workbench_owner.id})"
)
elif assigned_conflict:
conflict_message = (
"该账号已分配给其他客服:账号归属客服 "
f"{assigned_label}(ID {account.assigned_to})"
)
else:
assert workbench_owner is not None
conflict_message = (
"该账号已被其他人的工作台占用:个人工作台排他槽位归 "
f"{workbench_label}"
f"(ID {workbench_owner.id})"
)
errors.append(
WeiboImportIssue(
line=line_no,
message="该账号已被其他人的工作台占用,不能更新 CK",
message=(
conflict_message
+ ",不能更新 CK。超级管理员可勾选“强制更新”覆盖,原归属保持不变"
),
account_id=account.id,
uid=account.uid,
assigned_user_id=account.assigned_to,
assigned_username=(
assigned_user.username if assigned_user else ""
),
workbench_owner_id=(
workbench_owner.id if workbench_owner else None
),
workbench_owner_username=(
workbench_owner.username if workbench_owner else ""
),
)
)
continue
+6
View File
@@ -775,6 +775,12 @@ export interface WeiboAccountItem {
export interface WeiboImportError {
line: number;
message: string;
account_id: number | null;
uid: string;
assigned_user_id: number | null;
assigned_username: string;
workbench_owner_id: number | null;
workbench_owner_username: string;
}
export interface WeiboImportDuplicate {
+7 -1
View File
@@ -49,11 +49,17 @@ export const weiboApi = {
),
getAccount: (accountId: number) =>
api.get<WeiboAccountItem, WeiboAccountItem>(`/weibo/accounts/${accountId}`),
importCookies: (text: string, tag = '', updateExisting = false) =>
importCookies: (
text: string,
tag = '',
updateExisting = false,
forceUpdate = false,
) =>
api.post<WeiboImportResult, WeiboImportResult>('/weibo/accounts/import-cookies', {
text,
tag,
update_existing: updateExisting,
force_update: forceUpdate,
}),
updateAccount: (
accountId: number,
+41 -4
View File
@@ -192,6 +192,7 @@ export default function WeiboAccountsPage({
const canExchange = can('weibo:exchange');
const canDelete = can('weibo:delete');
const canAssign = can('weibo:assign');
const canForceUpdateCk = can('weibo:assign:force');
const canTransferCollab = can('weibo:workbench');
const [items, setItems] = useState<WeiboAccountItem[]>([]);
@@ -241,7 +242,10 @@ export default function WeiboAccountsPage({
text: string;
tag?: string;
update_existing?: boolean;
force_update?: boolean;
}>();
const updateExistingCk = Form.useWatch('update_existing', form);
const forceUpdateCk = Form.useWatch('force_update', form);
const [editForm] = Form.useForm<{ tag?: string; remark?: string }>();
const [editOpen, setEditOpen] = useState(false);
const [editAccount, setEditAccount] = useState<WeiboAccountItem | null>(null);
@@ -406,6 +410,7 @@ export default function WeiboAccountsPage({
values.text,
values.tag || '',
Boolean(values.update_existing),
Boolean(values.force_update),
);
setImportErrors(result.errors);
setImportDuplicates(result.duplicates);
@@ -3140,7 +3145,16 @@ export default function WeiboAccountsPage({
onCancel={() => setImportOpen(false)}
destroyOnHidden
>
<Form form={form} layout="vertical" preserve={false}>
<Form
form={form}
layout="vertical"
preserve={false}
onValuesChange={(changed) => {
if (changed.update_existing === false) {
form.setFieldValue('force_update', false);
}
}}
>
<Form.Item
name="text"
label="Cookie"
@@ -3159,6 +3173,26 @@ export default function WeiboAccountsPage({
重复账号时更新 CK(仍只有一条全局账号,不新建)
</Checkbox>
</Form.Item>
{canForceUpdateCk && (
<Form.Item
name="force_update"
valuePropName="checked"
style={{ marginTop: -8 }}
>
<Checkbox disabled={!updateExistingCk}>
强制更新已被分配或工作台占用的 CK(保留原归属)
</Checkbox>
</Form.Item>
)}
{forceUpdateCk && (
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
title="强制更新会跳过归属和工作台占用校验"
description="只替换 Cookie 和验证状态,不会取消或改变原账号归属与工作台成员。"
/>
)}
</Form>
{importErrors.length > 0 && (
<Alert
@@ -3168,9 +3202,12 @@ export default function WeiboAccountsPage({
description={
<Space orientation="vertical" size={2}>
{importErrors.slice(0, 8).map((item) => (
<Text key={`${item.line}-${item.message}`} style={{ fontSize: 12 }}>
第 {item.line} 行:{item.message}
</Text>
<div key={`${item.line}-${item.message}`}>
<Text style={{ fontSize: 12 }}>
第 {item.line} 行{item.uid ? ` · UID ${item.uid}` : ''}:
{item.message}
</Text>
</div>
))}
</Space>
}