Django 从 0 到 1 打造完整电商平台:收货地址管理
IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我也会在其它的平台持续发布最新文章,助你少走弯路。
前面几篇我们完成了用户注册、登录、个人中心等功能,用户体系已经基本成型。但电商平台还有一个绕不开的基础模块——收货地址。用户下单时总得告诉商家“送到哪儿”,所以今天我们就来实现收货地址的完整增删改查,并支持设置默认地址。
在第 2 篇设计数据库时,我们已经建好了Address模型,现在只需补上视图、表单、模板和路由。全程代码量不大,但有几个业务细节值得注意:默认地址的唯一性切换、地址数量上限、以及删除时的软提示。
一、需求分析
收货地址模块的功能点:
地址列表:展示当前用户所有收货地址,默认地址置顶且高亮。
新增地址:表单填写收件人、手机号、省市区、详细地址,可设为默认。
编辑地址:修改已有地址的信息。
删除地址:删除指定地址,若删除的是默认地址,则将最近更新的地址设为默认。
设置默认:在列表页一键设置默认地址(或通过编辑页勾选实现)。
权限控制:仅登录用户可操作,且只能操作自己的地址。
二、模型回顾
我们在apps/users/models.py中已定义Address,核心字段:
user:外键关联用户,related_name='addresses'receiver、phone、province、city、district、detailis_default:布尔字段,标记默认地址create_time、update_time
模型无需修改,直接用。
三、编写表单
在apps/users/forms.py中追加地址表单:
from .modelsimportAddress 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')ifphone and not phone.isdigit()or len(phone)!=11: raise forms.ValidationError('请输入有效的11位手机号')returnphone地址表单直接绑定 ModelForm,省去大量重复字段定义。手机号做了简单的格式校验。
四、编写视图
打开apps/users/views.py,在文件顶部确保导入Address模型和新表单:
from .modelsimportUser, Address from .formsimport(RegisterForm, LoginForm, UpdateProfileForm, ChangePasswordForm, AddressForm)4.1 地址列表
@login_required(login_url='users:login')def address_list(request): addresses=request.user.addresses.all()returnrender(request,'users/address_list.html',{'addresses':addresses})利用related_name='addresses'反向查询,一行拿到所有地址。
4.2 新增地址
@login_required(login_url='users:login')def address_create(request):ifrequest.method=='POST':form=AddressForm(request.POST)ifform.is_valid(): address=form.save(commit=False)address.user=request.user# 如果勾选了默认,先取消该用户其他默认地址ifaddress.is_default: request.user.addresses.filter(is_default=True).update(is_default=False)address.save()messages.success(request,'收货地址添加成功。')returnredirect('users:address_list')else: form=AddressForm()returnrender(request,'users/address_form.html',{'form':form,'title':'新增收货地址'})关键点:commit=False让你先不存入数据库,绑上user后再保存。设置默认地址时,必须先将其它地址的is_default置为False,保证默认地址唯一。
4.3 编辑地址
@login_required(login_url='users:login')def address_edit(request, pk): address=get_object_or_404(Address,pk=pk,user=request.user)ifrequest.method=='POST':form=AddressForm(request.POST,instance=address)ifform.is_valid(): updated_address=form.save(commit=False)# 如果本次设为默认,同样取消其他默认ifupdated_address.is_default: request.user.addresses.filter(is_default=True).exclude(pk=address.pk).update(is_default=False)updated_address.save()messages.success(request,'地址修改成功。')returnredirect('users:address_list')else: form=AddressForm(instance=address)returnrender(request,'users/address_form.html',{'form':form,'title':'编辑收货地址'})用get_object_or_404确保只能编辑自己的地址(通过user=request.user过滤)。
4.4 删除地址
@login_required(login_url='users:login')def address_delete(request, pk): address=get_object_or_404(Address,pk=pk,user=request.user)ifrequest.method=='POST':was_default=address.is_default address.delete()# 如果删除的是默认地址,尝试将最新的地址设为默认ifwas_default: latest_address=request.user.addresses.first()iflatest_address: latest_address.is_default=True latest_address.save(update_fields=['is_default'])messages.success(request,'地址已删除。')returnredirect('users:address_list')returnrender(request,'users/address_confirm_delete.html',{'address':address})删除默认地址后,自动将剩余地址中最新的一条(Meta.ordering我们设为了['-is_default', '-create_time'],所以first()是最近创建的)设置为默认。
4.5 快捷设置默认地址
我们也可以在列表页直接加一个“设为默认”按钮,用一个小视图处理:
@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}”的地址设为默认。')returnredirect('users:address_list')五、配置 URL
在apps/users/urls.py中添加新路由:
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:
{% extends'base.html'%}{% block title %}我的收货地址{% endblock %}{% block content %}<divclass="d-flex justify-content-between align-items-center mb-3"><h3>📍 收货地址管理</h3><ahref="{% url 'users:address_create' %}"class="btn btn-primary">+ 新增地址</a></div>{%ifaddresses %}<divclass="row">{%foraddrinaddresses %}<divclass="col-md-6 mb-3"><divclass="card shadow-sm {% if addr.is_default %}border-primary{% endif %}"><divclass="card-body">{%ifaddr.is_default %}<spanclass="badge bg-primary float-end">默认</span>{% endif %}<h5class="card-title">{{addr.receiver}}</h5><pclass="card-text mb-1">{{addr.phone}}</p><pclass="card-text text-muted">{{addr.province}}{{addr.city}}{{addr.district}}<br>{{addr.detail}}</p><divclass="mt-2"><ahref="{% url 'users:address_edit' addr.pk %}"class="btn btn-sm btn-outline-secondary">编辑</a>{%ifnot addr.is_default %}<formaction="{% url 'users:address_set_default' addr.pk %}"method="post"style="display:inline;">{% csrf_token %}<buttontype="submit"class="btn btn-sm btn-outline-primary">设为默认</button></form>{% endif %}<buttontype="button"class="btn btn-sm btn-outline-danger"onclick="confirmDelete({{ addr.pk }}, '{{ addr.receiver }}')">删除</button></div></div></div></div>{% endfor %}</div>{%else%}<divclass="alert alert-info">你还没有添加收货地址,<ahref="{% url 'users:address_create' %}">去添加</a>。</div>{% endif %}<!-- 隐藏的删除确认表单 --><formid="delete-form"method="post"action="{% url 'users:address_delete' 0 %}"style="display:none;">{% csrf_token %}</form>{% endblock %}{% block extra_js %}<script>functionconfirmDelete(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:
{% extends'base.html'%}{% block title %}{{title}}{% endblock %}{% block content %}<divclass="row justify-content-center"><divclass="col-md-8 col-lg-6"><divclass="card shadow-sm"><divclass="card-body p-4"><h3class="text-center mb-4">{%if'新增'intitle %}➕{%else%}✏️{% endif %}{{title}}</h3><formmethod="post"novalidate>{% csrf_token %}<divclass="row"><divclass="col-md-6 mb-3"><labelclass="form-label">收件人</label>{{form.receiver}}{{form.receiver.errors}}</div><divclass="col-md-6 mb-3"><labelclass="form-label">手机号</label>{{form.phone}}{{form.phone.errors}}</div></div><divclass="row"><divclass="col-md-4 mb-3"><labelclass="form-label">省份</label>{{form.province}}{{form.province.errors}}</div><divclass="col-md-4 mb-3"><labelclass="form-label">城市</label>{{form.city}}{{form.city.errors}}</div><divclass="col-md-4 mb-3"><labelclass="form-label">区/县</label>{{form.district}}{{form.district.errors}}</div></div><divclass="mb-3"><labelclass="form-label">详细地址</label>{{form.detail}}{{form.detail.errors}}</div><divclass="mb-3 form-check">{{form.is_default}}<labelclass="form-check-label"for="{{ form.is_default.id_for_label }}">{{form.is_default.label}}</label></div>{%ifform.non_field_errors %}<divclass="alert alert-danger">{{form.non_field_errors.0}}</div>{% endif %}<buttontype="submit"class="btn btn-primary w-100">保存</button><ahref="{% 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:
{% extends'base.html'%}{% block title %}删除地址{% endblock %}{% block content %}<divclass="row justify-content-center"><divclass="col-md-6"><divclass="card shadow-sm"><divclass="card-body text-center p-4"><h5class="mb-3">确定要删除以下收货地址吗?</h5><p><strong>{{address.receiver}}</strong>{{address.phone}}</p><pclass="text-muted">{{address.province}}{{address.city}}{{address.district}}{{address.detail}}</p><formmethod="post">{% csrf_token %}<buttontype="submit"class="btn btn-danger">确认删除</button><ahref="{% 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="#"替换为:
<ahref="{% url 'users:address_list' %}"class="text-decoration-none">收货地址</a>八、完整流程测试
启动服务器:
python manage.py runserver测试步骤:
登录:用手机号
13800138000登录。进入地址列表:导航栏点击个人中心 → 侧边栏“收货地址”,或直接访问
/users/address/。初始没有地址,显示提示信息。新增地址:点击“新增地址”,填写收件人“张三”、手机号 13800000001、省份广东、城市深圳、区/县南山区、详细地址“科技园路1号”,勾选“设为默认地址”,提交。
终端输出:
[22/May/2026 09:10:45]"POST /users/address/create/ HTTP/1.1"3020[22/May/2026 09:10:45]"GET /users/address/ HTTP/1.1"2002987地址卡片中显示“张三”并有蓝色“默认”徽章。
新增第二个地址:新增收件人“李四”、手机号 13800000002、北京市朝阳区、详细地址“望京 SOHO”,不勾选默认。提交后列表页展示两个地址卡片,默认地址依然是张三。
设为默认:在“李四”的卡片上点击“设为默认”按钮。页面刷新后,“李四”变为默认地址(带蓝框和徽章),张三不再有默认标记。
终端:
[22/May/2026 09:12:33]"POST /users/address/set_default/2/ HTTP/1.1"3020编辑地址:点击张三的“编辑”,修改手机号为 13900000000,勾选设为默认。保存后,张三重新变成默认地址,李四取消默认标记。
删除地址:点击李四的“删除”按钮,弹出确认框,确定后李四的地址消失。张三仍是默认。
终端:
[22/May/2026 09:15:00]"POST /users/address/delete/2/ HTTP/1.1"3020- 删除默认地址:现在只剩下张三一个默认地址,点击删除。页面显示“你还没有添加收货地址”,并在后台自动将无地址了(前面逻辑中如果删完没有地址则不设默认)。重新新增一个地址,应该没有默认标记,手动设置为默认正常。
九、数据验证
用dbshell查询地址表:
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 篇。
