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()
|
openid = (openid or '').strip()
|
||||||
|
|
||||||
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
|
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
|
||||||
if binding:
|
if binding and binding.yonghuid == uid:
|
||||||
return binding
|
return binding
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 独立 savepoint:并发/重复绑定时不污染外层 transaction.atomic
|
|
||||||
with 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,
|
club_id=club_id,
|
||||||
openid=openid,
|
openid=openid,
|
||||||
yonghuid=uid,
|
yonghuid=uid,
|
||||||
unionid=unionid or getattr(user, 'UnionID', None) or '',
|
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:
|
except IntegrityError:
|
||||||
return UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
|
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):
|
def get_wx_openid_for_user(user, club_id=None):
|
||||||
"""
|
"""
|
||||||
按俱乐部取微信支付 JSAPI 用 openid(须与 club.pay_app_id 对应)。
|
按俱乐部取微信支付 JSAPI 用 openid(须与 club.wx_appid 登录时换取的一致)。
|
||||||
优先 user_wx_openid(yonghuid, club_id);禁止回落到其他俱乐部的 openid。
|
仅读 user_wx_openid;禁止回落 User.OpenID/UserName(多俱乐部/旧 AppID 会错配)。
|
||||||
仅当目标俱乐部为 xq 且用户无任何 user_wx_openid 记录时,才回落 legacy User.OpenID。
|
|
||||||
"""
|
"""
|
||||||
if not user:
|
if not user:
|
||||||
return ''
|
return ''
|
||||||
cid = (club_id or '').strip() or get_user_club_id(user)
|
cid = (club_id or '').strip() or get_user_club_id(user)
|
||||||
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
|
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
|
||||||
if uid and cid:
|
if not uid or not 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:
|
|
||||||
return ''
|
return ''
|
||||||
|
binding = (
|
||||||
legacy = getattr(user, 'OpenID', None) or getattr(user, 'openid', None)
|
UserWxOpenid.query.filter(yonghuid=uid, club_id=cid)
|
||||||
if legacy:
|
.order_by('-id')
|
||||||
return legacy.strip()
|
.first()
|
||||||
username = (getattr(user, 'UserName', None) or '').strip()
|
)
|
||||||
if _looks_like_wx_openid(username):
|
if binding and binding.openid:
|
||||||
return username
|
return binding.openid
|
||||||
return ''
|
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):
|
def get_payment_openid(request, user=None, club_id=None):
|
||||||
"""下单/充值:按请求俱乐部解析 openid。"""
|
"""下单/充值:按请求俱乐部解析 openid。"""
|
||||||
from jituan.services.club_write import resolve_club_id_for_write
|
from jituan.services.club_write import resolve_club_id_for_write
|
||||||
user = user or getattr(request, 'user', None)
|
user = user or getattr(request, 'user', None)
|
||||||
cid = (club_id or '').strip() or resolve_club_id_for_write(request, user)
|
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:
|
if not openid and user:
|
||||||
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
|
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'[PAY_OPENID] 无法解析支付 openid uid=%s club=%s user_club=%s',
|
'[PAY_OPENID] 无法解析支付 openid uid=%s club=%s source=%s user_club=%s',
|
||||||
uid, cid, get_user_club_id(user),
|
uid, cid, source, get_user_club_id(user),
|
||||||
)
|
)
|
||||||
return openid, cid
|
return openid, cid
|
||||||
|
|||||||
@@ -51,7 +51,16 @@ def get_wechat_v2_config(club_id=None):
|
|||||||
|
|
||||||
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(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']
|
mch_id = club.mch_id or fallback['mch_id']
|
||||||
|
|
||||||
if mch_id and key_source == 'settings.WEIXIN_SHANGHUMIYAO':
|
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 {}
|
cfg_json = club.config_json or {}
|
||||||
return {
|
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'],
|
'MCHID': club.mch_id or fallback['MCHID'],
|
||||||
'PRIVATE_KEY_PATH': (
|
'PRIVATE_KEY_PATH': (
|
||||||
club.private_key_path
|
club.private_key_path
|
||||||
|
|||||||
@@ -447,6 +447,18 @@ class ClubManageView(APIView):
|
|||||||
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()
|
||||||
|
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 {
|
return {
|
||||||
'miniapp_wechat_v2_key': mch_key,
|
'miniapp_wechat_v2_key': mch_key,
|
||||||
|
|||||||
Reference in New Issue
Block a user