简介这是一份面向计算机专业毕业设计的网上鲜花销售系统完整项目适合正在筹备电商类课题的本专科生参考与复用。系统后端基于Django框架前端基于Vue框架采用前后端分离模式实现了鲜花分类展示、商品详情品种、颜色、价格、花语、购物车增减、订单提交与状态跟踪、用户信息维护、商家店铺与库存管理等功能覆盖线上鲜花交易核心环节。资源包共231个文件压缩后约10.91MB其中93个py文件负责后端模型与业务逻辑22个HTML、22个JS和11个CSS用于前端页面与交互另有PNG、JPG图片素材、PSD设计源文件、XML配置及字体文件等可支撑从界面还原到功能调试的完整学习过程。目前已有194人浏览学习适合用于毕业设计选题方案论证、快速搭建系统原型或在原代码基础上扩展营销、支付、数据统计等模块。1. 毕业设计网上鲜花销售系统一个看似常规选题里的完整工程闭环“网上鲜花销售系统”每年在高校毕设选题库里出现的频率极高但它远不是平铺直叙的增删改查。花束的多规格定价、配送时效对订单状态的约束、库存扣减与取消回补这些细节决定了这个系统是“跑通了”还是“做完整了”。这篇内容写给正在做或准备做该题目的同学以及需要辅导该选题的老师按“选型—建模—交易链路—答辩验收”的顺序讲清楚每一处取舍和常见坑点让老题目做出完成度。2. 选型先于编码网上鲜花销售系统的技术栈与目录结构设计2.1 SSM 三件套为什么是毕业设计里最稳妥的组合鲜花销售系统的数据规模不大但实体关系不少用户、分类、商品、订单、明细、地址、评论再算上购物车七八张表互相联动。技术选型的核心原则是“老师看得懂、自己控得住、出问题能定位”基于这三点SSMSpring 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 ResultT { private int code; // 200 成功400 业务失败500 系统异常 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT 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价格用 DECIMALstatus 控制上下架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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单号用“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); // 下面这行必须是需要分页的那个查询 ListProduct products productService.queryByCategory(categoryId); PageInfoProduct 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 ResultString 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) { ListCartItem 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 分钟 ListOrder 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.DriverjdbcUrl 里要带上serverTimezoneAsia/ShanghaiuseSSLfalse否则启动阶段就会报时区错误。答辩演示遵循一个原则能预置的数据不要现场生成。演示发货时提前把某个订单改为 status1登录后台刷新页面直接点“发货”演示支付时准备一个 status0 的订单现场支付演示库存不足时把某件热销商品的库存改为 1现场下两单第二单正好弹出错误提示。这些状态数据的跳转会让整个流程显得紧凑完整。如果部署到云服务器数据库连接串不要暴露公网端口JSP 项目通常直接跑在 Tomcat 的 8080 端口安全组只放行 8080 就行。演示前检查服务器时间是否和数据库服务器一致定时取消订单依赖的是数据库当前时间两边差太多会导致订单被误取消。这几个点处理完答辩演示基本能在一个自然流畅的节奏里把核心链路全部触发一遍。本文还有配套的精品资源点击获取