开头先唠两句。我刚做完一个 "基于 Java+SSM+Django 的网上花店系统",源码、论文、调试文档、答辩讲解视频全套齐活儿。说句实在话,这项目在网上搜代码的不少,但大多数贴出来的只有一堆 Controller 和 Mapper,真正把"两个技术栈为什么要共用""订单状态机怎么设计""前后端数据怎么对齐"讲明白的不多。这篇就把我整个从零搭到出包的思路和踩坑过程写出来,给正在做毕设或者想练全栈手感的同学一个参照。
1. 花店系统这个项目到底要解决什么问题
1.1 业务需求拆解:线上花店不只是"有商品有购物车"
很多人一听"网上花店系统",第一反应就是"不就是个商城吗"。真上手调研之后会发现,花卉这个品类和标准百货差异很大,需求拆解不好,做出来的系统看着完整,答辩时却经不起一句"为什么这么设计"。
我拆下来的核心痛点有三个:
- 时效性:鲜花是强时效商品。用户今天下单可能为了明天过节,所以订单从"支付成功"到"开始配送"的流转时间,系统中必须有明确的状态节点和操作按钮,不能只有一个笼统的"订单表"。
- 非标性:一束花不是一台手机,它可能有主花、配花、包装纸、贺卡留言。我做的商品模型里有一个"花材清单"字段,用来描述一束花由哪些花材构成,而不是简单地存一个标题和一张图。
- 经营双向性:光有用户端不够,花店老板需要后台管理商品上下架、库存调整、订单发货。所以项目必须有独立的后台管理端,而不是只在数据库中手动改数据。
我最后落地的功能清单大致如下:
| 端侧 | 功能模块 | 关键操作 |
|---|---|---|
| 用户端 | 商品浏览 | 按分类筛选、按销量排序、关键词搜索 |
| 用户端 | 购物车 | 加购、改数量、勾选结算、删除 |
| 用户端 | 下单支付 | 填写收货地址、创建订单、模拟支付、查看订单状态 |
| 用户端 | 评论 | 订单完成后对商品评分留言 |
| 管理端 | 商品管理 | 新增/编辑/上下架、库存调整 |
| 管理端 | 订单管理 | 订单列表、发货操作、查看详情 |
| 管理端 | 用户管理 | 用户列表、禁用启用 |
| 系统级 | 登录认证 | 用户端与后台分离登录,密码加密存储 |
1.2 技术选型背景:为什么毕设和实战项目里这套组合常见
标题里写的 "Java+SSM+Django" 一开始看上去有点怪,Java 和 Python 两套 Web 框架同时在项目里出现,实际生产项目不常见。但在"教学项目 + 毕设"这个场景下,这种组合反而有它的合理性:它可以用一个项目同时展示对 Spring+SpringMVC+MyBatis 这套经典 Java 技术栈的掌握,也展示 Django 全家桶的熟练度。
我实际采用的划分方式:
- Django 负责用户端:商品展示页、购物车、订单提交、支付模拟。Django 的 ORM 和模板引擎能快速把页面跑通,适合前台;
- Java SSM 负责管理后台:后台的权限、商品管理、订单处理、数据统计。用 Spring 的事务和 MyBatis 的 SQL 控制来做更顺手。
两个服务连接的是同一个 MySQL 数据库。这一点我要明确说:生产环境通常不建议两个 Web 框架直接连同一个库,会在中间加一层 API 网关,或者直接统一成一个技术栈。但毕设项目追求的是"业务闭环 + 工作量体现 + 技术展示面广",双栈直连 MySQL 是可接受的,只要在论文里把表结构设计和数据归属讲清楚,答辩不会露怯。
提示:如果怕两个端操作数据库时"打架",我建议约定一个写入原则——用户端只写
订单、购物车、评论这类业务表;管理端只写商品表、库存表,且通过字段级更新来减少覆盖。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SSM 与 Django 双栈并存:架构设计与协作方式
2.1 双端划分:谁能直连数据库,谁走服务调用
我最初也纠结过要不要让两端走 HTTP API 通信。后来实际权衡发现:走 API 会让工作量翻倍,而且两边都要写序列化和接口文档,对毕设节奏不友好。真正干净可维护的方案是"共享数据库 + 各管一摊"。
画图的话大致是这样:
code复制浏览器(用户端页面)
│
▼
Django(视图 + 模板渲染 + ORM)
│
▼
┌─────┴──────┐
│ MySQL数据库 │
└─────┬──────┘
│
▼
SSM 管理与统计接口
│
▼
浏览器(管理员后台)
两端中间没有多余的中间层,但表设计上要做好"数据归属权"的划分:
flower、category、stock_log归管理端写入;cart_item、order_info、order_item、review归用户端写入;user表两边都可能读写,但不直接互相修改,统一由各自框架的加密逻辑处理密码再落库。
2.2 页面渲染和接口之间的衔接方式
这套架构里有一个容易被忽略的经验:用户端页面不是纯 HTML + AJAX,而是 Django 模板 + ORM 渲染;管理端也不是 JSP,而是前后端不分离的 SpringMVC 视图加少量 AJAX。两边都用"服务端渲染"做主,数据提交用表单 POST。优势是演示时不依赖跨域配置,也不用手写一堆 Vue 异步代码,项目更容易跑通。
管理端如果用到 JSON 接口(比如后台统计图),我通常只在 SpringMVC 的 Controller 上使用 @ResponseBody 返回 Map 或者封装好的 Result 对象。这样既体现了我对前后端分离的理解,又不至于让项目失控。
架构层面另外一个重点是 Spring 事务与 Django 事务的一致性。由于两端共用数据库,跨端的"创建订单 + 扣库存"场景要尤其小心。我当时的处理方式是:用户端下单只写订单(状态为"待支付"),管理端在"确认发货"时才真正扣减库存。这样就把"扣库存"这一写操作收敛到了单一端(Java 后台),避免两个框架同时改库存行的并发问题。
3. 数据库建模:花店业务如何拆表
3.1 核心数据表设计
网上花店的表结构说难不难,说简单也容易漏字段。我们按业务对象过一遍我最终采用的表结构:
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键自增 |
| username | varchar(30) | 登录名,唯一 |
| password | varchar(64) | BCrypt 加密后的密码 |
| nickname | varchar(30) | 显示昵称 |
| phone | varchar(20) | 手机号 |
| avatar | varchar(255) | 头像地址 |
| status | tinyint | 1 正常 0 禁用 |
| create_time | datetime | 注册时间 |
花卉分类表(category)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| name | varchar(30) | 分类名 |
| sort_order | int | 展示顺序 |
分类我做了两级规划,但第一版先不过度设计,只设计一级分类:玫瑰、百合、康乃馨、向日葵、绿植、永生花。加 parent_id 是为了以后扩展,不过这次项目没用递归查询,管理端新增分类时也只是平级。
花卉商品表(flower)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| name | varchar(100) | 商品名,如"香槟玫瑰礼盒" |
| category_id | int | 所属分类 |
| price | decimal(10,2) | 售价 |
| original_price | decimal(10,2) | 原价,用于展示折扣 |
| stock | int | 库存 |
| sales | int | 销量 |
| main_image | varchar(255) | 主图 |
| detail | text | 详情描述 |
| material | varchar(255) | 花材清单 |
| status | tinyint | 1 上架 0 下架 |
商品表一定不能忽略 sales 和 status。sales 支撑用户端的"按销量排序"和后台的"热销商品"标签;status 支撑下架。我见过不少项目把 delete 和 down_shelf 混为一谈,最后后台只能物理删数据,这是不好的——线上运营需要的只是下架,不是删除。
购物车表(cart_item)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 用户 id |
| flower_id | int | 商品 id |
| quantity | int | 数量 |
| checked | tinyint | 是否勾选结算 |
| create_time | datetime | 加入时间 |
| update_time | datetime | 更新时间 |
订单表(order_info)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar(32) | 订单号,唯一 |
| user_id | int | 用户 id |
| total_amount | decimal(10,2) | 订单总金额 |
| receiver_name | varchar(20) | 收货人 |
| receiver_phone | varchar(20) | 收货电话 |
| receiver_address | varchar(255) | 收货地址 |
| payment_status | tinyint | 0 未支付 1 已支付 |
| order_status | tinyint | 0 待支付 1 待发货 2 待收货 3 已完成 4 已取消 |
| remark | varchar(255) | 用户备注(如贺卡留言) |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
| ship_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
订单明细表(order_item)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_id | int | 订单表主键 |
| flower_id | int | 商品 id |
| flower_name | varchar(100) | 快照:商品名 |
| flower_price | decimal(10,2) | 快照:成交单价 |
| quantity | int | 数量 |
| subtotal | decimal(10,2) | 小计 |
为什么要做 商品快照?这是关键。用户下单时商品价格是 50 元,管理员第二天改价成 60 元,如果订单明细里不存当时的价格,历史订单统计和售后都会出事。所以订单明细一定要冗余当时的商品名、单价、图片地址,而不是只存一个 flower_id 再去 join 商品表。
3.2 状态机设计:订单生命周期管理
订单状态是我在数据库层面最看重的一个点。用户端和管理端的每一个操作按钮,都是状态机里的一个合法迁移:
- 用户提交订单 →
payment_status=0, order_status=0(待支付) - 用户模拟支付 →
payment_status=1, order_status=1(待发货) - 管理员发货 →
order_status=2(待收货),记录ship_time - 用户确认收货 →
order_status=3(已完成),记录finish_time - 用户未支付时主动取消 →
order_status=4(已取消)
非法迁移要在代码里拦截。比如管理员不能把一个 "待支付" 订单直接发货,用户不能把 "待发货" 订单确认收货。
我当时在 order_info 表上建了几个联合索引:
sql复制ALTER TABLE order_info ADD INDEX idx_user_status (user_id, order_status);
ALTER TABLE order_info ADD INDEX idx_order_no (order_no);
ALTER TABLE order_info ADD INDEX idx_create_time (create_time);
因为用户端最常见查询是"我的订单列表",管理端最常见查询是"按状态筛选订单",这两个索引能扛住演示数据甚至几十万条级别的数据量。
4. 用户端核心流程实现:从浏览到下单
4.1 Django 端商品展示与查询优化
Django 端我用的是 MTV 模型,Controller 的角色由 views.py 承担。首页商品展示,我写了一个视图函数如下:
python复制# flowers/views.py
from django.shortcuts import render
from .models import Flower, Category
def index(request):
hot_flowers = Flower.objects.filter(status=1).order_by('-sales')[:8]
new_flowers = Flower.objects.filter(status=1).order_by('-id')[:8]
categories = Category.objects.all()
return render(request, 'index.html', {
'hot_flowers': hot_flowers,
'new_flowers': new_flowers,
'categories': categories,
})
这里有个小细节:order_by('-sales') 是按销量倒序,order_by('-id') 是按新增时间倒序。用户端列表页我用的是 Django 自带的 Paginator 分页,避免一次性把几百个商品全查出来。
分类页和搜索页的逻辑差不多:
python复制def category_flowers(request, category_id):
flowers = Flower.objects.filter(category_id=category_id, status=1)
# 支持按价格/销量排序
sort = request.GET.get('sort', 'default')
if sort == 'price_asc':
flowers = flowers.order_by('price')
elif sort == 'price_desc':
flowers = flowers.order_by('-price')
elif sort == 'sales':
flowers = flowers.order_by('-sales')
...
提示:
Filter(status=1)一定要带上,否则后台下架的商品还会在前台展示。这个字段逻辑很简单但特别容易漏,答辩演示时如果被发现"下架商品还能买",印象分会掉很多。
4.2 购物车与订单创建的业务逻辑
购物车我用 Django 的 session 做也行,用表也可以。我选了表,因为"持久化"更符合毕设评审的口味:用户换设备购物车还在,也方便后台统计活跃用户。
加购逻辑:
python复制def add_cart(request, flower_id):
flower = Flower.objects.get(id=flower_id)
cart_item, created = CartItem.objects.get_or_create(
user=request.user,
flower=flower,
defaults={'quantity': 1}
)
if not created:
cart_item.quantity += 1
cart_item.save()
return redirect('cart_detail')
get_or_create 是 Django 很实用的方法,用来避免"先查再插"的竞态。但是注意,get_or_create 对唯一约束有依赖,所以 user 和 flower 的组合最好在数据库里加 unique_together,保证同一用户同一商品只有一行。
订单创建是整个用户端最复杂的一段逻辑。我的做法是三步:
- 校验:从购物车勾选项里读取商品,检查是否全部上架、库存是否足够。
- 计算:遍历生成
order_item临时列表,计算total_amount。 - 落库:用
transaction.atomic()包住order_info和order_item的写入。
python复制from django.db import transaction
@transaction.atomic
def create_order(request):
cart_items = CartItem.objects.filter(user=request.user, checked=True)
if not cart_items.exists():
return render(request, 'cart.html', {'error': '请先勾选商品'})
total = 0
order_items = []
for item in cart_items:
flower = item.flower
if flower.status != 1:
raise ValueError(f'商品 {flower.name} 已下架')
if flower.stock < item.quantity:
raise ValueError(f'商品 {flower.name} 库存不足')
subtotal = flower.price * item.quantity
total += subtotal
order_items.append(OrderItem(
flower=flower,
flower_name=flower.name,
flower_price=flower.price,
quantity=item.quantity,
subtotal=subtotal
))
order_no = generate_order_no()
order = OrderInfo.objects.create(
order_no=order_no,
user=request.user,
total_amount=total,
receiver_name=request.POST['receiver_name'],
receiver_phone=request.POST['receiver_phone'],
receiver_address=request.POST['receiver_address'],
remark=request.POST.get('remark', ''),
)
OrderItem.objects.bulk_create(order_items)
cart_items.delete()
return order
@transaction.atomic 在这里极其关键:如果 bulk_create 失败,前面 create_order 已经写入的订单主表也会回滚,不会出现"有订单头没订单明细"的脏数据。
4.3 支付与配送状态模拟
真实支付系统需要接入三方支付,毕设通常用"模拟支付"来体现流程闭环。我在 Django 端做了个简单的支付页:展示订单金额和一个"确认支付"按钮,点击后把 payment_status 置 1、order_status 置 1,并记录 pay_time。
python复制def pay_order(request, order_id):
order = get_object_or_404(OrderInfo, id=order_id, user=request.user)
if order.order_status != 0:
return render(request, 'error.html', {'message': '订单状态不正确'})
order.payment_status = 1
order.order_status = 1
order.pay_time = timezone.now()
order.save()
return redirect('order_detail', order_id=order.id)
这个流程里没有真的扣库存,因为我的库存扣减放在管理端发货时。这样做的好处前面说过:扣库存的唯一入口在 Java 后台,避免两边同时改商品表造成超卖风险。对毕设答辩来说,这个设计思路本身就有解释价值。
5. 管理后台实现:商品、库存、订单管理
5.1 SSM 三层架构的落地拆分
管理后台我用了标准的 SSM 三层:
- Controller 层:接收请求、参数校验、返回视图或 JSON。
- Service 层:业务逻辑,事务边界在这里。
- Mapper 层:MyBatis 的 SQL 映射,只做数据读写。
以商品列表为例,Controller 大概长这样:
java复制@Controller
@RequestMapping("/admin/flower")
public class FlowerController {
@Autowired
private FlowerService flowerService;
@RequestMapping("/list")
public String list(Model model, @RequestParam(defaultValue = "1") int page) {
PageHelper.startPage(page, 10);
List<Flower> list = flowerService.listAll();
PageInfo<Flower> pageInfo = new PageInfo<>(list);
model.addAttribute("pageInfo", pageInfo);
return "admin/flower_list";
}
}
页面用了 JSP + JSTL,没有引前端框架。这个选择是刻意为之:管理端页面数量不多、形态也比较固定,用 JSP 能让部署包更轻,也不需要 Node.js 环境。如果你愿意,把管理端改成 Vue + Element UI 会好看很多,但工作量至少翻一倍,毕设时间紧的话不建议。
Service 层事务设置:
java复制@Service
public class FlowerServiceImpl implements FlowerService {
@Autowired
private FlowerMapper flowerMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveFlower(Flower flower) {
if (flower.getId() == null) {
flowerMapper.insert(flower);
} else {
flowerMapper.updateByPrimaryKeySelective(flower);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void shipOrder(Integer orderId) {
// 1. 更新订单状态为待收货, 记录发货时间
// 2. 校验并扣减库存
// 3. 写库存变动日志
}
}
rollbackFor = Exception.class 这个细节很关键。Spring 默认只回滚 RuntimeException,如果你抛的是 Exception 子类(比如 ServiceException),不加 rollbackFor 就不会回滚,库存会扣成功但订单状态更新失败,这是我踩过的坑。
5.2 认证与权限控制:管理后台防护怎么做才像样
管理后台不能裸奔。我用的方案是基于 HandlerInterceptor 的登录拦截 + 基于 Session 的权限标记,轻量但足够。
拦截器实现:
java复制public class AdminLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Object admin = request.getSession().getAttribute("admin");
if (admin == null) {
// AJAX 请求返回 401, 页面请求重定向到登录页
response.sendRedirect(request.getContextPath() + "/admin/login");
return false;
}
return true;
}
}
在 SpringMVC 配置里注册拦截路径:
xml复制<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/admin/**"/>
<mvc:exclude-mapping path="/admin/login"/>
<mvc:exclude-mapping path="/admin/loginSubmit"/>
<mvc:bean class="com.flower.admin.interceptor.AdminLoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
密码存储我用的不是 MD5,而是 BCryptPasswordEncoder。MD5 撞库太容易,只要是毕设,用 Spring Security 里的 BCryptPasswordEncoder 单独加个依赖就能用,成本极低但安全性说辞完全不同。
5.3 库存扣减与数据一致性
管理端发货时扣库存,最关键的就是防止超卖。我最终的方案是乐观锁 + 库存足够性校验:
sql复制UPDATE flower
SET stock = stock - #{quantity}, sales = sales + #{quantity}
WHERE id = #{flowerId}
AND stock >= #{quantity}
这个 SQL 写法的巧妙之处在于:条件 stock >= #{quantity} 保证库存不够时不会扣成负数,UPDATE 影响行数为 0 就说明要么商品不存在要么库存不足。MyBatis 执行后,我通过返回的 int rows 判断是否成功:
java复制int rows = flowerMapper.deductStock(flowerId, quantity);
if (rows == 0) {
throw new ServiceException("库存不足,扣减失败");
}
相比先 SELECT stock 再决定是否 UPDATE 的做法,单条 UPDATE 语句天然具备原子性,不需要显式加锁,并发情况下也不会超卖。这个思路在答辩里讲出来是核心竞争力。
关于"行级权限"这个词,热搜里也提到了。后台虽然没有复杂到要做数据权限流,但我用 AOP 做了操作日志切面:记录哪个管理员在什么时候对哪些商品/订单做了什么操作。这是我们后台和普通 demo 拉开差距的一个功能。
6. 双端共用数据库时,如何保证数据不错乱
6.1 字段归属:谁写入谁负责
我前面讲两端直连同一库,这是省事的做法,但省事必须建立在严格的字段归属规则上。我在论文里专门画了一张数据维护责任矩阵:
| 数据表 | Django 写入 | SSM 写入 | 备注 |
|---|---|---|---|
| user | 注册/登录/个人信息 | 后台禁用用户 | 两边都写,但互不覆盖 |
| category | 无 | 增删改 | 用户端只读 |
| flower | 无 | 增删改/上下架 | 用户端只读 |
| cart_item | 增删改查 | 无 | 用户端会话数据 |
| order_info | 下单/支付/取消/收货 | 发货 | 用户端只改状态机前半程 |
| order_item | 下单时写入 | 无 | 只写不改 |
| review | 评论写入 | 无 | 用户端数据 |
| stock_log | 无 | 发货扣库存时写入 | 审计日志 |
这个矩阵的价值在于:我写 Django 查询时永远不碰 flower.stock,Django 页面永远不展示"后台未上架的商品";Java 端也永远不会直接去改购物车表。虽然共用一个库,但对每张表的读写权限边界在代码层面已经约束住了。
用户端在"创建订单"时虽然会校验 flower.stock 是否足够,但那只用于给用户友好提示,真正的扣减以管理端发货时那条 UPDATE 为准。这样下单时即使显示库存充足,发货时发现不够,也会抛异常并通知管理员。
6.2 避免两边生成重复订单号的矛盾
订单号我原本想直接用数据库自增 id 拼一个串,后来发现双端和时间戳都可能导致神秘问题,干脆改造一下:
java复制String orderNo = String.format("%s%010d",
new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()),
ThreadLocalRandom.current().nextInt(1000000000));
再加数据库唯一索引兜底。毕设场景这个足够用了,不会因为并发量太大担心碰撞。如果你想让订单号更有"商业感",可以加上用户 id 的后五位,再拼接时间戳和随机数。
6.3 时间字段的时区统一问题
这个坑比较隐蔽。Django 的设置里 USE_TZ = True 时,写入 MySQL 的是 UTC 时间;Java 端 JDBC 默认用的是系统时区。两个端在同一张表上写 create_time,前台的"下单时间"和后端看到的"下单时间"会相差 8 个小时。
我的解决办法是统一约定,Django 端配置:
python复制# settings.py
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
同时 Java 的数据源 URL 上显式写死时区参数:
properties复制jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
两边统一用 datetime 类型存本地时间,彻底避免时区换算的困惑。
7. 从开发到部署:环境配置与踩坑实录
7.1 环境配置:JDK、Maven、Python、MySQL 一套配齐
新手拿到项目第一步容易摔在环境配置,特别是 Windows 下 JDK 装完发现 java -version 不认,基本都是环境变量没配对。
JDK 部分,我一般是这么引导:
- 安装 JDK(1.8 或 11 都行,这个项目 1.8 最稳),安装路径不要带空格和中文,我习惯放
C:\Java\jdk1.8.0_202。 - 新建系统变量
JAVA_HOME,值指向 JDK 安装根目录。 - 编辑
Path变量,新增%JAVA_HOME%\bin。 Win+R输入cmd,执行java -version验证。
Maven 就简单多了,解压后配一个 MAVEN_HOME,Path 里加 %MAVEN_HOME%\bin,然后在 settings.xml 里把本地仓库路径改到非 C 盘,避免 C 盘被撑爆。pom.xml 里我固定了依赖版本:
xml复制<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.2.22.RELEASE</spring.version>
<mybatis.version>3.5.6</mybatis.version>
</properties>
Python 端,我强烈建议用虚拟环境:
bash复制python -m venv venv
venv\Scripts\activate # Windows
pip install django==3.2
Django 3.2 还在兼容维护期,新本版 4.x 也完全能用,但某些第三方组件对 4.x 的兼容性不一,毕设没必要冒险。
7.2 Django 查询与删除对象的几个坑
热搜词里有一条是"django执行查询-删除对象",这块确实容易出错。Django 里删除有两种写法,语义完全不同:
python复制# 方式一:QuerySet 批量删除,返回删除条数字典
deleted_count = OrderItem.objects.filter(order_id=10).delete()
# 方式二:单个模型实例删除
item = OrderItem.objects.get(id=10)
item.delete()
最大的坑在于:QuerySet 的 delete() 会级联删除外键关联的对象。比如删除一个分类 Category.objects.filter(id=3).delete(),如果分类下有多个商品且商品外键指向分类,会连带把这些商品全部删掉。这是 Django 的默认行为,不是说删除接口掉得多,而是设计上的级联策略。我的建议是:业务表里所有外键关系都用 on_delete=models.SET_NULL 或 PROTECT,而不是默认的 CASCADE,防止误删。
python复制class Flower(models.Model):
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='flowers'
)
...
这样删分类时,商品的 category_id 会置空,而不至于整条商品消失。我是在一次测试中误删了分类、连带商品全没后才意识到要审视每一对外键关系的级联行为。
7.3 Cookie 与 Token 设置的细节
在这个项目里,用户端登录状态我用的是 Django 的 Session;但我也给移动接口预留了 Token 字段,以及在管理端用 Cookie 保存登录标记来做一个"7 天免登录"的小功能。这里有两个细节很多人第一次写会翻车:
Cookie 设置 HttpOnly 和 Secure。 如果 Token 能被 JS 读取,XSS 攻击就能把 Token 偷走。我在 Django 里这样设置:
python复制response.set_cookie(
'auth_token', token,
max_age=7 * 24 * 3600,
httponly=True,
samesite='Lax',
secure=False # 本地开发 http 用 False,生产 https 改 True
)
max_age 单位是秒,这里是 7 天。httponly=True 后,document.cookie 是读不到这个 Token 的,只能在请求头里由浏览器自动携带,这能有效拦截大部分 XSS 窃取。
Token 失效逻辑。 我们用户端登录时给每个用户生成一个随机 Token,Session 结束时把 Token 对应记录删掉。Django 里我写了这样的代码:
python复制def logout_view(request):
token = request.COOKIES.get('auth_token')
if token:
AuthToken.objects.filter(token=token, user=request.user).delete()
response = redirect('index')
response.delete_cookie('auth_token')
return response
注意 delete_cookie 要指定路径和域名与当初设置时一致,否则浏览器不会真正清除 cookie。这个坑我遇到过,明明点了退出,刷新页面还是登录状态,查了很久才发现是因为当初设置 cookie 时指定了 /admin/ 路径,退出时没带路径参数导致删错地方。
7.4 Java 端经典的排序与类型比较问题
热搜里反复出现 "java排序""冒泡排序java",管理后台的商品列表我就是用 Java 排序处理了一些非数据库字段类的逻辑。比如"按折扣力度排序",数据库里没有这个字段,我更习惯在内存中对查询结果排序:
java复制list.sort(Comparator.comparingDouble(Flower::getDiscountRate).reversed());
这个比在 SQL 里 ORDER BY price/original_price 直观很多。
另外,Java 里比较 BigDecimal 金额千万不要用 ==,要用 compareTo。举个例子:
java复制// 错误写法
if (totalAmount == order.getTotalAmount()) { ... }
// 正确写法
if (totalAmount.compareTo(order.getTotalAmount()) == 0) { ... }
BigDecimal 的 equals 还会比较精度,比如 50.0 和 50.00 的 equals 是 false,但 compareTo 返回 0。这个细节在订单金额校验时能救命。
还有一个小经验,Java 里做对象深拷贝,如果你不想引入序列化框架,可以这样:
java复制FlowerVO vo = BeanUtils.copyProperties(flower, new FlowerVO());
Spring 自带的 BeanUtils.copyProperties 是浅拷贝,对基本类型字段够用。如果要深拷贝一个对象图,可以用 JSON 序列化中转:
java复制ObjectMapper mapper = new ObjectMapper();
Flower copy = mapper.readValue(mapper.writeValueAsString(flower), Flower.class);
毕设里很少用到这么复杂的场景,但如果你有很多字段需要复制,这个方法比手动 get/set 省事可靠。
8. 项目交付物整理与答辩展示(源码+LW+调试文档+讲解)
8.1 LW(论文)的章节组织
论文结构我采用的是比较标准的毕设框架,但内容上的侧重点和代码是对应的:
- 绪论:研究背景、国内外现状、论文组织结构。
- 需求分析:系统目标、用户角色分析、功能需求、非功能需求。
- 系统设计:总体架构、技术选型、功能模块设计、数据库设计。
- 系统实现:按"用户端功能 + 管理端功能"逐块展示核心代码和截图。
- 系统测试:功能测试用例表、性能测试概述。
关键技巧是每个功能点的展示不要只贴代码,要贴"界面截图 + 核心逻辑代码 + 中文注释"三件套。论文的核心是评审老师通过截图和代码看到你的工作量,我自己会把每个功能点的文字描述控制在 200 到 300 字之间,简洁一点是加分项,大段抄需求不介绍方案反而是减分项。
数据库设计部分,建议附上 ER 图和表结构字段说明表。联合主键、索引字段、外键关系都要在说明里指出来。比如我设计了 order_item 的联合唯一索引 (order_id, flower_id),说明这是为了保证同一订单不会重复出现同一商品明细。
8.2 调试文档该写什么
调试文档不是"运行步骤说明书",它包括两部分:
- 环境搭建手册:JDK/Maven/Python/MySQL 的版本清单、步骤、常见报错处理方案。
- 业务调试指南:比如"支付流程走不通时,先查 payment_status 是否从 0 变 1""发货报库存不足时,去 flower 表看 stock 字段以及 stock_log 里扣减记录"。
我整理调试文档的目的是:这个项目交付给别人后,别人可以在一小时内从零跑起来;出问题时可以按文档里的"检查点"定位。这样项目才真正是可复现、可维护的,而不是一堆堆代码的简单堆叠。
调试文档在答辩演示时也能派上用场:如果现场环境抽风,照着文档排查,会比在台上手忙脚乱强得多。
8.3 答辩与演示的关键注意点
演示顺序我一般是"用户端下单流程 → 管理后台发货流程 → 双端及时刷新查看状态变化"这样的闭环,比零散展示功能效果强很多。因为评委能直观看到"用户支付 → 后台发货 → 用户确认 → 整个订单状态机走完"的联动。
答辩的高频问题分两类:
- 技术类:"为什么用两个语言开发?""库存怎么防超卖?""密码如何存储?"这几个问题准备充分,基本不会答不好。
- 业务类:"如果用户买花是送给别人,收货人不是登录用户怎么办?"这个问题是我系统里做了收货人字段和备注字段可以直接答:"系统支持填写收货人姓名电话地址,并记录买家留言,由配送人员送达时电话联系收货人即可。"
另外,项目中最容易被评委点名的方面是"订单支付是真支付吗"。我的回答很坦白:"这是模拟支付,真实生产需要接入微信/支付宝等第三方支付,系统已预留支付状态机接口,接入时可以扩展 PayService 的具体实现。"诚实承认边界 + 让系统留有扩展空间,比回避问题要好得多。
9. 对应届生和小白的一些实操经验
讲点代码之外,但和项目一样重要的东西。
第一,项目里的每个类和方法都要加清晰注释,而且注释写"为什么做",不是写"做了什么"。比如:
java复制// 这里使用乐观锁而不是先查后改,是为了防止并发发货时超卖
int rows = flowerMapper.deductStock(flowerId, quantity);
这种注释在答辩和论文写实现细节时可以直接用,而且会显得你真的在思考工程问题。
第二,数据库脚本要留一个干净的初始化版。在根目录建 sql/flower_shop.sql,包含建表语句和演示数据(几个分类、十几款花、一两个测试用户)。实测下来,演示数据非常重要,空表上讲解所有功能都显得苍白。而且数据要有故事性,比如"香槟玫瑰礼盒"销量最高、"教师节向日葵花束"库存紧张,讲解时每个数据点都能引出业务说明。
第三,截图要按流程去截,不要只是零散的功能截图。我在论文里每个功能块都配了三到四张联动的截图:进入页面、填写信息、提交成功、后台对应状态变化。这样论文看起来就是"完整闭环的演示",而不是"各自独立的页面快照"。
第四,尽量自己部署一遍再打包交付。我这边最后的交付是:源码压缩包、带初始化数据的 SQL、LW 论文 Word 版、调试文档、演示录屏。视频我录了三分钟,包含用户端下单和管理端发货的完整操作,以防答辩现场网络不给力、项目起不来时,可以直接放视频。
最后分享一个小经验:
做这类系统项目,最大的坑从来不是某个技术不会,而是"需求没有穷尽,开发到一半才发现要改数据结构"。 我第一版做的时候,订单表里没有加 remark(贺卡留言)字段,做到用户下单那一步才发现后台无法显示用户写的贺卡内容,只能回头改表 + 重跑初始化数据。所以老话说"先建模再写码"是真没毛病,数据库建模的时间花几个小时,后面能省几天返工时间。
做完整套流程,我对"双端共用数据库"这件事最大的体会是:两端可以各有技术栈,但业务边界必须极其清晰,不能一锅粥。提前把每一张表的读写职责定好,把订单状态机和库存扣减的口子都收敛到单端,后面无论加功能还是修 bug 都会舒服很多。如果你也在憋这个系统,按上面的结构去拆,应该不会跑偏。
