当前位置:   article > 正文

基于Django开发的电商购物平台(完整项目介绍 --> 项目环境 , 项目完整代码 , 项目服务器/虚拟机部署)_django电商平台

django电商平台

1-10_Django项目实战文档

本网站是基于Django+uwsgi+nginx+MySQL+redis+linux+requests开发的电商购物系统,
以及通过使用爬虫技术批量获取商品数据.
实现
客户端: 注册 , 登录 , 浏览记录保存, 购物车 , 订单等功能实现
管理端: 商品添加 , 用户管理等功能

项目内容较多 , 该博文只是对整体的大致思路介绍 , 如有疑问可以私信博主

项目的完整代码可见博主主页上传的资源
项目git地址: https://gitee.com/jixuonline/django_-shop-system
详细介绍: https://blog.csdn.net/xiugtt6141121/category_12658164.html
服务器部署教程: https://blog.csdn.net/xiugtt6141121/article/details/139497427

一、项目环境

python 3.8.10
django 3.2
mysql 5.7.40
redis
  • 1
  • 2
  • 3
  • 4

二、项目环境的配置

1、创建 Django 项目 —— ShopSystem

2、配置 MySQL 的连接引擎

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'shop_10',
        'USER' : 'root',
        'PASSWORD' : 'root',
        'HOST' : '127.0.0.1'
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3、配置静态文件项目检索路径

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
  • 1
  • 2

4、配置内存型数据库,配置 Redis 的连接引擎

# 配置 Redis 缓存数据库信息
CACHES = {
    # 默认使用的 Redis 数据库
    "default":{
        # 配置数据库指定引擎
        "BACKEND" : "django_redis.cache.RedisCache",
        # 配置使用 Redis 的数据库名称
        "LOCATION" : "redis://127.0.0.1:6379/0",
        "OPTIONS":{
            "CLIENT_CLASS" : "django_redis.client.DefaultClient"
        }
    },

    # 将 session 的数据保存位置修改到 redis 中
    "session":{
        # 配置数据库指定引擎
        "BACKEND" : "django_redis.cache.RedisCache",
        # 配置使用 Redis 的数据库名称
        "LOCATION" : "redis://127.0.0.1:6379/1",
        "OPTIONS":{
            "CLIENT_CLASS" : "django_redis.client.DefaultClient"
        }
    },

}

# 修改 session 默认的存储机制
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
# 配置 SESSION 要缓存的地方
SESSION_CACHE_ALIAS = "session"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

三、响应首页

创建响应首页的应用 —— contents

配置响应路由 ,实现响应的视图

# 响应首页
path('' , views.IndexView.as_view())

class IndexView(View):
    '''
    响应首页
    '''
    def get(self , request):
        return render(request , 'index.html')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

四、用户注册

1、实现用户的数据模型类
2、响应注册页面
3、接收用户输入的数据
4、对用户输入的数据进行校验
5、保存用户数据,注册成功
  • 1
  • 2
  • 3
  • 4
  • 5

创建应用实现用户逻辑操作 —— users

1、响应注册页面视图

class RegisterView(View):
    '''
    用户注册
    '''
    def get(self , request):
        return render(request , 'register.html')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、定义用户数据模型类

使用 auth 模块实现保存用户数据,自定义认证模型类别

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    mobile = models.CharField(max_length=11 , unique=True)
    
    class Meta:
        db_table = 'user'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

修改 Django 全局默认的认证模型类

# 配置自定义模型类
AUTH_USER_MODEL = 'users.User'

  • 1
  • 2
  • 3

3、后端数据校验

实现用户数据提交之后,Django 对数据校验是否合法,自定义 forms 表单类进行校验

在应用中创建 forms 模块

from django import forms

class RegisterForm(forms.Form):
    '''
    校验用户注册提交的数据
    '''
    username = forms.CharField(min_length= 5 , max_length= 15,
                               error_messages={
                                   "min_length":"用户名过短",
                                   "max_length":"用户名过长",
                                   "required":"用户名不允许为空"
                               })
    password = forms.CharField(min_length= 6 , max_length= 20,
                               error_messages={
                                   "min_length":"密码过短",
                                   "max_length":"密码过长",
                                   "required":"密码不允许为空"
                               })
    password2 = forms.CharField(min_length= 6 , max_length= 20,
                               error_messages={
                                   "min_length":"密码过短",
                                   "max_length":"密码过长",
                                   "required":"密码不允许为空"
                               })
    mobile = forms.CharField(min_length= 11 , max_length= 11,
                               error_messages={
                                   "min_length":"手机号过短",
                                   "max_length":"手机号过长",
                                   "required":"手机号不允许为空"
                               })
    
    # 使用全局钩子函数 , 检验两个密码是否一致
    def clean(self):
        clean_data = super().clean()
        pw = clean_data.get('password')
        pw2 = clean_data.get('password2')
        if pw != pw2:
            raise forms.ValidationError('两次密码不一致')
        return clean_data

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

在注册视图中,实现获取用户提交的数据, 进行校验数据,保存数据

    def post(self , request):
        # 获取用户提交的数据,将数据传递给 forms 组件进行校验
        register_form = RegisterForm(request.POST)

        # 判断用户校验的数据是否合法
        if register_form.is_valid():
            return HttpResponse('注册成功')
        return HttpResponse('注册失败')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4、校验用户名重复

# 校验用户名重复
re_path('^username/(?P<username>[A-Za-z0-9_]{5,15})/count/$' , views.UsernameCountView.as_view())

class UsernameCountView(View):
    '''
    判断用户名是否重复
    '''
    def get(self , request , username):
        # 根据参数从数据库获取数据
        count = User.objects.filter(username=username).count()
        return JsonResponse({'code':200 , 'errmsg':"OK" , 'count':count})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5、图片验证码

创建一个应用实现验证码的功能 —— verfications

1、配置缓冲验证码的 Redis 数据库

# 缓冲 验证码
    "ver_code":{
        "BACKEND" : "django_redis.cache.RedisCache",
        "LOCATION" : "redis://127.0.0.1:6379/2",
        "OPTIONS":{
            "CLIENT_CLASS" : "django_redis.client.DefaultClient"
        }
    },

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

视图

# 响应图片验证码
re_path('^image_code/(?P<uuid>[\w-]+)/$' , views.ImageCodeView.as_view())

class ImageCodeView(View):
    '''
    响应图片验证码
    '''
    def get(self , request , uuid):
        # 调用生成图片验证码的功能
        image , text = CodeImg.create_img()
        # 将验证码保存到数据库中
        redis_conn = get_redis_connection('ver_code')
        redis_conn.setex('image_%s'%uuid , 400 , text)
        return HttpResponse(image , content_type='image/png')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

修改前端页面中的对应标签内容

<li>
    <label>图形验证码:</label>
    <input type="text" name="image_code" id="pic_code" class="msg_input"
           v-model="image_code" @blur="check_image_code">
    <img v-bind:src="image_code_url" alt="图形验证码" class="pic_code"
         @click="generate_image_code">
    <span class="error_tip" v-show="error_image_code">请填写图形验证码</span>
</li>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

6、短信验证码

什么时候发送短信验证码
图片验证码校验成功之后发送

  • 1
  • 2
  • 3

发送短信验证码的功能使用:https://console.yuntongxun.com/member/main

安装:pip install ronglian_sms_sdk

在应用下创建一个包实现发送短信的功能 —— ronglianyun , 创建模块: ccp_sms

from ronglian_sms_sdk import SmsSDK
import json

accId = '2c94811c88bf3503018900ca795012ba'
accToken = '382b17b971884ddfad5c7ecadc07149b'
appId = '2c94811c88bf3503018900ca7a9d12c1'

# 单例模式
class CCP:

    _instance = None
    def __new__(cls, *args, **kwargs):
        # new 静态类方法,给对象分配内存空间
        if cls._instance is None:
            # 如果类属性数据为 None , 说明当前类中没有实例化对象
            # 给对象创建一个新的对象内存空间
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

    def send_message(self,mobile, datas):
        sdk = SmsSDK(accId, accToken, appId)
        tid = '1'
        resp = sdk.sendMessage(tid, mobile, datas)
        resp = json.loads(resp)
        # 判断短信验证码是否发送成功
        if resp["statusCode"] == "000000":
            # 短信验证码发送成功
            return 0
        else:
            return -1
        
send_code = CCP()
<li>
    <label>短信验证码:</label>
    <input type="text" name="sms_code" id="msg_code" class="msg_input"
           v-model="sms_code" @blur="check_sms_code">
    <a @click="send_sms_code" class="get_msg_code">获取短信验证码</a>
    <span class="error_tip" v-show="error_sms_code">请填写短信验证码</span>
</li>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

定义发送短信验证码的视图

# 发送短信验证码
re_path('^sms_code/(?P<mobile>1[3-9]\d{9})/$' , views.SmsCodeView.as_view()),


class SmsCodeView(View):
    '''
    发送短信验证码
    1、校验图片验证码
    在接收发送短信验证码期间内,不允许重复的调用该视图
    '''
    def get(self , request , mobile):
        # 接收参数:uuid , 用户输入图片验证码
        uuid = request.GET.get('uuid')
        image_code_client = request.GET.get('image_code')

        # 检验请求中的数据是否完整
        if not all([uuid , image_code_client]):
            return HttpResponse("缺少不要的参数")

        # 校验图片验证码
        # 从 Redis 数据库中获取该用户生成的图片验证码
        redis_conn = get_redis_connection('ver_code')
        image_code_server = redis_conn.get('image_%s'%uuid)

        # 从数据库中获取手机号标记变量
        sand_flag = redis_conn.get('sand_%s' % mobile)
        # 判断标记变量是否有值
        if sand_flag:
            return JsonResponse({'code':RETCODE.THROTTLINGERR , 'errmsg':'发送短信验证码过于频繁'})

        # 判断图片验证码是否在有效期内
        if image_code_server is None:
            return JsonResponse({'code':RETCODE.IMAGECODEERR , 'errmsg':'图片验证码失效'})
        # 将图片验证码删除
        redis_conn.delete('image_%s'%uuid)
        # 判断图片验证码是否正确
        image_code_server = image_code_server.decode()
        if image_code_client.lower() != image_code_server.lower():
            return JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '图片验证码输入错误'})
        # 图片验证码正确 , 发送短信验证码
        # 生成短信验证码
        sms_code = '%05d'%random.randint(0,99999)
        # 保存短信验证码
        redis_conn.setex('code_%s' % mobile, 400, sms_code)
        # 保存该手机的标记变量到数据库
        redis_conn.setex('sand_%s' % mobile, 60, 1)
        # 发送短信验证码
        send_code.send_message(mobile , (sms_code , 5))
        return JsonResponse({'code':RETCODE.OK , 'errmsg':'短信验证码发送成功'})
在项目中 ajax 响应的状态很多种。同一讲状态保存到一个文件包中,需要的时候进行调用。
在项目中创建一个共用包:utils
将 response_code 模块文本保存进去

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

7、完善注册视图

在 users 应用中的 forms 内对短信验证码的字段进行校验

sms_code = forms.CharField(max_length=5 , min_length=5)
class RegisterView(View):
    '''
    用户注册
    '''
    def get(self , request):
        return render(request , 'register.html')

    def post(self , request):
        # 获取用户提交的数据,将数据传递给 forms 组件进行校验
        register_form = RegisterForm(request.POST)

        # 判断用户校验的数据是否合法
        if register_form.is_valid():
            # 数据合法
            username = register_form.cleaned_data.get('username')
            password = register_form.cleaned_data.get('password')
            mobile = register_form.cleaned_data.get('mobile')
            sms_code_client = register_form.cleaned_data.get('sms_code')

            # 从 redis 中获取生成短信验证码的
            redis_conn = get_redis_connection('ver_code')
            sms_code_server = redis_conn.get('code_%s' % mobile)
            # 判断短信验证码是否有效
            if sms_code_server is None:
                return render(request , 'register.html' , {'sms_code_errmsg':'短信验证码失效'})

            if sms_code_client != sms_code_server.decode():
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码输入错误'})
            # 将数据保存到数据库中
            user = User.objects.create_user(username=username , password=password , mobile=mobile)
            # 做状态保持
            login(request , user)
            # 注册成功响应到登录页面
            return redirect('login')
        else:
            # 用户数据不合法
            # 从 forms 组件中获取数据异常信息
            context = {'forms_errmsg':register_form.errors}
            return render(request ,'register.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

修改前端注册页面中对应的部分标签 , 获取后端的校验用户数据的异常信息

<li class="reg_sub">
    <input type="submit" value="注 册">
    <!-- 获取后端校验表单数据的异常信息 -->
    {% if forms_errmsg %}
    <span style="color: red">{{ forms_errmsg }}</span>
    {% endif %}
</li>
<li>
    <label>短信验证码:</label>
    <input type="text" name="sms_code" id="msg_code" class="msg_input"
           v-model="sms_code" @blur="check_sms_code">
    <a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a>
    <span class="error_tip" v-show="error_sms_code">请填写短信验证码</span>
    <!-- 获取后端校验短信验证码的异常信息 -->
    {% if sms_code_errmsg %}
    <span style="color: red">{{ sms_code_errmsg }}</span>
    {% endif %}
</li>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

五、用户登录

1、响应登录页面的视图

# 用户登录
path('login/' , views.LoginView.as_view() , name='login'),

class LoginView(View):
    '''
    用户登录视图
    '''
    def get(self , request):
        return render(request , 'login.html')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2、用户名登录

1、校验用户数据

class LoginForm(forms.Form):
    username = forms.CharField(min_length=5, max_length=15,
                               error_messages={
                                   "min_length": "用户名过短",
                                   "max_length": "用户名过长",
                                   "required": "用户名不允许为空"
                               })
    password = forms.CharField(min_length=6, max_length=20,
                               error_messages={
                                   "min_length": "密码过短",
                                   "max_length": "密码过长",
                                   "required": "密码不允许为空"
                               })
    remembered = forms.BooleanField(required=False)
class LoginView(View):
    '''
    用户登录视图
    '''
    def get(self , request):
        return render(request , 'login.html')

    def post(self , request):
        login_form = LoginForm(request.POST)

        if login_form.is_valid():
            username = login_form.cleaned_data.get('username')
            password = login_form.cleaned_data.get('password')
            remembered = login_form.cleaned_data.get('remembered')

            if not all([username , password]):
                return HttpResponse('缺少必要的参数')

            # 通过认证模块到数据库中进行获取用户数据
            user = authenticate(username=username , password=password)

            # 判断在数据库中是否能够查询到用户数据
            if user is None:
                return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'})

            # 状态保持
            login(request , user)

            # 判断用户是否选择记住登录的状态
            if remembered:
                # 用户选择记住登录状态 , 状态默认保存14天
                request.session.set_expiry(None)
            else:
                # 用户状态不保存 , 关闭浏览器 , 数据销毁
                request.session.set_expiry(0)
            # 响应首页
            return redirect('index')
        else:
            context = {'forms_errors':login_form.errors}
            return render(request , 'login.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

3、手机号登录

Django 的 auth 认证系统中 默认是使用 用户名认证,使用其他数据进行认证 , 需要重新定义认证系统

1、实现一个类 , 这个类继承 ModelBackend
2、重写认证方法 authenticate

  • 1
  • 2
  • 3

在 users 的应用下 创建一个文件 —— utils

from django.contrib.auth.backends import ModelBackend
from users.models import User
import re

# 定义一个方法可以用手机号或者用户名查询数据的
def get_user(account):
    try:
        if re.match(r'1[3-9]\d{9}' , account):
            user = User.objects.get(mobile=account)
        else:
            user = User.objects.get(username=account)
    except Exception:
        return None
    else:
        return user


class UsernameMobileBackend(ModelBackend):
    # 重写用户认证方法
    def authenticate(self, request, username=None, password=None, **kwargs):
        # 调用查询用户数据的方法
        user = get_user(username)
        # 判断密码是否正确
        if user.check_password(password) and user:
            return user
        else:
            return None

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

修改 Django 项目中的配置文件的全局认证

# 配置自定义认证的方法
AUTHENTICATION_BACKENDS = ['users.utils.UsernameMobileBackend']

  • 1
  • 2
  • 3

4、首页显示用户名

在后端的登录请求的视图中,在用户登录成功之后将用户名写到 Cookie 中。

    def post(self , request):
        login_form = LoginForm(request.POST)

        if login_form.is_valid():
            username = login_form.cleaned_data.get('username')
            password = login_form.cleaned_data.get('password')
            remembered = login_form.cleaned_data.get('remembered')

            if not all([username , password]):
                return HttpResponse('缺少必要的参数')

            # 通过认证模块到数据库中进行获取用户数据
            user = authenticate(username=username , password=password)

            # 判断在数据库中是否能够查询到用户数据
            if user is None:
                return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'})

            # 状态保持
            login(request , user)

            # 判断用户是否选择记住登录的状态
            if remembered:
                # 用户选择记住登录状态 , 状态默认保存14天
                request.session.set_expiry(None)
            else:
                # 用户状态不保存 , 关闭浏览器 , 数据销毁
                request.session.set_expiry(0)
            # 响应首页
            response = redirect('index')
            # 将获取到的 用户名写入到 Cookie 中
            response.set_cookie('username' , user.username , 3600)
            return response
        else:
            context = {'forms_errors':login_form.errors}
            return render(request , 'login.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

5、用户退出登录

退出登录:将用户的数据从 SESSION 会话中删除掉。

logout方法: 清除 session 会话的数据

<div class="login_btn fl">
    欢迎您:<em>[[ username ]]</em>
    <span>|</span>
    <a href="{% url 'logout' %}">退出</a>
</div>
# 退出登录
path('logout/' , views.LogoutView.as_view() , name='logout')

class LogoutView(View):
    '''
    用户退出登录
    '''
    def get(self , request):
        # 清除用户保存的数据
        logout(request)
        response = redirect('index')
        # 清除保存的 Cookie 数据
        response.delete_cookie('username')
        return response

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

六、用户中心

1、响应用户中心

# 用户中心
path('info/' , views.UserInfoView.as_view() , name='info')

class UserInfoView(View):
    '''
    用户中心
    '''
    def get(self , request):
        return render(request , 'user_center_info.html')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2、判断登录状态

使用 Django 用户认证提供 :LoginRequiredMixin 进行用户登录判断并且可以配置重定向到原来的请求页面 , 实现效果直接继承即可

class UserInfoView(LoginRequiredMixin , View):
    '''
    用户中心
    '''
    def get(self , request):
        return render(request , 'user_center_info.html')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

到配置文件中重新定义认证登录重定向的 url

# 配置项目认证登录的重定向
LOGIN_URL = '/login/'

  • 1
  • 2
  • 3

修改登录的视图

    def post(self , request):
        login_form = LoginForm(request.POST)

        if login_form.is_valid():
            username = login_form.cleaned_data.get('username')
            password = login_form.cleaned_data.get('password')
            remembered = login_form.cleaned_data.get('remembered')

            if not all([username , password]):
                return HttpResponse('缺少必要的参数')

            # 通过认证模块到数据库中进行获取用户数据
            user = authenticate(username=username , password=password)

            # 判断在数据库中是否能够查询到用户数据
            if user is None:
                return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'})

            # 状态保持
            login(request , user)

            # 判断用户是否选择记住登录的状态
            if remembered:
                # 用户选择记住登录状态 , 状态默认保存14天
                request.session.set_expiry(None)
            else:
                # 用户状态不保存 , 关闭浏览器 , 数据销毁
                request.session.set_expiry(0)
                
            next = request.GET.get('next')
            if next:
                # next 有值,重定向到指定的 url 
                response = redirect(next)
            else:
                # 响应首页
                response = redirect('index')
            # 将获取到的 用户名写入到 Cookie 中
            response.set_cookie('username' , user.username , 3600)
            return response
        else:
            context = {'forms_errors':login_form.errors}
            return render(request , 'login.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

3、显示获取用户信息

class UserInfoView(LoginRequiredMixin , View):
    '''
    用户中心
    '''
    def get(self , request):
        # 从 request 中获取用户信息
        context = {
            "username" : request.user.username,
            "mobile" : request.user.mobile,
            "email" : request.user.email,
        }
        return render(request , 'user_center_info.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

4、添加邮箱

1、在用户数据模型类中补充:邮箱验证状态的字段

# 邮箱验证字段
email_active = models.BooleanField(default=False)

  • 1
  • 2
  • 3

验证登录的: LoginRequiredMixin 要求的返回值是 HttpResponse 对象 , 如果返回值的是 json 类型必须重写类方法

在项目全局的 utlis 包创建一个 view 模块

from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from utils.response_code import RETCODE

class LoginRequiredJSONMixin(LoginRequiredMixin):
    
    def handle_no_permission(self):
        # 让这个返回可以返回 json 类型对象即可
        return JsonResponse({'code':RETCODE.SESSIONERR , 'errmsg':'用户未登录'})
class EmailView(LoginRequiredJSONMixin , View):
    '''
    用户添加邮箱
    '''
    def put(self , request):
        # put 请求的参数放在 request 的 body 中,并且一个字节传输的数据
        # b'{'email':'123@com'}'
        json_str = request.body.decode()
        # '{'email':'123@com'}'
        # 使用 json 进行反序列化
        json_dict = json.loads(json_str)
        email = json_dict.get('email')

        # 校验数据
        if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$' , email):
            return HttpResponseForbidden('邮箱参数有误')

        # 保存邮箱到数据库中
        request.user.email = email
        request.user.save()

        # 添加成功
        return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK'})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

5、验证邮箱

让 Django 发送邮件 , 是无法直接发送,需要借助SMTP服务器进行中转

需要到 Django 项目的配置文件中配置邮箱的需要的信息

# 发送邮件的配置参数
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 	# 指定邮件后端
EMAIL_HOST = 'smtp.163.com'	 	# 发邮件主机
EMAIL_PORT = 25		# 发邮件的端口
EMAIL_HOST_USER = '17841687578@163.com'		# 授权邮箱
EMAIL_HOST_PASSWORD = 'BHPRRXBTMTCTGHVU'		# 邮箱授权时获取的密码,非登录邮箱的密码
EMAIL_FROM = 'AC-<17841687578@163.com>'		# 发件人抬头

# 设置邮箱的激活连接
EMAIL_VERIFY_URL = 'http://127.0.0.1:8000/verification/'
以网易云为例:在设置打开 SMTP/POP3
开启 IMAP/SMTP 和 POP3/SMTP
获取授权码

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

发送邮件

from django.core.mail import send_mail

subject = '邮件验证'
message = '阿宸真的超级帅'
from_email = 'AC-<17841687578@163.com>'
recipient_list = ['17841687578@163.com',]
html_message = '<h1>阿宸真的超级帅</h1>'
send_mail(subject, message, from_email, recipient_list , html_message=html_message)
'''
    subject: 邮件标题
    message: 邮件正文(普通的文本文件,字符串)
    from_email: 发件人抬头
    recipient_list: 收件人邮箱 (列表格式)
    html_message: 邮件正文(文件可以带渲染格式)
    '''

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

生成邮件激活连接 , 在 users 应用下的 utils 中操作

下载模块 itsdangerou==1.1.0
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSS
from ShopSystem import settings

def generate_verify_email_url(user):
    '''
    生成邮箱激活连接
    '''
    # 调用加密的方法
    s = TJWSS(settings.SECRET_KEY , 600)
    # 获取用户的基本数据
    data = {'user_id':user.id , 'email':user.email}
    token = s.dumps(data)
    return settings.EMAIL_VERIFY_URL+'?token='+token.decode()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在保存邮箱的视图中 , 进行对邮箱发送一个验证连接

class EmailView(LoginRequiredJSONMixin , View):
    '''
    用户添加邮箱
    '''
    def put(self , request):
        # put 请求的参数放在 request 的 body 中,并且一个字节传输的数据
        # b'{'email':'123@com'}'
        json_str = request.body.decode()
        # '{'email':'123@com'}'
        # 使用 json 进行反序列化
        json_dict = json.loads(json_str)
        email = json_dict.get('email')

        # 校验数据
        if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$' , email):
            return HttpResponseForbidden('邮箱参数有误')

        # 保存邮箱到数据库中
        request.user.email = email
        request.user.save()

        # 发送验证邮件
        subject = 'AC商城邮箱验证'
        # 调用生成加密验证邮箱连接
        veerify_url = generate_verify_email_url(request.user)
        html_message = f'<p>您的邮箱为:{email} , 请点击链接进行验证激活邮箱</p>' \
                       f'<p><a href="{veerify_url}">{veerify_url}</p>'
        send_mail(subject , '' , from_email=settings.EMAIL_FROM ,
                  recipient_list=[email],html_message=html_message)

        # 添加成功
        return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK'})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在 Django 中实现邮箱的数据验证,需要接收用户邮箱连接发送过来的参数

进行对参数解码,校验

def check_verify_email_token(token):
    '''
    校验邮箱连接中的参数
    '''
    s = TJWSS(settings.SECRET_KEY, 600)
    data = s.loads(token)
    # 获取解码好之后的参数
    user_id = data.get('user_id')
    email = data.get('email')
    # 从数据库中查询是否有该用户
    try:
        user = User.objects.get(id=user_id ,email=email)
    except Exception:
        return None
    else:
        return user

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

验证邮箱的视图

# 验证邮箱
path('verification/' , views.VerifyEmailView.as_view()),

class VerifyEmailView(View):
    '''
    邮箱验证
    '''
    def get(self , request):
        token = request.GET.get('token')
        if not token:
            return HttpResponseForbidden('缺少必要参数')

        # 调用解码的方法
        user = check_verify_email_token(token)

        if not user:
            return HttpResponseForbidden('用户不存在')

        # 判断用户优先是否已经验证
        if user.email_active == 0:
            # 邮箱没有验证
            user.email_active = 1
            user.save()
        else:
            return HttpResponseForbidden('用户邮箱已验证')
        # 验证成功,重定向到用户中心
        return redirect('info')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

6、收货地址

class AddressView(View):
    '''
    用户收货地址
    '''
    def get(self , request):
        return render(request , 'user_center_site.html')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

7、实现全国省市区名称数据

创建一个应用来操作实现地区数据 —— areas

设计地区数据模型类

from django.db import models

# 自关联
# id   name   -id
#  1  广东省   null
#  2  湖北省
#  3  广州市    1
#  4  天河区    3

class Area(models.Model):
    name = models.CharField(max_length=20)
    # 自关联 : self
    # SET_NULL: 删除被关联的数据 , 对应链接的数据字段值会设置为 NULL
    parent = models.ForeignKey('self' , on_delete=models.SET_NULL , null=True,blank=True,related_name='subs')
    
    class Meta:
        db_table = 'areas'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在前端中使用 ajax 发送 url = /areas/ ; 获取地区数据 , 判断当请求路由中没有携带参数,则获取的是省份的数据

携带了 area_id=1,获取的是市或者区的数据

from django.shortcuts import render
from django.views import View
from areas.models import Area
from utils.response_code import RETCODE
from django.http import JsonResponse
from django.core.cache import cache

class AreasView(View):
    '''
    响应地区数据
    '''
    def get(self , request):
        area_id = request.GET.get('area_id')
        # 判断是否存在 area_id 参数
        if not area_id:
            # 判断这个数据在内存中是否存在
            province_list = cache.get('province_list')
            if not province_list:
                # 获取省份的数据
                province_model_list = Area.objects.filter(parent_id__isnull=True)
                '''
                响应 json 数据
                {
                    'code' : 200
                    'errmsg' : OK
                    'province_list':[
                        {id:110000 ; name:北京市},
                        {id:120000 ; name:天津市},
                        ……
                    ]
                }
                '''
                province_list = []
                for province_model in province_model_list:
                    province_dict = {
                        "id" : province_model.id,
                        "name" : province_model.name,
                    }
                    province_list.append(province_dict)
                # 将数据缓存到内存中
                cache.set('province_list',province_list , 3600)
            return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'province_list':province_list})
        else:
            # 获取 市 或者 区 的数据
            '''
            {
                'code' : 200
                'errmsg' : OK
                sub_data : {
                    id : 省110000
                    name : 广东省
                    subs : [
                        {id , name},
                        {id , name},
                        {id , name},
                        ……
                    ]
                }
            }
            '''
            sub_data = cache.get('sub_data_%s'%area_id)
            if not sub_data:
                parent_model = Area.objects.get(id=area_id)
                # 获取关联 area_id 的对象数据
                sub_model_list = parent_model.subs.all()
                subs = []
                for sub_model in sub_model_list:
                    sub_dict = {
                        'id' : sub_model.id,
                        'name' : sub_model.name,
                    }
                    subs.append(sub_dict)
                sub_data = {
                    'id' : parent_model.id,
                    'name' : parent_model.name,
                    'subs' : subs
                }
                cache.set('sub_data_%s'%area_id , sub_data , 3600)
            return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'sub_data':sub_data})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

修改前端 user_center_site.html 中对应标签的内容

<div class="form_group">
    <label>*所在地区:</label>
    <select v-model="form_address.province_id">
        <option value="0">请选择</option>
        <option :value="province.id" v-for="province in provinces">[[ province.name ]]</option>
    </select>
    <select v-model="form_address.city_id">
        <option value="0">请选择</option>
        <option :value="city.id" v-for="city in cities">[[ city.name ]]</option>
    </select>
    <select v-model="form_address.district_id">
        <option value="0">请选择</option>
        <option :value="district.id" v-for="district in districts">[[ district.name ]]</option>
    </select>
</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

8、创建收货地址模型类

让其他模型类可以共用时间的字段,在项目中的 utils 包内创建一个 model 文件

from django.db import models

class BaseModel(models.Model):
    # 创建时间
    create_time = models.DateTimeField(auto_now_add=True)
    # 更新时间  
    update_time = models.DateTimeField(auto_now=True)
    
    class Meta:
        # 在迁移数据库的时候不为该模型类单独创建一张表
        abstract = True

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

需要使用时间的模型类继承上面该类即可。

创建用户收货地址模型类

class Address(BaseModel):
    # 用户收货地址
    # 关联用户
    user = models.ForeignKey(User , on_delete=models.CASCADE , related_name='address')
    receiver = models.CharField(max_length=20)
    province = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='province_address')
    city = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='city_address')
    district = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='district_address')
    palce = models.CharField(max_length=50)
    mobile = models.CharField(max_length=11)
    tel = models.CharField(max_length=20 , null=True , blank=True , default='')
    email = models.CharField(max_length=20 , null=True , blank=True , default='')
    is_delete = models.BooleanField(default=False)

    class Meta:
        db_table = 'address'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在用户个人数据模型类中保存默认收货地址 , 默认地址只有一个

default_address = models.ForeignKey('Address' , on_delete=models.SET_NULL , null=True, blank=True , related_name='users')

  • 1
  • 2

9、修改密码

# 修改密码
path('changepwd/' , views.ChangePasswordView.as_view(), name='changepwd'),

class ChangePasswordView(View):
    '''
    用户修改密码
    '''
    def get(self , request):
        return render(request,'user_center_pass.html')

    def post(self , request):
        # 接收用户输入的密码
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        new_password2 = request.POST.get('new_password2')

        # 校验数据 , 数据是否完整
        if not all([old_password , new_password , new_password2]):
            return HttpResponseForbidden('缺少必要的数据')

        # 校验旧密码是否正确
        if not request.user.check_password(old_password):
            return render(request , 'user_center_pass.html' , {'origin_password_errmsg':'旧密码不正确'})

        # 校验新密码中的数据是否合法
        if not re.match(r'^[0-9A-Za-z]{6,20}$' , new_password):
            return render(request, 'user_center_pass.html', {'change_password_errmsg': '密码格式不正确'})

        # 校验两次新密码是否一致
        if new_password != new_password2:
            return render(request, 'user_center_pass.html', {'change_password_errmsg': '两次密码不一致'})

        # 密码数据正确合法,将新的密码重新保存
        request.user.set_password(new_password)
        request.user.save()

        # 跟新状态保持 , 清理原有的密码数据
        logout(request)
        response = redirect('login')
        response.delete_cookie('username')
        return response

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

10、新增收货地址

# 新增收货地址
path('addresses/create/', views.AddressCreateView.as_view()),

class AddressCreateView(View):
    '''
    用户新增收货地址
    '''
    def post(self , request):
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        receiver = json_dict.get('receiver')
        province_id = json_dict.get('province_id')
        city_id = json_dict.get('city_id')
        district_id = json_dict.get('district_id')
        place = json_dict.get('place')
        mobile = json_dict.get('mobile')
        tel = json_dict.get('tel')
        email = json_dict.get('email')

        # 校验数据 , 数据完整性
        if not all([receiver , province_id , city_id , district_id , place , mobile]):
            return HttpResponseForbidden('缺少不要数据')
        if not re.match(r'^1[3-9]\d{9}$' , mobile):
            return HttpResponseForbidden('手机号输入有误')
        if tel:
            if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
                return HttpResponseForbidden('固定电话输入有误')
            if email:
                if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
                    return HttpResponseForbidden('邮箱输入有误')

                # 将数据保存到数据库中
                address = Address.objects.create(
                    user=request.user,
                    receiver = receiver,
                    province_id = province_id,
                    city_id = city_id,
                    district_id = district_id,
                    palce = place,
                    mobile = mobile,
                    tel = tel,
                    email = email,
                )
                address_dict = {
                    'id' : address.id,
                    'receiver': address.receiver,
                    'province': address.province.name,
                    'city': address.city.name,
                    'district': address.district.name,
                    'place': address.palce,
                    'mobile': address.mobile,
                    'tel': address.tel,
                    'email': address.email,
                }
                return  JsonResponse({'code':RETCODE.OK , 'errmsg':'新增地址成功' , 'address':address_dict})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

11、渲染收货地址

class AddressView(View):
    '''
    用户收货地址
    '''
    def get(self , request):
        # 获取当前登录的用户信息
        login_user = request.user
        # 根据当前登录的用户信息,获取对应的地址数据
        addresses = Address.objects.filter(user=login_user , is_delete=False)

        address_list = []
        for address in addresses:
            address_dict = {
                'id': address.id,
                'receiver': address.receiver,
                'province': address.province.name,
                'city': address.city.name,
                'district': address.district.name,
                'place': address.palce,
                'mobile': address.mobile,
                'tel': address.tel,
                'email': address.email,
            }
            address_list.append(address_dict)
        context = {
            'addresses' : address_list,
            # 获取用户的默认收货地址
            'default_address_id' : login_user.default_address_id,
            # 计算用户的收货地址个数
            'count':addresses.count()
        }
        return render(request , 'user_center_site.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

修改前端对应标签:user_center_site.html

<div class="right_content clearfix" v-cloak>
    <div class="site_top_con">
        <a @click="show_add_site">新增收货地址</a>
        <span>你已创建了<b>{{ count }}</b>个收货地址,最多可创建<b>20</b></span>
    </div>
    <div class="site_con" v-for="(address , index) in addresses" :key="address.id">
        <div class="site_title">
            <h3>[[ address.receiver ]]</h3>
            <a @click="show_edit_title(index)" class="edit_icon"></a>
            <em v-if="address.id === default_address_id">默认地址</em>
            <span class="del_site" @click="delete_address(index)">×</span>
        </div>
        <ul class="site_list">
            <li><span>收货人:</span><b>[[ address.receiver ]]</b></li>
            <li><span>所在地区:</span><b>[[ address.province ]] [[ address.city ]] [[ address.district ]]</b></li>
            <li><span>地址:</span><b>[[ address.place ]]</b></li>
            <li><span>手机:</span><b>[[ address.mobile ]]</b></li>
            <li><span>固定电话:</span><b>[[ address.tel ]]</b></li>
            <li><span>电子邮箱:</span><b>[[ address.email ]]</b></li>
        </ul>
        <div class="down_btn">
            <a v-if="address.id != default_address_id" @click="set_default(index)">设置默认地址</a>
            <a class="edit_icon" @click="show_edit_site(index)" >编辑</a>
        </div>
    </div>
</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

12、修改\删除收货地址

# 修改、删除收货地址
re_path('^addresses/(?P<address_id>\d+)/$', views.UpdateAddressView.as_view()),

class UpdateAddressView(View):
    '''
    修改/删除收货地址
    '''
    def put(self , request , address_id):
        # 用户修改收货地址
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        receiver = json_dict.get('receiver')
        province_id = json_dict.get('province_id')
        city_id = json_dict.get('city_id')
        district_id = json_dict.get('district_id')
        place = json_dict.get('place')
        mobile = json_dict.get('mobile')
        tel = json_dict.get('tel')
        email = json_dict.get('email')

        # 校验数据 , 数据完整性
        if not all([receiver, province_id, city_id, district_id, place, mobile]):
            return HttpResponseForbidden('缺少不要数据')
        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return HttpResponseForbidden('手机号输入有误')
        if tel:
            if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
                return HttpResponseForbidden('固定电话输入有误')
            if email:
                if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
                    return HttpResponseForbidden('邮箱输入有误')

                Address.objects.filter(id=address_id).update(
                    user=request.user,
                    receiver=receiver,
                    province_id=province_id,
                    city_id=city_id,
                    district_id=district_id,
                    palce=place,
                    mobile=mobile,
                    tel=tel,
                    email=email,
                )
                address = Address.objects.get(id=address_id)
                address_dict = {
                    'id': address.id,
                    'receiver': address.receiver,
                    'province': address.province.name,
                    'city': address.city.name,
                    'district': address.district.name,
                    'place': address.palce,
                    'mobile': address.mobile,
                    'tel': address.tel,
                    'email': address.email,
                }
                return JsonResponse({'code': RETCODE.OK, 'errmsg': '修改地址成功', 'address': address_dict})

            def delete(self , request , address_id):
                # 删除地址
                address = Address.objects.get(id=address_id)
                address.is_delete = True
                address.save()
                return JsonResponse({'code': RETCODE.OK, 'errmsg': '删除地址成功'})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

13、设置默认收货地址

# 设置用户默认收货地址
re_path('^addresses/(?P<address_id>\d+)/default/$', views.DefaultAddressView.as_view()),

class DefaultAddressView(View):
    '''
    设置默认收货地址
    '''
    def put(self , request , address_id):
        # 获取默认地址的数据对象
        address = Address.objects.get(id=address_id)
        request.user.default_address = address
        request.user.save()
        return JsonResponse({'code': RETCODE.OK, 'errmsg': '默认收货地址设置成功'})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

七、商品数据

SPU:标准产品单位 , 表示一组类似属性或者特征的商品集合。【商品的基本信息和属性:名称,描述,品牌】 , 对商品的基本定义

SKU:库存单位,表示具体的商品或者库存【商品的规格 , 价格,尺码,版本】

  • 1
  • 2
  • 3
  • 4

创建一个应用来操作商品 相关数据 —— goods

迁移数据库 , 执行 goods_data.sql 插入商品数据

1、首页的商品分类

{
1:{
	channels:[
		{id:1 , name:手机 , url:},
		{}
		{}
	],
	sub_cats:[
		{id:500
		name:手机通讯,
		sub_cat:[
			{id:520 , name:华为},
			{},……
		]},
		{}……
	]
},
2:{},
3:{},
……
}
class IndexView(View):
    '''
    响应首页
    '''
    def get(self , request):

        # 定义一个空的字典 , 存放商品频道分类的数据
        categories = {}
        # 查询商品分组频道的所有数据
        channels = GoodsChannel.objects.all()
        # 获取到所有的商品频道组
        for channel in channels:
            # 获取商品频道组的 id ,作为分组的 key
            group_id = channel.group_id
            # 判断获取到的分组 id 在字典中是否存在 , 存在则不添加
            if group_id not in categories:
                categories[group_id] = {'channels':[] , 'sub_cats':[]}

            # 查询一级商品的数据信息
            # 根据商品的外键数据 , 判断是否为一级商品类别
            cat1 = channel.category
            categories[group_id]['channels'].append(
                {
                    'id':cat1.id,
                    'name':cat1.name,
                    'url': channel.url
                }
            )

            # 获取二级的商品类别数据
            # 二级的数据根据一级的类别 id 进行获取:cat1.subs.all()
            for cat2 in cat1.subs.all():
                cat2.sub_cats = []
                categories[group_id]['sub_cats'].append(
                    {
                        'id':cat2.id,
                        'name':cat2.name,
                        'sub_cat':cat2.sub_cats
                    }
                )
                # 获取三级的数据
                for cat3 in cat2.subs.all():
                    cat2.sub_cats.append(
                        {
                            'id':cat3.id,
                            'name':cat3.name
                        }
                    )

        # 首页商品推荐广告数据
        # 获取所有的推荐商品广告类别
        content_categories = ContentCategory.objects.all()
        contents = {}
        for content_category in content_categories:
            contents[content_category.key] = Content.objects.filter(
                category_id=content_category.id,
                status=True
            ).all().order_by('sequence')

        context = {'categories':categories , 'contents':contents}
        print(contents)
        return render(request , 'index.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

修改index.html页面中对应的标签内容

<ul class="slide">
    {% for content in contents.index_lbt %}
    <li><a href="{{ content.url }}"><img src="/static/images/goods/{{ content.image }}.jpg" alt="幻灯片"></a></li>
    {% endfor %}
</ul>
<div class="news">
    <div class="news_title">
        <h3>快讯</h3>
        <a href="#">更多 &gt;</a>
    </div>
    <ul class="news_list">
        {% for content in contents.index_kx %}
        <li><a href="{{ content.url }}">{{ content.title }}</a></li>
        {% endfor %}
    </ul>
    {% for content in contents.index_ytgg %}
    <a href="{{ content.url }}" class="advs"><img src="/static/images/goods/{{ content.image }}.jpg"></a>
    {% endfor %}
</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2、商品列表页

因为商品列表中页需要商品分类的功能 , 将首页中的商品分类功能进行抽取出来单独定义一个模块中,需要导入调用。

在 contents 应用中。创建 utils 模块 , 实现商品分类模块功能。

制作列表页中列表导航栏(面包屑) , 在应用中创建 utils 模块 , 列表导航栏(面包屑) 。

from goods.models import GoodsCategory

def get_breadcrumb(category):
    # 一级:breadcrumb = {cat1:''}
    # 二级:breadcrumb = {cat1:'',cat2:''}
    # 三级:breadcrumb = {cat1:'',cat2:'',cat3:''}
    breadcrumb = {'cat1':'','cat2':'','cat3':''} 
    if category.parent == None:
        # 没有外键数据 , 说明类别属于一级
        breadcrumb['cat1'] = category
    elif GoodsCategory.objects.filter(parent_id = category.id).count() == 0:
        # 判断是否有外键被链接对象 , 如果没有说明这个是三级的数据
        # 三级是通过二级间接连接到一级的数据 , 无法直接拿到一级的名称
        cat2 = category.parent
        breadcrumb['cat1'] = cat2.parent
        breadcrumb['cat2'] = cat2
        breadcrumb['cat3'] = category
    else:
        # 二级
        breadcrumb['cat1'] = category.parent
        breadcrumb['cat2'] = category
    return breadcrumb
class GoodsListView(View):
    '''
    商品列表页
    '''
    def get(self ,request ,  category_id , pag_num):
        categories = get_categories()
        # 获取到当前列表的商品类别对象
        category = GoodsCategory.objects.get(id=category_id)
        # 调用生成面包屑的功能
        breadcrumb = get_breadcrumb(category)

        # 商品排序
        # 获取请求的参数:sort , 进行判断商品排序的方式
        # 如果没有 sort 参数 , 则按照默认排序
        sort = request.GET.get('sort' , 'default')
        if sort == 'price':
            sort_field = 'price'
        elif sort == 'hot':
            sort_field = 'sales'
        else:
            sort = 'default'
            sort_field = 'create_time'

        skus = SKU.objects.filter(is_launched=True , category_id=category_id).order_by(sort_field)

        # 对商品进行分页
        paginator = Paginator(skus , 5)
        # 获取当前页面的数据
        page_skus = paginator.page(pag_num)
        # 获取分页的总数
        total_num = paginator.num_pages

        context = {
            'categories':categories,
            'breadcrumb':breadcrumb,
            'page_skus':page_skus,
            'sort':sort,
            'pag_num':pag_num,
            'category_id':category_id,
            'total_num':total_num
        }
        return render(request , 'list.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

修改前端list.html对应的标签内容

<div class="breadcrumb">
    <a href="http://shouji.jd.com/">{{ breadcrumb.cat1.name }}</a>
    <span>></span>
    <a href="javascript:;">{{ breadcrumb.cat2.name }}</a>
    <span>></span>
    <a href="javascript:;">{{ breadcrumb.cat3.name }}</a>
</div>
<div class="r_wrap fr clearfix">
    <div class="sort_bar">
        <a href="{% url 'list' category_id pag_num %}?sort=default" 
           {% if sort == 'default' %} class="active"{% endif %}>默认</a>
        <a href="{% url 'list' category_id pag_num %}?sort=price"
           {% if sort == 'price' %} class="active"{% endif %}>价格</a>
        <a href="{% url 'list' category_id pag_num %}?sort=hot"
           {% if sort == 'hot' %} class="active"{% endif %}>人气</a>
    </div>
    <ul class="goods_type_list clearfix">
        {% for sku in page_skus %}
        <li>
            <a href="detail.html"><img src="/static/images/goods{{ sku.default_image.url }}.jpg"></a>
            <h4><a href="detail.html">{{ sku.name }}</a></h4>
            <div class="operate">
                <span class="price">¥{{ sku.price }}</span>
                <span class="unit"></span>
                <a href="#" class="add_goods" title="加入购物车"></a>
            </div>
        </li>
        {% endfor %}
    </ul>
</div>
<script>
    $(function () {
        $('#pagination').pagination({
            currentPage: {{ pag_num }},
                                    totalPage: {{ total_num }},
      callback:function (current) {
        location.href = '/list/{{ category_id }}/' + current + '/?sort={{ sort }}';
    }
    })
    });
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

在页面的底部

<!-- 分页器盒子 -->
<div class="pagenation">
    <div id="pagination" class="page"></div>
</div>

  • 1
  • 2
  • 3
  • 4
  • 5

3、热销商品排行

# 热销商品排行
re_path('^hot/(?P<category_id>\d+)/$' , views.HotGoodsView.as_view()),

class HotGoodsView(View):
    '''
    热销商品排行
    '''
    def get(self , request , category_id):
        # 获取该类别商品的数据
        skus = SKU.objects.filter(is_launched=True, category_id=category_id).order_by('-sales')[:2]
        hot_skus = []
        for sku in skus:
            sku_dict = {
                'id': sku.id,
                'name': sku.name,
                'price':sku.price,
                'default_image_url': settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg'
            }
            hot_skus.append(sku_dict)
            return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK' , 'hot_skus':hot_skus})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

修改前端 list.html 页面中对应的标签内容

<script type="text/javascript">
    let category_id = {{ category_id }};
</script>
<div class="new_goods">
    <h3>热销排行</h3>
    <ul>
        <li v-for="sku in hot_skus" :key="sku.id">
            <a href="detail.html"><img :src="sku.default_image_url"></a>
            <h4><a href="detail.html">[[ sku.name ]]</a></h4>
            <div class="price">¥[[ sku.price ]]</div>
        </li>
    </ul>
</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

4、商品详情页

# 商品详情页
# 这个是哪一个商品:sku.id
re_path('^detail/(?P<sku_id>\d+)/$', views.DetailGoodsView.as_view() ,name='detail'),

class DetailGoodsView(View):
    '''
    商品详情页
    '''
    def get(self , request , sku_id):
        categories = get_categories()
        sku = SKU.objects.get(id=sku_id)
        breadcrumb = get_breadcrumb(sku.category)

        # 通过 sku.id 获取商品对象的对应规格信息的选项
        sku_specs = SKUSpecification.objects.filter(sku_id=sku_id).order_by('spec_id')
        # 创建一个空的列表 用来存储当前 sku 对应的规格选项数据
        sku_key = []
        # 遍历当前 sku 的规格选项
        for spec in sku_specs:
            # 将每个规格选项的 ID 添加到 sku_key 列表中
            sku_key.append(spec.option.id)
            # [8, 11]  颜色:金色 ,内存:64GB
            # [1, 4, 7]  屏幕尺寸:13.3英寸 颜色:银色 ,内存:core i5/8G内存/512G存储

            # 获取当前商品的所有 sku
            # 保证选择不同的规格的情况下 , 商品不变
            spu_id = sku.spu_id
            skus = SKU.objects.filter(spu_id=spu_id)
            # 构建商品的不同规格参数,sku的选项字段
            spec_sku_map = {}
            for i in skus:
                # 获取sku规格的参数
                s_pecs = i.specs.order_by('spec_id')
                # 创建一个空列表 , 用于存储 sku 规格参数
                key = []
                # 遍历当前 sku 规格参数列表
                for spec in s_pecs:
                    key.append(spec.option.id)
                    spec_sku_map[tuple(key)] = i.id

                    # 获取当前商品的规格名称
                    # 根据商品的 ID 获取当前商品的所有规格名称
                    goods_specs = SPUSpecification.objects.filter(spu_id=spu_id).order_by('id')

                    # 前端渲染
                    # 实现根据规格选项生成对应 sku.id, 更新规格对象个规格选项信息。
                    # 为了给用户展示所有的规格参数
                    for index , spec in enumerate(goods_specs):
                        # 复制 sku_key 列表中的数据,避免直接 sku_key 列表中的内容
                        key = sku_key[:]
                        # 获取当前 specs 对象的规格名称
                        spec_options = spec.options.all()
                        # 遍历当前商品的规格名称
                        for spec_option in spec_options:
                            # 将当前规格选项对象 spec_option 的 id 赋值给 key列表中 index 的位置,用于查询对应 sku 参数内容
                            key[index] = spec_option.id
                            # 根据列表中的值, 在 spec_sku_map 字典中查询对应的 sku 数据
                            spec_option.sku_id = spec_sku_map.get(tuple(key))
                            # 更新每个规格对象的选项内容
                            spec.spec_options = spec_options

                            context = {
                                'categories' : categories,
                                'breadcrumb' : breadcrumb,
                                'sku':sku,
                                'specs' : goods_specs
                            }
                            return render(request , 'detail.html' , context=context)
<script type="text/javascript">
    let category_id = {{ sku.category_id }};
    let sku_price = {{ sku.price }};
    let sku_id = {{ sku.id }};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

5、统计分类商品的访问量

# 统计分类商品访问量
re_path('^detail/visit/(?P<category_id>\d+)/$' , views.DetailVisitView.as_view()),

class DetailVisitView(View):
    '''
    分类商品访问量
    '''
    def post(self , request , category_id):
        # 校验数据
        try:
            category = GoodsCategory.objects.get(id=category_id)
        except Exception:
            return HttpResponseForbidden('商品参数类别不存在')

        t = timezone.localtime()
        # yyyy-mm-dd , 格式化当前时间
        today = '%d-%02d-%02d'%(t.year , t.month , t.day)

        try:
            # 获取当前类别在数据库是否存在 , 如果修改时间以及访问量即可
            count_data = GoodsVisitCount.objects.get(category=category_id , date=today)
        except GoodsVisitCount.DoesNotExist:
            # DoesNotExist: 模型类数据不存在
            # 创建一个空的数据对象
            count_data = GoodsVisitCount()

        count_data.category = category
        count_data.date = today
        count_data.count += 1
        count_data.save()

        return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK"})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

6、商品搜索

全文搜索引擎

需要下载这两个框架
django_haystack
whoosh

  • 1
  • 2
  • 3
  • 4

可以对表中的某些字段进行关键字分析 , 建立关键词对应的索引数据

在配置文件中 INSTALLED_APPS 的列表中添加: haystack;

在配置文件末尾添加

# 配置 haystack 
HAYSTACK_CONNECTIONS = {
    'default': {
        # 设置搜索引擎
        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
        'PATH':os.path.join(BASE_DIR,'whoosh_index'),
    },
}
# 当数据库改变时,自动更新新引擎
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在 goods 应用中创建 search_indexes.py 文件

from haystack import indexes
from goods.models import SKU
# 类名必须为模型类名+Index
class SKUIndex(indexes.SearchIndex, indexes.Indexable):
    # document=True 代表搜索引擎将使用此字段的内容作为引擎进行检索
    # use_template=True 代表使用索引模板建立索引文件
    text = indexes.CharField(document=True, use_template=True)
    # 将索引类与模型类进行绑定
    def get_model(self):
        return SKU
    # 设置索引的查询范围
    def index_queryset(self, using=None):
        return self.get_model().objects.all()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在templates 目录下创建搜索引擎文件的

templates 
|---- search
	|---- indexes
		|---- goods(这个目录的名称是指定搜索模型类所在的应用名称)
			|---- sku_text.txt	(这个名称根据小写模型类名称_text.txt)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在 sku_text.txt 中配置搜索索引字段

# 指定根据表中的字段建立索引
{{ object.name }}
{{ object.caption }}

  • 1
  • 2
  • 3
  • 4

在终端执行创建全文搜索索引文件

python manage.py rebuild_index

  • 1
  • 2

实现分页,在 goods 应用的视图下

from haystack.query import SearchQuerySet

def search_view(request):

    query = request.GET.get('q', '')
    page = request.GET.get('page', 1)
    # 使用 Haystack 的 SearchQuerySet 进行搜索,过滤出包含搜索关键词的结果集
    search_results = SearchQuerySet().filter(content=query)
    paginator = Paginator(search_results, 6)  # 每页显示10条搜索结果

    try:
        results = paginator.page(page)
    except PageNotAnInteger:	# 处理用户在 URL 中输入的页数不是整数的情况,将当前页设为第一页
        results = paginator.page(1)
    except EmptyPage:	# 处理用户请求的页面超出搜索结果范围的情况,将当前页设为最后一页。
        results = paginator.page(paginator.num_pages)
    return render(request, 'search.html', {'results': results, 'query': query})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在应用下配置url

path('search/' , views.search_view ,  name='search')
<div class=" clearfix">
    <ul class="goods_type_list clearfix">
        {% for result in results %}
        <li>
            {# object取得才是sku对象 #}
            <a href="/detail/{{ result.object.id }}/"><img src="/static/images/goods/{{ result.object.default_image.url }}.jpg"></a>
            <h4><a href="/detail/{{ result.object.id }}/">{{ result.object.name }}</a></h4>
            <div class="operate">
                <span class="price">{{ result.object.price }}</span>
                <span>{{ result.object.comments }}评价</span>
            </div>
        </li>
        {% empty %}
        <p>没有找到您要查询的商品。</p>
        {% endfor %}
    </ul>
    <div class="pagination">
        {% if results.has_previous %}
        <a href="?q={{ query }}&page=1">&laquo; 首页</a>
        <a href="?q={{ query }}&page={{ results.previous_page_number }}">上一页</a>
        {% endif %}
        当前页: {{ results.number }} of 总页数: {{ results.paginator.num_pages }}
        {% if results.has_next %}
        <a href="?q={{ query }}&page={{ results.next_page_number }}">下一页</a>
        <a href="?q={{ query }}&page={{ results.paginator.num_pages }}">尾页 &raquo;</a>
        {% endif %}
    </div>
</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

八、购物车

1、响应添加购物车数据

创建应用实现购物车的功能操作 —— carts

配置 redis 数据库缓存购物车中的商品数据

# 缓存 购物车商品id
    "carts":{
        "BACKEND" : "django_redis.cache.RedisCache",
        "LOCATION" : "redis://127.0.0.1:6379/3",
        "OPTIONS":{
            "CLIENT_CLASS" : "django_redis.client.DefaultClient"
        }
    },
# 购物车页面
path('carts/' , views.CartsView.as_view() , name='carts'),

class CartsView(View):
    '''
    响应购物车视图
    '''
    def get(self , request):
        # 响应购物车页面
        return render(request , 'cart.html')
    def post(self , request):
        # 商品添加购物车
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        sku_id = json_dict.get('sku_id')
        count = json_dict.get('count')
        selected = json_dict.get('selected' , True)

        try:
            SKU.objects.get(id=sku_id)
        except Exception:
            return HttpResponseForbidden('sku_id 商品数据不存在')

        user = request.user
        redis_conn = get_redis_connection('carts')
        # user_id = {sku_id:count}
        redis_conn.hincrby('cart_%s'%user.id , sku_id , count)
        if selected:
            # 结果为 True , 勾选的进行保存
            redis_conn.sadd('selected_%s'%user.id , sku_id)
        return JsonResponse({'code': RETCODE.OK, 'errmsg':'OK'})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

2、响应渲染购物车页面

class CartsView(View):
    '''
    响应购物车视图
    '''
    def get(self , request):
        # 响应购物车页面
        user = request.user
        redis_conn = get_redis_connection('carts')
        redis_cart = redis_conn.hgetall('cart_%s' % user.id)
        redis_selected = redis_conn.smembers('selected_%s'%user.id)
        # cart_dict = {sku1:{count:200 , selected:true},{}……}
        cart_dict = {}
        for sku_id , count in redis_cart.items():
            cart_dict[int(sku_id)] = {
                'count': int(count),
                'selected' : sku_id in redis_selected
            }
        # 获取购物车所有的商品数据
        sku_ids = cart_dict.keys()
        skus = SKU.objects.filter(id__in=sku_ids)
        cart_skus = []
        for sku in skus:
            cart_skus_dict = {
                'id' : sku.id,
                'name':sku.name,
                'price' : str(sku.price),
                'count' : cart_dict.get(sku.id).get('count'),
                'selected' : str(cart_dict.get(sku.id).get('selected')),
                'amount':str(sku.price * cart_dict.get(sku.id).get('count')),
                'default_image_url':settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg'
            }
            cart_skus.append(cart_skus_dict)
        context = {'cart_skus':cart_skus}
        return render(request , 'cart.html' , context=context)
<script type="text/javascript">
    let carts = {{ cart_skus|safe }};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

3、修改购物车商品数据

    def put(self , request):
        # 修改购物车商品数据
        # {sku_id: 1, count: 2, selected: true}
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        sku_id = json_dict.get('sku_id')
        count = json_dict.get('count')
        selected = json_dict.get('selected', True)
        try:
            sku = SKU.objects.get(id=sku_id)
        except Exception:
            return HttpResponseForbidden('sku_id 商品数据不存在')

        user = request.user
        redis_conn = get_redis_connection('carts')
        redis_conn.hincrby('cart_%s' % user.id, sku_id, count)
        if selected:
            redis_conn.sadd('selected_%s' % user.id, sku_id)
        else:
            redis_conn.srem('selected_%s' % user.id, sku_id)
        cart_skus_dict = {
            'id': sku.id,
            'name':  sku.name,
            'price': sku.price,
            'count': count,
            'selected': selected,
            'amount': sku.price * count,
            'default_image_url': settings.STATIC_URL + 'images/goods/' + sku.default_image.url + '.jpg'
        }
        return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'cart_sku':cart_skus_dict})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

4、删除购物车商品

    def delete(self , request):
        # 删除购物车商品
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        sku_id = json_dict.get('sku_id')
        user = request.user
        redis_conn = get_redis_connection('carts')
        redis_conn.hdel('cart_%s' % user.id, sku_id)
        redis_conn.srem('selected_%s' % user.id, sku_id)
        return JsonResponse({'code': RETCODE.OK, 'errmsg': "OK"})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5、全选购物车商品

# 全选购物车
path('carts/selection/' , views.CratSelectAllView.as_view()),

class CratSelectAllView(View):
    '''
    全选购物车商品
    '''
    def put(self , request):
        json_dict = json.loads(request.body.decode())
        selected = json_dict.get('selected')
        user = request.user
        redis_conn = get_redis_connection('carts')
        redis_cart = redis_conn.hgetall('cart_%s' % user.id)
        redis_sku_id = redis_cart.keys()
        if selected:
            redis_conn.sadd('selected_%s' % user.id, *redis_sku_id)
        else:
            for sku_id in redis_sku_id:
                redis_conn.srem('selected_%s' % user.id, sku_id)
        return JsonResponse({'code': RETCODE.OK, 'errmsg': "OK"})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

5、浏览记录

用户浏览记录临时数据 , 数据读写频繁 , 存放在 redis

# 缓存 浏览记录商品 id
    "history":{
        "BACKEND" : "django_redis.cache.RedisCache",
        "LOCATION" : "redis://127.0.0.1:6379/4",
        "OPTIONS":{
            "CLIENT_CLASS" : "django_redis.client.DefaultClient"
        }
    },
# 用户浏览记录
path('browse_histories/' , views.UserBrowerHistoryView.as_view()),

class UserBrowerHistoryView(View):

    def get(self , request):
        redis_conn = get_redis_connection('history')
        user = request.user
        sku_ids = redis_conn.lrange('history_%s'%user.id , 0 , -1)

        skus = []
        for sku_id in sku_ids:
            sku = SKU.objects.get(id = sku_id)
            sku_dict = {
                'id':sku.id,
                'name': sku.name,
                'price' : sku.price,
                'default_image_url':settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg'
            }
            skus.append(sku_dict)
        return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'skus':skus})

    def post(self , request):
        json_dict = json.loads(request.body.decode())
        sku_id = json_dict.get('sku_id')

        try:
            SKU.objects.get(id=sku_id)
        except Exception:
            return HttpResponseForbidden('sku_id 商品数据不存在')

        redis_conn = get_redis_connection('history')
        user = request.user
        # 去重
        redis_conn.lrem('history_%s'%user.id , 0 ,  sku_id)
        redis_conn.lpush('history_%s'%user.id , sku_id)
        # 截取
        redis_conn.ltrim('history_%s'%user.id , 0 , 20)

        return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK"})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

九、订单

1、响应订单

创建应用实现订单的功能 —— orders

# 订单页面
path('settlement/' , views.OrderSettlementView.as_view() , name='settlement'),

class OrderSettlementView(View):
    '''
    购物车结算订单页面
    '''
    def get(self , request):
        user = request.user

        # 获取用户收货地址
        try:
            addresses = Address.objects.filter(is_delete=False , user=user)
        except Exception:
            addresses = None

        # 获取购物车商品数据
        redis_conn = get_redis_connection('carts')
        redis_cart = redis_conn.hgetall('cart_%s'%user.id)
        redis_selected = redis_conn.smembers('selected_%s'%user.id)

        # 获取勾选中的商品 id
        new_cart_dict = {}
        for sku_id in redis_selected:
            new_cart_dict[int(sku_id)] = int(redis_cart[sku_id])

        sku_ids = new_cart_dict.keys()
        skus = SKU.objects.filter(id__in=sku_ids)
        # 总件数 , 金额
        total_number = 0
        total_amount = 0
        for sku in skus:
            sku.count = new_cart_dict[sku.id]
            sku.amount = sku.price * sku.count
            total_number += sku.count
            total_amount += sku.amount

        freight = 35
        context = {
            'addresses': addresses,
            'skus' : skus,
            'total_amount' : total_amount,
            'total_number' : total_number,
            'freight' : freight,
            'payment_amount':total_amount + freight
        }

        return render(request , 'place_order.html' , context=context)
<script type="text/javascript">
    let default_address_id = {{ user.default_address.id }};
    let payment_amount = {{ payment_amount }};
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

2、提交订单

# 提交订单
path('orders/commit/' , views.OrderCommitView.as_view())

class OrderCommitView(View):
    '''
    提交订单
    '''
    def post(self , request):
        json_dict = json.loads(request.body.decode())
        address_id = json_dict.get('address_id')
        pay_method = json_dict.get('pay_method')

        try:
            address = Address.objects.get(id=address_id)
        except Exception:
            return HttpResponseForbidden('用户收货地址数据不存在')
        if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'] , OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
            return HttpResponseForbidden('支付方式不正确')
        user = request.user
        order_id = timezone.localdate().strftime('%Y%m%d%H%M%S')+('%05d'%user.id)

        # 创建一个事务,要么全部操作成功 , 要么全部操作失败
        with transaction.atomic():
            # 获取数据库最初的状态
            save_id = transaction.savepoint()
            try:
                order = OrderInfo.objects.create(
                    order_id= order_id,
                    user = user,
                    address=address,
                    total_count = 0,
                    total_amount = 0,
                    freight = 35,
                    pay_method = pay_method,
                    status= OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY'] else OrderInfo.ORDER_STATUS_ENUM['UNSEND']
                )

                # 获取购物车商品数据
                redis_conn = get_redis_connection('carts')
                redis_cart = redis_conn.hgetall('cart_%s' % user.id)
                redis_selected = redis_conn.smembers('selected_%s' % user.id)

                # 获取购物车中勾选的状态数据
                new_cart_dict = {}
                for sku_id in redis_selected:
                    new_cart_dict[int(sku_id)] = int(redis_cart[sku_id])
                skus = SKU.objects.filter(id__in = new_cart_dict.keys())
                #进行单件商品的计算
                for sku in skus:
                    sku_count = new_cart_dict[sku.id]

                    # 获取商品的销量和库存
                    origin_stock= sku.stock
                    origin_sales= sku.sales

                    # 判断商品的库存是否足够
                    if sku_count > origin_stock:
                        transaction.savepoint_rollback(save_id)
                        return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '商品库存不足'})

                    # 对库存进行减少 , 销量进行增加
                    sku.stock -= sku_count
                    sku.sales += sku_count
                    sku.save()

                    # 保存商品订单信息
                    OrderGoods.objects.create(
                        order=order,
                        sku=sku,
                        count=sku_count,
                        price = sku.price
                    )

                    # 计算单间商品的购买数量和总金额
                    order.total_count += sku_count
                    order.total_amount += sku_count * sku.price

                # 对总金额加入运费
                order.total_amount += order.freight
                order.save()

            except Exception:
                # 数据库操作异常 , 事务回滚
                transaction.savepoint_rollback(save_id)
                return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '订单提交失败'})

        # 提交事务
        transaction.savepoint_commit(save_id)
        return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK' , 'order_id':order_id})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

3、提交订单成功页面

# 我的订单
path('orders/info/' , views.UserOrderInfoView.as_view() , name='myorder')

class UserOrderInfoView(View):
    def get(self , request):
        page_num = request.GET.get('page_num')
        user = request.user
        orders = user.orderinfo_set.all()
        # 获取 商品订单数据
        for order in orders:
            # 支付方式 , 1 , 2
            order.pay_method_name = OrderInfo.PAY_METHOD_CHOICES[order.pay_method - 1][1]
            # 订单状态
            order.status_name = OrderInfo.PAY_METHOD_CHOICES[order.status - 1][1]

            order.sku_list = []
            order_goods = order.skus.all()
            for order_good in order_goods:
                sku = order_good.sku
                sku.count = order_good.count
                sku.amount = sku.price * sku.count
                order.sku_list.append(sku)

            # 制作分页
            if not page_num:
                page_num = 1
            page_num = int(page_num)
            paginatot = Paginator(orders , 5)
            page_orders = paginatot.page(page_num)
            total_page = paginatot.num_pages

            context = {
                'page_orders' : page_orders,
                'total_page':total_page,
                'page_num':page_num
            }

        return render(request , 'user_center_order.html' , context=context)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

十、项目部署

1、Nginx

Nginx:开源的高性能的HTTP和反向代理服务器

反向代理:服务器做出逆向操作 , 代理服务器接收用户发送的请求,解析转发给内部服务器,返回Response的响应。

WAF功能:阻止 web 攻击

Nginx特点:内存小 , 并发能力强 , 灵活好扩展

2、配置Linux环境

1、要有 Python 环境

2、要有 MySQL 数据库

3、下载 redis 数据库

sudo yum install redis

  • 1
  • 2

4、下载 Nginx

sudo yum install epel-release
yum install -y nginx

  • 1
  • 2
  • 3

5、下载 UWSGI

sudo yum install epel-release
yum install python3-devel
pip3.8 install uwsgi==2.0.19.1

  • 1
  • 2
  • 3
  • 4

6、下载项目需要的所有模块

pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple mysqlclient==2.1.0
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django==3.2
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pymysql
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pillow==8.3.0
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple ronglian_sms_sdk
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple itsdangerous==1.1.0
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple urllib3==1.26.15
pip2 install -i https://pypi.tuna.tsinghua.edu.cn/simple django_redis
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django_haystack
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple whoosh
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests

更新 pip版本: pip3 install --upgrade pip

在下载模块前,先下载需要的依赖
yum install python3-devel mysql-devel

项目中需要的模块
mysqlclient==2.1.1
django==3.2
pymysql
pillow==8.3.0
ronglian_sms_sdk
itsdangerous==1.1.0
urllib3==1.26.15
django_redis
django_haystack
whoosh
requests
出现:
Aonther app is ***** exit ***
另一个应用程序*****

执行:
rm -f /var/rum/yum.pid

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

3、项目部署

1、在项目上传到 Linux 之前 , 修改 settings.py 文件 , 允许所有主机访问

ALLOWED_HOSTS = ['*']

  • 1
  • 2

2、将搜索索引目录: whoosh_index 删除

3、将整个项目的数据迁移数据库记录文件全部删除

4、通过 Xftp 上传到 Linux 中:opt目录中

5、配置 uwsgi 的配置信息

到 etc 目录下创建 uwsg.d目录 mkdir uwsgi.d

进入创建的目录中,创建 uwsgi.ini 配置文件: vim uwsgi.ini

[uwsgi]
socket= 120.55.47.111:8080	
chdir=/opt/ShopSystem	
module=JiXuShopSystem/wsgi.py  
processes=2
threads=2
master=True
pidfile=uwsgi.pid
buffer-size = 65535

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

6、配置 Nginx

到 etc/nginx/nginx.conf

server {
        listen       8080;	
        # listen       [::]:80;
        server_name  120.55.47.111:10056;	
        # root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        # include /etc/nginx/default.d/*.conf;
        charset utf-8;
        location /static {
                alias /opt/www/django_-shop-system/static;	
        }

        location / {
                include uwsgi_params;
                uwsgi_pass 0.0.0.0:8005;
                uwsgi_param UWSGI_SCRITP django_-shop-system.wsgi;	
                uwsgi_param UWSGI_CHDIR /opt/www/django_-shop-system;	
        }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

7、进入MySQL数据库 , 创建项目需要的数据库。

8、启动

启动 nginx : nginx
启动uwsgi:进入uwsgi.d目录下执行: uwsgi --ini uwsgi.ini
启动redis : systemctl start redis

关闭防火墙:systemctl stop firewalld.service

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

9、迁移数据库,生成全文搜索索引

python3 manage.py rebuild_index

10、启动项目

python3.8 manage.py runserver 0.0.0.0:8000
一定要有ip和端口号 , 否则外部设备无法访问

  • 1
  • 2
  • 3

遭周文而舒志

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/1002882
推荐阅读
相关标签
  

闽ICP备14008679号