"""Payment + order fulfillment — ZPay/XorPay from NomadCNA.""" from fastapi import APIRouter, Header, HTTPException, Query, Request from fastapi.responses import PlainTextResponse from app.schemas import PayCreateRequest, PayCreateResponse, PayStatusResponse from app.services.auth import get_user_by_token from app.services import social_store from app.services.payment_service import ( DEV_AUTO_PAY, PAYMENT_DEFAULT_AMOUNT, PAYMENT_JOIN_AMOUNT, create_pay_redirect, parse_notify_order_id, query_order_paid, verify_notify, ) router = APIRouter(prefix="/pay", tags=["payment"]) def _token_user(authorization: str | None) -> dict: if not authorization or not authorization.startswith("Bearer "): raise HTTPException(401, "未登录") user = get_user_by_token(authorization[7:]) if not user: raise HTTPException(401, "未登录") return user def _client_ip(request: Request) -> str: forwarded = request.headers.get("x-forwarded-for", "") if forwarded: return forwarded.split(",")[0].strip() if request.client: return request.client.host return "" @router.post("/create", response_model=PayCreateResponse) async def create_payment( body: PayCreateRequest, request: Request, authorization: str | None = Header(None), ): user = _token_user(authorization) amount = PAYMENT_JOIN_AMOUNT if body.pay_type == "join" else PAYMENT_DEFAULT_AMOUNT order = social_store.create_order(user["id"], body.pay_type, amount, dev_auto_pay=DEV_AUTO_PAY) return_url = body.return_url or "https://nomadweb.nomadro.com/join/paid" ua = request.headers.get("user-agent", "") device = "wechat" if "micromessenger" in ua.lower() else "pc" redirect = await create_pay_redirect( order["id"], amount, return_url, body.provider or "", channel="wxpay" if device == "wechat" else "alipay", device=device, user_agent=ua, client_ip=_client_ip(request), ) return PayCreateResponse( order_id=order["id"], amount=amount, redirect_url=redirect, status=order["status"], ) @router.get("/status", response_model=PayStatusResponse) async def pay_status(order_id: str = Query(...), authorization: str | None = Header(None)): _token_user(authorization) order = social_store.get_order(order_id) if not order: raise HTTPException(404, "订单不存在") if order["status"] != "paid": if DEV_AUTO_PAY or await query_order_paid(order_id): social_store.mark_order_paid(order_id) order = social_store.get_order(order_id) return PayStatusResponse( order_id=order_id, status=order["status"], paid=order["status"] == "paid", ) @router.post("/complete") async def complete_order( order_id: str = Query(...), authorization: str | None = Header(None), ): user = _token_user(authorization) order = social_store.get_order(order_id) if not order: raise HTTPException(404, "订单不存在") if order["userId"] != user["id"]: raise HTTPException(403, "无权操作此订单") if order["status"] != "paid": if DEV_AUTO_PAY or await query_order_paid(order_id): social_store.mark_order_paid(order_id) else: raise HTTPException(402, "订单未支付") return {"success": True, "vip": social_store.is_vip(user["id"])} async def _notify_handler(provider: str, request: Request) -> PlainTextResponse: if request.method == "GET": data = dict(request.query_params) else: form = await request.form() data = dict(form) if not verify_notify(provider, data): raise HTTPException(400, "验签失败") oid = parse_notify_order_id(provider, data) if oid: social_store.mark_order_paid(oid) return PlainTextResponse("success" if provider == "zpay" else "ok") @router.api_route("/zpay_notify", methods=["GET", "POST"]) async def zpay_notify(request: Request): return await _notify_handler("zpay", request) @router.api_route("/xorpay_notify", methods=["GET", "POST"]) async def xorpay_notify(request: Request): return await _notify_handler("xorpay", request) @router.api_route("/notify", methods=["GET", "POST"]) async def pay_notify_legacy(request: Request, provider: str = Query("zpay")): return await _notify_handler(provider, request)