Files
fakabot/payments_lemzf_official.py
T

382 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""
柠檬支付官方标准对接模块
严格按照官方文档 https://api.lemzf.com/doc.html 实现
支持页面跳转支付和API接口支付
"""
import hashlib
import requests
import time
from typing import Dict, Any, Optional, Tuple
from urllib.parse import urlencode
class LemzfPayment:
"""柠檬支付官方标准对接类"""
def __init__(self, merchant_id: str, key: str, gateway: str = None, api_gateway: str = None):
"""
初始化柠檬支付
Args:
merchant_id: 商户ID
key: 商户密钥
gateway: 页面跳转网关 (submit.php)
api_gateway: API接口网关 (mapi.php)
"""
self.merchant_id = merchant_id
self.key = key
self.gateway = gateway or "https://a1004a.lempay.com/submit.php"
self.api_gateway = api_gateway or "https://a1004a.lempay.com/mapi.php"
def md5_sign(self, params: Dict[str, Any]) -> str:
"""
MD5签名算法 - 严格按照官方文档实现
1. 参数按ASCII码从小到大排序
2. 排除sign、sign_type、值为空或0的参数
3. 拼接为a=b&c=d格式
4. 末尾拼接KEY,进行MD5加密,结果小写
Args:
params: 参数字典
Returns:
str: MD5签名
"""
# 过滤参数:排除sign、sign_type、值为空或0的参数
filtered_params = {}
for k, v in params.items():
if k in ('sign', 'sign_type'):
continue
if v is None or v == '' or v == 0 or str(v) == '0':
continue
filtered_params[k] = v
# 按ASCII码排序
sorted_params = sorted(filtered_params.items())
# 拼接为URL键值对格式
param_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
# 拼接商户密钥
sign_str = param_str + self.key
# MD5加密,结果小写
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().lower()
def create_page_payment(self, order_id: str, amount: float, subject: str,
notify_url: str, return_url: str = None,
payment_type: str = None, device: str = "mobile") -> str:
"""
创建页面跳转支付链接
Args:
order_id: 商户订单号
amount: 支付金额
subject: 订单标题
notify_url: 异步通知地址
return_url: 同步跳转地址
payment_type: 支付方式 (alipay/wxpay/usdt等)
device: 设备类型 (mobile/pc)
Returns:
str: 支付链接
"""
params = {
'pid': self.merchant_id,
'type': payment_type,
'out_trade_no': order_id,
'notify_url': notify_url,
'name': subject,
'money': f"{amount:.2f}",
'device': device
}
# 添加return_url(如果提供)
if return_url:
params['return_url'] = return_url
# 生成签名
params['sign'] = self.md5_sign(params)
params['sign_type'] = 'MD5'
# 构建支付链接
query_string = urlencode(params)
return f"{self.gateway}?{query_string}"
def create_api_payment(self, order_id: str, amount: float, subject: str,
notify_url: str, payment_type: str, device: str = "mobile",
client_ip: str = "127.0.0.1") -> Dict[str, Any]:
"""
创建API接口支付
Args:
order_id: 商户订单号
amount: 支付金额
subject: 订单标题
notify_url: 异步通知地址
payment_type: 支付方式
device: 设备类型
Returns:
Dict: API响应结果
"""
params = {
'pid': self.merchant_id,
'type': payment_type,
'out_trade_no': order_id,
'notify_url': notify_url,
'name': subject,
'money': f"{amount:.2f}",
'device': device,
'clientip': client_ip
}
# 生成签名
params['sign'] = self.md5_sign(params)
params['sign_type'] = 'MD5'
try:
# 发送POST请求
response = requests.post(self.api_gateway, data=params, timeout=30)
response.raise_for_status()
# 解析JSON响应
result = response.json()
return result
except requests.RequestException as e:
return {
'code': -1,
'msg': f'网络请求失败: {str(e)}',
'data': None
}
except ValueError as e:
return {
'code': -1,
'msg': f'响应解析失败: {str(e)}',
'data': None
}
def verify_callback(self, params: Dict[str, Any]) -> bool:
"""
验证回调签名
Args:
params: 回调参数
Returns:
bool: 签名是否有效
"""
if 'sign' not in params:
return False
received_sign = params['sign']
calculated_sign = self.md5_sign(params)
return received_sign.lower() == calculated_sign.lower()
def query_order(self, out_trade_no: str) -> Dict[str, Any]:
"""
查询单个订单
Args:
out_trade_no: 商户订单号
Returns:
Dict: 查询结果
"""
params = {
'pid': self.merchant_id,
'out_trade_no': out_trade_no
}
sign = self.md5_sign(params)
query_url = f"https://a1004a.lempay.com/api.php?act=order&pid={self.merchant_id}&out_trade_no={out_trade_no}&sign={sign}"
try:
response = requests.get(query_url, timeout=30)
response.raise_for_status()
return response.json()
except Exception as e:
return {
'code': -1,
'msg': f'查询失败: {str(e)}'
}
def create_lemzf_payment(config: Dict[str, Any]) -> LemzfPayment:
"""
创建柠檬支付实例的工厂函数
Args:
config: 支付配置
Returns:
LemzfPayment: 柠檬支付实例
"""
return LemzfPayment(
merchant_id=config['merchant_id'],
key=config['key'],
gateway=config.get('gateway'),
api_gateway=config.get('api_gateway')
)
def create_payment(config: Dict[str, Any], order_id: str, amount: float,
subject: str, notify_url: str, return_url: str = None,
client_ip: str = "127.0.0.1") -> Tuple[bool, str]:
"""
创建柠檬支付订单 - 兼容原有接口
Args:
config: 支付配置
order_id: 订单号
amount: 金额
subject: 标题
notify_url: 通知地址
return_url: 返回地址
Returns:
Tuple[bool, str]: (是否成功, 支付链接或错误信息)
"""
try:
lemzf = create_lemzf_payment(config)
# 获取支付方式
payment_type = config.get('type', 'alipay')
device = config.get('device', 'mobile')
# 优先使用API接口支付
if config.get('api_gateway'):
result = lemzf.create_api_payment(
order_id=order_id,
amount=amount,
subject=subject,
notify_url=notify_url,
payment_type=payment_type,
device=device,
client_ip=client_ip
)
if result.get('code') == 1:
data = result.get('data', result)
# 优先使用官方短链接 (cashier.php)
payurl = data.get('payurl', '')
qrcode = data.get('qrcode', '')
urlscheme = data.get('urlscheme', '')
# 优先级:cashier.php短链接 > 其他payurl > qrcode > urlscheme
if payurl and 'cashier.php' in payurl:
print(f"✅ 使用官方短链接: {len(payurl)} 字符")
return True, payurl
elif payurl:
print(f"✅ 使用官方支付链接: {len(payurl)} 字符")
return True, payurl
elif qrcode:
print(f"✅ 使用官方二维码链接: {len(qrcode)} 字符")
return True, qrcode
elif urlscheme:
print(f"✅ 使用原生协议链接: {len(urlscheme)} 字符")
return True, urlscheme
# API失败时记录错误但继续尝试页面跳转
print(f"⚠️ API支付失败: {result.get('msg', '未知错误')}")
# 使用页面跳转支付作为备用方案
payment_url = lemzf.create_page_payment(
order_id=order_id,
amount=amount,
subject=subject,
notify_url=notify_url,
return_url=return_url,
payment_type=payment_type,
device=device
)
return True, payment_url
except Exception as e:
error_msg = f"柠檬支付创建失败: {str(e)}"
print(f"{error_msg}")
return False, error_msg
def verify_lemzf_callback(config: Dict[str, Any], params: Dict[str, Any]) -> bool:
"""
验证柠檬支付回调 - 兼容原有接口
Args:
config: 支付配置
params: 回调参数
Returns:
bool: 验证是否通过
"""
try:
lemzf = create_lemzf_payment(config)
return lemzf.verify_callback(params)
except Exception as e:
print(f"❌ 柠檬支付回调验证失败: {str(e)}")
return False
# 支付方式映射
LEMZF_PAYMENT_TYPES = {
'alipay': 'alipay', # 支付宝
'wxpay': 'wxpay', # 微信支付
'usdt': 'usdt', # USDT
'qqpay': 'qqpay', # QQ钱包
'bank': 'bank', # 网银支付
}
# 设备类型映射
LEMZF_DEVICE_TYPES = {
'mobile': 'mobile', # 手机
'pc': 'pc', # 电脑
}
if __name__ == "__main__":
# 测试代码
config = {
'merchant_id': '1506',
'key': 'test_key',
'gateway': 'https://66101506.lemzf.com/submit.php',
'api_gateway': 'https://66101506.lemzf.com/mapi.php',
'type': 'alipay',
'device': 'mobile'
}
# 测试创建支付
success, result = create_payment(
config=config,
order_id='TEST001',
amount=99.99,
subject='测试订单',
notify_url='https://example.com/notify'
)
print(f"创建支付: {'成功' if success else '失败'}")
print(f"结果: {result}")
# 测试签名验证
test_params = {
'pid': '1506',
'trade_no': '2024100400001',
'out_trade_no': 'TEST001',
'type': 'alipay',
'name': '测试订单',
'money': '99.99',
'trade_status': 'TRADE_SUCCESS',
'sign': 'test_sign'
}
lemzf = create_lemzf_payment(config)
calculated_sign = lemzf.md5_sign(test_params)
print(f"计算签名: {calculated_sign}")