Files
Django/jituan/services/club_user.py
XingQue 5d3e4e32a2 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>
2026-07-10 20:52:56 +08:00

282 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""用户所属俱乐部解析。"""
import logging
from django.db import IntegrityError, transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import UserWxOpenid
logger = logging.getLogger(__name__)
def resolve_invite_party_club(user, request_club_id=None):
"""
邀请链校验用:只返回已落库的俱乐部归属。
未归属任何俱乐部时返回 None勿默认 xq避免星之界扫码误拦
"""
if not user:
return None
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
req = (request_club_id or '').strip()
# 当前小程序 openid 绑定优先ClubID 字段曾默认 xq会误判新用户
if req and uid:
if UserWxOpenid.query.filter(yonghuid=uid, club_id=req).exists():
return req
explicit = (getattr(user, 'ClubID', None) or '').strip()
if explicit:
if explicit == CLUB_ID_DEFAULT and uid:
has_real_xq = (
UserWxOpenid.query.filter(yonghuid=uid, club_id=CLUB_ID_DEFAULT).exists()
or bool(getattr(user, 'OpenID', None))
)
if not has_real_xq:
explicit = ''
if explicit:
return explicit
if uid:
binding = UserWxOpenid.query.filter(yonghuid=uid).order_by('id').first()
if binding:
return binding.club_id
return None
def _user_querysets():
"""兼容 gvsdsdk User.query 与 Django User.objects。"""
from users.business_models import User
seen = set()
for qs in (getattr(User, 'objects', None), getattr(User, 'query', None)):
if qs is None or id(qs) in seen:
continue
seen.add(id(qs))
yield qs
def find_user_by_wx_openid(club_id, openid):
"""按俱乐部 openid 绑定或 legacy UserName/OpenID 定位用户。"""
from users.business_models import User
openid = (openid or '').strip()
club_id = (club_id or '').strip()
if not openid:
return None
def _user_by_uid(uid):
if not uid:
return None
user = User.objects.filter(UserUID=uid).first()
if not user:
user = User.query.filter(UserUID=uid).first()
return user
# 已有 openid 绑定(任意俱乐部)优先,避免重复注册
binding_any = UserWxOpenid.query.filter(openid=openid).order_by('id').first()
if binding_any:
user = _user_by_uid(binding_any.yonghuid)
if user:
return user
if club_id:
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
if binding:
user = _user_by_uid(binding.yonghuid)
if user:
return user
# 星之界等用户常 UserName=openid 且 OpenID 为空
for manager in _user_querysets():
user = manager.filter(UserName=openid).first()
if user:
return user
user = manager.filter(OpenID=openid).first()
if user:
return user
return None
def ensure_wx_openid_binding(club_id, openid, user, unionid=''):
if not club_id or not openid or not user:
return None
uid = getattr(user, 'UserUID', None)
if not uid:
return None
club_id = (club_id or '').strip()
openid = (openid or '').strip()
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
if binding and binding.yonghuid == uid:
return binding
try:
with transaction.atomic():
# 同一用户在同一俱乐部只保留当前 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()
def resolve_or_create_wx_user(club_id, openid, generate_uid, unionid=''):
"""
微信登录/注册统一找用户,避免 UserName(openid) 唯一键冲突。
星之界等俱乐部用户常 UserName=openid 且 OpenID 为空,不可 get_or_create(OpenID=...)。
返回 (user, created)。
"""
import uuid
from users.business_models import User
openid = (openid or '').strip()
club_id = (club_id or '').strip()
if not openid:
raise ValueError('openid 不能为空')
user = find_user_by_wx_openid(club_id, openid)
if user:
ensure_wx_openid_binding(club_id, openid, user, unionid)
return user, False
try:
with transaction.atomic():
user = User(
UserUUID=uuid.uuid4().bytes,
UserUID=generate_uid(),
UserName=openid,
OpenID=openid if club_id == CLUB_ID_DEFAULT else None,
UnionID=unionid or None,
ClubID='',
)
user.save()
except IntegrityError:
user = find_user_by_wx_openid(club_id, openid)
if not user:
raise
ensure_wx_openid_binding(club_id, openid, user, unionid)
return user, False
ensure_wx_openid_binding(club_id, openid, user, unionid)
return user, True
def get_user_club_id(user):
"""
解析用户归属 club注册/邀请链隔离用)。
优先级User.ClubID → user_wx_openid → 默认 xq。
"""
if not user:
return CLUB_ID_DEFAULT
club_id = getattr(user, 'ClubID', None) or getattr(user, 'club_id', None)
if club_id:
return club_id
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
if uid:
binding = UserWxOpenid.query.filter(yonghuid=uid).order_by('id').first()
if binding:
return binding.club_id
return CLUB_ID_DEFAULT
def ensure_user_club_id(user, club_id):
"""登录/注册后确保 User.ClubID 与绑定 club 一致。"""
if not user or not club_id:
return
current = getattr(user, 'ClubID', None)
if current != club_id and hasattr(user, 'ClubID'):
user.ClubID = club_id
user.save(update_fields=['ClubID'])
def _looks_like_wx_openid(value):
"""微信 openid 常见形态:以 o 开头、长度约 28。"""
v = (value or '').strip()
return len(v) >= 20 and v.startswith('o')
def get_wx_openid_for_user(user, club_id=None):
"""
按俱乐部取微信支付 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 not uid or not cid:
return ''
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, 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 source=%s user_club=%s',
uid, cid, source, get_user_club_id(user),
)
return openid, cid