最近一段时间,陆陆续续有不少同学来聊毕业设计选题,JSP在线鲜花订购系统这个名字出现的频率相当高。它听起来是一个很常见的后台管理系统,实际上正好把Java Web里最核心的几个点全串了一遍:JSP页面、Servlet控制层、JDBC/MySQL数据库、Session购物车、订单状态处理,甚至还能顺带把过滤器、分页、统计报表这些加分项加进去。做这个题目的人,十有八九不是为了花里胡哨的界面,而是想用一个完整项目把整个Java Web知识体系过一遍。
这篇文章就围绕这个题目,把我自己开发时的完整思路、技术选型、数据库设计、核心代码逻辑、部署细节和踩坑记录写出来。适合正在做毕业设计、课程设计,或者想快速上手一个Java Web项目的人参考。我不打算讲一堆教科书上的定义,而是按实际操作顺序,告诉你从新建工程到跑通下单整个流程应该怎么做。
1. 动工之前,先搞清它的核心需求和设计逻辑
1.1 这个系统到底在做什么
说白了,这是一个面向鲜花销售的B/S结构Web应用。用户打开浏览器注册登录,逛分类商品、看详情、加入购物车、提交订单,管理员登录后台管理商品、分类、库存、订单。把它拆开看,就是一个标准的信息管理系统外加一个微型电商模块。
这类题目的价值在于“麻雀虽小五脏俱全”。它既有面向用户的展示和交互,又有面向管理员的增删改查,还有数据库设计、状态流转、会话管理等容易被忽视但很重要的环节。做完之后你会发现,之前零散学过的JSP、Servlet、JavaBean、EL表达式、JSTL标签库,全部都有了实际落点。
如果你正在选毕业设计题目,看到“在线鲜花订购系统”这几个字先别觉得简单。真正要做得完整,至少要覆盖用户模块、商品模块、购物车模块、订单模块、后台管理模块这五大块。很多同学做到一半发现前后端数据对不上、Session取不到值、订单金额算错,基本都是因为一开始没把需求和模块边界理清楚。
1.2 技术栈为什么选JSP,而不是Spring Boot
这个问题几乎每次都会被问到。明明Spring Boot现在已经很普及,为什么毕业设计还要做JSP?我的真实想法是:如果目标是快速完成并通过答辩,而且你的基础还停留在教材阶段,那JSP+Servlet+MySQL这套组合是最稳妥的。
原因很简单:第一,JSP在线鲜花订购系统是经典的Java Web课程设计题目,网上资料多,遇到问题搜得到答案;第二,JSP页面可以直接嵌Java代码和EL表达式,对刚接触Web开发的人来说,数据是怎么从数据库跑到页面上来的,路径非常清晰;第三,Spring Boot虽然好,但涉及自动化配置、依赖管理、注解驱动这些概念,如果之前没用过,调试起来会特别痛苦。
当然,这不是说JSP有多先进。相反,JSP在现在的企业开发里已经不常用了。但毕业设计的核心评价标准是“完整、可运行、有逻辑”,而不是“用了多新的框架”。你把这套技术栈吃透,再去学Spring Boot,会发现自己理解的底层原理更扎实。
技术栈通常是这样:
- JDK 8/11,Tomcat 8.5或9,MySQL 5.7/8.0
- JSP + Servlet + JavaBean + JDBC
- 前端用JSP内置的EL表达式和JSTL,配合Bootstrap做基础样式
- 开发工具用IDEA或Eclipse,数据库管理用Navicat或MySQL命令行
这套组合不需要额外依赖太多jar包,也不用配置复杂的构建工具,适合在短时间完成。
1.3 功能模块划分,照着画图就行
动手写代码前,我建议先把功能模块拆分清楚。一般这样分:
- 用户端:注册、登录、修改个人信息、浏览商品、按分类筛选、关键词搜索、商品详情、加入购物车、查看购物车、修改数量、删除商品、提交订单、查看我的订单。
- 管理员端:管理员登录、商品管理(增删改查、上下架)、分类管理、订单管理(查看、发货、完成、取消)、用户管理。
- 公共模块:验证码、统一登录拦截、分页组件、图片上传。
这个划分看起来常规,但它是后面写数据库表和Servlet路由的依据。比如“购物车”听起来像一张表,实际上更合理的做法是购物车内容存前端Session或独立购物车表,要根据系统复杂度决定。
我的建议是:先画出页面流转图,从“首页”到“商品列表页”到“详情页”到“购物车”到“结算页”到“订单列表”,每一步对应哪些Servlet和JSP页面,写清楚之后,代码量再大也不会乱。
2. 关键技术点拆解:从登录到下单的每一环
2.1 用户登录注册与Session会话管理
登录功能看起来很基础,但JSP系统里最容易出错的就是Session的存取和用户状态判断。我的做法是这样:用户登录成功后,把用户对象放进Session,命名为“user”,然后在所有需要登录才能访问的页面里,通过判断Session里有没有这个键来决定是否跳转到登录页。
这里推荐用Filter统一拦截,而不是在每个JSP页面里重复写判断代码。一个典型的登录Servlet逻辑如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("user", user); response.sendRedirect("index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }关键点有三个:密码不能明文存,至少用MD5加盐或者SHA-256处理;注册时要先检查用户名是否重复;登录失败用forward返回登录页并在页面上显示错误信息,不要用sendRedirect,否则错误信息会丢失。
如果做到第二个版本,还可以加“记住我”的功能,也就是把用户ID存到Cookie并设置有效期。但这个属于加分项,优先把Session流程跑通最重要。
2.2 商品展示、分类筛选与关键词搜索
商品展示的核心不是页面有多花哨,而是数据查询接口设计得好不好。首页展示热销鲜花,通常从商品表里按销量排序取前几个。分类页根据分类ID查询商品列表,再配合分页。
分页是必然要做的,因为鲜花商品几十条不算多,但你不能不做分页,否则数据量和页面美观度都会受影响。我这里用的是最简单的物理分页:
SELECT * FROM flower WHERE category_id = ? ORDER BY create_time DESC LIMIT ?, ?;参数一是起始位置,参数二是每页条数。在Servlet或者DAO层里计算:
int pageSize = 8; int currentPage = request.getParameter("page") == null ? 1 : Integer.parseInt(request.getParameter("page")); int start = (currentPage - 1) * pageSize;搜索结果页也要做分页,搜索关键词用LIKE模糊匹配。要注意防止SQL注入,优先用PreparedStatement,不要直接用字符串拼接。
“分类下拉菜单”这种看似简单的东西,其实要查两次数据库:一次查询当前分类的商品列表,一次查询所有分类用于生成菜单。不要试图用一个查询搞定,代码会绕得很别扭。
2.3 购物车的数据结构设计
购物车是很多人的难点,因为它的数据来源可以有两个方案:存在数据库的购物车表,或者存在Session里的购物车对象。两种各有优缺点。
如果做的是课设或毕业设计,我强烈推荐用Session存购物车,因为实现简单,用户未登录也可以加购,结算时再判断登录。但有一个前提:购物车里会有多种商品,每种商品有数量,所以需要定义一个购物车条目的JavaBean:
public class CartItem { private Flower flower; private int quantity; // getter/setter省略 }购物车本体可以是一个Map,也可以是一个List。我推荐用Map的key存商品ID,value存CartItem对象。加购时先判断Map里有没有这个商品ID,有就数量加一,没有就新建条目。删除和修改数量也比较方便。
下单前一定要做一次库存校验。很多人忽略这一步,导致用户提交了两个订单,库存超卖,管理员后台看着一脸懵。校验逻辑是这样的:遍历购物车条目,对比数据库中当前库存和购买数量,如果库存不足,就把库存不足的商品信息返回页面,阻止下单。这套逻辑写起来不复杂,但会让你的项目显得很完整。
2.4 订单生成与库存扣减的先后问题
订单模块是整个系统的核心流程节点。用户点击提交订单后,需要做的事按顺序来:
- 从当前Session获取用户ID。
- 从Session获取购物车条目。
- 计算订单总金额。
- 插入订单主表,拿到订单ID。
- 遍历购物车条目,插入订单明细表,同时更新商品库存。
- 清空购物车。
- 跳转到订单成功页。
这里最怕出现“订单生成了,但库存没有更新”的问题。我建议把“更新库存”放到插入订单明细的循环里一起做,而不是等所有明细插完再单独更新。另外,要处理事务,也就是JSP+Servlet阶段最容易忽视的Connection事务管理。做法是在DAO层使用同一个Connection,先关闭自动提交,全部执行成功后再commit,任何一步失败就rollback。
很多同学喜欢用JDBC的每个DAO方法都自己开连接,结果订单主表和订单明细表分属两个连接,事务根本没法保证。我的做法是在Service层或Servlet中统一获取Connection,传到DAO方法里去执行,这样能保证订单数据的一致性。虽然代码看起来会多一点,但这就是答辩时的加分点。
3. 数据库设计和前后端联动,这步决定你写代码是否痛苦
3.1 数据表怎么建才够用
数据库设计的好坏,直接决定后面代码量。JSP在线鲜花订购系统不需要特别庞大的表,但基本表必须齐全。我推荐下面这几张表:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, nickname, phone, address, create_time | 用户表,密码建议存加密后的密文 |
| category | id, name, sort_order | 鲜花分类表 |
| flower | id, category_id, name, price, original_price, image, stock, sales, description, status | 商品表,status控制上下架 |
| cart | id, user_id, flower_id, quantity, create_time | 可选购物车表,如果用Session可以不要 |
| orders | id, order_no, user_id, total_price, status, create_time | 订单主表,status表示待付款/已付款/已发货/已完成/已取消 |
| order_item | id, order_id, flower_id, flower_name, price, quantity, subtotal | 订单明细表,冗余商品名称和价格,防止商品价格变动影响历史订单 |
我特别想强调order_item表的设计。很多同学偷懒,只存商品ID,觉得订单详情查询时再去联表查flower表就行。但这样一旦商品被删除,历史订单就查不到商品名了。订单明细里冗余一份“商品名称”和“下单时的价格”,看起来重复,却是消息记录设计里的实际经验。
订单状态建议用数字表示,但要在代码里定义常量,不要在页面里直接写死数字。比如0待付款、1已付款、2已发货、3已完成、4已取消。页面显示时用JSTL或者自定义函数把数字映射成中文,这样维护起来轻松很多。
3.2 JDBC连接与DAO层封装
数据库连接这块,最笨但也最可控的方式是自己写一个DBUtils类,读取配置文件里的数据库地址、用户名、密码,用DriverManager获取连接。不要每个DAO类里都硬编码一遍数据库连接参数,否则后期换数据库或改密码要改几十处。
我的DBUtils类简单长这样:
public class DBUtils { private static final String URL = "jdbc:mysql://localhost:3306/flower_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }如果是IDEA和Maven项目,也可以使用连接池例如c3p0或Druid,不过毕业设计里用上面这种方式更直接,也不会因为连接池配置出错耽误时间。但不管用哪种方式,连接用完必须关闭,而且关闭顺序是ResultSet、Statement、Connection,这样才能避免线上环境出现连接泄漏。
DAO层建议每个表对应一个DAO类,比如UserDao、FlowerDao、OrderDao。DAO里只放SQL操作,不放业务逻辑。订单事务这种需要跨多张表的操作,可以放在OrderService类里统一控制。
3.3 JSP、Servlet和JavaBean之间怎么配合
这是整个JavaWeb项目的灵魂。我用一句话总结:JSP负责展示,Servlet负责接收请求和调用业务层,JavaBean负责封装数据。
流程是这样的:浏览器请求xxxServlet,Servlet通过request.getParameter拿到参数,调用DAO查询数据库,把查询结果封装到JavaBean对象或List中,再通过request.setAttribute放进Request域,最后forward到某个JSP页面。JSP页面用EL表达式和JSTL直接取出数据并渲染。
注意,Servlet里尽量不要写太多业务代码,更不要在Servlet里拼SQL。如果你发现某个Servlet超过两百行,说明该把业务逻辑拆到Service类里了。页面中也不要写小脚本代码,更不要用<% %>去循环遍历列表,全部改用JSTL的c:forEach标签,页面会干净很多。
举一个简化但正确的跳转方式:
List<Flower> hotFlowers = flowerDao.findHotFlowers(); request.setAttribute("hotFlowers", hotFlowers); request.getRequestDispatcher("index.jsp").forward(request, response);页面里这样渲染:
<c:forEach items="${hotFlowers}" var="flower"> <div>${flower.name} ${flower.price}元</div> </c:forEach>这种模式下,前端开发和后端的边界非常清晰。哪怕你是一个人开发,也会发现排查问题容易很多。页面报错时不会怀疑是Java逻辑错误,而是直接去Servlet入口看数据链路。
4. 实操记录:搭环境、写登录、做页面、跑通下单
4.1 开发环境选择和项目目录
我用的是IDEA Community版加Tomcat 9,数据库MySQL 8。建项目的时候直接创建Java Enterprise项目,选择Web Application模板,添加JSP和Servlet依赖。如果你用的是Eclipse,注意把Deployment Assembly里的内容配好,否则Tomcat启动后访问不到页面。
项目目录结构一般这样:
src ├── dao │ ├── UserDao.java │ ├── FlowerDao.java │ └── OrderDao.java ├── entity │ ├── User.java │ ├── Flower.java │ ├── CartItem.java │ └── Order.java ├── service │ └── OrderService.java ├── servlet │ ├── LoginServlet.java │ ├── RegisterServlet.java │ ├── CartServlet.java │ └── OrderServlet.java ├── util │ └── DBUtils.java web ├── index.jsp ├── login.jsp ├── register.jsp ├── flowerList.jsp ├── flowerDetail.jsp ├── cart.jsp ├── orderConfirm.jsp ├── myOrders.jsp └── admin ├── adminLogin.jsp ├── flowerManage.jsp └── orderManage.jsp这个结构最大的好处是“看到包名就知道代码放哪里”。答辩时老师问起项目架构,你也能清晰表达java类分层的思路。很多同学把所有Servlet和DAO堆在一起,后期维护真的想哭。
4.2 登录注册功能实现过程
登录和注册其实是一套组合,注册的实现比登录多一点检查步骤。注册页提交用户名、密码、确认密码、手机号等信息到RegisterServlet。在Servlet里先判断用户名是否已存在,如果存在就返回注册页并提示“用户名已占用”,如果不存在就调用DAO插入用户。
这里有一个细节:确认密码不能只在前端校验,后端必须再校验一次。因为用户可以直接构造请求跳过页面检查,如果后端不校验,密码都可能不一致的数据都会被写进数据库。
密码存储我建议用MD5加固定盐值,或者直接用Java自带的MessageDigest生成MD5字符串。不推荐在毕业设计里上Spring Security,因为那会给项目增加太多额外复杂度。安全这东西做到“密码不明文存储、SQL用预处理语句”已经能让答辩过关。
用户登录成功后,记得把用户ID存到Session里,不要每次需要用户信息的时候都去查数据库。在JSP页面里,通过${sessionScope.user.username}就能直接显示当前登录用户的用户名。如果显示不出来,先检查登录时Session里保存的键名和页面取的是不是一致,这是最简单的排查思路。
4.3 商品列表和详情页的实现
商品列表页需要接收参数:分类ID、当前页码、搜索关键词。我写了一个FlowerServlet来处理所有商品查询相关的请求,通过action参数区分是查询全部分类、按分类查询还是搜索。
按分类查询的URL可以这样设计:http://localhost:8080/flower_shop/FlowerServlet?action=list&categoryId=1。这样既方便页面链接跳转,也方便在Servlet中做逻辑判断。在FlowerServlet中,根据action参数走不同的查询方法,把查询结果放到request里,然后转发到flowerList.jsp。
详情页比较简单,根据flowerId查询商品对象,然后展示图片、名称、价格、描述和库存。页面上会有一个“加入购物车”按钮,点的时候把flowerId通过GET请求传给CartServlet,同时带上数量。
我做这个功能时踩过的一个坑是:从详情页加购时,由于随手加上一个“返回上一页”的链接,结果用户刷新页面会重复加购。后来我把加购请求改为POST,并且加购成功后直接重定向到购物车页面,避免用户刷新提交重复数据。这个经验在答辩时特别值得提,因为展示了你对重复提交的理解。
4.4 购物车与订单功能串跑
购物车页面从Session中取出CartItem列表,用JSTL遍历显示每件商品名称、单价、数量和小计。页面底部自动计算总金额。计算总金额可以在Servlet里用Java算,也可以在前端用JavaScript算,但最终提交订单时,金额必须以服务端计算为准,不能信任前端传来的金额。
用户点击“去结算”后,跳转到订单确认页。这个页面展示收货人、手机号、地址和订单商品清单。确认页面需要从Session里读取购物车数据并再次显示,同时提供修改地址的输入框。结算Servlet要做三件事:读取Session里的用户和购物车、调用OrderService创建订单、清空Session购物车。
创建订单的第一步是生成订单号。不建议用数据库自增ID当订单号,因为太容易猜测。我用的方案是时间戳加用户ID再加随机数,拼接成一个20位以内的订单号。虽然不能和真实电商的订单号比,但比单纯自增ID看起来专业很多。
订单创建完成后,用sendRedirect跳转到订单成功页,并带上订单号参数。不要在创建订单后直接forward到成功页,否则用户按F5刷新,表单会再次提交,极可能生成两笔相同订单。这个坑我见过太多次了。
5. 踩坑实录、部署细节和答辩准备
5.1 我开发时遇到的高频坑
第一个坑是MySQL 8的时区问题。用MySQL 8连接数据库时,如果不加serverTimezone=Asia/Shanghai,Tomcat启动后执行查询会直接报错。这个问题非常经典,运行环境一换就出现,解决办法就是在数据库连接URL里显式指定时区。
第二个坑是JSP页面取不到Session里面的值。常见原因有两个:一是Session里保存的键名写错了,二是登录成功后用了sendRedirect跳转,而重定向会发起新的请求,如果之前的Session没有被正确跨请求保留,也会取不到。虽然Server端默认保持Session,但有时候你手动创建了一个新的Session对象,又没把属性放进去,就容易造成混乱。
第三个坑是编码问题。JSP页面明明设置了UTF-8,但数据库里中文还是乱码。这通常是三个环节没统一编码:页面编码、请求编码和数据库连接编码。我一般会在JSP文件头部设置contentType为UTF-8,再在Servlet里加一行:
request.setCharacterEncoding("UTF-8");数据库连接URL里也带上characterEncoding=utf8,这样中文乱码问题基本能绝迹。
5.2 常见问题排查速查表
我整理了一张速查表,基本上每个做这个题目的人都会遇到:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| JSP页面显示源码或404 | 项目没正确部署到Tomcat | 检查Deployment配置,重启Tomcat |
| 数据库中文乱码 | 页面/请求/连接编码不一致 | 统一UTF-8,设置characterEncoding |
| 连接数据库报Public Key Retrieval异常 | MySQL 8认证方式问题 | 连接URL加allowPublicKeyRetrieval=true |
| 登录后页面刷新又变成未登录 | Session生命周期短或取错键名 | 检查Session设置和JSP表达式 |
| 提交订单生成两笔订单 | 页面刷新重复提交 | 下单用POST并重定向到新页面 |
| 图片不显示 | 图片路径写错或未上传成功 | 检查相对路径和物理路径 |
| 更新库存后销量不变 | 忘记在订单循环里更新sales | 遍历订单明细时同步更新flower表 |
| 管理员页面普通用户也能访问 | 缺少权限拦截 | 用Filter拦截/admin路径 |
5.3 部署到Tomcat时最容易忽略的点
本地IDEA里跑得通,不代表换一台电脑也能跑起来。很多同学在答辩现场才翻车,原因就是部署没做对。最稳妥的方式是导出WAR包,把WAR包放到Tomcat的webapps目录下,启动Tomcat后访问http://localhost:8080/项目名/。
导出WAR包之前,注意数据库连接参数要改成答辩环境实际能用的值,不要继续用你本地的root密码。另外,不要把真实数据库里的几十条测试数据带到演示环境,最好是重新导入一份干净的SQL脚本,保证演示时数据可控。
Tomcat运行时还需要检查JAR包依赖。如果你用了MySQL Connector/J,一定要把这个jar包放到Tomcat的lib目录或者项目的WEB-INF/lib目录下。很多初学者在这个地方卡了很久,本地IDEA运行没问题,因为IDEA自动引入了依赖,但WAR包部署后却没有带上这个jar包,导致启动时找不到驱动类。
5.4 答辩时老师爱问的几个问题
答辩时老师不太会问你页面做得漂不漂亮,而会围绕技术和逻辑提问。下面是几个高频问题:
第一,“为什么订单明细表里要冗余商品名称和价格?”你要回答:因为历史订单需要保留下单那一刻的商品快照,如果商品改价或删除,订单详情不能跟着变。这个答案体现了你对数据设计边界有真实思考。
第二,“购物车为什么用Session而不用数据库?”你可以回答:为了减少数据库压力,用户在选购过程中的临时行为不一定要持久化,下订单时再把有效数据写入订单和订单明细表。同时也要补充说明,如果在高并发场景下,Session方案会有限制,所以企业系统会用Redis或数据库存储购物车。这种思辨单位会给你加分。
第三,“订单状态怎么转换?”你要能清晰说出状态流转:待付款到已付款,已付款到已发货,已发货到已完成,已完成或取消会有什么影响。还要提到哪些请求可以触发状态变更,比如管理员后台点击发货按钮,只有已付款订单才能改为已发货状态。
第四,“怎么防止SQL注入?”你要回答:使用PreparedStatement预编译语句,所有的用户输入都通过参数绑定传值,不要手动拼SQL字符串。同时提到前端校验和后端校验的区别。
我在实际做这个题目时,最深的体会是:完成功能本身不难,难的是让各个模块之间的数据保持一致。当你把订单、库存、Session购物车这三个模块串起来时,你会发现很多Bug的根源都是没有提前想清楚“数据从哪来、到哪里去”。如果你正在做JSP在线鲜花订购系统,我强烈建议你先花半天时间把表结构和Servlet路由梳理清楚,再动手写代码,这会省下你后面三天的调试时间。有些坑,确实是写一遍才能记住,希望这篇记录能帮你少走一点弯路。