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