Files
Django/jituan/services/club_cert_upload.py
XingQue 8e82a8457d feat: 收款与提现商户拆分,支持后台上传证书
收款仍用 mch_id/mch_key;提现优先 withdraw_mch_id(空则回落);证书分 pay/withdraw 目录落盘。退款逻辑未改。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 03:52:55 +08:00

121 lines
4.3 KiB
Python
Raw Permalink 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.
"""俱乐部微信商户证书落盘(收款 / 提现分目录)。"""
import logging
import os
import re
from django.conf import settings
logger = logging.getLogger(__name__)
# pay = 点单收款商户withdraw = 提现转账商户
CERT_KINDS = {
'pay_key': ('pay', 'apiclient_key.pem', 'pay_key_path'),
'pay_cert': ('pay', 'apiclient_cert.pem', 'pay_cert_path'),
'withdraw_key': ('withdraw', 'apiclient_key.pem', 'private_key_path'),
'withdraw_cert': ('withdraw', 'apiclient_cert.pem', None), # 与私钥同目录,不单独存字段
'withdraw_platform': ('withdraw', 'pub_key.pem', 'platform_cert_dir'), # 目录字段指向所在目录
}
_SAFE_CLUB = re.compile(r'^[a-zA-Z0-9_-]{1,16}$')
def club_wx_cert_root():
"""
证书根目录。优先 settings.CLUB_WX_CERT_ROOT
否则尝试沿用现网 secrets 父目录下的 wx_certs最后 BASE_DIR/wx_certs。
"""
configured = (getattr(settings, 'CLUB_WX_CERT_ROOT', None) or '').strip()
if configured:
return configured
# 兼容现网:.../sites/xxx/tuikuanzhengshu → .../sites/xxx/wx_certs
for attr in ('WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH'):
p = (getattr(settings, attr, None) or '').strip()
if p:
parent = os.path.dirname(os.path.dirname(os.path.abspath(p)))
if parent and parent not in ('.', os.sep):
return os.path.join(parent, 'wx_certs')
v3 = getattr(settings, 'WECHAT_PAY_V3_CONFIG', None) or {}
pk = (v3.get('PRIVATE_KEY_PATH') or '').strip()
if pk:
parent = os.path.dirname(os.path.dirname(os.path.abspath(pk)))
if parent and parent not in ('.', os.sep):
return os.path.join(parent, 'wx_certs')
return os.path.join(str(settings.BASE_DIR), 'wx_certs')
def club_cert_dir(club_id, bucket):
cid = (club_id or '').strip()
if not _SAFE_CLUB.match(cid):
raise ValueError('非法 club_id')
if bucket not in ('pay', 'withdraw'):
raise ValueError('非法证书分类')
path = os.path.join(club_wx_cert_root(), cid, bucket)
os.makedirs(path, mode=0o750, exist_ok=True)
return path
def _validate_pem(raw: bytes, expect_private: bool):
if not raw or len(raw) > 64 * 1024:
raise ValueError('文件过大或为空')
text = raw.decode('utf-8', errors='ignore')
if 'BEGIN' not in text or 'END' not in text:
raise ValueError('不是有效的 PEM 文件')
has_private = 'PRIVATE KEY' in text
if expect_private and not has_private:
raise ValueError('需要私钥 PEM含 PRIVATE KEY')
if not expect_private and has_private:
raise ValueError('请上传证书/公钥 PEM不要上传私钥')
return raw
def save_club_cert_file(club, kind, uploaded_file):
"""
保存上传的证书文件并回写 Club 字段。
kind: pay_key | pay_cert | withdraw_key | withdraw_cert | withdraw_platform
返回 {'path': ..., 'field': ..., 'kind': ...}
"""
if kind not in CERT_KINDS:
raise ValueError(f'不支持的文件类型: {kind}')
bucket, filename, field_name = CERT_KINDS[kind]
expect_private = kind in ('pay_key', 'withdraw_key')
raw = uploaded_file.read()
raw = _validate_pem(raw, expect_private=expect_private)
directory = club_cert_dir(club.club_id, bucket)
dest = os.path.join(directory, filename)
# 原子写入
tmp = dest + '.tmp'
with open(tmp, 'wb') as f:
f.write(raw)
os.chmod(tmp, 0o640)
os.replace(tmp, dest)
update_fields = []
if field_name == 'platform_cert_dir':
# 平台证书:字段存目录
setattr(club, field_name, directory)
update_fields.append(field_name)
saved_path = directory
elif field_name:
setattr(club, field_name, dest)
update_fields.append(field_name)
saved_path = dest
else:
# withdraw_cert只落盘序列号由私钥配对自动解析
saved_path = dest
if update_fields:
club.save(update_fields=update_fields)
logger.warning(
'[CLUB_CERT_UPLOAD] club=%s kind=%s path=%s field=%s',
club.club_id, kind, saved_path, field_name or '(none)',
)
return {
'kind': kind,
'path': saved_path,
'field': field_name,
'bucket': bucket,
'filename': filename,
}