Files
live-hub-py/web/backend/routers/yyb.py
T

356 lines
11 KiB
Python

"""应用宝和平精英充值工作台 API。"""
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_permission
from ..models import User, YybRechargeTask
from ..permissions import user_has_permission
from ..schemas import YybLoginRequest, YybSelectionRequest, YybTaskCreateRequest
from ..services.yyb_service import _utcnow, public_task, sync_task
from ..services.yyb_worker_client import YybWorkerClient, YybWorkerError
from ..services.audit_service import record_audit
router = APIRouter(prefix="/api/yyb", tags=["应用宝充值"])
def _get_task(
db: Session, task_id: int, current: User, write: bool = False
) -> YybRechargeTask:
"""读取任务。
查看:本人或 yyb:history;写操作:本人或 yyb:manage。
历史权限只读,不能通过本函数修改他人任务。
"""
task = db.query(YybRechargeTask).filter(YybRechargeTask.id == task_id).first()
if not task:
raise HTTPException(404, "充值任务不存在")
if task.created_by == current.id:
return task
if write:
if not user_has_permission(current, "yyb:manage"):
raise HTTPException(403, "无权操作他人的充值任务")
return task
if not user_has_permission(current, "yyb:history"):
raise HTTPException(403, "无权查看该充值任务")
return task
def _worker_call(call):
try:
return call()
except YybWorkerError as exc:
raise HTTPException(502, str(exc)) from exc
def _creator_username(db: Session, task: YybRechargeTask) -> str:
user = db.query(User).filter(User.id == task.created_by).first()
return user.username if user else ""
@router.post("/tasks")
def create_task(
payload: YybTaskCreateRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
data = _worker_call(YybWorkerClient().create_job)
task = YybRechargeTask(
task_id=uuid.uuid4().hex[:16],
created_by=current.id,
worker_job_id=str(data["job_id"]),
status=str(data.get("status", "created")),
phase="login",
message="请选择登录方式",
)
db.add(task)
record_audit(
db,
current,
action="recharge:yyb:create",
target=f"yyb_task:{task.task_id}",
detail={"task_id": task.task_id, "status": task.status},
)
db.commit()
db.refresh(task)
return public_task(task, include_qr=False, creator_username=current.username)
@router.post("/tasks/{task_id}/login")
def login(
task_id: int,
payload: YybLoginRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
task = _get_task(db, task_id, current, write=True)
data = _worker_call(
lambda: YybWorkerClient().login(
task.worker_job_id, payload.provider, payload.timeout
)
)
task.provider = payload.provider
task.status = str(data.get("status", "waiting_login"))
task.phase = "login"
task.message = "请扫码登录并在手机确认"
if data.get("qr_data"):
task.login_qr_data = data["qr_data"]
record_audit(
db,
current,
action="recharge:yyb:login",
target=f"yyb_task:{task.task_id}",
detail={
"task_id": task.task_id,
"provider": payload.provider,
"status": task.status,
},
)
db.commit()
return public_task(task, creator_username=_creator_username(db, task))
@router.get("/tasks/{task_id}")
def get_task(
task_id: int,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
task = _get_task(db, task_id, current)
try:
sync_task(db, task, YybWorkerClient())
except YybWorkerError:
# Worker 暂时重启时仍返回最近一次持久化状态。
pass
return public_task(
task,
include_qr=user_has_permission(current, "yyb:session"),
include_payment_qr=user_has_permission(current, "yyb:recharge"),
creator_username=_creator_username(db, task),
)
@router.get("/tasks")
def list_tasks(
scope: str = Query("mine", pattern="^(mine|all)$"),
status: str | None = Query(None, max_length=32),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
if scope == "all" and not user_has_permission(current, "yyb:history"):
raise HTTPException(403, "无权查看全部充值任务")
query = db.query(YybRechargeTask)
if scope == "mine":
query = query.filter(YybRechargeTask.created_by == current.id)
if status:
query = query.filter(YybRechargeTask.status == status)
rows = query.order_by(YybRechargeTask.id.desc()).limit(limit).all()
usernames = {
user.id: user.username
for user in db.query(User)
.filter(User.id.in_({row.created_by for row in rows}))
.all()
}
return [
public_task(
task, include_qr=False, creator_username=usernames.get(task.created_by, "")
)
for task in rows
]
@router.get("/tasks/{task_id}/selection-options")
def selection_options(
task_id: int,
platform: str = Query("android"),
points: int | None = Query(None),
zone_id: str | None = Query(None),
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
task = _get_task(db, task_id, current, write=True)
data = _worker_call(
lambda: YybWorkerClient().selection_options(
task.worker_job_id, platform, points, zone_id
)
)
task.platform = platform
db.commit()
return data
@router.post("/tasks/{task_id}/selection")
def selection(
task_id: int,
payload: YybSelectionRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
task = _get_task(db, task_id, current, write=True)
data = _worker_call(
lambda: YybWorkerClient().selection(task.worker_job_id, payload.model_dump())
)
selected = data.get("selection", payload.model_dump())
for field in (
"platform",
"points",
"product_id",
"zone_id",
"zone_name",
"role_id",
"role_name",
):
setattr(task, field, selected[field])
task.price_fen = int(selected.get("price_fen") or 0)
task.phase, task.status, task.message = (
"payment",
"ready",
"选择已保存,可以生成付款码",
)
record_audit(
db,
current,
action="recharge:yyb:selection",
target=f"yyb_task:{task.task_id}",
detail={
"task_id": task.task_id,
"product_id": task.product_id,
"points": task.points,
"zone_id": task.zone_id,
"zone_name": task.zone_name,
"role_id": task.role_id,
"role_name": task.role_name,
"price_fen": task.price_fen,
},
)
db.commit()
return public_task(task, creator_username=_creator_username(db, task))
@router.post("/tasks/{task_id}/payment")
def payment(
task_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:recharge")),
):
# 接手他人任务需 yyb:manage;锁行做原子状态迁移,防止并发双击重复下单。
_get_task(db, task_id, current, write=True)
task = (
db.query(YybRechargeTask)
.filter(YybRechargeTask.id == task_id)
.with_for_update()
.first()
)
if not task:
raise HTTPException(404, "充值任务不存在")
if task.status != "ready" or task.phase != "payment":
raise HTTPException(409, "当前状态不能生成付款码(仅待生成付款码状态可操作)")
if not task.product_id or not task.role_id:
raise HTTPException(400, "请先完成商品、区服和角色选择")
task.status = "ordering"
task.message = "正在创建商城订单"
task.payment_started_at = _utcnow()
db.commit()
try:
data = _worker_call(lambda: YybWorkerClient().payment(task.worker_job_id))
except HTTPException:
# Worker 不可用时回滚原子迁移,避免任务卡死在“下单中”。
task.status = "ready"
task.phase = "payment"
task.message = "生成付款码失败,请稍后重试"
task.payment_started_at = None
record_audit(
db,
current,
action="recharge:yyb:payment",
target=f"yyb_task:{task.task_id}",
detail="生成付款码失败",
success=False,
)
db.commit()
raise
task.status = str(data.get("status", "ordering"))
task.phase = str(data.get("phase", "payment"))
task.message = str(data.get("message", task.message))
record_audit(
db,
current,
action="recharge:yyb:payment",
target=f"yyb_task:{task.task_id}",
detail={
"task_id": task.task_id,
"product_id": task.product_id,
"points": task.points,
"price_fen": task.price_fen,
"status": task.status,
},
)
db.commit()
return public_task(
task, include_qr=False, creator_username=_creator_username(db, task)
)
@router.post("/tasks/{task_id}/payment/check")
def payment_check(
task_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:recharge")),
):
task = _get_task(db, task_id, current, write=True)
if task.status not in {"waiting_payment", "payment_timeout"}:
raise HTTPException(409, "当前任务状态不支持检测到账")
data = _worker_call(lambda: YybWorkerClient().payment_check(task.worker_job_id))
task.status = str(data.get("status", task.status))
task.phase = str(data.get("phase", task.phase))
task.message = str(data.get("message", task.message))
task.payment_last_checked_at = _utcnow()
if task.status == "success":
task.phase = "completed"
record_audit(
db,
current,
action="recharge:yyb:payment_check",
target=f"yyb_task:{task.task_id}",
detail={
"task_id": task.task_id,
"status": task.status,
"message": task.message,
},
success=task.status != "failed",
)
db.commit()
return public_task(
task, include_qr=False, creator_username=_creator_username(db, task)
)
@router.post("/tasks/{task_id}/stop")
def stop(
task_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("yyb:session")),
):
task = _get_task(db, task_id, current, write=True)
data = _worker_call(lambda: YybWorkerClient().stop(task.worker_job_id))
task.status = "stopped"
task.phase = str(data.get("phase", "stopped"))
task.message = str(data.get("message", "任务已停止"))
task.finished_at = _utcnow()
record_audit(
db,
current,
action="recharge:yyb:stop",
target=f"yyb_task:{task.task_id}",
detail={"task_id": task.task_id, "status": task.status},
)
db.commit()
return public_task(task, creator_username=_creator_username(db, task))