feat: 更新支付模块 (Stripe/PayPal/PingPong) 和 uni-app 配置
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user