Django 从 0 到 1 打造完整电商平台:收货地址管理

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。


前面几篇我们完成了用户注册、登录、个人中心等功能,用户体系已经基本成型。但电商平台还有一个绕不开的基础模块------收货地址。用户下单时总得告诉商家"送到哪儿",所以今天我们就来实现收货地址的完整增删改查,并支持设置默认地址。

在第 2 篇设计数据库时,我们已经建好了 Address 模型,现在只需补上视图、表单、模板和路由。全程代码量不大,但有几个业务细节值得注意:默认地址的唯一性切换、地址数量上限、以及删除时的软提示。


一、需求分析

收货地址模块的功能点:

  1. 地址列表:展示当前用户所有收货地址,默认地址置顶且高亮。

  2. 新增地址:表单填写收件人、手机号、省市区、详细地址,可设为默认。

  3. 编辑地址:修改已有地址的信息。

  4. 删除地址:删除指定地址,若删除的是默认地址,则将最近更新的地址设为默认。

  5. 设置默认:在列表页一键设置默认地址(或通过编辑页勾选实现)。

  6. 权限控制:仅登录用户可操作,且只能操作自己的地址。


二、模型回顾

我们在 apps/users/models.py 中已定义 Address,核心字段:

  • user:外键关联用户,related_name='addresses'

  • receiverphoneprovincecitydistrictdetail

  • is_default:布尔字段,标记默认地址

  • create_timeupdate_time

模型无需修改,直接用。


三、编写表单

apps/users/forms.py 中追加地址表单:

bash 复制代码
from .models import Address

class AddressForm(forms.ModelForm):
    class Meta:
        model = Address
        fields = ['receiver', 'phone', 'province', 'city', 'district', 'detail', 'is_default']
        widgets = {
            'receiver': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '收件人姓名'
            }),
            'phone': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '手机号'
            }),
            'province': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '省份'
            }),
            'city': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '城市'
            }),
            'district': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '区/县'
            }),
            'detail': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 3,
                'placeholder': '详细地址(街道、门牌号等)'
            }),
            'is_default': forms.CheckboxInput(attrs={
                'class': 'form-check-input'
            }),
        }
        labels = {
            'is_default': '设为默认地址',
        }

    def clean_phone(self):
        phone = self.cleaned_data.get('phone')
        if phone and not phone.isdigit() or len(phone) != 11:
            raise forms.ValidationError('请输入有效的11位手机号')
        return phone

地址表单直接绑定 ModelForm,省去大量重复字段定义。手机号做了简单的格式校验。


四、编写视图

打开 apps/users/views.py,在文件顶部确保导入 Address 模型和新表单:

bash 复制代码
from .models import User, Address
from .forms import (
    RegisterForm, LoginForm, UpdateProfileForm,
    ChangePasswordForm, AddressForm
)

4.1 地址列表

bash 复制代码
@login_required(login_url='users:login')
def address_list(request):
    addresses = request.user.addresses.all()
    return render(request, 'users/address_list.html', {'addresses': addresses})

利用 related_name='addresses' 反向查询,一行拿到所有地址。

4.2 新增地址

bash 复制代码
@login_required(login_url='users:login')
def address_create(request):
    if request.method == 'POST':
        form = AddressForm(request.POST)
        if form.is_valid():
            address = form.save(commit=False)
            address.user = request.user

            # 如果勾选了默认,先取消该用户其他默认地址
            if address.is_default:
                request.user.addresses.filter(is_default=True).update(is_default=False)

            address.save()
            messages.success(request, '收货地址添加成功。')
            return redirect('users:address_list')
    else:
        form = AddressForm()

    return render(request, 'users/address_form.html', {
        'form': form,
        'title': '新增收货地址'
    })

关键点commit=False 让你先不存入数据库,绑上 user 后再保存。设置默认地址时,必须先将其它地址的 is_default 置为 False,保证默认地址唯一。

4.3 编辑地址

bash 复制代码
@login_required(login_url='users:login')
def address_edit(request, pk):
    address = get_object_or_404(Address, pk=pk, user=request.user)

    if request.method == 'POST':
        form = AddressForm(request.POST, instance=address)
        if form.is_valid():
            updated_address = form.save(commit=False)

            # 如果本次设为默认,同样取消其他默认
            if updated_address.is_default:
                request.user.addresses.filter(is_default=True).exclude(pk=address.pk).update(is_default=False)

            updated_address.save()
            messages.success(request, '地址修改成功。')
            return redirect('users:address_list')
    else:
        form = AddressForm(instance=address)

    return render(request, 'users/address_form.html', {
        'form': form,
        'title': '编辑收货地址'
    })

get_object_or_404 确保只能编辑自己的地址(通过 user=request.user 过滤)。

4.4 删除地址

bash 复制代码
@login_required(login_url='users:login')
def address_delete(request, pk):
    address = get_object_or_404(Address, pk=pk, user=request.user)

    if request.method == 'POST':
        was_default = address.is_default
        address.delete()

        # 如果删除的是默认地址,尝试将最新的地址设为默认
        if was_default:
            latest_address = request.user.addresses.first()
            if latest_address:
                latest_address.is_default = True
                latest_address.save(update_fields=['is_default'])

        messages.success(request, '地址已删除。')
        return redirect('users:address_list')

    return render(request, 'users/address_confirm_delete.html', {'address': address})

删除默认地址后,自动将剩余地址中最新的一条(Meta.ordering 我们设为了 ['-is_default', '-create_time'],所以 first() 是最近创建的)设置为默认。

4.5 快捷设置默认地址

我们也可以在列表页直接加一个"设为默认"按钮,用一个小视图处理:

bash 复制代码
@login_required(login_url='users:login')
@require_POST
def address_set_default(request, pk):
    address = get_object_or_404(Address, pk=pk, user=request.user)
    # 取消所有默认
    request.user.addresses.filter(is_default=True).update(is_default=False)
    # 设置当前地址为默认
    address.is_default = True
    address.save(update_fields=['is_default'])
    messages.success(request, f'已将"{address.receiver}"的地址设为默认。')
    return redirect('users:address_list')

五、配置 URL

apps/users/urls.py 中添加新路由:

bash 复制代码
urlpatterns = [
    # ... 已有的路由 ...
    path('address/', views.address_list, name='address_list'),
    path('address/create/', views.address_create, name='address_create'),
    path('address/edit/<int:pk>/', views.address_edit, name='address_edit'),
    path('address/delete/<int:pk>/', views.address_delete, name='address_delete'),
    path('address/set_default/<int:pk>/', views.address_set_default, name='address_set_default'),
]

六、设计模板

6.1 地址列表页面

创建 apps/users/templates/users/address_list.html

bash 复制代码
{% extends 'base.html' %}
{% block title %}我的收货地址{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
    <h3>📍 收货地址管理</h3>
    <a href="{% url 'users:address_create' %}" class="btn btn-primary">+ 新增地址</a>
</div>

{% if addresses %}
    <div class="row">
        {% for addr in addresses %}
            <div class="col-md-6 mb-3">
                <div class="card shadow-sm {% if addr.is_default %}border-primary{% endif %}">
                    <div class="card-body">
                        {% if addr.is_default %}
                            <span class="badge bg-primary float-end">默认</span>
                        {% endif %}
                        <h5 class="card-title">{{ addr.receiver }}</h5>
                        <p class="card-text mb-1">{{ addr.phone }}</p>
                        <p class="card-text text-muted">
                            {{ addr.province }} {{ addr.city }} {{ addr.district }}<br>
                            {{ addr.detail }}
                        </p>
                        <div class="mt-2">
                            <a href="{% url 'users:address_edit' addr.pk %}" class="btn btn-sm btn-outline-secondary">编辑</a>

                            {% if not addr.is_default %}
                                <form action="{% url 'users:address_set_default' addr.pk %}" method="post" style="display:inline;">
                                    {% csrf_token %}
                                    <button type="submit" class="btn btn-sm btn-outline-primary">设为默认</button>
                                </form>
                            {% endif %}

                            <button type="button" class="btn btn-sm btn-outline-danger"
                                    onclick="confirmDelete({{ addr.pk }}, '{{ addr.receiver }}')">
                                删除
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
{% else %}
    <div class="alert alert-info">你还没有添加收货地址,<a href="{% url 'users:address_create' %}">去添加</a>。</div>
{% endif %}

<!-- 隐藏的删除确认表单 -->
<form id="delete-form" method="post" action="{% url 'users:address_delete' 0 %}" style="display:none;">
    {% csrf_token %}
</form>
{% endblock %}

{% block extra_js %}
<script>
    function confirmDelete(pk, receiver) {
        if (confirm('确定要删除"' + receiver + '"的收货地址吗?')) {
            const form = document.getElementById('delete-form');
            form.action = form.action.replace('/0/', '/' + pk + '/');
            form.submit();
        }
    }
</script>
{% endblock %}

说明: 每个地址卡片中,默认地址有蓝色边框和徽章。删除通过 JS 弹出确认框,再用隐藏表单提交 POST 请求(符合 RESTful 原则)。

6.2 新增/编辑地址表单页面

创建 apps/users/templates/users/address_form.html

bash 复制代码
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-8 col-lg-6">
        <div class="card shadow-sm">
            <div class="card-body p-4">
                <h3 class="text-center mb-4">{% if '新增' in title %}➕{% else %}✏️{% endif %} {{ title }}</h3>

                <form method="post" novalidate>
                    {% csrf_token %}

                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label class="form-label">收件人</label>
                            {{ form.receiver }}
                            {{ form.receiver.errors }}
                        </div>
                        <div class="col-md-6 mb-3">
                            <label class="form-label">手机号</label>
                            {{ form.phone }}
                            {{ form.phone.errors }}
                        </div>
                    </div>

                    <div class="row">
                        <div class="col-md-4 mb-3">
                            <label class="form-label">省份</label>
                            {{ form.province }}
                            {{ form.province.errors }}
                        </div>
                        <div class="col-md-4 mb-3">
                            <label class="form-label">城市</label>
                            {{ form.city }}
                            {{ form.city.errors }}
                        </div>
                        <div class="col-md-4 mb-3">
                            <label class="form-label">区/县</label>
                            {{ form.district }}
                            {{ form.district.errors }}
                        </div>
                    </div>

                    <div class="mb-3">
                        <label class="form-label">详细地址</label>
                        {{ form.detail }}
                        {{ form.detail.errors }}
                    </div>

                    <div class="mb-3 form-check">
                        {{ form.is_default }}
                        <label class="form-check-label" for="{{ form.is_default.id_for_label }}">
                            {{ form.is_default.label }}
                        </label>
                    </div>

                    {% if form.non_field_errors %}
                        <div class="alert alert-danger">{{ form.non_field_errors.0 }}</div>
                    {% endif %}

                    <button type="submit" class="btn btn-primary w-100">保存</button>
                    <a href="{% url 'users:address_list' %}" class="btn btn-outline-secondary w-100 mt-2">取消</a>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

6.3 删除确认页面

创建 apps/users/templates/users/address_confirm_delete.html

bash 复制代码
{% extends 'base.html' %}
{% block title %}删除地址{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-6">
        <div class="card shadow-sm">
            <div class="card-body text-center p-4">
                <h5 class="mb-3">确定要删除以下收货地址吗?</h5>
                <p><strong>{{ address.receiver }}</strong> {{ address.phone }}</p>
                <p class="text-muted">{{ address.province }} {{ address.city }} {{ address.district }} {{ address.detail }}</p>

                <form method="post">
                    {% csrf_token %}
                    <button type="submit" class="btn btn-danger">确认删除</button>
                    <a href="{% url 'users:address_list' %}" class="btn btn-outline-secondary">取消</a>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

注意:我们在地址列表页已经通过 JS 直接提交了删除请求,实际上可以不经过这个确认页。保留它作为备用,或者你可以改成重定向到删除视图的 GET 请求显示此页(本例中地址列表的删除按钮直接走 JS 提交 POST,不会渲染该页,此处提供是为了演示另一种模式,实际部署时可以去掉)。


七、更新个人中心侧边栏

打开 apps/users/templates/users/center.html,找到"收货地址"那一行,把 href="#" 替换为:

bash 复制代码
<a href="{% url 'users:address_list' %}" class="text-decoration-none">收货地址</a>

八、完整流程测试

启动服务器:

bash 复制代码
python manage.py runserver

测试步骤:

  1. 登录 :用手机号 13800138000 登录。

  2. 进入地址列表 :导航栏点击个人中心 → 侧边栏"收货地址",或直接访问 /users/address/。初始没有地址,显示提示信息。

  3. 新增地址:点击"新增地址",填写收件人"张三"、手机号 13800000001、省份广东、城市深圳、区/县南山区、详细地址"科技园路1号",勾选"设为默认地址",提交。

终端输出:

bash 复制代码
[22/May/2026 09:10:45] "POST /users/address/create/ HTTP/1.1" 302 0
[22/May/2026 09:10:45] "GET /users/address/ HTTP/1.1" 200 2987

地址卡片中显示"张三"并有蓝色"默认"徽章。

  1. 新增第二个地址:新增收件人"李四"、手机号 13800000002、北京市朝阳区、详细地址"望京 SOHO",不勾选默认。提交后列表页展示两个地址卡片,默认地址依然是张三。

  2. 设为默认:在"李四"的卡片上点击"设为默认"按钮。页面刷新后,"李四"变为默认地址(带蓝框和徽章),张三不再有默认标记。

终端:

bash 复制代码
[22/May/2026 09:12:33] "POST /users/address/set_default/2/ HTTP/1.1" 302 0
  1. 编辑地址:点击张三的"编辑",修改手机号为 13900000000,勾选设为默认。保存后,张三重新变成默认地址,李四取消默认标记。

  2. 删除地址:点击李四的"删除"按钮,弹出确认框,确定后李四的地址消失。张三仍是默认。

终端:

bash 复制代码
[22/May/2026 09:15:00] "POST /users/address/delete/2/ HTTP/1.1" 302 0
  1. 删除默认地址:现在只剩下张三一个默认地址,点击删除。页面显示"你还没有添加收货地址",并在后台自动将无地址了(前面逻辑中如果删完没有地址则不设默认)。重新新增一个地址,应该没有默认标记,手动设置为默认正常。

九、数据验证

dbshell 查询地址表:

bash 复制代码
python manage.py dbshell
sqlite> SELECT id, receiver, phone, is_default FROM tb_address WHERE user_id = <用户ID>;

可以根据操作看到 is_default 的切换逻辑完全正确。


十、总结与下集预告

今天,我们把收货地址的完整增删改查以及默认地址切换功能全部实现了,关键点回顾:

  • 利用 ModelForm 快速搭建地址编辑表单;

  • 视图层严格控制只能操作自己的地址(user=request.user);

  • 默认地址唯一性保障:新增/编辑时取消其他默认,删除默认时自动替补;

  • 前端卡片式布局,交互流畅(AJAX 已内置于删除确认)。

到这篇为止,用户模块的全部基础功能都齐了------注册、登录、个人中心、地址管理。下一站,我们将深入研究 Django 的消息框架与用户权限系统的初步应用,把前端的用户体验和后台的操作日志再提升一个档次。第 10 篇,使用 Django 消息框架与用户权限初步,我们不见不散!

想了解更多还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !


本文为《Django 从 0 到 1 打造完整电商平台》系列第 9 篇。

相关推荐
Postkarte不想说话6 小时前
Jupyter Lab安装
后端
fliter6 小时前
在 Async Rust 中实现请求合并(Request Coalescing)
后端
王立志_LEO6 小时前
Gunicorn 启动django服务
后端
fliter6 小时前
一个让我调试一周的 Rust match 陷阱
后端
一只大袋鼠6 小时前
SpringBoot 初学阶段知识点汇总(一)
spring boot·笔记·后端
Rust研习社6 小时前
Rust 官方拟定 LLM 政策,防止 LLM 污染开源社区?
开发语言·后端·ai·rust·开源
无风听海6 小时前
ASP.NET Core Minimal API 深度解析
后端·asp.net
IT_陈寒6 小时前
Java的finally块竟然不是你想的那个finally!
前端·人工智能·后端