feat: club 表独立 mch_key 字段,后台明确区分 APIv2 收款与 APIv3 提现
- 新增 club.mch_key 列并迁移 config_json 历史数据,api_v3_key 保持独立 - club-manage 读写 mch_key;支付读 club.mch_key 优先,兼容 config_json 回落 - 提现查单返回商户号改读俱乐部 V3 配置;不影响其他业务逻辑 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,6 +43,7 @@ class Command(BaseCommand):
|
|||||||
'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''),
|
'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''),
|
||||||
'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''),
|
'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''),
|
||||||
'pay_app_id': wx_v3.get('APPID', '') or getattr(settings, 'WEIXIN_APPID', ''),
|
'pay_app_id': wx_v3.get('APPID', '') or getattr(settings, 'WEIXIN_APPID', ''),
|
||||||
|
'mch_key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''),
|
||||||
'api_v3_key': wx_v3.get('API_V3_KEY', ''),
|
'api_v3_key': wx_v3.get('API_V3_KEY', ''),
|
||||||
'cert_serial_no': wx_v3.get('CERT_SERIAL_NO', ''),
|
'cert_serial_no': wx_v3.get('CERT_SERIAL_NO', ''),
|
||||||
'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''),
|
'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''),
|
||||||
@@ -75,7 +76,7 @@ class Command(BaseCommand):
|
|||||||
if v and not getattr(club, k, None):
|
if v and not getattr(club, k, None):
|
||||||
setattr(club, k, v)
|
setattr(club, k, v)
|
||||||
# V3 字段允许用 settings 补全(证书路径等)
|
# V3 字段允许用 settings 补全(证书路径等)
|
||||||
for field in ('api_v3_key', 'cert_serial_no', 'private_key_path', 'platform_cert_dir', 'pay_app_id'):
|
for field in ('mch_key', 'api_v3_key', 'cert_serial_no', 'private_key_path', 'platform_cert_dir', 'pay_app_id'):
|
||||||
val = defaults.get(field)
|
val = defaults.get(field)
|
||||||
if val and not getattr(club, field, None):
|
if val and not getattr(club, field, None):
|
||||||
setattr(club, field, val)
|
setattr(club, field, val)
|
||||||
|
|||||||
33
jituan/migrations/0012_club_mch_key.py
Normal file
33
jituan/migrations/0012_club_mch_key.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""club 表增加 mch_key 列(APIv2 小程序支付密钥),从 config_json 迁移历史数据。"""
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_mch_key_from_config_json(apps, schema_editor):
|
||||||
|
Club = apps.get_model('jituan', 'Club')
|
||||||
|
for club in Club.objects.all():
|
||||||
|
cfg = club.config_json or {}
|
||||||
|
legacy = (cfg.get('mch_key') or cfg.get('shanghumiyao') or '').strip()
|
||||||
|
if legacy and not (club.mch_key or '').strip():
|
||||||
|
club.mch_key = legacy
|
||||||
|
club.save(update_fields=['mch_key'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0011_club_payment_channel'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='mch_key',
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default='',
|
||||||
|
max_length=128,
|
||||||
|
verbose_name='APIv2商户密钥',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(copy_mch_key_from_config_json, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -15,7 +15,8 @@ class Club(QModel):
|
|||||||
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='')
|
mch_id = models.CharField(max_length=32, blank=True, default='')
|
||||||
pay_app_id = models.CharField(max_length=32, blank=True, default='')
|
pay_app_id = models.CharField(max_length=32, blank=True, default='')
|
||||||
api_v3_key = models.CharField(max_length=128, 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='')
|
cert_serial_no = models.CharField(max_length=64, blank=True, default='')
|
||||||
private_key_path = models.CharField(max_length=255, 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='')
|
platform_cert_dir = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ def _xml_text(root, tag, default=''):
|
|||||||
return node.text
|
return node.text
|
||||||
|
|
||||||
|
|
||||||
def _resolve_v2_mch_key(cfg_json, fallback_key):
|
def _resolve_v2_mch_key(club, cfg_json, fallback_key):
|
||||||
"""V2 JSAPI 统一下单用的商户 API 密钥(32 位 MD5 密钥,不是 api_v3_key)。"""
|
"""V2 JSAPI 统一下单用的商户 API 密钥(32 位 MD5 密钥,不是 api_v3_key)。"""
|
||||||
|
column_key = (getattr(club, 'mch_key', None) or '').strip() if club else ''
|
||||||
|
if column_key:
|
||||||
|
return column_key, 'club.mch_key'
|
||||||
cfg_json = cfg_json or {}
|
cfg_json = cfg_json or {}
|
||||||
for field in ('mch_key', 'shanghumiyao', 'WEIXIN_SHANGHUMIYAO'):
|
for field in ('mch_key', 'shanghumiyao', 'WEIXIN_SHANGHUMIYAO'):
|
||||||
val = (cfg_json.get(field) or '').strip()
|
val = (cfg_json.get(field) or '').strip()
|
||||||
@@ -51,7 +54,7 @@ def get_wechat_v2_config(club_id=None):
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
cfg_json = club.config_json or {}
|
cfg_json = club.config_json or {}
|
||||||
mch_key, key_source = _resolve_v2_mch_key(cfg_json, fallback['key'])
|
mch_key, key_source = _resolve_v2_mch_key(club, cfg_json, fallback['key'])
|
||||||
mini_appid = get_club_miniapp_appid(club)
|
mini_appid = get_club_miniapp_appid(club)
|
||||||
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()
|
||||||
|
|||||||
@@ -450,31 +450,47 @@ class ClubManageView(APIView):
|
|||||||
parser_classes = [JSONParser]
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
_EDITABLE_FIELDS = (
|
_EDITABLE_FIELDS = (
|
||||||
'name', 'status', 'wx_appid', 'wx_secret', 'mch_id', 'pay_app_id', 'api_v3_key',
|
'name', 'status', 'wx_appid', 'wx_secret', 'mch_id', 'pay_app_id',
|
||||||
|
'mch_key', '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',
|
||||||
'goeasy_appkey', 'goeasy_secret', 'sort_order',
|
'goeasy_appkey', 'goeasy_secret', 'sort_order',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _effective_mch_key(club):
|
||||||
|
cfg_json = club.config_json or {}
|
||||||
|
return (
|
||||||
|
(club.mch_key or '').strip()
|
||||||
|
or (cfg_json.get('mch_key') or '').strip()
|
||||||
|
or (cfg_json.get('shanghumiyao') or '').strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sync_mch_key_to_config_json(club, mch_key):
|
||||||
|
cfg = dict(club.config_json or {})
|
||||||
|
cfg['mch_key'] = (mch_key or '').strip()
|
||||||
|
club.config_json = cfg
|
||||||
|
|
||||||
def _pay_config_meta(self, club):
|
def _pay_config_meta(self, club):
|
||||||
"""明确区分:小程序 V2 支付密钥 vs APIv3 密钥,避免后台误导。"""
|
"""明确区分:小程序 V2 支付密钥 vs APIv3 密钥,避免后台误导。"""
|
||||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||||
|
|
||||||
cfg_json = club.config_json or {}
|
cfg_json = club.config_json or {}
|
||||||
mch_key = (cfg_json.get('mch_key') or cfg_json.get('shanghumiyao') or '').strip()
|
mch_key = self._effective_mch_key(club)
|
||||||
api_v3 = (club.api_v3_key or '').strip()
|
api_v3 = (club.api_v3_key or '').strip()
|
||||||
v2_cfg = get_wechat_v2_config(club.club_id)
|
v2_cfg = get_wechat_v2_config(club.club_id)
|
||||||
|
|
||||||
warnings = []
|
warnings = []
|
||||||
if not mch_key:
|
if not mch_key:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
'未配置 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,'
|
'未配置 APIv2 密钥 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,'
|
||||||
'未填时将回落服务器 app_secrets 旧密钥,易与当前商户号不匹配导致签名错误。'
|
'未填时将回落服务器 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 内容相同:通常不正确,V2(小程序支付)与 V3(提现转账)是两套不同密钥。'
|
||||||
)
|
)
|
||||||
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()
|
||||||
@@ -490,13 +506,17 @@ class ClubManageView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'mch_key': mch_key,
|
||||||
|
'mch_key_label': 'APIv2 密钥 (mch_key)',
|
||||||
|
'mch_key_help': '微信商户平台 → API安全 → 设置APIv2密钥;用于会员/点单/押金/积分等 JSAPI 收款',
|
||||||
|
'mch_key_configured': bool(mch_key),
|
||||||
'miniapp_wechat_v2_key': mch_key,
|
'miniapp_wechat_v2_key': mch_key,
|
||||||
'miniapp_wechat_v2_key_configured': bool(mch_key),
|
'miniapp_wechat_v2_key_configured': bool(mch_key),
|
||||||
'miniapp_wechat_v2_key_label': '小程序支付 APIv2 密钥 (mch_key)',
|
'miniapp_wechat_v2_key_label': 'APIv2 密钥 (mch_key)',
|
||||||
'miniapp_wechat_v2_key_help': '微信商户平台 → API安全 → 设置APIv2密钥;用于会员/点单/押金/积分等 JSAPI 支付',
|
'miniapp_wechat_v2_key_help': '微信商户平台 → API安全 → 设置APIv2密钥;用于会员/点单/押金/积分等 JSAPI 收款',
|
||||||
'api_v3_key': api_v3,
|
'api_v3_key': api_v3,
|
||||||
'api_v3_key_label': 'APIv3 密钥 (api_v3_key)',
|
'api_v3_key_label': 'APIv3 密钥 (api_v3_key)',
|
||||||
'api_v3_key_help': '仅用于微信 V3 接口(如转账到零钱),不用于小程序统一下单',
|
'api_v3_key_help': '微信商户平台 → API安全 → 设置APIv3密钥;仅用于自动提现/转账回调解密,不用于小程序收款',
|
||||||
'effective_miniapp_pay': {
|
'effective_miniapp_pay': {
|
||||||
'club_id': v2_cfg.get('club_id'),
|
'club_id': v2_cfg.get('club_id'),
|
||||||
'appid': v2_cfg.get('appid'),
|
'appid': v2_cfg.get('appid'),
|
||||||
@@ -522,13 +542,11 @@ class ClubManageView(APIView):
|
|||||||
'sort_order': club.sort_order,
|
'sort_order': club.sort_order,
|
||||||
}
|
}
|
||||||
if full:
|
if full:
|
||||||
cfg_json = club.config_json or {}
|
mch_key = self._effective_mch_key(club)
|
||||||
mch_key = (cfg_json.get('mch_key') or cfg_json.get('shanghumiyao') or '').strip()
|
|
||||||
base.update({
|
base.update({
|
||||||
'wx_secret': club.wx_secret,
|
'wx_secret': club.wx_secret,
|
||||||
'api_v3_key': club.api_v3_key,
|
|
||||||
# 小程序 JSAPI 支付实际使用的 V2 密钥(与 api_v3_key 不是同一个)
|
|
||||||
'mch_key': mch_key,
|
'mch_key': mch_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,
|
||||||
'private_key_path': club.private_key_path,
|
'private_key_path': club.private_key_path,
|
||||||
@@ -576,12 +594,13 @@ class ClubManageView(APIView):
|
|||||||
update_fields = []
|
update_fields = []
|
||||||
for field in self._EDITABLE_FIELDS:
|
for field in self._EDITABLE_FIELDS:
|
||||||
if field in request.data:
|
if field in request.data:
|
||||||
setattr(club, field, request.data[field])
|
val = request.data[field]
|
||||||
|
if field in ('mch_key', 'api_v3_key', 'wx_secret'):
|
||||||
|
val = (val or '').strip()
|
||||||
|
setattr(club, field, val)
|
||||||
update_fields.append(field)
|
update_fields.append(field)
|
||||||
if 'mch_key' in request.data:
|
if 'mch_key' in request.data:
|
||||||
cfg = dict(club.config_json or {})
|
self._sync_mch_key_to_config_json(club, request.data.get('mch_key'))
|
||||||
cfg['mch_key'] = (request.data.get('mch_key') or '').strip()
|
|
||||||
club.config_json = cfg
|
|
||||||
if 'config_json' not in update_fields:
|
if 'config_json' not in update_fields:
|
||||||
update_fields.append('config_json')
|
update_fields.append('config_json')
|
||||||
if update_fields:
|
if update_fields:
|
||||||
|
|||||||
@@ -1177,7 +1177,8 @@ def _sync_record_from_wx_query(auto_record, wx_data):
|
|||||||
if auto_record.zhuangtai == 0:
|
if auto_record.zhuangtai == 0:
|
||||||
auto_record.wechat_package = json.dumps(package, ensure_ascii=False)
|
auto_record.wechat_package = json.dumps(package, ensure_ascii=False)
|
||||||
auto_record.save(update_fields=['wechat_package', 'UpdateTime'])
|
auto_record.save(update_fields=['wechat_package', 'UpdateTime'])
|
||||||
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
|
from jituan.services.wechat_pay import get_wechat_v3_config, club_id_for_tixian_yonghuid
|
||||||
|
wx_cfg = get_wechat_v3_config(club_id_for_tixian_yonghuid(auto_record.yonghuid))
|
||||||
return RECONCILE_WAIT_CONFIRM, {
|
return RECONCILE_WAIT_CONFIRM, {
|
||||||
'tixian_id': auto_record.tixian_id,
|
'tixian_id': auto_record.tixian_id,
|
||||||
'package_info': package,
|
'package_info': package,
|
||||||
|
|||||||
Reference in New Issue
Block a user