nomadweb/backend/app/routers/payment.py
eric ff7b721acb feat: NomadCNA community layer, social matching, payments, and ebook
Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:38:13 -05:00

133 lines
4.1 KiB
Python

"""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)