feat(weibo): 增加账号分配与客服数据隔离
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""增加微博账号归属客服。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261108_0074"
|
||||
down_revision: str | None = "20261108_0073"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not inspector.has_table("weibo_accounts"):
|
||||
return
|
||||
columns = {column["name"] for column in inspector.get_columns("weibo_accounts")}
|
||||
if "assigned_to" not in columns:
|
||||
op.add_column(
|
||||
"weibo_accounts",
|
||||
sa.Column("assigned_to", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_weibo_accounts_assigned_to_users",
|
||||
"weibo_accounts",
|
||||
"users",
|
||||
["assigned_to"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
indexes = {index["name"] for index in inspector.get_indexes("weibo_accounts")}
|
||||
if "ix_weibo_accounts_assigned_to" not in indexes:
|
||||
op.create_index(
|
||||
"ix_weibo_accounts_assigned_to",
|
||||
"weibo_accounts",
|
||||
["assigned_to"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not inspector.has_table("weibo_accounts"):
|
||||
return
|
||||
foreign_keys = {fk["name"] for fk in inspector.get_foreign_keys("weibo_accounts")}
|
||||
if "fk_weibo_accounts_assigned_to_users" in foreign_keys:
|
||||
op.drop_constraint(
|
||||
"fk_weibo_accounts_assigned_to_users",
|
||||
"weibo_accounts",
|
||||
type_="foreignkey",
|
||||
)
|
||||
indexes = {index["name"] for index in inspector.get_indexes("weibo_accounts")}
|
||||
if "ix_weibo_accounts_assigned_to" in indexes:
|
||||
op.drop_index("ix_weibo_accounts_assigned_to", table_name="weibo_accounts")
|
||||
columns = {column["name"] for column in inspector.get_columns("weibo_accounts")}
|
||||
if "assigned_to" in columns:
|
||||
op.drop_column("weibo_accounts", "assigned_to")
|
||||
@@ -934,6 +934,11 @@ class WeiboAccount(Base):
|
||||
String(256), nullable=False, default="", server_default=""
|
||||
)
|
||||
activity: Mapped[str] = mapped_column(String(64), default="")
|
||||
assigned_to: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
is_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="1", index=True
|
||||
@@ -960,6 +965,11 @@ class WeiboAccount(Base):
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
assigned_user: Mapped[User | None] = relationship(
|
||||
"User",
|
||||
foreign_keys=[assigned_to],
|
||||
)
|
||||
|
||||
|
||||
class WeiboWorkbenchAccount(Base):
|
||||
"""用户个人微博工作台中的账号选择。"""
|
||||
|
||||
@@ -44,6 +44,7 @@ PERMISSIONS = {
|
||||
# 微博微豆
|
||||
"weibo:view": "查看微博账号",
|
||||
"weibo:workbench": "使用个人微博工作台",
|
||||
"weibo:assign": "查看全部微博账号并分配客服",
|
||||
"weibo:import": "导入/刷新微博 CK",
|
||||
"weibo:manage": "编辑、停用和启用微博账号",
|
||||
"weibo:account:ck_export": "复制微博外部 CK 账号的 CK(写剪贴板)",
|
||||
@@ -116,6 +117,7 @@ ROLE_PERMISSIONS = {
|
||||
"yyb:history",
|
||||
"weibo:view",
|
||||
"weibo:workbench",
|
||||
"weibo:assign",
|
||||
"weibo:import",
|
||||
"weibo:manage",
|
||||
"weibo:account:ck_export",
|
||||
|
||||
+243
-29
@@ -6,9 +6,9 @@ import os
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from loguru import logger
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session, defer
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from core.weibo import WeiboAuthError, WeiboError, WeiboExchangeUncertainError
|
||||
|
||||
@@ -22,6 +22,7 @@ from ..schemas import (
|
||||
WeiboAccountDisableRequest,
|
||||
WeiboAccountOut,
|
||||
WeiboAccountUpdate,
|
||||
WeiboBatchAssignRequest,
|
||||
WeiboBindQrOut,
|
||||
WeiboBindSchemeOut,
|
||||
WeiboCookieImport,
|
||||
@@ -80,15 +81,38 @@ def _int_or_none(value: object) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def _get_account(db: Session, account_id: int) -> WeiboAccount:
|
||||
account = db.query(WeiboAccount).filter(WeiboAccount.id == account_id).first()
|
||||
def _can_view_all_weibo_accounts(user: User) -> bool:
|
||||
return user_has_permission(user, "weibo:assign")
|
||||
|
||||
|
||||
def _visible_weibo_accounts_query(query, current: User):
|
||||
if _can_view_all_weibo_accounts(current):
|
||||
return query
|
||||
return query.filter(WeiboAccount.assigned_to == current.id)
|
||||
|
||||
|
||||
def _get_account(
|
||||
db: Session,
|
||||
account_id: int,
|
||||
current: User | None = None,
|
||||
) -> WeiboAccount:
|
||||
query = db.query(WeiboAccount).filter(WeiboAccount.id == account_id)
|
||||
if current is not None:
|
||||
query = _visible_weibo_accounts_query(query, current)
|
||||
account = query.first()
|
||||
if account is None:
|
||||
raise HTTPException(status_code=404, detail="微博账号不存在")
|
||||
return account
|
||||
|
||||
|
||||
def _account_out(account: WeiboAccount) -> WeiboAccountOut:
|
||||
return WeiboAccountOut.model_validate(account)
|
||||
return WeiboAccountOut.model_validate(account).model_copy(
|
||||
update={
|
||||
"assigned_username": (
|
||||
account.assigned_user.username if account.assigned_user else ""
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _apply_account_filters(
|
||||
@@ -98,6 +122,8 @@ def _apply_account_filters(
|
||||
tag: str,
|
||||
bind_status: str,
|
||||
enabled: bool | None,
|
||||
assign_status: str = "",
|
||||
assigned_user_id: int | None = None,
|
||||
):
|
||||
search_text = _query_str(search)
|
||||
if search_text:
|
||||
@@ -114,6 +140,14 @@ def _apply_account_filters(
|
||||
tag_value = _query_str(tag)
|
||||
if tag_value:
|
||||
query = query.filter(WeiboAccount.tag == tag_value)
|
||||
assign_value = _query_str(assign_status)
|
||||
if assign_value == "unassigned":
|
||||
query = query.filter(WeiboAccount.assigned_to.is_(None))
|
||||
elif assign_value == "assigned":
|
||||
query = query.filter(WeiboAccount.assigned_to.isnot(None))
|
||||
target_user_id = _int_or_none(assigned_user_id)
|
||||
if target_user_id is not None:
|
||||
query = query.filter(WeiboAccount.assigned_to == target_user_id)
|
||||
if isinstance(enabled, bool):
|
||||
query = query.filter(WeiboAccount.is_enabled.is_(enabled))
|
||||
bind_value = _query_str(bind_status)
|
||||
@@ -132,7 +166,10 @@ def _apply_account_filters(
|
||||
def _paginated_accounts(query, *, page: int, page_size: int) -> dict:
|
||||
total = query.order_by(None).count()
|
||||
rows = (
|
||||
query.options(defer(WeiboAccount.cookie))
|
||||
query.options(
|
||||
joinedload(WeiboAccount.assigned_user),
|
||||
defer(WeiboAccount.cookie),
|
||||
)
|
||||
.order_by(WeiboAccount.id.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
@@ -200,6 +237,8 @@ def list_accounts(
|
||||
tag: str = Query(""),
|
||||
bind_status: str = Query(""),
|
||||
enabled: bool | None = Query(None),
|
||||
assign_status: str = Query(""),
|
||||
assigned_user_id: int | None = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -209,11 +248,13 @@ def list_accounts(
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
query = _apply_account_filters(
|
||||
db.query(WeiboAccount),
|
||||
_visible_weibo_accounts_query(db.query(WeiboAccount), current),
|
||||
search=search,
|
||||
tag=tag,
|
||||
bind_status=bind_status,
|
||||
enabled=enabled,
|
||||
assign_status=assign_status,
|
||||
assigned_user_id=assigned_user_id,
|
||||
)
|
||||
return _paginated_accounts(query, page=page, page_size=page_size)
|
||||
|
||||
@@ -240,6 +281,8 @@ def list_workbench_accounts(
|
||||
)
|
||||
.filter(WeiboWorkbenchAccount.user_id == current.id)
|
||||
)
|
||||
if not _can_view_all_weibo_accounts(current):
|
||||
query = query.filter(WeiboAccount.assigned_to == current.id)
|
||||
query = _apply_account_filters(
|
||||
query,
|
||||
search=search,
|
||||
@@ -263,8 +306,11 @@ def list_workbench_pool(
|
||||
|
||||
_require_weibo_perm(current, "weibo:workbench")
|
||||
occupied = db.query(WeiboWorkbenchAccount.account_id)
|
||||
pool_query = db.query(WeiboAccount).filter(WeiboAccount.id.not_in(occupied))
|
||||
if not _can_view_all_weibo_accounts(current):
|
||||
pool_query = pool_query.filter(WeiboAccount.assigned_to == current.id)
|
||||
query = _apply_account_filters(
|
||||
db.query(WeiboAccount).filter(WeiboAccount.id.not_in(occupied)),
|
||||
pool_query,
|
||||
search=search,
|
||||
tag=tag,
|
||||
bind_status="",
|
||||
@@ -281,7 +327,7 @@ def list_workbench_tags(
|
||||
"""返回当前用户工作台中已有的标签。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:workbench")
|
||||
rows = (
|
||||
tag_query = (
|
||||
db.query(WeiboAccount.tag)
|
||||
.join(
|
||||
WeiboWorkbenchAccount,
|
||||
@@ -291,10 +337,10 @@ def list_workbench_tags(
|
||||
WeiboWorkbenchAccount.user_id == current.id,
|
||||
WeiboAccount.tag != "",
|
||||
)
|
||||
.distinct()
|
||||
.order_by(WeiboAccount.tag.asc())
|
||||
.all()
|
||||
)
|
||||
if not _can_view_all_weibo_accounts(current):
|
||||
tag_query = tag_query.filter(WeiboAccount.assigned_to == current.id)
|
||||
rows = tag_query.distinct().order_by(WeiboAccount.tag.asc()).all()
|
||||
return {"tags": [tag for (tag,) in rows if tag]}
|
||||
|
||||
|
||||
@@ -321,6 +367,24 @@ def add_workbench_accounts(
|
||||
missing = sorted(set(account_ids) - existing_ids)
|
||||
if missing:
|
||||
raise HTTPException(status_code=404, detail=f"账号不存在: {missing}")
|
||||
if not _can_view_all_weibo_accounts(current):
|
||||
unavailable = {
|
||||
account_id
|
||||
for (account_id,) in db.query(WeiboAccount.id)
|
||||
.filter(
|
||||
WeiboAccount.id.in_(account_ids),
|
||||
or_(
|
||||
WeiboAccount.assigned_to.is_(None),
|
||||
WeiboAccount.assigned_to != current.id,
|
||||
),
|
||||
)
|
||||
.all()
|
||||
}
|
||||
if unavailable:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="只能使用分配给自己的微博账号",
|
||||
)
|
||||
taken = {
|
||||
account_id
|
||||
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
|
||||
@@ -410,7 +474,11 @@ def batch_tag_accounts(
|
||||
account_ids = sorted(set(req.ids))
|
||||
if not account_ids:
|
||||
raise HTTPException(status_code=400, detail="请先选择账号")
|
||||
accounts = db.query(WeiboAccount).filter(WeiboAccount.id.in_(account_ids)).all()
|
||||
accounts = (
|
||||
_visible_weibo_accounts_query(db.query(WeiboAccount), current)
|
||||
.filter(WeiboAccount.id.in_(account_ids))
|
||||
.all()
|
||||
)
|
||||
actual_ids = {account.id for account in accounts}
|
||||
missing = sorted(set(account_ids) - actual_ids)
|
||||
if missing:
|
||||
@@ -434,6 +502,109 @@ def batch_tag_accounts(
|
||||
return {"updated": len(accounts), "tag": tag, "success": True}
|
||||
|
||||
|
||||
@router.post("/accounts/batch-assign")
|
||||
def batch_assign_accounts(
|
||||
req: WeiboBatchAssignRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量分配微博账号给客服,或取消分配。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:assign")
|
||||
account_ids = sorted(set(req.account_ids))
|
||||
if not account_ids:
|
||||
raise HTTPException(status_code=400, detail="请先选择账号")
|
||||
accounts = db.query(WeiboAccount).filter(WeiboAccount.id.in_(account_ids)).all()
|
||||
actual_ids = {account.id for account in accounts}
|
||||
missing = sorted(set(account_ids) - actual_ids)
|
||||
if missing:
|
||||
raise HTTPException(status_code=404, detail=f"账号不存在: {missing}")
|
||||
target = None
|
||||
if req.assigned_to is not None:
|
||||
target = (
|
||||
db.query(User)
|
||||
.filter(
|
||||
User.id == req.assigned_to,
|
||||
User.role == "support",
|
||||
User.is_active.is_(True),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail="目标客服不存在或已停用")
|
||||
previous_assignments = {account.id: account.assigned_to for account in accounts}
|
||||
if target is None:
|
||||
db.query(WeiboWorkbenchAccount).filter(
|
||||
WeiboWorkbenchAccount.account_id.in_(actual_ids)
|
||||
).delete(synchronize_session=False)
|
||||
else:
|
||||
db.query(WeiboWorkbenchAccount).filter(
|
||||
WeiboWorkbenchAccount.account_id.in_(actual_ids),
|
||||
WeiboWorkbenchAccount.user_id != target.id,
|
||||
).delete(synchronize_session=False)
|
||||
for account in accounts:
|
||||
account.assigned_to = target.id if target else None
|
||||
db.commit()
|
||||
target_name = target.username if target else ""
|
||||
_record_audit_safely(
|
||||
db,
|
||||
current,
|
||||
action="weibo:account:assign",
|
||||
target=f"weibo_accounts:{len(account_ids)}",
|
||||
detail={
|
||||
"account_ids": account_ids,
|
||||
"assigned_to": target.id if target else None,
|
||||
"assigned_username": target_name,
|
||||
"previous_assignments": previous_assignments,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"updated": len(accounts),
|
||||
"assigned_to": target.id if target else None,
|
||||
"assigned_username": target_name,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/assignments/summary")
|
||||
def assignments_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""微博账号分配概览。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:assign")
|
||||
rows = (
|
||||
db.query(User.id, User.username, func.count(WeiboAccount.id))
|
||||
.outerjoin(WeiboAccount, WeiboAccount.assigned_to == User.id)
|
||||
.filter(
|
||||
User.role == "support",
|
||||
User.is_active.is_(True),
|
||||
)
|
||||
.group_by(User.id, User.username)
|
||||
.order_by(User.username.asc())
|
||||
.all()
|
||||
)
|
||||
users = [
|
||||
{
|
||||
"id": user_id,
|
||||
"username": username,
|
||||
"assigned_count": assigned_count,
|
||||
}
|
||||
for user_id, username, assigned_count in rows
|
||||
]
|
||||
total_count = db.query(WeiboAccount).count()
|
||||
unassigned_count = (
|
||||
db.query(WeiboAccount).filter(WeiboAccount.assigned_to.is_(None)).count()
|
||||
)
|
||||
return {
|
||||
"total_count": total_count,
|
||||
"assigned_count": total_count - unassigned_count,
|
||||
"unassigned_count": unassigned_count,
|
||||
"support_users": users,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/accounts/tags")
|
||||
def list_account_tags(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -442,9 +613,9 @@ def list_account_tags(
|
||||
"""返回全局账号池已有的标签。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
tag_query = _visible_weibo_accounts_query(db.query(WeiboAccount.tag), current)
|
||||
rows = (
|
||||
db.query(WeiboAccount.tag)
|
||||
.filter(WeiboAccount.tag != "")
|
||||
tag_query.filter(WeiboAccount.tag != "")
|
||||
.distinct()
|
||||
.order_by(WeiboAccount.tag.asc())
|
||||
.all()
|
||||
@@ -452,6 +623,38 @@ def list_account_tags(
|
||||
return {"tags": [tag for (tag,) in rows if tag]}
|
||||
|
||||
|
||||
@router.get("/accounts/ids")
|
||||
def list_account_ids(
|
||||
search: str = Query(""),
|
||||
tag: str = Query(""),
|
||||
bind_status: str = Query(""),
|
||||
enabled: bool | None = Query(None),
|
||||
assign_status: str = Query(""),
|
||||
assigned_user_id: int | None = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""返回当前用户可见且符合筛选条件的全部微博账号 ID。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
query = _apply_account_filters(
|
||||
_visible_weibo_accounts_query(db.query(WeiboAccount), current),
|
||||
search=search,
|
||||
tag=tag,
|
||||
bind_status=bind_status,
|
||||
enabled=enabled,
|
||||
assign_status=assign_status,
|
||||
assigned_user_id=assigned_user_id,
|
||||
)
|
||||
account_ids = [
|
||||
account_id
|
||||
for (account_id,) in query.with_entities(WeiboAccount.id)
|
||||
.order_by(WeiboAccount.id.desc())
|
||||
.all()
|
||||
]
|
||||
return {"account_ids": account_ids, "total": len(account_ids)}
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}", response_model=WeiboAccountOut)
|
||||
def get_account(
|
||||
account_id: int,
|
||||
@@ -461,7 +664,7 @@ def get_account(
|
||||
"""查看一个微博账号的本地资料。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
return _account_out(_get_account(db, account_id))
|
||||
return _account_out(_get_account(db, account_id, current))
|
||||
|
||||
|
||||
@router.put("/accounts/{account_id}", response_model=WeiboAccountOut)
|
||||
@@ -474,7 +677,7 @@ def update_account(
|
||||
"""更新账号的人工标签和备注。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:manage")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
payload = req.model_dump(exclude_unset=True)
|
||||
changes = update_weibo_account_metadata(
|
||||
db,
|
||||
@@ -503,7 +706,7 @@ def disable_account(
|
||||
"""停用账号,保留 CK 和历史资料。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:manage")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
disable_weibo_account(db, account, reason=req.reason)
|
||||
_record_audit_safely(
|
||||
db,
|
||||
@@ -528,7 +731,7 @@ def enable_account(
|
||||
"""重新检测 CK 与支付风险,通过后启用账号。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:manage")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_refresh_or_http(account, db)
|
||||
enable_weibo_account(db, account)
|
||||
_record_audit_safely(
|
||||
@@ -557,10 +760,21 @@ def import_cookies(
|
||||
WEIBO_DEFAULT_ACTIVITY,
|
||||
req.update_existing,
|
||||
current.id,
|
||||
can_update_all=_can_view_all_weibo_accounts(current),
|
||||
)
|
||||
typed_errors = [
|
||||
WeiboImportError(line=item.line, message=item.message) for item in errors
|
||||
]
|
||||
if accounts and not _can_view_all_weibo_accounts(current):
|
||||
changed = False
|
||||
for account in accounts:
|
||||
if account.assigned_to != current.id:
|
||||
account.assigned_to = current.id
|
||||
changed = True
|
||||
if changed:
|
||||
db.commit()
|
||||
for account in accounts:
|
||||
db.refresh(account)
|
||||
if accounts:
|
||||
_record_audit_safely(
|
||||
db,
|
||||
@@ -598,7 +812,7 @@ def refresh_account(
|
||||
"""立即刷新一个微博账号的 CK 状态和资料。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
_refresh_or_http(account, db)
|
||||
_record_audit_safely(
|
||||
@@ -620,7 +834,7 @@ def copy_account_ck(
|
||||
"""复制已导入的微博 CK 明文;只走单独权限并保留审计。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:account:ck_export")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
ck_text = (account.cookie or "").strip()
|
||||
if not ck_text:
|
||||
raise HTTPException(status_code=409, detail="该账号没有保存 CK")
|
||||
@@ -649,7 +863,7 @@ def create_bind_qrcodes(
|
||||
|
||||
_require_weibo_perm(current, "weibo:bind")
|
||||
selected_channel = channel.strip() if isinstance(channel, str) else ""
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
snapshot = _refresh_or_http(account, db)
|
||||
if snapshot.is_bound and not snapshot.can_rebind:
|
||||
@@ -726,7 +940,7 @@ def list_mall_goods(
|
||||
"""手动查询微豆商城商品列表。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_mall(
|
||||
@@ -779,7 +993,7 @@ def exchange_goods(
|
||||
_require_weibo_perm(current, "weibo:exchange")
|
||||
if not req.confirm:
|
||||
raise HTTPException(status_code=400, detail="请先确认兑换商品")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
result = exchange_weibo_goods(
|
||||
@@ -837,7 +1051,7 @@ def list_exchange_records(
|
||||
"""手动查询微豆兑换记录。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_exchange_records(
|
||||
@@ -886,7 +1100,7 @@ def list_point_history(
|
||||
"""手动查询微豆获取和充值明细。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_point_history(
|
||||
@@ -936,7 +1150,7 @@ def create_recharge_order(
|
||||
"""手动创建助威充值订单并返回支付二维码。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:recharge")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
order = create_weibo_recharge_order(
|
||||
@@ -984,7 +1198,7 @@ def get_recharge_status(
|
||||
"""手动查询支付状态;支付动作仍由用户在微信/支付宝完成。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:recharge")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
status = query_weibo_payment_status(db, account, pay_id=req.pay_id)
|
||||
@@ -1007,7 +1221,7 @@ def delete_account(
|
||||
"""删除一个微博账号。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:delete")
|
||||
account = _get_account(db, account_id)
|
||||
account = _get_account(db, account_id, current)
|
||||
uid = account.uid
|
||||
db.delete(account)
|
||||
db.commit()
|
||||
|
||||
@@ -483,6 +483,8 @@ class WeiboAccountOut(BaseModel):
|
||||
tag: str = ""
|
||||
remark: str = ""
|
||||
activity: str = ""
|
||||
assigned_to: int | None = None
|
||||
assigned_username: str = ""
|
||||
is_enabled: bool = True
|
||||
disabled_at: datetime | None = None
|
||||
disabled_reason: str = ""
|
||||
@@ -515,6 +517,11 @@ class WeiboWorkbenchAccountsRequest(BaseModel):
|
||||
account_ids: list[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboBatchAssignRequest(BaseModel):
|
||||
account_ids: list[int] = Field(min_length=1)
|
||||
assigned_to: int | None = None
|
||||
|
||||
|
||||
class WeiboAccountDisableRequest(BaseModel):
|
||||
reason: str = Field("", max_length=256)
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ def import_weibo_cookies(
|
||||
activity: str = "",
|
||||
update_existing: bool = False,
|
||||
current_user_id: int | None = None,
|
||||
can_update_all: bool = False,
|
||||
) -> tuple[list[WeiboAccount], list[WeiboImportIssue], int]:
|
||||
"""逐行验证并导入微博 CK。
|
||||
|
||||
@@ -98,6 +99,18 @@ 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",
|
||||
)
|
||||
)
|
||||
continue
|
||||
owner_id = (
|
||||
db.query(WeiboWorkbenchAccount.user_id)
|
||||
.filter(WeiboWorkbenchAccount.account_id == account.id)
|
||||
|
||||
Reference in New Issue
Block a user