146 lines
4.5 KiB
Python
146 lines
4.5 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_EBOOK_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)
|
|
if body.pay_type == "ebook":
|
|
amount = PAYMENT_EBOOK_AMOUNT
|
|
product_name = "nomadro电子书下载版"
|
|
elif body.pay_type in ("join", "vip"):
|
|
amount = PAYMENT_JOIN_AMOUNT
|
|
product_name = "nomadro会员"
|
|
else:
|
|
amount = PAYMENT_DEFAULT_AMOUNT
|
|
product_name = "nomadro订单"
|
|
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"
|
|
try:
|
|
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),
|
|
product_name=product_name,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(503, str(exc)) from exc
|
|
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"]), "ebook": social_store.has_ebook(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)
|