refactor: replace direct WeChat/Alipay with unified pay-api gateway

Switch from direct WeChat Pay / Alipay integrations to the unified
宇之然 pay-api gateway (HMAC-SHA256 auth). Removes wechat_pay.py,
keeps PaymentGateway abstraction, adds UnifiedPayService. Simplifies
payment.py create_order to {plan, pay_type} params. Single webhook
endpoint replaces separate WeChat/Alipay notify handlers.
This commit is contained in:
TradeMate Dev
2026-05-29 18:36:50 +08:00
parent 5d2bced39f
commit 3e39cf0170
34 changed files with 973 additions and 424 deletions
+39
View File
@@ -8,6 +8,7 @@ from app.services.admin import AdminService
from app.services.translation_quota import TranslationQuotaService
from app.services.certification import CertificationService
from app.services.invoice import InvoiceService
from app.services.payment import PaymentService
from app.api.v1.deps import get_current_user
router = APIRouter()
@@ -274,3 +275,41 @@ async def admin_process_invoice(
if not result:
raise HTTPException(status_code=404, detail="Invoice not found")
return result
@router.get("/payments")
async def admin_list_payments(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
gateway: str = Query(default=""),
status: str = Query(default=""),
user_id: str = Query(default=""),
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
svc = PaymentService(db)
return await svc.admin_list_payments(page, size, gateway, status, user_id)
@router.get("/payments/stats")
async def admin_payment_stats(
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
svc = PaymentService(db)
return await svc.admin_payment_stats()
@router.post("/payments/refund")
async def admin_refund(
data: dict,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
order_no = data.get("order_no", "")
reason = data.get("reason", "")
svc = PaymentService(db)
try:
return await svc.admin_refund(order_no, reason)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
+55 -33
View File
@@ -1,10 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Request, Header
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from typing import Optional
from app.database import get_db
from app.services.payment import PaymentService
from app.services.wechat_pay import WeChatPayService
from app.api.v1.deps import get_current_user_id
from app.core.csrf import require_csrf_token
@@ -13,12 +12,12 @@ router = APIRouter()
class CreateOrderRequest(BaseModel):
plan: str
pay_type: str = "jsapi"
pay_type: str = "alipay"
class PaymentCallbackRequest(BaseModel):
payment_id: str
success: bool
class RefundRequest(BaseModel):
order_no: str
reason: str = ""
@router.get("/plans")
@@ -50,42 +49,65 @@ async def create_order(
raise HTTPException(status_code=400, detail=str(e))
@router.post("/callback")
async def payment_callback(
data: PaymentCallbackRequest,
@router.get("/query/{order_no}")
async def query_payment(
order_no: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
svc = PaymentService(db)
try:
return await svc.query_payment(user_id, order_no)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.get("/transactions")
async def list_transactions(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
svc = PaymentService(db)
return await svc.list_transactions(user_id, page, size)
@router.post("/refund")
async def refund(
data: RefundRequest,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
svc = PaymentService(db)
success = await svc.handle_payment_callback(data.payment_id, data.success)
if not success:
raise HTTPException(status_code=404, detail="Order not found")
return {"status": "ok"}
try:
return await svc.refund(user_id, data.order_no, data.reason)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/notify")
async def wechat_pay_notify(request: Request, db: AsyncSession = Depends(get_db)):
@router.post("/webhook")
async def unified_webhook(request: Request, db: AsyncSession = Depends(get_db)):
body = await request.body()
body_str = body.decode("utf-8")
headers = dict(request.headers)
wxpay = WeChatPayService()
if not wxpay.verify_callback(headers, body_str):
raise HTTPException(status_code=401, detail="签名验证失败")
import json
data = json.loads(body_str)
resource = data.get("resource", {})
ciphertext = resource.get("ciphertext", "")
nonce = resource.get("nonce", "")
associated_data = resource.get("associated_data", "")
try:
data = json.loads(body_str)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="无效的 JSON")
plaintext = wxpay.decrypt_callback(ciphertext, nonce, associated_data)
pay_data = json.loads(plaintext)
out_trade_no = pay_data.get("out_trade_no", "")
trade_state = pay_data.get("trade_state", "")
event = data.get("event", "")
pay_data = data.get("data", {})
merchant_order_id = pay_data.get("merchant_order_id", "")
order_id = pay_data.get("order_id", "")
transaction_id = pay_data.get("transaction_id", "")
amount = pay_data.get("amount", 0)
success = event == "recharge.completed"
success = trade_state == "SUCCESS"
svc = PaymentService(db)
await svc.handle_payment_callback(out_trade_no, success)
return {"code": "SUCCESS", "message": "OK"}
await svc.handle_callback(
merchant_order_id, order_id, transaction_id,
success, amount, body_str,
)
return {"code": 0, "message": "OK"}