423 lines
12 KiB
Python
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
|