170 lines
6.2 KiB
Python
170 lines
6.2 KiB
Python
"""后台 — 支付专用商户配置(与转账收款 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),
|
||
},
|
||
})
|