feat: 更新支付模块 (Stripe/PayPal/PingPong) 和 uni-app 配置

This commit is contained in:
TradeMate Dev
2026-06-16 13:32:50 +08:00
parent e5b1e7d588
commit 15d172e825
17 changed files with 1254 additions and 12 deletions
+57
View File
@@ -1,3 +1,4 @@
import json
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from typing import Optional
@@ -15,6 +16,13 @@ class PurchaseRequest(BaseModel):
pay_type: str = "alipay"
class StripePurchaseRequest(BaseModel):
package_id: str
gateway: str = "stripe"
success_url: str = "https://trade.yuzhiran.com/workspace/credits?pay=success"
cancel_url: str = "https://trade.yuzhiran.com/workspace/credits?pay=cancel"
class SubscribeRequest(BaseModel):
plan_id: str
pay_type: str = "alipay"
@@ -103,6 +111,55 @@ async def subscribe_plan(
return order
@router.post("/stripe-purchase")
async def stripe_purchase(
req: StripePurchaseRequest,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
svc = CreditService(db)
packages = await svc.get_packages()
pkg = next((p for p in packages if p["id"] == req.package_id), None)
if not pkg:
raise HTTPException(status_code=404, detail="次数包不存在")
from app.services.payment import get_gateway, gen_order_no
price_usd = pkg.get("price_usd") or round(pkg["price"] / 7, 2)
amount_cents = int(price_usd * 100)
order_no = gen_order_no(user_id)
gw = get_gateway(req.gateway)
sep = '&' if '?' in req.success_url else '?'
success_url = f"{req.success_url}{sep}order_id={order_no}"
gw_result = await gw.create_order(
order_no, amount_cents, f"{pkg['name_en']} ({pkg['credits']} credits)",
pay_type=req.gateway,
success_url=success_url,
cancel_url=req.cancel_url,
)
from app.models.payment_transaction import PaymentTransaction
txn = PaymentTransaction(
user_id=user_id, order_no=order_no, plan="credit_purchase",
amount=price_usd, gateway=req.gateway, pay_type=req.gateway,
status="pending", description=json.dumps({"credits": pkg["credits"]}),
gateway_order_no=gw_result.get("session_id", ""),
)
db.add(txn)
await db.flush()
return {
"status": "pending",
"order_id": order_no,
"session_url": gw_result.get("session_url"),
"session_id": gw_result.get("session_id"),
"amount": price_usd,
"currency": "USD",
"gateway": req.gateway,
}
@router.post("/cancel-subscription")
async def cancel_subscription(
user_id: str = Depends(get_current_user_id),
+32
View File
@@ -23,6 +23,11 @@ class AnalyzeRequest(BaseModel):
product_description: str
class MarketIntelRequest(BaseModel):
product_description: str
target_market: str = "US"
class OutreachRequest(BaseModel):
company: Dict[str, Any]
product: Dict[str, Any]
@@ -102,6 +107,33 @@ async def analyze_company(
raise HTTPException(status_code=500, detail="分析失败,请稍后重试")
@router.post("/market-intel")
async def market_intel(
req: MarketIntelRequest,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
if not req.product_description.strip():
raise HTTPException(status_code=400, detail="请填写产品描述")
credit_svc = CreditService(db)
ok, balance = await credit_svc.deduct(user_id, "market_intel")
if not ok:
raise HTTPException(
status_code=402,
detail=f"次数不足 (剩余 {balance:.1f}, 需要 20)"
)
svc = DiscoveryService(db=db)
try:
result = await svc.market_intel(req.product_description, req.target_market)
return {"success": True, "data": result, "credits_remaining": balance - 20}
except Exception as e:
await credit_svc.add_credits(user_id, 20, "refund", "市场分析失败退回次数")
logger.error(f"Market intel failed: {e}")
raise HTTPException(status_code=500, detail="分析失败,请稍后重试")
@router.post("/outreach")
async def generate_outreach(
req: OutreachRequest,
+137 -3
View File
@@ -1,12 +1,15 @@
from fastapi import APIRouter, Depends, HTTPException, Request, Query
import json
import logging
from fastapi import APIRouter, Depends, HTTPException, Request, Query, Header
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel
from typing import Optional
from app.database import get_db
from app.services.payment import PaymentService
from app.services.payment import PaymentService, GATEWAY_MAP
from app.services.unified_pay import UnifiedPayService
from app.models.payment_transaction import PaymentTransaction
from app.api.v1.deps import get_current_user_id
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -144,3 +147,134 @@ async def unified_webhook(request: Request, db: AsyncSession = Depends(get_db)):
amount, body_str,
)
return {"code": 0, "message": "OK"}
@router.post("/stripe-webhook")
async def stripe_webhook(
request: Request,
db: AsyncSession = Depends(get_db),
):
body = await request.body()
body_str = body.decode("utf-8")
stripe_gw = GATEWAY_MAP.get("stripe")
if not stripe_gw:
raise HTTPException(status_code=501, detail="Stripe 未配置")
if not stripe_gw.verify_callback(dict(request.headers), body_str):
raise HTTPException(status_code=403, detail="Stripe 签名验证失败")
parsed = stripe_gw.parse_callback(body_str, dict(request.headers))
if parsed.get("success"):
svc = PaymentService(db)
await svc.handle_callback(
parsed["order_no"],
parsed["gateway_order_id"],
parsed["gateway_order_no"],
True,
parsed["amount"],
body_str,
)
return {"status": "ok"}
@router.post("/paypal-capture")
async def paypal_capture(
request: Request,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
body = await request.json()
order_no = body.get("order_no", "")
token = body.get("token", "")
if not order_no or not token:
raise HTTPException(status_code=400, detail="缺少参数")
txn_result = await db.execute(
select(PaymentTransaction).where(
PaymentTransaction.order_no == order_no,
PaymentTransaction.user_id == user_id,
)
)
txn = txn_result.scalar_one_or_none()
if not txn:
raise HTTPException(status_code=404, detail="订单不存在")
if txn.status != "pending":
return {"status": "ok", "message": "已处理"}
paypal_gw = GATEWAY_MAP.get("paypal")
if not paypal_gw:
raise HTTPException(status_code=501, detail="PayPal 未配置")
try:
result = await paypal_gw.capture_order(token)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if result.get("completed"):
capture_id = result.get("capture_id", token)
svc = PaymentService(db)
await svc.handle_callback(
order_no, token, capture_id, True, txn.amount, json.dumps(result)
)
return {"status": "completed", "order_no": order_no}
raise HTTPException(status_code=400, detail=f"PayPal capture failed: {result.get('status')}")
@router.post("/paypal-webhook")
async def paypal_webhook(
request: Request,
db: AsyncSession = Depends(get_db),
):
body = await request.body()
body_str = body.decode("utf-8")
paypal_gw = GATEWAY_MAP.get("paypal")
if not paypal_gw:
raise HTTPException(status_code=501, detail="PayPal 未配置")
if not paypal_gw.verify_callback(dict(request.headers), body_str):
raise HTTPException(status_code=403, detail="PayPal 签名验证失败")
parsed = paypal_gw.parse_callback(body_str, dict(request.headers))
if parsed.get("success"):
svc = PaymentService(db)
await svc.handle_callback(
parsed["order_no"],
parsed["gateway_order_id"],
parsed["gateway_order_no"],
True,
parsed["amount"],
body_str,
)
return {"status": "ok"}
@router.post("/pingpong-webhook")
async def pingpong_webhook(
request: Request,
db: AsyncSession = Depends(get_db),
):
body = await request.body()
body_str = body.decode("utf-8")
pp_gw = GATEWAY_MAP.get("pingpong")
if not pp_gw:
raise HTTPException(status_code=501, detail="PingPong 未配置")
if not pp_gw.verify_callback(dict(request.headers), body_str):
raise HTTPException(status_code=403, detail="PingPong 签名验证失败")
parsed = pp_gw.parse_callback(body_str, dict(request.headers))
if parsed.get("success"):
svc = PaymentService(db)
await svc.handle_callback(
parsed["order_no"],
parsed["gateway_order_id"],
parsed["gateway_order_no"],
True,
parsed["amount"],
body_str,
)
return {"status": "ok"}