From 5d3e4e32a23fa8e32b94ea4018c2ef0b636088df Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 10 Jul 2026 20:52:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=94=AF?= =?UTF-8?q?=E4=BB=98=20AppID=20=E4=B8=8E=E7=99=BB=E5=BD=95=E4=B8=80?= =?UTF-8?q?=E8=87=B4=EF=BC=8C=E7=99=BB=E5=BD=95=E8=A6=86=E7=9B=96=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=20openid=20=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JSAPI 支付优先 club.wx_appid(与 jscode2session 一致,不再优先 pay_app_id) - 登录时同一用户+俱乐部只保留最新 openid,清除旧绑定 - 支付仅读 user_wx_openid,彻底禁用 legacy OpenID/UserName 回落 Co-authored-by: Cursor --- jituan/services/club_user.py | 91 ++++++++++++++++++++++------------- jituan/services/wechat_pay.py | 17 ++++++- jituan/views.py | 12 +++++ 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/jituan/services/club_user.py b/jituan/services/club_user.py index c62f9b0..fc72c63 100644 --- a/jituan/services/club_user.py +++ b/jituan/services/club_user.py @@ -110,18 +110,35 @@ def ensure_wx_openid_binding(club_id, openid, user, unionid=''): openid = (openid or '').strip() binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first() - if binding: + if binding and binding.yonghuid == uid: return binding try: - # 独立 savepoint:并发/重复绑定时不污染外层 transaction.atomic with transaction.atomic(): - return UserWxOpenid.query.create( + # 同一用户在同一俱乐部只保留当前 openid(AppID 变更/重新登录须覆盖旧绑定) + stale = UserWxOpenid.query.filter(yonghuid=uid, club_id=club_id).exclude(openid=openid) + stale_count = stale.count() + if stale_count: + logger.info( + '[WX_OPENID_BIND] uid=%s club=%s 清除 %d 条过期 openid 绑定', + uid, club_id, stale_count, + ) + stale.delete() + + binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first() + if binding: + return binding + + row = UserWxOpenid.query.create( club_id=club_id, openid=openid, yonghuid=uid, unionid=unionid or getattr(user, 'UnionID', None) or '', ) + if club_id == CLUB_ID_DEFAULT and getattr(user, 'OpenID', None) != openid: + user.OpenID = openid + user.save(update_fields=['OpenID']) + return row except IntegrityError: return UserWxOpenid.query.filter(club_id=club_id, openid=openid).first() @@ -206,53 +223,59 @@ def _looks_like_wx_openid(value): def get_wx_openid_for_user(user, club_id=None): """ - 按俱乐部取微信支付 JSAPI 用 openid(须与 club.pay_app_id 对应)。 - 优先 user_wx_openid(yonghuid, club_id);禁止回落到其他俱乐部的 openid。 - 仅当目标俱乐部为 xq 且用户无任何 user_wx_openid 记录时,才回落 legacy User.OpenID。 + 按俱乐部取微信支付 JSAPI 用 openid(须与 club.wx_appid 登录时换取的一致)。 + 仅读 user_wx_openid;禁止回落 User.OpenID/UserName(多俱乐部/旧 AppID 会错配)。 """ if not user: return '' cid = (club_id or '').strip() or get_user_club_id(user) uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None) - if uid and cid: - binding = UserWxOpenid.query.filter(yonghuid=uid, club_id=cid).first() - if binding and binding.openid: - return binding.openid - other_bindings = list( - UserWxOpenid.query.filter(yonghuid=uid) - .exclude(club_id=cid) - .values_list('club_id', flat=True)[:5] - ) - if other_bindings: - logger.warning( - '[PAY_OPENID] uid=%s 在俱乐部 %s 无 openid 绑定,但已有其他俱乐部绑定 %s;' - '禁止跨小程序回落 legacy openid,请在本小程序重新登录', - uid, cid, other_bindings, - ) - return '' - - if cid != CLUB_ID_DEFAULT: + if not uid or not cid: return '' - - legacy = getattr(user, 'OpenID', None) or getattr(user, 'openid', None) - if legacy: - return legacy.strip() - username = (getattr(user, 'UserName', None) or '').strip() - if _looks_like_wx_openid(username): - return username + binding = ( + UserWxOpenid.query.filter(yonghuid=uid, club_id=cid) + .order_by('-id') + .first() + ) + if binding and binding.openid: + return binding.openid return '' +def get_wx_openid_for_user_with_source(user, club_id=None): + """返回 (openid, source) 供支付诊断。""" + if not user: + return '', 'none' + cid = (club_id or '').strip() or get_user_club_id(user) + uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None) + if not uid or not cid: + return '', 'none' + binding = ( + UserWxOpenid.query.filter(yonghuid=uid, club_id=cid) + .order_by('-id') + .first() + ) + if binding and binding.openid: + return binding.openid, f'user_wx_openid#{binding.id}' + legacy = (getattr(user, 'OpenID', None) or '').strip() + username = (getattr(user, 'UserName', None) or '').strip() + logger.warning( + '[PAY_OPENID] uid=%s club=%s 无 user_wx_openid 绑定 legacy_OpenID=%s legacy_UserName=%s…', + uid, cid, bool(legacy), username[:8] if username else '', + ) + return '', 'missing_binding' + + def get_payment_openid(request, user=None, club_id=None): """下单/充值:按请求俱乐部解析 openid。""" from jituan.services.club_write import resolve_club_id_for_write user = user or getattr(request, 'user', None) cid = (club_id or '').strip() or resolve_club_id_for_write(request, user) - openid = get_wx_openid_for_user(user, cid) + openid, source = get_wx_openid_for_user_with_source(user, cid) if not openid and user: uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None) logger.warning( - '[PAY_OPENID] 无法解析支付 openid uid=%s club=%s user_club=%s', - uid, cid, get_user_club_id(user), + '[PAY_OPENID] 无法解析支付 openid uid=%s club=%s source=%s user_club=%s', + uid, cid, source, get_user_club_id(user), ) return openid, cid diff --git a/jituan/services/wechat_pay.py b/jituan/services/wechat_pay.py index 3873611..b461dee 100644 --- a/jituan/services/wechat_pay.py +++ b/jituan/services/wechat_pay.py @@ -51,7 +51,16 @@ def get_wechat_v2_config(club_id=None): cfg_json = club.config_json or {} mch_key, key_source = _resolve_v2_mch_key(cfg_json, fallback['key']) - appid = club.pay_app_id or club.wx_appid or fallback['appid'] + mini_appid = (club.wx_appid or '').strip() + pay_appid = (club.pay_app_id or '').strip() + if mini_appid and pay_appid and mini_appid != pay_appid: + logger.warning( + '[WX_V2_CONFIG] club=%s wx_appid(%s) 与 pay_app_id(%s) 不一致;' + '小程序 JSAPI 支付以 wx_appid 为准(openid 与登录 AppID 绑定)', + cid, mini_appid, pay_appid, + ) + # openid 由 jscode2session(club.wx_appid) 换取,支付 AppID 须与登录一致 + appid = mini_appid or pay_appid or fallback['appid'] mch_id = club.mch_id or fallback['mch_id'] if mch_id and key_source == 'settings.WEIXIN_SHANGHUMIYAO': @@ -204,7 +213,11 @@ def get_wechat_v3_config(club_id=None): cfg_json = club.config_json or {} return { - 'APPID': club.pay_app_id or club.wx_appid or fallback['APPID'], + 'APPID': ( + (club.wx_appid or '').strip() + or (club.pay_app_id or '').strip() + or fallback['APPID'] + ), 'MCHID': club.mch_id or fallback['MCHID'], 'PRIVATE_KEY_PATH': ( club.private_key_path diff --git a/jituan/views.py b/jituan/views.py index 0630833..cb2053e 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -447,6 +447,18 @@ class ClubManageView(APIView): warnings.append( 'mch_key 与 api_v3_key 内容相同:通常不正确,V2(小程序支付)与 V3(转账)是两套不同密钥。' ) + wx_a = (club.wx_appid or '').strip() + pay_a = (club.pay_app_id or '').strip() + if wx_a and pay_a and wx_a != pay_a: + warnings.append( + f'wx_appid({wx_a}) 与 pay_app_id({pay_a}) 不一致:' + '小程序登录/支付 openid 与 wx_appid 绑定,请保持一致或留空 pay_app_id。' + ) + if not wx_a and pay_a: + warnings.append( + f'wx_appid 为空但 pay_app_id={pay_a}:登录仍可能回落旧全局 AppID,' + '导致 openid 与支付 AppID 不匹配,请填写 wx_appid。' + ) return { 'miniapp_wechat_v2_key': mch_key,