""" 支付订单门面:持有 db + 底层 payment 客户端,提供 create_order / 回调 / 查询。 """ import asyncio import time import uuid from datetime import timedelta from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import transactional, utc_now from app.core.errors import ( AppError, BadRequestError, GatewayTimeoutError, NotFoundError, ServiceUnavailableError, ) from app.core.logging import get_logger 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_payment_service_client(): 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 _persist_failed_order(self, order: Order) -> None: async with transactional(self._db): self._db.add(order) order.status = "failed" 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 BadRequestError("无效的套餐 ID") if plan.price <= 0: raise BadRequestError("免费套餐无需支付") if payment_method not in ("wechat", "alipay"): raise BadRequestError("不支持的支付方式,仅支持 wechat / alipay") client = _get_payment_service_client() if not client.is_method_available(payment_method): if payment_method == "alipay": raise ServiceUnavailableError("支付宝支付接口正在开发中,暂时不可用") raise ServiceUnavailableError( 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), ) 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: await self._persist_failed_order(order) raise GatewayTimeoutError("微信支付初始化超时,请稍后重试。") from None except Exception as e: await self._persist_failed_order(order) logger.exception("微信支付客户端初始化失败: {}", e) raise ServiceUnavailableError(f"微信支付暂不可用: {e!s}") from e 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: await self._persist_failed_order(order) raise GatewayTimeoutError( "创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。" ) from None except PaymentError as e: await self._persist_failed_order(order) raise AppError( f"创建支付订单失败: {e.message}", status_code=500, error_code="PAYMENT_FAILED", ) from e except Exception as e: await self._persist_failed_order(order) logger.exception("创建支付订单异常: {}", e) raise AppError( f"创建支付订单异常: {type(e).__name__}: {e!s}", status_code=500, error_code="INTERNAL_ERROR", ) from e async with transactional(self._db): self._db.add(order) logger.info( "订单创建成功: order_no={}, payment_method={}, amount_fen={}", 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("支付回调: 订单不存在 {}", out_trade_no) return if order.status == "paid": logger.info("支付回调: 订单已处理过 {}", out_trade_no) return now = utc_now() async with transactional(self._db): 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( "用户 {} 订阅已升级为 {},到期: {}", user.id, order.plan_id, user.subscription_expires_at, ) logger.info( "支付成功处理完成: 订单 {}, 第三方交易号 {}", out_trade_no, trade_no ) async def handle_wechat_notify(self, headers: dict, body: str) -> dict: client = _get_payment_service_client() 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_payment_service_client() 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 NotFoundError("订单不存在") 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 ]