407 lines
14 KiB
Python
407 lines
14 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='', 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'):
|
||
"""按端解析应付的 AppID:H5→服务号;小程序→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 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'
|
||
|
||
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):
|
||
"""供统一下单选用 AppID:h5→official,其它→mini。"""
|
||
return 'official' if _client_type_from_request(request) == 'h5' else 'mini'
|