网上鲜花销售系统毕业设计全解析:从技术选型到订单事务实现
2026/9/16 14:54:13 网站建设 项目流程

简介:这是一份面向计算机专业毕业设计的网上鲜花销售系统完整项目,适合正在筹备电商类课题的本专科生参考与复用。系统后端基于Django框架,前端基于Vue框架,采用前后端分离模式,实现了鲜花分类展示、商品详情(品种、颜色、价格、花语)、购物车增减、订单提交与状态跟踪、用户信息维护、商家店铺与库存管理等功能,覆盖线上鲜花交易核心环节。资源包共231个文件,压缩后约10.91MB,其中93个py文件负责后端模型与业务逻辑,22个HTML、22个JS和11个CSS用于前端页面与交互,另有PNG、JPG图片素材、PSD设计源文件、XML配置及字体文件等,可支撑从界面还原到功能调试的完整学习过程。目前已有194人浏览学习,适合用于毕业设计选题方案论证、快速搭建系统原型,或在原代码基础上扩展营销、支付、数据统计等模块。

1. 毕业设计网上鲜花销售系统:一个看似常规选题里的完整工程闭环

“网上鲜花销售系统”每年在高校毕设选题库里出现的频率极高,但它远不是平铺直叙的增删改查。花束的多规格定价、配送时效对订单状态的约束、库存扣减与取消回补,这些细节决定了这个系统是“跑通了”还是“做完整了”。这篇内容写给正在做或准备做该题目的同学,以及需要辅导该选题的老师,按“选型—建模—交易链路—答辩验收”的顺序,讲清楚每一处取舍和常见坑点,让老题目做出完成度。

2. 选型先于编码:网上鲜花销售系统的技术栈与目录结构设计

2.1 SSM 三件套为什么是毕业设计里最稳妥的组合

鲜花销售系统的数据规模不大,但实体关系不少:用户、分类、商品、订单、明细、地址、评论,再算上购物车,七八张表互相联动。技术选型的核心原则是“老师看得懂、自己控得住、出问题能定位”,基于这三点,SSM(Spring + Spring MVC + MyBatis)配 JSP 和 MySQL 8.0,是多数情况下最稳的组合。

Spring Boot 确实省事,但毕设答辩的规则决定了配置过程也是评分点。手写 DataSource、配置 web.xml、理解 DispatcherServlet 如何接管请求,这些被自动配置吃掉的内容,在展示时反而能讲出东西来。MyBatis 相比 JPA 的优势是 SQL 完全可控,热销排行、分类统计这类报表 SQL,在 XML 里写成什么样就执行什么样,不会出现 ORM 自动生成的 SQL 和预期不符的情况。

技术项推荐方案备选方案备选方案的代价
后端框架SSM 手动配置Spring Boot自动配置让答辩少了很多可讲的点
ORM原生 MyBatisMyBatis-Plus自动填充和逻辑删除的特性容易被追问
数据库MySQL 8.0MySQL 5.75.7 对 JSON 和窗口函数支持较弱
视图层JSP + JSTLVue + ElementUI前后端分离需额外处理跨域和 Token
容器Tomcat 9JettyTomcat 排错资料最多,演示环境更成熟

需要提醒的是,不要为了所谓“技术亮点”引入微服务体系。对本科毕设来说,分布式事务、消息队列这些组件被质疑的风险远大于加分收益,“Spring Cloud 网关 + 多个微服务”一旦部署在单机上,反而让论文架构图和实际部署不一致。一套 SSM 单体应用,代码量 4000 到 8000 行,工作量足够支撑一篇完整的毕业论文。

2.2 目录结构:按功能分包,不要按层分包

很多同学照搬网上的 controller、service、dao、entity 四层结构。这种按层分包适合多人协作的大型工程,但毕设是单人开发,答辩老师看代码时关心的是“下单这条流程涉及哪些类”。按功能分包能明显降低阅读成本:

src/main/java/com/graduate/flowers/ ├── common/ # 统一返回体、全局异常、常量类 │ ├── Result.java │ └── GlobalExceptionHandler.java ├── user/ # 用户、地址、登录拦截 │ ├── UserController.java │ └── UserService.java ├── product/ # 商品、分类、评论 ├── cart/ # 购物车 ├── order/ # 订单、明细、定时任务 └── admin/ # 后台管理

每个功能包内部自带 Controller、Service、Mapper 三层,模块间通过 Service 接口交互,不直接调用其他包的 Mapper。MyBatis 的 XML 文件统一放resources/mapper/下,与 Java 接口同名同包,扫描时一条路径全部命中。这样做的好处是业务闭环在一个包内可读,改购物车逻辑不会碰到订单代码。

2.3 统一返回体与全局异常:两个类换来的完成度提升

既然选了分层结构,接口层的返回值格式就必须统一。定义一个泛型 Result 类作为所有 AJAX 接口的返回载体,再配全局异常处理器,接口的返回结构就全一致了。

// common/Result.java public class Result<T> { private int code; // 200 成功,400 业务失败,500 系统异常 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(int code, String msg) { Result<T> r = new Result<>(); r.code = code; r.msg = msg; return r; } }

配合@RestControllerAdvice捕获 BusinessException,业务代码里不需要到处 try-catch 返回信息。Service 层抛业务异常时带错误码,Controller 只管调 Service 然后返回 Result.success;未知异常由全局处理器兜底,日志记堆栈,前端收到的是友好的“系统繁忙”。打开浏览器开发者工具,Network 面板里所有接口返回体结构一致,演示观感比散装 Map 返回好很多。

3. 数据库建模:网上鲜花销售系统八张核心表的字段与关系设计

数据库设计是答辩提问的重灾区,ER 图直接印在论文里,老师一定会挑字段问细节。鲜花销售系统的核心矛盾在于订单要保存下单那一刻的商品快照,同时商品表里的价格和库存会随时变化。下面这套表结构是比较普遍的做法,可以直接套用。

3.1 核心表清单与字段规划

表名用途关键字段设计要点
user用户表id, username, password, rolerole 区分普通用户与管理员
category分类表id, name, parent_idparent_id 为 0 表示一级分类
product商品表price, stock, sales, status价格用 DECIMAL,status 控制上下架
address收货地址表user_id, detail, is_default一个用户多个地址,仅一个默认
cart购物车表user_id, product_id, quantity登录用户持久化购物车
orders订单表order_no, status, total_priceorder_no 全局唯一
order_item订单明细表order_id, product_name, price快照内容,不随商品表变化
comment评论表product_id, rating, contentrating 取 1~5

表名全小写、用单数,一是避免 order 和 SQL 关键字冲突写成 orders,二是论文 ER 图里表名和代码保持一致,不会出现“多一个 s”的尴尬。所有表引擎统一 InnoDB,字符集用 utf8mb4,鲜花商品名称里可能出现特殊符号,utf8mb4 支持更全。

3.2 订单主表与明细表的拆分逻辑

下单时同一个订单包含多件商品,如果只建一张订单表,商品字段会出现重复组,违背第一范式。拆成主表和明细表后,orders 管订单级数据,order_item 管每件商品的数据,两者通过 order_id 关联。

CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(40) NOT NULL UNIQUE COMMENT '订单编号', user_id BIGINT NOT NULL COMMENT '下单用户ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', total_price DECIMAL(10,2) NOT NULL COMMENT '订单总金额', receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(255) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '所属订单ID', product_id BIGINT NOT NULL COMMENT '商品ID,仅作追溯', product_name VARCHAR(100) NOT NULL COMMENT '商品名称快照', price DECIMAL(10,2) NOT NULL COMMENT '成交单价快照', quantity INT NOT NULL, INDEX idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单号用“yyyyMMddHHmmss + 用户ID”拼接,不会像自增 ID 那样暴露订单量。status 用 TINYINT 数字枚举,查询和统计效率高于字符串。三个收货人字段必须冗余到订单表,因为地址表后续可能修改,历史订单的配送信息不能跟着变。

order_item 里不建立指向 product 的外键,原因是明细记录本质是快照,商品删除后订单历史还要保留。外键策略应该是:订单相关表只做逻辑关联,应用层保证引用一致性;分类和商品的归属关系可以加外键,体现设计的完整性。这样划分既满足数据一致性要求,又避免级联操作破坏历史数据。

3.3 购物车表的定位:登录用户才落库

购物车可以做在 Session 里,也可以落库。游客购物车用 Session 存 Map,登录后用一个 Merge 逻辑合并到数据库 cart 表。cart 表不存商品名称和价格,只存 user_id、product_id、quantity,展示时 JOIN product 表取最新商品信息。

这种设计的取舍在论文里能写清楚:优点是不会出现购物车价格和商品页价格不一致的问题;缺点是对商品改价敏感,如果下单前商品价格变化,购物车结算页显示的可能是更新后的价格。如果要按“加购时价格为准”,就在 cart 表增加 price 字段。毕设做成“展示时取现行价格”最简单,答辩时把这个取舍讲明白,比藏着不问更稳妥。

4. 核心交易链路落地:从分页列表到购物车再到订单事务

4.1 商品列表分页:PageHelper 的三行写法与使用边界

商品列表是鲜花销售系统的主页面,分页用 PageHelper 时有个高频错误:startPage 和真正的 Mapper 查询之间插入了其他 SQL。PageHelper 拦截的是“紧随其后的下一条查询语句”,所以 startPage 必须紧贴目标查询,中间不能有别的数据库操作。

@RequestMapping("/product/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "12") Integer pageSize, @RequestParam(required = false) Long categoryId, Model model) { PageHelper.startPage(pageNum, pageSize); // 下面这行必须是需要分页的那个查询 List<Product> products = productService.queryByCategory(categoryId); PageInfo<Product> pageInfo = new PageInfo<>(products); model.addAttribute("pageInfo", pageInfo); model.addAttribute("categoryId", categoryId); return "product/list"; }

PageHelper 使用 ThreadLocal 在调用链上传递分页参数,用完自动清理,不用手动 reset。PageInfo 中封装了 total、pages、pageNum、list 等属性,JSP 分页条直接迭代替换即可。每页数量设置成 12,鲜花图片是竖图,一行 4 朵的栅格布局在 1366 分辨率下观感最自然,这也是演示时容易被忽略的细节。

4.2 购物车加购:并发下数量只增不减

登录用户的购物车使用 cart 表持久化。加购时有两条路:先 select 再 update,或直接执行自增更新。前者在并发下会出现覆盖丢数量的问题,后者用一条 UPDATE 把 quantity 做原子累加。

@PostMapping("/cart/add") @ResponseBody public Result<String> addToCart(@RequestParam Long productId, @RequestParam(defaultValue = "1") Integer quantity, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return Result.error(400, "请先登录"); } CartItem existing = cartMapper.selectByUserAndProduct(loginUser.getId(), productId); if (existing != null) { // 增量更新,避免并发覆盖 cartMapper.increaseQuantity(existing.getId(), quantity); } else { CartItem item = new CartItem(); item.setUserId(loginUser.getId()); item.setProductId(productId); item.setQuantity(quantity); cartMapper.insert(item); } return Result.success("加入购物车成功"); }

increaseQuantity 对应的 SQL 是UPDATE cart SET quantity = quantity + #{delta} WHERE id = #{id},没有先查后写的窗口,前后两次加购不会互相覆盖。安全方面,加购接口可以对 quantity 做上限校验,单件商品加购数量不超过 99,既能挡掉明显异常请求,也能在论文“系统安全”里写上一笔。

4.3 下单事务:库存扣减、订单生成、购物车清空必须同生共死

创建订单包含三步:扣减库存、插入主表和明细、清空购物车。其中任意一步失败都不能留下半截数据,这三步必须放进同一个事务。Spring 的声明式事务默认只在运行时异常时回滚,所以 rollbackFor 要显式写成 Exception.class。

@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, Long addressId) { List<CartItem> items = cartMapper.selectWithProduct(userId); if (items == null || items.isEmpty()) { throw new BusinessException("购物车为空"); } // 先扣库存,条件更新返回 0 表示失败 for (CartItem item : items) { int rows = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BusinessException("商品[" + item.getProductName() + "]库存不足"); } } // 生成主订单 Order order = new Order(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setStatus(OrderStatus.UNPAID); order.setTotalPrice(calcTotal(items)); order.setReceiver(...); orderMapper.insert(order); // 生成明细 for (CartItem item : items) { orderItemMapper.insert(convert(item, order.getId())); } // 清空购物车 cartMapper.deleteByUserId(userId); return order; }

deductStock 的 SQL 是这个事务的核心:

UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

条件更新直接复用行锁,把“查库存”和“扣库存”合并成一步。受影响行数为 1 表示扣减成功,为 0 表示库存不足或商品已下架,Service 层读返回值即可。不需要先 SELECT 再 if 判断的写法,那会增加一次查询和锁持有时间。

@Transactional只在外部调用时生效。如果同一个类里的 A 方法调用了标记事务的 B 方法,B 上的注解不会生效,因为调用发生在对象内部,没有经过 Spring 代理。下单方法应放在独立 Service 中被 Controller 或定时任务调用,而不是在类内自调用。

4.4 模拟支付与未支付订单的自动取消

真实支付需要对接第三方,毕业设计通常做模拟支付:待支付订单点击“立即支付”,后台只校验订单归属人,再把 status 从 0 改成 1。校验逻辑是必须的,防止把别人订单给支付了。pay_time 一起更新,后面统计报表能用上。

未支付订单的自动取消用 Spring Task 的 @Scheduled 注解:

@Scheduled(fixedDelay = 60000) @Transactional public void cancelExpiredOrders() { // 状态为 0 且创建时间早于当前时间 30 分钟 List<Order> expired = orderMapper.selectExpiredUnpaid(30); for (Order order : expired) { // 带条件更新状态,防止重复取消 int rows = orderMapper.cancelOrder(order.getId()); if (rows == 1) { for (OrderItem item : orderItemMapper.selectByOrderId(order.getId())) { productMapper.restock(item.getProductId(), item.getQuantity()); } } } }

cancelOrder 的 SQL 是UPDATE orders SET status = 4 WHERE id = #{id} AND status = 0,先通过受影响行数判断是否取消成功,成功才回补库存。fixedDelay 表示上次任务结束后间隔 60 秒再执行,和 fixedRate 从任务开始计时不同,对扫描型任务,fixedDelay 能避免任务重叠。这里的“取消订单回补库存”与下单时“扣减库存”是对应操作,一进一出,库存数据才闭环。

5. 答辩前的自测清单与数据预置的演示技巧

功能和代码都完成后,先按下面的清单过一遍,能省下答辩现场乱点的风险。

自测场景操作预期结果
注册注册一个不存在的用户名成功跳转登录页
重复注册再注册相同用户名提示“用户名已存在”
加购对同一商品加购两次数量累加不覆盖
并发加购两个窗口同时加购最终数量为两次之和
下单库存足够时下单订单生成,购物车清空
库存不足把库存改成 1 再下两单第二单提示库存不足
支付对他人订单执行支付提示无权操作
取消订单把订单创建时间改为 40 分钟前定时任务置为取消,库存回补

排查时如果接口返回 500,先看日志里有没有数据截断异常,再确认表和字段的字符集是否都是 utf8mb4。连接 MySQL 8.0 时,驱动类名是com.mysql.cj.jdbc.Driver,jdbcUrl 里要带上serverTimezone=Asia/Shanghai&useSSL=false,否则启动阶段就会报时区错误。

答辩演示遵循一个原则:能预置的数据不要现场生成。演示发货时,提前把某个订单改为 status=1,登录后台刷新页面直接点“发货”;演示支付时,准备一个 status=0 的订单,现场支付;演示库存不足时,把某件热销商品的库存改为 1,现场下两单,第二单正好弹出错误提示。这些状态数据的跳转会让整个流程显得紧凑完整。

如果部署到云服务器,数据库连接串不要暴露公网端口,JSP 项目通常直接跑在 Tomcat 的 8080 端口,安全组只放行 8080 就行。演示前检查服务器时间是否和数据库服务器一致,定时取消订单依赖的是数据库当前时间,两边差太多会导致订单被误取消。这几个点处理完,答辩演示基本能在一个自然流畅的节奏里把核心链路全部触发一遍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询