美多商城项目-04

省市区 + 地址管理

后端代码

Areas 模块

新建 apps/areas

进入/meiduo_mall/apps目录下:

python ../manage.py startapp areas

apps/areas/models.py

from django.db import models


class Area(models.Model):
    """
    省 / 市 / 区
    """
    name = models.CharField(max_length=20, verbose_name='名称')
    parent = models.ForeignKey(
        'self',
        on_delete=models.SET_NULL,
        related_name='subs',
        null=True,
        blank=True,
        verbose_name='上级行政区'
    )

    class Meta:
        db_table = 'tb_areas'
        verbose_name = '省市区'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

apps/areas/views.py

from django import http
from django.views import View
from django.core.cache import cache

from apps.areas.models import Area
from utils.response_code import RETCODE


class AreasView(View):
    """
    GET /areas/                 -> 省列表
    GET /areas/?area_id=xxxx    -> 市 / 区列表
    """

    def get(self, request):
        area_id = request.GET.get('area_id')

        # 获取省
        if not area_id:
            province_list = cache.get('province_list')
            if province_list is None:
                provinces = Area.objects.filter(parent__isnull=True)
                province_list = [
                    {'id': p.id, 'name': p.name}
                    for p in provinces
                ]
                cache.set('province_list', province_list, 24 * 3600)

            return http.JsonResponse({
                'code': RETCODE.OK,
                'errmsg': 'ok',
                'province_list': province_list
            })

        # 获取市 / 区
        sub_list = cache.get(f'sub_{area_id}')
        if sub_list is None:
            subs = Area.objects.filter(parent_id=area_id)
            sub_list = [
                {'id': s.id, 'name': s.name}
                for s in subs
            ]
            cache.set(f'sub_{area_id}', sub_list, 24 * 3600)

        return http.JsonResponse({
            'code': RETCODE.OK,
            'errmsg': 'ok',
            'sub_data': sub_list
        })

apps/areas/urls.py

from django.urls import path
from .views import AreasView

app_name = "areas"

urlpatterns = [
    path('areas/', AreasView.as_view(), name='areas'),
]

apps/areas/apps.py

from django.apps import AppConfig


class AreasConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.areas' 

settings.py 注册

INSTALLED_APPS = [
    ...
    'apps.areas',
]

Users 模块(Address 相关完整代码)

apps/users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser
.。≥
from apps import areas
from utils.models import BaseModel


class User(AbstractUser):
    mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')

    email_active = models.BooleanField(default=False, verbose_name='邮箱激活状态')

    default_address = models.ForeignKey(
        'Address',
        related_name='users',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        verbose_name='默认地址'
    )

    class Meta:
        db_table = 'tb_users'
        verbose_name = '用户'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username


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, blank=True, default='', verbose_name='固定电话')
    email = models.CharField(max_length=30, 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']

Address 视图

apps/users/views.py

# ============================================================
# Address 视图
# ============================================================
import json
import re
import logging

from django import http
from django.views import View

from utils.response_code import RETCODE
from utils.mixins import LoginRequiredJSONMixin
from apps.users.models import Address

logger = logging.getLogger('django')


# apps/users/views.py
import json
from django.shortcuts import render
from django import http
from django.views import View

from utils.mixins import LoginRequiredJSONMixin
from utils.response_code import RETCODE
from .models import Address


class AddressView(LoginRequiredMixin, View):
    def get(self, request):
        addresses = Address.objects.filter(
            user=request.user,
            is_deleted=False
        )

        address_list = []
        for addr in addresses:
            address_list.append({
                'id': addr.id,
                'title': addr.title,
                'receiver': addr.receiver,
                'province': addr.province.name,
                'province_id': addr.province_id,
                'city': addr.city.name,
                'city_id': addr.city_id,
                'district': addr.district.name,
                'district_id': addr.district_id,
                'place': addr.place,
                'mobile': addr.mobile,
                'tel': addr.tel,
                'email': addr.email,
            })

        context = {
            'addresses': address_list,
            'default_address_id': request.user.default_address_id
        }
        return render(request, 'users/user_center_site.html', context)


class AddressCreateView(LoginRequiredJSONMixin, View):
    def post(self, request):
        data = json.loads(request.body.decode())

        # ============================
        # 1️⃣ 地址数量限制(≤ 20)
        # ============================
        address_count = Address.objects.filter(
            user=request.user,
            is_deleted=False
        ).count()

        if address_count >= 20:
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '地址数量已达上限(20 个)'
            })

        # ============================
        # 2️⃣ 创建地址
        # ============================
        try:
            address = Address.objects.create(
                user=request.user,
                title=data.get('title'),
                receiver=data.get('receiver'),
                province_id=data.get('province_id'),
                city_id=data.get('city_id'),
                district_id=data.get('district_id'),
                place=data.get('place'),
                mobile=data.get('mobile'),
                tel=data.get('tel'),
                email=data.get('email'),
            )
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({
                'code': RETCODE.DBERR,
                'errmsg': '保存失败'
            })

        # ============================
        # 3️⃣ 如果是第一个地址 → 设为默认
        # ============================
        if address_count == 0:
            request.user.default_address = address
            request.user.save()

        # ============================
        # 4️⃣ 返回数据
        # ============================
        return http.JsonResponse({
            'code': RETCODE.OK,
            'errmsg': 'ok',
            'address': {
                'id': address.id,
                'title': address.title,
                'receiver': address.receiver,
                'province': address.province.name,
                'city': address.city.name,
                'district': address.district.name,
                'province_id': address.province_id,
                'city_id': address.city_id,
                'district_id': address.district_id,
                'place': address.place,
                'mobile': address.mobile,
                'tel': address.tel,
                'email': address.email,
            }
        })
# 更新/删除 地址
class AddressUpdateView(LoginRequiredJSONMixin, View):

    def put(self, request, address_id):
        data = json.loads(request.body.decode())

        try:
            address = Address.objects.get(
                id=address_id,
                user=request.user,
                is_deleted=False
            )
        except Address.DoesNotExist:
            return http.JsonResponse({
                'code': RETCODE.NODATAERR,
                'errmsg': '地址不存在'
            })

        # ===== 手动字段映射(关键)=====
        address.title = data.get('title', address.title)
        address.receiver = data.get('receiver', address.receiver)
        address.place = data.get('place', address.place)
        address.mobile = data.get('mobile', address.mobile)
        address.tel = data.get('tel', address.tel)
        address.email = data.get('email', address.email)

        # 外键要用 _id 赋值
        address.province_id = data.get('province_id', address.province_id)
        address.city_id = data.get('city_id', address.city_id)
        address.district_id = data.get('district_id', address.district_id)

        address.save()

        # 返回给前端的数据(和新增保持一致)
        return http.JsonResponse({
            'code': RETCODE.OK,
            'errmsg': 'ok',
            'address': {
                'id': address.id,
                'title': address.title,
                'receiver': address.receiver,
                'province': address.province.name,
                'city': address.city.name,
                'district': address.district.name,
                'province_id': address.province_id,
                'city_id': address.city_id,
                'district_id': address.district_id,
                'place': address.place,
                'mobile': address.mobile,
                'tel': address.tel,
                'email': address.email,
            }
        })
    def delete(self, request, address_id):
        """
        删除地址(逻辑删除)
        """
        try:
            address = Address.objects.get(
                id=address_id,
                user=request.user,
                is_deleted=False
            )
        except Address.DoesNotExist:
            return http.JsonResponse({
                'code': RETCODE.NODATAERR,
                'errmsg': '地址不存在'
            })

        address.is_deleted = True
        address.save()

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

# 设为默认地址
class DefaultAddressView(LoginRequiredJSONMixin, View):

    def put(self, request, address_id):
        try:
            address = Address.objects.get(
                id=address_id,
                user=request.user
            )
        except Address.DoesNotExist:
            return http.JsonResponse({
                'code': RETCODE.NODATAERR,
                'errmsg': '地址不存在'
            })

        request.user.default_address = address
        request.user.save()

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

class AddressTitleView(LoginRequiredJSONMixin, View):

    def put(self, request, address_id):
        data = json.loads(request.body.decode())
        title = data.get('title')

        if not title:
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '缺少 title'
            })

        Address.objects.filter(
            id=address_id,
            user=request.user
        ).update(title=title)

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

apps/users/urls.py

from django.urls import path
from .views import (
    AddressView,
    AddressUpdateView,
    DefaultAddressView,
    AddressTitleView,
)

urlpatterns = [
    # 用户注册登录相关
    # 用户名重复检查(AJAX)
    # ===== 图形验证码 =====
    # 保存邮箱(PUT)
    # 邮箱激活

    # 收货地址
    path('addresses/', AddressView.as_view(), name='addresses'),        # 页面
    path('addresses/create/', AddressCreateView.as_view()),             # POST API
    path('addresses/<int:address_id>/', AddressUpdateView.as_view()),
    path('addresses/<int:address_id>/default/', DefaultAddressView.as_view()),
    path('addresses/<int:address_id>/title/', AddressTitleView.as_view()),
]

抽象基类模型建议之后统一放在 utils.models.py 下面:

utils/models.py

from django.db import models


class BaseModel(models.Model):
    """
    抽象基类模型
    """
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")

    class Meta:
        abstract = True

数据库迁移

python manage.py makemigrations areas
python manage.py makemigrations users
python manage.py migrate

areas.sql 导入

mysql -u root -p meiduo_mall < areas.sql

后端部署总结

你现在的状态是:

数据库表已创建

  • tb_areas
  • tb_address
  • tb_users.default_address

后端接口已就绪

  • /areas/
  • /areas/?area_id=xxx
  • /addresses/create(GET / POST)
  • /addresses/<id>/(PUT / DELETE)
  • /addresses/<id>/default/
  • /addresses/<id>/title/

正确分工必须是:

URL类型用途谁在用
/users/addresses/页面 GET收货地址管理页面浏览器跳转
/users/addresses/create/POST API新增收货地址Vue
/users/addresses/<id>/PUT / DELETE API修改 / 删除地址Vue
/users/addresses/<id>/default/PUT API设为默认地址Vue
/users/addresses/<id>/title/PUT API修改地址标题Vue

浏览器直接打开:http://127.0.0.1:8000/areas/
应该看到类似:

{
  "code": "0",
  "errmsg": "ok",
  "province_list": [
    {"id": 110000, "name": "北京市"},
    {"id": 120000, "name": "天津市"}
  ]
}
美多商城项目-04

访问:http://127.0.0.1:8000/areas/?area_id=110000

美多商城项目-04

前端代码:

templates/users/user_center_site.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>美多商城-用户中心</title>
	<link rel="stylesheet" type="text/css" href="../static/css/reset.css">
	<link rel="stylesheet" type="text/css" href="../static/css/main.css">
    <script type="text/javascript" src="../static/js/host.js"></script>
	<script type="text/javascript" src="../static/js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="../static/js/axios-0.18.0.min.js"></script>
    <script type="text/javascript">
    let addresses = {{ addresses | safe }};
    let default_address_id = "{{ default_address_id }}";
</script>
</head>
<body>
	<div id="app" v-cloak>
	<div class="header_con">
		<div class="header">
			<div class="welcome fl">欢迎来到美多商城!</div>
			<div class="fr">
				<div class="login_info fl">
					欢迎您:<em>张 山</em>
				</div>
				<div class="login_btn fl">
					<a href="login.html">登录</a>
					<span>|</span>
					<a href="register.html">注册</a>
				</div>
				<div class="user_link fl">
					<span>|</span>
					<a href="user_center_info.html">用户中心</a>
					<span>|</span>
					<a href="cart.html">我的购物车</a>
					<span>|</span>
					<a href="../static/user_center_order.html">我的订单</a>
				</div>
			</div>
		</div>
	</div>

	<div class="search_bar clearfix">
		<a href="index.html" class="logo fl"><img src="../static/images/logo.png"></a>
		<div class="search_wrap fl">
			<form method="get" action="/search/" class="search_con">
                <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
                <input type="submit" class="input_btn fr" name="" value="搜索">
            </form>
			<ul class="search_suggest fl">
				<li><a href="#">索尼微单</a></li>
				<li><a href="#">优惠15元</a></li>
				<li><a href="#">美妆个护</a></li>
				<li><a href="#">买2免1</a></li>
			</ul>
		</div>
	</div>

	<div class="main_con clearfix">
		<div class="left_menu_con clearfix">
			<h3>用户中心</h3>
			<ul>
				<li><a href="user_center_info.html">· 个人信息</a></li>
				<li><a href="../static/user_center_order.html">· 全部订单</a></li>
				<li><a href="user_center_site.html" class="active">· 收货地址</a></li>
				<li><a href="../static/user_center_pass.html">· 修改密码</a></li>
			</ul>
		</div>
		<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" @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">设为默认</a>
            <a href="javascript:;" class="edit_icon" @click="show_edit_site(index)">编辑</a>
        </div>
    </div>
</div>
	</div>
	<div class="footer">
		<div class="foot_link">
			<a href="#">关于我们</a>
			<span>|</span>
			<a href="#">联系我们</a>
			<span>|</span>
			<a href="#">招聘人才</a>
			<span>|</span>
			<a href="#">友情链接</a>
		</div>
		<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
		<p>电话:010-****888    京ICP备*******8号</p>
	</div>

	<div class="pop_con" v-show="is_show_edit" v-cloak>
    <div class="site_con site_pop">
            <div class="site_pop_title">
                <h3 v-if="editing_address_index">编辑收货地址</h3>
                <h3 v-else>新增收货地址</h3>
                <a @click="is_show_edit=false">×</a>
            </div>
            <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 value="">请选择省</option>
						<option
							v-for="province in provinces"
							:key="province.id"
							:value="province.id">
							[[ province.name ]]
						</option>
					</select>

					<!-- 市 -->
					<select v-model="form_address.city_id">
						<option value="">请选择市</option>
						<option
							v-for="city in cities"
							:key="city.id"
							:value="city.id">
							[[ city.name ]]
						</option>
					</select>

					<!-- 区 -->
					<select v-model="form_address.district_id">
						<option value="">请选择区</option>
						<option
							v-for="district in districts"
							:key="district.id"
							: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>
    </div>
</div>

	<div class="pop_con2">
		<div class="confirm_pop">
				<div class="confirm_pop_title">
					<h3>确认删除</h3>
					<a href="javascript:;">×</a>
				</div>
				<p>您确认删除当前地址吗?</p>
				<input type="button" value="确 定" class="confirm_submit" />
				<input type="button" value="取 消" class="confirm_submit confirm_cancel"  />
		</div>
		<div class="mask"></div>
	</div>
	</div>
	<script type="text/javascript" src="../static/js/host.js"></script>
	<script type="text/javascript" src="../static/js/common.js"></script>
	<script type="text/javascript" src="../static/js/user_center_site.js"></script>
</body>
</html>

static/js/user_center_site.js

var vm = new Vue({
    el: '#app',
    delimiters: ['[[', ']]'],
    data: {
        host,
        is_show_edit: false,

        // 省市区数据
        provinces: [],
        cities: [],
        districts: [],

        // 地址列表
        addresses: [],
        default_address_id: null,

        // 编辑状态
        editing_address_index: '',

        // 表单数据
        form_address: {
            title: '',
            receiver: '',
            province_id: '',
            city_id: '',
            district_id: '',
            place: '',
            mobile: '',
            tel: '',
            email: '',
        }
    },

    mounted() {
        // Django 注入的数据
        this.addresses = JSON.parse(JSON.stringify(addresses));
        this.default_address_id = default_address_id;

        // 加载省份
        this.get_provinces();
    },

    watch: {
        // 省 -> 市
        'form_address.province_id'(val) {
            if (!val) {
                this.cities = [];
                this.districts = [];
                return;
            }
            axios.get(this.host + '/areas/?area_id=' + val)
                .then(res => {
                    if (res.data.code === '0') {
                        this.cities = res.data.sub_data;
                        this.districts = [];
                    }
                });
        },

        // 市 -> 区
        'form_address.city_id'(val) {
            if (!val) {
                this.districts = [];
                return;
            }
            axios.get(this.host + '/areas/?area_id=' + val)
                .then(res => {
                    if (res.data.code === '0') {
                        this.districts = res.data.sub_data;
                    }
                });
        }
    },

    methods: {
        // 获取省份
        get_provinces() {
            axios.get(this.host + '/areas/')
                .then(res => {
                    if (res.data.code === '0') {
                        this.provinces = res.data.province_list;
                    }
                });
        },

        // 显示新增弹窗
        show_add_site() {
            this.is_show_edit = true;
            this.editing_address_index = '';

            this.form_address = {
                title: '',
                receiver: '',
                province_id: '',
                city_id: '',
                district_id: '',
                place: '',
                mobile: '',
                tel: '',
                email: '',
            };

            this.cities = [];
            this.districts = [];
        },

        // 显示编辑弹窗
        show_edit_site(index) {
            this.is_show_edit = true;
            this.editing_address_index = index;

            // 深拷贝,避免污染原数据
            this.form_address = JSON.parse(JSON.stringify(this.addresses[index]));

            // 回显省市区
            axios.get(this.host + '/areas/?area_id=' + this.form_address.province_id)
                .then(res => {
                    this.cities = res.data.sub_data;
                });

            axios.get(this.host + '/areas/?area_id=' + this.form_address.city_id)
                .then(res => {
                    this.districts = res.data.sub_data;
                });
        },

        // 保存地址(新增 / 修改)
        save_address() {
            let url;
            let method;

            // ===== 新增地址 =====
            if (this.editing_address_index === '') {
                url = this.host + '/users/addresses/create/';
                method = 'post';
            }
            // ===== 编辑地址 =====
            else {
                url = this.host + '/users/addresses/' +
                      this.addresses[this.editing_address_index].id + '/';
                method = 'put';
            }

            // title 默认等于 receiver
            this.form_address.title = this.form_address.receiver;

            axios({
                url: url,
                method: method,
                data: this.form_address,
                headers: {
                    'X-CSRFToken': getCookie('csrftoken')
                }
            }).then(res => {
                if (res.data.code === '0') {
                    if (method === 'post') {
                        // 新增
                        this.addresses.unshift(res.data.address);

                            // ⭐ 如果这是第一个地址,前端同步设为默认
                        if (this.addresses.length === 1) {
                            this.default_address_id = res.data.address.id;
                        }
                    } else {
                        // 修改
                        this.$set(
                            this.addresses,
                            this.editing_address_index,
                            res.data.address
                        );
                    }
                    this.is_show_edit = false;
                } else {
                    alert(res.data.errmsg || '操作失败');
                }
            });
        },

        // 删除地址
        delete_address(index) {
            axios.delete(
                this.host + '/users/addresses/' + this.addresses[index].id + '/',
                {
                    headers: {
                        'X-CSRFToken': getCookie('csrftoken')
                    }
                }
            ).then(res => {
                if (res.data.code === '0') {
                    this.addresses.splice(index, 1);
                } else {
                    alert(res.data.errmsg || '删除失败');
                }
            });
        },

        // 设置默认地址
        set_default(index) {
            axios.put(
                this.host + '/users/addresses/' + this.addresses[index].id + '/default/',
                {},
                {
                    headers: {
                        'X-CSRFToken': getCookie('csrftoken')
                    }
                }
            ).then(res => {
                if (res.data.code === '0') {
                    this.default_address_id = this.addresses[index].id;
                } else {
                    alert(res.data.errmsg || '设置失败');
                }
            });
        }
    }
});

逻辑解析(编辑/删除 为例)

/users/addresses/<id>/ ——【编辑 / 删除 API】

path('addresses/<int:address_id>/', AddressUpdateView.as_view())

Vue 对应逻辑

修改地址

axios.put('/users/addresses/4/', form_address)

删除地址

axios.delete('/users/addresses/4/')

后端职责

class AddressUpdateView(LoginRequiredJSONMixin, View):
    def put(self, request, address_id):
        Address.objects.filter(id=address_id).update(...)

    def delete(self, request, address_id):
        Address.objects.filter(id=address_id).update(is_deleted=True)

测试:

http://127.0.0.1:8000/users/addresses/

美多商城项目-04

用户修改密码

完整链路:

HTML(v-model)
   ↓
Vue 校验
   ↓
axios PUT /users/password/
   ↓
后端 check_password
   ↓
user.set_password
   ↓
logout
   ↓
前端跳转登录页

templates/users/user_center_pass.html

var vm = new Vue({
    el: '#app',
    delimiters: ['[[', ']]'],
    data: {
        host,
        is_show_edit: false,

        // 省市区数据
        provinces: [],
        cities: [],
        districts: [],

        // 地址列表
        addresses: [],
        default_address_id: null,

        // 编辑状态
        editing_address_index: '',

        // 表单数据
        form_address: {
            title: '',
            receiver: '',
            province_id: '',
            city_id: '',
            district_id: '',
            place: '',
            mobile: '',
            tel: '',
            email: '',
        }
    },

    mounted() {
        // Django 注入的数据
        this.addresses = JSON.parse(JSON.stringify(addresses));
        this.default_address_id = default_address_id;

        // 加载省份
        this.get_provinces();
    },

    watch: {
        // 省 -> 市
        'form_address.province_id'(val) {
            if (!val) {
                this.cities = [];
                this.districts = [];
                return;
            }
            axios.get(this.host + '/areas/?area_id=' + val)
                .then(res => {
                    if (res.data.code === '0') {
                        this.cities = res.data.sub_data;
                        this.districts = [];
                    }
                });
        },

        // 市 -> 区
        'form_address.city_id'(val) {
            if (!val) {
                this.districts = [];
                return;
            }
            axios.get(this.host + '/areas/?area_id=' + val)
                .then(res => {
                    if (res.data.code === '0') {
                        this.districts = res.data.sub_data;
                    }
                });
        }
    },

    methods: {
        // 获取省份
        get_provinces() {
            axios.get(this.host + '/areas/')
                .then(res => {
                    if (res.data.code === '0') {
                        this.provinces = res.data.province_list;
                    }
                });
        },

        // 显示新增弹窗
        show_add_site() {
            this.is_show_edit = true;
            this.editing_address_index = '';

            this.form_address = {
                title: '',
                receiver: '',
                province_id: '',
                city_id: '',
                district_id: '',
                place: '',
                mobile: '',
                tel: '',
                email: '',
            };

            this.cities = [];
            this.districts = [];
        },

        // 显示编辑弹窗
        show_edit_site(index) {
            this.is_show_edit = true;
            this.editing_address_index = index;

            // 深拷贝,避免污染原数据
            this.form_address = JSON.parse(JSON.stringify(this.addresses[index]));

            // 回显省市区
            axios.get(this.host + '/areas/?area_id=' + this.form_address.province_id)
                .then(res => {
                    this.cities = res.data.sub_data;
                });

            axios.get(this.host + '/areas/?area_id=' + this.form_address.city_id)
                .then(res => {
                    this.districts = res.data.sub_data;
                });
        },

        // 保存地址(新增 / 修改)
        save_address() {
            let url;
            let method;

            // ===== 新增地址 =====
            if (this.editing_address_index === '') {
                url = this.host + '/users/addresses/create/';
                method = 'post';
            }
            // ===== 编辑地址 =====
            else {
                url = this.host + '/users/addresses/' +
                      this.addresses[this.editing_address_index].id + '/';
                method = 'put';
            }

            // title 默认等于 receiver
            this.form_address.title = this.form_address.receiver;

            axios({
                url: url,
                method: method,
                data: this.form_address,
                headers: {
                    'X-CSRFToken': getCookie('csrftoken')
                }
            }).then(res => {
                if (res.data.code === '0') {
                    if (method === 'post') {
                        // 新增
                        this.addresses.unshift(res.data.address);

                            // ⭐ 如果这是第一个地址,前端同步设为默认
                        if (this.addresses.length === 1) {
                            this.default_address_id = res.data.address.id;
                        }
                    } else {
                        // 修改
                        this.$set(
                            this.addresses,
                            this.editing_address_index,
                            res.data.address
                        );
                    }
                    this.is_show_edit = false;
                } else {
                    alert(res.data.errmsg || '操作失败');
                }
            });
        },

        // 删除地址
        delete_address(index) {
            axios.delete(
                this.host + '/users/addresses/' + this.addresses[index].id + '/',
                {
                    headers: {
                        'X-CSRFToken': getCookie('csrftoken')
                    }
                }
            ).then(res => {
                if (res.data.code === '0') {
                    this.addresses.splice(index, 1);
                } else {
                    alert(res.data.errmsg || '删除失败');
                }
            });
        },

        // 设置默认地址
        set_default(index) {
            axios.put(
                this.host + '/users/addresses/' + this.addresses[index].id + '/default/',
                {},
                {
                    headers: {
                        'X-CSRFToken': getCookie('csrftoken')
                    }
                }
            ).then(res => {
                if (res.data.code === '0') {
                    this.default_address_id = this.addresses[index].id;
                } else {
                    alert(res.data.errmsg || '设置失败');
                }
            });
        }
    }
});

static/js/user_center_pass.js

var vm = new Vue({
    el: '#app',
    // 修改Vue变量的读取语法,避免和django模板语法冲突
    delimiters: ['[[', ']]'],
    data: {
        host: host,
        old_pwd: '',
        new_pwd: '',
        new_cpwd: '',
        error_opwd: false,
        error_pwd: false,
        error_cpwd: false
    },
    methods: {
        // 检查旧密码
        check_opwd(){
            var re = /^[0-9A-Za-z]{8,20}$/;
            if (re.test(this.old_pwd)) {
                this.error_opwd = false;
            } else {
                this.error_opwd = true;
            }
        },
        // 检查新密码
        check_pwd(){
            var re = /^[0-9A-Za-z]{8,20}$/;
            if (re.test(this.new_pwd)) {
                this.error_pwd = false;
            } else {
                this.error_pwd = true;
            }
        },
        // 检查确认密码
        check_cpwd: function () {
            if (this.new_pwd != this.new_cpwd) {
                this.error_cpwd = true;
            } else {
                this.error_cpwd = false;
            }
        },
        // 提交修改密码
        on_submit: function () {
            this.check_opwd();
            this.check_pwd();
            this.check_cpwd();

            if (this.error_opwd || this.error_pwd || this.error_cpwd) {
                return;
            }

            axios.put(this.host + '/users/password/', {
                old_password: this.old_pwd,
                new_password: this.new_pwd
            }, {
                headers: {
                    'X-CSRFToken': getCookie('csrftoken')
                }
            }).then(res => {
                if (res.data.code === '0') {
                    alert('密码修改成功,请重新登录');
                    window.location.href = '/users/login/';
                } else {
                    alert(res.data.errmsg);
                }
            }).catch(err => {
                console.error(err);
                alert('请求失败,请查看控制台');
            });

        }

    }
});

apps/users/urls.py

from .views import PasswordView

...

urlpatterns = [
    ...
    # ===== 修改密码 =====
    path("password/", PasswordView.as_view(), name="password"),
]

apps/users/views.py

# ============================================================
# 修改密码
# ============================================================
import json
from django.views import View
from django import http
from django.contrib.auth import logout
import re
from utils.mixins import LoginRequiredJSONMixin
from utils.response_code import RETCODE

class PasswordView(LoginRequiredJSONMixin, View):

    """
    修改密码页面
    GET /users/password/
    """
    def get(self, request):
        return render(request, "users/user_center_pass.html")

    """
    PUT /users/password/
    """

    def put(self, request):
        try:
            data = json.loads(request.body.decode())
        except Exception:
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '参数格式错误'
            })

        old_password = data.get('old_password')
        new_password = data.get('new_password')

        # 1️⃣ 参数校验
        if not all([old_password, new_password]):
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '参数不完整'
            })

        # 2️⃣ 校验新密码格式
        if not re.match(r'^[0-9A-Za-z]{8,20}$', new_password):
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '新密码格式不正确'
            })

        user = request.user

        # 3️⃣ 校验旧密码
        if not user.check_password(old_password):
            return http.JsonResponse({
                'code': RETCODE.PARAMERR,
                'errmsg': '原密码错误'
            })

        # 4️⃣ 设置新密码(必须用 set_password)
        user.set_password(new_password)
        user.save()

        # 5️⃣ 安全处理:退出登录
        logout(request)

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

测试:

美多商城项目-04
美多商城项目-04

尝试新的登录即可

发布者:LJH,转发请注明出处:https://www.ljh.cool/44495.html

(0)
LJH的头像LJH
上一篇 2025年12月14日 下午8:41
下一篇 2026年1月11日 下午2:32

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注