Files

328 lines
18 KiB
Python
Raw Permalink Normal View History

2026-08-06 20:39:45 +08:00
import base64
import hashlib
import json
import os
import re
import secrets
from datetime import datetime
from pathlib import Path
2026-08-21 23:48:29 +08:00
from typing import Literal
2026-08-06 20:39:45 +08:00
from zoneinfo import ZoneInfo
import django
import orjson
import pydash
import requests
2026-08-21 23:48:29 +08:00
import xmltodict
2026-08-06 20:39:45 +08:00
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.PublicKey.RSA import RsaKey
from Crypto.Signature import pkcs1_15
from Crypto.Util.Padding import unpad
from dicttoxml import dicttoxml
from django.conf import settings
from pydantic import BaseModel, Field, SecretStr
from requests import Response
from sixGuoDjango.settings import base_dir_name
os.environ.setdefault(key="DJANGO_SETTINGS_MODULE", value=f"{base_dir_name}.settings")
django.setup()
class sixWechatPay六果泡泡支付(BaseModel):
2026-08-10 19:33:59 +08:00
mchid: str = Field(default="1700383419", alias="商户号", description="微信支付商户号")
appid: str = Field(default="wxf612e825377328f0", alias="商户号", description="微信支付商户号")
api_v3_key: str = Field(default="YWMGAIPTJTWZN4YB3YUTHWK34K9U9M1W", alias="APIv3密钥", description="用于回调解密")
api_v2_key: str = Field(default="OXRNKGGUYHGNJSSIHKFGVMAAOHRPXJAT", alias="APIv2密钥", description="仅老接口需要")
serial_no商户证书的序列号: str = Field(default="530160D54E8F56726F506DEE40D7FA49A558E8E0", description="商户证书的序列号")
2026-08-06 20:39:45 +08:00
# ---------- 小程序密钥 ----------
2026-08-10 19:33:59 +08:00
mini_program_app_secret: SecretStr = Field(default="8691f774e3c0c5a876b21a3e84109ef0", alias="小程序AppSecret")
2026-08-06 20:39:45 +08:00
# ---------- 商户API密钥对(用于签名请求) ----------
2026-08-10 19:33:59 +08:00
merchant_private_key_pem: str = Field(default=Path(settings.BASE_DIR).joinpath("wechat_微信支付证书/江苏六果科技有限公司支付证书/apiclient_key.pem").read_text(), alias="商户私钥PEM", description="apiclient_key.pem 内容")
2026-09-19 11:51:34 +08:00
merchant_public_key_pem: str = Field(default=Path(settings.BASE_DIR).joinpath("wechat_微信支付证书/江苏六果科技有限公司支付证书/apiclient_cert.pem").read_text(), alias="商户公钥PEM", description="apiclient_cert.pem 内容(很少用到)")
2026-08-06 20:39:45 +08:00
# ---------- 微信支付公钥(用于验签和加密敏感字段) ----------
2026-08-10 19:33:59 +08:00
wechatpay_public_key_id: str = Field(default="PUB_KEY_ID_0117003834192026080500181856002000", alias="微信支付公钥ID")
wechatpay_public_key_pem: str = Field(default=Path(settings.BASE_DIR).joinpath("wechat_微信支付证书/江苏六果科技有限公司支付证书/微信支付公钥.pem").read_text(), alias="微信支付公钥PEM", description="pub_key.pem 内容")
2026-08-21 23:48:29 +08:00
2026-08-06 20:39:45 +08:00
# ---------- 平台证书(灰度过渡期备用) ----------
2026-08-21 23:48:29 +08:00
platform_cert_pem: str = Field(default=Path(settings.BASE_DIR).joinpath("wechat_微信支付证书/江苏六果科技有限公司支付证书/sixgou_ping_tai_zheng_shu_cert.pem").read_text(), alias="平台证书PEM",
description="wechatpay.pem 内容")
platform_cert_pem_id: str = Field(default="11D762828E947104028F2D045DE78FA806D0D4F2", description="平台证书的证收序列号ID")
pay_uri: str | None = Field(default=None, description="请求支付时需要的微信的url地址")
pay_请求体: dict = Field(default=None, description="发请求签名时需要的请求体")
2026-08-10 19:33:59 +08:00
pay_methon方法: Literal["GET", "POST", "PATCH"] = Field(default="POST", description="发请求签名时需要的请求体")
2026-08-21 23:48:29 +08:00
pay_Headers: dict = Field(default={}, description="生成的初始化请求头")
2026-08-06 20:39:45 +08:00
def model_post_init(self, __context):
2026-08-10 19:33:59 +08:00
pass
2026-08-21 23:48:29 +08:00
""""""
if self.pay_uri is None or self.pay_uri == "":
return None
url_data = re.search(r'/v3.*', string=self.pay_uri).group()
utc_unix_timestamp = int(datetime.now(tz=ZoneInfo("utc")).timestamp())
random_str = secrets.token_hex(nbytes=16) # 随机字符串
if isinstance(self.pay_请求体, str):
body_inner = self.pay_请求体
elif isinstance(self.pay_请求体, dict):
body_inner = json.dumps(self.pay_请求体)
elif self.pay_请求体 is None:
body_inner = ""
elif isinstance(self.pay_请求体, bytes):
body_inner = self.pay_请求体.decode()
else:
raise TypeError(f"不支持的请求体类型: {type(self.pay_请求体)}")
pay_signaturestring创建成功的签名串 = f"{self.pay_methon方法}\n{url_data}\n{utc_unix_timestamp}\n{random_str}\n{body_inner}\n"
# 构造一个哈希对象
hash_obj = SHA256.new(pay_signaturestring创建成功的签名串.encode(encoding="utf-8"))
# 加载商户私钥
# 调用了 RSA.import_key() 方法,它会从字符串或文件中导入一个 PEM 格式的 RSA 私钥,并返回一个 RsaKey 类型的对象
private_key: RsaKey = RSA.import_key(self.merchant_private_key_pem) # 用私钥来加密
# 生成签名 使用商户的 私钥进行签名
signature = pkcs1_15.new(rsa_key=private_key).sign(msg_hash=hash_obj)
# 对签名进行Base64编码,符合微信支付API要求
encoded_signature = base64.b64encode(s=signature).decode(encoding="utf-8")
# 组装成微信官方需要的数据;
data = {
"mchid": self.mchid,
"nonce_str": random_str,
"serial_no": self.serial_no商户证书的序列号,
"timestamp": utc_unix_timestamp,
"signature": encoded_signature,
} # 组装好的字典数据;
ftr = "WECHATPAY2-SHA256-RSA2048"
s_with_space = ftr.ljust(len(ftr) + 1) # 将字符串长度 +1(填充空格)
str_list = pydash.join(array=[f'{key}="{value}"' for key, value in data.items()], separator=",")
# 生成请求头
Authorization = f"{s_with_space}{str_list}"
headers = {"Content-Type": "application/json", "Authorization": Authorization, }
headers.update({"Wechatpay-Serial": self.wechatpay_public_key_id})
self.pay_Headers = headers
return None
2026-08-06 20:39:45 +08:00
def 验证签名串(self, response_headers, response_data: dict, ):
2026-08-21 23:48:29 +08:00
""" 注释 """
public_key = RSA.import_key(self.wechatpay_public_key_pem) # 在这里需要使用最新的微信支付公钥证书
2026-08-06 20:39:45 +08:00
Wechatpay_Timestamp = pydash.get(response_headers, "Wechatpay-Timestamp")
Wechatpay_Nonce = pydash.get(response_headers, "Wechatpay-Nonce")
Wechatpay_Signa = pydash.get(response_headers, "Wechatpay-Signature")
2026-08-21 23:48:29 +08:00
Wechatpay_Serial = pydash.get(response_headers, "Wechatpay-Serial")
2026-08-06 20:39:45 +08:00
response_data_str = orjson.dumps(response_data, ).decode(encoding="utf-8")
2026-08-21 23:48:29 +08:00
2026-08-06 20:39:45 +08:00
string_to_verify = f"{Wechatpay_Timestamp}\n{Wechatpay_Nonce}\n{response_data_str}\n" # 构造验签名串
hash_obj = SHA256.new(string_to_verify.encode())
2026-08-21 23:48:29 +08:00
if Wechatpay_Serial == self.platform_cert_pem_id:
public_key = RSA.import_key(self.platform_cert_pem)
# 6. 验证签名
if Wechatpay_Serial == self.wechatpay_public_key_id:
public_key = RSA.import_key(self.wechatpay_public_key_pem) # 在这里需要使用最新的微信支付公钥证书
2026-08-06 20:39:45 +08:00
try:
pkcs1_15.new(public_key).verify(msg_hash=hash_obj, signature=base64.b64decode(Wechatpay_Signa))
return True # 签名验证成功
except (ValueError, TypeError) as e:
2026-08-21 23:48:29 +08:00
print(e, "捕获了签名串验证不通过的错误") # 签名验证失败
raise "捕获了签名串验证不通过的错误"
2026-08-06 20:39:45 +08:00
2026-09-19 11:51:34 +08:00
def getHeaders构造签名串(self, ):
2026-08-21 23:48:29 +08:00
"""注释"""
2026-08-06 20:39:45 +08:00
url_data = re.search(r'/v3.*', string=self.pay_uri).group()
utc_unix_timestamp = int(datetime.now(tz=ZoneInfo("utc")).timestamp())
random_str = secrets.token_hex(nbytes=16) # 随机字符串
2026-08-21 23:48:29 +08:00
# 统一处理请求体,得到 body_inner 字符串
2026-08-06 20:39:45 +08:00
if isinstance(self.pay_请求体, str):
2026-08-21 23:48:29 +08:00
body_inner = self.pay_请求体
elif isinstance(self.pay_请求体, dict):
body_inner = json.dumps(self.pay_请求体)
elif self.pay_请求体 is None:
2026-08-06 20:39:45 +08:00
body_inner = ""
2026-08-21 23:48:29 +08:00
elif isinstance(self.pay_请求体, bytes):
2026-08-06 20:39:45 +08:00
body_inner = self.pay_请求体.decode()
2026-08-21 23:48:29 +08:00
else:
raise TypeError(f"不支持的请求体类型: {type(self.pay_请求体)}")
pay_signaturestring创建成功的签名串 = f"{self.pay_methon方法}\n{url_data}\n{utc_unix_timestamp}\n{random_str}\n{body_inner}\n"
2026-08-06 20:39:45 +08:00
# 构造一个哈希对象
hash_obj = SHA256.new(pay_signaturestring创建成功的签名串.encode(encoding="utf-8"))
# 加载商户私钥
# 调用了 RSA.import_key() 方法,它会从字符串或文件中导入一个 PEM 格式的 RSA 私钥,并返回一个 RsaKey 类型的对象
private_key: RsaKey = RSA.import_key(self.merchant_private_key_pem) # 用私钥来加密
# 生成签名 使用商户的 私钥进行签名
signature = pkcs1_15.new(rsa_key=private_key).sign(msg_hash=hash_obj)
# 对签名进行Base64编码,符合微信支付API要求
encoded_signature = base64.b64encode(s=signature).decode(encoding="utf-8")
# 组装成微信官方需要的数据;
data = {
"mchid": self.mchid,
"nonce_str": random_str,
"serial_no": self.serial_no商户证书的序列号,
"timestamp": utc_unix_timestamp,
"signature": encoded_signature,
} # 组装好的字典数据;
ftr = "WECHATPAY2-SHA256-RSA2048"
s_with_space = ftr.ljust(len(ftr) + 1) # 将字符串长度 +1(填充空格)
str_list = pydash.join(array=[f'{key}="{value}"' for key, value in data.items()], separator=",")
# 生成请求头
Authorization = f"{s_with_space}{str_list}"
headers = {"Content-Type": "application/json", "Authorization": Authorization, }
# headers.update({"Wechatpay-Serial": self.payment.微信支付公钥ID})
return headers
def 返回前端支付数据(self, prepay_id: str, wechat_appid: str):
"""
小程序调起支付
通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付。
@param prepay_ids
@type prepay_id:
@return:
@rtype:
"""
utc_unix_timestamp = int(datetime.now(tz=ZoneInfo("utc")).timestamp())
random_string = secrets.token_hex(nbytes=30) # 随机字符串
# 组装微信官方要求的格式;
qian_mian_chuan = f"{wechat_appid}\n{utc_unix_timestamp}\n{random_string}\n{prepay_id}\n"
# # 使用组装好地签名串,进行sha256 哈希生成哈希对象
hash_obj = SHA256.new(qian_mian_chuan.encode())
# 生成商户私钥对象
# private_key = RSA.import_key( wechat_config.wechat私钥 )
private_key = RSA.import_key(self.merchant_private_key_pem)
# 生成签名
signature = pkcs1_15.new(private_key).sign(hash_obj)
# 将生成的签名串,转换为base格式
encoded_signature = base64.b64encode(signature).decode()
return {
"appId": self.appid,
"timeStamp": str(utc_unix_timestamp),
"nonceStr": random_string,
"package": prepay_id,
"signType": "RSA",
"paySign": encoded_signature,
}
def getpay_jsapi发起支付(self, ):
# 发送请求
2026-08-21 23:48:29 +08:00
response: Response = requests.post(url=self.pay_uri, json=self.pay_请求体, headers=self.pay_Headers)
2026-08-06 20:39:45 +08:00
prepay_id = pydash.get(obj=response.json(), path="prepay_id", default=None)
# 验证请求后返回的数据是否为微信官方所发送;
验证签名结果 = self.验证签名串(response_data=response.json(), response_headers=response.headers)
if 验证签名结果:
prepay_id = f"prepay_id={prepay_id}"
# 调用方法生成返回给前端的对象
return self.返回前端支付数据(prepay_id=prepay_id, wechat_appid=self.appid, ) # 调用 返回前端支付数据()方法生成返回前端的支付字典
else:
raise "验证不通过"
def modegmc解密(self, nonce: str, ciphertext: str, associated_data: str, ) -> dict:
"""
支付成功或者退款回调时需要解密参数,微信返回的响应体.解密后可以获取支付成功或者退款的详细的数据;
@param nonce: // 加密使用的随机串初始化向量
@type nonce:
@param ciphertext: // Base64编码后的密文(需要解密)
@type ciphertext: (需要解密)
@param associated_data: // 附加数据包(可能为空)
@type associated_data:
@return:
@rtype:
"""
nonce_bytes = nonce.encode(encoding="utf-8")
ad_bytes = associated_data.encode(encoding="utf-8")
encrypted_data = base64.b64decode(ciphertext)
cipher = AES.new(key=self.api_v3_key.encode(), mode=AES.MODE_GCM, nonce=nonce_bytes)
# 处理附加关联数据(AAD) - 微信支付中通常是"transaction"
# 这部分数据在加密过程中被验证但未被加密
# 必须与加密时使用的附加数据完全一致
cipher.update(assoc_data=ad_bytes)
auth_tag = encrypted_data[-16:] # 提取认证标签(最后16 字节)
# 提取实际加密内容
ciphertext_bytes = encrypted_data[:-16] # 提取实际加密内容
decrypted_data = cipher.decrypt_and_verify(ciphertext=ciphertext_bytes, received_mac_tag=auth_tag)
# 回调解密数据: dict = json.loads(decrypted_data.decode())
回调解密数据: dict = orjson.loads(decrypted_data.decode(encoding="utf-8"))
return 回调解密数据
def callBack回调解密(self, headers, response_body: dict) -> dict:
"""
执行微信支付回调方法(获取返回的参数数据)
@param request:
@type request:
"""
self.验证签名串(response_headers=headers, response_data=response_body)
ciphertext = pydash.get(obj=response_body, path="resource.ciphertext", default=None)
associated_data = pydash.get(obj=response_body, path="resource.associated_data", default=None)
nonce = pydash.get(obj=response_body, path="resource.nonce", default=None)
wei_xin_zhi_fu_hui_diao_data = self.modegmc解密(
2026-09-19 11:51:34 +08:00
nonce=nonce, ciphertext=ciphertext, associated_data=associated_data,
2026-08-06 20:39:45 +08:00
)
return wei_xin_zhi_fu_hui_diao_data
def getpay_navite发起二维码支付(self, ):
""" 二维码支付,可以适用于 网站 的用户扫码支付 """
2026-09-19 11:51:34 +08:00
# headers = self.getHeaders构造签名串()
response = requests.post(url=self.pay_uri, headers=self.pay_Headers, json=self.pay_请求体)
2026-08-06 20:39:45 +08:00
生成的支付链接 = response.json()
return 生成的支付链接
2026-08-21 23:48:29 +08:00
def v2签名返回XML(self, sign_data):
2026-08-06 20:39:45 +08:00
"""v2版本的签名:将扫码支付的body转换为微信支付所要求的数据类型"""
2026-08-21 23:48:29 +08:00
sorted_dict = dict(pydash.sort_by(collection=sign_data.items(), iteratee=0))
2026-08-06 20:39:45 +08:00
# 拼接字符串: (将密钥添加到最末尾)
params_list_str = pydash.join(array=[f"{k}={v}" for k, v in sorted_dict.items()], separator="&") + "&key=" + self.api_v2_key
# 得到params_str就是需要 参与 签名的字符串
signature = hashlib.md5(string=params_list_str.encode()).hexdigest().upper() # 使用MD5加密模式
2026-08-21 23:48:29 +08:00
update后的数据 = pydash.set_(obj=sign_data, path="sign", value=signature)
2026-08-06 20:39:45 +08:00
xml = dicttoxml(obj=update后的数据, custom_root="xml", xml_declaration=False, attr_type=False, cdata=True)
2026-08-21 23:48:29 +08:00
return xml
def getpay_code扫用户付款码(self, body_data, ) -> dict:
""" """
xml = self.v2签名返回XML(sign_data=body_data)
fu_kuan_url = "https://api.mch.weixin.qq.com/pay/micropay"
res = requests.post(url=fu_kuan_url, data=xml)
# 返回的数据 是 ISO-8859-1 类型
responseStr = res.text.encode("ISO-8859-1")
# 将请求得到的数据 转换为字典格式;
xml_dict = xmltodict.parse(responseStr.decode())
# 得到请求结果并进行解密
print(xml_dict)
return xml_dict
2026-08-06 20:39:45 +08:00
def 解密群号(self, encryptedData, iv, sessionKey):
"""
"""
try:
session_key_bytes = base64.b64decode(sessionKey)
encrypted_data_bytes = base64.b64decode(encryptedData)
iv_bytes = base64.b64decode(iv)
cipher = AES.new(key=session_key_bytes, mode=AES.MODE_CBC, iv=iv_bytes)
# 解密并去除填充
decrypted_data = unpad(cipher.decrypt(encrypted_data_bytes), AES.block_size).decode()
data_dict = orjson.loads(decrypted_data)
openGId = pydash.get(obj=data_dict, path="openGId")
timestamp = pydash.get(obj=data_dict, path="watermark.timestamp")
appid = pydash.get(obj=data_dict, path="watermark.appid")
return openGId, timestamp, appid
except Exception as e:
raise Exception(f"解密失败: {str(e)}")
def 发起退款(self, ):
pass
uri = f"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"
2026-09-19 11:51:34 +08:00
heardes = self.getHeaders构造签名串()
2026-08-06 20:39:45 +08:00
print(heardes)
response_data = requests.post(url=self.pay_uri, json=self.pay_请求体, headers=heardes)
print(response_data.json())
2026-09-19 11:51:34 +08:00
def getpay_发起提现(self):
pass
response: Response = requests.post(url=self.pay_uri, json=self.pay_请求体, headers=self.pay_Headers)
return response.json()
2026-08-06 20:39:45 +08:00
if __name__ == "__main__":
pass