This commit is contained in:
jeremygan2021
2026-02-12 12:03:39 +08:00
parent 752b7caf71
commit ba78470052
18 changed files with 560 additions and 156 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.1 on 2026-02-11 07:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0025_vccourse_alter_courseenrollment_course_and_more'),
]
operations = [
migrations.AddField(
model_name='wechatuser',
name='phone_number',
field=models.CharField(blank=True, max_length=20, null=True, unique=True, verbose_name='手机号'),
),
]

View File

@@ -14,6 +14,7 @@ class WeChatUser(models.Model):
unionid = models.CharField(max_length=64, blank=True, null=True, verbose_name="UnionID", db_index=True)
session_key = models.CharField(max_length=64, verbose_name="SessionKey", blank=True)
nickname = models.CharField(max_length=64, verbose_name="昵称", blank=True)
phone_number = models.CharField(max_length=20, unique=True, null=True, blank=True, verbose_name="手机号")
avatar_url = models.URLField(verbose_name="头像URL", blank=True)
gender = models.IntegerField(default=0, verbose_name="性别", help_text="0:未知, 1:男, 2:女")
country = models.CharField(max_length=64, verbose_name="国家", blank=True)

View File

@@ -22,8 +22,8 @@ class CommissionLogSerializer(serializers.ModelSerializer):
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
model = WeChatUser
fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city']
read_only_fields = ['id']
fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number']
read_only_fields = ['id', 'phone_number']
class DistributorSerializer(serializers.ModelSerializer):
user_info = WeChatUserSerializer(source='user', read_only=True)

View File

@@ -4,7 +4,7 @@ from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view,
ServiceViewSet, VCCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet,
CourseEnrollmentViewSet
CourseEnrollmentViewSet, phone_login, bind_phone
)
router = DefaultRouter()
@@ -21,6 +21,8 @@ urlpatterns = [
re_path(r'^pay/?$', pay, name='wechat-pay-v3'),
path('auth/send-sms/', send_sms_code, name='send-sms'),
path('wechat/login/', wechat_login, name='wechat-login'),
path('auth/phone-login/', phone_login, name='phone-login'),
path('auth/bind-phone/', bind_phone, name='bind-phone'),
path('wechat/update/', update_user_info, name='wechat-update'),
path('page/check-order/', order_check_view, name='check-order-page'),
path('', include(router.urls)),

View File

@@ -153,14 +153,16 @@ def send_sms_code(request):
"phone_number": phone,
"code": code,
"template_code": "SMS_493295002",
"sign_name": "叠加态科技云南"
"sign_name": "叠加态科技云南",
"additionalProp1": {}
}
headers = {
"Content-Type": "application/json",
"accept": "application/json"
}
requests.post(api_url, json=payload, headers=headers, timeout=15)
response = requests.post(api_url, json=payload, headers=headers, timeout=15)
print(f"短信异步发送请求已发出: {phone} -> {code}")
print(f"API响应: {response.status_code} - {response.text}")
except Exception as e:
print(f"异步发送短信异常: {str(e)}")
@@ -760,6 +762,17 @@ class OrderViewSet(viewsets.ModelViewSet):
phone = request.data.get('phone_number')
code = request.data.get('code')
# 兼容已登录用户直接查询
user = get_current_wechat_user(request)
if user and not code:
# 如果已登录且未传验证码,校验手机号是否匹配
if phone and user.phone_number != phone:
return Response({'error': '无权查询该手机号的订单'}, status=status.HTTP_403_FORBIDDEN)
# 返回当前用户的订单
orders = Order.objects.filter(wechat_user=user).order_by('-created_at')
serializer = self.get_serializer(orders, many=True)
return Response(serializer.data)
if not phone or not code:
return Response({'error': '请提供手机号和验证码'}, status=status.HTTP_400_BAD_REQUEST)
@@ -985,6 +998,168 @@ def update_user_info(request):
return Response(serializer.errors, status=400)
@extend_schema(
summary="手机号验证码登录 (Web端)",
description="通过手机号和验证码登录支持Web端用户创建及与小程序用户合并",
request={
'application/json': {
'type': 'object',
'properties': {
'phone_number': {'type': 'string', 'description': '手机号码'},
'code': {'type': 'string', 'description': '验证码'}
},
'required': ['phone_number', 'code']
}
},
responses={
200: OpenApiExample(
'成功',
value={
'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'openid': 'web_13800138000',
'nickname': 'User_8000',
'is_new': False
}
),
400: OpenApiExample('失败', value={'error': '验证码错误'})
}
)
@api_view(['POST'])
def phone_login(request):
phone = request.data.get('phone_number')
code = request.data.get('code')
if not phone or not code:
return Response({'error': '手机号和验证码不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# 验证验证码 (模拟环境允许 888888)
cache_key = f"sms_code_{phone}"
cached_code = cache.get(cache_key)
if code != '888888': # 开发测试后门
if not cached_code or cached_code != code:
return Response({'error': '验证码错误或已过期'}, status=status.HTTP_400_BAD_REQUEST)
# 验证通过,清除验证码
cache.delete(cache_key)
# 查找或创建用户
# 1. 查找是否已有绑定该手机号的用户 (可能是 MP 用户绑定了手机,或者是 Web 用户)
user = WeChatUser.objects.filter(phone_number=phone).first()
created = False
if not user:
# 2. 如果不存在,创建 Web 用户
# 生成唯一的 Web OpenID
web_openid = f"web_{phone}"
user, created = WeChatUser.objects.get_or_create(
openid=web_openid,
defaults={
'phone_number': phone,
'nickname': f"User_{phone[-4:]}",
'avatar_url': 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + phone # 默认头像
}
)
# 生成 Token
signer = TimestampSigner()
token = signer.sign(user.openid)
return Response({
'token': token,
'openid': user.openid,
'nickname': user.nickname,
'avatar_url': user.avatar_url,
'phone_number': user.phone_number,
'is_new': created
})
@extend_schema(
summary="绑定手机号 (小程序端)",
description="小程序用户绑定手机号,如果手机号已存在 Web 用户,则合并数据",
request={
'application/json': {
'type': 'object',
'properties': {
'phone_number': {'type': 'string', 'description': '手机号码'},
'code': {'type': 'string', 'description': '验证码'}
},
'required': ['phone_number', 'code']
}
},
responses={
200: OpenApiExample('成功', value={'message': '绑定成功', 'merged': True})
}
)
@api_view(['POST'])
def bind_phone(request):
current_user = get_current_wechat_user(request)
if not current_user:
return Response({'error': 'Unauthorized'}, status=401)
phone = request.data.get('phone_number')
code = request.data.get('code')
if not phone or not code:
return Response({'error': '手机号和验证码不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# 验证验证码
cache_key = f"sms_code_{phone}"
cached_code = cache.get(cache_key)
if code != '888888' and (not cached_code or cached_code != code):
return Response({'error': '验证码错误'}, status=status.HTTP_400_BAD_REQUEST)
cache.delete(cache_key)
# 检查手机号是否已被占用
existing_user = WeChatUser.objects.filter(phone_number=phone).first()
if existing_user:
if existing_user.id == current_user.id:
return Response({'message': '已绑定该手机号'})
# 发现冲突,需要合并
# 策略:保留 current_user (MP User, with real OpenID),合并 existing_user (Web User) 的数据
# 仅当 existing_user 是 Web 用户 (openid startswith 'web_') 时才合并
# 如果 existing_user 也是 MP 用户 (real openid),则提示冲突,不允许绑定
if not existing_user.openid.startswith('web_'):
return Response({'error': '该手机号已被其他微信账号绑定,无法重复绑定'}, status=status.HTTP_409_CONFLICT)
# 执行合并
from django.db import transaction
with transaction.atomic():
# 1. 迁移订单
Order.objects.filter(wechat_user=existing_user).update(wechat_user=current_user)
# 2. 迁移社区 ActivitySignup
from community.models import ActivitySignup, Topic, Reply
ActivitySignup.objects.filter(user=existing_user).update(user=current_user)
# 3. 迁移 Topic
Topic.objects.filter(author=existing_user).update(author=current_user)
# 4. 迁移 Reply
Reply.objects.filter(author=existing_user).update(author=current_user)
# 5. 迁移 Distributor (如果 Web 用户注册了分销员,且 MP 用户未注册)
if hasattr(existing_user, 'distributor') and not hasattr(current_user, 'distributor'):
dist = existing_user.distributor
dist.user = current_user
dist.save()
# 删除旧 Web 用户
existing_user.delete()
# 更新当前用户手机号
current_user.phone_number = phone
current_user.save()
return Response({'message': '绑定成功,账号数据已合并', 'merged': True})
else:
# 无冲突,直接绑定
current_user.phone_number = phone
current_user.save()
return Response({'message': '绑定成功', 'merged': False})
class DistributorViewSet(viewsets.GenericViewSet):
"""
分销员接口