You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
29 KiB
29 KiB
支付 SDK 封装服务
文档版本:v3.1
最后更新:2026-01-29
服务类型:技术服务(SDK 封装)
目录
服务概述
支付服务是一个纯技术服务,封装了微信支付和支付宝支付的 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 类
# 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 类
# 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 类
# 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
配置说明
环境变量配置
# 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 文件示例
# 微信支付
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
依赖安装
# 微信支付 SDK
pip install wechatpayv3==1.2.6
# 支付宝 SDK
pip install python-alipay-sdk==3.7.0
# 注意:日志使用 Python 标准库 logging,无需额外安装
使用示例
在 recharge-service 中使用
# 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 路由中的依赖注入
# 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:
# 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
集成测试
使用沙箱环境测试真实的支付流程:
# 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
运行测试
# 运行所有单元测试
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)处理
- 记录详细日志便于排查
相关文档
文档版本: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)
- 初始版本(已废弃)