fix: 小程序支付 AppID 与登录一致,登录覆盖过期 openid 绑定

- JSAPI 支付优先 club.wx_appid(与 jscode2session 一致,不再优先 pay_app_id)
- 登录时同一用户+俱乐部只保留最新 openid,清除旧绑定
- 支付仅读 user_wx_openid,彻底禁用 legacy OpenID/UserName 回落

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 20:52:56 +08:00
parent f601541eb0
commit 5d3e4e32a2
3 changed files with 84 additions and 36 deletions

View File

@@ -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(
# 同一用户在同一俱乐部只保留当前 openidAppID 变更/重新登录须覆盖旧绑定)
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

View File

@@ -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

View File

@@ -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,