refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env) 统一错误契约 Auth 与事务边界 Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client 可观测性(OpenTelemetry + LGTM)
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
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
|
||||
from app.core.deps_types import DbDep
|
||||
|
||||
_payment_service = None
|
||||
|
||||
@@ -21,7 +20,7 @@ def get_payment_service() -> PaymentService:
|
||||
|
||||
|
||||
def get_payment_order_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
db: DbDep,
|
||||
plan_service: PlanService = Depends(get_plan_service),
|
||||
) -> PaymentOrderService:
|
||||
"""Payment order facade: create_order, callbacks, list/status."""
|
||||
|
||||
@@ -7,11 +7,17 @@ import time
|
||||
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.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 (
|
||||
@@ -47,6 +53,11 @@ class PaymentOrderService:
|
||||
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,
|
||||
@@ -62,23 +73,18 @@ class PaymentOrderService:
|
||||
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")
|
||||
raise BadRequestError("无效的套餐 ID")
|
||||
if plan.price <= 0:
|
||||
raise HTTPException(status_code=400, detail="免费套餐无需支付")
|
||||
raise BadRequestError("免费套餐无需支付")
|
||||
if payment_method not in ("wechat", "alipay"):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="不支持的支付方式,仅支持 wechat / alipay"
|
||||
)
|
||||
raise BadRequestError("不支持的支付方式,仅支持 wechat / alipay")
|
||||
|
||||
client = _get_payment_service_client()
|
||||
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} 支付暂不可用,请选择其他支付方式",
|
||||
raise ServiceUnavailableError("支付宝支付接口正在开发中,暂时不可用")
|
||||
raise ServiceUnavailableError(
|
||||
f"{payment_method} 支付暂不可用,请选择其他支付方式"
|
||||
)
|
||||
|
||||
amount_fen = int(plan_price * 100)
|
||||
@@ -96,8 +102,6 @@ class PaymentOrderService:
|
||||
created_at=now,
|
||||
expired_at=now + timedelta(minutes=ORDER_EXPIRE_MINUTES),
|
||||
)
|
||||
self._db.add(order)
|
||||
await self._db.flush()
|
||||
|
||||
if payment_method == "wechat":
|
||||
try:
|
||||
@@ -106,16 +110,12 @@ class PaymentOrderService:
|
||||
timeout=WECHAT_INIT_TIMEOUT_SEC,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(
|
||||
status_code=504, detail="微信支付初始化超时,请稍后重试。"
|
||||
)
|
||||
await self._persist_failed_order(order)
|
||||
raise GatewayTimeoutError("微信支付初始化超时,请稍后重试。") from None
|
||||
except Exception as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
await self._persist_failed_order(order)
|
||||
logger.exception("微信支付客户端初始化失败: {}", e)
|
||||
raise HTTPException(status_code=503, detail=f"微信支付暂不可用: {e!s}")
|
||||
raise ServiceUnavailableError(f"微信支付暂不可用: {e!s}") from e
|
||||
|
||||
try:
|
||||
payment_result = await asyncio.wait_for(
|
||||
@@ -124,32 +124,33 @@ class PaymentOrderService:
|
||||
payment_method,
|
||||
order_no,
|
||||
amount_fen,
|
||||
f"岁月时书 - {plan_display_name}",
|
||||
f"岁月留书 - {plan_display_name}",
|
||||
),
|
||||
timeout=PREPAY_TIMEOUT_SEC,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail="创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。",
|
||||
)
|
||||
await self._persist_failed_order(order)
|
||||
raise GatewayTimeoutError(
|
||||
"创建预支付超时,请检查网络或稍后重试。若为微信支付,请确认商户配置与网络可达微信服务器。"
|
||||
) from None
|
||||
except PaymentError as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"创建支付订单失败: {e.message}"
|
||||
)
|
||||
await self._persist_failed_order(order)
|
||||
raise AppError(
|
||||
f"创建支付订单失败: {e.message}",
|
||||
status_code=500,
|
||||
error_code="PAYMENT_FAILED",
|
||||
) from e
|
||||
except Exception as e:
|
||||
order.status = "failed"
|
||||
await self._db.flush()
|
||||
await self._persist_failed_order(order)
|
||||
logger.exception("创建支付订单异常: {}", e)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"创建支付订单异常: {type(e).__name__}: {e!s}"
|
||||
)
|
||||
raise AppError(
|
||||
f"创建支付订单异常: {type(e).__name__}: {e!s}",
|
||||
status_code=500,
|
||||
error_code="INTERNAL_ERROR",
|
||||
) from e
|
||||
|
||||
await self._db.commit()
|
||||
async with transactional(self._db):
|
||||
self._db.add(order)
|
||||
logger.info(
|
||||
"订单创建成功: order_no={}, payment_method={}, amount_fen={}",
|
||||
order_no,
|
||||
@@ -173,29 +174,29 @@ class PaymentOrderService:
|
||||
logger.info("支付回调: 订单已处理过 {}", 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(
|
||||
"用户 {} 订阅已升级为 {},到期: {}",
|
||||
user.id,
|
||||
order.plan_id,
|
||||
user.subscription_expires_at,
|
||||
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)
|
||||
)
|
||||
await self._db.commit()
|
||||
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
|
||||
)
|
||||
@@ -232,7 +233,7 @@ class PaymentOrderService:
|
||||
)
|
||||
order = result.scalar_one_or_none()
|
||||
if order is None:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
raise NotFoundError("订单不存在")
|
||||
return OrderStatusResponse(
|
||||
order_id=order.id,
|
||||
plan_id=order.plan_id,
|
||||
|
||||
@@ -72,6 +72,8 @@ class PaymentConfig:
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings) -> "PaymentConfig":
|
||||
from app.core.runtime_constants import misc_defaults
|
||||
|
||||
wechat_private_key = (
|
||||
getattr(settings, "wechat_pay_private_key", "") or ""
|
||||
).strip()
|
||||
@@ -90,9 +92,7 @@ class PaymentConfig:
|
||||
wechat_private_key_path = (
|
||||
getattr(settings, "wechat_pay_private_key_path", "") or ""
|
||||
).strip()
|
||||
alipay_under = (
|
||||
getattr(settings, "alipay_under_development", "true") or "true"
|
||||
).lower()
|
||||
alipay_under = misc_defaults.alipay_under_development.lower()
|
||||
config = cls(
|
||||
wechat=WeChatPayConfig(
|
||||
app_id=getattr(settings, "wechat_pay_app_id", "") or "",
|
||||
@@ -115,7 +115,7 @@ class PaymentConfig:
|
||||
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",
|
||||
sign_type=misc_defaults.alipay_sign_type,
|
||||
),
|
||||
alipay_under_development=alipay_under in ("true", "1", "yes"),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
"""支付模块异常定义(从 payment 迁入 app)"""
|
||||
"""支付模块异常定义(继承 AppError,由全局 handler 统一映射)。"""
|
||||
|
||||
from app.core.errors import AppError
|
||||
|
||||
_PAYMENT_CODE_MAP: dict[str, tuple[int, str]] = {
|
||||
"PAYMENT_CONFIG_ERROR": (502, "PROVIDER_ERROR"),
|
||||
"PAYMENT_CREATE_ERROR": (502, "PROVIDER_ERROR"),
|
||||
"PAYMENT_NOTIFY_ERROR": (400, "BAD_REQUEST"),
|
||||
"PAYMENT_QUERY_ERROR": (502, "PROVIDER_ERROR"),
|
||||
"PAYMENT_ERROR": (400, "BAD_REQUEST"),
|
||||
}
|
||||
|
||||
|
||||
class PaymentError(Exception):
|
||||
class PaymentError(AppError):
|
||||
def __init__(self, message: str = "支付异常", code: str = "PAYMENT_ERROR"):
|
||||
self.message = message
|
||||
status_code, error_code = _PAYMENT_CODE_MAP.get(code, (400, code))
|
||||
super().__init__(message, status_code=status_code, error_code=error_code)
|
||||
self.code = code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class PaymentConfigError(PaymentError):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.deps_types import CurrentUserDep
|
||||
from app.core.errors import BadRequestError
|
||||
from app.core.logging import get_logger
|
||||
from app.core.openapi import error_responses
|
||||
from app.features.payment.deps import get_payment_order_service
|
||||
from app.features.payment.order_service import PaymentOrderService
|
||||
from app.features.payment.schemas import (
|
||||
@@ -15,25 +17,20 @@ from app.features.payment.schemas import (
|
||||
)
|
||||
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": "支付服务暂不可用"},
|
||||
},
|
||||
responses=error_responses(401, 404, 500, 502, 503, 504),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/create-order", response_model=CreateOrderResponse)
|
||||
async def create_order(
|
||||
request: CreateOrderRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: CurrentUserDep,
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
plan_service: PlanService = Depends(get_plan_service),
|
||||
):
|
||||
@@ -41,7 +38,7 @@ async def create_order(
|
||||
(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")
|
||||
raise BadRequestError("无效的套餐 ID")
|
||||
return await service.create_order(
|
||||
user_id=current_user.id,
|
||||
user_subscription_type=current_user.subscription_type,
|
||||
@@ -87,7 +84,7 @@ async def alipay_notify(
|
||||
@router.get("/order/{order_id}/status", response_model=OrderStatusResponse)
|
||||
async def get_order_status(
|
||||
order_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: CurrentUserDep,
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
return await service.get_order_status(order_id, current_user.id)
|
||||
@@ -95,7 +92,7 @@ async def get_order_status(
|
||||
|
||||
@router.get("/orders", response_model=List[OrderListResponse])
|
||||
async def list_orders(
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: CurrentUserDep,
|
||||
service: PaymentOrderService = Depends(get_payment_order_service),
|
||||
):
|
||||
return await service.list_orders(current_user.id)
|
||||
|
||||
Reference in New Issue
Block a user