"""微博微豆账号导入、刷新与绑定 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, ) -> 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: 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