feat(weibo): 增加外部 CK 账号 CRUD 与生命周期
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""增加微博账号备注与启停生命周期。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261108_0070"
|
||||
down_revision: str | None = "20261107_0069"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "weibo_accounts"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
if "remark" not in columns:
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column(
|
||||
"remark",
|
||||
sa.String(length=256),
|
||||
nullable=False,
|
||||
server_default="",
|
||||
),
|
||||
)
|
||||
if "is_enabled" not in columns:
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column(
|
||||
"is_enabled",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("1"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
f"ix_{TABLE}_is_enabled",
|
||||
TABLE,
|
||||
["is_enabled"],
|
||||
unique=False,
|
||||
)
|
||||
if "disabled_at" not in columns:
|
||||
op.add_column(TABLE, sa.Column("disabled_at", sa.DateTime(), nullable=True))
|
||||
if "disabled_reason" not in columns:
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column(
|
||||
"disabled_reason",
|
||||
sa.String(length=256),
|
||||
nullable=False,
|
||||
server_default="",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
indexes = {index["name"] for index in inspector.get_indexes(TABLE)}
|
||||
if f"ix_{TABLE}_is_enabled" in indexes:
|
||||
op.drop_index(f"ix_{TABLE}_is_enabled", table_name=TABLE)
|
||||
for column in ("disabled_reason", "disabled_at", "is_enabled", "remark"):
|
||||
if column in columns:
|
||||
op.drop_column(TABLE, column)
|
||||
@@ -930,8 +930,19 @@ class WeiboAccount(Base):
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
tag: Mapped[str] = mapped_column(String(64), default="")
|
||||
remark: Mapped[str] = mapped_column(
|
||||
String(256), nullable=False, default="", server_default=""
|
||||
)
|
||||
activity: Mapped[str] = mapped_column(String(64), default="")
|
||||
|
||||
is_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="1", index=True
|
||||
)
|
||||
disabled_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
disabled_reason: Mapped[str] = mapped_column(
|
||||
String(256), nullable=False, default="", server_default=""
|
||||
)
|
||||
|
||||
ck_check_status: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="", server_default=""
|
||||
)
|
||||
|
||||
@@ -44,6 +44,7 @@ PERMISSIONS = {
|
||||
# 微博微豆
|
||||
"weibo:view": "查看微博账号",
|
||||
"weibo:import": "导入/刷新微博 CK",
|
||||
"weibo:manage": "编辑、停用和启用微博账号",
|
||||
"weibo:bind": "生成微博绑定二维码",
|
||||
"weibo:recharge": "创建微博助威充值订单",
|
||||
"weibo:exchange": "执行微博微豆兑换",
|
||||
@@ -110,6 +111,7 @@ ROLE_PERMISSIONS = {
|
||||
"yyb:history",
|
||||
"weibo:view",
|
||||
"weibo:import",
|
||||
"weibo:manage",
|
||||
"weibo:bind",
|
||||
"weibo:recharge",
|
||||
"weibo:exchange",
|
||||
|
||||
@@ -16,7 +16,9 @@ from ..deps import get_current_user
|
||||
from ..models import User, WeiboAccount
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
WeiboAccountDisableRequest,
|
||||
WeiboAccountOut,
|
||||
WeiboAccountUpdate,
|
||||
WeiboBindQrOut,
|
||||
WeiboBindSchemeOut,
|
||||
WeiboCookieImport,
|
||||
@@ -38,6 +40,8 @@ from ..schemas import (
|
||||
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_schemes,
|
||||
import_weibo_cookies,
|
||||
@@ -46,6 +50,7 @@ from ..services.weibo_service import (
|
||||
query_weibo_payment_status,
|
||||
query_weibo_point_history,
|
||||
refresh_weibo_account,
|
||||
update_weibo_account_metadata,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/weibo", tags=["微博微豆"])
|
||||
@@ -81,6 +86,11 @@ def _account_out(account: WeiboAccount) -> WeiboAccountOut:
|
||||
return WeiboAccountOut.model_validate(account)
|
||||
|
||||
|
||||
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(
|
||||
@@ -128,6 +138,7 @@ def _record_audit_safely(
|
||||
def list_accounts(
|
||||
search: 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),
|
||||
@@ -146,8 +157,11 @@ def list_accounts(
|
||||
WeiboAccount.nickname.ilike(pattern),
|
||||
WeiboAccount.game_role_name.ilike(pattern),
|
||||
WeiboAccount.tag.ilike(pattern),
|
||||
WeiboAccount.remark.ilike(pattern),
|
||||
)
|
||||
)
|
||||
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))
|
||||
@@ -175,6 +189,95 @@ def list_accounts(
|
||||
}
|
||||
|
||||
|
||||
@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,
|
||||
@@ -231,6 +334,7 @@ def refresh_account(
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
_ensure_enabled(account)
|
||||
_refresh_or_http(account, db)
|
||||
_record_audit_safely(
|
||||
db,
|
||||
@@ -252,6 +356,7 @@ def create_bind_qrcodes(
|
||||
|
||||
_require_weibo_perm(current, "weibo:bind")
|
||||
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 = (
|
||||
@@ -314,6 +419,7 @@ def list_mall_goods(
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_mall(
|
||||
db,
|
||||
@@ -366,6 +472,7 @@ def exchange_goods(
|
||||
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,
|
||||
@@ -423,6 +530,7 @@ def list_exchange_records(
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_exchange_records(
|
||||
db,
|
||||
@@ -471,6 +579,7 @@ def list_point_history(
|
||||
|
||||
_require_weibo_perm(current, "weibo:view")
|
||||
account = _get_account(db, account_id)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
data = query_weibo_point_history(
|
||||
db,
|
||||
@@ -520,6 +629,7 @@ def create_recharge_order(
|
||||
|
||||
_require_weibo_perm(current, "weibo:recharge")
|
||||
account = _get_account(db, account_id)
|
||||
_ensure_enabled(account)
|
||||
try:
|
||||
order = create_weibo_recharge_order(
|
||||
db,
|
||||
@@ -567,6 +677,7 @@ def get_recharge_status(
|
||||
|
||||
_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:
|
||||
|
||||
@@ -480,7 +480,11 @@ class WeiboAccountOut(BaseModel):
|
||||
uid: str
|
||||
nickname: str = ""
|
||||
tag: str = ""
|
||||
remark: str = ""
|
||||
activity: str = ""
|
||||
is_enabled: bool = True
|
||||
disabled_at: datetime | None = None
|
||||
disabled_reason: str = ""
|
||||
ck_check_status: str = ""
|
||||
ck_check_message: str = ""
|
||||
ck_checked_at: datetime | None = None
|
||||
@@ -496,6 +500,15 @@ class WeiboAccountOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class WeiboAccountUpdate(BaseModel):
|
||||
tag: str | None = Field(None, max_length=64)
|
||||
remark: str | None = Field(None, max_length=256)
|
||||
|
||||
|
||||
class WeiboAccountDisableRequest(BaseModel):
|
||||
reason: str = Field("", max_length=256)
|
||||
|
||||
|
||||
class WeiboImportResult(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
@@ -135,6 +135,64 @@ def refresh_weibo_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,
|
||||
|
||||
Reference in New Issue
Block a user