Files
live-hub-py/web/backend/services/weibo_service.py
T

423 lines
12 KiB
Python

"""微博微豆账号导入、刷新与绑定 Scheme 服务。"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from sqlalchemy.orm import Session
from core.weibo import (
WeiboAuthError,
WeiboClient,
WeiboError,
WeiboExchangeResult,
WeiboHomeSnapshot,
WeiboPaymentStatus,
WeiboRechargeOrder,
normalize_weibo_cookie,
)
from ..models import WeiboAccount, WeiboWorkbenchAccount
@dataclass(frozen=True, slots=True)
class WeiboImportIssue:
line: int
message: str
def _now() -> datetime:
return datetime.now(UTC)
def _error_status(exc: Exception) -> str:
return "invalid" if isinstance(exc, WeiboAuthError) else "error"
def _apply_snapshot(
account: WeiboAccount,
snapshot: WeiboHomeSnapshot,
cookie: str,
) -> None:
account.uid = snapshot.uid
account.nickname = snapshot.nickname
account.cookie = cookie
account.cookie_updated_at = _now()
account.is_bound = snapshot.is_bound
account.game_role_name = snapshot.role_name
account.can_rebind = snapshot.can_rebind
account.rebind_remain_days = snapshot.rebind_remain_days
account.weidou_balance = snapshot.weidou_balance
if snapshot.activity:
account.activity = snapshot.activity
account.ck_check_status = "valid"
account.ck_check_message = ""
account.ck_checked_at = _now()
def import_weibo_cookies(
db: Session,
text: str,
tag: str = "",
activity: str = "",
update_existing: bool = False,
current_user_id: int | None = None,
can_update_all: bool = False,
) -> tuple[list[WeiboAccount], list[WeiboImportIssue], int]:
"""逐行验证并导入微博 CK。
返回 ``(成功账号, 错误列表, 跳过数)``。单行 CK 失效或网络失败只记录该行,
不阻断其余账号导入。
"""
accounts: list[WeiboAccount] = []
errors: list[WeiboImportIssue] = []
skipped = 0
seen_uids: set[str] = set()
clean_tag = (tag or "").strip()
for line_no, raw_line in enumerate((text or "").splitlines(), 1):
raw = raw_line.strip()
if not raw:
continue
try:
cookie = normalize_weibo_cookie(raw)
client = WeiboClient(cookie)
snapshot = client.fetch_home(activity)
client.check_payment_access(activity, home=snapshot)
except (ValueError, WeiboError) as exc:
errors.append(WeiboImportIssue(line=line_no, message=str(exc)))
continue
if snapshot.uid in seen_uids:
skipped += 1
continue
seen_uids.add(snapshot.uid)
account = (
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)
.scalar()
)
if owner_id is not None and owner_id != current_user_id:
errors.append(
WeiboImportIssue(
line=line_no,
message="该账号已被其他人的工作台占用,不能更新 CK",
)
)
continue
if account is not None and not update_existing:
skipped += 1
continue
if account is None:
account = WeiboAccount(uid=snapshot.uid, cookie=cookie)
db.add(account)
_apply_snapshot(account, snapshot, cookie)
if clean_tag:
account.tag = clean_tag
accounts.append(account)
if accounts:
db.commit()
for account in accounts:
db.refresh(account)
return accounts, errors, skipped
def refresh_weibo_account(
db: Session,
account: WeiboAccount,
*,
activity: str = "",
commit: bool = True,
) -> WeiboHomeSnapshot:
"""刷新一个微博账号;失败时保留旧资料并记录本次检查状态。"""
try:
client = WeiboClient(account.cookie or "")
snapshot = client.fetch_home(activity)
client.check_payment_access(activity, home=snapshot)
except (WeiboError, ValueError) as exc:
account.ck_check_status = _error_status(exc)
account.ck_check_message = str(exc)
account.ck_checked_at = _now()
if commit:
db.commit()
raise
_apply_snapshot(account, snapshot, account.cookie or "")
if commit:
db.commit()
db.refresh(account)
return snapshot
def update_weibo_account_metadata(
db: Session,
account: WeiboAccount,
*,
tag: str | None = None,
remark: str | None = None,
) -> dict[str, dict[str, str]]:
"""更新人工维护的账号标签与备注。"""
changes: dict[str, dict[str, str]] = {}
if tag is not None:
clean_tag = tag.strip()
if clean_tag != (account.tag or ""):
changes["tag"] = {"old": account.tag or "", "new": clean_tag}
account.tag = clean_tag
if remark is not None:
clean_remark = remark.strip()
if clean_remark != (account.remark or ""):
changes["remark"] = {
"old": account.remark or "",
"new": clean_remark,
}
account.remark = clean_remark
if changes:
db.commit()
db.refresh(account)
return changes
def disable_weibo_account(
db: Session,
account: WeiboAccount,
*,
reason: str = "",
) -> None:
"""停用账号;保留 CK 和资料,后续可重新启用。"""
if not account.is_enabled:
return
account.is_enabled = False
account.disabled_at = _now()
account.disabled_reason = reason.strip()[:256]
db.commit()
db.refresh(account)
def enable_weibo_account(db: Session, account: WeiboAccount) -> None:
"""启用已通过完整 CK 检测的账号。"""
if account.is_enabled:
return
account.is_enabled = True
account.disabled_at = None
account.disabled_reason = ""
db.commit()
db.refresh(account)
def generate_weibo_bind_schemes(
account: WeiboAccount,
bind_url: str,
) -> list[dict[str, str]]:
"""分别生成微信、QQ Scheme;单渠道失败不阻断另一渠道。"""
client = WeiboClient()
result: list[dict[str, str]] = []
for channel, label in (("wx", "微信"), ("qq", "QQ")):
try:
scheme = client.generate_bind_scheme(bind_url, channel)
result.append(
{"channel": channel, "label": label, "scheme": scheme, "error": ""}
)
except (WeiboError, ValueError) as exc:
result.append(
{
"channel": channel,
"label": label,
"scheme": "",
"error": str(exc),
}
)
return result
def generate_weibo_bind_qrcode(
account: WeiboAccount,
bind_url: str,
channel: str,
) -> dict[str, str]:
"""生成指定渠道的小程序码,并尽力附带可复制的 Scheme。"""
_ = account
label = {"wx": "微信", "qq": "QQ"}[channel]
client = WeiboClient()
try:
qrcode = client.generate_bind_qrcode(bind_url, channel)
except (WeiboError, ValueError) as exc:
return {
"channel": channel,
"label": label,
"scheme": "",
"qrcode_image": "",
"qrcode_token": "",
"error": str(exc),
}
scheme = ""
try:
scheme = client.generate_bind_scheme(bind_url, channel)
except (WeiboError, ValueError):
# 二维码图片是主结果,Scheme 仅用于复制,不影响扫码绑定。
pass
return {
"channel": channel,
"label": label,
"scheme": scheme,
"qrcode_image": qrcode.image_src,
"qrcode_token": qrcode.token,
"error": "",
}
def _effective_activity(account: WeiboAccount, fallback: str) -> str:
return account.activity or fallback
def _mark_manual_failure(
db: Session,
account: WeiboAccount,
exc: Exception,
) -> None:
"""只有明确登录态失效才改动 CK 状态,业务失败不能把有效 CK 标坏。"""
if isinstance(exc, WeiboAuthError):
account.ck_check_status = "invalid"
account.ck_check_message = str(exc)
account.ck_checked_at = _now()
db.commit()
def query_weibo_mall(
db: Session,
account: WeiboAccount,
*,
activity: str = "",
) -> dict:
try:
data = WeiboClient(account.cookie or "").list_mall(
_effective_activity(account, activity)
)
except (WeiboError, ValueError) as exc:
_mark_manual_failure(db, account, exc)
raise
if isinstance(data.get("weidou_balance"), int):
account.weidou_balance = data["weidou_balance"]
db.commit()
return data
def query_weibo_exchange_records(
db: Session,
account: WeiboAccount,
*,
page: int,
page_size: int,
activity: str = "",
) -> dict:
try:
return WeiboClient(account.cookie or "").list_exchange_records(
_effective_activity(account, activity),
page=page,
page_size=page_size,
)
except (WeiboError, ValueError) as exc:
_mark_manual_failure(db, account, exc)
raise
def exchange_weibo_goods(
db: Session,
account: WeiboAccount,
*,
goods_id: int,
expected_price: int,
activity: str = "",
) -> WeiboExchangeResult:
"""预检并执行一次微博微豆兑换,然后刷新账号余额。"""
try:
result = WeiboClient(account.cookie or "").exchange_goods(
goods_id,
expected_price=expected_price,
activity=_effective_activity(account, activity),
)
except (WeiboError, ValueError) as exc:
_mark_manual_failure(db, account, exc)
raise
if result.balance_after is not None:
account.weidou_balance = result.balance_after
db.commit()
return result
def query_weibo_point_history(
db: Session,
account: WeiboAccount,
*,
page: int,
page_size: int,
activity: str = "",
) -> dict:
try:
return WeiboClient(account.cookie or "").list_point_history(
_effective_activity(account, activity),
page=page,
page_size=page_size,
)
except (WeiboError, ValueError) as exc:
_mark_manual_failure(db, account, exc)
raise
def create_weibo_recharge_order(
db: Session,
account: WeiboAccount,
*,
amount_yuan: int,
activity: str = "",
) -> WeiboRechargeOrder:
try:
return WeiboClient(account.cookie or "").create_recharge_order(
amount_yuan=amount_yuan,
activity=_effective_activity(account, activity),
)
except (WeiboError, ValueError) as exc:
_mark_manual_failure(db, account, exc)
raise
def query_weibo_payment_status(
db: Session,
account: WeiboAccount,
*,
pay_id: str,
) -> WeiboPaymentStatus:
try:
return WeiboClient(account.cookie or "").get_payment_status(pay_id)
except Exception as exc:
_mark_manual_failure(db, account, exc)
raise