Files
Django/jituan/services/club_user.py
XingQue f601541eb0 fix: 支付 openid 按俱乐部隔离,禁止跨小程序回落 legacy 字段
appid 已正确读 club 表,但 openid 在无 xq 绑定时会错误回落 UserName/OpenID(可能是星之界 openid),导致 appid 与 openid 不匹配。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 20:47:35 +08:00

259 lines
8.3 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:
return binding
try:
# 独立 savepoint并发/重复绑定时不污染外层 transaction.atomic
with transaction.atomic():
return UserWxOpenid.query.create(
club_id=club_id,
openid=openid,
yonghuid=uid,
unionid=unionid or getattr(user, 'UnionID', None) or '',
)
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.pay_app_id 对应)。
优先 user_wx_openid(yonghuid, club_id);禁止回落到其他俱乐部的 openid。
仅当目标俱乐部为 xq 且用户无任何 user_wx_openid 记录时,才回落 legacy User.OpenID。
"""
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:
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
return ''
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)
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),
)
return openid, cid