Files
Django/jituan/services/club_user.py
XingQue 017d176790 fix: 星之界/星阙支付按俱乐部分别取 openid 与 AppID,启动静默刷新绑定
- resolve_club_payment_openid: xzj 回落 UserName 并 lazy 写绑定;xq 回落 OpenID;禁止跨俱乐部混用
- 统一 get_club_miniapp_appid;非 xq 禁止回落全局 AppID
- 新增 /jituan/auth/refresh-wx-bind;小程序有 token 时静默刷新 openid

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

306 lines
10 KiB
Python
Raw Permalink 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 resolve_club_payment_openid(user, club_id=None):
"""
支付专用:按俱乐部取 openid与 club 表 miniapp AppID 对应。
1) user_wx_openid(yonghuid, club_id)
2) 无本 club 绑定但有其他 club → 拒绝(须在本小程序重新登录)
3) 无任何绑定时 lazy backfillxq→User.OpenIDxzj 等→UserName(openid)
"""
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.strip(), f'user_wx_openid#{binding.id}'
other_clubs = list(
UserWxOpenid.query.filter(yonghuid=uid)
.exclude(club_id=cid)
.values_list('club_id', flat=True)[:5]
)
if other_clubs:
logger.warning(
'[PAY_OPENID] uid=%s 无 club=%s 绑定,已有其他俱乐部 %s;须在本小程序重新登录',
uid, cid, other_clubs,
)
return '', 'other_club_only'
username = (getattr(user, 'UserName', None) or '').strip()
legacy_oid = (getattr(user, 'OpenID', None) or getattr(user, 'openid', None) or '').strip()
candidate = ''
source = ''
if cid == CLUB_ID_DEFAULT:
if legacy_oid and _looks_like_wx_openid(legacy_oid):
candidate, source = legacy_oid, 'legacy_OpenID'
elif _looks_like_wx_openid(username):
candidate, source = username, 'legacy_UserName'
if candidate:
logger.info('[PAY_OPENID] uid=%s club=%s lazy backfill from %s', uid, cid, source)
ensure_wx_openid_binding(cid, candidate, user)
return candidate, source
logger.warning(
'[PAY_OPENID] uid=%s club=%s 无绑定 legacy_UserName=%s… legacy_OpenID=%s',
uid, cid, username[:10] if username else '', bool(legacy_oid),
)
return '', 'missing'
def get_wx_openid_for_user(user, club_id=None):
openid, _ = resolve_club_payment_openid(user, club_id)
return openid
def get_wx_openid_for_user_with_source(user, club_id=None):
return resolve_club_payment_openid(user, club_id)
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 openid:
logger.info('[PAY_OPENID] uid=%s club=%s openid=%s… source=%s',
getattr(user, 'UserUID', ''), cid,
openid[:6] + '...' if len(openid) > 10 else openid, source)
elif 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