diff --git a/backend/config/__pycache__/settings.cpython-313.pyc b/backend/config/__pycache__/settings.cpython-313.pyc index 505eb13..f54023d 100644 Binary files a/backend/config/__pycache__/settings.cpython-313.pyc and b/backend/config/__pycache__/settings.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index 5bcffbb..0d97ef1 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/admin.cpython-313.pyc b/backend/shop/__pycache__/admin.cpython-313.pyc index f0692ff..2d1d4ea 100644 Binary files a/backend/shop/__pycache__/admin.cpython-313.pyc and b/backend/shop/__pycache__/admin.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-313.pyc b/backend/shop/__pycache__/models.cpython-313.pyc index fa1ef35..4f3cd77 100644 Binary files a/backend/shop/__pycache__/models.cpython-313.pyc and b/backend/shop/__pycache__/models.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-313.pyc b/backend/shop/__pycache__/serializers.cpython-313.pyc index 76f8aff..1db39fc 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-313.pyc and b/backend/shop/__pycache__/serializers.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-313.pyc b/backend/shop/__pycache__/urls.cpython-313.pyc index dc0bc45..48d6192 100644 Binary files a/backend/shop/__pycache__/urls.cpython-313.pyc and b/backend/shop/__pycache__/urls.cpython-313.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 97bf4b1..477e8b8 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-313.pyc b/backend/shop/__pycache__/views.cpython-313.pyc index 794b194..e85c483 100644 Binary files a/backend/shop/__pycache__/views.cpython-313.pyc and b/backend/shop/__pycache__/views.cpython-313.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 624e39c..fa15012 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -311,8 +311,8 @@ class OrderAdmin(ModelAdmin): @admin.register(WeChatUser) class WeChatUserAdmin(ModelAdmin): - list_display = ('nickname', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at') - search_fields = ('nickname', 'openid') + list_display = ('nickname', 'phone_number', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at') + search_fields = ('nickname', 'openid', 'phone_number') list_filter = ('is_star', 'gender', 'province', 'city', 'created_at') readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at') @@ -329,7 +329,7 @@ class WeChatUserAdmin(ModelAdmin): fieldsets = ( ('基本信息', { - 'fields': ('user', 'nickname', 'avatar_url', 'gender') + 'fields': ('user', 'nickname', 'phone_number', 'avatar_url', 'gender') }), ('专家认证', { 'fields': ('is_star', 'title'), diff --git a/backend/shop/utils.py b/backend/shop/utils.py new file mode 100644 index 0000000..7bf8209 --- /dev/null +++ b/backend/shop/utils.py @@ -0,0 +1,43 @@ +import requests +from django.core.cache import cache +from .models import WeChatPayConfig + +def get_access_token(config=None): + """ + 获取微信接口调用凭证 (client_credential) + """ + # 尝试从缓存获取 + cache_key = 'wechat_access_token' + if config: + cache_key = f'wechat_access_token_{config.app_id}' + + token = cache.get(cache_key) + if token: + return token + + if not config: + # 优先查找指定 AppID + config = WeChatPayConfig.objects.filter(app_id='wxdf2ca73e6c0929f0').first() + if not config: + config = WeChatPayConfig.objects.filter(is_active=True).first() + + if not config or not config.app_id or not config.app_secret: + return None + + url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={config.app_id}&secret={config.app_secret}" + try: + response = requests.get(url, timeout=10) + data = response.json() + + if 'access_token' in data: + token = data['access_token'] + expires_in = data.get('expires_in', 7200) + # 缓存 Token,留出 200 秒缓冲时间 + cache.set(cache_key, token, expires_in - 200) + return token + else: + print(f"获取 AccessToken 失败: {data}") + except Exception as e: + print(f"获取 AccessToken 异常: {str(e)}") + + return None diff --git a/backend/shop/views.py b/backend/shop/views.py index 9d04602..25eb913 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -10,6 +10,7 @@ from django.http import HttpResponse from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample from .models import ESP32Config, Order, WeChatPayConfig, Service, VCCourse, ServiceOrder, Salesperson, CommissionLog, WeChatUser, Distributor, Withdrawal, CourseEnrollment from .serializers import ESP32ConfigSerializer, OrderSerializer, ServiceSerializer, VCCourseSerializer, ServiceOrderSerializer, WeChatUserSerializer, DistributorSerializer, WithdrawalSerializer, CommissionLogSerializer, CourseEnrollmentSerializer +from .utils import get_access_token from django.core.signing import TimestampSigner, BadSignature, SignatureExpired from django.contrib.auth.models import User from wechatpayv3 import WeChatPay, WeChatPayType @@ -953,9 +954,13 @@ def get_current_wechat_user(request): @extend_schema( summary="微信小程序登录", + description="支持通过 code 登录,以及可选的 phone_code 用于直接获取手机号并合并 Web 用户账号", request={ 'application/json': { - 'properties': {'code': {'type': 'string', 'description': 'wx.login获取的code'}}, + 'properties': { + 'code': {'type': 'string', 'description': 'wx.login获取的code'}, + 'phone_code': {'type': 'string', 'description': 'getPhoneNumber获取的code (可选)'} + }, 'required': ['code'] } }, @@ -964,14 +969,21 @@ def get_current_wechat_user(request): @api_view(['POST']) def wechat_login(request): code = request.data.get('code') + phone_code = request.data.get('phone_code') + if not code: return Response({'error': 'Code is required'}, status=400) - config = WeChatPayConfig.objects.filter(is_active=True).first() + # 1. 获取配置 (优先使用指定 AppID) + target_app_id = 'wxdf2ca73e6c0929f0' + config = WeChatPayConfig.objects.filter(app_id=target_app_id).first() + if not config: + config = WeChatPayConfig.objects.filter(is_active=True).first() + if not config or not config.app_id or not config.app_secret: return Response({'error': 'WeChat config missing'}, status=500) - # 换取 OpenID + # 2. 换取 OpenID url = f"https://api.weixin.qq.com/sns/jscode2session?appid={config.app_id}&secret={config.app_secret}&js_code={code}&grant_type=authorization_code" try: res = requests.get(url, timeout=10) @@ -985,26 +997,70 @@ def wechat_login(request): openid = data.get('openid') session_key = data.get('session_key') unionid = data.get('unionid') + + # 3. 处理手机号 (尝试获取并合并 Web 用户) + user = None + phone_number = None + + if phone_code: + access_token = get_access_token(config) + if access_token: + try: + phone_url = f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}" + phone_res = requests.post(phone_url, json={'code': phone_code}, timeout=5) + phone_data = phone_res.json() + + if phone_data.get('errcode') == 0: + phone_info = phone_data.get('phone_info') + phone_number = phone_info.get('purePhoneNumber') + + if phone_number: + # 查找已存在的用户 (Web 用户或已绑定手机的 MP 用户) + existing_user = WeChatUser.objects.filter(phone_number=phone_number).first() + if existing_user: + user = existing_user + # 如果是 Web 虚拟账号 (openid 以 web_ 开头),更新为真实 OpenID + if user.openid.startswith('web_') or not user.openid: + user.openid = openid + user.session_key = session_key + user.unionid = unionid + user.save() + # 如果已是真实账号但 OpenID 不匹配,可能是不同 AppID,暂不处理(避免覆盖) + except Exception as e: + print(f"获取手机号失败: {e}") - # 创建或更新用户 - user, created = WeChatUser.objects.update_or_create( - openid=openid, - defaults={ + # 4. 创建或更新用户 (如果未通过手机号找到) + if not user: + defaults = { 'session_key': session_key, 'unionid': unionid } - ) + if phone_number: + defaults['phone_number'] = phone_number + + user, created = WeChatUser.objects.update_or_create( + openid=openid, + defaults=defaults + ) + else: + # 如果找到了用户,且 OpenID 匹配(或刚被更新),更新 session_key + if user.openid == openid: + user.session_key = session_key + user.unionid = unionid + user.save() + created = False # 生成 Token signer = TimestampSigner() - token = signer.sign(openid) + token = signer.sign(user.openid) return Response({ 'token': token, 'id': user.id, - 'openid': openid, + 'openid': user.openid, 'is_new': created, - 'nickname': user.nickname + 'nickname': user.nickname, + 'phone_number': user.phone_number }) @extend_schema( diff --git a/frontend/src/components/CreateTopicModal.jsx b/frontend/src/components/CreateTopicModal.jsx index bcf7fe8..143688e 100644 --- a/frontend/src/components/CreateTopicModal.jsx +++ b/frontend/src/components/CreateTopicModal.jsx @@ -175,7 +175,8 @@ const CreateTopicModal = ({ visible, onClose, onSuccess, initialValues, isEditMo footer={null} destroyOnClose width={1000} - style={{ top: 20 }} + centered + maskClosable={false} >