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

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

No files matched your search

+3
View File
@@ -667,5 +667,8 @@ WECHAT_PAY_V3_CONFIG = {
# 多商户证书上传根目录(每个商户一个子目录:{BASE}/{mch_id}/)
WECHAT_MCH_CERT_BASE_DIR = '/opt/1panel/apps/openresty/openresty/www/sites/119.28.221.247.1199/huizhifuzhengshu'
# 支付专用证书目录(与收款 huizhifuzhengshu、旧 tuikuanzhengshu 物理隔离)
WECHAT_PAY_CERT_BASE_DIR = '/opt/1panel/apps/openresty/openresty/www/sites/119.28.221.247.1199/zhifuzhengshu'
CROSS_PLATFORM_SYNC_TOKEN = 'xK9mP2vL8qW5rT7y'
@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dingdan', '0017_dingdan_guanshi_shangjia_fencheng'),
]
operations = [
migrations.AddField(
model_name='dingdan',
name='pay_mch_id',
field=models.CharField(
blank=True,
db_index=True,
default='',
max_length=32,
verbose_name='下单支付商户号',
),
),
]
+7
View File
@@ -64,6 +64,13 @@ class Dingdan(models.Model):
db_index=True,
verbose_name='微信支付订单号'
)
pay_mch_id = models.CharField(
max_length=32,
blank=True,
default='',
db_index=True,
verbose_name='下单支付商户号',
)
class Meta:
db_table = 'dingdan'
+47 -42
View File
@@ -17,6 +17,7 @@ from decimal import Decimal
from django.db import transaction
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from utils.pay_mch_config import pay_cfg, get_current_pay_mch_id, get_pay_wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
@@ -202,6 +203,7 @@ class CreateOrderView(APIView):
'tupian':shangpin_tupian,
'user1_id':f"Boss{request.user.yonghuid}",
'leixing_id': qiangdan_fields['leixing_id'],
'pay_mch_id': get_current_pay_mch_id(),
}
# 创建订单记录
@@ -260,9 +262,9 @@ class CreateOrderView(APIView):
try:
# 微信支付配置
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
NOTIFY_URL = wx_cfg.WEIXIN_NOTIFY_URL
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
NOTIFY_URL = pay_cfg.WEIXIN_NOTIFY_URL
if not all([APPID, MCHID, KEY, NOTIFY_URL]):
raise Exception('微信支付配置不完整')
@@ -509,7 +511,7 @@ class WechatPayNotifyView(APIView):
# 按key排序
sorted_keys = sorted(data.keys())
stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys])
stringSignTemp = f"{stringA}&key={wx_cfg.WEIXIN_SHANGHUMIYAO}"
stringSignTemp = f"{stringA}&key={pay_cfg.WEIXIN_SHANGHUMIYAO}"
calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}")
return calc_sign == sign
@@ -833,8 +835,8 @@ class PaymentVerifyView(APIView):
若查询失败,抛出异常
"""
appid = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.WEIXIN_SHANGHUMIYAO
mch_id = pay_cfg.WEIXIN_MCHID
key = pay_cfg.WEIXIN_SHANGHUMIYAO
nonce_str = self._generate_nonce_str()
params = {
@@ -1058,6 +1060,7 @@ class VirtualCreateOrderView(APIView):
'tupian': shangpin_tupian,
'user1_id': f"Boss{current_user.yonghuid}",
'leixing_id': qiangdan_fields['leixing_id'],
'pay_mch_id': get_current_pay_mch_id(),
}
dingdan = Dingdan.objects.create(**order_data)
@@ -7130,7 +7133,8 @@ class AdTongYiTuiKuanPingTai(APIView):
weixin_refund_result = self.call_weixin_refund(
out_trade_no=dingdan_id,
jine=jine,
transaction_id=transaction_id
transaction_id=transaction_id,
pay_mch_id=getattr(dingdan_obj, 'pay_mch_id', '') or '',
)
if not weixin_refund_result.get('success'):
@@ -7217,20 +7221,24 @@ class AdTongYiTuiKuanPingTai(APIView):
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def call_weixin_refund(self, out_trade_no, jine, transaction_id=None):
def call_weixin_refund(self, out_trade_no, jine, transaction_id=None, pay_mch_id=None):
"""
调用微信退款接口
:param out_trade_no: 商户订单号
:param jine: 退款金额
:param transaction_id: 微信支付订单号(优先使用)
:param pay_mch_id: 下单时的支付商户号(有则按原商户退)
:return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'}
"""
try:
# 检查配置
required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH']
for setting in required_settings:
if not hasattr(settings, setting) or not getattr(settings, setting):
return {'success': False, 'msg': f'缺少微信支付配置: {setting}'}
pcfg = get_pay_wx_cfg(pay_mch_id)
mch_id = pcfg.get('MCHID') or ''
key = pcfg.get('KEY') or ''
cert_path = pcfg.get('CERT_PATH') or ''
key_path = pcfg.get('KEY_PATH') or ''
appid = wx_cfg.WEIXIN_APPID
if not all([appid, mch_id, key, cert_path, key_path]):
return {'success': False, 'msg': '缺少微信支付配置'}
# 生成退款单号
refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}"
@@ -7238,13 +7246,13 @@ class AdTongYiTuiKuanPingTai(APIView):
# 构造请求参数
refund_data = {
'appid': wx_cfg.WEIXIN_APPID,
'mch_id': wx_cfg.WEIXIN_MCHID,
'appid': appid,
'mch_id': mch_id,
'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
'out_refund_no': refund_id,
'total_fee': total_fee_in_fen,
'refund_fee': total_fee_in_fen,
'op_user_id': wx_cfg.WEIXIN_MCHID,
'op_user_id': mch_id,
'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS'
}
# 优先使用 transaction_id,否则用 out_trade_no
@@ -7256,7 +7264,7 @@ class AdTongYiTuiKuanPingTai(APIView):
# 生成签名
sorted_params = sorted(refund_data.items())
sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
sign_string += f"&key={wx_cfg.WEIXIN_SHANGHUMIYAO}"
sign_string += f"&key={key}"
sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
refund_data['sign'] = sign
@@ -7266,7 +7274,7 @@ class AdTongYiTuiKuanPingTai(APIView):
url,
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
cert=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.WEIXIN_KEY_PATH),
cert=(cert_path, key_path),
timeout=30
)
@@ -7473,7 +7481,11 @@ class AdTongYiTuiKuanPingTai(APIView):
# ========== 修改点开始 ==========
# 13. 先调用微信退款接口,成功后才更新数据库
weixin_refund_result = self.call_weixin_refund(dingdan_id, jine)
weixin_refund_result = self.call_weixin_refund(
dingdan_id,
jine,
pay_mch_id=getattr(dingdan_obj, 'pay_mch_id', '') or '',
)
if not weixin_refund_result.get('success'):
# 微信退款失败,直接返回错误,数据库不更新
@@ -7588,27 +7600,20 @@ class AdTongYiTuiKuanPingTai(APIView):
def call_weixin_refund(self, dingdan_id, jine):
def call_weixin_refund(self, dingdan_id, jine, pay_mch_id=None):
"""
调用微信退款接口
参考你提供的接口,但根据现有模型和配置调整
需要在settings.py中配置以下参数:
WEIXIN_APPID = '你的小程序appid'
WEIXIN_MCHID = '你的商户号'
WEIXIN_SHANGHUMIYAO = '你的商户密钥'
WEIXIN_CERT_PATH = '证书路径/cert/apiclient_cert.pem'
WEIXIN_KEY_PATH = '证书路径/cert/apiclient_key.pem'
调用微信退款接口。pay_mch_id 有值时按原下单商户退款。
返回:{'success': True/False, 'msg': '消息'}
"""
try:
# 检查必要的配置
required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH',
'WEIXIN_KEY_PATH']
for setting in required_settings:
if not hasattr(settings, setting) or not getattr(settings, setting):
return {'success': False, 'msg': f'缺少微信支付配置: {setting}'}
pcfg = get_pay_wx_cfg(pay_mch_id)
mch_id = pcfg.get('MCHID') or ''
key = pcfg.get('KEY') or ''
cert_path = pcfg.get('CERT_PATH') or ''
key_path = pcfg.get('KEY_PATH') or ''
appid = wx_cfg.WEIXIN_APPID
if not all([appid, mch_id, key, cert_path, key_path]):
return {'success': False, 'msg': '缺少微信支付配置'}
# 生成退款单号
refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}"
@@ -7618,21 +7623,21 @@ class AdTongYiTuiKuanPingTai(APIView):
# 准备微信退款参数
refund_data = {
'appid': wx_cfg.WEIXIN_APPID,
'mch_id': wx_cfg.WEIXIN_MCHID,
'appid': appid,
'mch_id': mch_id,
'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
'out_trade_no': dingdan_id, # 商户订单号(我们的订单ID)
'out_refund_no': refund_id, # 商户退款单号
'total_fee': total_fee_in_fen, # 订单总金额(分)
'refund_fee': total_fee_in_fen, # 退款金额(分),全额退款
'op_user_id': wx_cfg.WEIXIN_MCHID, # 操作员ID,默认为商户号
'op_user_id': mch_id, # 操作员ID,默认为商户号
'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' # 退款资金来源
}
# 生成签名
sorted_params = sorted(refund_data.items())
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f"&key={wx_cfg.WEIXIN_SHANGHUMIYAO}"
sign_string = '&'.join([f"{key_}={value}" for key_, value in sorted_params if value])
sign_string += f"&key={key}"
sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
refund_data['sign'] = sign
@@ -7651,7 +7656,7 @@ class AdTongYiTuiKuanPingTai(APIView):
weixin_refund_url,
data=xml_data.encode('utf-8'),
headers=headers,
cert=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.WEIXIN_KEY_PATH),
cert=(cert_path, key_path),
timeout=timeout
)
+7 -5
View File
@@ -28,6 +28,7 @@ import xmltodict
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from utils.pay_mch_config import pay_cfg, get_pay_wx_cfg
from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory
# apps/houtai/views.py 继续添加
@@ -1484,12 +1485,13 @@ class CrossPlatformRefundView(APIView):
def _call_wechat_refund(self, order):
"""调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息"""
# 获取微信支付配置
# 获取微信支付配置(优先订单下单商户)
appid = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.WEIXIN_SHANGHUMIYAO
cert_path = wx_cfg.WEIXIN_CERT_PATH
key_path = wx_cfg.WEIXIN_KEY_PATH
pcfg = get_pay_wx_cfg(getattr(order, 'pay_mch_id', None))
mch_id = pcfg.get('MCHID') or ''
key = pcfg.get('KEY') or ''
cert_path = pcfg.get('CERT_PATH') or ''
key_path = pcfg.get('KEY_PATH') or ''
if not all([appid, mch_id, key, cert_path, key_path]):
missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
@@ -0,0 +1,76 @@
# 支付商户独立字段:从旧 weixin_* 回填,收款侧不动
from django.db import migrations, models
def copy_weixin_to_pay(apps, schema_editor):
XcxSysPeizhi = apps.get_model('peizhi', 'XcxSysPeizhi')
for row in XcxSysPeizhi.objects.all():
updates = []
if not (row.pay_mch_id or '').strip() and (row.weixin_mchid or '').strip():
row.pay_mch_id = row.weixin_mchid
updates.append('pay_mch_id')
if not (row.pay_api_v2_key or '').strip() and (row.weixin_shanghu_miyao or '').strip():
row.pay_api_v2_key = row.weixin_shanghu_miyao
updates.append('pay_api_v2_key')
if not (row.pay_notify_url or '').strip() and (row.weixin_notify_url or '').strip():
row.pay_notify_url = row.weixin_notify_url
updates.append('pay_notify_url')
if not (row.pay_cert_path or '').strip() and (row.weixin_cert_path or '').strip():
row.pay_cert_path = row.weixin_cert_path
updates.append('pay_cert_path')
if not (row.pay_key_path or '').strip() and (row.weixin_key_path or '').strip():
row.pay_key_path = row.weixin_key_path
updates.append('pay_key_path')
if updates:
row.save(update_fields=updates)
def noop_reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('peizhi', '0024_xcx_sys_peizhi_shangjia_paifa_yajin'),
]
operations = [
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_mch_id',
field=models.CharField(blank=True, default='', max_length=32, verbose_name='支付商户号'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_api_v2_key',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='支付APIv2密钥'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_api_v3_key',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='支付APIv3密钥'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_notify_url',
field=models.CharField(blank=True, default='', max_length=300, verbose_name='支付回调URL(支付商户)'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_cert_path',
field=models.CharField(blank=True, default='', max_length=500, verbose_name='支付退款证书路径'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_key_path',
field=models.CharField(blank=True, default='', max_length=500, verbose_name='支付退款密钥路径'),
),
migrations.AddField(
model_name='xcxsyspeizhi',
name='pay_cert_serial_no',
field=models.CharField(blank=True, default='', max_length=128, verbose_name='支付商户证书序列号'),
),
migrations.RunPython(copy_weixin_to_pay, noop_reverse),
]
+8
View File
@@ -694,6 +694,14 @@ class XcxSysPeizhi(models.Model):
weixin_notify_url = models.CharField(max_length=300, blank=True, default='', verbose_name='支付回调URL')
weixin_cert_path = models.CharField(max_length=500, blank=True, default='', verbose_name='退款证书路径')
weixin_key_path = models.CharField(max_length=500, blank=True, default='', verbose_name='退款密钥路径')
# 支付专用商户(与提现收款 WechatPayMchConfig / 旧 weixin_* 分离)
pay_mch_id = models.CharField(max_length=32, blank=True, default='', verbose_name='支付商户号')
pay_api_v2_key = models.CharField(max_length=128, blank=True, default='', verbose_name='支付APIv2密钥')
pay_api_v3_key = models.CharField(max_length=128, blank=True, default='', verbose_name='支付APIv3密钥')
pay_notify_url = models.CharField(max_length=300, blank=True, default='', verbose_name='支付回调URL(支付商户)')
pay_cert_path = models.CharField(max_length=500, blank=True, default='', verbose_name='支付退款证书路径')
pay_key_path = models.CharField(max_length=500, blank=True, default='', verbose_name='支付退款密钥路径')
pay_cert_serial_no = models.CharField(max_length=128, blank=True, default='', verbose_name='支付商户证书序列号')
notify_base_url = models.CharField(
max_length=300, blank=True, default='https://wenhe.nmslb.com/hqhd', verbose_name='业务回调根URL'
)
+169
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),
},
})
+4
View File
@@ -15,6 +15,7 @@ from .wx_mch_views import (
WechatMchListView, WechatMchSaveView, WechatMchUploadView,
WechatMchUserBindListView, WechatMchUserBindSaveView, WechatMchUserBindDeleteView,
)
from .pay_mch_views import PayMchQueryView, PayMchUpdateView, PayMchUploadView
from .xieyi_views import XieyiListView, XieyiDetailView, XieyiSignStatusView, XieyiSignView, XieyiCheckView
urlpatterns = [
@@ -63,6 +64,9 @@ urlpatterns = [
path('wxmchUserLb', WechatMchUserBindListView.as_view(), name='用户绑定转账商户列表'),
path('wxmchUserBj', WechatMchUserBindSaveView.as_view(), name='保存用户绑定转账商户'),
path('wxmchUserSc', WechatMchUserBindDeleteView.as_view(), name='删除用户绑定转账商户'),
path('paymchhq', PayMchQueryView.as_view(), name='获取支付商户配置'),
path('paymchgx', PayMchUpdateView.as_view(), name='更新支付商户配置'),
path('paymchscwj', PayMchUploadView.as_view(), name='上传支付商户证书'),
path('xieyihq', XieyiListView.as_view(), name='小程序获取协议列表'),
path('xieyixq', XieyiDetailView.as_view(), name='小程序获取协议详情'),
path('xieyijc', XieyiCheckView.as_view(), name='小程序检查协议签署'),
+14 -13
View File
@@ -6,6 +6,7 @@ from rest_framework.throttling import AnonRateThrottle
from django.db import models
from shangpin.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin
from utils.xcx_sys_config import get_shangpin_shenhe_zhuangtai
from utils.pay_mch_config import pay_cfg
from utils.jwt_auth import OptionalYonghuidJWTAuthentication, YonghuidJWTAuthentication
from yonghu.models import UserMain
import logging
@@ -549,8 +550,8 @@ class YajinGoumai(APIView):
"""生成微信小程序支付参数"""
# 获取配置
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -876,7 +877,7 @@ class YajinHuitiao(View):
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
@@ -1126,8 +1127,8 @@ class JifenBuchong(APIView):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -1429,7 +1430,7 @@ class JifenHuitiao(View):
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
@@ -1724,8 +1725,8 @@ class HuiyuanGoumai(APIView):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -2269,7 +2270,7 @@ class HuiyuanHuitiao(View):
sorted_params = sorted(params.items(), key=lambda x: x[0])
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
return sign == calculated_sign
except Exception as e:
@@ -2632,7 +2633,7 @@ class HuiyuanHuitiao(View):
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
@@ -2963,8 +2964,8 @@ class ShangjiaChongzhi(APIView):
"""生成微信小程序支付参数"""
# 获取配置
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
# 根据支付类型获取对应的配置
if pay_type == 'shangjia':
@@ -3262,7 +3263,7 @@ class ShangjiaHuitiao(View):
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
+150
View File
@@ -0,0 +1,150 @@
"""
支付专用商户配置(与提现收款 WechatPayMchConfig / wechat_mch_service 完全分离)。
读取优先级:
1) XcxSysPeizhi.pay_*
2) 过渡回退:旧 weixin_* / settings.WEIXIN_*
历史订单退款:若订单 pay_mch_id 与当前支付商户不一致,但等于旧 weixin_mchid,则用旧 weixin_* 退款。
"""
import logging
import os
from django.conf import settings
from utils.xcx_sys_config import wx_cfg, _val
logger = logging.getLogger(__name__)
def pay_cert_base_dir():
base = getattr(settings, 'WECHAT_PAY_CERT_BASE_DIR', '') or ''
if base:
return base
return os.path.join(str(settings.BASE_DIR), 'zhifuzhengshu')
def _pay_val(pay_attr, legacy_db_attr, settings_attr, default=''):
"""pay_* 优先;空则回退旧 weixin_* / settings。"""
v = _val(pay_attr, None, None)
if v is not None and v != '':
return v
return _val(legacy_db_attr, settings_attr, default) or default
class PayWxConfig:
"""支付商户配置代理(属性名兼容旧 WEIXIN_*,便于替换调用点)。"""
@property
def WEIXIN_MCHID(self):
return _pay_val('pay_mch_id', 'weixin_mchid', 'WEIXIN_MCHID', '')
@property
def WEIXIN_SHANGHUMIYAO(self):
return _pay_val('pay_api_v2_key', 'weixin_shanghu_miyao', 'WEIXIN_SHANGHUMIYAO', '')
@property
def WEIXIN_NOTIFY_URL(self):
return _pay_val('pay_notify_url', 'weixin_notify_url', 'WEIXIN_NOTIFY_URL', '')
@property
def WEIXIN_CERT_PATH(self):
return _pay_val('pay_cert_path', 'weixin_cert_path', 'WEIXIN_CERT_PATH', '')
@property
def WEIXIN_KEY_PATH(self):
return _pay_val('pay_key_path', 'weixin_key_path', 'WEIXIN_KEY_PATH', '')
@property
def PAY_API_V3_KEY(self):
return _val('pay_api_v3_key', None, '') or ''
@property
def PAY_CERT_SERIAL_NO(self):
return _val('pay_cert_serial_no', None, '') or ''
# 别名,语义更清晰
@property
def PAY_MCH_ID(self):
return self.WEIXIN_MCHID
@property
def PAY_API_V2_KEY(self):
return self.WEIXIN_SHANGHUMIYAO
pay_cfg = PayWxConfig()
def get_current_pay_mch_id():
return (pay_cfg.PAY_MCH_ID or '').strip()
def pay_cfg_as_dict():
return {
'MCHID': pay_cfg.WEIXIN_MCHID,
'KEY': pay_cfg.WEIXIN_SHANGHUMIYAO,
'NOTIFY_URL': pay_cfg.WEIXIN_NOTIFY_URL,
'CERT_PATH': pay_cfg.WEIXIN_CERT_PATH,
'KEY_PATH': pay_cfg.WEIXIN_KEY_PATH,
'API_V3_KEY': pay_cfg.PAY_API_V3_KEY,
'CERT_SERIAL_NO': pay_cfg.PAY_CERT_SERIAL_NO,
}
def get_pay_wx_cfg(mch_id=None):
"""
按商户号取支付配置(用于退款反查)。
- mch_id 空 / 等于当前支付商户 → 当前 pay_cfg
- mch_id 等于旧 weixin_mchid 且与当前不同 → 用旧 weixin_*(换号后历史单退款)
- 否则若 zhifuzhengshu/{mch_id}/ 下有证书,拼路径但仍需密钥;密钥仅当前/旧库有,故仍回退当前并打日志
"""
current = pay_cfg_as_dict()
want = (mch_id or '').strip()
current_mch = (current.get('MCHID') or '').strip()
if not want or want == current_mch:
return current
legacy_mch = (wx_cfg.WEIXIN_MCHID or '').strip()
if want == legacy_mch and want != current_mch:
return {
'MCHID': wx_cfg.WEIXIN_MCHID,
'KEY': wx_cfg.WEIXIN_SHANGHUMIYAO,
'NOTIFY_URL': wx_cfg.WEIXIN_NOTIFY_URL,
'CERT_PATH': wx_cfg.WEIXIN_CERT_PATH,
'KEY_PATH': wx_cfg.WEIXIN_KEY_PATH,
'API_V3_KEY': '',
'CERT_SERIAL_NO': '',
}
cert_path = os.path.join(pay_cert_base_dir(), want, 'apiclient_cert.pem')
key_path = os.path.join(pay_cert_base_dir(), want, 'apiclient_key.pem')
if os.path.isfile(cert_path) and os.path.isfile(key_path):
logger.warning(
'支付商户 %s 与当前配置 %s 不一致,仅找到证书文件,密钥仍用当前配置,退款可能失败',
want, current_mch,
)
out = dict(current)
out['MCHID'] = want
out['CERT_PATH'] = cert_path
out['KEY_PATH'] = key_path
return out
logger.warning(
'订单支付商户 %s 无可用历史配置,回退当前支付商户 %s',
want, current_mch,
)
return current
def apply_pay_cfg_to_proxy(cfg_dict):
"""把 get_pay_wx_cfg 结果转成带 WEIXIN_* 属性的简单对象,兼容旧代码写法。"""
class _Cfg:
pass
o = _Cfg()
o.WEIXIN_MCHID = cfg_dict.get('MCHID') or ''
o.WEIXIN_SHANGHUMIYAO = cfg_dict.get('KEY') or ''
o.WEIXIN_NOTIFY_URL = cfg_dict.get('NOTIFY_URL') or ''
o.WEIXIN_CERT_PATH = cfg_dict.get('CERT_PATH') or ''
o.WEIXIN_KEY_PATH = cfg_dict.get('KEY_PATH') or ''
return o
+24 -18
View File
@@ -6,6 +6,7 @@ import requests
from django.db import transaction, IntegrityError
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from utils.pay_mch_config import pay_cfg, get_pay_wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
@@ -9211,7 +9212,8 @@ class KefuPlatformRefundView(APIView):
refund_result = self.call_wechat_refund(
dingdan_id=order.dingdan_id,
jine=order.jine,
transaction_id=wechat_transaction_id
transaction_id=wechat_transaction_id,
pay_mch_id=getattr(order, 'pay_mch_id', '') or '',
)
if refund_result['code'] != 0:
@@ -9337,16 +9339,17 @@ class KefuPlatformRefundView(APIView):
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ==================== 商户微信支付退款 ====================
def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
def call_wechat_refund(self, dingdan_id, jine, transaction_id=None, pay_mch_id=None):
"""
调用微信支付退款接口 secapi/pay/refund
:return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
"""
appid = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.WEIXIN_SHANGHUMIYAO
cert_path = wx_cfg.WEIXIN_CERT_PATH
key_path = wx_cfg.WEIXIN_KEY_PATH
pcfg = get_pay_wx_cfg(pay_mch_id)
mch_id = pcfg.get('MCHID') or ''
key = pcfg.get('KEY') or ''
cert_path = pcfg.get('CERT_PATH') or ''
key_path = pcfg.get('KEY_PATH') or ''
if not all([appid, mch_id, key, cert_path, key_path]):
missing = [
@@ -9596,7 +9599,8 @@ class KefuPlatformRefundView(APIView):
refund_result = self.call_wechat_refund(
dingdan_id=order.dingdan_id,
jine=order.jine,
transaction_id=wechat_transaction_id
transaction_id=wechat_transaction_id,
pay_mch_id=getattr(order, 'pay_mch_id', '') or '',
)
if refund_result['code'] != 0:
@@ -9672,20 +9676,22 @@ class KefuPlatformRefundView(APIView):
logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})
def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
def call_wechat_refund(self, dingdan_id, jine, transaction_id=None, pay_mch_id=None):
"""
调用微信支付退款接口
:param dingdan_id: 商户订单号(我们的订单ID)
:param jine: 退款金额
:param transaction_id: 微信支付订单号(优先使用)
:param pay_mch_id: 下单支付商户号
:return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
"""
# 获取配置
appid = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.WEIXIN_SHANGHUMIYAO
cert_path = wx_cfg.WEIXIN_CERT_PATH
key_path = wx_cfg.WEIXIN_KEY_PATH
pcfg = get_pay_wx_cfg(pay_mch_id)
mch_id = pcfg.get('MCHID') or ''
key = pcfg.get('KEY') or ''
cert_path = pcfg.get('CERT_PATH') or ''
key_path = pcfg.get('KEY_PATH') or ''
if not all([appid, mch_id, key, cert_path, key_path]):
missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
@@ -14837,8 +14843,8 @@ class FaKuanPayView(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
if pay_type == 'fakuan':
PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
@@ -15098,7 +15104,7 @@ class FaKuanHuitiaoView(View):
# 拼接字符串
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
# 计算签名
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
@@ -15360,8 +15366,8 @@ class KaohePayView(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
"""生成微信JSAPI支付参数"""
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
MCHID = pay_cfg.WEIXIN_MCHID
KEY = pay_cfg.WEIXIN_SHANGHUMIYAO
if pay_type == 'kaohe':
PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
@@ -15679,7 +15685,7 @@ class KaohePayCallbackView(View):
sorted_params = sorted(params.items(), key=lambda x: x[0])
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}'
sign_string += f'&key={pay_cfg.WEIXIN_SHANGHUMIYAO}'
calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper()
result = sign == calculated_sign