feat: UFO H5 服务号 OAuth 登录与同商户公众号 JSAPI 支付
This commit is contained in:
38
jituan/management/commands/set_club_official.py
Normal file
38
jituan/management/commands/set_club_official.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""写入俱乐部服务号配置(H5 网页授权)。不改小程序 wx_appid。"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from jituan.models import Club
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '设置俱乐部 official_appid / official_secret / h5_domain'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--club-id', required=True)
|
||||
parser.add_argument('--official-appid', required=True)
|
||||
parser.add_argument('--official-secret', required=True)
|
||||
parser.add_argument('--h5-domain', default='')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
club_id = (options['club_id'] or '').strip()
|
||||
appid = (options['official_appid'] or '').strip()
|
||||
secret = (options['official_secret'] or '').strip()
|
||||
h5_domain = (options['h5_domain'] or '').strip()
|
||||
if not club_id or not appid or not secret:
|
||||
raise CommandError('club-id / official-appid / official-secret 必填')
|
||||
|
||||
try:
|
||||
club = Club.query.get(club_id=club_id)
|
||||
except Club.DoesNotExist:
|
||||
raise CommandError(f'俱乐部不存在: {club_id}')
|
||||
|
||||
club.official_appid = appid
|
||||
club.official_secret = secret
|
||||
if h5_domain:
|
||||
club.h5_domain = h5_domain
|
||||
club.save()
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'已更新 club={club_id} official_appid={appid} '
|
||||
f'secret_len={len(secret)} h5_domain={club.h5_domain or "(未改)"}'
|
||||
))
|
||||
22
jituan/migrations/0022_user_wx_openid_wx_appid.py
Normal file
22
jituan/migrations/0022_user_wx_openid_wx_appid.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('jituan', '0021_club_migrate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userwxopenid',
|
||||
name='wx_appid',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default='',
|
||||
db_index=True,
|
||||
help_text='产生该 openid 的 AppID:小程序或服务号;空=历史小程序绑定',
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -140,6 +140,11 @@ class UserWxOpenid(QModel):
|
||||
openid = models.CharField(max_length=64, db_index=True)
|
||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
|
||||
unionid = models.CharField(max_length=64, null=True, blank=True)
|
||||
# 小程序与服务号 openid 不同;同用户同俱乐部可各绑一条,支付时按 AppID 选取
|
||||
wx_appid = models.CharField(
|
||||
max_length=32, blank=True, default='', db_index=True,
|
||||
help_text='产生该 openid 的 AppID:小程序或服务号;空=历史小程序绑定',
|
||||
)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -143,6 +143,62 @@ def wechat_jscode2session_for_request(request, code):
|
||||
return jscode2session(code, club_id=club_id)
|
||||
|
||||
|
||||
def get_club_official_credentials(club_id):
|
||||
"""服务号 AppID/Secret(H5 网页授权 + 公众号 JSAPI)。"""
|
||||
club_id = club_id or CLUB_ID_DEFAULT
|
||||
club = None
|
||||
try:
|
||||
club = Club.query.get(club_id=club_id)
|
||||
except Club.DoesNotExist:
|
||||
club = None
|
||||
except Exception:
|
||||
logger.exception('读取俱乐部失败 club_id=%s', club_id)
|
||||
club = None
|
||||
if not club:
|
||||
return '', '', None
|
||||
appid = (club.official_appid or '').strip()
|
||||
secret = (club.official_secret or '').strip()
|
||||
return appid, secret, club
|
||||
|
||||
|
||||
def oauth2_access_token(code, club_id=None):
|
||||
"""
|
||||
服务号网页授权:code → openid(sns/oauth2/access_token)。
|
||||
"""
|
||||
appid, secret, club = get_club_official_credentials(club_id)
|
||||
if not appid or not secret:
|
||||
return {'errmsg': '俱乐部服务号配置未设置(official_appid/official_secret)'}
|
||||
|
||||
url = 'https://api.weixin.qq.com/sns/oauth2/access_token'
|
||||
params = {
|
||||
'appid': appid,
|
||||
'secret': secret,
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
}
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
result = response.json()
|
||||
if 'openid' in result:
|
||||
return {
|
||||
'openid': result['openid'],
|
||||
'access_token': result.get('access_token', ''),
|
||||
'refresh_token': result.get('refresh_token', ''),
|
||||
'scope': result.get('scope', ''),
|
||||
'unionid': result.get('unionid', ''),
|
||||
'club_id': (club.club_id if club else None) or club_id or CLUB_ID_DEFAULT,
|
||||
'wx_appid': appid,
|
||||
}
|
||||
errcode = result.get('errcode', 'unknown')
|
||||
errmsg = result.get('errmsg', '未知错误')
|
||||
return {'errmsg': f'[{errcode}]{errmsg}'}
|
||||
except requests.exceptions.Timeout:
|
||||
return {'errmsg': '请求微信接口超时'}
|
||||
except Exception as e:
|
||||
logger.exception('oauth2_access_token 异常 club_id=%s', club_id)
|
||||
return {'errmsg': f'请求微信接口异常: {str(e)}'}
|
||||
|
||||
|
||||
def jscode2session(code, club_id=None, app_id=None):
|
||||
"""
|
||||
按俱乐部微信配置换取 openid。
|
||||
|
||||
@@ -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,12 +175,29 @@ 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:
|
||||
@@ -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'
|
||||
|
||||
@@ -123,6 +123,7 @@ def login_or_register_by_club(code, club_id=None, app_id=None, client_ip=''):
|
||||
|
||||
openid = wx_data['openid']
|
||||
unionid = wx_data.get('unionid', '')
|
||||
wx_appid = wx_data.get('wx_appid', '') or ''
|
||||
resolved_club_id = wx_data.get('club_id') or club_id or CLUB_ID_DEFAULT
|
||||
|
||||
with transaction.atomic():
|
||||
@@ -140,13 +141,16 @@ def login_or_register_by_club(code, club_id=None, app_id=None, client_ip=''):
|
||||
|
||||
if not user:
|
||||
user, created = resolve_or_create_wx_user(
|
||||
resolved_club_id, openid, _generate_user_uid, unionid=unionid,
|
||||
resolved_club_id, openid, _generate_user_uid,
|
||||
unionid=unionid, wx_appid=wx_appid,
|
||||
)
|
||||
if created:
|
||||
_ensure_boss_profile(user)
|
||||
_ensure_consumer_role(user)
|
||||
else:
|
||||
ensure_wx_openid_binding(resolved_club_id, openid, user, unionid)
|
||||
ensure_wx_openid_binding(
|
||||
resolved_club_id, openid, user, unionid, wx_appid=wx_appid,
|
||||
)
|
||||
|
||||
if unionid and unionid != (user.UnionID or ''):
|
||||
user.UnionID = unionid
|
||||
@@ -208,9 +212,10 @@ def refresh_wx_openid_binding(code, user, club_id=None, app_id=None):
|
||||
|
||||
openid = wx_data['openid']
|
||||
unionid = wx_data.get('unionid', '')
|
||||
wx_appid = wx_data.get('wx_appid', '') or ''
|
||||
resolved_club_id = wx_data.get('club_id') or club_id or CLUB_ID_DEFAULT
|
||||
|
||||
ensure_wx_openid_binding(resolved_club_id, openid, user, unionid)
|
||||
ensure_wx_openid_binding(resolved_club_id, openid, user, unionid, wx_appid=wx_appid)
|
||||
ensure_user_club_id(user, resolved_club_id)
|
||||
if unionid and unionid != (getattr(user, 'UnionID', None) or ''):
|
||||
user.UnionID = unionid
|
||||
@@ -221,3 +226,99 @@ def refresh_wx_openid_binding(code, user, club_id=None, app_id=None):
|
||||
'openid_preview': openid[:6] + '...' + openid[-4:] if len(openid) > 10 else openid,
|
||||
'wx_appid': wx_data.get('wx_appid', ''),
|
||||
}, None
|
||||
|
||||
|
||||
def login_or_register_by_official_oauth(code, club_id=None, client_ip=''):
|
||||
"""
|
||||
服务号网页授权登录(H5)。
|
||||
换 openid 用 club.official_appid/secret;建用户逻辑与小程序登录一致。
|
||||
"""
|
||||
from jituan.services.club_resolver import oauth2_access_token
|
||||
|
||||
wx_data = oauth2_access_token(code, club_id=club_id)
|
||||
if not wx_data or 'openid' not in wx_data:
|
||||
return None, (wx_data or {}).get('errmsg', '微信授权失败')
|
||||
|
||||
openid = wx_data['openid']
|
||||
unionid = wx_data.get('unionid', '')
|
||||
wx_appid = wx_data.get('wx_appid', '') or ''
|
||||
resolved_club_id = wx_data.get('club_id') or club_id or CLUB_ID_DEFAULT
|
||||
|
||||
with transaction.atomic():
|
||||
binding = UserWxOpenid.query.filter(
|
||||
club_id=resolved_club_id, openid=openid
|
||||
).first()
|
||||
|
||||
user = None
|
||||
created = False
|
||||
|
||||
if binding:
|
||||
user = User.query.filter(UserUID=binding.yonghuid).first()
|
||||
if not user:
|
||||
user = User.objects.filter(UserUID=binding.yonghuid).first()
|
||||
|
||||
if not user:
|
||||
user, created = resolve_or_create_wx_user(
|
||||
resolved_club_id, openid, _generate_user_uid,
|
||||
unionid=unionid, wx_appid=wx_appid,
|
||||
)
|
||||
if created:
|
||||
_ensure_boss_profile(user)
|
||||
_ensure_consumer_role(user)
|
||||
else:
|
||||
ensure_wx_openid_binding(
|
||||
resolved_club_id, openid, user, unionid, wx_appid=wx_appid,
|
||||
)
|
||||
|
||||
if unionid and unionid != (user.UnionID or ''):
|
||||
user.UnionID = unionid
|
||||
if client_ip:
|
||||
user.IP = client_ip
|
||||
user.UserLastLoginDate = timezone.now()
|
||||
user.save()
|
||||
|
||||
ensure_user_club_id(user, resolved_club_id)
|
||||
|
||||
roles = _collect_role_status(user)
|
||||
refresh = RefreshToken.for_user(user)
|
||||
token = str(refresh.access_token)
|
||||
|
||||
staff_fields = {}
|
||||
order_counts = {}
|
||||
group_info = {}
|
||||
try:
|
||||
from merchant_ops.services.authz import staff_fields_for_login
|
||||
staff_fields = staff_fields_for_login(user) or {}
|
||||
except Exception:
|
||||
logger.exception('staff_fields_for_login 失败 uid=%s', getattr(user, 'UserUID', ''))
|
||||
|
||||
try:
|
||||
from users.views import WechatMiniProgramLoginView
|
||||
login_view = WechatMiniProgramLoginView()
|
||||
order_counts = login_view.jisuanOrderShuliang(user.UserUID) or {}
|
||||
group_info = login_view.huoquQunPeizhi() or {}
|
||||
except Exception:
|
||||
logger.exception('登录附属信息失败 uid=%s', getattr(user, 'UserUID', ''))
|
||||
|
||||
data = {
|
||||
'token': token,
|
||||
'nicheng': roles['boss_nickname'],
|
||||
'uid': user.UserUID,
|
||||
'touxiang': user.Avatar or '',
|
||||
'shangjiastatus': roles['shangjia_status'],
|
||||
'dashoustatus': roles['dashou_status'],
|
||||
'guanshistatus': roles['guanshi_status'],
|
||||
'zuzhangstatus': roles['zuzhang_status'],
|
||||
'kaoheguanstatus': roles['kaoheguan_status'],
|
||||
'dingdantiaoshu': order_counts,
|
||||
'dashouqun': group_info.get('dashouqun', ''),
|
||||
'dashouqunid': group_info.get('dashouqunid', ''),
|
||||
'guanshiqun': group_info.get('guanshiqun', ''),
|
||||
'guanshiqunid': group_info.get('guanshiqunid', ''),
|
||||
'club_id': resolved_club_id,
|
||||
'is_new_user': created,
|
||||
'login_client': 'h5',
|
||||
'wx_appid': wx_appid,
|
||||
**staff_fields,
|
||||
}
|
||||
return data, None
|
||||
|
||||
@@ -172,10 +172,12 @@ def _resolve_v2_mch_key(club, cfg_json, fallback_key, club_id=None):
|
||||
return '', 'missing'
|
||||
|
||||
|
||||
def get_wechat_v2_config(club_id=None):
|
||||
def get_wechat_v2_config(club_id=None, app_source='mini'):
|
||||
"""
|
||||
小程序 JSAPI 支付 V2 配置。
|
||||
各俱乐部(含 xq)优先读 club 表;字段为空时按项回落 settings,兼容旧部署。
|
||||
微信 JSAPI 支付 V2 配置。
|
||||
app_source:
|
||||
- mini:小程序 AppID(默认,现网行为)
|
||||
- official:服务号 AppID(H5);商户号/密钥与小程序相同,仅换 AppID
|
||||
"""
|
||||
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
fallback = {
|
||||
@@ -194,6 +196,7 @@ def get_wechat_v2_config(club_id=None):
|
||||
cfg_json = club.config_json or {}
|
||||
mch_key, key_source = _resolve_v2_mch_key(club, cfg_json, fallback['key'], club_id=cid)
|
||||
mini_appid = get_club_miniapp_appid(club)
|
||||
official_appid = (club.official_appid or '').strip()
|
||||
wx_a = (club.wx_appid or '').strip()
|
||||
pay_a = (club.pay_app_id or '').strip()
|
||||
if wx_a and pay_a and wx_a != pay_a:
|
||||
@@ -202,7 +205,13 @@ def get_wechat_v2_config(club_id=None):
|
||||
'小程序 JSAPI 以 wx_appid 优先(openid 与登录 AppID 绑定)',
|
||||
cid, wx_a, pay_a,
|
||||
)
|
||||
if mini_appid:
|
||||
|
||||
source = (app_source or 'mini').strip().lower()
|
||||
if source in ('official', 'h5', 'oa'):
|
||||
appid = official_appid
|
||||
if not appid:
|
||||
logger.error('[WX_V2_CONFIG] club=%s H5 支付需要 official_appid', cid)
|
||||
elif mini_appid:
|
||||
appid = mini_appid
|
||||
elif cid == CLUB_ID_DEFAULT:
|
||||
appid = fallback['appid']
|
||||
@@ -236,6 +245,7 @@ def get_wechat_v2_config(club_id=None):
|
||||
'key': mch_key,
|
||||
'club_id': cid,
|
||||
'_key_source': key_source,
|
||||
'_app_source': source if source in ('official', 'h5', 'oa') else 'mini',
|
||||
# 明确标记:这是点单/会员收款商户
|
||||
'merchant_role': 'pay',
|
||||
'cert_path': pay_cert,
|
||||
|
||||
@@ -58,6 +58,7 @@ from jituan.views import (
|
||||
ClubSzxxView,
|
||||
ClubUserSummaryView,
|
||||
ClubWechatLoginView,
|
||||
ClubWechatOAuthLoginView,
|
||||
ClubWxBindRefreshView,
|
||||
)
|
||||
|
||||
@@ -125,6 +126,7 @@ from jituan.views_order_deal import (
|
||||
|
||||
urlpatterns = [
|
||||
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
|
||||
path('auth/wechat-oauth-login', ClubWechatOAuthLoginView.as_view(), name='jituan_wechat_oauth_login'),
|
||||
path('auth/refresh-wx-bind', ClubWxBindRefreshView.as_view(), name='jituan_refresh_wx_bind'),
|
||||
path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'),
|
||||
path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'),
|
||||
|
||||
@@ -14,7 +14,11 @@ from jituan.constants import SUPER_ADMIN_PHONES, ADMIN_ROLE_LABELS, CLUB_ID_DEFA
|
||||
from jituan.models import Club, AdminAssignment
|
||||
from jituan.services.admin_context import build_admin_club_context, is_kefu_backend_account, can_manage_admin_assignments
|
||||
from jituan.services.kefu_menu import build_menu_access_payload
|
||||
from jituan.services.wechat_login import login_or_register_by_club, refresh_wx_openid_binding
|
||||
from jituan.services.wechat_login import (
|
||||
login_or_register_by_club,
|
||||
login_or_register_by_official_oauth,
|
||||
refresh_wx_openid_binding,
|
||||
)
|
||||
from jituan.services.caiwu_stats import build_caiwu_payload
|
||||
from jituan.services.szxx_stats import build_szxx_payload
|
||||
from jituan.services.admin_assignments import (
|
||||
@@ -79,6 +83,39 @@ class ClubWechatLoginView(APIView):
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class ClubWechatOAuthLoginView(APIView):
|
||||
"""
|
||||
服务号网页授权登录(H5 UniApp)。
|
||||
POST /jituan/auth/wechat-oauth-login
|
||||
body: { code, club_id? }
|
||||
不改动 wechat-login;仅新增本接口。
|
||||
"""
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
code = (request.data.get('code') or '').strip()
|
||||
club_id = (request.data.get('club_id') or '').strip() or None
|
||||
if not code:
|
||||
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
data, err = login_or_register_by_official_oauth(
|
||||
code, club_id=club_id, client_ip=_client_ip(request),
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 2, 'msg': f'微信登录失败: {err}', 'data': None},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'code': 0, 'msg': '登录成功', 'data': data})
|
||||
except Exception as e:
|
||||
logger.exception('ClubWechatOAuthLoginView 异常')
|
||||
return Response({
|
||||
'code': 99,
|
||||
'msg': f'登录异常: {type(e).__name__}: {e}',
|
||||
'data': None,
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class ClubWxBindRefreshView(APIView):
|
||||
"""
|
||||
已登录用户静默刷新当前小程序 openid 绑定(支付前 AppID/openid 对齐)。
|
||||
|
||||
@@ -990,6 +990,7 @@ class CreateOrderView(APIView):
|
||||
club_id=club_id,
|
||||
)
|
||||
else:
|
||||
from jituan.services.club_user import get_pay_app_source
|
||||
pay_params = self.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=shangpin_jiage,
|
||||
@@ -997,6 +998,7 @@ class CreateOrderView(APIView):
|
||||
user_ip=self.get_client_ip(request),
|
||||
openid=user_openid,
|
||||
club_id=club_id,
|
||||
app_source=get_pay_app_source(request),
|
||||
)
|
||||
|
||||
if not pay_params:
|
||||
@@ -1029,20 +1031,25 @@ class CreateOrderView(APIView):
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid, club_id=None):
|
||||
def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid, club_id=None, app_source='mini'):
|
||||
"""
|
||||
调用微信支付统一下单接口,生成支付参数
|
||||
调用微信支付统一下单接口,生成支付参数。
|
||||
app_source=official/h5:用服务号 AppID + 同商户号(H5);跳过付呗小程序通道。
|
||||
"""
|
||||
try:
|
||||
from jituan.constants import FUBEI_BIZ_ORDER
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
pay_cfg = get_wechat_v2_config(club_id)
|
||||
source = (app_source or 'mini').strip().lower()
|
||||
pay_cfg = get_wechat_v2_config(club_id, app_source=source)
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '')
|
||||
|
||||
# H5/服务号:复用同一商户号走微信直连 JSAPI,不走付呗小程序通道
|
||||
fb_params = None
|
||||
if source not in ('official', 'h5', 'oa'):
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=club_id,
|
||||
out_trade_no=dingdanid,
|
||||
@@ -1058,8 +1065,8 @@ class CreateOrderView(APIView):
|
||||
# NOAUTH 诊断日志:打印生效的 club_id / appid / mchid / openid 指纹
|
||||
_oid_preview = (openid or '')[:6] + '...' + (openid or '')[-4:] if (openid or '') else '(empty)'
|
||||
logger.warning(
|
||||
"[WX_PAY_NOAUTH_DIAG] dingdanid=%s club_id=%s appid=%s mch_id=%s openid_preview=%s openid_len=%d",
|
||||
dingdanid, club_id, APPID, MCHID, _oid_preview, len(openid or ''),
|
||||
"[WX_PAY_NOAUTH_DIAG] dingdanid=%s club_id=%s app_source=%s appid=%s mch_id=%s openid_preview=%s openid_len=%d",
|
||||
dingdanid, club_id, source, APPID, MCHID, _oid_preview, len(openid or ''),
|
||||
)
|
||||
|
||||
if not all([APPID, MCHID, KEY, NOTIFY_URL]):
|
||||
@@ -1308,6 +1315,7 @@ class ContinuePayView(APIView):
|
||||
club_id=club_id,
|
||||
)
|
||||
else:
|
||||
from jituan.services.club_user import get_pay_app_source
|
||||
pay_params = helper.generate_wechat_pay_params(
|
||||
dingdanid=dingdanid,
|
||||
jine=jine,
|
||||
@@ -1315,6 +1323,7 @@ class ContinuePayView(APIView):
|
||||
user_ip=helper.get_client_ip(request),
|
||||
openid=user_openid,
|
||||
club_id=club_id,
|
||||
app_source=get_pay_app_source(request),
|
||||
)
|
||||
except Exception as pay_exc:
|
||||
err_text = str(pay_exc)
|
||||
|
||||
@@ -288,7 +288,7 @@ class ZhuanpanGoumai(APIView):
|
||||
ZhuanpanJihui.query.filter(czjilu_dingdan_id=dingdanid).delete()
|
||||
return Response({
|
||||
'code': 400,
|
||||
'message': '当前小程序未绑定微信openid,请重新登录',
|
||||
'message': '当前端未绑定微信openid,请重新登录',
|
||||
}, status=400)
|
||||
pay_params = self._wechat_pay_params(request, dingdanid, jine, pay_openid, club_id)
|
||||
|
||||
@@ -307,10 +307,14 @@ class ZhuanpanGoumai(APIView):
|
||||
from jituan.services.wechat_pay import get_wechat_v2_config
|
||||
from jituan.constants import FUBEI_BIZ_CZJILU
|
||||
from jituan.services.mini_pay_router import maybe_create_fubei_jsapi_params
|
||||
from jituan.services.club_user import get_pay_app_source
|
||||
|
||||
PAY_DESCRIPTION = getattr(settings, 'ZHUANPAN_PAY_DESCRIPTION', '转盘购票')
|
||||
NOTIFY_URL = getattr(settings, 'ZHUANPAN_PAY_NOTIFY_URL', '')
|
||||
app_source = get_pay_app_source(request)
|
||||
|
||||
fb_params = None
|
||||
if app_source not in ('official', 'h5', 'oa'):
|
||||
fb_params = maybe_create_fubei_jsapi_params(
|
||||
club_id=club_id,
|
||||
out_trade_no=dingdanid,
|
||||
@@ -323,7 +327,7 @@ class ZhuanpanGoumai(APIView):
|
||||
if fb_params is not None:
|
||||
return fb_params
|
||||
|
||||
pay_cfg = get_wechat_v2_config(club_id)
|
||||
pay_cfg = get_wechat_v2_config(club_id, app_source=app_source)
|
||||
APPID = pay_cfg['appid']
|
||||
MCHID = pay_cfg['mch_id']
|
||||
KEY = pay_cfg['key']
|
||||
|
||||
Reference in New Issue
Block a user