支付与收款商户号分离:支付改读 pay_* 独立配置与证书目录,收款侧不动。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-27 05:41:56 +08:00
parent 89fa08e5de
commit db91626e7f
12 changed files with 531 additions and 78 deletions

169
peizhi/pay_mch_views.py Normal file
View File

@@ -0,0 +1,169 @@
"""后台 — 支付专用商户配置(与转账收款 wxmch* 完全分离,权限 8080a"""
import logging
import os
import re
from django.db import transaction
from rest_framework import permissions
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from peizhi.models import XcxSysPeizhi
from peizhi.xcx_config_views import _authorize_miniapp_config, _mask_secret
from utils.pay_mch_config import pay_cert_base_dir, pay_cfg
from utils.xcx_sys_config import clear_xcx_sys_peizhi_cache
logger = logging.getLogger(__name__)
_SAFE_NAME = re.compile(r'[^A-Za-z0-9._-]+')
def _get_or_create_row():
row = XcxSysPeizhi.objects.first()
if row is None:
row = XcxSysPeizhi.objects.create()
return row
def _serialize_pay(row, hide_secret=True):
v2 = (row.pay_api_v2_key or '') if row else ''
v3 = (row.pay_api_v3_key or '') if row else ''
cert = (row.pay_cert_path or '') if row else ''
key = (row.pay_key_path or '') if row else ''
return {
'pay_mch_id': (row.pay_mch_id or '') if row else pay_cfg.PAY_MCH_ID,
'pay_api_v2_key': _mask_secret(v2) if hide_secret else v2,
'pay_api_v2_key_set': bool(v2),
'pay_api_v3_key': _mask_secret(v3) if hide_secret else v3,
'pay_api_v3_key_set': bool(v3),
'pay_notify_url': (row.pay_notify_url or '') if row else '',
'pay_cert_path': cert,
'pay_key_path': key,
'pay_cert_serial_no': (row.pay_cert_serial_no or '') if row else '',
'pay_cert_exists': bool(cert and os.path.isfile(cert)),
'pay_key_exists': bool(key and os.path.isfile(key)),
'cert_base_dir': pay_cert_base_dir(),
'effective_mch_id': pay_cfg.PAY_MCH_ID,
}
class PayMchQueryView(APIView):
"""POST /peizhi/paymchhq — 查询支付商户配置"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
row = XcxSysPeizhi.objects.first()
return Response({'code': 0, 'data': _serialize_pay(row)})
class PayMchUpdateView(APIView):
"""POST /peizhi/paymchgx — 更新支付商户配置"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
data = request.data
with transaction.atomic():
row = _get_or_create_row()
if 'pay_mch_id' in data:
row.pay_mch_id = str(data.get('pay_mch_id') or '').strip()[:32]
if 'pay_notify_url' in data:
row.pay_notify_url = str(data.get('pay_notify_url') or '').strip()[:300]
if 'pay_cert_serial_no' in data:
row.pay_cert_serial_no = str(data.get('pay_cert_serial_no') or '').strip()[:128]
if 'pay_cert_path' in data:
row.pay_cert_path = str(data.get('pay_cert_path') or '').strip()[:500]
if 'pay_key_path' in data:
row.pay_key_path = str(data.get('pay_key_path') or '').strip()[:500]
v2 = data.get('pay_api_v2_key')
if v2 is not None and str(v2).strip() and '****' not in str(v2):
row.pay_api_v2_key = str(v2).strip()[:128]
v3 = data.get('pay_api_v3_key')
if v3 is not None and str(v3).strip() and '****' not in str(v3):
row.pay_api_v3_key = str(v3).strip()[:128]
row.save()
clear_xcx_sys_peizhi_cache()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_pay(row)})
class PayMchUploadView(APIView):
"""
POST /peizhi/paymchscwj — 上传支付退款证书/密钥
form-data: username, file_type(cert|key), file
可选 mch_id默认用当前 pay_mch_id
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
ALLOWED_TYPES = {
'cert': 'apiclient_cert.pem',
'key': 'apiclient_key.pem',
}
def post(self, request):
ok, err = _authorize_miniapp_config(request)
if not ok:
return err
file_type = str(request.data.get('file_type') or '').strip()
upload = request.FILES.get('file')
mch_id = str(request.data.get('mch_id') or request.data.get('pay_mch_id') or '').strip()
if file_type not in self.ALLOWED_TYPES:
return Response({'code': 400, 'msg': 'file_type 仅支持 cert / key'})
if not upload:
return Response({'code': 400, 'msg': '请上传文件'})
row = _get_or_create_row()
if not mch_id:
mch_id = (row.pay_mch_id or pay_cfg.PAY_MCH_ID or '').strip()
if not mch_id:
return Response({'code': 400, 'msg': '请先填写支付商户号再上传证书'})
dest_name = self.ALLOWED_TYPES[file_type]
folder = os.path.join(pay_cert_base_dir(), mch_id)
os.makedirs(folder, exist_ok=True)
dest_path = os.path.join(folder, dest_name)
try:
with open(dest_path, 'wb') as f:
for chunk in upload.chunks():
f.write(chunk)
except OSError as e:
logger.error('上传支付证书失败 mch=%s err=%s', mch_id, e, exc_info=True)
return Response({'code': 500, 'msg': f'写入失败: {e}'})
if file_type == 'cert':
row.pay_cert_path = dest_path
row.save(update_fields=['pay_cert_path', 'update_time'])
else:
row.pay_key_path = dest_path
row.save(update_fields=['pay_key_path', 'update_time'])
if not (row.pay_mch_id or '').strip():
row.pay_mch_id = mch_id
row.save(update_fields=['pay_mch_id', 'update_time'])
clear_xcx_sys_peizhi_cache()
return Response({
'code': 0,
'msg': '上传成功',
'data': {
'mch_id': mch_id,
'file_type': file_type,
'path': dest_path,
**_serialize_pay(row),
},
})