feat: UFO H5 服务号 OAuth 登录与同商户公众号 JSAPI 支付
This commit is contained in:
@@ -99,7 +99,12 @@ def find_user_by_wx_openid(club_id, openid):
|
||||
return None
|
||||
|
||||
|
||||
def ensure_wx_openid_binding(club_id, openid, user, unionid=''):
|
||||
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)
|
||||
@@ -108,25 +113,40 @@ def ensure_wx_openid_binding(club_id, openid, user, unionid=''):
|
||||
|
||||
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():
|
||||
# 同一用户在同一俱乐部只保留当前 openid(AppID 变更/重新登录须覆盖旧绑定)
|
||||
stale = UserWxOpenid.query.filter(yonghuid=uid, club_id=club_id).exclude(openid=openid)
|
||||
stale_count = stale.count()
|
||||
# 仅清理同一 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 清除 %d 条过期 openid 绑定',
|
||||
uid, club_id, stale_count,
|
||||
'[WX_OPENID_BIND] uid=%s club=%s appid=%s 清除 %d 条过期 openid 绑定',
|
||||
uid, club_id, wx_appid or '(legacy)', stale_count,
|
||||
)
|
||||
stale.delete()
|
||||
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(
|
||||
@@ -134,6 +154,7 @@ def ensure_wx_openid_binding(club_id, openid, user, unionid=''):
|
||||
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
|
||||
@@ -143,7 +164,7 @@ def ensure_wx_openid_binding(club_id, openid, user, unionid=''):
|
||||
return UserWxOpenid.query.filter(club_id=club_id, openid=openid).first()
|
||||
|
||||
|
||||
def resolve_or_create_wx_user(club_id, openid, generate_uid, unionid=''):
|
||||
def resolve_or_create_wx_user(club_id, openid, generate_uid, unionid='', wx_appid=''):
|
||||
"""
|
||||
微信登录/注册统一找用户,避免 UserName(openid) 唯一键冲突。
|
||||
星之界等俱乐部用户常 UserName=openid 且 OpenID 为空,不可 get_or_create(OpenID=...)。
|
||||
@@ -154,14 +175,31 @@ def resolve_or_create_wx_user(club_id, openid, generate_uid, unionid=''):
|
||||
|
||||
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)
|
||||
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(
|
||||
@@ -177,10 +215,10 @@ def resolve_or_create_wx_user(club_id, openid, generate_uid, unionid=''):
|
||||
user = find_user_by_wx_openid(club_id, openid)
|
||||
if not user:
|
||||
raise
|
||||
ensure_wx_openid_binding(club_id, openid, user, unionid)
|
||||
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)
|
||||
ensure_wx_openid_binding(club_id, openid, user, unionid, wx_appid=wx_appid)
|
||||
return user, True
|
||||
|
||||
|
||||
@@ -221,11 +259,42 @@ def _looks_like_wx_openid(value):
|
||||
return len(v) >= 20 and v.startswith('o')
|
||||
|
||||
|
||||
def resolve_club_payment_openid(user, club_id=None):
|
||||
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,与 club 表 miniapp AppID 对应。
|
||||
1) user_wx_openid(yonghuid, club_id)
|
||||
2) 无本 club 绑定但有其他 club → 拒绝(须在本小程序重新登录)
|
||||
支付专用:按俱乐部取 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:
|
||||
@@ -235,14 +304,37 @@ def resolve_club_payment_openid(user, club_id=None):
|
||||
if not uid or not cid:
|
||||
return '', 'none'
|
||||
|
||||
binding = (
|
||||
UserWxOpenid.query.filter(yonghuid=uid, club_id=cid)
|
||||
.order_by('-id')
|
||||
.first()
|
||||
)
|
||||
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)
|
||||
@@ -277,29 +369,38 @@ def resolve_club_payment_openid(user, club_id=None):
|
||||
return '', 'missing'
|
||||
|
||||
|
||||
def get_wx_openid_for_user(user, club_id=None):
|
||||
openid, _ = resolve_club_payment_openid(user, club_id)
|
||||
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):
|
||||
return resolve_club_payment_openid(user, club_id)
|
||||
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。"""
|
||||
"""下单/充值:按请求俱乐部 + 端类型解析 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)
|
||||
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 openid=%s… source=%s',
|
||||
getattr(user, 'UserUID', ''), cid,
|
||||
openid[:6] + '...' if len(openid) > 10 else openid, source)
|
||||
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 source=%s user_club=%s',
|
||||
uid, cid, source, get_user_club_id(user),
|
||||
'[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'
|
||||
|
||||
Reference in New Issue
Block a user