diff --git a/jituan/management/commands/set_club_official.py b/jituan/management/commands/set_club_official.py new file mode 100644 index 0000000..55554d8 --- /dev/null +++ b/jituan/management/commands/set_club_official.py @@ -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 "(未改)"}' + )) diff --git a/jituan/migrations/0022_user_wx_openid_wx_appid.py b/jituan/migrations/0022_user_wx_openid_wx_appid.py new file mode 100644 index 0000000..79543e2 --- /dev/null +++ b/jituan/migrations/0022_user_wx_openid_wx_appid.py @@ -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, + ), + ), + ] diff --git a/jituan/models.py b/jituan/models.py index 42985c6..b1f400a 100644 --- a/jituan/models.py +++ b/jituan/models.py @@ -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: diff --git a/jituan/services/club_resolver.py b/jituan/services/club_resolver.py index 191e8d5..12436be 100644 --- a/jituan/services/club_resolver.py +++ b/jituan/services/club_resolver.py @@ -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。 diff --git a/jituan/services/club_user.py b/jituan/services/club_user.py index 2a96e97..327188c 100644 --- a/jituan/services/club_user.py +++ b/jituan/services/club_user.py @@ -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' diff --git a/jituan/services/wechat_login.py b/jituan/services/wechat_login.py index 4c1d3b0..36e33fd 100644 --- a/jituan/services/wechat_login.py +++ b/jituan/services/wechat_login.py @@ -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 diff --git a/jituan/services/wechat_pay.py b/jituan/services/wechat_pay.py index fd15e4c..3c465b5 100644 --- a/jituan/services/wechat_pay.py +++ b/jituan/services/wechat_pay.py @@ -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, diff --git a/jituan/urls.py b/jituan/urls.py index 79e3495..9609531 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -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'), diff --git a/jituan/views.py b/jituan/views.py index e3acbf7..a71caf3 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -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 对齐)。 diff --git a/orders/views/payment.py b/orders/views/payment.py index b52a113..53e1328 100644 --- a/orders/views/payment.py +++ b/orders/views/payment.py @@ -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,37 +1031,42 @@ 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', '') - fb_params = maybe_create_fubei_jsapi_params( - club_id=club_id, - out_trade_no=dingdanid, - amount_yuan=jine, - body=beizhu, - openid=openid, - notify_url=NOTIFY_URL, - biz_type=FUBEI_BIZ_ORDER, - ) + # 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, + amount_yuan=jine, + body=beizhu, + openid=openid, + notify_url=NOTIFY_URL, + biz_type=FUBEI_BIZ_ORDER, + ) if fb_params is not None: return fb_params # 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) diff --git a/products/views/zhuanpan_payment.py b/products/views/zhuanpan_payment.py index c4d3cda..e307fc4 100644 --- a/products/views/zhuanpan_payment.py +++ b/products/views/zhuanpan_payment.py @@ -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,23 +307,27 @@ 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 = maybe_create_fubei_jsapi_params( - club_id=club_id, - out_trade_no=dingdanid, - amount_yuan=jine, - body=PAY_DESCRIPTION, - openid=openid, - notify_url=NOTIFY_URL, - biz_type=FUBEI_BIZ_CZJILU, - ) + 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, + amount_yuan=jine, + body=PAY_DESCRIPTION, + openid=openid, + notify_url=NOTIFY_URL, + biz_type=FUBEI_BIZ_CZJILU, + ) 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']