This commit is contained in:
jeremygan2021
2026-02-12 17:13:30 +08:00
parent ee3e932aa5
commit 5a7b2032c4
14 changed files with 122 additions and 19 deletions

View File

@@ -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'),

43
backend/shop/utils.py Normal file
View File

@@ -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

View File

@@ -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(

View File

@@ -175,7 +175,8 @@ const CreateTopicModal = ({ visible, onClose, onSuccess, initialValues, isEditMo
footer={null}
destroyOnClose
width={1000}
style={{ top: 20 }}
centered
maskClosable={false}
>
<Form
form={form}

View File

@@ -65,6 +65,7 @@ const LoginModal = ({ visible, onClose, onLoginSuccess }) => {
onCancel={onClose}
footer={null}
destroyOnClose
centered
>
<Form
form={form}

View File

@@ -194,7 +194,7 @@ const ForumDetail = () => {
backdropFilter: 'blur(10px)',
marginBottom: 30
}}
bodyStyle={{ padding: '30px' }}
styles={{ body: { padding: '30px' } }}
>
<div style={{ marginBottom: 20 }}>
{topic.is_pinned && <Tag color="red" style={{ marginRight: 10 }}>置顶</Tag>}
@@ -353,11 +353,13 @@ const ForumDetail = () => {
{/* Edit Modal */}
<CreateTopicModal
visible={editModalVisible}
onClose={() => setEditModalVisible(false)}
onClose={() => {
setEditModalVisible(false);
// Workaround for scroll issue: Force reload page on close
window.location.reload();
}}
onSuccess={() => {
fetchTopic();
// setEditModalVisible(false) is called in modal's submit handler wrapper?
// CreateTopicModal calls onSuccess then onClose. So we just need to refresh here.
}}
initialValues={topic}
isEditMode={true}