feat(weibo): 增加外部 CK 单行复制
This commit is contained in:
@@ -704,6 +704,7 @@ data.totalCount
|
||||
6. 兑换接口是 GET 状态变更,必须设置本地防重锁并禁止自动重试。
|
||||
7. 支付二维码从 `createorder.data.url` 对应的收银台 HTML 中解析,不要重新计算 `qr.weibo.cn` 的 `sign`。
|
||||
8. 不记录或回显完整 Cookie、支付 URL、二维码短链、uid、vuid、昵称和订单号。
|
||||
9. 已导入 CK 的单行复制走 `POST /api/weibo/accounts/{account_id}/ck/copy`,由独立权限 `weibo:account:ck_export` 控制;列表/详情不返回 CK 明文,复制动作写 `weibo:account:ck_export` 审计,但审计 detail 只记账号 ID 与 UID,不写 CK 本体。
|
||||
|
||||
## 10. 鉴权与错误处理
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ from core.weibo import (
|
||||
WeiboRechargeError,
|
||||
WeiboRechargeOrder,
|
||||
)
|
||||
from web.backend.models import User, WeiboAccount
|
||||
from web.backend.models import AuditLog, User, WeiboAccount
|
||||
from web.backend.routers import weibo as weibo_router
|
||||
from web.backend.routers.weibo import (
|
||||
copy_account_ck,
|
||||
create_bind_qrcodes,
|
||||
disable_account,
|
||||
enable_account,
|
||||
@@ -221,6 +222,57 @@ def test_refresh_marks_payment_login_expiry_as_invalid(db, current_user):
|
||||
assert "6102" in account.ck_check_message
|
||||
|
||||
|
||||
def test_copy_account_ck_returns_plaintext_and_audits_without_ck(db, current_user):
|
||||
account_out = import_cookies(
|
||||
WeiboCookieImport(text="SCF=a; SUB=b", tag=""),
|
||||
db=db,
|
||||
current=current_user,
|
||||
).accounts[0]
|
||||
|
||||
result = copy_account_ck(account_out.id, db=db, current=current_user)
|
||||
|
||||
dumped = result.model_dump()
|
||||
assert dumped["account_id"] == account_out.id
|
||||
assert dumped["ck_text"] == "SCF=a; SUB=b"
|
||||
audit = (
|
||||
db.query(AuditLog).filter(AuditLog.action == "weibo:account:ck_export").one()
|
||||
)
|
||||
assert dumped["uid"] in audit.detail
|
||||
assert "SUB=b" not in audit.detail
|
||||
|
||||
|
||||
def test_copy_account_ck_uses_export_permission_defaults(db, current_user):
|
||||
account_out = import_cookies(
|
||||
WeiboCookieImport(text="SCF=a; SUB=b", tag=""),
|
||||
db=db,
|
||||
current=current_user,
|
||||
).accounts[0]
|
||||
support = User(
|
||||
username="weibo-copy-support",
|
||||
password_hash="hash",
|
||||
role="support",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(support)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
copy_account_ck(account_out.id, db=db, current=support)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
operator = User(
|
||||
username="weibo-copy-operator",
|
||||
password_hash="hash",
|
||||
role="operation",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(operator)
|
||||
db.commit()
|
||||
result = copy_account_ck(account_out.id, db=db, current=operator)
|
||||
assert result.ck_text == "SCF=a; SUB=b"
|
||||
|
||||
|
||||
def test_list_accounts_returns_parallel_page_shape(db, current_user):
|
||||
assert (
|
||||
import_cookies(
|
||||
|
||||
@@ -45,6 +45,7 @@ PERMISSIONS = {
|
||||
"weibo:view": "查看微博账号",
|
||||
"weibo:import": "导入/刷新微博 CK",
|
||||
"weibo:manage": "编辑、停用和启用微博账号",
|
||||
"weibo:account:ck_export": "复制微博外部 CK 账号的 CK(写剪贴板)",
|
||||
"weibo:bind": "生成微博绑定二维码",
|
||||
"weibo:recharge": "创建微博助威充值订单",
|
||||
"weibo:exchange": "执行微博微豆兑换",
|
||||
@@ -115,6 +116,7 @@ ROLE_PERMISSIONS = {
|
||||
"weibo:view",
|
||||
"weibo:import",
|
||||
"weibo:manage",
|
||||
"weibo:account:ck_export",
|
||||
"weibo:bind",
|
||||
"weibo:recharge",
|
||||
"weibo:exchange",
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..deps import get_current_user
|
||||
from ..models import User, WeiboAccount
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
WeiboAccountCkOut,
|
||||
WeiboAccountDisableRequest,
|
||||
WeiboAccountOut,
|
||||
WeiboAccountUpdate,
|
||||
@@ -347,6 +348,33 @@ def refresh_account(
|
||||
return _account_out(account)
|
||||
|
||||
|
||||
@router.post("/accounts/{account_id}/ck/copy", response_model=WeiboAccountCkOut)
|
||||
def copy_account_ck(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""复制已导入的微博 CK 明文;只走单独权限并保留审计。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:account:ck_export")
|
||||
account = _get_account(db, account_id)
|
||||
ck_text = (account.cookie or "").strip()
|
||||
if not ck_text:
|
||||
raise HTTPException(status_code=409, detail="该账号没有保存 CK")
|
||||
_record_audit_safely(
|
||||
db,
|
||||
current,
|
||||
action="weibo:account:ck_export",
|
||||
target=f"weibo_account:{account.id}",
|
||||
detail={"account_id": account.id, "uid": account.uid},
|
||||
)
|
||||
return WeiboAccountCkOut(
|
||||
account_id=account.id,
|
||||
uid=account.uid,
|
||||
ck_text=ck_text,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/accounts/{account_id}/bind-qrcodes", response_model=WeiboBindQrOut)
|
||||
def create_bind_qrcodes(
|
||||
account_id: int,
|
||||
|
||||
@@ -509,6 +509,12 @@ class WeiboAccountDisableRequest(BaseModel):
|
||||
reason: str = Field("", max_length=256)
|
||||
|
||||
|
||||
class WeiboAccountCkOut(BaseModel):
|
||||
account_id: int
|
||||
uid: str
|
||||
ck_text: str
|
||||
|
||||
|
||||
class WeiboImportResult(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
@@ -801,6 +801,12 @@ export interface WeiboBindQrResult {
|
||||
schemes: WeiboBindScheme[];
|
||||
}
|
||||
|
||||
export interface WeiboAccountCkResult {
|
||||
account_id: number;
|
||||
uid: string;
|
||||
ck_text: string;
|
||||
}
|
||||
|
||||
export interface WeiboGoodsItem {
|
||||
id: number;
|
||||
goods_name: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
MessageResponse,
|
||||
PaginatedResponse,
|
||||
WeiboAccountItem,
|
||||
WeiboAccountCkResult,
|
||||
WeiboBindQrResult,
|
||||
WeiboExchangeRecordsResult,
|
||||
WeiboExchangeResult,
|
||||
@@ -55,6 +56,10 @@ export const weiboApi = {
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/refresh`,
|
||||
),
|
||||
copyAccountCk: (accountId: number) =>
|
||||
api.post<WeiboAccountCkResult, WeiboAccountCkResult>(
|
||||
`/weibo/accounts/${accountId}/ck/copy`,
|
||||
),
|
||||
bindQrcodes: (accountId: number, channel: 'wx' | 'qq') =>
|
||||
api.post<WeiboBindQrResult, WeiboBindQrResult>(
|
||||
`/weibo/accounts/${accountId}/bind-qrcodes`,
|
||||
|
||||
@@ -86,6 +86,7 @@ export default function WeiboAccountsPage() {
|
||||
const { can } = usePermissions();
|
||||
const canImport = can('weibo:import');
|
||||
const canManage = can('weibo:manage');
|
||||
const canExportCk = can('weibo:account:ck_export');
|
||||
const canBind = can('weibo:bind');
|
||||
const canRecharge = can('weibo:recharge');
|
||||
const canExchange = can('weibo:exchange');
|
||||
@@ -100,6 +101,7 @@ export default function WeiboAccountsPage() {
|
||||
const [enabledFilter, setEnabledFilter] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshingId, setRefreshingId] = useState<number | null>(null);
|
||||
const [copyingId, setCopyingId] = useState<number | null>(null);
|
||||
const [mutatingId, setMutatingId] = useState<number | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [batchBusy, setBatchBusy] = useState(false);
|
||||
@@ -216,6 +218,19 @@ export default function WeiboAccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCk = async (account: WeiboAccountItem) => {
|
||||
setCopyingId(account.id);
|
||||
try {
|
||||
const result = await weiboApi.copyAccountCk(account.id);
|
||||
await copyTextToClipboard(result.ck_text);
|
||||
message.success(`${account.nickname || account.uid} 的 CK 已复制`);
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setCopyingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const openQr = (account: WeiboAccountItem) => {
|
||||
if (!account.is_enabled) {
|
||||
message.warning('账号已停用,请先启用');
|
||||
@@ -699,6 +714,16 @@ export default function WeiboAccountsPage() {
|
||||
>
|
||||
功能
|
||||
</Button>
|
||||
{canExportCk && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
loading={copyingId === account.id}
|
||||
onClick={() => void handleCopyCk(account)}
|
||||
>
|
||||
复制CK
|
||||
</Button>
|
||||
)}
|
||||
{canManage && (
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
Reference in New Issue
Block a user