Merge branch 'refactor/backend-architecture' into development
This commit is contained in:
0
api/app/features/payment/__init__.py
Normal file
0
api/app/features/payment/__init__.py
Normal file
145
api/app/features/payment/alipay_client.py
Normal file
145
api/app/features/payment/alipay_client.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
支付宝 OpenAPI 封装(从 payment 迁入 app)
|
||||
"""
|
||||
from app.core.logging import get_logger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from app.features.payment.payment_config import AlipayConfig
|
||||
from app.features.payment.schemas import NotifyResult, PaymentResult, PaymentStatus
|
||||
from app.features.payment.payment_exceptions import (
|
||||
PaymentConfigError,
|
||||
PaymentCreateError,
|
||||
PaymentNotifyError,
|
||||
PaymentQueryError,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AlipayClient:
|
||||
def __init__(self, config: AlipayConfig):
|
||||
self._config = config
|
||||
self._client = None
|
||||
|
||||
def _ensure_client(self):
|
||||
if not self._config.is_configured:
|
||||
raise PaymentConfigError("支付宝配置不完整,请检查环境变量")
|
||||
if self._client is None:
|
||||
try:
|
||||
from alipay import AliPay
|
||||
self._client = AliPay(
|
||||
appid=self._config.app_id,
|
||||
app_notify_url=self._config.notify_url,
|
||||
app_private_key_string=self._config.private_key,
|
||||
alipay_public_key_string=self._config.alipay_public_key,
|
||||
sign_type=self._config.sign_type,
|
||||
debug=False,
|
||||
)
|
||||
logger.info("支付宝客户端初始化成功")
|
||||
except Exception as e:
|
||||
raise PaymentConfigError(f"支付宝客户端初始化失败: {e}")
|
||||
|
||||
def create_app_order(
|
||||
self,
|
||||
out_trade_no: str,
|
||||
total_amount: int,
|
||||
subject: str,
|
||||
) -> PaymentResult:
|
||||
self._ensure_client()
|
||||
try:
|
||||
amount_yuan = f"{total_amount / 100:.2f}"
|
||||
order_string = self._client.api_alipay_trade_app_pay(
|
||||
out_trade_no=out_trade_no,
|
||||
total_amount=amount_yuan,
|
||||
subject=subject,
|
||||
notify_url=self._config.notify_url,
|
||||
)
|
||||
if order_string:
|
||||
logger.info("支付宝订单创建成功: %s", out_trade_no)
|
||||
return PaymentResult(
|
||||
success=True,
|
||||
payment_method="alipay",
|
||||
out_trade_no=out_trade_no,
|
||||
alipay_order_string=order_string,
|
||||
)
|
||||
raise PaymentCreateError("支付宝下单失败: 返回空订单字符串")
|
||||
except PaymentCreateError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("支付宝订单创建异常: %s", e)
|
||||
raise PaymentCreateError(f"支付宝下单异常: {e}")
|
||||
|
||||
def verify_notify(self, params: Dict[str, str]) -> NotifyResult:
|
||||
self._ensure_client()
|
||||
try:
|
||||
sign = params.pop("sign", None)
|
||||
params.pop("sign_type", None)
|
||||
if not sign:
|
||||
raise PaymentNotifyError("支付宝回调缺少签名参数")
|
||||
success = self._client.verify(params, sign)
|
||||
if success:
|
||||
trade_status = params.get("trade_status", "")
|
||||
out_trade_no = params.get("out_trade_no", "")
|
||||
trade_no = params.get("trade_no", "")
|
||||
total_amount_str = params.get("total_amount", "0")
|
||||
total_amount = int(float(total_amount_str) * 100)
|
||||
return NotifyResult(
|
||||
success=True,
|
||||
out_trade_no=out_trade_no,
|
||||
trade_no=trade_no,
|
||||
total_amount=total_amount,
|
||||
trade_status=trade_status,
|
||||
)
|
||||
raise PaymentNotifyError("支付宝回调验签失败")
|
||||
except PaymentNotifyError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("支付宝回调处理异常: %s", e)
|
||||
raise PaymentNotifyError(f"支付宝回调处理失败: {e}")
|
||||
|
||||
def query_order(self, out_trade_no: str) -> PaymentStatus:
|
||||
self._ensure_client()
|
||||
try:
|
||||
result = self._client.api_alipay_trade_query(out_trade_no=out_trade_no)
|
||||
if result and result.get("code") == "10000":
|
||||
trade_status = result.get("trade_status", "")
|
||||
trade_no = result.get("trade_no", "")
|
||||
total_amount_str = result.get("total_amount", "0")
|
||||
total_amount = int(float(total_amount_str) * 100)
|
||||
unified_status = self._map_trade_status(trade_status)
|
||||
return PaymentStatus(
|
||||
success=True,
|
||||
out_trade_no=out_trade_no,
|
||||
trade_no=trade_no,
|
||||
trade_status=unified_status,
|
||||
total_amount=total_amount,
|
||||
)
|
||||
error_msg = result.get("sub_msg", result.get("msg", "未知错误")) if result else "空结果"
|
||||
raise PaymentQueryError(f"查询支付宝订单失败: {error_msg}")
|
||||
except PaymentQueryError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("查询支付宝订单异常: %s", e)
|
||||
raise PaymentQueryError(f"查询支付宝订单异常: {e}")
|
||||
|
||||
def close_order(self, out_trade_no: str) -> bool:
|
||||
self._ensure_client()
|
||||
try:
|
||||
result = self._client.api_alipay_trade_close(out_trade_no=out_trade_no)
|
||||
if result and result.get("code") == "10000":
|
||||
logger.info("支付宝订单已关闭: %s", out_trade_no)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("关闭支付宝订单异常: %s", e)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _map_trade_status(alipay_status: str) -> str:
|
||||
status_map = {
|
||||
"WAIT_BUYER_PAY": "NOTPAY",
|
||||
"TRADE_CLOSED": "CLOSED",
|
||||
"TRADE_SUCCESS": "SUCCESS",
|
||||
"TRADE_FINISHED": "SUCCESS",
|
||||
}
|
||||
return status_map.get(alipay_status, alipay_status)
|
||||
28
api/app/features/payment/deps.py
Normal file
28
api/app/features/payment/deps.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_async_db
|
||||
from app.features.payment.order_service import PaymentOrderService
|
||||
from app.features.payment.payment_config import PaymentConfig
|
||||
from app.features.payment.payment_facade import PaymentService
|
||||
from app.features.plan.deps import get_plan_service
|
||||
from app.features.plan.service import PlanService
|
||||
|
||||
_payment_service = None
|
||||
|
||||
|
||||
def get_payment_service() -> PaymentService:
|
||||
"""统一支付门面(wechat/alipay),供 PaymentOrderService 与 startup 使用。"""
|
||||
global _payment_service
|
||||
if _payment_service is None:
|
||||
config = PaymentConfig.from_env()
|
||||
_payment_service = PaymentService(config)
|
||||
return _payment_service
|
||||
|
||||
|
||||
def get_payment_order_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
plan_service: PlanService = Depends(get_plan_service),
|
||||
) -> PaymentOrderService:
|
||||
"""Payment order facade: create_order, callbacks, list/status."""
|
||||
return PaymentOrderService(db=db, plan_service=plan_service)
|
||||
23
api/app/features/payment/models.py
Normal file
23
api/app/features/payment/models.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.db import Base, utc_now
|
||||
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = "orders"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
|
||||
plan_id = Column(String, nullable=False)
|
||||
plan_name = Column(String, nullable=False)
|
||||
amount = Column(Integer, nullable=False)
|
||||
currency = Column(String, default="CNY")
|
||||
payment_method = Column(String, nullable=False)
|
||||
status = Column(String, default="pending")
|
||||
trade_no = Column(String, nullable=True, index=True)
|
||||
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
expired_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
user = relationship("User", back_populates="orders")
|
||||
231
api/app/features/payment/order_service.py
Normal file
231
api/app/features/payment/order_service.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
支付订单门面:持有 db + 底层 payment 客户端,提供 create_order / 回调 / 查询。
|
||||
"""
|
||||
import asyncio
|
||||
from app.core.logging import get_logger
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import utc_now
|
||||
from app.features.payment.models import Order
|
||||
from app.features.payment.schemas import (
|
||||
CreateOrderResponse,
|
||||
OrderListResponse,
|
||||
OrderStatusResponse,
|
||||
)
|
||||
from app.features.plan.service import PlanService
|
||||
from app.features.user.models import User
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ORDER_EXPIRE_MINUTES = 30
|
||||
SUBSCRIPTION_DURATION_DAYS = {"pro": 365, "pro_plus": 365, "premium": 365, "test": 365}
|
||||
PREPAY_TIMEOUT_SEC = 25
|
||||
WECHAT_INIT_TIMEOUT_SEC = 35
|
||||
|
||||
|
||||
def _generate_order_no() -> str:
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
short_uuid = uuid.uuid4().hex[:8].upper()
|
||||
return f"LE{timestamp}{short_uuid}"
|
||||
|
||||
|
||||
def _get_legacy_payment_service():
|
||||
from app.features.payment.deps import get_payment_service
|
||||
return get_payment_service()
|
||||
|
||||
|
||||
class PaymentOrderService:
|
||||
def __init__(self, db: AsyncSession, plan_service: PlanService):
|
||||
self._db = db
|
||||
self._plan_service = plan_service
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
user_id: str,
|
||||
user_subscription_type: str,
|
||||
plan_id: str,
|
||||
plan_display_name: str,
|
||||
plan_price: float,
|
||||
plan_currency: str,
|
||||
payment_method: str,
|
||||
) -> CreateOrderResponse:
|
||||
from app.features.payment.payment_exceptions import PaymentError
|
||||
|
||||
plans = self._plan_service.get_plans_for_api()
|
||||
plan = next((p for p in plans if p.id == plan_id), None)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=400, detail="无效的套餐 ID")
|
||||
if plan.price <= 0:
|
||||
raise HTTPException(status_code=400, detail="免费套餐无需支付")
|
||||
if payment_method not in ("wechat", "alipay"):
|
||||
raise HTTPException(status_code=400, detail="不支持的支付方式,仅支持 wechat / alipay")
|
||||
|
||||
client = _get_legacy_payment_service()
|
||||
if not client.is_method_available(payment_method):
|
||||
if payment_method == "alipay":
|
||||
raise HTTPException(status_code=503, detail="支付宝支付接口正在开发中,暂时不可用")
|
||||
raise HTTPException(status_code=503, detail=f"{payment_method} 支付暂不可用,请选择其他支付方式")
|
||||
|
||||
amount_fen = int(plan_price * 100)
|
||||
order_no = _generate_order_no()
|
||||
now = utc_now()
|
||||
order = Order(
|
||||
id=order_no,
|
||||
user_id=user_id,
|
||||
plan_id=plan_id,
|
||||
plan_name=plan_display_name,
|
||||
amount=amount_fen,
|
||||
currency=plan_currency,
|
||||
payment_method=payment_method,
|
||||
status="pending",
|
||||
created_at=now,
|
||||
expired_at=now + timedelta(minutes=ORDER_EXPIRE_MINUTES),
|
||||
)
|
||||
self._db.add(order)
|
||||
await self._db.flush()
|
||||
|
||||
if payment_method == "wechat":
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.to_thread(client.wechat_client.ensure_client),
|
||||
timeout=WECHAT_INIT_TIMEOUT_SEC,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(status_code=504, detail="微信支付初始化超时,请稍后重试。")
|
||||
except Exception as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
logger.exception("微信支付客户端初始化失败: %s", e)
|
||||
raise HTTPException(status_code=503, detail=f"微信支付暂不可用: {e!s}")
|
||||
|
||||
try:
|
||||
payment_result = await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
client.create_payment,
|
||||
payment_method,
|
||||
order_no,
|
||||
amount_fen,
|
||||
f"岁月时书 - {plan_display_name}",
|
||||
),
|
||||
timeout=PREPAY_TIMEOUT_SEC,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail="创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。",
|
||||
)
|
||||
except PaymentError as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(status_code=500, detail=f"创建支付订单失败: {e.message}")
|
||||
except Exception as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
logger.exception("创建支付订单异常: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"创建支付订单异常: {type(e).__name__}: {e!s}")
|
||||
|
||||
await self._db.commit()
|
||||
logger.info("订单创建成功: order_no=%s, payment_method=%s, amount_fen=%s", order_no, payment_method, amount_fen)
|
||||
return CreateOrderResponse(
|
||||
order_id=order_no,
|
||||
payment_method=payment_method,
|
||||
wechat_params=payment_result.wechat_params,
|
||||
alipay_order_string=payment_result.alipay_order_string,
|
||||
)
|
||||
|
||||
async def handle_payment_success(self, out_trade_no: str, trade_no: str) -> None:
|
||||
result = await self._db.execute(select(Order).where(Order.id == out_trade_no))
|
||||
order = result.scalar_one_or_none()
|
||||
if order is None:
|
||||
logger.warning("支付回调: 订单不存在 %s", out_trade_no)
|
||||
return
|
||||
if order.status == "paid":
|
||||
logger.info("支付回调: 订单已处理过 %s", out_trade_no)
|
||||
return
|
||||
now = utc_now()
|
||||
order.status = "paid"
|
||||
order.trade_no = trade_no
|
||||
order.paid_at = now
|
||||
user_result = await self._db.execute(select(User).where(User.id == order.user_id))
|
||||
user = user_result.scalar_one_or_none()
|
||||
if user:
|
||||
duration_days = SUBSCRIPTION_DURATION_DAYS.get(order.plan_id, 365)
|
||||
if user.subscription_expires_at and user.subscription_expires_at > now:
|
||||
user.subscription_expires_at = user.subscription_expires_at + timedelta(days=duration_days)
|
||||
else:
|
||||
user.subscription_expires_at = now + timedelta(days=duration_days)
|
||||
user.subscription_type = order.plan_id
|
||||
logger.info("用户 %s 订阅已升级为 %s,到期: %s", user.id, order.plan_id, user.subscription_expires_at)
|
||||
await self._db.commit()
|
||||
logger.info("支付成功处理完成: 订单 %s, 第三方交易号 %s", out_trade_no, trade_no)
|
||||
|
||||
async def handle_wechat_notify(self, headers: dict, body: str) -> dict:
|
||||
client = _get_legacy_payment_service()
|
||||
notify_result = client.handle_wechat_notify(headers=headers, body=body)
|
||||
if notify_result.success and notify_result.trade_status == "SUCCESS":
|
||||
await self.handle_payment_success(
|
||||
notify_result.out_trade_no,
|
||||
notify_result.trade_no,
|
||||
)
|
||||
return {"code": "SUCCESS", "message": "成功"}
|
||||
|
||||
async def handle_alipay_notify(self, params: dict) -> str:
|
||||
client = _get_legacy_payment_service()
|
||||
notify_result = client.handle_alipay_notify(params=params)
|
||||
if notify_result.success and notify_result.trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED", "SUCCESS"):
|
||||
await self.handle_payment_success(
|
||||
notify_result.out_trade_no,
|
||||
notify_result.trade_no,
|
||||
)
|
||||
return "success"
|
||||
|
||||
async def get_order_status(self, order_id: str, user_id: str) -> OrderStatusResponse:
|
||||
result = await self._db.execute(
|
||||
select(Order).where(Order.id == order_id, Order.user_id == user_id)
|
||||
)
|
||||
order = result.scalar_one_or_none()
|
||||
if order is None:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
return OrderStatusResponse(
|
||||
order_id=order.id,
|
||||
plan_id=order.plan_id,
|
||||
plan_name=order.plan_name,
|
||||
amount=order.amount,
|
||||
currency=order.currency,
|
||||
payment_method=order.payment_method,
|
||||
status=order.status,
|
||||
trade_no=order.trade_no,
|
||||
created_at=order.created_at.isoformat() if order.created_at else "",
|
||||
paid_at=order.paid_at.isoformat() if order.paid_at else None,
|
||||
)
|
||||
|
||||
async def list_orders(self, user_id: str) -> list[OrderListResponse]:
|
||||
result = await self._db.execute(
|
||||
select(Order).where(Order.user_id == user_id).order_by(Order.created_at.desc())
|
||||
)
|
||||
orders = result.scalars().all()
|
||||
return [
|
||||
OrderListResponse(
|
||||
id=o.id,
|
||||
plan_id=o.plan_id,
|
||||
plan_name=o.plan_name,
|
||||
amount=o.amount,
|
||||
currency=o.currency,
|
||||
status=o.status,
|
||||
payment_method=o.payment_method,
|
||||
created_at=o.created_at.isoformat() if o.created_at else "",
|
||||
paid_at=o.paid_at.isoformat() if o.paid_at else None,
|
||||
)
|
||||
for o in orders
|
||||
]
|
||||
147
api/app/features/payment/payment_config.py
Normal file
147
api/app/features/payment/payment_config.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
支付模块配置(从 payment 迁入 app,从 app.core.config.settings 读取)
|
||||
"""
|
||||
from app.core.logging import get_logger
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeChatPayConfig:
|
||||
app_id: str = ""
|
||||
mch_id: str = ""
|
||||
api_v3_key: str = ""
|
||||
private_key_path: str = ""
|
||||
private_key: str = ""
|
||||
cert_serial_no: str = ""
|
||||
notify_url: str = ""
|
||||
platform_public_key: str = ""
|
||||
platform_public_key_path: str = ""
|
||||
platform_public_key_id: str = ""
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
has_key = bool(self.private_key.strip()) or bool(self.private_key_path.strip())
|
||||
return all(
|
||||
[
|
||||
self.app_id,
|
||||
self.mch_id,
|
||||
self.api_v3_key,
|
||||
has_key,
|
||||
self.cert_serial_no,
|
||||
self.notify_url,
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def use_platform_public_key(self) -> bool:
|
||||
has_pub = bool(self.platform_public_key.strip()) or bool(
|
||||
self.platform_public_key_path.strip()
|
||||
)
|
||||
return has_pub and bool(self.platform_public_key_id.strip())
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlipayConfig:
|
||||
app_id: str = ""
|
||||
private_key: str = ""
|
||||
alipay_public_key: str = ""
|
||||
notify_url: str = ""
|
||||
sign_type: str = "RSA2"
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.app_id,
|
||||
self.private_key,
|
||||
self.alipay_public_key,
|
||||
self.notify_url,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentConfig:
|
||||
wechat: WeChatPayConfig = field(default_factory=WeChatPayConfig)
|
||||
alipay: AlipayConfig = field(default_factory=AlipayConfig)
|
||||
alipay_under_development: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings) -> "PaymentConfig":
|
||||
wechat_private_key = (
|
||||
getattr(settings, "wechat_pay_private_key", "") or ""
|
||||
).strip()
|
||||
if wechat_private_key:
|
||||
wechat_private_key = (
|
||||
wechat_private_key.strip('"')
|
||||
.strip("'")
|
||||
.lstrip("\ufeff")
|
||||
.replace("\\n", "\n")
|
||||
)
|
||||
wechat_platform_pub = (
|
||||
getattr(settings, "wechat_pay_platform_public_key", "") or ""
|
||||
).strip()
|
||||
if wechat_platform_pub and "\\n" in wechat_platform_pub:
|
||||
wechat_platform_pub = wechat_platform_pub.replace("\\n", "\n")
|
||||
wechat_private_key_path = (
|
||||
getattr(settings, "wechat_pay_private_key_path", "") or ""
|
||||
).strip()
|
||||
alipay_under = (
|
||||
getattr(settings, "alipay_under_development", "true") or "true"
|
||||
).lower()
|
||||
config = cls(
|
||||
wechat=WeChatPayConfig(
|
||||
app_id=getattr(settings, "wechat_pay_app_id", "") or "",
|
||||
mch_id=getattr(settings, "wechat_pay_mch_id", "") or "",
|
||||
api_v3_key=getattr(settings, "wechat_pay_api_v3_key", "") or "",
|
||||
private_key_path=wechat_private_key_path,
|
||||
private_key=wechat_private_key if not wechat_private_key_path else "",
|
||||
cert_serial_no=getattr(settings, "wechat_pay_cert_serial_no", "") or "",
|
||||
notify_url=getattr(settings, "wechat_pay_notify_url", "") or "",
|
||||
platform_public_key=wechat_platform_pub,
|
||||
platform_public_key_path=(
|
||||
getattr(settings, "wechat_pay_platform_public_key_path", "") or ""
|
||||
).strip(),
|
||||
platform_public_key_id=(
|
||||
getattr(settings, "wechat_pay_platform_public_key_id", "") or ""
|
||||
).strip(),
|
||||
),
|
||||
alipay=AlipayConfig(
|
||||
app_id=getattr(settings, "alipay_app_id", "") or "",
|
||||
private_key=getattr(settings, "alipay_private_key", "") or "",
|
||||
alipay_public_key=getattr(settings, "alipay_public_key", "") or "",
|
||||
notify_url=getattr(settings, "alipay_notify_url", "") or "",
|
||||
sign_type=getattr(settings, "alipay_sign_type", "RSA2") or "RSA2",
|
||||
),
|
||||
alipay_under_development=alipay_under in ("true", "1", "yes"),
|
||||
)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "PaymentConfig":
|
||||
from app.core.config import settings
|
||||
|
||||
config = cls.from_settings(settings)
|
||||
if config.wechat.is_configured:
|
||||
mode = (
|
||||
"平台公钥模式"
|
||||
if config.wechat.use_platform_public_key
|
||||
else "平台证书模式"
|
||||
)
|
||||
key_src = "私钥内容" if config.wechat.private_key.strip() else "私钥路径"
|
||||
logger.info(
|
||||
"微信支付配置已加载: APP_ID=%s, MCH_ID=%s, 模式=%s, 商户私钥=%s",
|
||||
config.wechat.app_id,
|
||||
config.wechat.mch_id,
|
||||
mode,
|
||||
key_src,
|
||||
)
|
||||
else:
|
||||
logger.warning("微信支付配置不完整,微信支付将不可用")
|
||||
if config.alipay.is_configured:
|
||||
logger.info("支付宝配置已加载: APP_ID=%s", config.alipay.app_id)
|
||||
else:
|
||||
logger.warning("支付宝配置不完整,支付宝支付将不可用")
|
||||
return config
|
||||
28
api/app/features/payment/payment_exceptions.py
Normal file
28
api/app/features/payment/payment_exceptions.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""支付模块异常定义(从 payment 迁入 app)"""
|
||||
|
||||
|
||||
class PaymentError(Exception):
|
||||
def __init__(self, message: str = "支付异常", code: str = "PAYMENT_ERROR"):
|
||||
self.message = message
|
||||
self.code = code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class PaymentConfigError(PaymentError):
|
||||
def __init__(self, message: str = "支付配置错误"):
|
||||
super().__init__(message=message, code="PAYMENT_CONFIG_ERROR")
|
||||
|
||||
|
||||
class PaymentCreateError(PaymentError):
|
||||
def __init__(self, message: str = "创建支付订单失败"):
|
||||
super().__init__(message=message, code="PAYMENT_CREATE_ERROR")
|
||||
|
||||
|
||||
class PaymentNotifyError(PaymentError):
|
||||
def __init__(self, message: str = "支付回调处理失败"):
|
||||
super().__init__(message=message, code="PAYMENT_NOTIFY_ERROR")
|
||||
|
||||
|
||||
class PaymentQueryError(PaymentError):
|
||||
def __init__(self, message: str = "查询支付状态失败"):
|
||||
super().__init__(message=message, code="PAYMENT_QUERY_ERROR")
|
||||
93
api/app/features/payment/payment_facade.py
Normal file
93
api/app/features/payment/payment_facade.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
统一支付服务门面(从 payment 迁入 app)
|
||||
"""
|
||||
from app.core.logging import get_logger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from app.features.payment.payment_config import PaymentConfig
|
||||
from app.features.payment.wechat_client import WeChatPayClient
|
||||
from app.features.payment.alipay_client import AlipayClient
|
||||
from app.features.payment.schemas import NotifyResult, PaymentResult, PaymentStatus
|
||||
from app.features.payment.payment_exceptions import PaymentError, PaymentConfigError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
PAYMENT_METHOD_WECHAT = "wechat"
|
||||
PAYMENT_METHOD_ALIPAY = "alipay"
|
||||
SUPPORTED_METHODS = {PAYMENT_METHOD_WECHAT, PAYMENT_METHOD_ALIPAY}
|
||||
|
||||
|
||||
class PaymentService:
|
||||
def __init__(self, config: PaymentConfig):
|
||||
self._config = config
|
||||
self._wechat_client: Optional[WeChatPayClient] = None
|
||||
self._alipay_client: Optional[AlipayClient] = None
|
||||
|
||||
@property
|
||||
def wechat_client(self) -> WeChatPayClient:
|
||||
if self._wechat_client is None:
|
||||
self._wechat_client = WeChatPayClient(self._config.wechat)
|
||||
return self._wechat_client
|
||||
|
||||
@property
|
||||
def alipay_client(self) -> AlipayClient:
|
||||
if self._alipay_client is None:
|
||||
self._alipay_client = AlipayClient(self._config.alipay)
|
||||
return self._alipay_client
|
||||
|
||||
def create_payment(
|
||||
self,
|
||||
method: str,
|
||||
out_trade_no: str,
|
||||
total_amount: int,
|
||||
description: str,
|
||||
) -> PaymentResult:
|
||||
self._validate_method(method)
|
||||
if method == PAYMENT_METHOD_WECHAT:
|
||||
return self.wechat_client.create_app_order(
|
||||
out_trade_no=out_trade_no,
|
||||
total_amount=total_amount,
|
||||
description=description,
|
||||
)
|
||||
return self.alipay_client.create_app_order(
|
||||
out_trade_no=out_trade_no,
|
||||
total_amount=total_amount,
|
||||
subject=description,
|
||||
)
|
||||
|
||||
def handle_wechat_notify(
|
||||
self, headers: Dict[str, str], body: str
|
||||
) -> NotifyResult:
|
||||
return self.wechat_client.verify_notify(headers=headers, body=body)
|
||||
|
||||
def handle_alipay_notify(self, params: Dict[str, str]) -> NotifyResult:
|
||||
return self.alipay_client.verify_notify(params=params)
|
||||
|
||||
def query_payment(self, method: str, out_trade_no: str) -> PaymentStatus:
|
||||
self._validate_method(method)
|
||||
if method == PAYMENT_METHOD_WECHAT:
|
||||
return self.wechat_client.query_order(out_trade_no=out_trade_no)
|
||||
return self.alipay_client.query_order(out_trade_no=out_trade_no)
|
||||
|
||||
def close_payment(self, method: str, out_trade_no: str) -> bool:
|
||||
self._validate_method(method)
|
||||
if method == PAYMENT_METHOD_WECHAT:
|
||||
return self.wechat_client.close_order(out_trade_no=out_trade_no)
|
||||
return self.alipay_client.close_order(out_trade_no=out_trade_no)
|
||||
|
||||
def is_method_available(self, method: str) -> bool:
|
||||
if method == PAYMENT_METHOD_WECHAT:
|
||||
return self._config.wechat.is_configured
|
||||
if method == PAYMENT_METHOD_ALIPAY:
|
||||
if getattr(self._config, "alipay_under_development", True):
|
||||
return False
|
||||
return self._config.alipay.is_configured
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _validate_method(method: str):
|
||||
if method not in SUPPORTED_METHODS:
|
||||
raise PaymentError(
|
||||
f"不支持的支付方式: {method}",
|
||||
code="UNSUPPORTED_METHOD",
|
||||
)
|
||||
22
api/app/features/payment/repo.py
Normal file
22
api/app/features/payment/repo.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Payment repository — Order data access."""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.features.payment.models import Order
|
||||
|
||||
|
||||
async def get_order_by_id(order_id: str, db: AsyncSession) -> Order | None:
|
||||
return await db.get(Order, order_id)
|
||||
|
||||
|
||||
async def get_orders_by_user(user_id: str, db: AsyncSession) -> list[Order]:
|
||||
stmt = select(Order).where(Order.user_id == user_id).order_by(Order.created_at.desc())
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_order_by_trade_no(out_trade_no: str, db: AsyncSession) -> Order | None:
|
||||
stmt = select(Order).where(Order.id == out_trade_no)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
117
api/app/features/payment/router.py
Normal file
117
api/app/features/payment/router.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from app.core.logging import get_logger
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.features.payment.deps import get_payment_order_service
|
||||
from app.features.payment.order_service import PaymentOrderService
|
||||
from app.features.payment.schemas import (
|
||||
CreateOrderRequest,
|
||||
CreateOrderResponse,
|
||||
OrderListResponse,
|
||||
OrderStatusResponse,
|
||||
)
|
||||
from app.features.plan.deps import get_plan_service
|
||||
from app.features.plan.service import PlanService
|
||||
from app.features.user.models import User
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/payment",
|
||||
tags=["payment"],
|
||||
responses={
|
||||
401: {"description": "认证失败"},
|
||||
404: {"description": "订单不存在"},
|
||||
503: {"description": "支付服务暂不可用"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/create-order", response_model=CreateOrderResponse)
|
||||
async def create_order(
|
||||
request: CreateOrderRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
plan_service: PlanService = Depends(get_plan_service),
|
||||
):
|
||||
plan = next((p for p in plan_service.get_plans_for_api() if p.id == request.plan_id), None)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=400, detail="无效的套餐 ID")
|
||||
return await service.create_order(
|
||||
user_id=current_user.id,
|
||||
user_subscription_type=current_user.subscription_type,
|
||||
plan_id=plan.id,
|
||||
plan_display_name=plan.display_name,
|
||||
plan_price=plan.price,
|
||||
plan_currency=plan.currency,
|
||||
payment_method=request.payment_method,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/notify/wechat", include_in_schema=False)
|
||||
async def wechat_notify(
|
||||
request: Request,
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
try:
|
||||
headers = dict(request.headers)
|
||||
body = await request.body()
|
||||
return await service.handle_wechat_notify(headers=headers, body=body.decode("utf-8"))
|
||||
except Exception as e:
|
||||
logger.exception("微信支付回调处理失败: %s", e)
|
||||
return {"code": "FAIL", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/notify/alipay", include_in_schema=False)
|
||||
async def alipay_notify(
|
||||
request: Request,
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
try:
|
||||
form_data = await request.form()
|
||||
params = {key: value for key, value in form_data.items()}
|
||||
result = await service.handle_alipay_notify(params=params)
|
||||
return PlainTextResponse(result)
|
||||
except Exception as e:
|
||||
logger.exception("支付宝回调处理失败: %s", e)
|
||||
return PlainTextResponse("fail")
|
||||
|
||||
|
||||
@router.get("/order/{order_id}/status", response_model=OrderStatusResponse)
|
||||
async def get_order_status(
|
||||
order_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
return await service.get_order_status(order_id, current_user.id)
|
||||
|
||||
|
||||
@router.get("/orders", response_model=List[OrderListResponse])
|
||||
async def list_orders(
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
return await service.list_orders(current_user.id)
|
||||
|
||||
|
||||
# 订单列表路由(兼容旧版 /api/orders)
|
||||
orders_router = APIRouter(
|
||||
prefix="/api/orders",
|
||||
tags=["orders"],
|
||||
responses={
|
||||
401: {"description": "认证失败"},
|
||||
404: {"description": "订单不存在"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@orders_router.get("", response_model=List[OrderListResponse])
|
||||
async def get_orders(
|
||||
current_user: User = Depends(get_current_user),
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
"""获取当前用户的订单列表"""
|
||||
return await service.list_orders(current_user.id)
|
||||
68
api/app/features/payment/schemas.py
Normal file
68
api/app/features/payment/schemas.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""支付模块 Pydantic 模型定义(从 payment 迁入 app)"""
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PaymentResult(BaseModel):
|
||||
success: bool
|
||||
payment_method: str
|
||||
out_trade_no: str
|
||||
wechat_params: Optional[Dict[str, str]] = None
|
||||
alipay_order_string: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class NotifyResult(BaseModel):
|
||||
success: bool
|
||||
out_trade_no: Optional[str] = None
|
||||
trade_no: Optional[str] = None
|
||||
total_amount: Optional[int] = None
|
||||
trade_status: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentStatus(BaseModel):
|
||||
success: bool
|
||||
out_trade_no: str
|
||||
trade_no: Optional[str] = None
|
||||
trade_status: str
|
||||
total_amount: Optional[int] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class CreateOrderRequest(BaseModel):
|
||||
plan_id: str
|
||||
payment_method: str
|
||||
|
||||
|
||||
class CreateOrderResponse(BaseModel):
|
||||
order_id: str
|
||||
payment_method: str
|
||||
wechat_params: Optional[Dict[str, str]] = None
|
||||
alipay_order_string: Optional[str] = None
|
||||
|
||||
|
||||
class OrderStatusResponse(BaseModel):
|
||||
order_id: str
|
||||
plan_id: str
|
||||
plan_name: str
|
||||
amount: int
|
||||
currency: str
|
||||
payment_method: str
|
||||
status: str
|
||||
trade_no: Optional[str] = None
|
||||
created_at: str
|
||||
paid_at: Optional[str] = None
|
||||
|
||||
|
||||
class OrderListResponse(BaseModel):
|
||||
id: str
|
||||
plan_id: str
|
||||
plan_name: str
|
||||
amount: int
|
||||
currency: str
|
||||
status: str
|
||||
payment_method: str
|
||||
created_at: str
|
||||
paid_at: Optional[str] = None
|
||||
13
api/app/features/payment/service.py
Normal file
13
api/app/features/payment/service.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
支付 feature 对外暴露:统一门面与配置(实现已迁入 app)
|
||||
"""
|
||||
from app.features.payment.payment_config import PaymentConfig
|
||||
from app.features.payment.payment_facade import PaymentService
|
||||
from app.features.payment.payment_exceptions import PaymentConfigError, PaymentError
|
||||
|
||||
__all__ = [
|
||||
"PaymentService",
|
||||
"PaymentConfig",
|
||||
"PaymentError",
|
||||
"PaymentConfigError",
|
||||
]
|
||||
247
api/app/features/payment/wechat_client.py
Normal file
247
api/app/features/payment/wechat_client.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
微信支付 API v3 封装(从 payment 迁入 app)
|
||||
"""
|
||||
import json
|
||||
from app.core.logging import get_logger
|
||||
import os
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from app.features.payment.payment_config import WeChatPayConfig
|
||||
from app.features.payment.schemas import NotifyResult, PaymentResult, PaymentStatus
|
||||
from app.features.payment.payment_exceptions import (
|
||||
PaymentConfigError,
|
||||
PaymentCreateError,
|
||||
PaymentNotifyError,
|
||||
PaymentQueryError,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _normalize_pem_key(key: str) -> str:
|
||||
if not key or not key.strip():
|
||||
return key
|
||||
key = key.strip().lstrip("\ufeff").strip('"').strip("'")
|
||||
key = key.replace("\\n", "\n")
|
||||
key = key.replace("\r\n", "\n").replace("\r", "\n")
|
||||
return key
|
||||
|
||||
|
||||
def _resolve_key_path(key_path: str) -> str:
|
||||
if not key_path or not key_path.strip():
|
||||
return key_path
|
||||
key_path = key_path.strip()
|
||||
if os.path.isabs(key_path):
|
||||
return key_path
|
||||
abs_cwd = os.path.abspath(key_path)
|
||||
if os.path.isfile(abs_cwd):
|
||||
return abs_cwd
|
||||
try:
|
||||
# app/features/payment/wechat_client.py -> api/
|
||||
api_dir = os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
)
|
||||
abs_api = os.path.normpath(os.path.join(api_dir, key_path))
|
||||
if os.path.isfile(abs_api):
|
||||
return abs_api
|
||||
except Exception:
|
||||
pass
|
||||
return abs_cwd
|
||||
|
||||
|
||||
class WeChatPayClient:
|
||||
def __init__(self, config: WeChatPayConfig):
|
||||
self._config = config
|
||||
self._client = None
|
||||
|
||||
def ensure_client(self):
|
||||
self._ensure_client()
|
||||
|
||||
def _ensure_client(self):
|
||||
if not self._config.is_configured:
|
||||
raise PaymentConfigError("微信支付配置不完整,请检查环境变量")
|
||||
if self._client is None:
|
||||
try:
|
||||
from wechatpayv3 import WeChatPay, WeChatPayType
|
||||
|
||||
if self._config.private_key_path and self._config.private_key_path.strip():
|
||||
key_path = _resolve_key_path(self._config.private_key_path)
|
||||
with open(key_path, "r", encoding="utf-8") as f:
|
||||
private_key = f.read()
|
||||
elif self._config.private_key and self._config.private_key.strip():
|
||||
private_key = self._config.private_key.strip()
|
||||
else:
|
||||
raise PaymentConfigError(
|
||||
"未配置微信支付商户私钥:请设置 WECHAT_PAY_PRIVATE_KEY_PATH 或 WECHAT_PAY_PRIVATE_KEY"
|
||||
)
|
||||
private_key = _normalize_pem_key(private_key)
|
||||
if not private_key or "-----BEGIN" not in private_key:
|
||||
raise PaymentConfigError(
|
||||
"微信支付商户私钥格式错误:需为 PEM 格式。"
|
||||
)
|
||||
kwargs = dict(
|
||||
wechatpay_type=WeChatPayType.APP,
|
||||
mchid=self._config.mch_id,
|
||||
private_key=private_key,
|
||||
cert_serial_no=self._config.cert_serial_no,
|
||||
appid=self._config.app_id,
|
||||
apiv3_key=self._config.api_v3_key,
|
||||
notify_url=self._config.notify_url,
|
||||
)
|
||||
if self._config.use_platform_public_key:
|
||||
if self._config.platform_public_key_path and self._config.platform_public_key_path.strip():
|
||||
key_path = _resolve_key_path(self._config.platform_public_key_path)
|
||||
with open(key_path, "r", encoding="utf-8") as f:
|
||||
platform_pub_key = _normalize_pem_key(f.read())
|
||||
elif self._config.platform_public_key and self._config.platform_public_key.strip():
|
||||
platform_pub_key = _normalize_pem_key(
|
||||
self._config.platform_public_key.strip()
|
||||
)
|
||||
else:
|
||||
raise PaymentConfigError(
|
||||
"平台公钥模式需设置 WECHAT_PAY_PLATFORM_PUBLIC_KEY 或 WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH"
|
||||
)
|
||||
if not platform_pub_key or "-----BEGIN" not in platform_pub_key:
|
||||
raise PaymentConfigError("微信支付平台公钥格式错误,需为 PEM 格式")
|
||||
kwargs["public_key"] = platform_pub_key
|
||||
kwargs["public_key_id"] = self._config.platform_public_key_id.strip()
|
||||
else:
|
||||
kwargs["timeout"] = (10, 25)
|
||||
self._client = WeChatPay(**kwargs)
|
||||
logger.info("微信支付客户端初始化成功")
|
||||
except FileNotFoundError:
|
||||
key_path = (
|
||||
_resolve_key_path(self._config.private_key_path)
|
||||
if self._config.private_key_path
|
||||
else self._config.private_key_path
|
||||
)
|
||||
raise PaymentConfigError(
|
||||
f"微信支付商户私钥文件不存在: {key_path}"
|
||||
)
|
||||
except PaymentConfigError:
|
||||
raise
|
||||
except Exception as e:
|
||||
msg = str(e).strip()
|
||||
if "platform certificate" in msg.lower():
|
||||
raise PaymentConfigError(
|
||||
"无法获取微信支付平台证书。请改用平台公钥模式:设置 "
|
||||
"WECHAT_PAY_PLATFORM_PUBLIC_KEY 与 WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID。"
|
||||
)
|
||||
raise PaymentConfigError(f"微信支付客户端初始化失败: {e}")
|
||||
|
||||
def create_app_order(
|
||||
self,
|
||||
out_trade_no: str,
|
||||
total_amount: int,
|
||||
description: str,
|
||||
) -> PaymentResult:
|
||||
self._ensure_client()
|
||||
try:
|
||||
code, message = self._client.pay(
|
||||
description=description,
|
||||
out_trade_no=out_trade_no,
|
||||
amount={"total": total_amount, "currency": "CNY"},
|
||||
pay_type=self._get_pay_type(),
|
||||
)
|
||||
result = json.loads(message)
|
||||
if code in range(200, 300):
|
||||
prepay_id = result.get("prepay_id", "")
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = out_trade_no
|
||||
pay_params = self._client.sign(
|
||||
data=[self._config.app_id, timestamp, nonce_str, prepay_id]
|
||||
)
|
||||
wechat_params = {
|
||||
"appId": self._config.app_id,
|
||||
"partnerId": self._config.mch_id,
|
||||
"prepayId": prepay_id,
|
||||
"nonceStr": nonce_str,
|
||||
"timeStamp": timestamp,
|
||||
"sign": pay_params,
|
||||
"packageValue": "Sign=WXPay",
|
||||
}
|
||||
logger.info("微信支付预订单创建成功: %s", out_trade_no)
|
||||
return PaymentResult(
|
||||
success=True,
|
||||
payment_method="wechat",
|
||||
out_trade_no=out_trade_no,
|
||||
wechat_params=wechat_params,
|
||||
)
|
||||
else:
|
||||
error_msg = result.get("message", "未知错误")
|
||||
raise PaymentCreateError(f"微信支付下单失败: {error_msg}")
|
||||
except PaymentCreateError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("微信支付预订单创建异常: %s", e)
|
||||
raise PaymentCreateError(f"微信支付下单异常: {e}")
|
||||
|
||||
def verify_notify(self, headers: Dict[str, str], body: str) -> NotifyResult:
|
||||
self._ensure_client()
|
||||
try:
|
||||
result = self._client.callback(headers=headers, body=body)
|
||||
if result:
|
||||
event_data = result if isinstance(result, dict) else json.loads(result)
|
||||
trade_state = event_data.get("trade_state", "")
|
||||
out_trade_no = event_data.get("out_trade_no", "")
|
||||
transaction_id = event_data.get("transaction_id", "")
|
||||
amount_info = event_data.get("amount", {})
|
||||
total_amount = amount_info.get("total", 0)
|
||||
return NotifyResult(
|
||||
success=True,
|
||||
out_trade_no=out_trade_no,
|
||||
trade_no=transaction_id,
|
||||
total_amount=total_amount,
|
||||
trade_status=trade_state,
|
||||
)
|
||||
raise PaymentNotifyError("微信支付回调验签失败")
|
||||
except PaymentNotifyError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("微信支付回调处理异常: %s", e)
|
||||
raise PaymentNotifyError(f"微信支付回调处理失败: {e}")
|
||||
|
||||
def query_order(self, out_trade_no: str) -> PaymentStatus:
|
||||
self._ensure_client()
|
||||
try:
|
||||
code, message = self._client.query(out_trade_no=out_trade_no)
|
||||
result = json.loads(message)
|
||||
if code in range(200, 300):
|
||||
trade_state = result.get("trade_state", "NOTPAY")
|
||||
transaction_id = result.get("transaction_id", "")
|
||||
amount_info = result.get("amount", {})
|
||||
total_amount = amount_info.get("total", 0)
|
||||
return PaymentStatus(
|
||||
success=True,
|
||||
out_trade_no=out_trade_no,
|
||||
trade_no=transaction_id,
|
||||
trade_status=trade_state,
|
||||
total_amount=total_amount,
|
||||
)
|
||||
error_msg = result.get("message", "未知错误")
|
||||
raise PaymentQueryError(f"查询微信支付订单失败: {error_msg}")
|
||||
except PaymentQueryError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("查询微信支付订单异常: %s", e)
|
||||
raise PaymentQueryError(f"查询微信支付订单异常: {e}")
|
||||
|
||||
def close_order(self, out_trade_no: str) -> bool:
|
||||
self._ensure_client()
|
||||
try:
|
||||
code, message = self._client.close(out_trade_no=out_trade_no)
|
||||
if code in range(200, 300):
|
||||
logger.info("微信支付订单已关闭: %s", out_trade_no)
|
||||
return True
|
||||
logger.warning("关闭微信支付订单失败: %s - %s", code, message)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("关闭微信支付订单异常: %s", e)
|
||||
return False
|
||||
|
||||
def _get_pay_type(self):
|
||||
from wechatpayv3 import WeChatPayType
|
||||
return WeChatPayType.APP
|
||||
Reference in New Issue
Block a user