From 49ad57bbb2e0fa15125c8a322428408e87751351 Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 10 Jul 2026 21:34:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20club=20=E8=A1=A8=E7=8B=AC=E7=AB=8B=20mc?= =?UTF-8?q?h=5Fkey=20=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E6=98=8E=E7=A1=AE=E5=8C=BA=E5=88=86=20APIv2=20=E6=94=B6?= =?UTF-8?q?=E6=AC=BE=E4=B8=8E=20APIv3=20=E6=8F=90=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 club.mch_key 列并迁移 config_json 历史数据,api_v3_key 保持独立 - club-manage 读写 mch_key;支付读 club.mch_key 优先,兼容 config_json 回落 - 提现查单返回商户号改读俱乐部 V3 配置;不影响其他业务逻辑 Co-authored-by: Cursor --- jituan/management/commands/seed_club_xq.py | 3 +- jituan/migrations/0012_club_mch_key.py | 33 +++++++++++++++ jituan/models.py | 3 +- jituan/services/wechat_pay.py | 7 +++- jituan/views.py | 49 +++++++++++++++------- users/tixian_shenhe_services.py | 3 +- 6 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 jituan/migrations/0012_club_mch_key.py diff --git a/jituan/management/commands/seed_club_xq.py b/jituan/management/commands/seed_club_xq.py index adf2a3b..d9544f0 100644 --- a/jituan/management/commands/seed_club_xq.py +++ b/jituan/management/commands/seed_club_xq.py @@ -43,6 +43,7 @@ class Command(BaseCommand): 'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''), 'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''), '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', ''), 'cert_serial_no': wx_v3.get('CERT_SERIAL_NO', ''), 'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''), @@ -75,7 +76,7 @@ class Command(BaseCommand): if v and not getattr(club, k, None): setattr(club, k, v) # 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) if val and not getattr(club, field, None): setattr(club, field, val) diff --git a/jituan/migrations/0012_club_mch_key.py b/jituan/migrations/0012_club_mch_key.py new file mode 100644 index 0000000..3b1c414 --- /dev/null +++ b/jituan/migrations/0012_club_mch_key.py @@ -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), + ] diff --git a/jituan/models.py b/jituan/models.py index 09151fa..a116cdd 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -15,7 +15,8 @@ class Club(QModel): 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='') - 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='') private_key_path = models.CharField(max_length=255, blank=True, default='') platform_cert_dir = models.CharField(max_length=255, blank=True, default='') diff --git a/jituan/services/wechat_pay.py b/jituan/services/wechat_pay.py index 418def2..b7c9780 100644 --- a/jituan/services/wechat_pay.py +++ b/jituan/services/wechat_pay.py @@ -18,8 +18,11 @@ def _xml_text(root, tag, default=''): 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)。""" + 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 {} for field in ('mch_key', 'shanghumiyao', 'WEIXIN_SHANGHUMIYAO'): val = (cfg_json.get(field) or '').strip() @@ -51,7 +54,7 @@ def get_wechat_v2_config(club_id=None): return fallback 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) wx_a = (club.wx_appid or '').strip() pay_a = (club.pay_app_id or '').strip() diff --git a/jituan/views.py b/jituan/views.py index fe39c17..848ba5c 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -450,31 +450,47 @@ class ClubManageView(APIView): parser_classes = [JSONParser] _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', 'official_appid', 'official_secret', 'official_token', 'encoding_aes_key', 'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix', '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): """明确区分:小程序 V2 支付密钥 vs APIv3 密钥,避免后台误导。""" from jituan.services.wechat_pay import get_wechat_v2_config 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() v2_cfg = get_wechat_v2_config(club.club_id) warnings = [] if not mch_key: warnings.append( - '未配置 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,' + '未配置 APIv2 密钥 mch_key:小程序/会员/押金/点单等微信支付不会使用 api_v3_key,' '未填时将回落服务器 app_secrets 旧密钥,易与当前商户号不匹配导致签名错误。' ) 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 内容相同:通常不正确,V2(小程序支付)与 V3(提现转账)是两套不同密钥。' ) wx_a = (club.wx_appid or '').strip() pay_a = (club.pay_app_id or '').strip() @@ -490,13 +506,17 @@ class ClubManageView(APIView): ) 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_configured': bool(mch_key), - 'miniapp_wechat_v2_key_label': '小程序支付 APIv2 密钥 (mch_key)', - 'miniapp_wechat_v2_key_help': '微信商户平台 → API安全 → 设置APIv2密钥;用于会员/点单/押金/积分等 JSAPI 支付', + 'miniapp_wechat_v2_key_label': 'APIv2 密钥 (mch_key)', + 'miniapp_wechat_v2_key_help': '微信商户平台 → API安全 → 设置APIv2密钥;用于会员/点单/押金/积分等 JSAPI 收款', 'api_v3_key': api_v3, 'api_v3_key_label': 'APIv3 密钥 (api_v3_key)', - 'api_v3_key_help': '仅用于微信 V3 接口(如转账到零钱),不用于小程序统一下单', + 'api_v3_key_help': '微信商户平台 → API安全 → 设置APIv3密钥;仅用于自动提现/转账回调解密,不用于小程序收款', 'effective_miniapp_pay': { 'club_id': v2_cfg.get('club_id'), 'appid': v2_cfg.get('appid'), @@ -522,13 +542,11 @@ class ClubManageView(APIView): 'sort_order': club.sort_order, } if full: - 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) base.update({ 'wx_secret': club.wx_secret, - 'api_v3_key': club.api_v3_key, - # 小程序 JSAPI 支付实际使用的 V2 密钥(与 api_v3_key 不是同一个) 'mch_key': mch_key, + 'api_v3_key': club.api_v3_key, 'pay_config': self._pay_config_meta(club), 'cert_serial_no': club.cert_serial_no, 'private_key_path': club.private_key_path, @@ -576,12 +594,13 @@ class ClubManageView(APIView): update_fields = [] for field in self._EDITABLE_FIELDS: 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) if 'mch_key' in request.data: - cfg = dict(club.config_json or {}) - cfg['mch_key'] = (request.data.get('mch_key') or '').strip() - club.config_json = cfg + self._sync_mch_key_to_config_json(club, request.data.get('mch_key')) if 'config_json' not in update_fields: update_fields.append('config_json') if update_fields: diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index 753493e..d3587a1 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -1177,7 +1177,8 @@ def _sync_record_from_wx_query(auto_record, wx_data): if auto_record.zhuangtai == 0: auto_record.wechat_package = json.dumps(package, ensure_ascii=False) 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, { 'tixian_id': auto_record.tixian_id, 'package_info': package,