收货地址管理|Flask电商毕设,省市区联动+默认地址+下单快照 ⚠️免责声明本文项目仅供技术学习参考严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新学术诚信从我做起 先把三个坑说在前面收货地址很多人以为就是个增删改查。建张表存收货人、电话、地址然后CRUD四件套收工。真做下来会发现翻车的地方不在CRUD在三个细节省市区怎么存、默认地址怎么保证只有一个、下单时地址要不要存快照。前两个是常规题。第三个——下单地址快照——是很多人到最后答辩被老师问住的地方。这篇把这三个都说清楚。一、数据表设计先看表。CREATE TABLE address ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL COMMENT 所属用户, receiver varchar(50) NOT NULL COMMENT 收货人, phone varchar(20) NOT NULL COMMENT 手机号, province varchar(50) NOT NULL COMMENT 省, city varchar(50) NOT NULL COMMENT 市, district varchar(50) NOT NULL COMMENT 区/县, detail varchar(200) NOT NULL COMMENT 详细地址, is_default tinyint DEFAULT 0 COMMENT 是否默认0否1是, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个点。省市区拆三个字段别合成一个。有人图省事直接存一个广东省深圳市南山区的字符串。能跑但以后想做按地区统计订单就抓瞎了。拆开存扩展性强。detail 只存详细地址街道、门牌号别把省市区再重复存一遍。is_default 用 0/1不用布尔类型跨数据库兼容性好。user_id 建索引因为每次查地址列表都按用户查。补充提示演示毕设没有加updated_at更新时间字段如果是商用项目建议补上方便追踪地址修改记录。二、新增地址import re app.route(/address/add, methods[GET, POST]) login_required def address_add(): if request.method POST: receiver request.form.get(receiver, ).strip() phone request.form.get(phone, ).strip() province request.form.get(province, ).strip() city request.form.get(city, ).strip() district request.form.get(district, ).strip() detail request.form.get(detail, ).strip() is_default 1 if request.form.get(is_default) else 0 # 基础校验 if not all([receiver, phone, province, city, district, detail]): flash(请把地址信息填完整, error) return redirect(url_for(address_add)) # 手机号格式校验 if not re.match(r^1[3-9]\d{9}$, phone): flash(手机号格式不对, error) return redirect(url_for(address_add)) # 第一个地址自动设为默认 count Address.query.filter_by(user_idcurrent_user.id).count() if count 0: is_default 1 # 设了默认先把其他的默认取消 if is_default: Address.query.filter_by(user_idcurrent_user.id).update({is_default: 0}) addr Address( user_idcurrent_user.id, receiverreceiver, phonephone, provinceprovince, citycity, districtdistrict, detaildetail, is_defaultis_default ) db.session.add(addr) db.session.commit() flash(地址添加成功, success) return redirect(url_for(address_list)) return render_template(address/form.html)两个小逻辑值得说。第一个地址自动设为默认。用户加第一个地址时不用给他设为默认的选项——因为只有一个它就是默认。设默认时先清空其他默认。保证一个用户永远只有一个默认地址。⚠️代码短板提醒上面为了教程简洁省略了try‑except事务捕获。毕设演示可以正常运行实际项目需要增加异常捕获与db.session.rollback()回滚防止数据库中途报错造成部分数据更新。三、默认地址怎么保证只有一个这是最容易写错的地方。错误写法# ❌ 只改当前这条其他还是默认会出现多个默认地址 addr.is_default 1 db.session.commit()正确写法——先把该用户所有地址的 is_default 清零再设当前这条def set_default_address(address_id, user_id): 设置默认地址先全部清零再单独设置 Address.query.filter_by(user_iduser_id).update({is_default: 0}) addr Address.query.filter_by(idaddress_id, user_iduser_id).first() if addr: addr.is_default 1 db.session.commit()注意filter_by(user_iduser_id)这个条件不能漏。漏了会怎样A用户能改B用户的默认地址。这是越权。所以记住一条所有涉及地址的操作都要带上 user_id 过滤别只按 id 查。效果长这样——两条地址只有一条标着默认四、省市区联动前端三级联动思路很简单省变了市的选项跟着变市变了区跟着变。数据源一般是一份 JSON省市区数据。// 简化版用省市区数据字典 const regionData { 广东省: { 深圳市: [南山区, 福田区, 宝安区], 广州市: [天河区, 越秀区, 海珠区] }, // ... }; function loadProvince() { const sel document.getElementById(province); for (let p in regionData) { sel.add(new Option(p, p)); } } function loadCity() { const province document.getElementById(province).value; const citySel document.getElementById(city); citySel.length 1; // 保留请选择 document.getElementById(district).length 1; // 区也重置 if (province regionData[province]) { for (let c in regionData[province]) { citySel.add(new Option(c, c)); } } } // district 区县加载逻辑同理真正的省市区数据有几百KB毕设直接用现成的 JSON 文件网上一搜就有不用自己手写。一个坑编辑地址时页面加载完要先手动触发一次联动把省市区回填上不然下拉框是空的。别问我是怎么知道的。实际跑起来的表单长这样省、市、区县三个下拉框选省份下拉框展开露出可选省份选完省城市跟着联动带出注意前端联动只负责UI展示不能信任前端提交过来的省市区参数后端至少要做非空校验商用系统还需要校验行政区域合法性。五、下单时的地址快照重点前面都是常规操作。这一节是重点。先看一个场景。用户10月1号下单收货地址是深圳市南山区科技园1号。10月5号他搬家了在个人中心把地址改成了深圳市福田区XX路2号。现在问题来了他10月1号那个订单收货地址应该显示哪个如果订单表里只存了一个address_id指向地址表——那么地址一改历史订单的地址也跟着变了。用户点开10月1号的订单看到的是新家的地址。这是错的。历史订单要还原的是下单那一刻的信息不是现在的信息。正确做法订单表里存地址快照。CREATE TABLE orders ( ... -- 可以保留address_id当做参考业务展示优先使用快照字段 receiver_name varchar(50) NOT NULL COMMENT 收货人快照, receiver_phone varchar(20) NOT NULL COMMENT 手机号快照, receiver_address varchar(300) NOT NULL COMMENT 完整收货地址快照, ... );下单时把地址拍一张照塞进订单里app.route(/order/create, methods[POST]) login_required def order_create(): address_id request.form.get(address_id, typeint) # 取地址用户选了就用选的没选就用默认的 if address_id: address Address.query.filter_by(idaddress_id, user_idcurrent_user.id).first() else: address Address.query.filter_by(user_idcurrent_user.id, is_default1).first() if not address: flash(请先添加收货地址, error) return redirect(url_for(cart)) # 拼成完整地址 full_address f{address.province}{address.city}{address.district}{address.detail} order Order( user_idcurrent_user.id, # 地址快照——存进去就固定了之后地址怎么改都不影响这一单 receiver_nameaddress.receiver, receiver_phoneaddress.phone, receiver_addressfull_address, ... ) db.session.add(order) db.session.commit()下单后进订单详情收货人和地址就是下单那一刻的快照之后改地址、删地址都不影响这单为什么这么做因为订单是历史凭证。它记录的是当时发生了什么不是现在是什么样。同一个道理订单明细表里也要存商品快照——商品名、图片、单价。这样商品下架了、改名了、涨价了历史订单也不会变。这也解释了另一个问题地址可以被删但订单不受影响——因为订单里已经存了完整信息根本不依赖地址表。这一点答辩很爱问记住。六、补充地址编辑功能实现前面实现了新增、删除地址实际项目还需要编辑地址功能。编辑接口很容易踩两个坑越权访问、修改默认地址逻辑处理。逻辑注意点查询地址必须同时校验id和user_id防止用户篡改id参数读取、修改他人地址。只有用户本次勾选“设为默认地址”时才执行清空全部默认标记不勾选则保留这条地址原本的默认状态不要随意覆盖。编辑页面复用新增地址的模板回显旧地址数据。前端省市区下拉框回显时页面加载完要手动触发联动函数否则下拉框数据为空。app.route(/address/edit/int:address_id, methods[GET, POST]) login_required def address_edit(address_id): # 查询地址必须带上user_id越权防护 addr Address.query.filter_by(idaddress_id, user_idcurrent_user.id).first() if not addr: flash(地址不存在或无权操作, error) return redirect(url_for(address_list)) if request.method POST: receiver request.form.get(receiver, ).strip() phone request.form.get(phone, ).strip() province request.form.get(province, ).strip() city request.form.get(city, ).strip() district request.form.get(district, ).strip() detail request.form.get(detail, ).strip() is_default 1 if request.form.get(is_default) else 0 # 基础非空校验 if not all([receiver, phone, province, city, district, detail]): flash(请把地址信息填完整, error) return render_template(address/form.html, addraddr) # 手机号正则校验 if not re.match(r^1[3-9]\d{9}$, phone): flash(手机号格式不对, error) return render_template(address/form.html, addraddr) # 如果本次编辑勾选设为默认先清空该用户所有默认地址 if is_default: Address.query.filter_by(user_idcurrent_user.id).update({is_default: 0}) addr.is_default 1 # 更新普通字段 addr.receiver receiver addr.phone phone addr.province province addr.city city addr.district district addr.detail detail db.session.commit() flash(地址修改成功, success) return redirect(url_for(address_list)) # GET请求渲染表单回显原有地址 return render_template(address/form.html, addraddr)⚠️代码为教程演示简化版本生产环境建议加上 try‑except rollback 数据库事务处理。❌很多同学写编辑时的错误示范# ❌错误写法不管勾不勾选默认直接覆盖is_default addr.is_default is_default问题用户编辑地址没有勾选“设为默认”会把原本的默认地址直接变成非默认业务bug。✅正确只有is_default等于1的时候才去修改默认状态。form.html 复用表单模板参考!-- form.html 新增/编辑复用表单 -- form methodpost div label收货人/label input typetext namereceiver value{{ addr.receiver if addr else }} required /div div label手机号/label input typetext namephone value{{ addr.phone if addr else }} required /div div select idprovince nameprovince required option value请选择省份/option /select /div div select idcity namecity required option value请选择城市/option /select /div div select iddistrict namedistrict required option value请选择区县/option /select /div div label详细地址/label input typetext namedetail value{{ addr.detail if addr else }} required /div div input typecheckbox nameis_default value1 {% if addr and addr.is_default %}checked{% endif %} label设为默认地址/label /div button typesubmit保存/button /form {% if addr %} !-- 编辑模式页面加载完成回填省市区 -- script window.onload function(){ // 给下拉框赋值再触发联动 document.getElementById(province).value{{ addr.province }}; loadCity(); document.getElementById(city).value{{ addr.city }}; // 此处自行补充加载区县的函数调用 } /script {% endif %}编辑回显效果——省市区下拉框自动回填默认勾选框也带出来七、删除地址app.route(/address/delete/int:address_id, methods[POST]) login_required def address_delete(address_id): # 一定要带 user_id防止删别人的地址 addr Address.query.filter_by(idaddress_id, user_idcurrent_user.id).first() if not addr: flash(地址不存在, error) return redirect(url_for(address_list)) was_default addr.is_default db.session.delete(addr) db.session.commit() # 如果删掉的是默认地址把剩下最新的一个补为默认 if was_default: first Address.query.filter_by(user_idcurrent_user.id)\ .order_by(Address.created_at.desc()).first() if first: first.is_default 1 db.session.commit() flash(地址已删除, success) return redirect(url_for(address_list))注意最后那段删掉默认地址后要补一个默认上去。不然用户下次下单、没主动选地址时系统就找不到默认地址了。边界业务处理删除前校验地址归属不能删除其他用户地址如果被删除的是默认地址则自动将剩余地址中创建时间最新的地址设置为新默认特殊情况当用户只有一条地址删除后该用户没有任何地址不会生成默认下单页面会提示用户新增地址。这种边界情况就是毕设和能跑就行的分水岭。地址这块答辩爱问这几问1. 为什么下单要存地址快照不直接关联地址表答订单是历史凭证。用户之后可能修改或删除地址如果订单只存 address_id历史订单的收货信息就会跟着变这是错的。存快照能保证订单记录的是下单那一刻的真实信息。同理订单明细里也要存商品快照。2. 一个用户能有几个默认地址答只能有一个。设置默认时先把该用户所有地址的 is_default 清零再设置目标地址。另外所有地址操作都要带上 user_id 条件防止越权。3. 省市区为什么要拆成三个字段答方便按地区做统计和筛选。如果合成一个字符串以后想做广东省的订单有多少就得做字符串匹配效率和准确性都差。4. 用户删除地址后历史订单会受影响吗答不会。订单里存的是地址快照不依赖地址表。地址删了订单信息依然完整。收货地址这块代码量不大但藏着几个容易踩的专业坑。省市区怎么存、默认地址怎么保唯一、下单要不要快照——这三关一过个人中心这块基本就稳了。尤其是地址快照这个点它背后是电商系统的一条基本设计原则订单记录的是历史不是现状。想明白这一条购物车、订单、售后那些地方很多设计就通了——这也是我做完这套最实在的体会。梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处