feat(weibo): 支持工作台账号转入外部单协作

This commit is contained in:
yml
2026-11-11 20:32:42 +08:00
parent 50bb829101
commit 3f860c3272
8 changed files with 237 additions and 3 deletions
+53
View File
@@ -45,6 +45,8 @@ from ..schemas import (
WeiboWorkbenchBatchImportItem,
WeiboWorkbenchBatchImportOut,
WeiboWorkbenchBatchImportRequest,
WeiboWorkbenchTransferToCollabOut,
WeiboWorkbenchTransferToCollabRequest,
)
from ..services.audit_service import record_audit
from ..services.weibo_service import (
@@ -607,6 +609,57 @@ def add_workbench_accounts(
return {"account_ids": account_ids, "success": True}
@router.post(
"/workbench/accounts/transfer-to-collab",
response_model=WeiboWorkbenchTransferToCollabOut,
)
def transfer_workbench_to_collab(
req: WeiboWorkbenchTransferToCollabRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""把当前工作台账号移出,并引导到外部单协作新建订单。"""
_require_weibo_perm(current, "weibo:workbench")
membership = (
db.query(WeiboWorkbenchAccount)
.filter(
WeiboWorkbenchAccount.user_id == current.id,
WeiboWorkbenchAccount.account_id == req.account_id,
)
.first()
)
if membership is None:
raise HTTPException(status_code=404, detail="该账号不在当前微博工作台")
account = _get_account(db, req.account_id, current)
_ensure_enabled(account)
if not str(account.cookie or "").strip():
raise HTTPException(
status_code=400, detail="该账号没有可用 Cookie,无法转入外部单协作"
)
db.delete(membership)
db.commit()
_record_audit_safely(
db,
current,
action="weibo:workbench:transfer_collab",
target=f"weibo_account:{account.id}",
detail={
"account_id": account.id,
"uid": account.uid,
"nickname": account.nickname,
},
)
return WeiboWorkbenchTransferToCollabOut(
account_id=account.id,
uid=account.uid,
nickname=account.nickname,
game_role_name=account.game_role_name,
)
@router.post("/workbench/accounts/remove")
def remove_workbench_accounts(
req: WeiboWorkbenchAccountsRequest,
+12
View File
@@ -521,6 +521,18 @@ class WeiboWorkbenchAccountsRequest(BaseModel):
account_ids: list[int] = Field(default_factory=list)
class WeiboWorkbenchTransferToCollabRequest(BaseModel):
account_id: int = Field(..., gt=0)
class WeiboWorkbenchTransferToCollabOut(BaseModel):
success: bool = True
account_id: int
uid: str = ""
nickname: str = ""
game_role_name: str = ""
class WeiboWorkbenchBatchImportRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=100_000)
+8
View File
@@ -839,6 +839,14 @@ export interface WeiboWorkbenchBatchImportResult {
items: WeiboWorkbenchBatchImportItem[];
}
export interface WeiboWorkbenchTransferToCollabResult {
success: boolean;
account_id: number;
uid: string;
nickname: string;
game_role_name: string;
}
export interface WeiboBatchTagResult {
updated: number;
tag: string;
+8
View File
@@ -16,6 +16,7 @@ import type {
WeiboImportResult,
WeiboWorkbenchMutationResult,
WeiboWorkbenchBatchImportResult,
WeiboWorkbenchTransferToCollabResult,
} from './types';
export const weiboApi = {
@@ -128,6 +129,13 @@ export const weiboApi = {
'/weibo/workbench/accounts/batch-import',
{ text },
),
transferWorkbenchToCollab: (accountId: number) =>
api.post<
WeiboWorkbenchTransferToCollabResult,
WeiboWorkbenchTransferToCollabResult
>('/weibo/workbench/accounts/transfer-to-collab', {
account_id: accountId,
}),
removeWorkbenchAccounts: (accountIds: number[]) =>
api.post<WeiboWorkbenchMutationResult, WeiboWorkbenchMutationResult>(
'/weibo/workbench/accounts/remove',
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Alert,
AutoComplete,
@@ -37,6 +38,7 @@ import {
ReloadOutlined,
SearchOutlined,
StopOutlined,
SwapOutlined,
ZoomInOutlined,
} from '@ant-design/icons';
import {
@@ -159,6 +161,7 @@ export default function WeiboAccountsPage({
}) {
const { token } = theme.useToken();
const { can } = usePermissions();
const navigate = useNavigate();
const isWorkbench = mode === 'workbench';
const canImport = can('weibo:import');
const canManage = can('weibo:manage');
@@ -168,6 +171,7 @@ export default function WeiboAccountsPage({
const canExchange = can('weibo:exchange');
const canDelete = can('weibo:delete');
const canAssign = can('weibo:assign');
const canTransferCollab = can('weibo:workbench');
const [items, setItems] = useState<WeiboAccountItem[]>([]);
const [total, setTotal] = useState(0);
@@ -184,6 +188,7 @@ export default function WeiboAccountsPage({
const [mutatingId, setMutatingId] = useState<number | null>(null);
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [batchBusy, setBatchBusy] = useState(false);
const [transferringId, setTransferringId] = useState<number | null>(null);
const [batchTagOpen, setBatchTagOpen] = useState(false);
const [batchTagValue, setBatchTagValue] = useState('');
const [batchTagging, setBatchTagging] = useState(false);
@@ -463,6 +468,28 @@ export default function WeiboAccountsPage({
}
};
const transferToCollab = async () => {
if (!isWorkbench || selectedIds.length !== 1) {
message.warning('请选择一个微博工作台账号');
return;
}
const accountId = selectedIds[0];
setTransferringId(accountId);
try {
const result = await weiboApi.transferWorkbenchToCollab(accountId);
message.success(
`${result.nickname || result.uid} 已移出工作台,正在打开外部单协作`,
);
navigate('/weibo/collab', {
state: { transferAccountId: result.account_id },
});
} catch (error) {
message.error(getErrorMessage(error));
} finally {
setTransferringId(null);
}
};
const handleBatchTag = async () => {
if (selectedIds.length === 0) return;
setBatchTagging(true);
@@ -1735,6 +1762,16 @@ export default function WeiboAccountsPage({
批量充值
</Button>
)}
{isWorkbench && canTransferCollab && selectedIds.length === 1 && (
<Button
size="small"
icon={<SwapOutlined />}
loading={transferringId === selectedIds[0]}
onClick={() => void transferToCollab()}
>
转入外部单协作
</Button>
)}
{!isWorkbench && canManage && (
<Button
size="small"
+38 -3
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Alert,
AutoComplete,
@@ -128,6 +129,8 @@ interface OrderFormValues {
export default function WeiboCollabPage() {
const { token } = theme.useToken();
const { can } = usePermissions();
const location = useLocation();
const navigate = useNavigate();
const canEdit = can('weibo:collab:edit');
const canManage = can('weibo:collab:manage');
@@ -163,6 +166,7 @@ export default function WeiboCollabPage() {
const [detailMetrics, setDetailMetrics] = useState<WeiboCollabMetric[]>([]);
const [detailTimeline, setDetailTimeline] = useState<WeiboCollabTimeline[]>([]);
const [detailLoading, setDetailLoading] = useState(false);
const transferHandledRef = useRef<number | null>(null);
const [statusOpen, setStatusOpen] = useState(false);
const [statusTarget, setStatusTarget] = useState<WeiboCollabStatus>('to_recharge');
@@ -297,7 +301,7 @@ export default function WeiboCollabPage() {
if (!goodsOptions.some((item) => item.value === option.value)) goodsOptions.push(option);
}
const lookupAccount = async (accountId?: number | null) => {
const lookupAccount = useCallback(async (accountId?: number | null) => {
setLookup(null);
if (!accountId) return;
setLookupLoading(true);
@@ -310,7 +314,7 @@ export default function WeiboCollabPage() {
} finally {
setLookupLoading(false);
}
};
}, []);
const openCreate = () => {
setEditing(null);
@@ -325,6 +329,37 @@ export default function WeiboCollabPage() {
setEditOpen(true);
};
const transferAccountId = (
location.state as { transferAccountId?: number } | null
)?.transferAccountId;
useEffect(() => {
if (!transferAccountId || transferHandledRef.current === transferAccountId) {
return;
}
transferHandledRef.current = transferAccountId;
setEditing(null);
setLookup(null);
form.resetFields();
form.setFieldsValue({
owner_id: owners[0]?.id,
account_id: transferAccountId,
required_weidou: 0,
home_images: [],
draft: false,
});
setEditOpen(true);
void lookupAccount(transferAccountId);
navigate(location.pathname, { replace: true, state: null });
}, [
form,
location.pathname,
lookupAccount,
navigate,
owners,
transferAccountId,
]);
const openEdit = (order: WeiboCollabOrder) => {
setEditing(order);
form.setFieldsValue({