Files
sixGuoDjango/wechat_项目工具包/微信支付工具包/微信支付类.py
T
2026-09-19 11:51:34 +08:00

441 lines
22 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.
"""
Author: xiao 792282@qq.com
Date: 2025-08-10 11:36:05
LastEditors: xiao 792282@qq.com
LastEditTime: 2025-08-17 13:38:39
FilePath: django_微信支付/支付工具/微信支付类.py
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
"""
import os
import django
from sixGuoDjango.settings import base_dir_name
from wechat_微信支付证书.江苏六果科技服务商支付证书.江苏六果科技有限公司服务商支付参数 import six六果服务商支付参数
os.environ.setdefault(key="DJANGO_SETTINGS_MODULE", value=f"{base_dir_name}.settings")
django.setup()
from pathlib import Path
import requests
from wechat_微信支付证书.江苏六果科技有限公司支付证书.江苏六果科技有限公司支付参数 import sixpay六果科技公司
import orjson
from Crypto.Util.Padding import unpad
from django.conf import settings
"""微信支付清爽版本"""
import base64
import hashlib
import json
import re
import secrets
import time
from datetime import datetime
from typing import Optional, Literal
import pydash
import xmltodict
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Hash import SHA1, SHA256
from Crypto.PublicKey import RSA
from Crypto.PublicKey.RSA import RsaKey
from Crypto.Signature import pkcs1_15
from dicttoxml import dicttoxml
from pydantic import BaseModel
from requests import Response, post, get
class paymentCategory(sixpay六果科技公司):
"""1"""
pass
class wechatPay(BaseModel):
payment: type[sixpay六果科技公司] | type[six六果服务商支付参数]
def 构造签名串(self, uri: str, body_data: dict | bytes | str | None = None, method: Optional[Literal["GET", "POST", "PATCH"]] = "POST", ):
"""
"""
url_data = re.search(r'/v3.*', string=uri).group()
shi_jian_chou = str(int(time.time())) # 时间戳
random_str = secrets.token_hex(nbytes=16) # 随机字符串
# 创建签名请求参数
signaturestring: str | None = None
if isinstance(body_data, str):
signaturestring = f"{method}\n{url_data}\n{shi_jian_chou}\n{random_str}\n{body_data}\n"
if isinstance(body_data, dict):
body_inner = json.dumps(obj=body_data)
signaturestring = f"{method}\n{url_data}\n{shi_jian_chou}\n{random_str}\n{body_inner}\n"
if body_data is None:
body_inner = ""
signaturestring = f"{method}\n{url_data}\n{shi_jian_chou}\n{random_str}\n{body_inner}\n"
if isinstance(body_data, bytes):
body_inner = body_data.decode()
signaturestring = f"{method}\n{url_data}\n{shi_jian_chou}\n{random_str}\n{body_inner}\n"
# 构造一个哈希对象
hash_obj = SHA256.new(signaturestring.encode(encoding="utf-8"))
# 加载商户私钥
# 调用了 RSA.import_key() 方法,它会从字符串或文件中导入一个 PEM 格式的 RSA 私钥,并返回一个 RsaKey 类型的对象
private_key: RsaKey = RSA.import_key(self.payment.private_key)
# 生成签名 使用商户的 私钥进行签名
signature = pkcs1_15.new(rsa_key=private_key).sign(msg_hash=hash_obj)
# 对签名进行Base64编码,符合微信支付API要求
encoded_signature = base64.b64encode(s=signature).decode()
# 组装成微信官方需要的数据;
data = {
"mchid": self.payment.mchid,
"nonce_str": random_str,
"serial_no": self.payment.serial_no,
"timestamp": shi_jian_chou,
"signature": encoded_signature,
} # 组装好的字典数据;
ftr = "WECHATPAY2-SHA256-RSA2048"
s_with_space = ftr.ljust(len(ftr) + 1) # 将字符串长度 +1(填充空格)
str_list = ",".join([f'{key}="{value}"' for key, value in data.items()])
# 生成请求头
Authorization = f"{s_with_space}{str_list}"
headers = {"Content-Type": "application/json", "Authorization": Authorization, }
# headers.update({"Wechatpay-Serial": self.payment.微信支付公钥ID})
return headers
def getpay_jsapi(self, uri, body_data: dict | bytes | str | None = None, method: Optional[
Literal["GET", "POST"]] = "POST",
):
headers = self.构造签名串(uri=uri, body_data=body_data, method=method)
# 发送请求
response: Response = requests.post(url=uri, json=body_data, headers=headers)
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.payment.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:
"""
key_bytes = self.payment.apiv3.encode()
nonce_bytes = nonce.encode(encoding="utf-8")
ad_bytes = associated_data.encode(encoding="utf-8")
encrypted_data = base64.b64decode(ciphertext)
cipher = AES.new(key=key_bytes, 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 modegmc解密电子发票邮箱账号(self, ciphertext):
pass
encrypted_bytes = base64.b64decode(ciphertext)
# 3. 构建 OAEP 解密器(SHA-1)
private_key: RsaKey = RSA.import_key(self.payment.private_key)
cipher = PKCS1_OAEP.new(key=private_key, hashAlgo=SHA1)
plaintext_bytes = cipher.decrypt(encrypted_bytes)
return plaintext_bytes.decode("utf-8")
def getpay_navite(self, navite_body: dict, ):
"""
@param navite_body:
@type navite_body:
@return:
@rtype:
"""
native_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native"
headers = self.构造签名串(body_data=navite_body, uri=native_url, method="POST")
response = post(url=native_url, headers=headers, json=navite_body)
生成的支付链接 = response.json()
print(生成的支付链接)
return 生成的支付链接
def callBack回调解密(self, headers, response_body: dict) -> dict:
"""
执行微信支付回调方法(获取返回的参数数据)
@param request:
@type request:
"""
# response_data = response_body.decode()
# 调用验证签名串方法,(必需要验证确保是微信官方的回调数据;)
验证签名串结果数据 = self.验证签名串(response_headers=headers, response_data=response_body)
print("🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏🍎 🍏", 验证签名串结果数据)
# 验证成功后;拿到返回的json数据
# res_data = json.loads(s=response_data) # TODO 一定要序列化.
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解密(
nonce=nonce, ciphertext=ciphertext,
associated_data=associated_data,
)
return wei_xin_zhi_fu_hui_diao_data
def 返回前端支付数据(self, prepay_id: str, wechat_appid: str):
"""
小程序调起支付
通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付。
@param prepay_ids
@type prepay_id:
@return:
@rtype:
"""
shi_jian_chou = str(int(time.time())) # 生在时间戳
random_string = secrets.token_hex(nbytes=30) # 随机字符串
# 组装微信官方要求的格式;
qian_mian_chuan = f"{wechat_appid}\n{shi_jian_chou}\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.payment.private_key)
# 生成签名
signature = pkcs1_15.new(private_key).sign(hash_obj)
# 将生成的签名串,转换为base格式
encoded_signature = base64.b64encode(signature).decode()
data_obj = {
"appId": self.payment.appid,
"timeStamp": shi_jian_chou,
"nonceStr": random_string,
"package": prepay_id,
"signType": "RSA",
"paySign": encoded_signature,
}
return {
"response": data_obj,
}
def transferAccounts商家转账到零钱(self, transferAccountsBody):
"""
向微信用户发起转账接口;(实测已经领到钱了)前端直接调用 wx就可以,(目前没有解决ts报错的 问题)
"""
# uri = "https://api.mch.weixin.qq.com/v3/transfer/batches" # (旧接口)
uri = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills"
# transferAccountsBody = {
# "appid": self.payment.appid, "out_bill_no": order_random, # 必填 integer【商户单号】
# # 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
# "transfer_scene_id": "1000",
# # 必填 integer【转账场景ID】 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1000(现金营销),1006(企业报销)等
# # "openid":"ohQkD7A-N93kljcFmkCyG6e_D9vg",
# "openid": mini_openid, # 必填 integer【收款用户OpenID】 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见参数说明。
# # "user_name":"757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45", # 【收款用户姓名】
# # 收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。
# "transfer_amount": transfer_amount, # 必填 integer【转账金额】 转账金额单位为“分”。
# "transfer_remark": "新会员开通有礼", # 必填 string(32)【转账备注】 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
# "notify_url": "https://www.sixgou.site", # 选填【通知地址】 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数。
# "user_recv_perception": "现金奖励", # 选填 【用户收款感知】 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,
# # 必填 array[TransferSceneReportInfo] 【转账场景报备信息】 各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见转账场景报备信息字段说明。
# "transfer_scene_report_infos": [
# {
# "info_type": "活动名称", # 必填 string(15)【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值
# "info_content": "新会员有礼", # 必填 【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参
# }, {
# "info_type": "奖励说明", "info_content": "注册会员抽奖一等奖",
# },
# ],
# }
headers = self.构造签名串(uri=uri, body_data=transferAccountsBody, method="POST")
data = post(url=uri, json=transferAccountsBody, headers=headers)
package = data.json()
package_info = pydash.get(obj=package, path="package_info")
前端调用的数据 = {
"mchId": self.payment.mchid, "appId": self.payment.appid, "package": package_info,
}
# return 前端调用的数据
return package
"""
前端调用的示例
const demo_fun = () => {
wx.requestMerchantTransfer({
// 你的参数
mchId: '1700383419',
appId: "wxf612e825377328f0",
package: "ABBQO+oYAAABAAAAAAB99dFZt82j95u0HhbyZxAAAADnGpepZahT9IkJjn90
+1qgGS6abB6UNQ9r1BZy26rL41bBjlwNMlDnYxPA/nGPWXtoOeFekxLE3iayC1CrxUngRWxwI/X79IcQ1+mBcUFwAUn3+f8=",
success: (res: any) => {
// res.err_msg将在页面展示成功后返回应用时返回ok,并不代表付款成功
console.log('success:', res);
},
});
}
"""
def transferAccounts商家转账查询(self, transfer_bill_no: str):
"""
:param transfer_bill_no:
:type transfer_bill_no:
"""
uri = (f"https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/"
f"{transfer_bill_no}")
header = self.构造签名串(method="GET", uri=uri)
data = get(url=uri, headers=header)
print(data.json())
def 验证签名串(self, response_headers, response_data: dict, ):
"""
@param response_headers:
@type response_headers:
@param response_data:
@type response_data:
@return:
@rtype:
"""
# public_key = RSA.import_key(self.payment.wechat_certificate)
public_key = RSA.import_key(self.payment.pem微信支付公钥)
Wechatpay_Timestamp = pydash.get(response_headers, "Wechatpay-Timestamp")
Wechatpay_Nonce = pydash.get(response_headers, "Wechatpay-Nonce")
Wechatpay_Signa = pydash.get(response_headers, "Wechatpay-Signature")
response_data_str = orjson.dumps(response_data, ).decode(encoding="utf-8")
string_to_verify = f"{Wechatpay_Timestamp}\n{Wechatpay_Nonce}\n{response_data_str}\n" # 构造验签名串
hash_obj = SHA256.new(string_to_verify.encode())
# 6. 验证签名
try:
pkcs1_15.new(public_key).verify(msg_hash=hash_obj, signature=base64.b64decode(Wechatpay_Signa))
return True # 签名验证成功
except (ValueError, TypeError) as e:
raise e # 签名验证失败
def v2签名返回XML(self, sign_data: dict):
"""v2版本的签名:将扫码支付的body转换为微信支付所要求的数据类型"""
sorted_dict = dict(pydash.sort_by(collection=sign_data.items(), iteratee=0))
# 拼接字符串: (将密钥添加到最末尾)
params_str = "&".join(f"{k}={v}" for k, v in sorted_dict.items()) + '&key=' + self.payment.apiv2
# 得到params_str就是需要 参与 签名的字符串
signature = hashlib.md5(string=params_str.encode()).hexdigest().upper() # 使用MD5加密模式
sign_data.update(sign=signature) # 将sign(签名的数据添加到dict数据中)
xml = dicttoxml(obj=sign_data, custom_root="xml", xml_declaration=False, attr_type=False, cdata=True)
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 = 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
def 获取平台证书(self, ):
"""
@param save_path:
@type save_path:
@return:
@rtype:
"""
# save_path为首次保存存商户平台证书的路径(首次使用小程序需要获取这个证书,)就是调用本类方法就可以获取
certificate_url = "https://api.mch.weixin.qq.com/v3/certificates"
headers = self.构造签名串(uri=certificate_url, method="GET")
print(headers)
res = get(url=certificate_url, headers=headers, )
data = json.loads(res.text)
print(data)
ciphertext: str = pydash.get(obj=data, path="data[0].encrypt_certificate.ciphertext")
nonce: str = pydash.get(obj=data, path="data[0].encrypt_certificate.nonce")
associated_data: str = pydash.get(obj=data, path="data[0].encrypt_certificate.associated_data")
ciphertext_data = base64.b64decode(ciphertext.encode())
# 调用后得到证书信息,需要保存起来
cipher = AES.new(key=self.payment.apiv3.encode(), mode=AES.MODE_GCM, nonce=nonce.encode())
# 处理附加关联数据(AAD) - 微信支付中通常是"transaction"
# 这部分数据在加密过程中被验证但未被加密
# 必须与加密时使用的附加数据完全一致
cipher.update(associated_data.encode())
auth_tag = ciphertext_data[-16:] # 提取认证标签(最后16 字节)
# 提取实际加密内容
ciphertext_bytes = ciphertext_data[:-16] # 提取实际加密内容
decrypted_data = cipher.decrypt_and_verify(ciphertext=ciphertext_bytes, received_mac_tag=auth_tag)
print(decrypted_data.decode(), "decrypted_data=====================")
save_path = Path(settings.BASE_DIR).joinpath(
"wechat_微信支付证书/江苏六果科技有限公司支付证书/sixgou_ping_tai_zheng_shu_cert.pem",
).write_text(decrypted_data.decode())
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 fai发放代金券(self, openid):
"""
:param openid:
:return:
"""
pass
date = datetime.now().strftime("%Y%m%d")
random_str = secrets.token_hex(6)
example_out_request_no = f"{paymentCategory.mchid}{date}{random_str}"
post_data = {
"stock_id": "21056560", # 【创建批次的商户号】批次创建方商户号。校验规则:接口传入的批次号需由stock_creator_mchid所创建
"out_request_no": example_out_request_no, # 【商户单据号】商户此次发放凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性
"appid": paymentCategory.appid,
"stock_creator_mchid": paymentCategory.mchid, # 【创建批次的商户号】批次创建方商户号。
# "coupon_value": 300, # 【指定面额发券,面额】指定面额发券场景,券面额,其他场景不需要填,单位:分。
# "coupon_minimum": 300, # 【指定面额发券,券门槛】指定面额发券批次门槛,其他场景不需要,单位:分
}
uri = f"https://api.mch.weixin.qq.com/v3/marketing/favor/users/{openid}/coupons"
headers = self.构造签名串(uri=uri, body_data=post_data, method="POST")
result_data = post(url=uri, json=post_data, headers=headers)
print(result_data.json())
return result_data.json()
def 发起退款(self, body):
pass
uri = f"https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"
heardes = self.构造签名串(body_data=body, uri=uri, method="POST")
print(heardes)
response_data = requests.post(url=uri, json=body, headers=heardes)
print(response_data.json())
if __name__ == '__main__':
pass
# wechat_data = wechatPay(payment=paymentCategory).获取平台证书()
wechat_data = wechatPay(payment=paymentCategory).fai发放代金券(openid="o6oV61xoMYAFNpi4RpVjn3BFGB1U")
# 方法已经封装了请求结果,直接获得就是请求的结果 url (支付的url) 再由前端生成二维码
# wechat_data = wechatPay(payment=sixpay六果科技公司).navite用户扫生成的二维码(navite_body=native_body)
# headers = wechatPay(payment=sixpay六果科技公司).构造签名串(uri=native_url, method="POST", body_data=native_body)
# print(headers)