forum
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
43
backend/shop/utils.py
Normal 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
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -65,6 +65,7 @@ const LoginModal = ({ visible, onClose, onLoginSuccess }) => {
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
centered
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user