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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user