feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置

This commit is contained in:
XingQue
2026-06-24 05:02:18 +08:00
parent ce9b09f096
commit dcc4936428
82 changed files with 5748 additions and 924 deletions

View File

@@ -3,6 +3,7 @@ import time
import hashlib
import base64
import json
import os
from urllib.parse import urlparse
from cryptography.hazmat.primitives import serialization, hashes
@@ -11,29 +12,33 @@ from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from django.conf import settings
import os
def load_private_key():
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
key_path = settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH']
def _get_v3_config(club_id=None):
from jituan.services.wechat_pay import get_wechat_v3_config
return get_wechat_v3_config(club_id)
def load_private_key(club_id=None):
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
cfg = _get_v3_config(club_id)
key_path = cfg['PRIVATE_KEY_PATH']
with open(key_path, 'rb') as f:
key_data = f.read()
# cryptography 自动识别格式
private_key = serialization.load_pem_private_key(
return serialization.load_pem_private_key(
key_data,
password=None,
backend=default_backend()
backend=default_backend(),
)
return private_key
def build_authorization(method, url, body):
"""生成V3接口的Authorization"""
private_key = load_private_key()
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
def build_authorization(method, url, body, club_id=None):
"""生成 V3 接口的 Authorization 头。"""
cfg = _get_v3_config(club_id)
private_key = load_private_key(club_id)
mchid = cfg['MCHID']
serial_no = cfg['CERT_SERIAL_NO']
parsed = urlparse(url)
path = parsed.path
@@ -43,25 +48,25 @@ def build_authorization(method, url, body):
timestamp = str(int(time.time()))
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
# 构造签名串
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
# 使用 cryptography 签名
signature = private_key.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
hashes.SHA256(),
)
signature_b64 = base64.b64encode(signature).decode('utf-8')
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
return auth
return (
f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",'
f'timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
)
def verify_wechat_sign(headers, body):
"""验证微信回调签名(需提前下载平台证书)"""
# 这部分使用 rsa 库验签,也可以改用 cryptography但 rsa 已足够
def verify_wechat_sign(headers, body, club_id=None):
"""验证微信回调签名(需提前下载平台证书)"""
import rsa
serial = headers.get('Wechatpay-Serial')
signature = headers.get('Wechatpay-Signature')
timestamp = headers.get('Wechatpay-Timestamp')
@@ -75,13 +80,14 @@ def verify_wechat_sign(headers, body):
try:
ts = int(headers.get('Wechatpay-Timestamp', '0'))
if abs(time.time() - ts) > 300: # 5分钟有效期
if abs(time.time() - ts) > 300:
return False
except (ValueError, TypeError):
return False
cfg = _get_v3_config(club_id)
message = '\n'.join([timestamp, nonce, body]) + '\n'
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
cert_path = f"{cfg['PLATFORM_CERT_DIR']}/{serial}.pem"
if not os.path.exists(cert_path):
return False
@@ -96,9 +102,27 @@ def verify_wechat_sign(headers, body):
return False
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
"""解密回调数据(使用 AES-GCM"""
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
def resolve_callback_club_id(headers, body):
"""
多商户回调:依次用各俱乐部平台证书验签,返回匹配的 club_id。
验签失败返回 None。
"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
if verify_wechat_sign(headers, body, club_id=CLUB_ID_DEFAULT):
return CLUB_ID_DEFAULT
for club in Club.query.filter(status=1).exclude(club_id=CLUB_ID_DEFAULT):
if verify_wechat_sign(headers, body, club_id=club.club_id):
return club.club_id
return None
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=None):
"""解密回调数据(使用 AES-GCM"""
cfg = _get_v3_config(club_id)
api_v3_key = cfg['API_V3_KEY'].encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)