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停用')
|
||||
wx_appid = models.CharField(max_length=32, blank=True, default='')
|
||||
wx_secret = models.CharField(max_length=128, blank=True, default='')
|
||||
mch_id = models.CharField(max_length=32, blank=True, default='')
|
||||
pay_app_id = models.CharField(max_length=32, blank=True, default='')
|
||||
mch_key = models.CharField(max_length=128, blank=True, default='', verbose_name='APIv2商户密钥')
|
||||
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='')
|
||||
private_key_path = models.CharField(max_length=255, blank=True, default='')
|
||||
platform_cert_dir = models.CharField(max_length=255, blank=True, default='')
|
||||
# —— 点单/会员等收款商户(V2 JSAPI;退款也应对齐这套)——
|
||||
mch_id = models.CharField(max_length=32, blank=True, default='', verbose_name='收款商户号')
|
||||
pay_app_id = models.CharField(max_length=32, blank=True, default='', verbose_name='收款AppID')
|
||||
mch_key = models.CharField(max_length=128, blank=True, default='', verbose_name='收款APIv2密钥')
|
||||
pay_cert_path = models.CharField(
|
||||
max_length=512, 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_secret = models.CharField(max_length=128, 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,
|
||||
)
|
||||
|
||||
# 收款商户证书(供退款等对齐收款商户;未配时留空,调用方可回落 settings)
|
||||
pay_cert = _pick_club_field(club, cfg_json, 'pay_cert_path')
|
||||
pay_key = _pick_club_field(club, cfg_json, 'pay_key_path')
|
||||
|
||||
return {
|
||||
'appid': appid,
|
||||
'mch_id': mch_id,
|
||||
'key': mch_key,
|
||||
'club_id': cid,
|
||||
'_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 {}
|
||||
|
||||
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', '')
|
||||
|
||||
# 共用商户的俱乐部:私钥/证书目录与星阙相同(settings 路径 + 同目录 apiclient_cert.pem)
|
||||
# 共用商户的俱乐部:默认用 settings 私钥;仅当库中路径存在且文件在盘上才覆盖
|
||||
# (避免旧库里曾填过、但原先被忽略的无效路径突然接管提现)
|
||||
private_key_path = path_defaults['PRIVATE_KEY_PATH']
|
||||
if cid not in SHARED_WX_MERCHANT_CLUBS:
|
||||
private_key_path = (
|
||||
_pick_club_field(club, cfg_json, '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 = (
|
||||
_pick_club_field(club, cfg_json, 'platform_cert_dir')
|
||||
@@ -405,7 +423,10 @@ def get_wechat_v3_config(club_id=None):
|
||||
# 序列号必须从与私钥配对的 apiclient_cert.pem 读取,禁止盲用数据库/写死值
|
||||
cert_serial = resolve_merchant_cert_serial(private_key_path, '')
|
||||
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 = {
|
||||
'APPID': appid,
|
||||
@@ -416,6 +437,7 @@ def get_wechat_v3_config(club_id=None):
|
||||
'PLATFORM_CERT_DIR': platform_cert_dir,
|
||||
'TRANSFER_SCENE_ID': transfer_scene_id,
|
||||
'club_id': cid,
|
||||
'merchant_role': 'withdraw',
|
||||
}
|
||||
_log_v3_config(cfg, source='club_db' if club else 'xq_fallback')
|
||||
return cfg
|
||||
|
||||
@@ -41,6 +41,7 @@ from jituan.views import (
|
||||
ClubAdminAssignmentListView,
|
||||
ClubAuditLogListView,
|
||||
ClubManageView,
|
||||
ClubCertUploadView,
|
||||
ClubCaiwuView,
|
||||
ClubChongzhiFinanceView,
|
||||
ClubDashouRegisterView,
|
||||
@@ -102,6 +103,7 @@ urlpatterns = [
|
||||
path('auth/perm-debug', ClubPermDebugView.as_view(), name='jituan_perm_debug'),
|
||||
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-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||
path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'),
|
||||
path('houtai/szxx', ClubSzxxView.as_view(), name='jituan_szxx'),
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from django.utils import timezone
|
||||
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.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -450,8 +450,11 @@ class ClubManageView(APIView):
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
_EDITABLE_FIELDS = (
|
||||
'name', 'status', 'wx_appid', 'wx_secret', 'mch_id', 'pay_app_id',
|
||||
'mch_key', 'api_v3_key',
|
||||
'name', 'status', 'wx_appid', 'wx_secret',
|
||||
# 收款商户
|
||||
'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',
|
||||
'official_appid', 'official_secret', 'official_token', 'encoding_aes_key',
|
||||
'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix',
|
||||
@@ -485,12 +488,20 @@ class ClubManageView(APIView):
|
||||
warnings = []
|
||||
if not mch_key:
|
||||
warnings.append(
|
||||
'未配置 APIv2 密钥 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,'
|
||||
'未填时将回落服务器 app_secrets 旧密钥,易与当前商户号不匹配导致签名错误。'
|
||||
'未配置收款 APIv2 密钥 mch_key:点单/会员等收款不会使用提现 api_v3_key。'
|
||||
)
|
||||
if mch_key and api_v3 and mch_key == api_v3:
|
||||
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()
|
||||
pay_a = (club.pay_app_id or '').strip()
|
||||
@@ -537,6 +548,7 @@ class ClubManageView(APIView):
|
||||
'oss_prefix': club.oss_prefix,
|
||||
'mch_id': club.mch_id,
|
||||
'pay_app_id': club.pay_app_id,
|
||||
'withdraw_mch_id': getattr(club, 'withdraw_mch_id', '') or '',
|
||||
'goeasy_appkey': club.goeasy_appkey,
|
||||
'template_id': club.template_id,
|
||||
'sort_order': club.sort_order,
|
||||
@@ -546,6 +558,8 @@ class ClubManageView(APIView):
|
||||
base.update({
|
||||
'wx_secret': club.wx_secret,
|
||||
'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,
|
||||
'pay_config': self._pay_config_meta(club),
|
||||
'cert_serial_no': club.cert_serial_no,
|
||||
@@ -638,6 +652,69 @@ class ClubManageView(APIView):
|
||||
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):
|
||||
"""
|
||||
俱乐部维度财务(新接口)。
|
||||
|
||||
Reference in New Issue
Block a user