# 支付 SDK 封装服务 > **文档版本**:v3.1 > **最后更新**:2026-01-29 > **服务类型**:技术服务(SDK 封装) --- ## 目录 1. [服务概述](#服务概述) 2. [支付方式](#支付方式) 3. [架构设计](#架构设计) 4. [服务实现](#服务实现) 5. [配置说明](#配置说明) 6. [使用示例](#使用示例) 7. [测试](#测试) --- ## 服务概述 支付服务是一个**纯技术服务**,封装了微信支付和支付宝支付的 SDK 调用。它不管理任何业务数据,不暴露 API 接口,只被 `recharge-service` 内部调用。 ### 服务定位 - ✅ **技术服务**:封装第三方支付 SDK - ✅ **无状态**:不存储任何数据 - ✅ **内部调用**:只被 recharge-service 使用 - ❌ **非业务服务**:不包含业务逻辑 - ❌ **无数据表**:不管理数据库表 - ❌ **无 API 路由**:不对外暴露接口 ### 职责 - 调用微信支付 SDK 创建支付订单 - 调用支付宝 SDK 创建支付订单 - 验证微信支付回调签名 - 验证支付宝回调签名 --- ## 支付方式 ### 1. 微信支付 支持的支付类型: - **Native 扫码支付**:生成二维码,用户扫码支付 - **H5 支付**:移动端网页支付 - **小程序支付**:微信小程序内支付 - **JSAPI 支付**:微信公众号内支付 ### 2. 支付宝支付 支持的支付类型: - **扫码支付**:生成二维码,用户扫码支付 - **H5 支付**:移动端网页支付 - **APP 支付**:移动应用内支付 --- ## 架构设计 ### 服务分层 ``` ┌─────────────────────────────────────────────┐ │ recharge-service (充值业务服务) │ │ - 管理 recharge_orders 表 │ │ - 管理 payment_callbacks 表 │ │ - 处理充值业务逻辑 │ └─────────────────────────────────────────────┘ ↓ 调用 ┌─────────────────────────────────────────────┐ │ payment-service (支付 SDK 封装) │ │ - WechatPayment 类 │ │ - AlipayPayment 类 │ │ - 纯技术服务,无业务逻辑 │ └─────────────────────────────────────────────┘ ↓ 调用 ┌─────────────────────────────────────────────┐ │ 第三方支付平台 │ │ - 微信支付 API │ │ - 支付宝 API │ └─────────────────────────────────────────────┘ ``` ### 调用流程 ``` recharge-service.create_order() ↓ payment-service.create_payment(order_no, amount, payment_method) ↓ WechatPayment.create_payment() / AlipayPayment.create_payment() ↓ 返回支付凭证 (qrcode_url, h5_url 等) ↓ recharge-service 返回给前端 ``` --- ## 服务实现 ### PaymentService 类 ```python # app/services/payment_service.py import logging from decimal import Decimal from typing import TypedDict from app.services.payment.wechat_payment import WechatPayment from app.services.payment.alipay_payment import AlipayPayment from app.core.exceptions import ValidationError, PaymentError logger = logging.getLogger(__name__) class PaymentResult(TypedDict): """支付结果类型定义""" qrcode_url: str expires_in: int class PaymentService: """支付 SDK 封装服务(纯技术服务,无状态)""" def __init__(self): """初始化支付客户端""" self.wechat = WechatPayment() self.alipay = AlipayPayment() async def create_payment( self, order_no: str, amount: Decimal, payment_method: str, description: str ) -> PaymentResult: """创建支付订单 Args: order_no: 订单号(由 recharge-service 生成) amount: 支付金额(元,使用 Decimal 保证精度) payment_method: 支付方式(wechat/alipay) description: 支付描述 Returns: PaymentResult: 支付凭证,包含: - qrcode_url: 二维码 URL(扫码支付) - expires_in: 过期时间(秒) Raises: ValidationError: 支付方式不支持或参数错误 PaymentError: 支付订单创建失败 """ logger.info( "创建支付订单: order_no=%s, amount=%s, method=%s", order_no, amount, payment_method ) try: if payment_method == 'wechat': return await self.wechat.create_payment(order_no, amount, description) elif payment_method == 'alipay': return await self.alipay.create_payment(order_no, amount, description) else: raise ValidationError(f"不支持的支付方式: {payment_method}") except ValidationError: raise # 直接向上抛出验证错误 except Exception as e: logger.error( "创建支付订单失败: order_no=%s", order_no, exc_info=True ) raise PaymentError(f"支付订单创建失败: {str(e)}") from e async def verify_callback( self, payment_method: str, callback_data: dict ) -> bool: """验证支付回调签名 Args: payment_method: 支付方式(wechat/alipay) callback_data: 回调数据(包含签名) Returns: bool: 签名是否有效 """ logger.info( "验证支付回调: method=%s, order_no=%s", payment_method, callback_data.get('order_no') ) try: if payment_method == 'wechat': return await self.wechat.verify_callback(callback_data) elif payment_method == 'alipay': return await self.alipay.verify_callback(callback_data) else: logger.warning("不支持的支付方式: method=%s", payment_method) return False except Exception as e: logger.error( "验证回调签名失败: method=%s", payment_method, exc_info=True ) return False ``` ### WechatPayment 类 ```python # app/services/payment/wechat_payment.py import logging from decimal import Decimal from typing import TypedDict from wechatpayv3 import WeChatPay, WeChatPayType from app.core.config import settings from app.core.exceptions import PaymentError logger = logging.getLogger(__name__) class PaymentResult(TypedDict): """支付结果类型定义""" qrcode_url: str expires_in: int class WechatPayment: """微信支付 SDK 封装""" def __init__(self): """初始化微信支付客户端""" self.wxpay = WeChatPay( wechatpay_type=WeChatPayType.NATIVE, mchid=settings.WECHAT_PAY_MCHID, private_key=settings.WECHAT_PAY_PRIVATE_KEY, cert_serial_no=settings.WECHAT_PAY_CERT_SERIAL_NO, apiv3_key=settings.WECHAT_PAY_APIV3_KEY, appid=settings.WECHAT_APPID ) async def create_payment( self, order_no: str, amount: Decimal, description: str ) -> PaymentResult: """创建微信支付订单 Args: order_no: 订单号 amount: 支付金额(元,Decimal 类型) description: 支付描述 Returns: PaymentResult: { 'qrcode_url': 'weixin://wxpay/bizpayurl?pr=xxx', 'expires_in': 1800 } Raises: PaymentError: 微信支付订单创建失败 """ logger.info( "调用微信支付 API: order_no=%s, amount=%s", order_no, amount ) # 金额转换为分(整数) amount_fen = int(amount * 100) code, message = self.wxpay.pay( description=description, out_trade_no=order_no, amount={'total': amount_fen}, notify_url=settings.WECHAT_PAY_NOTIFY_URL ) if code == 200: logger.info("微信支付订单创建成功: order_no=%s", order_no) return { 'qrcode_url': message.get('code_url'), 'expires_in': 1800 # 30 分钟 } else: logger.error( "微信支付订单创建失败: order_no=%s, code=%s, message=%s", order_no, code, message ) raise PaymentError(f"微信支付创建失败:{message}") async def verify_callback(self, callback_data: dict) -> bool: """验证微信支付回调签名 Args: callback_data: 回调数据,包含: - headers: 请求头(包含签名) - body: 请求体 Returns: bool: 签名是否有效 """ try: result = self.wxpay.callback( headers=callback_data.get('headers'), body=callback_data.get('body') ) logger.info( "微信支付回调验证成功: order_no=%s", callback_data.get('order_no') ) return result is not None except Exception as e: logger.error( "微信支付回调验证失败: order_no=%s", callback_data.get('order_no'), exc_info=True ) return False ``` ### AlipayPayment 类 ```python # app/services/payment/alipay_payment.py import logging from decimal import Decimal from typing import TypedDict from alipay import AliPay from app.core.config import settings from app.core.exceptions import PaymentError logger = logging.getLogger(__name__) class PaymentResult(TypedDict): """支付结果类型定义""" qrcode_url: str expires_in: int class AlipayPayment: """支付宝 SDK 封装""" def __init__(self): """初始化支付宝客户端""" self.alipay = AliPay( appid=settings.ALIPAY_APPID, app_notify_url=settings.ALIPAY_NOTIFY_URL, app_private_key_string=settings.ALIPAY_PRIVATE_KEY, alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY, sign_type="RSA2" ) async def create_payment( self, order_no: str, amount: Decimal, description: str ) -> PaymentResult: """创建支付宝支付订单 Args: order_no: 订单号 amount: 支付金额(元,Decimal 类型) description: 支付描述 Returns: PaymentResult: { 'qrcode_url': 'https://qr.alipay.com/xxx', 'expires_in': 1800 } Raises: PaymentError: 支付宝订单创建失败 """ logger.info( "调用支付宝 API: order_no=%s, amount=%s", order_no, amount ) order_string = self.alipay.api_alipay_trade_precreate( subject=description, out_trade_no=order_no, total_amount=str(amount) ) if order_string.get('code') == '10000': logger.info("支付宝订单创建成功: order_no=%s", order_no) return { 'qrcode_url': order_string.get('qr_code'), 'expires_in': 1800 # 30 分钟 } else: logger.error( "支付宝订单创建失败: order_no=%s, code=%s, msg=%s", order_no, order_string.get('code'), order_string.get('msg') ) raise PaymentError(f"支付宝创建失败:{order_string.get('msg')}") async def verify_callback(self, callback_data: dict) -> bool: """验证支付宝回调签名 Args: callback_data: 回调数据(表单参数) Returns: bool: 签名是否有效 """ try: # 提取签名 data = callback_data.copy() signature = data.pop('sign', None) sign_type = data.pop('sign_type', None) if not signature: logger.warning("支付宝回调缺少签名") return False # 验证签名 is_valid = self.alipay.verify(data, signature) if is_valid: logger.info( "支付宝回调验证成功: order_no=%s", data.get('out_trade_no') ) else: logger.warning( "支付宝回调验证失败: order_no=%s", data.get('out_trade_no') ) return is_valid except Exception as e: logger.error( "支付宝回调验证异常: order_no=%s", callback_data.get('out_trade_no'), exc_info=True ) return False ``` --- ## 配置说明 ### 环境变量配置 ```python # app/core/config.py from pydantic_settings import BaseSettings from pydantic import Field class Settings(BaseSettings): """应用配置(支付相关)""" # 微信支付配置 WECHAT_PAY_MCHID: str = Field(..., description="微信支付商户号") WECHAT_PAY_PRIVATE_KEY: str = Field(..., description="微信支付私钥(PEM 格式)") WECHAT_PAY_CERT_SERIAL_NO: str = Field(..., description="微信支付证书序列号") WECHAT_PAY_APIV3_KEY: str = Field(..., description="微信支付 APIv3 密钥") WECHAT_APPID: str = Field(..., description="微信公众号/小程序 AppID") WECHAT_PAY_NOTIFY_URL: str = Field(..., description="微信支付回调 URL") # 支付宝配置 ALIPAY_APPID: str = Field(..., description="支付宝应用 ID") ALIPAY_PRIVATE_KEY: str = Field(..., description="支付宝应用私钥(PEM 格式)") ALIPAY_PUBLIC_KEY: str = Field(..., description="支付宝公钥(PEM 格式)") ALIPAY_NOTIFY_URL: str = Field(..., description="支付宝回调 URL") model_config = { "env_file": ".env", "extra": "forbid" # 禁止额外字段 } settings = Settings() ``` ### .env 文件示例 ```bash # 微信支付 WECHAT_PAY_MCHID=1234567890 WECHAT_PAY_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n-----END PRIVATE KEY----- WECHAT_PAY_CERT_SERIAL_NO=ABC123DEF456 WECHAT_PAY_APIV3_KEY=your_apiv3_key_32_characters WECHAT_APPID=wx1234567890abcdef WECHAT_PAY_NOTIFY_URL=https://api.jointo.ai/api/v1/recharge/callbacks/wechat # 支付宝 ALIPAY_APPID=2021001234567890 ALIPAY_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY----- ALIPAY_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY----- ALIPAY_NOTIFY_URL=https://api.jointo.ai/api/v1/recharge/callbacks/alipay ``` ### 依赖安装 ```bash # 微信支付 SDK pip install wechatpayv3==1.2.6 # 支付宝 SDK pip install python-alipay-sdk==3.7.0 # 注意:日志使用 Python 标准库 logging,无需额外安装 ``` --- ## 使用示例 ### 在 recharge-service 中使用 ```python # app/services/recharge_service.py from decimal import Decimal from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession from app.services.payment_service import PaymentService from app.repositories.recharge_repository import RechargeRepository from app.services.credit_service import CreditService class RechargeService: """充值服务(业务逻辑层)""" def __init__( self, session: AsyncSession, repository: RechargeRepository, credit_service: CreditService, payment_service: PaymentService = Depends() # 依赖注入 ): self.session = session self.repository = repository self.credit_service = credit_service self.payment_service = payment_service async def create_order( self, user_id: str, package_id: str, payment_method: str ) -> dict: """创建充值订单 Args: user_id: 用户 ID package_id: 充值套餐 ID payment_method: 支付方式(wechat/alipay) Returns: 订单信息和支付凭证 """ # 1. 创建订单记录 order = await self.repository.create( user_id=user_id, package_id=package_id, payment_method=payment_method ) # 2. 调用 payment-service 创建支付 payment_params = await self.payment_service.create_payment( order_no=order.order_no, amount=Decimal(str(order.amount)), # 转换为 Decimal payment_method=payment_method, description=f"充值 {order.credits} 积分" ) # 3. 返回订单和支付凭证 return { 'order': { 'order_no': order.order_no, 'amount': float(order.amount), 'credits': order.credits, 'status': order.status }, 'paymentParams': payment_params } async def handle_payment_callback( self, payment_method: str, callback_data: dict ) -> dict: """处理支付回调 Args: payment_method: 支付方式(wechat/alipay) callback_data: 回调数据 Returns: 处理结果 """ # 1. 验证签名 is_valid = await self.payment_service.verify_callback( payment_method=payment_method, callback_data=callback_data ) if not is_valid: return {'success': False, 'message': '签名验证失败'} # 2. 更新订单状态 order_no = callback_data.get('order_no') await self.repository.update(order_no, {'status': 'paid'}) # 3. 增加积分 order = await self.repository.get_by_order_no(order_no) await self.credit_service.add_credits( user_id=order.user_id, amount=order.credits, reason=f"充值订单 {order_no}" ) return {'success': True, 'message': '充值成功'} ``` ### API 路由中的依赖注入 ```python # app/api/v1/recharge.py from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_session from app.services.recharge_service import RechargeService from app.services.payment_service import PaymentService from app.repositories.recharge_repository import RechargeRepository from app.services.credit_service import CreditService router = APIRouter(prefix="/recharge", tags=["充值"]) def get_recharge_service( session: AsyncSession = Depends(get_session), payment_service: PaymentService = Depends() ) -> RechargeService: """获取充值服务实例(依赖注入)""" repository = RechargeRepository(session) credit_service = CreditService(session) return RechargeService( session=session, repository=repository, credit_service=credit_service, payment_service=payment_service ) @router.post("/orders") async def create_recharge_order( user_id: str, package_id: str, payment_method: str, service: RechargeService = Depends(get_recharge_service) ): """创建充值订单""" return await service.create_order(user_id, package_id, payment_method) @router.post("/callbacks/{payment_method}") async def handle_payment_callback( payment_method: str, callback_data: dict, service: RechargeService = Depends(get_recharge_service) ): """处理支付回调""" return await service.handle_payment_callback(payment_method, callback_data) ``` --- ## 测试 ### 单元测试 测试支付服务的核心逻辑,mock 第三方 SDK: ```python # tests/unit/test_payment_service.py import pytest from decimal import Decimal from unittest.mock import AsyncMock, patch, MagicMock from app.services.payment_service import PaymentService from app.core.exceptions import ValidationError, PaymentError @pytest.fixture def payment_service(): """创建支付服务实例""" return PaymentService() @pytest.mark.asyncio async def test_create_wechat_payment_success(payment_service): """测试创建微信支付订单成功""" with patch.object(payment_service.wechat, 'create_payment') as mock_create: mock_create.return_value = { 'qrcode_url': 'weixin://wxpay/bizpayurl?pr=test123', 'expires_in': 1800 } result = await payment_service.create_payment( order_no='TEST001', amount=Decimal('100.00'), payment_method='wechat', description='测试订单' ) assert result['qrcode_url'].startswith('weixin://') assert result['expires_in'] == 1800 mock_create.assert_called_once_with( 'TEST001', Decimal('100.00'), '测试订单' ) @pytest.mark.asyncio async def test_create_alipay_payment_success(payment_service): """测试创建支付宝订单成功""" with patch.object(payment_service.alipay, 'create_payment') as mock_create: mock_create.return_value = { 'qrcode_url': 'https://qr.alipay.com/test123', 'expires_in': 1800 } result = await payment_service.create_payment( order_no='TEST002', amount=Decimal('200.00'), payment_method='alipay', description='测试订单' ) assert result['qrcode_url'].startswith('https://qr.alipay.com/') assert result['expires_in'] == 1800 @pytest.mark.asyncio async def test_create_payment_invalid_method(payment_service): """测试不支持的支付方式""" with pytest.raises(ValidationError) as exc_info: await payment_service.create_payment( order_no='TEST003', amount=Decimal('100.00'), payment_method='invalid', description='测试订单' ) assert '不支持的支付方式' in str(exc_info.value) @pytest.mark.asyncio async def test_create_payment_sdk_error(payment_service): """测试 SDK 调用失败""" with patch.object(payment_service.wechat, 'create_payment') as mock_create: mock_create.side_effect = Exception('网络错误') with pytest.raises(PaymentError) as exc_info: await payment_service.create_payment( order_no='TEST004', amount=Decimal('100.00'), payment_method='wechat', description='测试订单' ) assert '支付订单创建失败' in str(exc_info.value) @pytest.mark.asyncio async def test_verify_wechat_callback_success(payment_service): """测试验证微信回调成功""" callback_data = { 'headers': {'Wechatpay-Signature': 'test_signature'}, 'body': '{"order_no": "TEST001"}', 'order_no': 'TEST001' } with patch.object(payment_service.wechat, 'verify_callback') as mock_verify: mock_verify.return_value = True result = await payment_service.verify_callback('wechat', callback_data) assert result is True mock_verify.assert_called_once_with(callback_data) @pytest.mark.asyncio async def test_verify_alipay_callback_success(payment_service): """测试验证支付宝回调成功""" callback_data = { 'out_trade_no': 'TEST002', 'trade_status': 'TRADE_SUCCESS', 'sign': 'test_signature', 'sign_type': 'RSA2' } with patch.object(payment_service.alipay, 'verify_callback') as mock_verify: mock_verify.return_value = True result = await payment_service.verify_callback('alipay', callback_data) assert result is True @pytest.mark.asyncio async def test_verify_callback_invalid_method(payment_service): """测试验证不支持的支付方式回调""" result = await payment_service.verify_callback('invalid', {}) assert result is False ``` ### 集成测试 使用沙箱环境测试真实的支付流程: ```python # tests/integration/test_payment_integration.py import pytest from decimal import Decimal from app.services.payment_service import PaymentService from app.core.config import settings @pytest.mark.integration @pytest.mark.asyncio async def test_wechat_payment_sandbox(): """测试微信支付沙箱环境(需要配置沙箱密钥)""" if not settings.WECHAT_PAY_SANDBOX_MODE: pytest.skip("未启用微信支付沙箱模式") service = PaymentService() result = await service.create_payment( order_no='SANDBOX_TEST_001', amount=Decimal('0.01'), # 沙箱环境使用小额测试 payment_method='wechat', description='沙箱测试订单' ) assert 'qrcode_url' in result assert result['expires_in'] > 0 @pytest.mark.integration @pytest.mark.asyncio async def test_alipay_payment_sandbox(): """测试支付宝沙箱环境(需要配置沙箱密钥)""" if not settings.ALIPAY_SANDBOX_MODE: pytest.skip("未启用支付宝沙箱模式") service = PaymentService() result = await service.create_payment( order_no='SANDBOX_TEST_002', amount=Decimal('0.01'), payment_method='alipay', description='沙箱测试订单' ) assert 'qrcode_url' in result assert result['expires_in'] > 0 ``` ### 运行测试 ```bash # 运行所有单元测试 docker exec jointo-server-app pytest tests/unit/test_payment_service.py -v # 运行集成测试(需要配置沙箱环境) docker exec jointo-server-app pytest tests/integration/test_payment_integration.py -v --integration # 查看测试覆盖率 docker exec jointo-server-app pytest tests/unit/test_payment_service.py --cov=app.services.payment_service --cov-report=html ``` --- ## 关键设计原则 ### 1. 单一职责 - payment-service 只负责与第三方支付平台交互 - 不处理业务逻辑(订单管理、积分增加等) - 不存储任何数据 ### 2. 无状态设计 - 不依赖数据库(无 AsyncSession) - 不管理数据表 - 每次调用都是独立的 ### 3. 内部服务 - 不暴露 API 路由 - 只被 recharge-service 调用 - 不直接面向用户 ### 4. 错误处理 - 所有异常向上抛出 - 由调用方(recharge-service)处理 - 记录详细日志便于排查 --- ## 相关文档 - [充值管理服务](./recharge-service.md) - 调用 payment-service 的业务服务 - [积分管理服务](./credit-service.md) - 充值成功后增加积分 - [用户管理服务](./user-service.md) - 用户核心服务 --- **文档版本**:v3.1 **最后更新**:2026-01-29 ### 变更记录 **v3.1 (2026-01-29)** - ✅ **技术栈合规性修复**:符合 Jointo 技术栈规范 - ✅ **日志系统**:从 loguru 改为标准库 logging - ✅ **依赖版本**:固定版本号(wechatpayv3==1.2.6, python-alipay-sdk==3.7.0) - ✅ **配置类**:添加 Pydantic Field 验证和 extra="forbid" - ✅ **异常处理**:使用项目自定义异常(PaymentError, ValidationError) - ✅ **类型注解**:金额使用 Decimal,定义 PaymentResult TypedDict - ✅ **依赖注入**:补充 FastAPI Depends 使用说明和示例 - ✅ **测试文档**:添加完整的单元测试和集成测试示例 **v3.0 (2026-01-26)** - ✅ **重构为纯技术服务**:移除所有业务逻辑 - ✅ **移除数据表**:删除 payments 表定义 - ✅ **移除 Repository 层**:不再管理数据 - ✅ **移除 API 路由**:不对外暴露接口 - ✅ **移除 Schema 定义**:简化为字典返回 - ✅ **简化依赖**:不依赖 AsyncSession - ✅ **明确定位**:SDK 封装服务,只被 recharge-service 调用 - ✅ **完善文档**:添加架构图、使用示例、设计原则 **v2.0 (2026-01-26)** - 规范化为符合 Jointo 技术栈的实现(已废弃) **v1.0 (2025-01-14)** - 初始版本(已废弃)