IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。
前面几篇我们完成了用户注册、登录、个人中心等功能,用户体系已经基本成型。但电商平台还有一个绕不开的基础模块------收货地址。用户下单时总得告诉商家"送到哪儿",所以今天我们就来实现收货地址的完整增删改查,并支持设置默认地址。
在第 2 篇设计数据库时,我们已经建好了 Address 模型,现在只需补上视图、表单、模板和路由。全程代码量不大,但有几个业务细节值得注意:默认地址的唯一性切换、地址数量上限、以及删除时的软提示。
一、需求分析
收货地址模块的功能点:
-
地址列表:展示当前用户所有收货地址,默认地址置顶且高亮。
-
新增地址:表单填写收件人、手机号、省市区、详细地址,可设为默认。
-
编辑地址:修改已有地址的信息。
-
删除地址:删除指定地址,若删除的是默认地址,则将最近更新的地址设为默认。
-
设置默认:在列表页一键设置默认地址(或通过编辑页勾选实现)。
-
权限控制:仅登录用户可操作,且只能操作自己的地址。
二、模型回顾
我们在 apps/users/models.py 中已定义 Address,核心字段:
-
user:外键关联用户,related_name='addresses' -
receiver、phone、province、city、district、detail -
is_default:布尔字段,标记默认地址 -
create_time、update_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
测试步骤:
-
登录 :用手机号
13800138000登录。 -
进入地址列表 :导航栏点击个人中心 → 侧边栏"收货地址",或直接访问
/users/address/。初始没有地址,显示提示信息。 -
新增地址:点击"新增地址",填写收件人"张三"、手机号 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
地址卡片中显示"张三"并有蓝色"默认"徽章。
-
新增第二个地址:新增收件人"李四"、手机号 13800000002、北京市朝阳区、详细地址"望京 SOHO",不勾选默认。提交后列表页展示两个地址卡片,默认地址依然是张三。
-
设为默认:在"李四"的卡片上点击"设为默认"按钮。页面刷新后,"李四"变为默认地址(带蓝框和徽章),张三不再有默认标记。
终端:
bash
[22/May/2026 09:12:33] "POST /users/address/set_default/2/ HTTP/1.1" 302 0
-
编辑地址:点击张三的"编辑",修改手机号为 13900000000,勾选设为默认。保存后,张三重新变成默认地址,李四取消默认标记。
-
删除地址:点击李四的"删除"按钮,弹出确认框,确定后李四的地址消失。张三仍是默认。
终端:
bash
[22/May/2026 09:15:00] "POST /users/address/delete/2/ HTTP/1.1" 302 0
- 删除默认地址:现在只剩下张三一个默认地址,点击删除。页面显示"你还没有添加收货地址",并在后台自动将无地址了(前面逻辑中如果删完没有地址则不设默认)。重新新增一个地址,应该没有默认标记,手动设置为默认正常。
九、数据验证
用 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 篇。