"""微博微豆:CK 导入、账号刷新与绑定二维码。""" from __future__ import annotations import os from fastapi import APIRouter, Depends, HTTPException, Query from loguru import logger from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, defer 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, WeiboBindQrOut, WeiboBindSchemeOut, WeiboCookieImport, WeiboExchangeOut, WeiboExchangeRecordItem, WeiboExchangeRecordsOut, WeiboExchangeRequest, WeiboGoodsOut, WeiboImportError, WeiboImportResult, WeiboMallListOut, WeiboPaymentStatusOut, WeiboPaymentStatusRequest, WeiboPointHistoryOut, WeiboPointRecordItem, WeiboRechargeOrderOut, WeiboRechargeRequest, WeiboWorkbenchAccountsRequest, ) 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() 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 _get_account(db: Session, account_id: int) -> WeiboAccount: account = db.query(WeiboAccount).filter(WeiboAccount.id == account_id).first() if account is None: raise HTTPException(status_code=404, detail="微博账号不存在") return account def _account_out(account: WeiboAccount) -> WeiboAccountOut: return WeiboAccountOut.model_validate(account) def _apply_account_filters( query, *, search: str, tag: str, bind_status: str, enabled: bool | 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) 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(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), 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( db.query(WeiboAccount), search=search, tag=tag, bind_status=bind_status, enabled=enabled, ) 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) ) 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) query = _apply_account_filters( db.query(WeiboAccount).filter(WeiboAccount.id.not_in(occupied)), search=search, tag=tag, bind_status="", enabled=True, ) return _paginated_accounts(query, page=page, page_size=page_size) @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") rows = ( db.query(WeiboAccount.tag) .join( WeiboWorkbenchAccount, WeiboWorkbenchAccount.account_id == WeiboAccount.id, ) .filter( WeiboWorkbenchAccount.user_id == current.id, WeiboAccount.tag != "", ) .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}") 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/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 = 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}") 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.get("/accounts/tags") def list_account_tags( db: Session = Depends(get_db), current: User = Depends(get_current_user), ): """返回全局账号池已有的标签。""" _require_weibo_perm(current, "weibo:view") rows = ( db.query(WeiboAccount.tag) .filter(WeiboAccount.tag != "") .distinct() .order_by(WeiboAccount.tag.asc()) .all() ) return {"tags": [tag for (tag,) in rows if tag]} @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)) @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) 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) 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) _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 = import_weibo_cookies( db, req.text, req.tag, WEIBO_DEFAULT_ACTIVITY, req.update_existing, current.id, ) typed_errors = [ WeiboImportError(line=item.line, message=item.message) for item in errors ] 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, "errors": len(errors), }, ) message = f"成功导入 {len(accounts)} 个微博账号" if errors: message += f",失败 {len(errors)} 条" if skipped: message += f",跳过 {skipped} 条重复" return WeiboImportResult( success=bool(accounts) or (skipped > 0 and not errors), message=message, count=len(accounts), skipped=skipped, errors=typed_errors, 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) _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) 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) _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) _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) _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) _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) _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) _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) _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) 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}