diff --git a/Fresh_Ecommerce/__pycache__/settings.cpython-38.pyc b/Fresh_Ecommerce/__pycache__/settings.cpython-38.pyc index 2ba9098e415763356b653757bb7caf3401e1cd08..2a64a6ce6c251cb64359f97dcb79e2d008270c93 100644 Binary files a/Fresh_Ecommerce/__pycache__/settings.cpython-38.pyc and b/Fresh_Ecommerce/__pycache__/settings.cpython-38.pyc differ diff --git a/Fresh_Ecommerce/__pycache__/urls.cpython-38.pyc b/Fresh_Ecommerce/__pycache__/urls.cpython-38.pyc index 46415d5453abcbcc3f76d6fb50d8de71853fa27c..c98ecfa955a241e70cff4c3ffec93e03c7087fe8 100644 Binary files a/Fresh_Ecommerce/__pycache__/urls.cpython-38.pyc and b/Fresh_Ecommerce/__pycache__/urls.cpython-38.pyc differ diff --git a/Fresh_Ecommerce/settings.py b/Fresh_Ecommerce/settings.py index e85feb4d2dcaf7a24eee0af53ce985fa022f3fbe..98886b1656d6468c57fecaeb27c8d258e3b66785 100644 --- a/Fresh_Ecommerce/settings.py +++ b/Fresh_Ecommerce/settings.py @@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ - +import datetime import os import sys @@ -19,7 +19,6 @@ sys.path.insert(0, BASE_DIR) sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) sys.path.insert(0, os.path.join(BASE_DIR, 'extra_apps')) - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ @@ -33,7 +32,6 @@ ALLOWED_HOSTS = [] AUTH_USER_MODEL = 'users.UserProfile' - # Application definition INSTALLED_APPS = [ @@ -42,7 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'apps.users.apps.UsersConfig', + 'users.apps.UsersConfig', 'goods.apps.GoodsConfig', 'trade.apps.TradeConfig', 'user_operation.apps.UserOperationConfig', @@ -53,6 +51,7 @@ INSTALLED_APPS = [ 'rest_framework', 'django_filters', 'corsheaders', + 'rest_framework.authtoken' ] MIDDLEWARE = [ @@ -90,7 +89,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'Fresh_Ecommerce.wsgi.application' - # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases @@ -100,12 +98,11 @@ DATABASES = { 'NAME': 'fresh_ec', 'USER': 'root', 'PASSWORD': 'root', - 'HOST':'127.0.0.1', + 'HOST': '127.0.0.1', 'OPTIONS': {'init_command': 'SET default_storage_engine=INNODB;'} } } - # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators @@ -124,7 +121,6 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -138,7 +134,6 @@ USE_L10N = True USE_TZ = False - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ @@ -149,5 +144,29 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # DRF配置 REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] -} \ No newline at end of file + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ] +} + +# 自定义用户认证配置 +AUTHENTICATION_BACKENDS = [ + 'users.views.CustomBackend', +] + +# JWT配置 +JWT_AUTH = { + # 过期时间 + 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), + # 请求头前缀 + 'JWT_AUTH_HEADER_PREFIX': 'JWT', +} + +# 手机号码验证正则表达式 +REGEX_MOBILE = '^1[35789]\d{9}$|^147\d{8}$' + +# 云片网APIKEY +APIKEY = 'edf71361381f31b3957beda37f20xxxx' # 更换为自己的API_KEY \ No newline at end of file diff --git a/Fresh_Ecommerce/urls.py b/Fresh_Ecommerce/urls.py index 54cea6438fb625f6eaf08dab554089731dca7ad7..4ec85a4c79bb6b40155e4773ac617eba415297a5 100644 --- a/Fresh_Ecommerce/urls.py +++ b/Fresh_Ecommerce/urls.py @@ -18,10 +18,13 @@ from django.conf.urls import url, include from django.views.static import serve from rest_framework.documentation import include_docs_urls from rest_framework.routers import DefaultRouter +from rest_framework.authtoken import views +from rest_framework_jwt.views import obtain_jwt_token import xadmin from .settings import MEDIA_ROOT from goods.views import GoodsListViewSet, CategoryViewSet +from users.views import SmsCodeViewSet, UserViewSet # Create a router and register our viewsets with it. @@ -33,14 +36,26 @@ router.register(r'goods', GoodsListViewSet, basename='goods') # 配置categories的路由 router.register(r'categorys', CategoryViewSet, basename='categorys') +# 配置短信验证码路由 +router.register(r'codes', SmsCodeViewSet, basename='codes') + +# 配置注册路由 +router.register(r'users', UserViewSet, basename='users') + urlpatterns = [ url(r'^xadmin/', xadmin.site.urls), url(r'^media/(?P.*)$', serve, {'document_root':MEDIA_ROOT}), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + # url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), # 商品列表页 url(r'^', include(router.urls)), # 文档路由 - url(r'docs/', include_docs_urls(title='生鲜电商')) + url(r'docs/', include_docs_urls(title='生鲜电商')), + + # DRF自带认证路由 + url(r'^api-token-auth/', views.obtain_auth_token, name='api_token_auth'), + + # JWT认证路由 + url(r'^login/', obtain_jwt_token), ] \ No newline at end of file diff --git a/README.md b/README.md index 6c3434ef231213a88e73def618b3748f9906b3de..9564f087cd76cf00a588f4dcd38698eb0faa2c8c 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,7 @@ 主要实现DRF(Django Restful Framework),先通过普通方式实现商品详情页,再通过DRF的各种View实现,最后实现过滤(包括字段过滤、搜索和筛选)。 #### V1.3 -先通过嵌套方式实现商品类别数据接口,再通过Vue展示商品分类,最后实现展示商品列表页数据和搜索功能。 \ No newline at end of file +先通过嵌套方式实现商品类别数据接口,再通过Vue展示商品分类,最后实现展示商品列表页数据和搜索功能。 + +#### V1.4 +先实现在DRF中使用token,再使用JSON Web Token登录,再实现短信发送和注册功能,最后实现前后端结合、完成注册功能。 \ No newline at end of file diff --git a/apps/goods/__pycache__/views.cpython-38.pyc b/apps/goods/__pycache__/views.cpython-38.pyc index f6d7df370b20bff588445f0a36aadc9d86892400..f2f82031923dd68b8554ab843a6a439390b84eaf 100644 Binary files a/apps/goods/__pycache__/views.cpython-38.pyc and b/apps/goods/__pycache__/views.cpython-38.pyc differ diff --git a/apps/goods/views.py b/apps/goods/views.py index 5edfd2427eb0621344c4a3caa0434b5903f76b2e..3cb9628b0f84f9f32eaa9fca11578fb89b5f4298 100644 --- a/apps/goods/views.py +++ b/apps/goods/views.py @@ -17,7 +17,7 @@ class GoodsPagination(PageNumberPagination): class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): '''商品列表页,并实现分页、搜索、过滤、排序''' - queryset = Goods.objects.filter(is_delete=False) + queryset = Goods.objects.filter(is_delete=False).order_by('id') serializer_class = GoodsSerializer pagination_class = GoodsPagination filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] diff --git a/apps/users/__pycache__/apps.cpython-38.pyc b/apps/users/__pycache__/apps.cpython-38.pyc index 2c44ec0193f1a179c967534cb8140c7d5c93409a..bbf1832fa33e00465578434d04ace5c322167cef 100644 Binary files a/apps/users/__pycache__/apps.cpython-38.pyc and b/apps/users/__pycache__/apps.cpython-38.pyc differ diff --git a/apps/users/__pycache__/models.cpython-38.pyc b/apps/users/__pycache__/models.cpython-38.pyc index 59f236024e52bc770531afb20e3ca8f9e486cee7..042d5e934f4d29f3e9f4cd7862cc72283060dc12 100644 Binary files a/apps/users/__pycache__/models.cpython-38.pyc and b/apps/users/__pycache__/models.cpython-38.pyc differ diff --git a/apps/users/__pycache__/serializers.cpython-38.pyc b/apps/users/__pycache__/serializers.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cae5815badcc91e2c1d04b0abcf80e7bd160fde Binary files /dev/null and b/apps/users/__pycache__/serializers.cpython-38.pyc differ diff --git a/apps/users/__pycache__/signals.cpython-38.pyc b/apps/users/__pycache__/signals.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0dd906c944f120543e97fc1711fc5eb2ac63efa Binary files /dev/null and b/apps/users/__pycache__/signals.cpython-38.pyc differ diff --git a/apps/users/__pycache__/views.cpython-38.pyc b/apps/users/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2f6b4eae0358d2f1babdd1c5cea0dcc632c4ae7 Binary files /dev/null and b/apps/users/__pycache__/views.cpython-38.pyc differ diff --git a/apps/users/apps.py b/apps/users/apps.py index f1485dd861db854fa0a0dd26ffa54c4527926304..04d87ca00630f32ff29b88793fb5b2ebd01f48cf 100644 --- a/apps/users/apps.py +++ b/apps/users/apps.py @@ -1,6 +1,8 @@ from django.apps import AppConfig - class UsersConfig(AppConfig): - name = 'apps.users' + name = 'users' verbose_name = '用户管理' + + def ready(self): + import users.signals diff --git a/apps/users/migrations/0003_auto_20200728_2119.py b/apps/users/migrations/0003_auto_20200728_2119.py new file mode 100644 index 0000000000000000000000000000000000000000..8f62e322274bd58fcf44d4f67ee3e47687966cb9 --- /dev/null +++ b/apps/users/migrations/0003_auto_20200728_2119.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.8 on 2020-07-28 21:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_auto_20200725_0954'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='mobile', + field=models.CharField(blank=True, max_length=11, null=True, verbose_name='电话'), + ), + ] diff --git a/apps/users/migrations/__pycache__/0003_auto_20200728_2119.cpython-38.pyc b/apps/users/migrations/__pycache__/0003_auto_20200728_2119.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c13ee44578c9ef873988c917298673db2b45469e Binary files /dev/null and b/apps/users/migrations/__pycache__/0003_auto_20200728_2119.cpython-38.pyc differ diff --git a/apps/users/models.py b/apps/users/models.py index 19483128cb77740cc289ea3111e560535270ed77..28dc7f1045e8906edebbbd0e9a8ad4c0a3132eaf 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -12,7 +12,7 @@ class UserProfile(AbstractUser): birthday = models.DateField(null=True, blank=True, verbose_name='出生日期') gender = models.CharField(max_length=6, choices=(('male', u'男'), ('female', u'女')), default='female', verbose_name='性别') - mobile = models.CharField(max_length=11, verbose_name='电话') + mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name='电话') email = models.CharField(max_length=50, null=True, blank=True, verbose_name='邮箱') is_delete = models.BooleanField(default=False, verbose_name='是否删除') @@ -26,6 +26,7 @@ class UserProfile(AbstractUser): class VerifyCode(models.Model): + '''短信验证码''' code = models.CharField(max_length=10, verbose_name='验证码') mobile = models.CharField(max_length=11, verbose_name='电话') diff --git a/apps/users/serializers.py b/apps/users/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..012297dd58bbbe90db3eccda3fa795e9f831b41b --- /dev/null +++ b/apps/users/serializers.py @@ -0,0 +1,74 @@ +import re +from datetime import datetime, timedelta + +from rest_framework import serializers +from rest_framework.validators import UniqueValidator +from django.contrib.auth import get_user_model + +from Fresh_Ecommerce.settings import REGEX_MOBILE +from .models import VerifyCode + +User = get_user_model() + +class SmsSerializer(serializers.Serializer): + '''短信发送序列化''' + + mobile = serializers.CharField(max_length=11) + + def validate_mobile(self, mobile): + '''验证手机号码''' + + + # 验证手机号码是否合法 + if not re.match(REGEX_MOBILE, mobile): + raise serializers.ValidationError('手机号格式有误,请重新输入') + + # 验证手机是否注册 + if User.objects.filter(mobile=mobile).count(): + raise serializers.ValidationError('手机号已经被注册过,请更换手机号重新注册或直接使用该手机号登录') + + # 验证短信发送频率 + one_minute_ago = datetime.now() - timedelta(minutes=1) + if VerifyCode.objects.filter(add_time__gt=one_minute_ago, mobile=mobile).count(): + raise serializers.ValidationError('验证码发送频率过快,请稍后再试') + + return mobile + + +class UserRegSerializer(serializers.ModelSerializer): + '''用户序列化''' + code = serializers.CharField(max_length=4, min_length=4, label='验证码', write_only=True, + help_text='验证码', + error_messages={ + 'required': '请输入验证码', + 'blank': '请输入验证码', + 'max_length': '请输入4位验证码', + 'min_length': '请输入4位验证码' + }) + username = serializers.CharField(required=True, allow_blank=False, label='用户名', validators=[UniqueValidator(queryset=User.objects.filter(is_delete=False), message='用户已经存在')]) + password = serializers.CharField(label='密码', write_only=True, style={'input_type': 'password'}) + + def validate_code(self, code): + verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time') + + # 验证验证码是否存在 + if verify_records: + last_record = verify_records[0] + five_minute_ago = datetime.now() - timedelta(minutes=5) + # 验证验证码是否过期 + if five_minute_ago > last_record.add_time: + raise serializers.ValidationError('验证码已过期,请重新验证') + # 验证验证码是否正确 + if last_record.code != code: + raise serializers.ValidationError('验证码错误') + else: + raise serializers.ValidationError('数据有误,请重新验证') + + def validate(self, attrs): + attrs['mobile'] = attrs['username'] + del attrs['code'] + return attrs + + class Meta: + model = User + fields = ['username', 'code', 'mobile', 'password'] \ No newline at end of file diff --git a/apps/users/signals.py b/apps/users/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..b4c14f26411701e27c21cc971086289a08208648 --- /dev/null +++ b/apps/users/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from django.contrib.auth import get_user_model +from django.dispatch import receiver +from rest_framework.authtoken.models import Token + +User = get_user_model() + +@receiver(post_save, sender=User) +def create_auth_token(sender, instance=None, created=False, **kwargs): + if created: + password = instance.password + instance.set_password(password) + instance.save() \ No newline at end of file diff --git a/apps/users/views.py b/apps/users/views.py index 91ea44a218fbd2f408430959283f0419c921093e..1677c58b166e7b955bbf89b34a7b17e4c896735e 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -1,3 +1,87 @@ -from django.shortcuts import render +from random import choice + +from django.db.models import Q +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model +from rest_framework import viewsets, status +from rest_framework.mixins import CreateModelMixin +from rest_framework.response import Response +from rest_framework_jwt.serializers import jwt_encode_handler, jwt_payload_handler + +from .models import VerifyCode +from .serializers import SmsSerializer, UserRegSerializer +from utils.yunpian import yunpian + + +User = get_user_model() # Create your views here. + + +class CustomBackend(ModelBackend): + '''自定义用户验证''' + + def authenticate(self, request, username=None, password=None, **kwargs): + try: + user = User.objects.get(Q(username=username)|Q(mobile=username)) + if user.check_password(password) and user.is_delete != True: + return user + except Exception as e: + return None + + +class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet): + '''发送短信验证码''' + + serializer_class = SmsSerializer + + def generate_code(self): + '''生成4位数验证码''' + seeds = '1234567890' + random_str = [] + for i in range(4): + random_str.append(choice(seeds)) + + return ''.join(random_str) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + mobile = serializer.validated_data['mobile'] + print(mobile) + code = self.generate_code() + sms_status = yunpian.send_sms(code, mobile) + print(sms_status) + if sms_status['code'] != 0: + return Response({ + 'mobile': sms_status['msg'] + }, status=status.HTTP_400_BAD_REQUEST) + else: + code_record = VerifyCode(code=code, mobile=mobile) + code_record.save() + return Response({ + 'mobile': mobile, + 'code': code + }, status=status.HTTP_201_CREATED) + + +class UserViewSet(CreateModelMixin, viewsets.GenericViewSet): + '''用户''' + + serializer_class = UserRegSerializer + queryset = User.objects.filter(is_delete=False) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.perform_create(serializer) + re_dict = serializer.data + payload = jwt_payload_handler(user) + re_dict['token'] = jwt_encode_handler(payload) + re_dict['name'] = user.name if user.name else user.username + headers = self.get_success_headers(serializer.data) + return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer): + return serializer.save() \ No newline at end of file diff --git a/apps/utils/__init__.py b/apps/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/utils/__pycache__/__init__.cpython-38.pyc b/apps/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d719edef4cb82fcb0f4888d69328ae718475568 Binary files /dev/null and b/apps/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/apps/utils/__pycache__/yunpian.cpython-38.pyc b/apps/utils/__pycache__/yunpian.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95899fee6d38e334bb868c882ecce59b497c6887 Binary files /dev/null and b/apps/utils/__pycache__/yunpian.cpython-38.pyc differ diff --git a/apps/utils/yunpian.py b/apps/utils/yunpian.py new file mode 100644 index 0000000000000000000000000000000000000000..6ca20f571a954deee51dcf6272d1e67c8a4925d1 --- /dev/null +++ b/apps/utils/yunpian.py @@ -0,0 +1,26 @@ +import requests +import json + +from Fresh_Ecommerce.settings import APIKEY + +class YunPian(object): + def __init__(self): + self.api_key = APIKEY + self.single_send_url = 'https://sms.yunpian.com/v2/sms/single_send.json' + + def send_sms(self, code, mobile): + params = { + 'apikey': self.api_key, + 'mobile': mobile, + 'text': '【Python进化讲堂】欢迎您注册Fresh_Ecommerce ,验证码:{}(5分钟内有效,如非本人操作,请忽略)'.format(code) + } + response = requests.post(self.single_send_url, data=params) + re_dict = json.loads(response.text) + return re_dict + + +yunpian = YunPian() + + +if __name__ == '__main__': + yunpian.send_sms('1234', '13312345678') # 改为你自己的手机号 \ No newline at end of file diff --git a/fresh_online/src/api/api.js b/fresh_online/src/api/api.js index a9d92ad2ce7c467149a73b0911e261f50a1a6b42..e47ec1e51c35d9d2699ecca007122c0f00bcf908 100644 --- a/fresh_online/src/api/api.js +++ b/fresh_online/src/api/api.js @@ -55,12 +55,12 @@ export const getFav = goodsId => { return axios.get(`${host}/userfavs/`+goodsId+ //登录 export const login = params => { - return axios.post(`${host}/login/`, params) + return axios.post(`${local_host}/login/`, params) } //注册 -export const register = parmas => { return axios.post(`${host}/users/`, parmas) } +export const register = parmas => { return axios.post(`${local_host}/users/`, parmas) } //短信 export const getMessage = parmas => { return axios.post(`${host}/code/`, parmas) } diff --git a/requirements.txt b/requirements.txt index d50c3a5f2bffe3f9add59deeb2137494a0425db1..1bd630193182ce90740952cd0a2bcfc95b4724f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ django-guardian==2.3.0 django-import-export==2.3.0 django-reversion==3.0.7 djangorestframework==3.11.0 +djangorestframework-jwt==1.11.0 et-xmlfile==1.0.1 future==0.18.2 httplib2==0.18.0 @@ -28,6 +29,7 @@ mysqlclient==1.4.6 odfpy==1.4.1 openpyxl==3.0.4 Pillow==7.2.0 +PyJWT==1.7.1 pytz==2020.1 PyYAML==5.3.1 requests==2.24.0