appid 已正确读 club 表,但 openid 在无 xq 绑定时会错误回落 UserName/OpenID(可能是星之界 openid),导致 appid 与 openid 不匹配。 Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
8.3 KiB
Python
259 lines
8.3 KiB
Python
"""用户所属俱乐部解析。"""
|
||
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
|