增加 微信支付相关的代码

This commit is contained in:
aaron 2025-01-17 16:51:50 +08:00
parent a0e7309b69
commit 23945c0b10
5 changed files with 262 additions and 7 deletions

View File

@ -1,11 +1,19 @@
from fastapi import APIRouter, Depends, Response from fastapi import APIRouter, Depends, Response, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.database import get_db from app.models.database import get_db
from app.models.user import UserInfo,UserDB, PhoneLoginRequest, generate_user_code from app.models.user import UserInfo,UserDB, PhoneLoginRequest, generate_user_code
from app.models.order import ShippingOrderDB, OrderStatus
from app.core.response import success_response, error_response, ResponseModel from app.core.response import success_response, error_response, ResponseModel
from app.core.wechat import WeChatClient from app.core.wechat import WeChatClient, get_wechat_pay_client
from app.core.security import create_access_token, set_jwt_cookie from app.core.security import create_access_token, set_jwt_cookie
from pydantic import BaseModel from pydantic import BaseModel
import json
import time
from datetime import datetime, timezone
from app.api.deps import get_current_user
from app.core.config import settings
import random
import string
router = APIRouter() router = APIRouter()
@ -74,3 +82,151 @@ async def wechat_phone_login(
except Exception as e: except Exception as e:
db.rollback() db.rollback()
return error_response(code=500, message=f"登录失败: {str(e)}") return error_response(code=500, message=f"登录失败: {str(e)}")
@router.post("/pay/order", response_model=ResponseModel)
async def create_wechat_pay_order(
orderid: str,
db: Session = Depends(get_db),
current_user: UserDB = Depends(get_current_user)
):
"""创建微信支付订单"""
# 查询订单
order = db.query(ShippingOrderDB).filter(
ShippingOrderDB.orderid == orderid,
ShippingOrderDB.userid == current_user.userid,
ShippingOrderDB.status == OrderStatus.UNPAID # 只能支付新创建的订单
).first()
if not order:
return error_response(code=404, message="订单不存在或状态不正确")
# 检查是否已经支付
if order.pay_status:
return error_response(code=400, message="订单已支付")
try:
# 获取微信支付客户端
wx_pay_client = WeChatClient()
# 创建支付订单
resp_data = wx_pay_client.create_jsapi_payment(
orderid=orderid,
amount=int(order.final_amount * 100),
openid=current_user.openid,
description=f"蜂快到家-配送订单{orderid}"
)
# 更新订单支付信息
order.prepay_id = resp_data.get("prepay_id")
db.commit()
# 生成支付参数
timestamp = str(int(time.time()))
nonce_str = generate_random_string()
package = f"prepay_id={resp_data.get('prepay_id')}"
# 构建签名数据
sign_data = f"{settings.WECHAT_APPID}\n{timestamp}\n{nonce_str}\n{package}\n"
signature = wx_pay_client.sign(sign_data.encode())
return success_response(data={
"orderid": orderid,
"payment_params": {
"appId": settings.WECHAT_APPID,
"timeStamp": timestamp,
"nonceStr": nonce_str,
"package": package,
"signType": "RSA",
"paySign": signature
}
})
except Exception as e:
db.rollback()
return error_response(code=500, message=f"创建支付订单失败: {str(e)}")
@router.post("/pay/notify")
async def wechat_pay_notify(request: Request):
"""微信支付回调通知"""
try:
# 获取微信支付客户端
wx_pay_client = WeChatClient()
# 读取原始请求数据
body = await request.body()
# 验证签名
headers = request.headers
signature = headers.get("Wechatpay-Signature")
timestamp = headers.get("Wechatpay-Timestamp")
nonce = headers.get("Wechatpay-Nonce")
serial_no = headers.get("Wechatpay-Serial")
if not all([signature, timestamp, nonce, serial_no]):
return error_response(code=400, message="缺少必要的请求头")
# 验证签名
sign_str = f"{timestamp}\n{nonce}\n{body.decode()}\n"
if not wx_pay_client.verify_signature(
sign_str.encode(),
signature,
serial_no
):
return error_response(code=401, message="签名验证失败")
# 解密数据
data = json.loads(body)
resource = data.get("resource")
if not resource:
return error_response(code=400, message="缺少资源数据")
# 解密回调数据
decrypted_data = wx_pay_client.decrypt_callback(
resource.get("associated_data", ""),
resource.get("nonce", ""),
resource.get("ciphertext", "")
)
# 解析解密后的数据
notify_data = json.loads(decrypted_data)
# 获取订单信息
trade_state = notify_data.get("trade_state")
orderid = notify_data.get("out_trade_no")
transaction_id = notify_data.get("transaction_id")
# 处理支付结果
if trade_state == "SUCCESS":
# 获取数据库会话
db = next(get_db())
try:
# 查询并更新订单
order = db.query(ShippingOrderDB).filter(
ShippingOrderDB.orderid == orderid,
ShippingOrderDB.pay_status == False # 避免重复处理
).first()
if order:
# 更新订单支付状态
order.pay_status = True
order.pay_time = datetime.now(timezone.utc)
order.transaction_id = transaction_id
db.commit()
return success_response(message="支付成功")
except Exception as e:
db.rollback()
# 记录错误日志
print(f"处理支付回调失败: {str(e)}")
return error_response(code=500, message=f"处理失败: {str(e)}")
finally:
db.close()
return success_response(message="回调处理成功")
except Exception as e:
# 记录错误日志
print(f"支付回调异常: {str(e)}")
return error_response(code=500, message=f"回调处理异常: {str(e)}")

View File

@ -58,6 +58,9 @@ class Settings(BaseSettings):
WECHAT_APPID: str = "wx3cc5b7dcb28f2756" WECHAT_APPID: str = "wx3cc5b7dcb28f2756"
WECHAT_SECRET: str = "fdf03e0ff428097c2a264da50b7d804e" WECHAT_SECRET: str = "fdf03e0ff428097c2a264da50b7d804e"
WECHAT_MCH_ID: str = "1688852888"
WECHAT_PRIVATE_KEY_PATH: str = "app/core/wechat_private_key.pem"
WECHAT_CERT_SERIAL_NO: str = "1688852888"
class Config: class Config:
case_sensitive = True case_sensitive = True

View File

@ -1,5 +1,19 @@
import aiohttp import aiohttp
from app.core.config import settings from app.core.config import settings
import requests
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
import json
import base64
import time
import random
import string
def generate_random_string(length=32):
"""生成指定长度的随机字符串"""
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
class WeChatClient: class WeChatClient:
"""微信客户端""" """微信客户端"""
@ -7,8 +21,53 @@ class WeChatClient:
def __init__(self): def __init__(self):
self.appid = settings.WECHAT_APPID self.appid = settings.WECHAT_APPID
self.secret = settings.WECHAT_SECRET self.secret = settings.WECHAT_SECRET
self.mchid = settings.WECHAT_MCH_ID
self.private_key = self._load_private_key()
self.cert_serial_no = settings.WECHAT_CERT_SERIAL_NO
self.access_token = None self.access_token = None
def _load_private_key(self):
"""加载商户私钥"""
with open(settings.WECHAT_PRIVATE_KEY_PATH, 'rb') as f:
return serialization.load_pem_private_key(
f.read(),
password=None
)
def sign(self, data: bytes) -> str:
"""签名数据"""
signature = self.private_key.sign(
data,
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode()
def post(self, path: str, **kwargs) -> requests.Response:
"""发送 POST 请求到微信支付接口"""
url = f"https://api.mch.weixin.qq.com{path}"
timestamp = str(int(time.time()))
nonce = generate_random_string()
# 准备签名数据
body = kwargs.get('json', '')
body_str = json.dumps(body) if body else ''
sign_str = f"POST\n{path}\n{timestamp}\n{nonce}\n{body_str}\n"
# 计算签名
signature = self.sign(sign_str.encode())
# 设置认证信息
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce}",signature="{signature}",timestamp="{timestamp}",serial_no="{self.cert_serial_no}"'
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': auth
}
return requests.post(url, headers=headers, **kwargs)
async def get_access_token(self): async def get_access_token(self):
"""获取接口调用凭证""" """获取接口调用凭证"""
if self.access_token: if self.access_token:
@ -42,3 +101,35 @@ class WeChatClient:
if result.get("errcode") == 0: if result.get("errcode") == 0:
return result.get("phone_info") return result.get("phone_info")
raise Exception(result.get("errmsg", "获取手机号失败")) raise Exception(result.get("errmsg", "获取手机号失败"))
def create_jsapi_payment(self, orderid: str, amount: int, openid: str, description: str) -> dict:
"""创建 JSAPI 支付订单
Args:
orderid: 订单号
amount: 支付金额()
openid: 用户openid
description: 商品描述
"""
pay_data = {
"appid": settings.WECHAT_APPID,
"mchid": settings.WECHAT_MCH_ID,
"description": description,
"out_trade_no": orderid,
"notify_url": f"{settings.API_BASE_URL}/api/wechat/pay/notify",
"amount": {
"total": amount,
"currency": "CNY"
},
"payer": {
"openid": openid
}
}
# 调用微信支付API
resp = self.post("/v3/pay/transactions/jsapi", json=pay_data)
if resp.status_code != 200:
raise Exception(f"微信支付下单失败: {resp.json().get('message')}")
return resp.json()

View File

@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import Optional, List from typing import Optional, List
from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, Enum from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, Enum, Boolean
from sqlalchemy.sql import func from sqlalchemy.sql import func
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .database import Base from .database import Base
@ -41,6 +41,10 @@ class ShippingOrderDB(Base):
) )
deliveryman_user_id = Column(Integer, ForeignKey("users.userid"), nullable=True) deliveryman_user_id = Column(Integer, ForeignKey("users.userid"), nullable=True)
cancel_user_id = Column(Integer, ForeignKey("users.userid"), nullable=True) cancel_user_id = Column(Integer, ForeignKey("users.userid"), nullable=True)
pay_status = Column(Boolean, default=False) # 支付状态
prepay_id = Column(String(64)) # 微信支付预支付ID
pay_time = Column(DateTime(timezone=True)) # 支付时间
transaction_id = Column(String(64)) # 微信支付交易号
class ShippingOrderPackageDB(Base): class ShippingOrderPackageDB(Base):
__tablename__ = "shipping_order_packages" __tablename__ = "shipping_order_packages"

View File

@ -13,3 +13,4 @@ unisms
cos-python-sdk-v5==1.9.25 cos-python-sdk-v5==1.9.25
bcrypt bcrypt
aiohttp==3.9.1 aiohttp==3.9.1
cryptography==42.0.2