Files
ragflow_python/asr-monitor-test/app/payment_service.py

1092 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer
import hashlib
import random
import string
import xml.etree.ElementTree as ET
import requests
import time
from datetime import datetime, timedelta, date
from decimal import Decimal
from uuid import UUID
import json
import logging
from app.database import *
from jose import JWTError, jwt
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.backends import default_backend
import httpx
class CustomJSONResponse(JSONResponse):
"""
自定义 JSON 响应类,处理特殊类型:
- datetime: 转换为 ISO 8601 字符串
- date: 转换为 ISO 8601 字符串
- Decimal: 转换为 float
"""
def render(self, content: any) -> bytes:
"""
重写渲染方法,使用自定义编码器
"""
class EnhancedJSONEncoder(json.JSONEncoder):
def default(self, obj):
"""
增强型 JSON 编码器,处理多种特殊类型:
- datetime: 转换为 ISO 8601 字符串
- date: 转换为 ISO 8601 字符串
- time: 转换为 ISO 8601 字符串
- Decimal: 转换为 float
- UUID: 转换为字符串
- numpy 类型: 转换为 Python 原生类型
"""
# 处理日期时间类型
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, date):
return obj.isoformat()
# 处理 Decimal 类型
if isinstance(obj, Decimal):
return float(obj)
# 处理 UUID 类型
if isinstance(obj, UUID):
return str(obj)
"""
# 处理 numpy 类型
if isinstance(obj, np.integer):
return int(obj)
if isinstance(obj, np.floating):
return float(obj)
if isinstance(obj, np.ndarray):
return obj.tolist()
"""
# 处理其他自定义类型
if hasattr(obj, '__json__'):
return obj.__json__()
# 默认处理
return super().default(obj)
return json.dumps(
content,
ensure_ascii=False,
allow_nan=False,
indent=None,
separators=(",", ":"),
cls=EnhancedJSONEncoder
).encode("utf-8")
payment_router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
logger = logging.getLogger("payment")
# 微信支付配置
WX_APPID = "wx446813bfb3a6985a"
WX_MCH_ID = "1721301006"
WX_PAY_KEY = "7xK9pR2qY5vN3zW8bL1cD4fG6hJ7mQ0t"
WX_PAY_KEY_V3 = "7xK9pR2qY5vN3zW8bL1cD4fG6hJ7mQ0t"
WX_MCH_CERT_SERIAL_NO = "6292D0A4D7B092E361D6DD22C2FD7831479D48D9" # 证书序列号 v3使用
WX_PAY_NOTIFY_URL = "https://ragflow.szzysztech.com/apitest2/payment/wx_notify"
JWT_SECRET_KEY = "3e5b8d7f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d"
ALGORITHM = "HS256"
WX_PLATFORM_CERTS = {}
# 加载商户私钥
def load_private_key():
"""
从PEM格式文件加载商户私钥
假设私钥文件名为 apiclient_key.pem 并位于项目根目录
"""
try:
# 从文件加载 - 实际部署时建议使用环境变量或安全存储
key_path = "./payment_cert/apiclient_key.pem"
with open(key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
backend=default_backend()
)
return private_key
except Exception as e:
logging.error(f"加载商户私钥失败: {str(e)}")
# 备用方案:从环境变量加载
key_str = os.getenv("WX_MCH_PRIVATE_KEY")
if key_str:
return serialization.load_pem_private_key(
key_str.encode(),
password=None,
backend=default_backend()
)
raise Exception("无法加载微信支付商户私钥")
# 全局加载一次私钥
try:
MERCHANT_PRIVATE_KEY = load_private_key()
except Exception as e:
logging.error(f"初始化商户私钥失败: {str(e)}")
MERCHANT_PRIVATE_KEY = None
def get_wx_platform_public_key(serial_no: str):
"""获取微信平台公钥"""
if serial_no in WX_PLATFORM_CERTS:
return WX_PLATFORM_CERTS[serial_no]
# 实际应用中应从微信API获取证书这里简化处理
# 建议实现自动下载证书的机制
cert_path = "./payment_cert/apiclient_key.pem"
try:
with open(cert_path, "rb") as cert_file:
public_key = serialization.load_pem_public_key(
cert_file.read(),
backend=default_backend()
)
WX_PLATFORM_CERTS[serial_no] = public_key
return public_key
except Exception as e:
logger.error(f"加载平台证书失败: {str(e)}")
return None
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""
可选用户依赖项不抛出401错误
返回: 用户对象 或 None
"""
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
return get_user_by_id(user_id)
except (JWTError, StopIteration):
return None
@payment_router.post("/create_order")
async def create_order_api(
request: Request,
current_user: dict = Depends(get_current_user)
):
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
data = await request.json()
museum_id = data.get("museum_id")
sub_id = data.get("sub_id") # 参数名改为 subscription_id
# 获取博物馆订阅信息
museum_subscription = get_museum_subscription_by_id(sub_id)
if not museum_subscription:
raise HTTPException(status_code=404, detail="订阅不存在")
# 免费订阅直接激活
if museum_subscription["price"] == 0:
order_id = f"FREE_{int(time.time())}"
# 创建订单记录 - 使用数据库辅助函数
create_order({
"order_id": order_id,
"user_id": current_user["user_id"],
"museum_subscription_id": subscription_id,
"amount": 0,
"status": "activated",
"create_date": datetime.now()
})
# 激活订阅 - 使用数据库辅助函数
activate_user_subscription(
user_id=current_user["user_id"],
museum_subscription_id=sub_id,
order_id=order_id
)
return JSONResponse({
"code": 0,
"msg": "订阅激活成功",
"data": {"order_id": order_id}
})
# 付费订阅创建待支付订单
order_id = f"hxb_{int(time.time())}{random.randint(1000, 9999)}"
# 创建订单记录 - 使用数据库辅助函数
create_order({
"order_id": order_id,
"user_id": current_user["user_id"],
"museum_subscription_id": sub_id,
"amount": museum_subscription["price"],
"status": "created",
"create_date": datetime.now()
})
return JSONResponse({
"code": 0,
"msg": "订单创建成功",
"data": {
"order_id": order_id,
"amount": float(museum_subscription["price"])
}
})
# 获取支付参数
@payment_router.post("/prepay")
async def get_payment_params(
request: Request,
current_user: dict = Depends(get_current_user)
):
try:
data = await request.json()
order_id = data.get("order_id")
version = data.get("version", "v2") # 默认为 v3
if not order_id:
raise HTTPException(status_code=400, detail="缺少order_id参数")
# 获取订单信息 - 使用数据库辅助函数
order = get_order_by_id(order_id = order_id)
if not order:
raise HTTPException(status_code=404, detail="订单不存在")
if order["user_id"] != current_user["user_id"]:
raise HTTPException(status_code=403, detail="无权访问此订单")
if order["status"] != "created":
raise HTTPException(status_code=400, detail="订单状态异常")
# 获取用户openid
user = get_user_by_id(current_user["user_id"])
if not user or not user.get("openid"):
raise HTTPException(status_code=400, detail="用户信息不完整")
# 获取订阅信息以获取产品名称
museum_subscription = get_museum_subscription_by_id(order["museum_subscription_id"])
if not museum_subscription:
raise HTTPException(status_code=404, detail="关联订阅不存在")
# 获取模板名称
template = get_subscription_template_by_id(museum_subscription["template_id"])
if not template:
raise HTTPException(status_code=404, detail="订阅模板不存在")
product_name = f"博物馆讲解服务-{template['name']}"
amount_in_cents = int(order["amount"] * 100) # 转换为分
# 根据版本调用不同的支付接口
if version.lower() == "v2":
# V2 支付
logging.info(f"使用微信支付V2接口")
prepay_params = await generate_wx_prepay_params_v2(
order_id=order["order_id"],
total_fee=amount_in_cents,
openid=user["openid"],
body=product_name
)
# 生成支付签名
pay_sign = generate_pay_sign(
appid=WX_APPID,
timestamp=prepay_params["timestamp"],
noncestr=prepay_params["noncestr"],
prepay_id=prepay_params["prepay_id"]
)
response_data = {
"appid": prepay_params["appId"],
"timeStamp": prepay_params["timestamp"],
"nonceStr": prepay_params["noncestr"],
"package": f"prepay_id={prepay_params['prepay_id']}",
"signType": "MD5",
"paySign": pay_sign
}
else: # 默认为 v3
# V3 支付
logging.info(f"使用微信支付V3接口")
prepay_params = await generate_wx_prepay_params_v3(
order_id=order["order_id"],
total_fee=amount_in_cents,
openid=user["openid"],
body=product_name
)
# V3 已返回完整的支付参数
response_data = {
"appId": prepay_params["appId"],
"timeStamp": prepay_params["timeStamp"],
"nonceStr": prepay_params["nonceStr"],
"package": prepay_params["package"],
"signType": "RSA", # V3 使用 RSA 签名
"paySign": prepay_params["paySign"]
}
return JSONResponse({
"code": 0,
"msg": "success",
"data":response_data
})
except Exception as e:
logger.error(f"获取支付参数失败: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"获取支付参数失败: {str(e)}")
# 微信支付回调处理兼容V2和V3
@payment_router.post("/wx_notify")
async def wx_payment_notify(request: Request):
headers = request.headers
body = await request.body()
# 判断支付版本V3有特定头信息
if "Wechatpay-Signature" in headers:
return await handle_v3_notify(headers, body)
else:
return await handle_v2_notify(body)
async def handle_v3_notify(headers: dict, body: bytes):
"""处理V3回调"""
# 1. 验证签名
if not verify_wx_signature_v3(headers, body):
logger.warning("V3签名验证失败")
return generate_json_response("FAIL", "签名验证失败")
try:
# 2. 解析JSON数据
data = json.loads(body)
resource = data.get("resource", {})
# 3. 解密资源数据
decrypted_data = decrypt_v3_resource(resource)
if not decrypted_data:
logger.error("V3回调数据解密失败")
return generate_json_response("FAIL", "数据解密失败")
logger.info(f"V3解密数据: {json.dumps(decrypted_data, indent=2)}")
# 4. 检查支付状态
if decrypted_data.get("trade_state") != "SUCCESS":
logger.warning(f"支付未成功: {decrypted_data.get('trade_state_desc')}")
return generate_json_response("FAIL", "支付未成功")
# 5. 提取订单信息
order_id = decrypted_data.get("out_trade_no")
transaction_id = decrypted_data.get("transaction_id")
total_fee = float(decrypted_data["amount"]["payer_total"]) / 100 # 转换为元
# 6. 处理支付成功逻辑
success, msg = await process_payment_success(order_id, transaction_id, total_fee)
if not success:
return generate_json_response("FAIL", msg)
return generate_json_response("SUCCESS", "OK")
except Exception as e:
logger.exception(f"V3回调处理异常: {str(e)}")
return generate_json_response("FAIL", f"处理失败: {str(e)}")
async def handle_v2_notify(body: bytes):
"""处理V2回调"""
try:
logging.info(f"V2 wx_notify回调数据: {body}")
data = xml_to_dict(body.decode('utf-8'))
# 1. 验证签名
if not verify_wx_signature_v2(data, WX_PAY_KEY):
logger.warning("V2签名验证失败")
return generate_xml_response("FAIL", "签名验证失败")
# 2. 检查支付状态
if data.get("return_code") != "SUCCESS":
logger.warning(f"支付失败: {data.get('return_msg')}")
return generate_xml_response("FAIL", "支付失败")
# 3. 提取订单信息
order_id = data.get("out_trade_no")
transaction_id = data.get("transaction_id")
total_fee = float(data.get("total_fee", 0)) / 100 # 转换为元
# 4. 处理支付成功逻辑
success, msg = await process_payment_success(order_id, transaction_id, total_fee)
if not success:
return generate_xml_response("FAIL", msg)
return generate_xml_response("SUCCESS", "OK")
except Exception as e:
logger.exception(f"V2回调处理异常: {str(e)}")
return generate_xml_response("FAIL", f"处理失败: {str(e)}")
@payment_router.post("/check_payment")
async def check_payment_status(
request: Request,
current_user: dict = Depends(get_current_user)
):
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
data = await request.json()
order_id = data.get("order_id")
if not order_id:
raise HTTPException(status_code=400, detail="缺少订单号")
# 查询订单状态
order = get_order_by_id(order_id = order_id)
if not order:
return JSONResponse({"code": 404, "msg": "订单不存在"})
# 检查订单是否属于当前用户
if order["user_id"] != current_user["user_id"]:
return JSONResponse({"code": 403, "msg": "无权访问此订单"})
# 返回支付状态
return JSONResponse({
"code": 0,
"msg": "success",
"data": {
"order_id": order_id,
"paid": order["status"] in ["paid", "activated"],
"status": order["status"]
}
})
@payment_router.get("/get_subscriptions/{museum_id}")
async def get_museum_subscriptions_by_museum_id(
museum_id: str,
request: Request,
current_user: dict = Depends(get_current_user)
):
result = get_museum_subscriptions_by_museum(museum_id)
return CustomJSONResponse(result)
@payment_router.post("/get_order_list")
async def get_order_list(
request: Request,
current_user: dict = Depends(get_current_user)
):
data = await request.json()
museum_id = data.get("museum_id")
result = get_order_by_id(user_id = current_user["user_id"],combined=True,museum_id=museum_id)
return CustomJSONResponse({
"code": 0,
"msg": "success",
"data": result})
@payment_router.get("/get_order_detail/{order_id}")
async def get_order_detial(
order_id: str,
request: Request,
current_user: dict = Depends(get_current_user)
):
result = get_order_by_id(order_id = order_id,combined=True)
return CustomJSONResponse({
"code": 0,
"msg": "success",
"data": result})
@payment_router.post("/get_user_museum_subscriptions")
async def get_user_museum_subscriptions(
request: Request,
current_user: dict = Depends(get_current_user)
):
data = await request.json()
museum_id = data.get("museum_id")
user_id = current_user["user_id"] # 用户id
is_free = False
museum_info = get_museum_by_id(museum_id=museum_id)
if museum_info and museum_info['free']:
is_free = True
is_free_period = is_museum_free_period(museum_id)
is_subscription_valid = get_user_valid_subscription(user_id, museum_id)
can_access = False
can_access = is_free or is_free_period or is_subscription_valid
result = {
'can_access': can_access,
'is_free': is_free,
'is_free_period': is_free_period,
'is_subscription_valid': is_subscription_valid
}
return CustomJSONResponse({
"code": 0,
"msg": "success",
"data": result})
# --- 支付工具函数 ---
async def generate_wx_prepay_params_v2(order_id: str, total_fee: int, openid: str, body: str):
"""生成微信预支付参数 - 修复签名错误"""
try:
# 准备请求参数
nonce_str = generate_nonce_str(32)
params = {
"appid": WX_APPID,
"mch_id": WX_MCH_ID,
"nonce_str": nonce_str,
"body": body[:128], # 商品描述
"out_trade_no": order_id,
"total_fee": str(total_fee), # 确保字符串格式
"spbill_create_ip": "127.0.0.1", # 终端IP
"notify_url": WX_PAY_NOTIFY_URL,
"trade_type": "JSAPI",
"openid": openid
}
params1={'appid': 'wx446813bfb3a6985a',
'mch_id': '1721301006',
'nonce_str': '7qi8x7npD2S5JCD8LPV2mHEX5FgeWa1U',
'body': '博物馆讲解服务-1个月有效',
'out_trade_no': 'PAY_17515540373111',
'total_fee': '500',
'spbill_create_ip': '1.13.185.116',
'notify_url': 'https://yourdomain.com/api/payment/wx_notify',
'trade_type': 'JSAPI',
'openid': 'obKSz7V6a-avAF-vtQrnk_rnuSGE'}
logging.info(f"generate_wx_prepay_params--0 {params}")
# 生成签名 - 添加详细日志
sign_str_before = '&'.join([f"{k}={v}" for k, v in sorted(params.items())])
params["sign"] = generate_sign_v2(params, WX_PAY_KEY)
logging.info(f"Generated sign: {params['sign']}")
# 转换为XML
xml_data = dict_to_xml(params)
# 调用微信统一下单接口
try:
response = requests.post(
"https://api.mch.weixin.qq.com/pay/unifiedorder",
data=xml_data.encode('utf-8'),
headers={"Content-Type": "application/xml"},
timeout=10
)
#logging.info(f"微信支付接口响应状态码: {response.status_code}")
#logging.info(f"微信支付接口响应内容: {response.text}")
except Timeout:
logging.error("微信支付接口请求超时")
raise Exception("微信支付接口请求超时")
except ConnectionError:
logging.error("无法连接到微信支付服务器")
raise Exception("网络连接失败,请检查网络设置")
except RequestException as req_err:
logging.error(f"微信支付请求异常: {str(req_err)}")
raise Exception(f"微信支付请求失败: {str(req_err)}")
# 检查HTTP响应状态
if response.status_code != 200:
logging.error(f"微信支付接口返回错误状态码: {response.status_code}")
raise Exception(f"微信支付接口错误: HTTP {response.status_code}")
# 解析响应
response.encoding = 'utf-8'
try:
response_data = xml_to_dict(response.text)
except Exception as parse_err:
logging.error(f"解析微信响应XML失败: {parse_err}")
raise Exception("微信支付返回数据格式错误")
logging.info(f"generate_wx_prepay_params--3 响应数据: {response_data}")
# 检查返回结果
if response_data.get("return_code") != "SUCCESS":
error_msg = response_data.get("return_msg", "未知错误")
# 尝试解码可能的乱码
try:
error_msg = error_msg.encode('latin1').decode('utf-8')
except:
pass
logging.error(f"微信支付下单失败(return_code): {error_msg}")
raise Exception(f"微信支付下单失败: {error_msg}")
if response_data.get("result_code") != "SUCCESS":
error_code = response_data.get("err_code", "")
error_msg = response_data.get("err_code_des", "未知错误")
# 尝试解码可能的乱码
try:
error_msg = error_msg.encode('latin1').decode('utf-8')
except:
pass
logging.error(f"微信支付下单失败(result_code): [{error_code}] {error_msg}")
raise Exception(f"微信支付下单失败: {error_msg} (错误代码: {error_code})")
# 检查必须的预支付ID
prepay_id = response_data.get("prepay_id")
if not prepay_id:
logging.error("微信支付返回缺少prepay_id")
raise Exception("微信支付返回数据不完整")
# 返回预支付参数
return {
"appId": params["appid"],
"prepay_id": prepay_id,
"timestamp": str(int(time.time())),
"noncestr": nonce_str
}
except Exception as e:
logging.error(f"generate_wx_prepay_params 发生异常: {str(e)}", exc_info=True)
raise
def generate_sign_v2(params: dict, api_key: str) -> str:
"""生成微信支付签名 - 修复版"""
# 1. 参数按ASCII码排序
sorted_params = sorted(params.items(), key=lambda x: x[0])
# 2. 拼接成URL参数字符串仅排除None和sign字段
sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params
if k != "sign" and v is not None]) # 移除非空检查
# 3. 拼接API密钥
sign_str += f"&key={api_key}"
# 记录详细的签名字符串(调试用)
# logging.info(f"Sign string: {sign_str}")
#appid=wx446813bfb3a6985a&body=博物馆讲解服务-1个月有效&mch_id=1721301006&nonce_str=7qi8x7npD2S5JCD8LPV2mHEX5FgeWa1U&notify_url=https://yourdomain.com/api/payment/wx_notify&openid=obKSz7V6a-avAF-vtQrnk_rnuSGE&out_trade_no=PAY_17515540373111&spbill_create_ip=127.0.0.1&total_fee=500&trade_type=JSAPI
# 4. MD5加密并转为大写
md5 = hashlib.md5()
md5.update(sign_str.encode('utf-8'))
return md5.hexdigest().upper()
def generate_pay_sign(appid: str, timestamp: str, noncestr: str, prepay_id: str) -> str:
"""生成支付签名 (用于小程序端)"""
params = {
"appId": appid,
"timeStamp": timestamp,
"nonceStr": noncestr,
"package": f"prepay_id={prepay_id}",
"signType": "MD5"
}
return generate_sign_v2(params, WX_PAY_KEY)
def generate_nonce_str(length=32):
"""生成随机字符串"""
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
def dict_to_xml(data: dict) -> str:
"""字典转XML - 修复特殊字符处理"""
xml = "<xml>"
for key, value in data.items():
# 确保所有值都转为字符串
str_value = str(value)
# 对非数字类型使用CDATA包裹
if not str_value.isdigit():
xml += f"<{key}><![CDATA[{str_value}]]></{key}>"
else:
xml += f"<{key}>{str_value}</{key}>"
xml += "</xml>"
return xml
def xml_to_dict(xml_str: str) -> dict:
"""XML转字典 - 增强错误处理"""
try:
root = ET.fromstring(xml_str)
result = {}
for child in root:
result[child.tag] = child.text
return result
except ET.ParseError as e:
logging.error(f"XML解析失败: {e}")
return {}
def verify_wx_signature_v2(data: dict, api_key: str) -> bool:
"""验证微信签名"""
if "sign" not in data:
return False
sign = data.pop("sign")
calculated_sign = generate_sign_v2(data, api_key)
return sign == calculated_sign
def generate_xml_response(return_code: str, return_msg: str) -> Response:
"""生成支付回调XML响应"""
response_data = {
"return_code": return_code,
"return_msg": return_msg
}
xml_content = dict_to_xml(response_data)
return Response(
content=xml_content,
media_type="application/xml",
headers={"Cache-Control": "no-cache"}
)
def generate_json_response(code: str, message: str) -> JSONResponse:
return JSONResponse(
content={"code": code, "message": message},
status_code=200
)
# 新增的V3相关函数
def verify_wx_signature_v3(headers: dict, body: bytes) -> bool:
"""验证V3回调签名"""
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]):
logger.error("V3回调缺少必要头信息")
return False
# 构造验签名串
sign_str = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
logger.debug(f"V3签名串: {sign_str}")
try:
# 获取微信平台公钥
public_key = get_wx_platform_public_key(serial_no)
if not public_key:
logger.error("无法获取微信平台公钥")
return False
# 验证签名
public_key.verify(
base64.b64decode(signature),
sign_str.encode('utf-8'),
padding.PKCS1v15(),
SHA256()
)
return True
except Exception as e:
logger.error(f"V3签名验证失败: {str(e)}")
return False
def decrypt_v3_resource(resource: dict) -> dict:
"""解密V3回调资源数据"""
try:
ciphertext = resource.get('ciphertext')
nonce = resource.get('nonce')
associated_data = resource.get('associated_data', '')
if not all([ciphertext, nonce]):
logger.error("V3回调缺少解密参数")
return {}
# 使用AES-GCM解密
key_bytes = WX_PAY_KEY_V3.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)
# 创建解密器
cipher = Cipher(
algorithms.AES(key_bytes),
modes.GCM(nonce.encode('utf-8'), min_tag_length=16),
backend=default_backend()
)
decryptor = cipher.decryptor()
# 添加附加认证数据
if associated_data:
decryptor.authenticate_additional_data(associated_data.encode('utf-8'))
# 解密数据
decrypted = decryptor.update(ciphertext_bytes) + decryptor.finalize()
return json.loads(decrypted)
except Exception as e:
logger.error(f"V3解密失败: {str(e)}")
return {}
async def process_payment_success(order_id: str, transaction_id: str, total_fee: float):
"""处理支付成功逻辑"""
# 更新订单状态
update_order(order_id, {
"status": "paid",
"transaction_id": transaction_id,
"pay_time": datetime.now(),
"amount": total_fee
})
logging.info(f"支付成功,更新订单状态 {order_id} {transaction_id} {total_fee}")
# 获取订单信息
order = get_order_by_id(order_id = order_id)
if not order:
logger.error(f"订单不存在: {order_id}")
return False, "订单不存在"
# 激活订阅
success = activate_user_subscription(
user_id=order["user_id"],
museum_subscription_id=order["museum_subscription_id"],
order_id=order_id
)
if success:
# 更新订单状态为已激活
update_order(order_id, {"status": "activated"})
return True, "OK"
# 套餐激活函数
def activate_user_product(user_id: str, product_id: int, museum_id: int, order_id: str):
product = get_product_by_id(product_id)
if not product:
return
# 计算有效期
start_time = datetime.now()
end_time = calculate_expiry(start_time, product["validity_type"])
# 创建用户套餐记录
create_user_product({
"user_id": user_id,
"product_id": product_id,
"museum_id": museum_id,
"order_id": order_id,
"start_time": start_time,
"end_time": end_time,
"is_active": True
})
# 禁用同一博物馆的旧套餐
deactivate_previous_products(user_id, museum_id)
def calculate_expiry(start_time: datetime, validity_type: str) -> datetime:
if validity_type == "free":
return start_time + timedelta(days=7) # 免费套餐7天
elif validity_type == "1month":
return start_time + timedelta(days=30)
elif validity_type == "1year":
return start_time + timedelta(days=365)
else: # permanent
return datetime(2999, 12, 31)
async def generate_wx_prepay_params_v3(order_id: str, total_fee: int, openid: str, body: str):
"""微信支付v3统一下单"""
if MERCHANT_PRIVATE_KEY is None:
raise Exception("商户私钥未初始化,无法进行签名")
try:
# 确保序列号格式正确
cert_serial_no = WX_MCH_CERT_SERIAL_NO
if not cert_serial_no:
raise Exception("商户证书序列号格式错误")
# 1. 构造请求参数
nonce_str = generate_nonce_str(32)
params = {
"appid": WX_APPID,
"mchid": WX_MCH_ID,
"description": body[:128],
"out_trade_no": order_id,
"notify_url": WX_PAY_NOTIFY_URL,
"amount": {
"total": total_fee, # 单位:分
"currency": "CNY"
},
"payer": {
"openid": openid
},
"scene_info": {
"payer_client_ip": "1.13.185.116",
"device_id": "013467007045764",
"store_info": {
"id": "0005",
"name": "深圳博物馆",
"area_code": "440305",
"address": "广东省深圳市南山区科技中一道10000号"
}
},
}
# 2. 生成签名
timestamp = str(int(time.time())) # 确保是10位整数
method = "POST"
url_path = "/v3/pay/transactions/jsapi"
body_str = json.dumps(params, separators=(',', ':')) # 无空格JSON
# 记录请求参数
logging.info(f"V3统一下单参数: {json.dumps(params, indent=2)}")
logging.info(f"V3请求体: {body_str}")
signature = generate_v3_sign(method, url_path, timestamp, nonce_str, body_str)
# 3. 构造认证头
auth_header = f'WECHATPAY2-SHA256-RSA2048 ' \
f'mchid="{WX_MCH_ID}",' \
f'nonce_str="{nonce_str}",' \
f'signature="{signature}",' \
f'timestamp="{timestamp}",' \
f'serial_no="{cert_serial_no}"'
#logging.info(f"V3 Authorization头: {auth_header}")
# 4. 发送请求 - 使用content确保body完全一致
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",
headers={
"Authorization": auth_header,
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "MyServer/1.0"
},
content=body_str.encode('utf-8'), # 使用手动序列化的body
timeout=15
)
# 5. 处理响应
if response.status_code != 200:
error_detail = response.text
logging.error(f"V3接口错误: {response.status_code} {error_detail}")
# 尝试解析错误信息
try:
error_data = response.json()
error_code = error_data.get("code", "UNKNOWN")
error_msg = error_data.get("message", "未知错误")
raise Exception(f"微信支付V3接口错误: [{error_code}] {error_msg}")
except:
raise Exception(f"微信支付V3接口错误: HTTP {response.status_code}")
resp_data = response.json()
logging.info(f"V3统一下单响应: {json.dumps(resp_data, indent=2)}")
if "prepay_id" not in resp_data:
error_code = resp_data.get("code", "UNKNOWN")
error_msg = resp_data.get("message", "未知错误")
logging.error(f"V3下单失败: [{error_code}] {error_msg}")
raise Exception(f"微信支付V3下单失败: {error_msg}")
# 6. 生成小程序支付参数
prepay_id = resp_data["prepay_id"]
pay_sign = generate_v3_pay_sign(WX_APPID, timestamp, nonce_str, prepay_id)
await query_order_by_out_trade_no(order_id)
return {
"appId": WX_APPID,
"timeStamp": timestamp,
"nonceStr": nonce_str,
"package": f"prepay_id={prepay_id}",
"signType": "RSA",
"paySign": pay_sign
}
except httpx.TimeoutException:
logging.error("微信支付V3接口请求超时")
raise Exception("微信支付接口请求超时")
except httpx.RequestError as req_err:
logging.error(f"微信支付V3网络请求失败: {str(req_err)}")
raise Exception(f"网络连接失败: {str(req_err)}")
except Exception as e:
logging.error(f"V3统一下单异常: {str(e)}", exc_info=True)
raise
def generate_v3_sign(method: str, url: str, timestamp: str, nonce: str, body: str = "") -> str:
"""生成V3请求签名"""
# 构造签名字符串 (必须严格按照此格式)
message = f"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n"
# 记录签名串用于调试
logging.info(f"V3签名串: {message}")
# 使用私钥签名
signature = MERCHANT_PRIVATE_KEY.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
SHA256()
)
return base64.b64encode(signature).decode('utf-8')
def generate_v3_pay_sign(appid: str, timestamp: str, nonce_str: str, prepay_id: str) -> str:
"""生成小程序支付签名(V3)"""
# 构造签名字符串 (必须严格按照此格式)
sign_str = f"{appid}\n{timestamp}\n{nonce_str}\nprepay_id={prepay_id}\n"
# 记录签名串用于调试
logging.info(f"V3支付签名串: {sign_str}")
# 使用私钥签名
signature = MERCHANT_PRIVATE_KEY.sign(
sign_str.encode('utf-8'),
padding.PKCS1v15(),
SHA256()
)
return base64.b64encode(signature).decode('utf-8')
async def query_order_by_out_trade_no(out_trade_no: str):
"""通过商户订单号查询订单状态"""
try:
# 准备请求参数
nonce_str = generate_nonce_str(32)
timestamp = str(int(time.time()))
url_path = f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={WX_MCH_ID}"
# 生成签名
signature = generate_v3_sign("GET", url_path, timestamp, nonce_str)
# 构造认证头
auth_header = f'WECHATPAY2-SHA256-RSA2048 ' \
f'mchid="{WX_MCH_ID}",' \
f'nonce_str="{nonce_str}",' \
f'signature="{signature}",' \
f'timestamp="{timestamp}",' \
f'serial_no="{WX_MCH_CERT_SERIAL_NO}"'
# 发送请求
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.mch.weixin.qq.com{url_path}",
headers={
"Authorization": auth_header,
"Accept": "application/json",
"User-Agent": "MyServer/1.0"
},
timeout=10
)
# 处理响应
if response.status_code != 200:
error_detail = response.text
logging.error(f"订单查询失败: {response.status_code} {error_detail}")
return None
else:
logging.info(f"订单查询成功: {response.text}")
return response.json()
except Exception as e:
logging.error(f"订单查询异常: {str(e)}", exc_info=True)
return None
"""
需要在 database.py 中实现以下函数:
get_product_by_id
create_order
update_order
get_user_by_id
create_user_product
deactivate_user_products
"""