当前位置: 首页 > news >正文

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中追加地址表单:

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

测试步骤:

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

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

  3. 新增地址:点击“新增地址”,填写收件人“张三”、手机号 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

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

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

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

终端:

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

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

终端:

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

九、数据验证

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 篇。

http://www.jsqmd.com/news/868171/

相关文章:

  • Windows 11/10系统瘦身与性能优化:手把手教你用DISM禁用不常用功能
  • 得物数仓AI开发痛点多,Harness工程四层分工让研发流水线更可靠!
  • ubuntu虚拟环境中安装python包,报错
  • MPI_Win_allocate_shared介绍和使用
  • ops-softmax:Transformer 推理中的概率归一化引擎
  • 贴片晶振的广泛应用与768kHz晶振的优势
  • 阿里巴巴与厦门大学联手打造“时装变色龙“
  • OpenClaw:高效管理分布式Agent开发团队
  • Claude Code 国内替代方案:基于百炼的配置与实践
  • Newman安装之nodejs下载安装
  • ops-reduce:ReduceMax 与 ReduceMean 的并行优化
  • 把大脑交给自己,而非交给 AI —— Files.md 的“极简知识管理“哲学
  • RK3588下位机程序无响应问题排查
  • 百度网盘提取码终极查询指南:10秒告别手动搜索的烦恼
  • 北大等研究揭示:AI答题正确背后存在可被捕捉的作弊行为漏洞
  • python文化旅游服务系统 小程序系统
  • 私有化 IM vs 公有云 IM:3 个维度告诉你该怎么选
  • 为什么你的 AI 应用做不成 Agent
  • 抖音下载神器:免费批量下载视频、图集、音乐和直播回放完整指南
  • 基于TinyEngine低代码引擎的AI Agent开发完整指南
  • 长沙短视频拍摄引流哪家更值得信赖
  • python新能源汽车4s店车辆管理系统
  • 5分钟搞定Windows 11区域语言模拟:Locale Remulator终极指南
  • 2026年4月正规的接待台定制源头厂家推荐,创意接待台定制吸引目光 - 品牌推荐师
  • 有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用
  • Python/JS/Go三语言生成质量对比,错误率、可维护性、安全漏洞全维度打分,开发者速查清单!
  • 2026年IEEE TEVC,面向城市电缆布线优化的双层多精度搜索框架
  • 2026现阶段合肥养老中心怎么选?聚焦专业护理价值的深度指南 - 2026年企业推荐榜
  • 2026大模型安全评估报告|一键通关撰写攻略(备案专用)
  • ElevenLabs方言支持白皮书(2024Q2):安徽话覆盖度仅61.7%?我们逆向解析其phoneme inventory并开源替代音素映射表