feat: 更新支付模块 (Stripe/PayPal/PingPong) 和 uni-app 配置
This commit is contained in:
@@ -92,6 +92,61 @@ URL: {company_url}
|
||||
logger.warning(f"Analysis AI parse failed: {e}")
|
||||
return self._template_analysis(company_url)
|
||||
|
||||
async def market_intel(self, product_description: str, target_market: str) -> Dict[str, Any]:
|
||||
queries = self._build_queries(product_description, target_market)
|
||||
search_results = await self._web_search_all(queries[:3])
|
||||
companies = search_results.get("results", [])[:10] if search_results else []
|
||||
|
||||
if not self._ai_available:
|
||||
return {
|
||||
"market": target_market,
|
||||
"product": product_description,
|
||||
"trends": [],
|
||||
"competitors": [],
|
||||
"opportunities": "找到 {} 家潜在客户".format(len(companies)),
|
||||
}
|
||||
|
||||
company_summary = "\n".join(
|
||||
f"- {c.get('title','')}: {c.get('snippet','')[:200]}"
|
||||
for c in companies[:5]
|
||||
)
|
||||
|
||||
prompt = f"""产品: {product_description}
|
||||
目标市场: {target_market}
|
||||
|
||||
搜索到的相关公司:
|
||||
{company_summary}
|
||||
|
||||
请提供该市场的详细分析报告,以 JSON 格式返回:
|
||||
{{
|
||||
"market_size": "市场规模评估",
|
||||
"trends": ["趋势1", "趋势2", "趋势3"],
|
||||
"competitors": [{{"name": "竞品名", "strength": "优势", "weakness": "劣势"}}],
|
||||
"opportunities": ["机会点1", "机会点2"],
|
||||
"challenges": ["挑战1", "挑战2"],
|
||||
"entry_strategy": "进入市场建议",
|
||||
"price_range": "价格区间参考",
|
||||
"regulatory_notes": "法规注意事项"
|
||||
}}"""
|
||||
|
||||
try:
|
||||
result = await self.ai.chat(prompt,
|
||||
system_prompt="你是资深国际贸易市场分析师。只返回 JSON,不要其他内容。")
|
||||
content = result.get("reply", "")
|
||||
parsed = self._extract_json(content)
|
||||
if not parsed:
|
||||
raise ValueError("Failed to parse AI response")
|
||||
return {**parsed, "market": target_market, "product": product_description}
|
||||
except Exception as e:
|
||||
logger.error(f"Market intel failed: {e}")
|
||||
return {
|
||||
"market": target_market,
|
||||
"product": product_description,
|
||||
"trends": ["分析暂时不可用"],
|
||||
"competitors": [],
|
||||
"opportunities": f"找到 {len(companies)} 家相关公司",
|
||||
}
|
||||
|
||||
async def outreach(self, company_info: Dict[str, Any], product_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not self._ai_available:
|
||||
return self._template_outreach(company_info, product_info)
|
||||
|
||||
@@ -35,14 +35,29 @@ GATEWAY_MAP: Dict[str, PaymentGateway] = {}
|
||||
def init_gateways():
|
||||
if settings.PAY_API_KEY:
|
||||
GATEWAY_MAP["unified"] = UnifiedPayService()
|
||||
if settings.STRIPE_SECRET_KEY:
|
||||
from app.services.stripe_pay import StripePaymentService
|
||||
GATEWAY_MAP["stripe"] = StripePaymentService()
|
||||
if settings.PAYPAL_CLIENT_ID and settings.PAYPAL_CLIENT_SECRET:
|
||||
from app.services.paypal_pay import PayPalPaymentService
|
||||
GATEWAY_MAP["paypal"] = PayPalPaymentService()
|
||||
if settings.PINGPONG_CLIENT_ID and settings.PINGPONG_ACC_ID and settings.PINGPONG_SECRET_KEY:
|
||||
from app.services.pingpong_pay import PingPongCheckoutService
|
||||
GATEWAY_MAP["pingpong"] = PingPongCheckoutService()
|
||||
|
||||
|
||||
def get_gateway(pay_type: str) -> PaymentGateway:
|
||||
gw = GATEWAY_MAP.get("unified")
|
||||
if pay_type in ("card", "stripe", "alipay_stripe", "wechat_stripe"):
|
||||
gw = GATEWAY_MAP.get("stripe") or gw
|
||||
if pay_type in ("paypal",):
|
||||
gw = GATEWAY_MAP.get("paypal") or gw
|
||||
if pay_type in ("pingpong",):
|
||||
gw = GATEWAY_MAP.get("pingpong") or gw
|
||||
if not gw:
|
||||
raise ValueError("支付网关未配置,请设置 PAY_API_KEY")
|
||||
raise ValueError("支付网关未配置,请设置 PAY_API_KEY / STRIPE_SECRET_KEY / PAYPAL_CLIENT_ID / PINGPONG 配置")
|
||||
if not gw.supports(pay_type):
|
||||
raise ValueError(f"支付方式 {pay_type} 不被支持(仅支持 alipay/wechat)")
|
||||
raise ValueError(f"支付方式 {pay_type} 不被支持")
|
||||
return gw
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import httpx
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from app.config import settings
|
||||
from app.services.payment_gateway import PaymentGateway
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAYPAL_API_BASE = "https://api-m.paypal.com"
|
||||
PAYPAL_API_SANDBOX = "https://api-m.sandbox.paypal.com"
|
||||
|
||||
|
||||
class PayPalPaymentService(PaymentGateway):
|
||||
name = "paypal"
|
||||
supported_types = ["paypal", "card"]
|
||||
|
||||
def __init__(self):
|
||||
self.client_id = settings.PAYPAL_CLIENT_ID or ""
|
||||
self.client_secret = settings.PAYPAL_CLIENT_SECRET or ""
|
||||
self.webhook_id = settings.PAYPAL_WEBHOOK_ID or ""
|
||||
self.sandbox = settings.PAYPAL_SANDBOX
|
||||
self._base_url = PAYPAL_API_SANDBOX if self.sandbox else PAYPAL_API_BASE
|
||||
self._access_token = None
|
||||
self._token_expires = 0
|
||||
|
||||
def _is_configured(self) -> bool:
|
||||
return bool(self.client_id and self.client_secret)
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
if self._access_token and datetime.utcnow().timestamp() < self._token_expires - 60:
|
||||
return self._access_token
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self._base_url}/v1/oauth2/token",
|
||||
auth=(self.client_id, self.client_secret),
|
||||
data={"grant_type": "client_credentials"},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise ValueError(f"PayPal OAuth failed: {resp.text}")
|
||||
data = resp.json()
|
||||
self._access_token = data["access_token"]
|
||||
self._token_expires = datetime.utcnow().timestamp() + data.get("expires_in", 32400)
|
||||
return self._access_token
|
||||
|
||||
async def create_order(self, order_no: str, amount: int, description: str,
|
||||
**kwargs) -> Dict[str, Any]:
|
||||
if not self._is_configured():
|
||||
raise ValueError("PayPal 未配置")
|
||||
|
||||
token = await self._get_access_token()
|
||||
pay_type = kwargs.get("pay_type", "paypal")
|
||||
success_url = kwargs.get("success_url", "https://trade.yuzhiran.com/workspace/credits?paypal=success")
|
||||
cancel_url = kwargs.get("cancel_url", "https://trade.yuzhiran.com/workspace/credits?paypal=cancel")
|
||||
|
||||
usd_amount = round(amount / 100, 2)
|
||||
|
||||
payload = {
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [{
|
||||
"reference_id": order_no,
|
||||
"description": description[:127],
|
||||
"amount": {
|
||||
"currency_code": "USD",
|
||||
"value": str(usd_amount),
|
||||
"breakdown": {
|
||||
"item_total": {
|
||||
"currency_code": "USD",
|
||||
"value": str(usd_amount)
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": [{
|
||||
"name": description[:127],
|
||||
"unit_amount": {
|
||||
"currency_code": "USD",
|
||||
"value": str(usd_amount)
|
||||
},
|
||||
"quantity": "1",
|
||||
"category": "DIGITAL_GOODS"
|
||||
}]
|
||||
}],
|
||||
"payment_source": {
|
||||
"paypal": {
|
||||
"experience_context": {
|
||||
"payment_method_preference": "IMMEDIATE_PAYMENT_REQUIRED",
|
||||
"landing_page": "LOGIN",
|
||||
"user_action": "PAY_NOW",
|
||||
"return_url": success_url,
|
||||
"cancel_url": cancel_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self._base_url}/v2/checkout/orders",
|
||||
json=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"PayPal-Request-Id": order_no,
|
||||
},
|
||||
)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise ValueError(f"PayPal create order failed: {resp.text}")
|
||||
|
||||
data = resp.json()
|
||||
approval_url = ""
|
||||
for link in data.get("links", []):
|
||||
if link["rel"] == "payer-action":
|
||||
approval_url = link["href"]
|
||||
break
|
||||
|
||||
return {
|
||||
"gateway_order_id": data["id"],
|
||||
"merchant_order_id": order_no,
|
||||
"session_url": approval_url,
|
||||
"session_id": data["id"],
|
||||
"amount": usd_amount,
|
||||
"status": data["status"],
|
||||
}
|
||||
|
||||
async def query_order(self, order_no: str) -> Dict[str, Any]:
|
||||
if not self._is_configured():
|
||||
return {"status": "unknown"}
|
||||
token = await self._get_access_token()
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"{self._base_url}/v2/checkout/orders/{order_no}",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return {"status": "unknown"}
|
||||
data = resp.json()
|
||||
return {
|
||||
"status": data.get("status", "unknown"),
|
||||
"payment_status": "completed" if data.get("status") == "COMPLETED" else data.get("status", ""),
|
||||
"amount": float(data.get("purchase_units", [{}])[0].get("amount", {}).get("value", 0)),
|
||||
"currency": data.get("purchase_units", [{}])[0].get("amount", {}).get("currency_code", "USD"),
|
||||
}
|
||||
|
||||
async def refund(self, order_no: str, amount: int, reason: str = "") -> Dict[str, Any]:
|
||||
token = await self._get_access_token()
|
||||
usd_amount = round(amount / 100, 2)
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self._base_url}/v2/payments/captures/{order_no}/refund",
|
||||
json={
|
||||
"amount": {"currency_code": "USD", "value": str(usd_amount)},
|
||||
"note_to_payer": reason or "Refund",
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise ValueError(f"PayPal refund failed: {resp.text}")
|
||||
data = resp.json()
|
||||
return {"status": data.get("status", "COMPLETED"), "refund_id": data.get("id", "")}
|
||||
|
||||
async def query_refund(self, order_no: str) -> Dict[str, Any]:
|
||||
return {"status": "not_implemented"}
|
||||
|
||||
def verify_callback(self, headers: dict, body: str) -> bool:
|
||||
if not self.webhook_id:
|
||||
logger.warning("PayPal webhook ID not configured")
|
||||
return False
|
||||
|
||||
transmission_id = headers.get("paypal-transmission-id", "")
|
||||
transmission_time = headers.get("paypal-transmission-time", "")
|
||||
cert_url = headers.get("paypal-cert-url", "")
|
||||
actual_sig = headers.get("paypal-transmission-sig", "")
|
||||
auth_algo = headers.get("paypal-auth-algo", "")
|
||||
|
||||
if not all([transmission_id, transmission_time, cert_url, actual_sig, auth_algo]):
|
||||
logger.warning("PayPal webhook missing required headers")
|
||||
return False
|
||||
|
||||
try:
|
||||
token_resp = httpx.post(
|
||||
f"{self._base_url}/v1/oauth2/token",
|
||||
auth=(self.client_id, self.client_secret),
|
||||
data={"grant_type": "client_credentials"},
|
||||
headers={"Accept": "application/json"},
|
||||
timeout=10,
|
||||
)
|
||||
if token_resp.status_code != 200:
|
||||
return False
|
||||
token = token_resp.json()["access_token"]
|
||||
|
||||
resp = httpx.post(
|
||||
f"{self._base_url}/v1/notifications/verify-webhook-signature",
|
||||
json={
|
||||
"auth_algo": auth_algo,
|
||||
"cert_url": cert_url,
|
||||
"transmission_id": transmission_id,
|
||||
"transmission_sig": actual_sig,
|
||||
"transmission_time": transmission_time,
|
||||
"webhook_id": self.webhook_id,
|
||||
"webhook_event": json.loads(body),
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
result = resp.json()
|
||||
return result.get("verification_status") == "SUCCESS"
|
||||
except Exception as e:
|
||||
logger.error(f"PayPal webhook verification failed: {e}")
|
||||
return False
|
||||
|
||||
def parse_callback(self, body: str, headers: dict) -> Dict[str, Any]:
|
||||
event = json.loads(body)
|
||||
event_type = event.get("event_type", "")
|
||||
resource = event.get("resource", {})
|
||||
|
||||
order_id = ""
|
||||
amount = 0
|
||||
if resource:
|
||||
order_id = resource.get("id", "")
|
||||
amt_field = resource.get("amount", {})
|
||||
if isinstance(amt_field, dict) and (amt_field.get("value") or amt_field.get("total")):
|
||||
amount = float(amt_field.get("value", amt_field.get("total", 0)))
|
||||
else:
|
||||
units = resource.get("purchase_units", [])
|
||||
amount = float(units[0]["amount"]["value"]) if units and units[0].get("amount") else 0
|
||||
|
||||
custom_id = ""
|
||||
purchase_units = resource.get("purchase_units", [])
|
||||
if purchase_units:
|
||||
custom_id = purchase_units[0].get("reference_id", "")
|
||||
|
||||
return {
|
||||
"event": event_type,
|
||||
"order_no": custom_id,
|
||||
"gateway_order_id": order_id,
|
||||
"gateway_order_no": resource.get("payments", {}).get("captures", [{}])[0].get("id", order_id) if resource else order_id,
|
||||
"amount": amount,
|
||||
"success": event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED", "CHECKOUT.ORDER.COMPLETED"),
|
||||
"raw": resource,
|
||||
}
|
||||
|
||||
async def capture_order(self, order_id: str) -> Dict[str, Any]:
|
||||
token = await self._get_access_token()
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self._base_url}/v2/checkout/orders/{order_id}/capture",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise ValueError(f"PayPal capture failed: {resp.text}")
|
||||
data = resp.json()
|
||||
status = data.get("status", "")
|
||||
capture_id = ""
|
||||
for pu in data.get("purchase_units", []):
|
||||
for cap in pu.get("payments", {}).get("captures", []):
|
||||
if cap.get("status") == "COMPLETED":
|
||||
capture_id = cap["id"]
|
||||
break
|
||||
return {
|
||||
"status": status,
|
||||
"capture_id": capture_id,
|
||||
"completed": status == "COMPLETED",
|
||||
}
|
||||
|
||||
async def close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,188 @@
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from typing import Dict, Any, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import settings
|
||||
from app.services.payment_gateway import PaymentGateway
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PINGPONG_HOST_SANDBOX = "https://sandbox-acquirer-payment.pingpongx.com"
|
||||
PINGPONG_HOST_PROD_EU = "https://acquirer-payment.pingpongx.com"
|
||||
PINGPONG_HOST_PROD_US = "https://acquirer-payment-checkout-us.pingpongx.com"
|
||||
|
||||
|
||||
class PingPongCheckoutService(PaymentGateway):
|
||||
name = "pingpong"
|
||||
supported_types = ["pingpong", "card"]
|
||||
|
||||
def __init__(self):
|
||||
self.client_id = settings.PINGPONG_CLIENT_ID or ""
|
||||
self.acc_id = settings.PINGPONG_ACC_ID or ""
|
||||
self.secret_key = settings.PINGPONG_SECRET_KEY or ""
|
||||
self.sandbox = settings.PINGPONG_SANDBOX
|
||||
if self.sandbox:
|
||||
self._base_url = PINGPONG_HOST_SANDBOX
|
||||
else:
|
||||
region = (settings.PINGPONG_REGION or "EU").upper()
|
||||
if region == "US":
|
||||
self._base_url = PINGPONG_HOST_PROD_US
|
||||
else:
|
||||
self._base_url = PINGPONG_HOST_PROD_EU
|
||||
|
||||
def _is_configured(self) -> bool:
|
||||
return bool(self.client_id and self.acc_id and self.secret_key)
|
||||
|
||||
def _sign(self, params: dict) -> str:
|
||||
sorted_keys = sorted(k for k in params if k != "sign")
|
||||
sign_str = "&".join(f"{k}={params[k]}" for k in sorted_keys)
|
||||
raw = sign_str + self.secret_key
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
def _verify_sign(self, params: dict) -> bool:
|
||||
if "sign" not in params:
|
||||
return False
|
||||
expected = self._sign(params)
|
||||
return expected == params["sign"].upper()
|
||||
|
||||
async def _request(self, path: str, biz_data: dict) -> dict:
|
||||
biz_json = json.dumps(biz_data, separators=(",", ":"))
|
||||
body = {
|
||||
"accId": self.acc_id,
|
||||
"clientId": self.client_id,
|
||||
"signType": "SHA256",
|
||||
"version": "1.0",
|
||||
"bizContent": biz_json,
|
||||
}
|
||||
body["sign"] = self._sign(body)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"{self._base_url}{path}",
|
||||
json=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise ValueError(f"PingPong request failed: {resp.status_code} {resp.text}")
|
||||
data = resp.json()
|
||||
if data.get("code") != "000000":
|
||||
raise ValueError(f"PingPong error: {data.get('code')} - {data.get('description', '')}")
|
||||
return data
|
||||
|
||||
async def create_order(self, order_no: str, amount: int, description: str,
|
||||
**kwargs) -> Dict[str, Any]:
|
||||
if not self._is_configured():
|
||||
raise ValueError("PingPong 未配置")
|
||||
|
||||
usd_amount = round(amount / 100, 2)
|
||||
usd_str = f"{usd_amount:.2f}"
|
||||
success_url = kwargs.get("success_url", "https://trade.yuzhiran.com/workspace/credits?pingpong=success")
|
||||
cancel_url = kwargs.get("cancel_url", "https://trade.yuzhiran.com/workspace/credits?pingpong=cancel")
|
||||
notification_url = kwargs.get("notification_url", "https://trade.yuzhiran.com/api/v1/payment/pingpong-webhook")
|
||||
|
||||
shopper_ip = kwargs.get("shopper_ip", "127.0.0.1")
|
||||
|
||||
biz_data = {
|
||||
"merchantTransactionId": order_no,
|
||||
"amount": usd_str,
|
||||
"currency": "USD",
|
||||
"paymentType": "SALE",
|
||||
"shopperIP": shopper_ip,
|
||||
"notificationUrl": notification_url,
|
||||
"payResultUrl": success_url,
|
||||
"cancelUrl": cancel_url,
|
||||
"goods": [{
|
||||
"name": description[:127] or "Credit Package",
|
||||
"unitPrice": usd_str,
|
||||
"number": "1",
|
||||
}],
|
||||
}
|
||||
|
||||
result = await self._request("/v4/payment/prePay", biz_data)
|
||||
bc = result.get("bizContent", {})
|
||||
|
||||
return {
|
||||
"gateway_order_id": bc.get("transactionId", ""),
|
||||
"merchant_order_id": order_no,
|
||||
"session_url": bc.get("paymentUrl", ""),
|
||||
"session_id": bc.get("token", ""),
|
||||
"amount": usd_amount,
|
||||
"status": "pending",
|
||||
}
|
||||
|
||||
async def query_order(self, order_no: str) -> Dict[str, Any]:
|
||||
if not self._is_configured():
|
||||
return {"status": "unknown"}
|
||||
biz_data = {"merchantTransactionId": order_no}
|
||||
try:
|
||||
result = await self._request("/v4/payment/query", biz_data)
|
||||
bc = result.get("bizContent", {})
|
||||
return {
|
||||
"status": bc.get("status", "unknown"),
|
||||
"payment_status": "completed" if bc.get("status") == "SUCCESS" else bc.get("status", ""),
|
||||
"amount": float(bc.get("amount", 0)),
|
||||
"currency": bc.get("currency", "USD"),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"PingPong query failed: {e}")
|
||||
return {"status": "unknown"}
|
||||
|
||||
async def refund(self, order_no: str, amount: int, reason: str = "") -> Dict[str, Any]:
|
||||
usd_amount = round(amount / 100, 2)
|
||||
biz_data = {
|
||||
"merchantTransactionId": order_no,
|
||||
"amount": f"{usd_amount:.2f}",
|
||||
"currency": "USD",
|
||||
"reason": reason or "Refund",
|
||||
}
|
||||
try:
|
||||
result = await self._request("/v4/refund", biz_data)
|
||||
return {"status": "COMPLETED", "refund_id": result.get("bizContent", {}).get("refundId", "")}
|
||||
except Exception as e:
|
||||
raise ValueError(f"PingPong refund failed: {e}")
|
||||
|
||||
async def query_refund(self, order_no: str) -> Dict[str, Any]:
|
||||
biz_data = {"merchantTransactionId": order_no}
|
||||
try:
|
||||
result = await self._request("/v4/refund/query", biz_data)
|
||||
return {"status": result.get("bizContent", {}).get("status", "unknown")}
|
||||
except Exception:
|
||||
return {"status": "not_implemented"}
|
||||
|
||||
def verify_callback(self, headers: dict, body: str) -> bool:
|
||||
try:
|
||||
data = json.loads(body)
|
||||
return self._verify_sign(data)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(f"PingPong callback verify failed: {e}")
|
||||
return False
|
||||
|
||||
def parse_callback(self, body: str, headers: dict) -> Dict[str, Any]:
|
||||
data = json.loads(body)
|
||||
bc_raw = data.get("bizContent", "{}")
|
||||
if isinstance(bc_raw, str):
|
||||
try:
|
||||
bc = json.loads(bc_raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
bc = {}
|
||||
else:
|
||||
bc = bc_raw
|
||||
|
||||
status = bc.get("status", "")
|
||||
return {
|
||||
"event": status,
|
||||
"order_no": bc.get("merchantTransactionId", ""),
|
||||
"gateway_order_id": bc.get("transactionId", ""),
|
||||
"gateway_order_no": bc.get("transactionId", ""),
|
||||
"amount": float(bc.get("amount", 0)),
|
||||
"success": status == "SUCCESS",
|
||||
"raw": bc,
|
||||
}
|
||||
|
||||
async def close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,123 @@
|
||||
import logging
|
||||
import stripe
|
||||
from typing import Optional, Dict, Any
|
||||
from app.config import settings
|
||||
from app.services.payment_gateway import PaymentGateway
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StripePaymentService(PaymentGateway):
|
||||
name = "stripe"
|
||||
supported_types = ["card", "alipay", "wechat"]
|
||||
|
||||
def __init__(self):
|
||||
self.secret_key = settings.STRIPE_SECRET_KEY
|
||||
self.webhook_secret = settings.STRIPE_WEBHOOK_SECRET
|
||||
if self.secret_key:
|
||||
stripe.api_key = self.secret_key
|
||||
|
||||
async def create_order(self, order_no: str, amount: int, description: str,
|
||||
**kwargs) -> Dict[str, Any]:
|
||||
if not self.secret_key:
|
||||
raise ValueError("Stripe 未配置")
|
||||
|
||||
pay_type = kwargs.get("pay_type", "card")
|
||||
success_url = kwargs.get("success_url", "")
|
||||
cancel_url = kwargs.get("cancel_url", "")
|
||||
|
||||
payment_method_types = ["card"]
|
||||
if pay_type == "alipay":
|
||||
payment_method_types = ["alipay"]
|
||||
elif pay_type == "wechat":
|
||||
payment_method_types = ["wechat_pay"]
|
||||
|
||||
session = stripe.checkout.Session.create(
|
||||
payment_method_types=payment_method_types,
|
||||
line_items=[{
|
||||
"price_data": {
|
||||
"currency": "usd",
|
||||
"product_data": {"name": description},
|
||||
"unit_amount": amount,
|
||||
},
|
||||
"quantity": 1,
|
||||
}],
|
||||
mode="payment",
|
||||
success_url=success_url or "https://trade.yuzhiran.com/workspace/credits?stripe=success",
|
||||
cancel_url=cancel_url or "https://trade.yuzhiran.com/workspace/credits?stripe=cancel",
|
||||
metadata={"order_no": order_no},
|
||||
)
|
||||
|
||||
return {
|
||||
"gateway_order_id": session.id,
|
||||
"merchant_order_id": order_no,
|
||||
"session_url": session.url,
|
||||
"session_id": session.id,
|
||||
"amount": amount / 100,
|
||||
"status": "pending",
|
||||
}
|
||||
|
||||
async def query_order(self, order_no: str) -> Dict[str, Any]:
|
||||
try:
|
||||
session = stripe.checkout.Session.retrieve(order_no)
|
||||
return {
|
||||
"status": session.status,
|
||||
"payment_status": session.payment_status,
|
||||
"amount": session.amount_total / 100 if session.amount_total else 0,
|
||||
"currency": session.currency or "usd",
|
||||
"customer_email": session.customer_details.email if session.customer_details else None,
|
||||
}
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe query failed: {e}")
|
||||
return {"status": "unknown"}
|
||||
|
||||
async def refund(self, order_no: str, amount: int, reason: str = "") -> Dict[str, Any]:
|
||||
try:
|
||||
payment_intents = stripe.checkout.Session.list(
|
||||
payment_intent=True
|
||||
)
|
||||
refund = stripe.Refund.create(
|
||||
payment_intent=order_no,
|
||||
amount=amount,
|
||||
reason="requested_by_customer",
|
||||
)
|
||||
return {"status": refund.status, "refund_id": refund.id}
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe refund failed: {e}")
|
||||
raise ValueError(f"退款失败: {e}")
|
||||
|
||||
async def query_refund(self, order_no: str) -> Dict[str, Any]:
|
||||
return {"status": "not_implemented"}
|
||||
|
||||
def verify_callback(self, headers: dict, body: str) -> bool:
|
||||
if not self.webhook_secret:
|
||||
logger.warning("Stripe webhook secret not configured")
|
||||
return False
|
||||
try:
|
||||
sig_header = headers.get("stripe-signature", "")
|
||||
stripe.Webhook.construct_event(body, sig_header, self.webhook_secret)
|
||||
return True
|
||||
except stripe.error.SignatureVerificationError as e:
|
||||
logger.warning(f"Stripe webhook signature invalid: {e}")
|
||||
return False
|
||||
|
||||
def parse_callback(self, body: str, headers: dict) -> Dict[str, Any]:
|
||||
event = stripe.Webhook.construct_event(body, headers.get("stripe-signature", ""), self.webhook_secret)
|
||||
session = event.data.object
|
||||
return {
|
||||
"event": event.type,
|
||||
"order_no": session.get("metadata", {}).get("order_no", ""),
|
||||
"gateway_order_id": session.get("id", ""),
|
||||
"gateway_order_no": session.get("payment_intent", ""),
|
||||
"amount": (session.get("amount_total", 0) or 0) / 100,
|
||||
"success": event.type == "checkout.session.completed",
|
||||
"raw": session,
|
||||
}
|
||||
|
||||
async def close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
try:
|
||||
stripe.checkout.Session.expire(order_no)
|
||||
return {"status": "expired"}
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe close order failed: {e}")
|
||||
raise ValueError(f"关闭订单失败: {e}")
|
||||
Reference in New Issue
Block a user