Django项目实践(商城):十一、收货地址

在这里插入图片描述

(根据居然老师直播课内容整理)

一、页面功能简介

  • 在“用户中心”的任一界面,点击左边“收货地址”后,显示下面界面在这里插入图片描述
  • 此界面包括以下4个功能:
    • 新增收货地址
    • 删除当前收货地址
    • 编辑(当前记录收货地址)
    • 设为默认
  • 以4个功能中,“新增收货地址”和“编辑” 需要弹出表单,进行编辑

二、显示收货地址信息页面

1、实现分析

  • 在“用户中心”的任一界面,点击左边“收货地址”
    在这里插入图片描述
  • 前端向后端发起 user/addresses路由发起 get请求
    在这里插入图片描述
  • 后端接收请求,判断用户是否登录
  • 如果已登录,返回页面
  • 如果未登录,跳转到登录页面

2、后端veiw实现

  • 用户登录后才能使用此功能
#  /apps/users/views.py 

class AddressView(LoginRequiredMixin, View):
    """用户收货地址"""
    def get(self, request):
        """提供收货地址界面"""
        return render(request, 'user_center_site.html')

3、路由注册

    #  /apps/users/urls.py 
    
    # 展示用户地址
    path('addresses/', views.AddressView.as_view(),name="addresses"),

三、收货地址数据模型

  • 用户地址模型类定在users应用的models.py中

1、收货地址模型类

class Address(BaseModel):
    """用户地址"""
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='addresses', verbose_name='用户')
    title = models.CharField(max_length=20, verbose_name='地址名称')
    receiver = models.CharField(max_length=20, verbose_name='收货人')
    province = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='province_addresses', verbose_name='省')
    city = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='city_addresses', verbose_name='市')
    district = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='district_addresses', verbose_name='区')
    place = models.CharField(max_length=50, verbose_name='地址')
    mobile = models.CharField(max_length=11, verbose_name='手机')
    tel = models.CharField(max_length=20, null=True, blank=True, default='', verbose_name='固定电话')
    email = models.CharField(max_length=30, null=True, blank=True, default='', verbose_name='电子邮箱')
    is_deleted = models.BooleanField(default=False, verbose_name='逻辑删除')
    class Meta:
        db_table = 'tb_address'
        verbose_name = '用户地址'
        verbose_name_plural = verbose_name
        ordering = ['-update_time']  # 按 update_time倒序  排序

2、收货地址模型类说明

  • 收货地址模型类中省、市、县的外键指向areas/models里面的Area。
    • 指明外键时,可以使用应用名.模型类名来定义,也可以使用模型类 在这里插入图片描述
  • ordering表示在进行收货地址模型查询时,默认使用的排序方式。
    • ordering = [’-update_time’] : 根据更新的时间倒叙。

3、补充用户模型默认地址字段

  • 默认地址字段应保存到user表中,与用户绑定,需要修改User模型
    在这里插入图片描述

4、数据库迁移

python manage.py makemigrations
python manage.py migrate

四、新增用户收货地址

在这里插入图片描述

1、接口设计和定义

1.1 请求方式:

选项 方案
请求方法 POST
请求地址 /users/addresses/create/

1.2 请求参数 :

参数名 类型 是否必传 说明
eceiver string 收货人
province_id string 省份ID
city_id string 城市ID
district_id string 区县ID
place string 收货地址
mobile string 手机号
tel string 固定电话
email string 邮箱

1.3 响应结果 : json

响应结果 响应内容
code 状态码
errmsg 错误信息
id 地址ID
receiver 收货人
province 省份名称
city 城市名称
district 区县名称
place 收货地址
mobile 手机号
tel 固定电话
email 邮箱

2、后端view实现

  • 判断用户是否登录
  • 接收参数
    • post参数存放在request.body中
  • 校验参数
    • 必传参数量否传递
    • 手机号校验
    • 如果填有固定电话,固定电话需要校验
    • 如果填了邮箱,邮箱需校验
  • 保存到数据库
  • 根据前端需要,返回详细数据
class AddressCreateView(LoginRequiredJSONMixin, View):
    """新增地址"""
    def post(self,request):
        # 接收参数
        json_dict=json.loads(request.body.decode())
        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 http.HttpResponseForbidden('缺少必传参数')

        if not re.match(r"^1[3-9]\d{9}$", mobile):
            return http.HttpResponseForbidden('参数mobile有误')

        if tel and not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
            return http.HttpResponseForbidden('参数固定电话有误')

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

        # 保存
        try:
            address = Address.objects.create(
                user=request.user,
                title=receiver,
                receiver=receiver,
                province_id=province_id,
                city_id=city_id,
                district_id=district_id,
                place=place,
                mobile=mobile,
                tel=tel,
                email=email,
            )
        except Exception as e:
            logging.error(e)
            return http.HttpResponseServerError({"code": RETCODE.DBERR, "errmsg": "新增收获地址失败"})

        # 响应
        address_dict = {
            "id": address.id,
            "receiver": address.receiver,
            "province": address.province.name,
            "city": address.city.name,
            "district": address.district.name,
            "place": address.place,
            "mobile": address.mobile,
            "tel": address.tel,
            "email": address.email
        }
        return http.JsonResponse({"code": RETCODE.OK, "errmsg": "新增收货地址成功", "address": address_dict})

3、定义路由

在这里插入图片描述

4、前端实现

4.1 新增地址JS

        // 新增地址
        save_address(){
    
    
            if (this.error_receiver || this.error_place || this.error_mobile || this.error_email || !this.form_address.province_id || !this.form_address.city_id || !this.form_address.district_id ) {
    
    
                alert('信息填写有误!');
            } else {
    
    
               // 新增地址
               let url = '/users/addresses/create/';
               axios.post(url, this.form_address, {
    
    
                   headers: {
    
    
                       'X-CSRFToken':getCookie('csrftoken')
                   },
                   responseType: 'json'
               })
                   .then(response => {
    
    
                       if (response.data.code == '0') {
    
    
                           // 局部刷新界面:展示所有地址信息,将新的地址添加到头部
                           this.addresses.splice(0, 0, response.data.address);
                           this.is_show_edit = false;
                       } else if (response.data.code == '4101') {
    
    
                           location.href = '/login/?next=/users/addresses/';
                       } else {
    
    
                           alert(response.data.errmsg);
                       }
                   })
                   .catch(error => {
    
    
                       console.log(error.response);
                   })
            }
        },

4.2 新增地址html

<form>
    <div class="form_group">
        <label>*收货人:</label>
        <input v-model="form_address.receiver" @blur="check_receiver" type="text" class="receiver">
        <span v-show="error_receiver" class="receiver_error">请填写收件人</span>
    </div>
    <div class="form_group">
        <label>*所在地区:</label>
        <select v-model="form_address.province_id">
            <option v-for="province in provinces" :value="province.id">[[ province.name ]]</option>
        </select>
        <select v-model="form_address.city_id">
            <option v-for="city in cities" :value="city.id">[[ city.name ]]</option>
        </select>
        <select v-model="form_address.district_id">
            <option v-for="district in districts" :value="district.id">[[ district.name ]]</option>
        </select>
    </div>
    <div class="form_group">
        <label>*详细地址:</label>
        <input v-model="form_address.place" @blur="check_place" type="text" class="place">
        <span v-show="error_place" class="place_error">请填写地址信息</span>
    </div>
    <div class="form_group">
        <label>*手机:</label>
        <input v-model="form_address.mobile" @blur="check_mobile" type="text" class="mobile">
        <span v-show="error_mobile" class="mobile_error">手机信息有误</span>
    </div>
    <div class="form_group">
        <label>固定电话:</label>
        <input v-model="form_address.tel" @blur="check_tel" type="text" class="tel">
        <span v-show="error_tel" class="tel_error">固定电话有误</span>
    </div>
    <div class="form_group">
        <label>邮箱:</label>
        <input v-model="form_address.email" @blur="check_email" type="text" class="email">
        <span v-show="error_email" class="email_error">邮箱信息有误</span>
    </div>
    <input @click="save_address" type="button" name="" value="新 增" class="info_submit">
    <input @click="is_show_edit=false" type="reset" name="" value="取 消" class="info_submit info_reset">
</form>

5、后端优化

5.1 用户地址不能超过限制,提交后端后就需要判断

  • 前端提交后,应首先判断用户收货地址数据量是否超过上限,
  • 用户信息可以通过request.user得到
    在这里插入图片描述

5.2 设置默认收货地址

  • 新增第一个地址时,应该添加为默认收货地址
  • 或者新增时,判断一下用户默认地址是否为空,如果为空,将收货地下添加为默认收货地址
    在这里插入图片描述

五、完善显示收货地址页面

  • 点击收货地址页面时,后端就应该将已有收货址信息传递给前端,以供显示

1、后端实现

1.1 展示地址请求方式:

选项 方案
请求方法 POST
请求地址 /users/addresses/

1.2 展示地址请求参数: 无

1.3 展示地址响应结果:HTML,参数

参数为{“addresses”:[address字典,],“default_address_id”:缺省地址id}

1.4 代码实现:

  • 获取当前用户的收货地址列表
  • 循环生成每个地址字典组成的用户地址列表
  • 将用户字典列表与缺省地址组合成参数
  • 返回html和参数
class AddressView(LoginRequiredMixin, View):
    """用户收货地址"""

    def get(self, request):
        """提供收货地址界面"""
        login_user=request.user
        addresses=Address.Objects.filter(user=login_user, is_deleted=False)

        address_list = []
        for address in addresses:
            address_dict = {
    
    
                'id': address.id,
                'title': address.title,
                'receiver': address.receiver,
                'province': address.province.name,
                'city': address.city.name,
                'district': address.district.name,
                'place': address.place,
                'mobile': address.mobile,
                'tel': address.tel,
                'email': address.email
            }
            address_list.append(address_dict)
        context = {
    
    
            'addresses': address_list,
            "default_address_id": request.user.default_address_id if request.user.default_address_id else 0            
        }
        return render(request, 'user_center_site.html',context)

2、前端js

  • 将后端模板数据传递到Vue.js
  • /static/js/user_center_site.js
    在这里插入图片描述
  • /templates/user_center_site.html
    在这里插入图片描述

3、user_center_site.html中渲染地址信息

<div class="right_content clearfix" v-cloak>
    <div class="site_top_con">
        <a @click="show_add_site">新增收货地址</a>
        <span>你已创建了<b>[[ addresses.length ]]</b>个收货地址,最多可创建<b>20</b></span>
    </div>
    <div class="site_con" v-for="(address, index) in addresses">
        <div class="site_title">
            <h3>[[ address.title ]]</h3>
            <a href="javascript:;" class="edit_icon"></a>
            <em v-if="address.id===default_address_id">默认地址</em>
            <span class="del_site">×</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">设为默认</a>
            <a href="javascript:;" class="edit_icon">编辑</a>
        </div>
    </div>
</div>

4、完善user_center_site.js中成功新增地址后的局部刷新

在这里插入图片描述

六、编辑收货地址

  • 点击某条收货地址“编辑”按钮时,会显示用户地址修改界面
    在这里插入图片描述
  • 删除地址后端逻辑和新增地址后端逻辑非常的相似。

1、修改地址接口设计和定义

1.1 修改地址请求方式:

选项 方案
请求方法 PUT
请求地址 /addresses/(?P<address_id>\d+)/

1.2 修改地址请求参数: 路径参数 和 JSON

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)
eceiver string 收货人
province_id string 省份ID
city_id string 城市ID
district_id string 区县ID
place string 收货地址
mobile string 手机号
tel string 固定电话
email string 邮箱

1.3 修改地址响应结果:JSON

响应结果 响应内容
code 状态码
errmsg 错误信息
id 地址ID
receiver 收货人
province 省份名称
city 城市名称
district 区县名称
place 收货地址
mobile 手机号
tel 固定电话
email 邮箱

2、后端view实现

  • 判断用户是否登录
  • 接收参数
    • PUT参数存放在request.body中
  • 校验参数
    • 必传参数量否传递
    • 手机号校验
    • 如果填有固定电话,固定电话需要校验
    • 如果填了邮箱,邮箱需校验
  • 更新用户地址
    • 有两种方法:
    • 方法一:Address.objects.get(id=address_id)得到对象,然后依次赋值
    • 方法二:Address.objects.filter(id=address_id).update(参数)
      • 此函数返回值是更新记录的数量,切记
  • 构造响应数据
    • 采用保存方法二时,需要获取修改地址的对象
  • 根据前端需要,返回详细数据
class UpdateDestoryAddressView(LoginRequiredJSONMixin, View):
    """更新和删除地址"""

    def put(self, request, address_id):
        # 接收参数
        json_dict = json.loads(request.body.decode())
        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 http.HttpResponseForbidden('缺少必传参数')

        if not re.match(r"^1[3-9]\d{9}$", mobile):
            return http.HttpResponseForbidden('参数mobile有误')

        if tel and not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
            return http.HttpResponseForbidden('参数固定电话有误')

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

        # 更新数据
        # address = Address.objects.get(id=address_id)
        # address.title = receiver
        # address.save()
        try:
            # update 返回受影响的行数
            Address.objects.filter(id=address_id,user=request.user).update(
                user=request.user,
                title=receiver,
                receiver=receiver,
                province_id=province_id,
                city_id=city_id,
                district_id=district_id,
                place=place,
                mobile=mobile,
                tel=tel,
                email=email,
            )
        except Exception as e:
            return http.JsonResponse({
    
    'code': RETCODE.DBERR, 'errmsg': '修改地址失败'})

        address = Address.objects.get(id=address_id)

        address_dict = {
    
    
            'id': address.id,
            'receiver': address.title,
            'province': address.province.name,
            'city': address.city.name,
            'district': address.district.name,
            'place': address.place,
            'mobile': address.mobile,
            'tel': address.tel,
            'email': address.email
        }
        # 响应新的地址给前端渲染
        return http.JsonResponse({
    
    'code': RETCODE.OK, 'errmsg': '修改地址成功', 'address': address_dict})

3、 路由定义

在这里插入图片描述

3、修改地址前端逻辑实现

3.1 添加修改地址的标记

  • 新增地址和修改地址的交互不同。
  • 为了区分用户是新增地址还是修改地址,我们可以选择添加一个变量,作为标记。
  • 为了方便得到正在修改的地址信息,我们可以选择展示地址时对应的序号作为标记。
    在这里插入图片描述

3.2 实现编辑按钮对应的事件

在这里插入图片描述

七、 删除收货地址

在这里插入图片描述

  • 本项目采用逻辑删除,所以与修改相似,无需界面
    • 将is_deleted=True即可

1、修改地址接口设计和定义

1.1 修改地址请求方式:

选项 方案
请求方法 DELETE
请求地址 /addresses/(?P<address_id>\d+)/

1.2 修改地址请求参数: 路径参数 和 JSON

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)

1.3 修改地址响应结果:JSON

响应结果 响应内容
code 状态码
errmsg 错误信息

2、后端view实现

  • 判断用户是否登录
  • 获取登录用户对象
  • 通过 Address.objects.get(id=address_id) 得到对象,将is_deleted赋值为 True
  • 判断登录用户的缺省地址是否是删除对象
    • 如果是,将登录用户的缺省地址置空
  • 返回前端操作状态
class UpdateDestoryAddressView(LoginRequiredJSONMixin, View):
    """更新收获地址"""

    def delete(self, request, address_id):
        pass

    def delete(self, request, address_id):

        login_user=request.user
        try:
            address=Address.objects.get(id=address_id,user=login_user)
            address.is_deleted=True
            address.save()
            if login_user.default_address_id==address.id:
                login_user.default_address=None
                login_user.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '删除地址失败'})

        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '删除地址成功'})

3、路由定义

  • 删除与修改路由一样,只是方法不同,故共用一个路由
    在这里插入图片描述

4、删除地址前端逻辑实现

  • 后端返回成功后,删除address列表中当前序号的元素
    • this.addresses.splice(起始序号,要删除的项目数量)
  • /static/js/user_center_site.js
    在这里插入图片描述
    在这里插入图片描述

八、设置默认地址

在这里插入图片描述

1、接口设计和定义

1.1 请求方式:

选项 方案
请求方法 PUT
请求地址 addresses/(?P<address_id>\d+)/default/

1.2 请求参数: 路径参数

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)

1.3 响应结果:JSON

响应结果 响应内容
code 状态码
errmsg 错误信息

2、后端view实现

  • 判断用户是否登录
  • 获取登录用户对象
  • 通过 Address.objects.get(id=address_id,user=登录用户) 得到对象
  • 判断登录用户的缺省地址是否是查询到的对象
    • 如果不是,返回数据错误
  • 将查询到的对象赋值给登录用户缺省地址,并保存
  • 返回前端操作结果
class DefaultAddressView(LoginRequiredJSONMixin,View):
    """设置默认地址"""

    def put(self,request,address_id):
        login_user=request.user

        try:
            address=Address.objects.get(id=address_id,user=login_user)
            # if not address:
            #     return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '收货地址数据有误'})
            login_user.default_address=address
            login_user.save()
        except Address.DoesNotExist as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '设置默认收货地址失败'})

        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '设置默认收货地址成功'})

3、路由定义

在这里插入图片描述

4、前端逻辑实现

  • 默认地址标识实现方法:
  • 专门定义了一个变量default_address_id,记录默认地址id
  • 每条记录都有一个默认地址标识,只有当此行地址id=default_address_id时,默认地址标只才显示
  • 当默认地址返回正确后,将该记录的地址id 赋值给default_address_id
    在这里插入图片描述
    在这里插入图片描述

九、修改地址标题

在这里插入图片描述

1、接口设计和定义

1.1 请求方式:

选项 方案
请求方法 PUT
请求地址 addresses/(?P<address_id>\d+)/title/

1.2 请求参数: 路径参数 和 JSON

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)
title string 要修改的 title名称

1.3 响应结果:JSON

响应结果 响应内容
code 状态码
errmsg 错误信息

2、后端view实现

  • 判断用户是否登录
  • 获取参数:put 参数在request.body中
    • address_id是路径参数,路由解析得到
    • json_dict=json.loads(request.body.decode())
  • 校验参数
    • 判断 title是否存在
  • 获取登录用户对象
  • 通过 Address.objects.get(id=address_id,user=登录用户) 得到对象
  • 将title的值赋给对象的title,并保存
  • 返回前端操作结果
class TitleAddressView(LoginRequiredJSONMixin,View):
    """设置地址标题"""

    def put(self,request,address_id):
        login_user=request.user
        json_dict=json.loads(request.body.decode())

        if not json_dict or not json_dict["title"]:
            return http.HttpResponseForbidden('缺少必传参数')

        try:
            address=Address.objects.get(user=login_user,id=address_id)
            address.title=json_dict["title"]
            address.save()
        except Address.DoesNotExist as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '收货地址标题保存失败'})

        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '收货地址标题保存成功'})

3、路由定义

在这里插入图片描述

4、前端逻辑实现

  • 定义一个变量edit_title_index,用于控制编辑框显示
  • 每个地址信息上方都有编辑框和按钮,只有当edit_title_index=当前idex时,才会显示
    • 当前index是信息大列表中的序号

4.1 用超链接点击事件,显示编辑框及按钮

在这里插入图片描述
在这里插入图片描述

4.2 编辑框及保存取消按钮显示

在这里插入图片描述

4.2 取消和保存处理

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/laoluobo76/article/details/114272580