feat(weibo): 增加微博微豆基础自动化
This commit is contained in:
@@ -155,6 +155,7 @@ _SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
|
||||
("login_tasks", "cookie"),
|
||||
("huya_accounts", "account_password"),
|
||||
("huya_accounts", "cookie"),
|
||||
("weibo_accounts", "cookie"),
|
||||
("huya_register_batches", "fixed_password"),
|
||||
("huya_register_items", "password"),
|
||||
("huya_register_items", "cookie"),
|
||||
|
||||
@@ -33,6 +33,7 @@ from .routers import (
|
||||
login,
|
||||
proxy,
|
||||
users,
|
||||
weibo,
|
||||
yyb,
|
||||
)
|
||||
from .schemas import AppInfo
|
||||
@@ -165,6 +166,7 @@ app.include_router(login.router)
|
||||
app.include_router(proxy.router)
|
||||
app.include_router(cookies.router)
|
||||
app.include_router(huya.router)
|
||||
app.include_router(weibo.router)
|
||||
app.include_router(douyu.router)
|
||||
app.include_router(douyu_collab.router)
|
||||
app.include_router(yyb.router)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""微博微豆账号基础表。
|
||||
|
||||
第一版只支持 CK 导入、账号资料刷新和绑定二维码展示。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261107_0068"
|
||||
down_revision: str | None = "20261107_0067"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "weibo_accounts"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if TABLE in set(sa.inspect(bind).get_table_names()):
|
||||
return
|
||||
op.create_table(
|
||||
TABLE,
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column("uid", sa.String(length=32), nullable=False),
|
||||
sa.Column("nickname", sa.String(length=128), nullable=False, server_default=""),
|
||||
# EncryptedText 在应用层加解密,DDL 是普通文本列。
|
||||
sa.Column("cookie", sa.Text(), nullable=False),
|
||||
sa.Column("cookie_updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("tag", sa.String(length=64), nullable=False, server_default=""),
|
||||
sa.Column(
|
||||
"ck_check_status", sa.String(length=16), nullable=False, server_default=""
|
||||
),
|
||||
sa.Column("ck_check_message", sa.Text(), nullable=False),
|
||||
sa.Column("ck_checked_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_bound", sa.Boolean(), nullable=True),
|
||||
sa.Column(
|
||||
"game_role_name", sa.String(length=128), nullable=False, server_default=""
|
||||
),
|
||||
sa.Column("can_rebind", sa.Boolean(), nullable=True),
|
||||
sa.Column("rebind_remain_days", sa.Integer(), nullable=True),
|
||||
sa.Column("weidou_balance", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
)
|
||||
op.create_index(f"ix_{TABLE}_uid", TABLE, ["uid"], unique=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if TABLE in set(sa.inspect(bind).get_table_names()):
|
||||
op.drop_table(TABLE)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""记录微博账号最近识别的活动标识。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261107_0069"
|
||||
down_revision: str | None = "20261107_0068"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "weibo_accounts"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in sa.inspect(bind).get_columns(TABLE)}
|
||||
if "activity" not in columns:
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column(
|
||||
"activity", sa.String(length=64), nullable=False, server_default=""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in sa.inspect(bind).get_columns(TABLE)}
|
||||
if "activity" in columns:
|
||||
op.drop_column(TABLE, "activity")
|
||||
@@ -912,6 +912,44 @@ class HuyaAccount(Base):
|
||||
tasks: Mapped[list[HuyaTask]] = relationship("HuyaTask", back_populates="account")
|
||||
|
||||
|
||||
class WeiboAccount(Base):
|
||||
"""微博微豆账号。
|
||||
|
||||
第一版只保存 CK 与微豆首页快照,不承载登录密码、支付或兑换数据。
|
||||
"""
|
||||
|
||||
__tablename__ = "weibo_accounts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
uid: Mapped[str] = mapped_column(
|
||||
String(32), unique=True, nullable=False, index=True
|
||||
)
|
||||
nickname: Mapped[str] = mapped_column(String(128), default="")
|
||||
cookie: Mapped[str] = mapped_column(EncryptedText(), nullable=False)
|
||||
cookie_updated_at: Mapped[datetime | None] = mapped_column(
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
tag: Mapped[str] = mapped_column(String(64), default="")
|
||||
activity: Mapped[str] = mapped_column(String(64), default="")
|
||||
|
||||
ck_check_status: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="", server_default=""
|
||||
)
|
||||
ck_check_message: Mapped[str] = mapped_column(Text, default="")
|
||||
ck_checked_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
is_bound: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
game_role_name: Mapped[str] = mapped_column(String(128), default="")
|
||||
can_rebind: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
rebind_remain_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
weidou_balance: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
class HuyaRegisterBatch(Base):
|
||||
"""虎牙自动注册批次(持久化元数据)"""
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ PERMISSIONS = {
|
||||
"yyb:recharge": "应用宝充值与付款",
|
||||
"yyb:manage": "接管并操作他人的应用宝任务",
|
||||
"yyb:history": "查看应用宝充值历史",
|
||||
# 微博微豆
|
||||
"weibo:view": "查看微博账号",
|
||||
"weibo:import": "导入/刷新微博 CK",
|
||||
"weibo:bind": "生成微博绑定二维码",
|
||||
"weibo:recharge": "创建微博助威充值订单",
|
||||
"weibo:exchange": "执行微博微豆兑换",
|
||||
"weibo:delete": "删除微博账号",
|
||||
# 虎牙
|
||||
"huya:account": "虎牙账号管理(兼容旧权限)",
|
||||
"huya:view_all": "查看所有虎牙账号",
|
||||
@@ -101,6 +108,12 @@ ROLE_PERMISSIONS = {
|
||||
"yyb:recharge",
|
||||
"yyb:manage",
|
||||
"yyb:history",
|
||||
"weibo:view",
|
||||
"weibo:import",
|
||||
"weibo:bind",
|
||||
"weibo:recharge",
|
||||
"weibo:exchange",
|
||||
"weibo:delete",
|
||||
"huya:account",
|
||||
"huya:view_all",
|
||||
"huya:import",
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
"""微博微豆:CK 导入、账号刷新与绑定二维码。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from loguru import logger
|
||||
from sqlalchemy import or_
|
||||
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
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
WeiboAccountOut,
|
||||
WeiboBindQrOut,
|
||||
WeiboBindSchemeOut,
|
||||
WeiboCookieImport,
|
||||
WeiboExchangeOut,
|
||||
WeiboExchangeRecordItem,
|
||||
WeiboExchangeRecordsOut,
|
||||
WeiboExchangeRequest,
|
||||
WeiboGoodsOut,
|
||||
WeiboImportError,
|
||||
WeiboImportResult,
|
||||
WeiboMallListOut,
|
||||
WeiboPaymentStatusOut,
|
||||
WeiboPaymentStatusRequest,
|
||||
WeiboPointHistoryOut,
|
||||
WeiboPointRecordItem,
|
||||
WeiboRechargeOrderOut,
|
||||
WeiboRechargeRequest,
|
||||
)
|
||||
from ..services.audit_service import record_audit
|
||||
from ..services.weibo_service import (
|
||||
create_weibo_recharge_order,
|
||||
exchange_weibo_goods,
|
||||
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,
|
||||
)
|
||||
|
||||
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 _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(""),
|
||||
bind_status: 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:view")
|
||||
query = db.query(WeiboAccount)
|
||||
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),
|
||||
)
|
||||
)
|
||||
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),
|
||||
)
|
||||
|
||||
total = query.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,
|
||||
}
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
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),
|
||||
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)
|
||||
_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}/bind-qrcodes", response_model=WeiboBindQrOut)
|
||||
def create_bind_qrcodes(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""生成微信和 QQ 绑定 Scheme,供前端直接渲染二维码。"""
|
||||
|
||||
_require_weibo_perm(current, "weibo:bind")
|
||||
account = _get_account(db, account_id)
|
||||
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="微博没有返回当前账号的绑定链接")
|
||||
|
||||
schemes = [
|
||||
WeiboBindSchemeOut(**item)
|
||||
for item in generate_weibo_bind_schemes(account, snapshot.bind_url)
|
||||
]
|
||||
successful = [item for item in schemes if item.scheme]
|
||||
if not successful:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="微信和 QQ 绑定二维码都生成失败: "
|
||||
+ "; ".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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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}
|
||||
@@ -462,6 +462,160 @@ class AccountCheckBatchOut(BaseModel):
|
||||
items: list[AccountCheckItemOut]
|
||||
|
||||
|
||||
# ---- 微博微豆 ----
|
||||
class WeiboCookieImport(BaseModel):
|
||||
"""批量导入微博 CK;每行一个完整 Cookie。"""
|
||||
|
||||
text: str = Field(..., min_length=1)
|
||||
tag: str = Field("", max_length=64)
|
||||
|
||||
|
||||
class WeiboImportError(BaseModel):
|
||||
line: int
|
||||
message: str
|
||||
|
||||
|
||||
class WeiboAccountOut(BaseModel):
|
||||
id: int
|
||||
uid: str
|
||||
nickname: str = ""
|
||||
tag: str = ""
|
||||
activity: str = ""
|
||||
ck_check_status: str = ""
|
||||
ck_check_message: str = ""
|
||||
ck_checked_at: datetime | None = None
|
||||
is_bound: bool | None = None
|
||||
game_role_name: str = ""
|
||||
can_rebind: bool | None = None
|
||||
rebind_remain_days: int | None = None
|
||||
weidou_balance: int | None = None
|
||||
cookie_updated_at: datetime | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class WeiboImportResult(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
count: int
|
||||
skipped: int
|
||||
errors: list[WeiboImportError] = Field(default_factory=list)
|
||||
accounts: list[WeiboAccountOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboBindSchemeOut(BaseModel):
|
||||
channel: str
|
||||
label: str
|
||||
scheme: str = ""
|
||||
error: str = ""
|
||||
|
||||
|
||||
class WeiboBindQrOut(BaseModel):
|
||||
account_id: int
|
||||
is_bound: bool
|
||||
can_rebind: bool
|
||||
rebind_remain_days: int
|
||||
schemes: list[WeiboBindSchemeOut]
|
||||
|
||||
|
||||
class WeiboGoodsOut(BaseModel):
|
||||
id: int
|
||||
goods_name: str = ""
|
||||
goods_image: str = ""
|
||||
price: int = 0
|
||||
remain_count: int | None = None
|
||||
is_sold_out: bool = False
|
||||
goods_desc: str = ""
|
||||
|
||||
|
||||
class WeiboMallListOut(BaseModel):
|
||||
account_id: int
|
||||
weidou_balance: int = 0
|
||||
goods: list[WeiboGoodsOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboExchangeRequest(BaseModel):
|
||||
goods_id: int = Field(..., gt=0)
|
||||
expected_price: int = Field(..., gt=0)
|
||||
confirm: bool = False
|
||||
|
||||
|
||||
class WeiboExchangeOut(BaseModel):
|
||||
account_id: int
|
||||
goods_id: int
|
||||
goods_name: str
|
||||
price: int
|
||||
balance_before: int
|
||||
balance_after: int | None = None
|
||||
remain_before: int | None = None
|
||||
remain_after: int | None = None
|
||||
verified: bool
|
||||
verify_error: str = ""
|
||||
|
||||
|
||||
class WeiboExchangeRecordItem(BaseModel):
|
||||
goods_name: str = ""
|
||||
game_account: str = ""
|
||||
cost_weidou: int | None = None
|
||||
exchange_time: str = ""
|
||||
reward_status: int | None = None
|
||||
raw: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class WeiboExchangeRecordsOut(BaseModel):
|
||||
account_id: int
|
||||
page: int
|
||||
page_size: int
|
||||
total: int
|
||||
items: list[WeiboExchangeRecordItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboPointRecordItem(BaseModel):
|
||||
id: str = ""
|
||||
source_type: int | None = None
|
||||
record_type: int | None = None
|
||||
source_id: str = ""
|
||||
source_name: str = ""
|
||||
point_num: int = 0
|
||||
create_time: str = ""
|
||||
expire_time: str = ""
|
||||
|
||||
|
||||
class WeiboPointHistoryOut(BaseModel):
|
||||
account_id: int
|
||||
page: int
|
||||
page_size: int
|
||||
total: int
|
||||
items: list[WeiboPointRecordItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class WeiboRechargeRequest(BaseModel):
|
||||
amount_yuan: int = Field(1, ge=1, le=1000)
|
||||
|
||||
|
||||
class WeiboRechargeOrderOut(BaseModel):
|
||||
account_id: int
|
||||
order_id: str
|
||||
pay_id: str
|
||||
amount_yuan: int
|
||||
single_reward: int
|
||||
alipay_qr_code: str
|
||||
desktop_pay_url: str = ""
|
||||
|
||||
|
||||
class WeiboPaymentStatusRequest(BaseModel):
|
||||
pay_id: str = Field(..., min_length=1, max_length=128)
|
||||
|
||||
|
||||
class WeiboPaymentStatusOut(BaseModel):
|
||||
pay_id: str
|
||||
out_pay_id: str = ""
|
||||
status: str
|
||||
sub_status: str = ""
|
||||
|
||||
|
||||
# ---- 虎牙 ----
|
||||
class HuyaCookieImport(BaseModel):
|
||||
"""批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。"""
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
"""微博微豆账号导入、刷新与绑定 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
|
||||
|
||||
|
||||
@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 = "",
|
||||
) -> 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)
|
||||
snapshot = WeiboClient(cookie).fetch_home(activity)
|
||||
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 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 = "",
|
||||
) -> WeiboHomeSnapshot:
|
||||
"""刷新一个微博账号;失败时保留旧资料并记录本次检查状态。"""
|
||||
|
||||
try:
|
||||
snapshot = WeiboClient(account.cookie or "").fetch_home(activity)
|
||||
except (WeiboError, ValueError) as exc:
|
||||
account.ck_check_status = _error_status(exc)
|
||||
account.ck_check_message = str(exc)
|
||||
account.ck_checked_at = _now()
|
||||
db.commit()
|
||||
raise
|
||||
|
||||
_apply_snapshot(account, snapshot, account.cookie or "")
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return snapshot
|
||||
|
||||
|
||||
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 _effective_activity(account: WeiboAccount, fallback: str) -> str:
|
||||
return account.activity or fallback
|
||||
|
||||
|
||||
def _mark_manual_failure(
|
||||
db: Session,
|
||||
account: WeiboAccount,
|
||||
exc: Exception,
|
||||
) -> None:
|
||||
account.ck_check_status = _error_status(exc)
|
||||
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
|
||||
@@ -31,6 +31,7 @@ const HuyaTasksPage = lazy(() => import('./pages/HuyaTasksPage'));
|
||||
const HuyaElitePage = lazy(() => import('./pages/HuyaElitePage'));
|
||||
const HuyaCollabPage = lazy(() => import('./pages/HuyaCollabPage'));
|
||||
const HuyaBrowserExchangePage = lazy(() => import('./pages/HuyaBrowserExchangePage'));
|
||||
const WeiboAccountsPage = lazy(() => import('./pages/WeiboAccountsPage'));
|
||||
const YybRechargePage = lazy(() => import('./pages/YybRechargePage'));
|
||||
const AuditLogsPage = lazy(() => import('./pages/AuditLogsPage'));
|
||||
|
||||
@@ -105,6 +106,7 @@ function AppContent() {
|
||||
<Route path="huya/elite" element={lazyRoute(<HuyaElitePage />)} />
|
||||
<Route path="huya/collab" element={lazyRoute(<HuyaCollabPage />)} />
|
||||
<Route path="huya/browser-exchange" element={lazyRoute(<HuyaBrowserExchangePage />)} />
|
||||
<Route path="weibo/accounts" element={lazyRoute(<WeiboAccountsPage />)} />
|
||||
<Route path="yyb/recharge" element={lazyRoute(<YybRechargePage />)} />
|
||||
<Route path="proxy" element={lazyRoute(<ProxyPage />)} />
|
||||
<Route path="automation" element={lazyRoute(<AutomationPage />)} />
|
||||
|
||||
@@ -14,4 +14,5 @@ export { huyaApi } from './huya';
|
||||
export { loginApi } from './login';
|
||||
export { proxyApi } from './proxy';
|
||||
export { userApi } from './users';
|
||||
export { weiboApi } from './weibo';
|
||||
export { yybApi } from './yyb';
|
||||
|
||||
@@ -634,6 +634,7 @@ export interface DouyuTaskAccountItem {
|
||||
uid: string;
|
||||
nickname: string;
|
||||
tag: string;
|
||||
activity: string;
|
||||
ck_check_status: string;
|
||||
ck_checked_at: string | null;
|
||||
points: number | null;
|
||||
@@ -745,6 +746,137 @@ export interface DouyuXpdGoodsItem extends DouyuGoodsItem {
|
||||
goods_left: number | null;
|
||||
}
|
||||
|
||||
// ==================== Weibo ====================
|
||||
|
||||
export interface WeiboAccountItem {
|
||||
id: number;
|
||||
uid: string;
|
||||
nickname: string;
|
||||
tag: string;
|
||||
ck_check_status: string;
|
||||
ck_check_message: string;
|
||||
ck_checked_at: string | null;
|
||||
is_bound: boolean | null;
|
||||
game_role_name: string;
|
||||
can_rebind: boolean | null;
|
||||
rebind_remain_days: number | null;
|
||||
weidou_balance: number | null;
|
||||
cookie_updated_at: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface WeiboImportError {
|
||||
line: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WeiboImportResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
count: number;
|
||||
skipped: number;
|
||||
errors: WeiboImportError[];
|
||||
accounts: WeiboAccountItem[];
|
||||
}
|
||||
|
||||
export interface WeiboBindScheme {
|
||||
channel: 'wx' | 'qq';
|
||||
label: string;
|
||||
scheme: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface WeiboBindQrResult {
|
||||
account_id: number;
|
||||
is_bound: boolean;
|
||||
can_rebind: boolean;
|
||||
rebind_remain_days: number;
|
||||
schemes: WeiboBindScheme[];
|
||||
}
|
||||
|
||||
export interface WeiboGoodsItem {
|
||||
id: number;
|
||||
goods_name: string;
|
||||
goods_image: string;
|
||||
price: number;
|
||||
remain_count: number | null;
|
||||
is_sold_out: boolean;
|
||||
goods_desc: string;
|
||||
}
|
||||
|
||||
export interface WeiboMallListResult {
|
||||
account_id: number;
|
||||
weidou_balance: number;
|
||||
goods: WeiboGoodsItem[];
|
||||
}
|
||||
|
||||
export interface WeiboExchangeRecordItem {
|
||||
goods_name: string;
|
||||
game_account: string;
|
||||
cost_weidou: number | null;
|
||||
exchange_time: string;
|
||||
reward_status: number | null;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WeiboExchangeRecordsResult {
|
||||
account_id: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
items: WeiboExchangeRecordItem[];
|
||||
}
|
||||
|
||||
export interface WeiboExchangeResult {
|
||||
account_id: number;
|
||||
goods_id: number;
|
||||
goods_name: string;
|
||||
price: number;
|
||||
balance_before: number;
|
||||
balance_after: number | null;
|
||||
remain_before: number | null;
|
||||
remain_after: number | null;
|
||||
verified: boolean;
|
||||
verify_error: string;
|
||||
}
|
||||
|
||||
export interface WeiboPointRecordItem {
|
||||
id: string;
|
||||
source_type: number | null;
|
||||
record_type: number | null;
|
||||
source_id: string;
|
||||
source_name: string;
|
||||
point_num: number;
|
||||
create_time: string;
|
||||
expire_time: string;
|
||||
}
|
||||
|
||||
export interface WeiboPointHistoryResult {
|
||||
account_id: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
items: WeiboPointRecordItem[];
|
||||
}
|
||||
|
||||
export interface WeiboRechargeOrderResult {
|
||||
account_id: number;
|
||||
order_id: string;
|
||||
pay_id: string;
|
||||
amount_yuan: number;
|
||||
single_reward: number;
|
||||
alipay_qr_code: string;
|
||||
desktop_pay_url: string;
|
||||
}
|
||||
|
||||
export interface WeiboPaymentStatusResult {
|
||||
pay_id: string;
|
||||
out_pay_id: string;
|
||||
status: string;
|
||||
sub_status: string;
|
||||
}
|
||||
|
||||
// ==================== Huya ====================
|
||||
|
||||
export interface HuyaAccountItem {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import api from './client';
|
||||
import type {
|
||||
MessageResponse,
|
||||
PaginatedResponse,
|
||||
WeiboAccountItem,
|
||||
WeiboBindQrResult,
|
||||
WeiboExchangeRecordsResult,
|
||||
WeiboExchangeResult,
|
||||
WeiboMallListResult,
|
||||
WeiboPaymentStatusResult,
|
||||
WeiboPointHistoryResult,
|
||||
WeiboRechargeOrderResult,
|
||||
WeiboImportResult,
|
||||
} from './types';
|
||||
|
||||
export const weiboApi = {
|
||||
listAccounts: (params: {
|
||||
search?: string;
|
||||
bind_status?: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}) =>
|
||||
api.get<PaginatedResponse<WeiboAccountItem>, PaginatedResponse<WeiboAccountItem>>(
|
||||
'/weibo/accounts',
|
||||
{ params },
|
||||
),
|
||||
importCookies: (text: string, tag = '') =>
|
||||
api.post<WeiboImportResult, WeiboImportResult>('/weibo/accounts/import-cookies', {
|
||||
text,
|
||||
tag,
|
||||
}),
|
||||
refreshAccount: (accountId: number) =>
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/refresh`,
|
||||
),
|
||||
bindQrcodes: (accountId: number) =>
|
||||
api.post<WeiboBindQrResult, WeiboBindQrResult>(
|
||||
`/weibo/accounts/${accountId}/bind-qrcodes`,
|
||||
undefined,
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
mallList: (accountId: number) =>
|
||||
api.post<WeiboMallListResult, WeiboMallListResult>(
|
||||
`/weibo/accounts/${accountId}/mall-list`,
|
||||
undefined,
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
exchangeGoods: (accountId: number, goodsId: number, expectedPrice: number) =>
|
||||
api.post<WeiboExchangeResult, WeiboExchangeResult>(
|
||||
`/weibo/accounts/${accountId}/exchange`,
|
||||
{
|
||||
goods_id: goodsId,
|
||||
expected_price: expectedPrice,
|
||||
confirm: true,
|
||||
},
|
||||
{ timeout: 120000 },
|
||||
),
|
||||
exchangeRecords: (accountId: number, page = 1, pageSize = 20) =>
|
||||
api.post<WeiboExchangeRecordsResult, WeiboExchangeRecordsResult>(
|
||||
`/weibo/accounts/${accountId}/exchange-records`,
|
||||
undefined,
|
||||
{ params: { page, page_size: pageSize }, timeout: 60000 },
|
||||
),
|
||||
pointHistory: (accountId: number, page = 1, pageSize = 20) =>
|
||||
api.post<WeiboPointHistoryResult, WeiboPointHistoryResult>(
|
||||
`/weibo/accounts/${accountId}/point-history`,
|
||||
undefined,
|
||||
{ params: { page, page_size: pageSize }, timeout: 60000 },
|
||||
),
|
||||
createRechargeOrder: (accountId: number, amountYuan: number) =>
|
||||
api.post<WeiboRechargeOrderResult, WeiboRechargeOrderResult>(
|
||||
`/weibo/accounts/${accountId}/recharge/orders`,
|
||||
{ amount_yuan: amountYuan },
|
||||
{ timeout: 120000 },
|
||||
),
|
||||
rechargeStatus: (accountId: number, payId: string) =>
|
||||
api.post<WeiboPaymentStatusResult, WeiboPaymentStatusResult>(
|
||||
`/weibo/accounts/${accountId}/recharge/status`,
|
||||
{ pay_id: payId },
|
||||
{ timeout: 30000 },
|
||||
),
|
||||
deleteAccount: (accountId: number) =>
|
||||
api.delete<MessageResponse, MessageResponse>(`/weibo/accounts/${accountId}`),
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ApartmentOutlined, MobileOutlined, ShoppingCartOutlined,
|
||||
BookOutlined, TrophyOutlined, ShopOutlined, GlobalOutlined,
|
||||
SafetyCertificateOutlined, FileSearchOutlined, RobotOutlined, FileExcelOutlined,
|
||||
MessageOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { getUser, clearAuth, type AuthUser } from '../store/auth';
|
||||
@@ -52,6 +53,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
const menuItems: MenuProps['items'] = [];
|
||||
const douyuItems: NonNullable<MenuProps['items']> = [];
|
||||
const huyaItems: NonNullable<MenuProps['items']> = [];
|
||||
const weiboItems: NonNullable<MenuProps['items']> = [];
|
||||
const systemItems: NonNullable<MenuProps['items']> = [];
|
||||
|
||||
// Dashboard - 所有人可见
|
||||
@@ -124,12 +126,19 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
huyaItems.push({ key: '/huya/collab', label: '外部单协作', icon: <FileExcelOutlined /> });
|
||||
}
|
||||
|
||||
if (can('weibo:view')) {
|
||||
weiboItems.push({ key: '/weibo/accounts', label: '账号与绑定', icon: <MessageOutlined /> });
|
||||
}
|
||||
|
||||
if (douyuItems.length > 0) {
|
||||
menuItems.push({ key: 'group-douyu', type: 'group', label: '斗鱼', children: douyuItems });
|
||||
}
|
||||
if (huyaItems.length > 0) {
|
||||
menuItems.push({ key: 'group-huya', type: 'group', label: '虎牙', children: huyaItems });
|
||||
}
|
||||
if (weiboItems.length > 0) {
|
||||
menuItems.push({ key: 'group-weibo', type: 'group', label: '微博', children: weiboItems });
|
||||
}
|
||||
|
||||
// 系统
|
||||
if (can('proxy:manage')) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user