feat: 收款与提现商户拆分,支持后台上传证书
收款仍用 mch_id/mch_key;提现优先 withdraw_mch_id(空则回落);证书分 pay/withdraw 目录落盘。退款逻辑未改。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
70
jituan/migrations/0013_club_pay_withdraw_split.py
Normal file
70
jituan/migrations/0013_club_pay_withdraw_split.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0012_club_mch_key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='pay_cert_path',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default='', help_text='apiclient_cert.pem,退款/V2 证书用',
|
||||||
|
max_length=512, verbose_name='收款商户证书路径',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='pay_key_path',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default='', help_text='apiclient_key.pem,退款/V2 证书用',
|
||||||
|
max_length=512, verbose_name='收款商户私钥路径',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='withdraw_mch_id',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default='', help_text='空则回落收款商户号 mch_id',
|
||||||
|
max_length=32, verbose_name='提现商户号',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='mch_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=32, verbose_name='收款商户号'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='pay_app_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=32, verbose_name='收款AppID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='mch_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=128, verbose_name='收款APIv2密钥'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='api_v3_key',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=128, verbose_name='提现APIv3密钥'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='cert_serial_no',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=64, verbose_name='提现证书序列号'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='private_key_path',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=512, verbose_name='提现商户私钥路径'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='club',
|
||||||
|
name='platform_cert_dir',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=512, verbose_name='提现平台证书目录'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -13,13 +13,34 @@ class Club(QModel):
|
|||||||
status = models.IntegerField(default=1, verbose_name='1启用0停用')
|
status = models.IntegerField(default=1, verbose_name='1启用0停用')
|
||||||
wx_appid = models.CharField(max_length=32, blank=True, default='')
|
wx_appid = models.CharField(max_length=32, blank=True, default='')
|
||||||
wx_secret = models.CharField(max_length=128, blank=True, default='')
|
wx_secret = models.CharField(max_length=128, blank=True, default='')
|
||||||
mch_id = models.CharField(max_length=32, blank=True, default='')
|
# —— 点单/会员等收款商户(V2 JSAPI;退款也应对齐这套)——
|
||||||
pay_app_id = models.CharField(max_length=32, blank=True, default='')
|
mch_id = models.CharField(max_length=32, blank=True, default='', verbose_name='收款商户号')
|
||||||
mch_key = models.CharField(max_length=128, blank=True, default='', verbose_name='APIv2商户密钥')
|
pay_app_id = models.CharField(max_length=32, blank=True, default='', verbose_name='收款AppID')
|
||||||
api_v3_key = models.CharField(max_length=128, blank=True, default='', verbose_name='APIv3密钥')
|
mch_key = models.CharField(max_length=128, blank=True, default='', verbose_name='收款APIv2密钥')
|
||||||
cert_serial_no = models.CharField(max_length=64, blank=True, default='')
|
pay_cert_path = models.CharField(
|
||||||
private_key_path = models.CharField(max_length=255, blank=True, default='')
|
max_length=512, blank=True, default='',
|
||||||
platform_cert_dir = models.CharField(max_length=255, blank=True, default='')
|
verbose_name='收款商户证书路径',
|
||||||
|
help_text='apiclient_cert.pem,退款/V2 证书用',
|
||||||
|
)
|
||||||
|
pay_key_path = models.CharField(
|
||||||
|
max_length=512, blank=True, default='',
|
||||||
|
verbose_name='收款商户私钥路径',
|
||||||
|
help_text='apiclient_key.pem,退款/V2 证书用',
|
||||||
|
)
|
||||||
|
# —— 提现/转账商户(V3;可与收款商户号不同)——
|
||||||
|
withdraw_mch_id = models.CharField(
|
||||||
|
max_length=32, blank=True, default='',
|
||||||
|
verbose_name='提现商户号',
|
||||||
|
help_text='空则回落收款商户号 mch_id',
|
||||||
|
)
|
||||||
|
api_v3_key = models.CharField(max_length=128, blank=True, default='', verbose_name='提现APIv3密钥')
|
||||||
|
cert_serial_no = models.CharField(max_length=64, blank=True, default='', verbose_name='提现证书序列号')
|
||||||
|
private_key_path = models.CharField(
|
||||||
|
max_length=512, blank=True, default='', verbose_name='提现商户私钥路径',
|
||||||
|
)
|
||||||
|
platform_cert_dir = models.CharField(
|
||||||
|
max_length=512, blank=True, default='', verbose_name='提现平台证书目录',
|
||||||
|
)
|
||||||
official_appid = models.CharField(max_length=32, blank=True, default='')
|
official_appid = models.CharField(max_length=32, blank=True, default='')
|
||||||
official_secret = models.CharField(max_length=128, blank=True, default='')
|
official_secret = models.CharField(max_length=128, blank=True, default='')
|
||||||
official_token = models.CharField(max_length=64, blank=True, default='')
|
official_token = models.CharField(max_length=64, blank=True, default='')
|
||||||
|
|||||||
120
jituan/services/club_cert_upload.py
Normal file
120
jituan/services/club_cert_upload.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""俱乐部微信商户证书落盘(收款 / 提现分目录)。"""
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -226,12 +226,20 @@ def get_wechat_v2_config(club_id=None):
|
|||||||
cid, mch_id,
|
cid, mch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 收款商户证书(供退款等对齐收款商户;未配时留空,调用方可回落 settings)
|
||||||
|
pay_cert = _pick_club_field(club, cfg_json, 'pay_cert_path')
|
||||||
|
pay_key = _pick_club_field(club, cfg_json, 'pay_key_path')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'appid': appid,
|
'appid': appid,
|
||||||
'mch_id': mch_id,
|
'mch_id': mch_id,
|
||||||
'key': mch_key,
|
'key': mch_key,
|
||||||
'club_id': cid,
|
'club_id': cid,
|
||||||
'_key_source': key_source,
|
'_key_source': key_source,
|
||||||
|
# 明确标记:这是点单/会员收款商户
|
||||||
|
'merchant_role': 'pay',
|
||||||
|
'cert_path': pay_cert,
|
||||||
|
'key_path': pay_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -378,16 +386,26 @@ def get_wechat_v3_config(club_id=None):
|
|||||||
|
|
||||||
xq = XQ_WX_PAY if cid in SHARED_WX_MERCHANT_CLUBS else {}
|
xq = XQ_WX_PAY if cid in SHARED_WX_MERCHANT_CLUBS else {}
|
||||||
|
|
||||||
mch_id = _pick_club_field(club, cfg_json, 'mch_id') or xq.get('mch_id', '')
|
# 提现商户号:优先 withdraw_mch_id,空则回落收款 mch_id(兼容旧数据)
|
||||||
|
mch_id = (
|
||||||
|
_pick_club_field(club, cfg_json, 'withdraw_mch_id')
|
||||||
|
or _pick_club_field(club, cfg_json, 'mch_id')
|
||||||
|
or xq.get('mch_id', '')
|
||||||
|
)
|
||||||
api_v3_key = _pick_club_field(club, cfg_json, 'api_v3_key') or xq.get('api_v3_key', '')
|
api_v3_key = _pick_club_field(club, cfg_json, 'api_v3_key') or xq.get('api_v3_key', '')
|
||||||
|
|
||||||
# 共用商户的俱乐部:私钥/证书目录与星阙相同(settings 路径 + 同目录 apiclient_cert.pem)
|
# 共用商户的俱乐部:默认用 settings 私钥;仅当库中路径存在且文件在盘上才覆盖
|
||||||
|
# (避免旧库里曾填过、但原先被忽略的无效路径突然接管提现)
|
||||||
private_key_path = path_defaults['PRIVATE_KEY_PATH']
|
private_key_path = path_defaults['PRIVATE_KEY_PATH']
|
||||||
if cid not in SHARED_WX_MERCHANT_CLUBS:
|
if cid not in SHARED_WX_MERCHANT_CLUBS:
|
||||||
private_key_path = (
|
private_key_path = (
|
||||||
_pick_club_field(club, cfg_json, 'private_key_path')
|
_pick_club_field(club, cfg_json, 'private_key_path')
|
||||||
or private_key_path
|
or private_key_path
|
||||||
)
|
)
|
||||||
|
elif club:
|
||||||
|
uploaded_key = (getattr(club, 'private_key_path', None) or '').strip()
|
||||||
|
if uploaded_key and os.path.isfile(uploaded_key):
|
||||||
|
private_key_path = uploaded_key
|
||||||
|
|
||||||
platform_cert_dir = (
|
platform_cert_dir = (
|
||||||
_pick_club_field(club, cfg_json, 'platform_cert_dir')
|
_pick_club_field(club, cfg_json, 'platform_cert_dir')
|
||||||
@@ -405,7 +423,10 @@ def get_wechat_v3_config(club_id=None):
|
|||||||
# 序列号必须从与私钥配对的 apiclient_cert.pem 读取,禁止盲用数据库/写死值
|
# 序列号必须从与私钥配对的 apiclient_cert.pem 读取,禁止盲用数据库/写死值
|
||||||
cert_serial = resolve_merchant_cert_serial(private_key_path, '')
|
cert_serial = resolve_merchant_cert_serial(private_key_path, '')
|
||||||
if not cert_serial:
|
if not cert_serial:
|
||||||
cert_serial = xq.get('cert_serial_no', '')
|
cert_serial = (
|
||||||
|
_pick_club_field(club, cfg_json, 'cert_serial_no')
|
||||||
|
or xq.get('cert_serial_no', '')
|
||||||
|
)
|
||||||
|
|
||||||
cfg = {
|
cfg = {
|
||||||
'APPID': appid,
|
'APPID': appid,
|
||||||
@@ -416,6 +437,7 @@ def get_wechat_v3_config(club_id=None):
|
|||||||
'PLATFORM_CERT_DIR': platform_cert_dir,
|
'PLATFORM_CERT_DIR': platform_cert_dir,
|
||||||
'TRANSFER_SCENE_ID': transfer_scene_id,
|
'TRANSFER_SCENE_ID': transfer_scene_id,
|
||||||
'club_id': cid,
|
'club_id': cid,
|
||||||
|
'merchant_role': 'withdraw',
|
||||||
}
|
}
|
||||||
_log_v3_config(cfg, source='club_db' if club else 'xq_fallback')
|
_log_v3_config(cfg, source='club_db' if club else 'xq_fallback')
|
||||||
return cfg
|
return cfg
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from jituan.views import (
|
|||||||
ClubAdminAssignmentListView,
|
ClubAdminAssignmentListView,
|
||||||
ClubAuditLogListView,
|
ClubAuditLogListView,
|
||||||
ClubManageView,
|
ClubManageView,
|
||||||
|
ClubCertUploadView,
|
||||||
ClubCaiwuView,
|
ClubCaiwuView,
|
||||||
ClubChongzhiFinanceView,
|
ClubChongzhiFinanceView,
|
||||||
ClubDashouRegisterView,
|
ClubDashouRegisterView,
|
||||||
@@ -102,6 +103,7 @@ urlpatterns = [
|
|||||||
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
||||||
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
|
||||||
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
path('houtai/club-manage', ClubManageView.as_view(), name='jituan_club_manage'),
|
||||||
|
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
||||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||||
path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'),
|
path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'),
|
||||||
path('houtai/szxx', ClubSzxxView.as_view(), name='jituan_szxx'),
|
path('houtai/szxx', ClubSzxxView.as_view(), name='jituan_szxx'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -450,8 +450,11 @@ class ClubManageView(APIView):
|
|||||||
parser_classes = [JSONParser]
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
_EDITABLE_FIELDS = (
|
_EDITABLE_FIELDS = (
|
||||||
'name', 'status', 'wx_appid', 'wx_secret', 'mch_id', 'pay_app_id',
|
'name', 'status', 'wx_appid', 'wx_secret',
|
||||||
'mch_key', 'api_v3_key',
|
# 收款商户
|
||||||
|
'mch_id', 'pay_app_id', 'mch_key', 'pay_cert_path', 'pay_key_path',
|
||||||
|
# 提现商户
|
||||||
|
'withdraw_mch_id', 'api_v3_key',
|
||||||
'cert_serial_no', 'private_key_path', 'platform_cert_dir',
|
'cert_serial_no', 'private_key_path', 'platform_cert_dir',
|
||||||
'official_appid', 'official_secret', 'official_token', 'encoding_aes_key',
|
'official_appid', 'official_secret', 'official_token', 'encoding_aes_key',
|
||||||
'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix',
|
'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix',
|
||||||
@@ -485,12 +488,20 @@ class ClubManageView(APIView):
|
|||||||
warnings = []
|
warnings = []
|
||||||
if not mch_key:
|
if not mch_key:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
'未配置 APIv2 密钥 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,'
|
'未配置收款 APIv2 密钥 mch_key:点单/会员等收款不会使用提现 api_v3_key。'
|
||||||
'未填时将回落服务器 app_secrets 旧密钥,易与当前商户号不匹配导致签名错误。'
|
|
||||||
)
|
)
|
||||||
if mch_key and api_v3 and mch_key == api_v3:
|
if mch_key and api_v3 and mch_key == api_v3:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
'mch_key 与 api_v3_key 内容相同:通常不正确,V2(小程序支付)与 V3(提现转账)是两套不同密钥。'
|
'收款 mch_key 与提现 api_v3_key 相同:通常不正确,收款与提现是两套密钥。'
|
||||||
|
)
|
||||||
|
withdraw_mch = (getattr(club, 'withdraw_mch_id', None) or '').strip()
|
||||||
|
pay_mch = (club.mch_id or '').strip()
|
||||||
|
if withdraw_mch and pay_mch and withdraw_mch != pay_mch:
|
||||||
|
# 这是预期场景,给信息而非警告
|
||||||
|
pass
|
||||||
|
elif not withdraw_mch and pay_mch:
|
||||||
|
warnings.append(
|
||||||
|
'提现商户号未单独填写:提现将回落收款商户号 mch_id。若两套商户不同请填写 withdraw_mch_id。'
|
||||||
)
|
)
|
||||||
wx_a = (club.wx_appid or '').strip()
|
wx_a = (club.wx_appid or '').strip()
|
||||||
pay_a = (club.pay_app_id or '').strip()
|
pay_a = (club.pay_app_id or '').strip()
|
||||||
@@ -537,6 +548,7 @@ class ClubManageView(APIView):
|
|||||||
'oss_prefix': club.oss_prefix,
|
'oss_prefix': club.oss_prefix,
|
||||||
'mch_id': club.mch_id,
|
'mch_id': club.mch_id,
|
||||||
'pay_app_id': club.pay_app_id,
|
'pay_app_id': club.pay_app_id,
|
||||||
|
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
|
||||||
'goeasy_appkey': club.goeasy_appkey,
|
'goeasy_appkey': club.goeasy_appkey,
|
||||||
'template_id': club.template_id,
|
'template_id': club.template_id,
|
||||||
'sort_order': club.sort_order,
|
'sort_order': club.sort_order,
|
||||||
@@ -546,6 +558,8 @@ class ClubManageView(APIView):
|
|||||||
base.update({
|
base.update({
|
||||||
'wx_secret': club.wx_secret,
|
'wx_secret': club.wx_secret,
|
||||||
'mch_key': mch_key,
|
'mch_key': mch_key,
|
||||||
|
'pay_cert_path': getattr(club, 'pay_cert_path', '') or '',
|
||||||
|
'pay_key_path': getattr(club, 'pay_key_path', '') or '',
|
||||||
'api_v3_key': club.api_v3_key,
|
'api_v3_key': club.api_v3_key,
|
||||||
'pay_config': self._pay_config_meta(club),
|
'pay_config': self._pay_config_meta(club),
|
||||||
'cert_serial_no': club.cert_serial_no,
|
'cert_serial_no': club.cert_serial_no,
|
||||||
@@ -638,6 +652,69 @@ class ClubManageView(APIView):
|
|||||||
return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)
|
return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class ClubCertUploadView(APIView):
|
||||||
|
"""
|
||||||
|
POST /jituan/houtai/club-cert-upload
|
||||||
|
multipart: phone, club_id, kind, file
|
||||||
|
kind: pay_key | pay_cert | withdraw_key | withdraw_cert | withdraw_platform
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = (request.data.get('phone') or request.data.get('username') or '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if not can_manage_admin_assignments(request.user):
|
||||||
|
return Response({'code': 403, 'msg': '仅集团超管可上传商户证书'}, status=403)
|
||||||
|
|
||||||
|
club_id = (request.data.get('club_id') or '').strip()
|
||||||
|
kind = (request.data.get('kind') or '').strip()
|
||||||
|
uploaded = request.FILES.get('file')
|
||||||
|
if not club_id or not kind or not uploaded:
|
||||||
|
return Response({'code': 400, 'msg': '需要 club_id、kind、file'})
|
||||||
|
|
||||||
|
club = Club.query.filter(club_id=club_id).first()
|
||||||
|
if not club:
|
||||||
|
return Response({'code': 404, 'msg': f'俱乐部 {club_id} 不存在'})
|
||||||
|
|
||||||
|
from jituan.services.club_cert_upload import save_club_cert_file, CERT_KINDS
|
||||||
|
if kind not in CERT_KINDS:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': f'kind 无效,支持: {", ".join(CERT_KINDS.keys())}',
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
result = save_club_cert_file(club, kind, uploaded)
|
||||||
|
except ValueError as exc:
|
||||||
|
return Response({'code': 400, 'msg': str(exc)})
|
||||||
|
except OSError as exc:
|
||||||
|
logger.exception('证书落盘失败 club=%s kind=%s', club_id, kind)
|
||||||
|
return Response({'code': 500, 'msg': f'写入失败: {exc}'})
|
||||||
|
|
||||||
|
club = Club.query.filter(club_id=club_id).first()
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '上传成功',
|
||||||
|
'data': {
|
||||||
|
**result,
|
||||||
|
'pay_cert_path': getattr(club, 'pay_cert_path', '') or '',
|
||||||
|
'pay_key_path': getattr(club, 'pay_key_path', '') or '',
|
||||||
|
'private_key_path': club.private_key_path or '',
|
||||||
|
'platform_cert_dir': club.platform_cert_dir or '',
|
||||||
|
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
|
||||||
|
'mch_id': club.mch_id or '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
logger.exception('ClubCertUploadView 异常')
|
||||||
|
return Response({'code': 99, 'msg': '证书上传失败'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class ClubCaiwuView(APIView):
|
class ClubCaiwuView(APIView):
|
||||||
"""
|
"""
|
||||||
俱乐部维度财务(新接口)。
|
俱乐部维度财务(新接口)。
|
||||||
|
|||||||
Reference in New Issue
Block a user