优化了部分支付宝界面
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"""应用宝支付元数据字段:金额与支付时间线"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260812_0022"
|
||||
down_revision: Union[str, None] = "20260812_0021"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("yyb_recharge_tasks", sa.Column("price_fen", sa.Integer(), nullable=True))
|
||||
op.add_column("yyb_recharge_tasks", sa.Column("payment_started_at", sa.DateTime(), nullable=True))
|
||||
op.add_column("yyb_recharge_tasks", sa.Column("payment_qr_created_at", sa.DateTime(), nullable=True))
|
||||
op.add_column("yyb_recharge_tasks", sa.Column("payment_last_checked_at", sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("yyb_recharge_tasks", "payment_last_checked_at")
|
||||
op.drop_column("yyb_recharge_tasks", "payment_qr_created_at")
|
||||
op.drop_column("yyb_recharge_tasks", "payment_started_at")
|
||||
op.drop_column("yyb_recharge_tasks", "price_fen")
|
||||
@@ -144,12 +144,16 @@ class YybRechargeTask(Base):
|
||||
zone_name = Column(String(128), default="")
|
||||
role_id = Column(String(64), default="")
|
||||
role_name = Column(String(128), default="")
|
||||
price_fen = Column(Integer, nullable=True)
|
||||
status = Column(String(32), default="created", nullable=False, index=True)
|
||||
phase = Column(String(32), default="login", nullable=False)
|
||||
message = Column(String(512), default="")
|
||||
result = Column(JSON, nullable=True)
|
||||
login_qr_data = Column(EncryptedText(), default="")
|
||||
payment_qr_data = Column(EncryptedText(), default="")
|
||||
payment_started_at = Column(DateTime, nullable=True)
|
||||
payment_qr_created_at = Column(DateTime, nullable=True)
|
||||
payment_last_checked_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ PERMISSIONS = {
|
||||
"douyu:config": "斗鱼配置管理",
|
||||
"yyb:session": "应用宝扫码登录与角色查询",
|
||||
"yyb:recharge": "应用宝充值与付款",
|
||||
"yyb:manage": "接管并操作他人的应用宝任务",
|
||||
"yyb:history": "查看应用宝充值历史",
|
||||
# 虎牙
|
||||
"huya:account": "虎牙账号管理(兼容旧权限)",
|
||||
@@ -67,6 +68,7 @@ ROLE_PERMISSIONS = {
|
||||
"douyu:config",
|
||||
"yyb:session",
|
||||
"yyb:recharge",
|
||||
"yyb:manage",
|
||||
"yyb:history",
|
||||
"huya:account",
|
||||
"huya:view_all",
|
||||
|
||||
+94
-25
@@ -12,18 +12,29 @@ 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 public_task, sync_task
|
||||
from ..services.yyb_service import _utcnow, public_task, sync_task
|
||||
from ..services.yyb_worker_client import YybWorkerClient, YybWorkerError
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/yyb", tags=["应用宝充值"])
|
||||
|
||||
|
||||
def _get_task(db: Session, task_id: int, current: User) -> YybRechargeTask:
|
||||
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 and not user_has_permission(current, "yyb:history"):
|
||||
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
|
||||
|
||||
@@ -35,6 +46,11 @@ def _worker_call(call):
|
||||
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)
|
||||
@@ -44,12 +60,12 @@ def create_task(payload: YybTaskCreateRequest, db: Session = Depends(get_db), cu
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return public_task(task, include_qr=False)
|
||||
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)
|
||||
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"))
|
||||
@@ -58,7 +74,7 @@ def login(task_id: int, payload: YybLoginRequest, db: Session = Depends(get_db),
|
||||
if data.get("qr_data"):
|
||||
task.login_qr_data = data["qr_data"]
|
||||
db.commit()
|
||||
return public_task(task)
|
||||
return public_task(task, creator_username=_creator_username(db, task))
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}")
|
||||
@@ -70,18 +86,34 @@ def get_task(task_id: int, db: Session = Depends(get_db), current: User = Depend
|
||||
# Worker 暂时重启时仍返回最近一次持久化状态。
|
||||
pass
|
||||
return public_task(task, include_qr=user_has_permission(current, "yyb:session"),
|
||||
include_payment_qr=user_has_permission(current, "yyb:recharge"))
|
||||
include_payment_qr=user_has_permission(current, "yyb:recharge"),
|
||||
creator_username=_creator_username(db, task))
|
||||
|
||||
|
||||
@router.get("/tasks")
|
||||
def list_tasks(limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), current: User = Depends(require_permission("yyb:history"))):
|
||||
tasks = db.query(YybRechargeTask).order_by(YybRechargeTask.id.desc()).limit(limit).all()
|
||||
return [public_task(task, include_qr=False) for task in 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)
|
||||
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()
|
||||
@@ -90,35 +122,72 @@ def selection_options(task_id: int, platform: str = Query("android"), points: in
|
||||
|
||||
@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)
|
||||
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.phase, task.status, task.message = "payment", "ready", "选择已保存,可以创建付款码"
|
||||
task.price_fen = int(selected.get("price_fen") or 0)
|
||||
task.phase, task.status, task.message = "payment", "ready", "选择已保存,可以生成付款码"
|
||||
db.commit()
|
||||
return public_task(task)
|
||||
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"))):
|
||||
task = _get_task(db, task_id, current)
|
||||
# 接手他人任务需 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, "请先完成商品、区服和角色选择")
|
||||
data = _worker_call(lambda: YybWorkerClient().payment(task.worker_job_id))
|
||||
task.status = str(data.get("status", "running"))
|
||||
task.phase = "payment"
|
||||
task.message = "正在创建订单和付款码"
|
||||
task.status = "ordering"
|
||||
task.message = "正在创建商城订单"
|
||||
task.payment_started_at = _utcnow()
|
||||
db.commit()
|
||||
return public_task(task)
|
||||
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
|
||||
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))
|
||||
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"
|
||||
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)
|
||||
task = _get_task(db, task_id, current, write=True)
|
||||
data = _worker_call(lambda: YybWorkerClient().stop(task.worker_job_id))
|
||||
task.status = "failed"
|
||||
task.phase = "stopped"
|
||||
task.message = "任务已停止"
|
||||
task.status = "stopped"
|
||||
task.phase = str(data.get("phase", "stopped"))
|
||||
task.message = str(data.get("message", "任务已停止"))
|
||||
task.finished_at = _utcnow()
|
||||
db.commit()
|
||||
return public_task(task)
|
||||
return public_task(task, creator_username=_creator_username(db, task))
|
||||
|
||||
@@ -15,12 +15,27 @@ def _utcnow():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _from_ts(value) -> datetime | None:
|
||||
try:
|
||||
return datetime.fromtimestamp(int(value), tz=timezone.utc) if value else None
|
||||
except (TypeError, ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_orphan_yyb_tasks(db: Session, message: str) -> int:
|
||||
rows = db.query(YybRechargeTask).filter(YybRechargeTask.status.in_(["created", "waiting_login", "running", "ready"])).all()
|
||||
rows = db.query(YybRechargeTask).filter(
|
||||
YybRechargeTask.status.in_(["created", "waiting_login", "ready", "running", "ordering", "waiting_payment"])
|
||||
).all()
|
||||
for task in rows:
|
||||
task.status = "failed"
|
||||
task.message = message
|
||||
task.finished_at = _utcnow()
|
||||
if task.status == "ordering":
|
||||
task.status = "waiting_payment"
|
||||
task.message = "服务重启,支付流程中断,可重新检测到账"
|
||||
elif task.status == "waiting_payment":
|
||||
task.message = "服务重启,到账检测中断,可重新检测到账"
|
||||
else:
|
||||
task.status = "failed"
|
||||
task.message = message
|
||||
task.finished_at = _utcnow()
|
||||
if rows:
|
||||
db.commit()
|
||||
return len(rows)
|
||||
@@ -44,23 +59,36 @@ def sync_task(db: Session, task: YybRechargeTask, worker: YybWorkerClient) -> Yy
|
||||
"logs": data.get("logs", []),
|
||||
"payment_status": data.get("payment_status"),
|
||||
}
|
||||
qr_created_at = _from_ts(data.get("payment_qr_created_at"))
|
||||
if qr_created_at:
|
||||
task.payment_qr_created_at = qr_created_at
|
||||
last_checked_at = _from_ts(data.get("payment_last_checked_at"))
|
||||
if last_checked_at:
|
||||
task.payment_last_checked_at = last_checked_at
|
||||
if task.status in {"success", "failed"} and task.finished_at is None:
|
||||
task.finished_at = _utcnow()
|
||||
if task.status not in {"success", "failed", "stopped"} and task.finished_at is not None:
|
||||
task.finished_at = None
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
def public_task(task: YybRechargeTask, include_qr: bool = True,
|
||||
include_payment_qr: bool | None = None) -> dict[str, Any]:
|
||||
include_payment_qr: bool | None = None,
|
||||
creator_username: str = "") -> dict[str, Any]:
|
||||
result: dict[str, Any] = {
|
||||
"id": task.id, "task_id": task.task_id,
|
||||
"provider": task.provider, "platform": task.platform, "points": task.points,
|
||||
"price_fen": task.price_fen,
|
||||
"product_id": task.product_id, "zone_id": task.zone_id, "zone_name": task.zone_name,
|
||||
"role_id": task.role_id, "role_name": task.role_name, "status": task.status,
|
||||
"phase": task.phase, "message": task.message, "result": task.result,
|
||||
"created_by": task.created_by, "created_at": task.created_at,
|
||||
"finished_at": task.finished_at,
|
||||
"created_by": task.created_by, "created_by_username": creator_username,
|
||||
"created_at": task.created_at, "finished_at": task.finished_at,
|
||||
"payment_started_at": task.payment_started_at,
|
||||
"payment_qr_created_at": task.payment_qr_created_at,
|
||||
"payment_last_checked_at": task.payment_last_checked_at,
|
||||
}
|
||||
if task.result:
|
||||
result["login_qr_mime_type"] = task.result.get("login_qr_mime_type", "image/jpeg")
|
||||
|
||||
@@ -53,5 +53,8 @@ class YybWorkerClient:
|
||||
def payment(self, worker_job_id: str) -> dict[str, Any]:
|
||||
return self._request("POST", f"/v1/jobs/{worker_job_id}/payment")
|
||||
|
||||
def payment_check(self, worker_job_id: str) -> dict[str, Any]:
|
||||
return self._request("POST", f"/v1/jobs/{worker_job_id}/payment/check")
|
||||
|
||||
def stop(self, worker_job_id: str) -> dict[str, Any]:
|
||||
return self._request("POST", f"/v1/jobs/{worker_job_id}/stop")
|
||||
|
||||
Reference in New Issue
Block a user