- 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>
306 lines
10 KiB
Python
306 lines
10 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 and binding.yonghuid == uid:
|
||
return binding
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
# 同一用户在同一俱乐部只保留当前 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,
|
||
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 backfill:xq→User.OpenID;xzj 等→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
|