1482 lines
47 KiB
Python
1482 lines
47 KiB
Python
"""微博微豆:CK 导入、账号刷新与绑定二维码。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from loguru import logger
|
||
from sqlalchemy import func, or_
|
||
from sqlalchemy.exc import IntegrityError
|
||
from sqlalchemy.orm import Session, defer, joinedload
|
||
|
||
from core.weibo import WeiboAuthError, WeiboError, WeiboExchangeUncertainError
|
||
|
||
from ..database import get_db
|
||
from ..deps import get_current_user
|
||
from ..models import User, WeiboAccount, WeiboWorkbenchAccount
|
||
from ..permissions import user_has_permission
|
||
from ..schemas import (
|
||
WeiboAccountBatchTagRequest,
|
||
WeiboAccountCkOut,
|
||
WeiboAccountDisableRequest,
|
||
WeiboAccountOut,
|
||
WeiboAccountUpdate,
|
||
WeiboBatchAssignRequest,
|
||
WeiboBindQrOut,
|
||
WeiboBindSchemeOut,
|
||
WeiboCookieImport,
|
||
WeiboExchangeOut,
|
||
WeiboExchangeRecordItem,
|
||
WeiboExchangeRecordsOut,
|
||
WeiboExchangeRequest,
|
||
WeiboGoodsOut,
|
||
WeiboImportDuplicateOut,
|
||
WeiboImportError,
|
||
WeiboImportResult,
|
||
WeiboMallListOut,
|
||
WeiboPaymentStatusOut,
|
||
WeiboPaymentStatusRequest,
|
||
WeiboPointHistoryOut,
|
||
WeiboPointRecordItem,
|
||
WeiboRechargeOrderOut,
|
||
WeiboRechargeRequest,
|
||
WeiboWorkbenchAccountsRequest,
|
||
WeiboWorkbenchBatchImportItem,
|
||
WeiboWorkbenchBatchImportOut,
|
||
WeiboWorkbenchBatchImportRequest,
|
||
WeiboWorkbenchTransferToCollabOut,
|
||
WeiboWorkbenchTransferToCollabRequest,
|
||
)
|
||
from ..services.audit_service import record_audit
|
||
from ..services.weibo_service import (
|
||
create_weibo_recharge_order,
|
||
disable_weibo_account,
|
||
enable_weibo_account,
|
||
exchange_weibo_goods,
|
||
generate_weibo_bind_qrcode,
|
||
generate_weibo_bind_schemes,
|
||
import_weibo_cookies,
|
||
query_weibo_exchange_records,
|
||
query_weibo_mall,
|
||
query_weibo_payment_status,
|
||
query_weibo_point_history,
|
||
refresh_weibo_account,
|
||
update_weibo_account_metadata,
|
||
)
|
||
|
||
router = APIRouter(prefix="/api/weibo", tags=["微博微豆"])
|
||
WEIBO_DEFAULT_ACTIVITY = os.getenv("WEIBO_DEFAULT_ACTIVITY", "").strip()
|
||
WEIBO_WORKBENCH_BATCH_IMPORT_MAX = 200
|
||
|
||
|
||
def _require_weibo_perm(user: User, permission: str) -> None:
|
||
if not user_has_permission(user, permission):
|
||
raise HTTPException(status_code=403, detail="权限不足")
|
||
|
||
|
||
def _query_str(value) -> str:
|
||
return value.strip() if isinstance(value, str) else ""
|
||
|
||
|
||
def _int_or_none(value: object) -> int | None:
|
||
if value is None or value == "":
|
||
return None
|
||
try:
|
||
return int(str(value))
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
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).model_copy(
|
||
update={
|
||
"assigned_username": (
|
||
account.assigned_user.username if account.assigned_user else ""
|
||
)
|
||
}
|
||
)
|
||
|
||
|
||
def _apply_account_filters(
|
||
query,
|
||
*,
|
||
search: str,
|
||
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:
|
||
pattern = f"%{search_text}%"
|
||
query = query.filter(
|
||
or_(
|
||
WeiboAccount.uid.ilike(pattern),
|
||
WeiboAccount.nickname.ilike(pattern),
|
||
WeiboAccount.game_role_name.ilike(pattern),
|
||
WeiboAccount.tag.ilike(pattern),
|
||
WeiboAccount.remark.ilike(pattern),
|
||
)
|
||
)
|
||
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)
|
||
if bind_value == "unbound":
|
||
query = query.filter(WeiboAccount.is_bound.is_(False))
|
||
elif bind_value == "bound":
|
||
query = query.filter(WeiboAccount.is_bound.is_(True))
|
||
elif bind_value == "rebindable":
|
||
query = query.filter(
|
||
WeiboAccount.is_bound.is_(True),
|
||
WeiboAccount.can_rebind.is_(True),
|
||
)
|
||
return query
|
||
|
||
|
||
def _paginated_accounts(query, *, page: int, page_size: int) -> dict:
|
||
total = query.order_by(None).count()
|
||
rows = (
|
||
query.options(
|
||
joinedload(WeiboAccount.assigned_user),
|
||
defer(WeiboAccount.cookie),
|
||
)
|
||
.order_by(WeiboAccount.id.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
return {
|
||
"items": [_account_out(account) for account in rows],
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size,
|
||
}
|
||
|
||
|
||
def _ensure_enabled(account: WeiboAccount) -> None:
|
||
if not account.is_enabled:
|
||
raise HTTPException(status_code=409, detail="微博账号已停用,请先启用")
|
||
|
||
|
||
def _refresh_or_http(account: WeiboAccount, db: Session):
|
||
try:
|
||
return refresh_weibo_account(
|
||
db,
|
||
account,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except WeiboAuthError as exc:
|
||
raise HTTPException(status_code=400, detail=f"CK 已失效: {exc}") from exc
|
||
except WeiboError as exc:
|
||
raise HTTPException(status_code=502, detail=f"刷新微博账号失败: {exc}") from exc
|
||
|
||
|
||
def _manual_http_error(exc: Exception) -> HTTPException:
|
||
if isinstance(exc, WeiboAuthError):
|
||
return HTTPException(status_code=400, detail=f"CK 已失效: {exc}")
|
||
return HTTPException(status_code=502, detail=str(exc))
|
||
|
||
|
||
def _record_audit_safely(
|
||
db: Session,
|
||
current: User,
|
||
*,
|
||
action: str,
|
||
target: str,
|
||
detail: dict,
|
||
) -> None:
|
||
"""审计是旁路,失败只记 warning,不回滚已经完成的账号操作。"""
|
||
|
||
try:
|
||
record_audit(
|
||
db,
|
||
current,
|
||
action=action,
|
||
target=target,
|
||
detail=detail,
|
||
)
|
||
db.commit()
|
||
except Exception as exc: # noqa: BLE001 旁路写入不能带崩主流程
|
||
db.rollback()
|
||
logger.warning(f"记录微博审计失败: {exc}")
|
||
|
||
|
||
@router.get("/accounts")
|
||
def list_accounts(
|
||
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),
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=200),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""查看已导入的微博账号。"""
|
||
|
||
_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,
|
||
)
|
||
return _paginated_accounts(query, page=page, page_size=page_size)
|
||
|
||
|
||
@router.get("/workbench/accounts")
|
||
def list_workbench_accounts(
|
||
search: str = Query(""),
|
||
tag: str = Query(""),
|
||
bind_status: str = Query(""),
|
||
enabled: bool | None = Query(None),
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=200),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""查看当前用户个人工作台中的微博账号。"""
|
||
|
||
_require_weibo_perm(current, "weibo:workbench")
|
||
query = (
|
||
db.query(WeiboAccount)
|
||
.join(
|
||
WeiboWorkbenchAccount,
|
||
WeiboWorkbenchAccount.account_id == WeiboAccount.id,
|
||
)
|
||
.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,
|
||
tag=tag,
|
||
bind_status=bind_status,
|
||
enabled=enabled,
|
||
)
|
||
return _paginated_accounts(query, page=page, page_size=page_size)
|
||
|
||
|
||
@router.get("/workbench/pool")
|
||
def list_workbench_pool(
|
||
search: str = Query(""),
|
||
tag: str = Query(""),
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=200),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""查看可加入当前用户工作台、且尚未加入的账号池账号。"""
|
||
|
||
_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(
|
||
pool_query,
|
||
search=search,
|
||
tag=tag,
|
||
bind_status="",
|
||
enabled=True,
|
||
)
|
||
return _paginated_accounts(query, page=page, page_size=page_size)
|
||
|
||
|
||
@router.post(
|
||
"/workbench/accounts/batch-import",
|
||
response_model=WeiboWorkbenchBatchImportOut,
|
||
)
|
||
def import_workbench_accounts_by_text(
|
||
req: WeiboWorkbenchBatchImportRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""按 UID/用户名/角色批量解析账号并加入当前用户工作台。"""
|
||
|
||
_require_weibo_perm(current, "weibo:workbench")
|
||
identifiers: list[str] = []
|
||
for token in re.split(r"[\s,,;;]+", req.text.strip()):
|
||
value = token.strip()
|
||
if value and value not in identifiers:
|
||
identifiers.append(value)
|
||
if not identifiers:
|
||
raise HTTPException(status_code=400, detail="请先粘贴微博用户名或 UID")
|
||
if len(identifiers) > WEIBO_WORKBENCH_BATCH_IMPORT_MAX:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"一次最多导入 {WEIBO_WORKBENCH_BATCH_IMPORT_MAX} 个账号",
|
||
)
|
||
|
||
visible_query = _visible_weibo_accounts_query(db.query(WeiboAccount), current)
|
||
accounts = visible_query.filter(
|
||
or_(
|
||
WeiboAccount.uid.in_(identifiers),
|
||
WeiboAccount.nickname.in_(identifiers),
|
||
WeiboAccount.game_role_name.in_(identifiers),
|
||
)
|
||
).all()
|
||
|
||
def grouped(field: str) -> dict[str, list[WeiboAccount]]:
|
||
result: dict[str, list[WeiboAccount]] = {}
|
||
for account in accounts:
|
||
value = str(getattr(account, field) or "").strip()
|
||
if value:
|
||
result.setdefault(value, []).append(account)
|
||
return result
|
||
|
||
by_uid = grouped("uid")
|
||
by_nickname = grouped("nickname")
|
||
by_role = grouped("game_role_name")
|
||
account_ids = [account.id for account in accounts]
|
||
occupancy = {
|
||
account_id: user_id
|
||
for account_id, user_id in db.query(
|
||
WeiboWorkbenchAccount.account_id,
|
||
WeiboWorkbenchAccount.user_id,
|
||
)
|
||
.filter(WeiboWorkbenchAccount.account_id.in_(account_ids))
|
||
.all()
|
||
}
|
||
|
||
items: list[WeiboWorkbenchBatchImportItem] = []
|
||
to_add: list[WeiboAccount] = []
|
||
planned_ids: set[int] = set()
|
||
for identifier in identifiers:
|
||
candidates = by_uid.get(identifier) or by_nickname.get(identifier) or []
|
||
if not candidates:
|
||
candidates = by_role.get(identifier) or []
|
||
if not candidates:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
identifier=identifier,
|
||
status="not_found",
|
||
message="当前可见账号池中没有匹配项",
|
||
)
|
||
)
|
||
continue
|
||
if len(candidates) > 1 and identifier not in by_uid:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
identifier=identifier,
|
||
status="ambiguous",
|
||
message=f"匹配到 {len(candidates)} 个账号,请改用 UID",
|
||
)
|
||
)
|
||
continue
|
||
|
||
account = candidates[0]
|
||
base = {
|
||
"identifier": identifier,
|
||
"account_id": account.id,
|
||
"uid": account.uid,
|
||
"nickname": account.nickname,
|
||
}
|
||
occupant = occupancy.get(account.id)
|
||
if not account.is_enabled:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
status="disabled",
|
||
message="账号已停用",
|
||
**base,
|
||
)
|
||
)
|
||
elif occupant is not None and occupant != current.id:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
status="unavailable",
|
||
message="账号已在其他人的工作台中使用",
|
||
**base,
|
||
)
|
||
)
|
||
elif occupant == current.id:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
status="already_added",
|
||
message="账号已在本工作台",
|
||
**base,
|
||
)
|
||
)
|
||
elif account.id in planned_ids:
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
status="duplicate",
|
||
message="同一账号在本次名单中重复",
|
||
**base,
|
||
)
|
||
)
|
||
else:
|
||
planned_ids.add(account.id)
|
||
to_add.append(account)
|
||
items.append(
|
||
WeiboWorkbenchBatchImportItem(
|
||
status="added",
|
||
message="已加入工作台",
|
||
**base,
|
||
)
|
||
)
|
||
|
||
try:
|
||
db.add_all(
|
||
[
|
||
WeiboWorkbenchAccount(user_id=current.id, account_id=account.id)
|
||
for account in to_add
|
||
]
|
||
)
|
||
db.commit()
|
||
except IntegrityError as exc:
|
||
db.rollback()
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail="部分账号刚被其他工作台占用,请刷新后重试",
|
||
) from exc
|
||
if to_add:
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:workbench:batch_import",
|
||
target=f"weibo_user:{current.id}",
|
||
detail={"account_ids": [account.id for account in to_add]},
|
||
)
|
||
|
||
added = sum(item.status == "added" for item in items)
|
||
already_added = sum(item.status == "already_added" for item in items)
|
||
failed = len(items) - added - already_added
|
||
return WeiboWorkbenchBatchImportOut(
|
||
requested=len(identifiers),
|
||
added=added,
|
||
already_added=already_added,
|
||
failed=failed,
|
||
account_ids=[account.id for account in to_add],
|
||
items=items,
|
||
)
|
||
|
||
|
||
@router.get("/workbench/tags")
|
||
def list_workbench_tags(
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""返回当前用户工作台中已有的标签。"""
|
||
|
||
_require_weibo_perm(current, "weibo:workbench")
|
||
tag_query = (
|
||
db.query(WeiboAccount.tag)
|
||
.join(
|
||
WeiboWorkbenchAccount,
|
||
WeiboWorkbenchAccount.account_id == WeiboAccount.id,
|
||
)
|
||
.filter(
|
||
WeiboWorkbenchAccount.user_id == current.id,
|
||
WeiboAccount.tag != "",
|
||
)
|
||
)
|
||
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]}
|
||
|
||
|
||
@router.post("/workbench/accounts")
|
||
def add_workbench_accounts(
|
||
req: WeiboWorkbenchAccountsRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""把账号池账号加入当前用户的工作台。"""
|
||
|
||
_require_weibo_perm(current, "weibo:workbench")
|
||
account_ids = sorted(
|
||
{account_id for account_id in req.account_ids if account_id > 0}
|
||
)
|
||
if not account_ids:
|
||
raise HTTPException(status_code=400, detail="请先选择账号")
|
||
existing_ids = {
|
||
account_id
|
||
for (account_id,) in db.query(WeiboAccount.id)
|
||
.filter(WeiboAccount.id.in_(account_ids))
|
||
.all()
|
||
}
|
||
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)
|
||
.filter(
|
||
WeiboWorkbenchAccount.user_id != current.id,
|
||
WeiboWorkbenchAccount.account_id.in_(account_ids),
|
||
)
|
||
.all()
|
||
}
|
||
if taken:
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail="部分账号已在其他人的工作台中使用,请先让原使用人移出",
|
||
)
|
||
selected_ids = {
|
||
account_id
|
||
for (account_id,) in db.query(WeiboWorkbenchAccount.account_id)
|
||
.filter(
|
||
WeiboWorkbenchAccount.user_id == current.id,
|
||
WeiboWorkbenchAccount.account_id.in_(account_ids),
|
||
)
|
||
.all()
|
||
}
|
||
db.add_all(
|
||
[
|
||
WeiboWorkbenchAccount(user_id=current.id, account_id=account_id)
|
||
for account_id in account_ids
|
||
if account_id not in selected_ids
|
||
]
|
||
)
|
||
try:
|
||
db.commit()
|
||
except IntegrityError as exc:
|
||
db.rollback()
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail="账号已被其他工作台占用,请刷新后重试",
|
||
) from exc
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:workbench:add",
|
||
target=f"weibo_user:{current.id}",
|
||
detail={"account_ids": account_ids},
|
||
)
|
||
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,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""从当前用户工作台移出账号,不影响账号池和其他用户。"""
|
||
|
||
_require_weibo_perm(current, "weibo:workbench")
|
||
account_ids = sorted(
|
||
{account_id for account_id in req.account_ids if account_id > 0}
|
||
)
|
||
if not account_ids:
|
||
raise HTTPException(status_code=400, detail="请先选择账号")
|
||
db.query(WeiboWorkbenchAccount).filter(
|
||
WeiboWorkbenchAccount.user_id == current.id,
|
||
WeiboWorkbenchAccount.account_id.in_(account_ids),
|
||
).delete(synchronize_session=False)
|
||
db.commit()
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:workbench:remove",
|
||
target=f"weibo_user:{current.id}",
|
||
detail={"account_ids": account_ids},
|
||
)
|
||
return {"account_ids": account_ids, "success": True}
|
||
|
||
|
||
@router.post("/accounts/batch-tag")
|
||
def batch_tag_accounts(
|
||
req: WeiboAccountBatchTagRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""批量设置微博账号标签。"""
|
||
|
||
_require_weibo_perm(current, "weibo:manage")
|
||
account_ids = sorted(set(req.ids))
|
||
if not account_ids:
|
||
raise HTTPException(status_code=400, detail="请先选择账号")
|
||
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:
|
||
raise HTTPException(status_code=404, detail=f"账号不存在: {missing}")
|
||
tag = req.tag.strip()
|
||
previous = {account.id: account.tag or "" for account in accounts}
|
||
for account in accounts:
|
||
account.tag = tag
|
||
db.commit()
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:batch_tag",
|
||
target=f"weibo_accounts:{len(account_ids)}",
|
||
detail={
|
||
"account_ids": account_ids,
|
||
"tag": tag,
|
||
"previous_tags": previous,
|
||
},
|
||
)
|
||
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),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""返回全局账号池已有的标签。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
tag_query = _visible_weibo_accounts_query(db.query(WeiboAccount.tag), current)
|
||
rows = (
|
||
tag_query.filter(WeiboAccount.tag != "")
|
||
.distinct()
|
||
.order_by(WeiboAccount.tag.asc())
|
||
.all()
|
||
)
|
||
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,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""查看一个微博账号的本地资料。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
return _account_out(_get_account(db, account_id, current))
|
||
|
||
|
||
@router.put("/accounts/{account_id}", response_model=WeiboAccountOut)
|
||
def update_account(
|
||
account_id: int,
|
||
req: WeiboAccountUpdate,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""更新账号的人工标签和备注。"""
|
||
|
||
_require_weibo_perm(current, "weibo:manage")
|
||
account = _get_account(db, account_id, current)
|
||
payload = req.model_dump(exclude_unset=True)
|
||
changes = update_weibo_account_metadata(
|
||
db,
|
||
account,
|
||
tag=payload.get("tag"),
|
||
remark=payload.get("remark"),
|
||
)
|
||
if changes:
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:update",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={"account_id": account.id, "uid": account.uid, "changes": changes},
|
||
)
|
||
return _account_out(account)
|
||
|
||
|
||
@router.post("/accounts/{account_id}/disable", response_model=WeiboAccountOut)
|
||
def disable_account(
|
||
account_id: int,
|
||
req: WeiboAccountDisableRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""停用账号,保留 CK 和历史资料。"""
|
||
|
||
_require_weibo_perm(current, "weibo:manage")
|
||
account = _get_account(db, account_id, current)
|
||
disable_weibo_account(db, account, reason=req.reason)
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:disable",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={
|
||
"account_id": account.id,
|
||
"uid": account.uid,
|
||
"reason": account.disabled_reason,
|
||
},
|
||
)
|
||
return _account_out(account)
|
||
|
||
|
||
@router.post("/accounts/{account_id}/enable", response_model=WeiboAccountOut)
|
||
def enable_account(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""重新检测 CK 与支付风险,通过后启用账号。"""
|
||
|
||
_require_weibo_perm(current, "weibo:manage")
|
||
account = _get_account(db, account_id, current)
|
||
_refresh_or_http(account, db)
|
||
enable_weibo_account(db, account)
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:enable",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={"account_id": account.id, "uid": account.uid},
|
||
)
|
||
return _account_out(account)
|
||
|
||
|
||
@router.post("/accounts/import-cookies", response_model=WeiboImportResult)
|
||
def import_cookies(
|
||
req: WeiboCookieImport,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""导入微博 CK;每个非空行一条完整 Cookie。"""
|
||
|
||
_require_weibo_perm(current, "weibo:import")
|
||
accounts, errors, skipped, duplicates = import_weibo_cookies(
|
||
db,
|
||
req.text,
|
||
req.tag,
|
||
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
|
||
]
|
||
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:
|
||
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,
|
||
current,
|
||
action="weibo:account:import",
|
||
target=f"count:{len(accounts)}",
|
||
detail={
|
||
"uids": [account.uid for account in accounts],
|
||
"count": len(accounts),
|
||
"skipped": skipped,
|
||
"duplicate_uids": [item.uid for item in duplicates],
|
||
"errors": len(errors),
|
||
},
|
||
)
|
||
message = f"成功导入 {len(accounts)} 个微博账号"
|
||
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],
|
||
)
|
||
|
||
|
||
@router.post("/accounts/{account_id}/refresh", response_model=WeiboAccountOut)
|
||
def refresh_account(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""立即刷新一个微博账号的 CK 状态和资料。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
_refresh_or_http(account, db)
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:refresh",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={"account_id": account.id, "uid": account.uid},
|
||
)
|
||
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, current)
|
||
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,
|
||
channel: str = Query("", pattern="^(wx|qq)?$"),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""生成微信和 QQ 绑定 Scheme,供前端直接渲染二维码。"""
|
||
|
||
_require_weibo_perm(current, "weibo:bind")
|
||
selected_channel = channel.strip() if isinstance(channel, str) else ""
|
||
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:
|
||
detail = (
|
||
f"账号已绑定角色「{snapshot.role_name}」,当前不可换绑"
|
||
if snapshot.role_name
|
||
else "账号已绑定角色,当前不可换绑"
|
||
)
|
||
if snapshot.rebind_remain_days:
|
||
detail += f",还需等待 {snapshot.rebind_remain_days} 天"
|
||
raise HTTPException(status_code=409, detail=detail)
|
||
if not snapshot.bind_url:
|
||
raise HTTPException(status_code=409, detail="微博没有返回当前账号的绑定链接")
|
||
|
||
if selected_channel:
|
||
items = [
|
||
generate_weibo_bind_qrcode(account, snapshot.bind_url, selected_channel)
|
||
]
|
||
else:
|
||
# 不传 channel 时保留旧接口:一次返回微信、QQ 两个 Scheme。
|
||
items = generate_weibo_bind_schemes(account, snapshot.bind_url)
|
||
schemes = [WeiboBindSchemeOut(**item) for item in items]
|
||
successful = [
|
||
item
|
||
for item in schemes
|
||
if (item.qrcode_image if selected_channel else item.scheme)
|
||
]
|
||
if not successful:
|
||
prefix = (
|
||
f"{schemes[0].label}绑定二维码生成失败"
|
||
if selected_channel and schemes
|
||
else "微信和 QQ 绑定二维码都生成失败"
|
||
)
|
||
raise HTTPException(
|
||
status_code=502,
|
||
detail=prefix
|
||
+ ": "
|
||
+ "; ".join(f"{item.label}={item.error}" for item in schemes if item.error),
|
||
)
|
||
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:bind_qrcode",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={
|
||
"account_id": account.id,
|
||
"uid": account.uid,
|
||
"channels": [
|
||
{
|
||
"channel": item.channel,
|
||
"success": bool(item.scheme),
|
||
"error": item.error,
|
||
}
|
||
for item in schemes
|
||
],
|
||
},
|
||
)
|
||
return WeiboBindQrOut(
|
||
account_id=account.id,
|
||
is_bound=snapshot.is_bound,
|
||
can_rebind=snapshot.can_rebind,
|
||
rebind_remain_days=snapshot.rebind_remain_days,
|
||
schemes=schemes,
|
||
)
|
||
|
||
|
||
@router.post("/accounts/{account_id}/mall-list", response_model=WeiboMallListOut)
|
||
def list_mall_goods(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动查询微豆商城商品列表。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
data = query_weibo_mall(
|
||
db,
|
||
account,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
goods: list[WeiboGoodsOut] = []
|
||
for item in data.get("list") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
try:
|
||
goods_id = int(item.get("id") or 0)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if goods_id <= 0:
|
||
continue
|
||
goods.append(
|
||
WeiboGoodsOut(
|
||
id=goods_id,
|
||
goods_name=str(item.get("goods_name") or ""),
|
||
goods_image=str(item.get("goods_image") or ""),
|
||
price=_int_or_none(item.get("price")) or 0,
|
||
remain_count=_int_or_none(item.get("remain_count")),
|
||
is_sold_out=bool(item.get("is_sold_out")),
|
||
goods_desc=str(item.get("goods_desc") or ""),
|
||
)
|
||
)
|
||
return WeiboMallListOut(
|
||
account_id=account.id,
|
||
weidou_balance=int(data.get("weidou_balance") or 0),
|
||
goods=goods,
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/accounts/{account_id}/exchange",
|
||
response_model=WeiboExchangeOut,
|
||
)
|
||
def exchange_goods(
|
||
account_id: int,
|
||
req: WeiboExchangeRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动兑换商品;服务端只提交一次,不自动重试。"""
|
||
|
||
_require_weibo_perm(current, "weibo:exchange")
|
||
if not req.confirm:
|
||
raise HTTPException(status_code=400, detail="请先确认兑换商品")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
result = exchange_weibo_goods(
|
||
db,
|
||
account,
|
||
goods_id=req.goods_id,
|
||
expected_price=req.expected_price,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except WeiboExchangeUncertainError as exc:
|
||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:mall:exchange",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={
|
||
"account_id": account.id,
|
||
"uid": account.uid,
|
||
"goods_id": result.goods_id,
|
||
"goods_name": result.goods_name,
|
||
"price": result.price,
|
||
"balance_before": result.balance_before,
|
||
"balance_after": result.balance_after,
|
||
"verified": result.verified,
|
||
},
|
||
)
|
||
return WeiboExchangeOut(
|
||
account_id=account.id,
|
||
goods_id=result.goods_id,
|
||
goods_name=result.goods_name,
|
||
price=result.price,
|
||
balance_before=result.balance_before,
|
||
balance_after=result.balance_after,
|
||
remain_before=result.remain_before,
|
||
remain_after=result.remain_after,
|
||
verified=result.verified,
|
||
verify_error=result.verify_error,
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/accounts/{account_id}/exchange-records",
|
||
response_model=WeiboExchangeRecordsOut,
|
||
)
|
||
def list_exchange_records(
|
||
account_id: int,
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=100),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动查询微豆兑换记录。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
data = query_weibo_exchange_records(
|
||
db,
|
||
account,
|
||
page=page,
|
||
page_size=page_size,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
items: list[WeiboExchangeRecordItem] = []
|
||
for item in data.get("list") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
items.append(
|
||
WeiboExchangeRecordItem(
|
||
goods_name=str(item.get("goods_name") or ""),
|
||
game_account=str(item.get("game_account") or ""),
|
||
cost_weidou=_int_or_none(item.get("cost_weidou")),
|
||
exchange_time=str(item.get("exchange_time") or ""),
|
||
reward_status=_int_or_none(item.get("reward_status")),
|
||
raw=item,
|
||
)
|
||
)
|
||
return WeiboExchangeRecordsOut(
|
||
account_id=account.id,
|
||
page=int(data.get("page") or data.get("page_no") or page),
|
||
page_size=int(data.get("pageSize") or data.get("page_size") or page_size),
|
||
total=int(data.get("totalCount") or data.get("total") or 0),
|
||
items=items,
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/accounts/{account_id}/point-history",
|
||
response_model=WeiboPointHistoryOut,
|
||
)
|
||
def list_point_history(
|
||
account_id: int,
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=100),
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动查询微豆获取和充值明细。"""
|
||
|
||
_require_weibo_perm(current, "weibo:view")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
data = query_weibo_point_history(
|
||
db,
|
||
account,
|
||
page=page,
|
||
page_size=page_size,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
items: list[WeiboPointRecordItem] = []
|
||
for item in data.get("list") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
items.append(
|
||
WeiboPointRecordItem(
|
||
id=str(item.get("id") or ""),
|
||
source_type=_int_or_none(item.get("source_type")),
|
||
record_type=_int_or_none(item.get("record_type")),
|
||
source_id=str(item.get("source_id") or ""),
|
||
source_name=str(item.get("source_name") or ""),
|
||
point_num=_int_or_none(item.get("point_num")) or 0,
|
||
create_time=str(item.get("create_time") or ""),
|
||
expire_time=str(item.get("expire_time") or ""),
|
||
)
|
||
)
|
||
return WeiboPointHistoryOut(
|
||
account_id=account.id,
|
||
page=int(data.get("page_num") or page),
|
||
page_size=int(data.get("page_size") or page_size),
|
||
total=int(data.get("total") or 0),
|
||
items=items,
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/accounts/{account_id}/recharge/orders",
|
||
response_model=WeiboRechargeOrderOut,
|
||
)
|
||
def create_recharge_order(
|
||
account_id: int,
|
||
req: WeiboRechargeRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动创建助威充值订单并返回支付二维码。"""
|
||
|
||
_require_weibo_perm(current, "weibo:recharge")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
order = create_weibo_recharge_order(
|
||
db,
|
||
account,
|
||
amount_yuan=req.amount_yuan,
|
||
activity=WEIBO_DEFAULT_ACTIVITY,
|
||
)
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:recharge:create_order",
|
||
target=f"weibo_account:{account.id}",
|
||
detail={
|
||
"account_id": account.id,
|
||
"uid": account.uid,
|
||
"amount_yuan": order.amount_yuan,
|
||
"single_reward": order.single_reward,
|
||
"order_id": order.order_id,
|
||
},
|
||
)
|
||
return WeiboRechargeOrderOut(
|
||
account_id=account.id,
|
||
order_id=order.order_id,
|
||
pay_id=order.pay_id,
|
||
amount_yuan=order.amount_yuan,
|
||
single_reward=order.single_reward,
|
||
alipay_qr_code=order.alipay_qr_code,
|
||
desktop_pay_url=order.desktop_pay_url,
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/accounts/{account_id}/recharge/status",
|
||
response_model=WeiboPaymentStatusOut,
|
||
)
|
||
def get_recharge_status(
|
||
account_id: int,
|
||
req: WeiboPaymentStatusRequest,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""手动查询支付状态;支付动作仍由用户在微信/支付宝完成。"""
|
||
|
||
_require_weibo_perm(current, "weibo:recharge")
|
||
account = _get_account(db, account_id, current)
|
||
_ensure_enabled(account)
|
||
try:
|
||
status = query_weibo_payment_status(db, account, pay_id=req.pay_id)
|
||
except (WeiboError, ValueError) as exc:
|
||
raise _manual_http_error(exc) from exc
|
||
return WeiboPaymentStatusOut(
|
||
pay_id=status.pay_id,
|
||
out_pay_id=status.out_pay_id,
|
||
status=status.status,
|
||
sub_status=status.sub_status,
|
||
)
|
||
|
||
|
||
@router.delete("/accounts/{account_id}")
|
||
def delete_account(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current: User = Depends(get_current_user),
|
||
):
|
||
"""删除一个微博账号。"""
|
||
|
||
_require_weibo_perm(current, "weibo:delete")
|
||
account = _get_account(db, account_id, current)
|
||
uid = account.uid
|
||
db.delete(account)
|
||
db.commit()
|
||
_record_audit_safely(
|
||
db,
|
||
current,
|
||
action="weibo:account:delete",
|
||
target=f"weibo_account:{account_id}",
|
||
detail={"account_id": account_id, "uid": uid},
|
||
)
|
||
return {"message": "微博账号已删除", "success": True}
|