"""应用宝和平精英充值工作台 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))