Files
Django/jituan/services/club_user.py

407 lines
14 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='', wx_appid=''):
"""
绑定 openid。
wx_appid 用于区分小程序 / 服务号;只清理「同 club + 同 wx_appid」的旧绑定
避免 H5 登录冲掉小程序 openid或反过来
"""
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()
wx_appid = (wx_appid or '').strip()
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
if binding and binding.yonghuid == uid:
if wx_appid and getattr(binding, 'wx_appid', '') != wx_appid:
binding.wx_appid = wx_appid
if unionid:
binding.unionid = unionid
binding.save()
return binding
try:
with transaction.atomic():
# 仅清理同一 AppID 下的过期绑定;小程序与服务号可并存
stale_qs = UserWxOpenid.query.filter(yonghuid=uid, club_id=club_id).exclude(openid=openid)
if wx_appid:
stale_qs = stale_qs.filter(wx_appid=wx_appid)
else:
stale_qs = stale_qs.filter(wx_appid='')
stale_count = stale_qs.count()
if stale_count:
logger.info(
'[WX_OPENID_BIND] uid=%s club=%s appid=%s 清除 %d 条过期 openid 绑定',
uid, club_id, wx_appid or '(legacy)', stale_count,
)
stale_qs.delete()
binding = UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
if binding:
if binding.yonghuid != uid:
return binding
if wx_appid and getattr(binding, 'wx_appid', '') != wx_appid:
binding.wx_appid = wx_appid
binding.save(update_fields=['wx_appid'])
return binding
row = UserWxOpenid.query.create(
club_id=club_id,
openid=openid,
yonghuid=uid,
unionid=unionid or getattr(user, 'UnionID', None) or '',
wx_appid=wx_appid,
)
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='', wx_appid=''):
"""
微信登录/注册统一找用户,避免 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()
wx_appid = (wx_appid 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, wx_appid=wx_appid)
return user, False
# 有 unionid 时尽量合并到已有用户(小程序 / 服务号同人)
if unionid:
binding_u = UserWxOpenid.query.filter(unionid=unionid).order_by('id').first()
if binding_u:
user = User.objects.filter(UserUID=binding_u.yonghuid).first()
if not user:
user = User.query.filter(UserUID=binding_u.yonghuid).first()
if user:
ensure_wx_openid_binding(club_id, openid, user, unionid, wx_appid=wx_appid)
return user, False
for manager in _user_querysets():
user = manager.filter(UnionID=unionid).first()
if user:
ensure_wx_openid_binding(club_id, openid, user, unionid, wx_appid=wx_appid)
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, wx_appid=wx_appid)
return user, False
ensure_wx_openid_binding(club_id, openid, user, unionid, wx_appid=wx_appid)
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 _client_type_from_request(request):
if not request:
return 'mini'
meta = getattr(request, 'META', None) or {}
header = (meta.get('HTTP_X_CLIENT') or meta.get('HTTP_X_PAY_CLIENT') or '').strip().lower()
data = getattr(request, 'data', None) or {}
body = ''
if isinstance(data, dict):
body = (data.get('client_type') or data.get('pay_client') or '').strip().lower()
val = header or body
if val in ('h5', 'official', 'mp-h5', 'oa'):
return 'h5'
return 'mini'
def resolve_prefer_wx_appid(club_id, client_type='mini'):
"""按端解析应付的 AppIDH5→服务号小程序→wx_appid。"""
from jituan.models import Club
cid = (club_id or '').strip()
if not cid:
return ''
try:
club = Club.query.get(club_id=cid)
except Exception:
return ''
if client_type == 'h5':
return (club.official_appid or '').strip()
return (club.wx_appid or club.pay_app_id or '').strip()
def resolve_club_payment_openid(user, club_id=None, prefer_wx_appid=''):
"""
支付专用:按俱乐部取 openid。
prefer_wx_appid 有值时优先取该 AppID 绑定H5 服务号 / 小程序分流)。
1) user_wx_openid(yonghuid, club_id[, wx_appid])
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'
prefer = (prefer_wx_appid or '').strip()
qs = UserWxOpenid.query.filter(yonghuid=uid, club_id=cid)
binding = None
if prefer:
binding = qs.filter(wx_appid=prefer).order_by('-id').first()
if not binding and prefer:
# 历史行 wx_appid 为空:仅小程序端可回落
pass
if not binding and not prefer:
binding = qs.order_by('-id').first()
if not binding and prefer:
# 小程序端:允许回落空 wx_appid 历史绑定
from jituan.models import Club
try:
club = Club.query.get(club_id=cid)
mini = (club.wx_appid or club.pay_app_id or '').strip()
except Exception:
mini = ''
if prefer == mini:
binding = qs.filter(wx_appid='').order_by('-id').first()
if binding and binding.openid:
return binding.openid.strip(), f'user_wx_openid#{binding.id}'
if prefer:
logger.warning(
'[PAY_OPENID] uid=%s club=%s 无 appid=%s 绑定,须先在对应端完成微信登录',
uid, cid, prefer,
)
return '', 'missing_appid_bind'
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, prefer_wx_appid=''):
openid, _ = resolve_club_payment_openid(user, club_id, prefer_wx_appid=prefer_wx_appid)
return openid
def get_wx_openid_for_user_with_source(user, club_id=None, prefer_wx_appid=''):
return resolve_club_payment_openid(user, club_id, prefer_wx_appid=prefer_wx_appid)
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)
client_type = _client_type_from_request(request)
prefer = resolve_prefer_wx_appid(cid, client_type)
openid, source = get_wx_openid_for_user_with_source(user, cid, prefer_wx_appid=prefer)
if openid:
logger.info(
'[PAY_OPENID] uid=%s club=%s client=%s appid=%s openid=%s… source=%s',
getattr(user, 'UserUID', ''), cid, client_type, prefer or '-',
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 client=%s appid=%s source=%s user_club=%s',
uid, cid, client_type, prefer or '-', source, get_user_club_id(user),
)
return openid, cid
def get_pay_app_source(request):
"""供统一下单选用 AppIDh5→official其它→mini。"""
return 'official' if _client_type_from_request(request) == 'h5' else 'mini'