简介:这是一份JavaWeb网上购物商城项目的完整源码,配套SQL数据库脚本,主要面向计算机、通信、人工智能、自动化等专业学生,适用于期末课程设计、课程大作业及毕业设计。项目代码经过调试与测试,可正常运行,覆盖商城前台展示、购物车、订单处理、后台管理等典型功能,能帮助学习者理解JavaWeb项目的整体结构与开发流程。资源包共252个文件,包含Java源文件、JSP动态页面、JavaScript脚本、CSS样式表、XML配置文件、JAR依赖库、SQL数据库脚本以及页面图片等,压缩包大小26.35MB,目录清晰,方便按模块查找和使用。已有1969人浏览学习,适合具有一定JavaWeb基础、希望快速上手完整商城项目的读者。通过参考源码可掌握前后端交互、数据库设计、会话管理等核心点;能力较强的读者还可在此基础上修改功能,完成个性化毕业设计或课程作业。
1. JavaWeb商城购买源码:课设救急模板,能不能跑全看这三处配置
期末旺季一到,JavaWeb 课程设计群里问得最多的就是“有没有商城项目源码”。这份 JavaWeb 购物商城源码就是为这个场景准备的,Java 语言编写,源码和数据库脚本打包在一起,从用户注册、商品浏览、购物车、下单支付,到后台的商品管理和订单处理,完整闭环。它适合毕业设计、期末大作业,也适合想拿 Servlet + JSP + MySQL 练手的人。需要说明的是,这类源码包能不能跑起来,不取决于代码,而取决于三处配置:JDK 与 Tomcat 版本匹配、数据库连接参数、部署方式。这篇笔记就把这三处拆开讲清楚,顺带把最常见的几个坑也列出来。
2. 技术选型与项目结构:Servlet+JSP+MySQL 这套经典组合好在哪
实话讲,现在外面 JavaWeb 课设的代码包,十有八九是 Spring Boot 写的。这个项目不一样,它是原生的 Servlet + JSP + JDBC,没有 Spring 全家桶,也就没有那一大堆需要靠 Maven 拉取的依赖。课设场景下这反而是优点:工程结构直观,请求处理链路短,跑起来对机器和网络环境的要求低,答辩时老师问“你这个请求是怎么从页面到数据库的”,你能把每一层都讲清楚。
数据库用 MySQL,连接方式走的是连接池,工程里自带 C3P0 或类似的数据源配置。页面是 JSP,配合 EL 表达式和 JSTL 渲染数据,浏览器端只用了少量 jQuery,没有前后端分离,也没有 Node 环境的要求。这一整套组合下来,只要电脑上装了 JDK 8、Tomcat 9、MySQL 5.7 或 8.0,半小时之内就能看到登录页。
2.1 三层架构目录划分:controller、service、dao 各管什么
工程内部是典型的 JavaWeb 三层架构,这个分层直接影响答辩时你怎么讲清楚“高内聚低耦合”:
- controller 层(也叫 servlet 层):负责接收浏览器请求、取参数、调用 service、决定跳转哪个页面
- service 层:负责业务逻辑,比如登录校验、下单时库存扣减、订单状态变更
- dao 层:负责 JDBC 访问数据库,执行 SQL,把 ResultSet 封装成对象
拿登录这个功能举例,三层分别长这样。controller 层,就是登录请求对应的 Servlet:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private final UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 取页面参数:登录表单里两个字段的 name String username = req.getParameter("username"); String password = req.getParameter("password"); // 2. 调 service 层做业务校验 User user = userService.login(username, password); if (user != null) { // 登录成功,把用户对象放进 session,后续页面靠它识别身份 req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index"); } else { // 登录失败,带上提示信息转发回登录页 req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }这段代码里值得注意两个点:一是req.getParameter取的字段名必须和 JSP 表单里的name属性一致,二是登录成功后用的是sendRedirect而不是forward。原因是 redirect 走的是浏览器重新发起请求,地址栏变成/index,刷新不会出现“表单重复提交”的浏览器报警;而 forward 是服务器内部跳转,地址栏不变,刷新会把登录动作再执行一遍。这个细节答辩时讲出来,老师会觉得你真写过代码。
service 层和 dao 层的配合,核心是“service 不碰 SQL,dao 不碰业务”:
public class UserService { private final UserDao userDao = new UserDao(); public User login(String username, String password) { // 业务层做数据校验或处理,比如把明文密码再做一次 MD5 // 如果数据库里存的不是明文,这里就要做同样的算法再传给 dao return userDao.selectByUsernameAndPassword(username, password); } }public class UserDao { public User selectByUsernameAndPassword(String username, String password) { // 用 JDBC 写预编译 SQL,? 占位符防止 SQL 注入 String sql = "select * from user where username = ? and password = ?"; // 走 DBUtil 拿连接,PreparedStatement 绑定参数 // 执行后把 ResultSet 里的 uid、username、nickname 等字段封装成 User 对象 // 查不到记录就返回 null,Service 层根据 null 判断登录失败 return null; } }DBUtil是工程里自带的数据库连接工具类,一般都在util包下,用了 C3P0 或 Druid 连接池。连接池的作用是避免每次数据库访问都重新建立 TCP 连接,课设规模下性能差异感觉不出来,但它能避免一种典型的翻车:数据库连接没关,运行一段时间后报Too many connections。所以看代码时重点看一眼 dao 层有没有在 finally 里关 ResultSet、Statement、Connection。
2.2 前端页面体系:JSP 页面与静态资源分工
页面这块,工程的逻辑是 JSP 渲染动态数据,css、js、images目录放静态资源。商品列表页是典型的“JSP + EL + JSTL”组合。Servlet 把查询结果放进 request 后转发给 JSP,JSP 里用${}直接取数据:
<c:forEach items="${page.list}" var="p"> <div class="item"> <a href="detail?pid=${p.pid}"> <img src="${pageContext.request.contextPath}/${p.image}" /> <p class="name">${p.pname}</p> <p class="price">¥${p.price}</p> </a> </div> </c:forEach>这里有一个新手特别容易踩的坑:${page.list}里的page是 Servlet 里req.setAttribute("page", pageBean)设置的对象名,它必须和 JSP 里写的一致,改一处就得同步改另一处。${p.pname}是 EL 表达式调用的p.getPname()方法,所以实体类的属性名和 getter 方法的命名必须遵循 JavaBean 规范,否则页面上直接显示空字符串,而且不报任何异常。这种“数据没渲染出来但不报错”的问题是最难排查的,我一般会先看商品实体类有没有getPname()方法。
2.3 依赖与运行环境匹配:JDK、Tomcat 版本决定了你能不能跑起来
这个项目是原生 JavaWeb 工程,没有 Maven 的pom.xml,依赖靠的是 Tomcat 自带的servlet-api.jar,所以版本匹配是第一步。常见做法是 JDK 1.8 配 Tomcat 9.0.x。如果你电脑上装的是 Tomcat 10 及以上,那就要注意了:Tomcat 10 把javax.servlet包名改成了jakarta.servlet,代码里所有import javax.servlet.*和@WebServlet注解都会报红,编译直接失败。解决办法是卸载 Tomcat 10,换回 Tomcat 9.0,不要在代码层面硬改,几十个 Servlet 逐个改 import 没什么性价比。
数据库驱动这块,MySQL 5.7 用mysql-connector-java-5.1.x,MySQL 8.0 用mysql-connector-java-8.0.x。判断工程用的是哪个版本,直接看lib目录下的 jar 包文件名就行了。如果连接串里写的是jdbc:mysql://localhost:3306/shopdb?useSSL=false&serverTimezone=Asia/Shanghai,那必然是 8.x 的驱动;如果连接串很干净,没有serverTimezone,多半是 5.1.x。这个细节直接关系下一章的数据库配置能不能一次连上。
3. 数据库设计与核心功能:订单状态机和库存扣减体现业务深度
商城项目的数据库表设计是有规律可循的。拿到shopdb.sql之后,别急着往 Navicat 里拖,先打开脚本扫一遍表结构。这么做有两个好处:一是答辩时被问到“表之间什么关系”你能张口就来;二是能在导数据之前发现脚本里有没有明显的问题。这个工程的表设计走的是经典商品系统。
3.1 四张核心表怎么建:从用户表到订单明细表
用户表和商品表是基础,购物车表和订单表是业务核心。建表脚本大概长这样:
CREATE TABLE `user` ( `uid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(20) NOT NULL, `password` varchar(32) NOT NULL, `nickname` varchar(20) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, PRIMARY KEY (`uid`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;用户表的password字段长度是 32 位,对应的是 MD5 加密后的 32 位十六进制字符串。如果注册时密码长度超过 20 位,要么是业务层做了加密后存储,要么是明文的限制。课程设计里用 MD5 就够了,但答辩时老师可能会问“MD5 安全吗”,你得准备一句:MD5 不可逆,但容易撞库,生产环境会用加盐或者 BCrypt,课设场景用 MD5 是为了演示加密流程。
CREATE TABLE `product` ( `pid` int(11) NOT NULL AUTO_INCREMENT, `pname` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL, `stock` int(11) DEFAULT '0', `image` varchar(255) DEFAULT NULL, `pdesc` varchar(500) DEFAULT NULL, `cid` int(11) DEFAULT NULL, `status` tinyint(1) DEFAULT '1', PRIMARY KEY (`pid`), KEY `idx_cid` (`cid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;商品表里关键的字段是status和stock。status控制上下架,1 表示上架,0 表示下架,前台列表只查status = 1的商品,后台管理员可以修改这个值。stock是库存,下单时要扣减,后面专门讲这个逻辑。price必须用decimal(10,2)而不是 float 或 double,float 在电商场景下算总价容易产生小数误差,这是很多老项目里价格对不上的根源。
CREATE TABLE `orders` ( `oid` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) NOT NULL, `order_no` varchar(32) NOT NULL, `total_price` decimal(10,2) NOT NULL, `status` tinyint(1) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, `consignee` varchar(20) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, `address` varchar(255) DEFAULT NULL, PRIMARY KEY (`oid`), KEY `idx_orders_uid` (`uid`), CONSTRAINT `fk_orders_uid` FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;订单表把收货人姓名、电话、地址冗余进了订单,而不是通过外键去关联用户表的地址。这是刻意的设计:下单那一刻的收货信息是快照,用户日后改了默认地址,已经生成的订单不受影响。答辩时能说出“冗余快照”这四个字,老师会认为你想过真实业务。
CREATE TABLE `order_item` ( `item_id` int(11) NOT NULL AUTO_INCREMENT, `oid` int(11) NOT NULL, `pid` int(11) NOT NULL, `pname` varchar(100) DEFAULT NULL, `price` decimal(10,2) DEFAULT NULL, `num` int(11) DEFAULT '1', PRIMARY KEY (`item_id`), KEY `idx_item_oid` (`oid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;订单明细表的存在是为了记录“下单那一刻买了什么、什么价格”。它不设外键关联商品表,pname和price直接冗余在明细里。原因很实际:商品改名、改价之后,历史订单还是要能打出当时的商品名和成交价。这个“历史快照”思路也是订单系统和普通增删改查明显的分水岭。
3.2 订单状态流转:从待付款到已完成的一条状态机
订单表里status字段用数字表示状态,常见做法是:
| status | 含义 | 对应操作 |
|---|---|---|
| 0 | 待付款 | 用户下单后默认状态 |
| 1 | 已付款待发货 | 用户点击支付或后台模拟支付后进入 |
| 2 | 已发货待收货 | 后台订单管理里点击发货 |
| 3 | 已完成 | 用户确认收货或后台强制完成 |
| -1 | 已取消 | 用户取消或超时未支付 |
状态流转不是随便 update 一个数字,正常代码里会做一个状态变更方法,限定“从什么状态能变成什么状态”。比如发货操作必须要求当前状态是 1,如果订单处于待付款状态,后台点击发货应该被拦截。代码示意:
public boolean ship(int oid) { // 发货操作:把订单从待发货(1)改为已发货(2) // SQL 里带上当前状态条件,防止并发下状态被错误覆盖 String sql = "update orders set status = 2 where oid = ? and status = 1"; int rows = orderDao.update(sql, oid); // 返回影响行数,0 说明订单状态不是待发货,属于非法操作 return rows > 0; }这条 update 语句带上了and status = 1,课设规模下看不出必要性,但它是状态机正确性的关键防线。不带这个条件的话,哪怕订单已经发货变成 2,再点一次发货按钮也会成功执行,界面上的状态就被打回去了。加了条件之后,第二次点击影响行数为 0,业务层拦截掉,不给用户重复操作留机会。
后台订单列表页一般会给管理员一个下拉框或者一组按钮去切换订单状态,切换时调用对应的 Service 方法。你在跑这个项目时,可以专门测试一下“乱序切换”——比如跳过付款直接发货,看看代码能不能拦住。能拦住,说明工程质量不错;拦不住,答辩前自己改掉,这一个点就够你写进“系统不足与改进”了。
3.3 商品上下架与库存扣减:两个容易写错的地方
先说库存。最简单也最容易出错的下单逻辑是这样:先查出商品库存,判断库存够不够,够就 update 库存,再插入订单。这个顺序在并发场景下是错的价格、超卖问题,比如一件商品只剩 1 件,两个用户同时下单,两个线程都查出库存是 1,都认为可以买,然后都执行了 update,库存就变成了 -1。防超卖扣减的标准写法是把判断和扣减合在一条 SQL 里:
-- 下单扣库存,必须带上 stock >= ? 条件,影响行数为 0 说明库存不足 update product set stock = stock - 1 where pid = ? and stock >= 1;这条语句的巧妙之处在于:MySQL 的行锁保证同一时间只有一个事务能执行成功,update 的where条件里包含了库存判断,影响行数是 0 就说明已经被别人抢走了。业务层在 Java 代码里判断:
public boolean decreaseStock(int pid, int num) { String sql = "update product set stock = stock - ? where pid = ? and stock >= ?"; return productDao.update(sql, num, pid, num) > 0; }库存不足时返回 false,然后整个下单流程回滚事务,商品不会进入订单表。这是电商系统的红线问题,聊天里常说的“超卖”就是这么来的。课设项目里没有高并发压测,但代码写不写这个条件,识货的人一眼就能看出来。
再说上下架。前台商品列表在 service 层或 SQL 里都强制拼上status = 1:
public List<Product> findOnSaleList(int cid) { // 前台列表只查上架商品,下架商品不会出现在首页和分类页 String sql = "select * from product where cid = ? and status = 1"; return productDao.queryList(sql, cid); }后台管理端在编辑商品时维护status字段,前台不需要知道这个字段的存在,直接过滤掉。很多跑不起来或者页面异常的代码包就是在这里混了:前台查询漏掉状态过滤,导致后台下架的商品仍然显示,答辩演示删商品时当场翻车。
4. 本地跑起来:IDEA 导入、数据库初始化、Tomcat 配置三步走
拿到压缩包解压之后,你会看到源码目录、shopdb.sql数据库脚本、README.txt说明文件。接下来要做的就是把源码导进 IDEA,把数据库建起来,然后配置 Tomcat 启动。这三步每一步都有版本陷阱,按顺序操作基本一次过。
4.1 IDEA 导入工程:JDK 版本和模块识别是第一步
打开 IDEA,File -> Open选择解压出来的源码根目录。如果工程里有pom.xml,IDEA 会识别成 Maven 工程并自动下载依赖;如果工程没有 Maven 结构,IDEA 会把它当成普通目录,这时需要手动配模块。
shop-web/ ├── src/ │ ├── main/ │ │ ├── java/ // 所有 .java 源码 │ │ ├── webapp/ // JSP、css、js │ │ └── resources/ // db.properties 等配置文件 ├── lib/ // mysql-connector jar 包 └── shopdb.sql导入后的第一步不是写代码,而是确认三处设置:
File -> Project Structure -> Project SDK:确认是 1.8,不是 17 或 21。版本太高容易把代码里过时的语法标红。Project Structure -> Modules -> 选中你的模块 -> Dependencies:点击加号,把lib目录下的 jar 包(数据库驱动加连接池 jar)逐个添加进去。漏掉这一步,运行时会报ClassNotFoundException: com.mysql.cj.jdbc.Driver。File -> Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler:确认字节码版本是 1.8。
如果是 Maven 工程,逻辑一样,只是依赖从中央仓库拉。课设网络环境经常拉一半超时,这里我建议切到国内镜像源,在settings.xml里加阿里云镜像,能省不少等待时间。
4.2 数据库初始化:建库、导表、改数据库连接配置
数据库初始化用命令行或者图形化工具都行。命令行是:
mysql -u root -p create database shopdb default character set utf8; use shopdb; source C:/Users/你的目录/shopdb.sql;source后面跟的是shopdb.sql的绝对路径,路径里有中文或空格会报错,直接放 D 盘根目录最省事。导入完成后执行show tables;,能看到 user、product、orders、order_item 这几张表就说明脚本执行成功。除了表,脚本里一般还有几条测试数据,比如管理员账号和几件商品,方便你启动后直接登录后台。
数据库建好之后,改连接配置。连接参数集中在src/resources/db.properties(也有工程放在src/db.properties,按实际位置找):
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/shopdb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的数据库密码 jdbc.maxPoolSize=10 jdbc.minPoolSize=3serverTimezone=Asia/Shanghai是 MySQL 8.0 的强制要求,不加会报时区错误。characterEncoding=utf8保证写入数据库的中文不是乱码。useSSL=false是关闭 SSL 连接警告,MySQL 8.0 默认开启 SSL,本地开发关闭就好,否则控制台会刷一堆 SSL 告警日志。
jdbc.password改成你自己本机 MySQL 的密码。这个文件改完记得确认 IDEA 有没有把它复制到 target 或 out 目录,经常有人改完源文件但构建输出目录里还是旧配置,启动后报连接失败,找半天才发现是这个问题。
4.3 配置 Tomcat 并启动:从 Add Configuration 到浏览器看到首页
IDEA 里配置 Tomcat 的路径是:Run -> Edit Configurations -> 左上角加号 -> Tomcat Server -> Local。在Application server下拉框里选你本机安装的 Tomcat,没有的话先New指向 Tomcat 解压目录。
然后切到Deployment标签页,点加号选Artifact...,选中带war exploded的那个选项。这里要特别说明:课设工程在 IDEA 里启动,用war exploded(解压目录模式)是常见做法,它不需要每次改代码都重新打 war 包,热部署快。Application context填/shop,或者保持空和/一致,取决于你想用什么路径访问。填了/shop之后,浏览器访问地址就是http://localhost:8080/shop/index。
配置完成后点 Debug 或 Run 启动。第一次启动可以从下方Tomcat Log控制台观察日志,看到下面这行就说明启动成功:
INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [C:\...\apache-tomcat-9.0.x\webapps\shop]打开浏览器输入http://localhost:8080/shop/index,看到商品列表页面,整个项目就跑起来了。如果页面报 404,先检查Application context和浏览器地址是否一致;如果报 500 且提示数据库连接失败,回头检查 4.2 的配置;如果页面渲染出来但不显示数据,多半是 url 路径里的上下文没带上,或者 JSP 里的${pageContext.request.contextPath}写漏了。
5. 避坑排查:导入即红、连不上库、乱码、登录失效四件事
以下是整理自大量实操的踩坑记录,每条都按照“现象 -> 原因 -> 解决”来写。代码跑不起来的 90% 原因都集中在这几条里,排查顺序也按这个来。
5.1 导入即红:javax.servlet找不到
现象:IDEA 里所有import javax.servlet.*和@WebServlet注解全部红色报错,编译不通过。
原因:本机安装的是 Tomcat 10 及以上版本,而 Tomcat 10 的 Servlet API 包名从javax.servlet改成了jakarta.servlet,旧代码无法直接使用。Tomcat 9 及以下版本仍然是javax.servlet。
解决:卸载 Tomcat 10,下载并安装 Tomcat 9.0.x(Apache 官网的 Binary Distributions 目录选择core下的 zip 包),然后在 IDEA 的Run -> Edit Configurations里重新指定 Application server。注意,替换之后要把 Artifact 里的依赖也重新刷新一下,右键模块Open Module Settings -> Dependencies确认 servlet-api 已经指向新的 Tomcat。从那以后我每次提醒别人都会多说一句:这个坑不用改代码,换版本是唯一后悔药。
5.2 连不上数据库:CommunicationsException与serverTimezone
现象:Tomcat 启动时或浏览器第一次访问数据库时报Communications link failure或The server time zone value is unrecognized,页面 500。
原因:有两层。第一层是驱动版本与 MySQL 版本不匹配,MySQL 8.x 的认证方式默认是caching_sha2_password,老驱动mysql-connector-java-5.1.x根本连不上。第二层是 MySQL 8.0 之后默认时区不是 UTC,连接串没加serverTimezone就报错。
解决:确认lib目录或 Maven 依赖里的驱动是mysql-connector-java-8.0.x,连接串按 4.2 里的完整写法配齐。如果用的 MySQL 是 8.0,还要注意用户认证方式,可以在 MySQL 命令行执行:
-- 如果你的 root 用户是 caching_sha2_password,改成 mysql_native_password 兼容老驱动 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;改完再启动 Tomcat,数据库连接就通了。顺带说明,本地开发时 MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,不要少写中间的cj,这也是个容易让人反复横跳的细节。
5.3 页面中文乱码:从 db.properties 到 JSP 编码一个都不能少
现象:商品名称、分类名称在页面上显示成???,或者 JSP 页面本身出现乱码。
原因:有三处编码不一致。一是数据库连接串里缺characterEncoding=utf8,JDBC 向 MySQL 发送中文时按默认编码转换。二是 JSP 页面头部没设置pageEncoding。三是 Tomcat 接收 POST 请求参数时没有做编码处理,默认按 ISO-8859-1 解码。
解决:逐项检查。db.properties连接串加上characterEncoding=utf8;每个 JSP 文件第二行写<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;然后在工程里新建一个过滤器,把所有请求和响应统一处理:
@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 请求参数统一按 UTF-8 解码 request.setCharacterEncoding("UTF-8"); // 响应内容统一按 UTF-8 输出 response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); } }这个过滤器是工程级的全局配置,放进filter包后所有 Servlet 和 JSP 自动生效。测试方法是:注册一个新用户,用户名写成中文,登录后看个人信息页的显示。之前见过一个项目,首页商品是从 SQL 脚本里查出来的所以正常,但用户注册的中文名全乱,加了这个过滤器立刻就好了。
5.4 登录状态刷新就丢:session 与重定向的配合问题
现象:登录成功后跳转首页,用户状态还在;但刷新一次页面或点击二级链接后,页面右上角又没有“欢迎 xxx”了,回到未登录状态。
原因:登录成功时把用户对象放错了作用域,常见有两种错误写法:一是放在 request 里,req.setAttribute("loginUser", user),request 生命周期在一次转发或重定向结束时就完了;二是登录 Servlet 里创建了一个新的UserService或UserDao对象,这种跟状态没关系,真正的根源多半是 session 的isNew()行为——每次请求都新建 session,说明浏览器禁用了 Cookie 或者服务器 Session 配置不对。
解决:确认登录成功后放的是 session,且跳转路径正确:
// 正确写法:把用户放进 session req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index");同时检查 JSP 里判断登录状态的方式,用${not empty sessionScope.loginUser}而不是${not empty loginUser}。还要确认浏览器没有禁用 Cookie,Tomcat 默认通过JSESSIONIDCookie 维护会话,禁用后每次请求都是新 session,表现出来就是“永远处于未登录状态”,这种玄学问题在课程设计演示时常遇到,提前把浏览器 Cookie 设置检查一遍。
5.5 分页 totalCount 对不上:count(*) 和列表查询条件不一致
现象:数据库有 20 条商品,每页显示 5 条,分页组件显示 3 页,但点击第三页是空页面或者报错。
原因:分页的核心是两条 SQL,一条是count(*)查总记录数,一条是带limit查当前页数据。两条 SQL 的where条件不一致时就会出现这种情况。常见的是列表查询带了status = 1过滤,但 count 查询没带,数据库 20 条里有 8 条下架商品,列表显示 12 条分 3 页,但 count 返回 20 显示 4 页。
解决:把总记录数和列表查询封装在同一个 dao 方法或 service 方法里,保证 WHERE 子句完全一致:
public PageBean<Product> findPage(int currentPage, int pageSize) { PageBean<Product> page = new PageBean<>(); // 总记录数:SELECT count(*) FROM product WHERE status = 1 page.setTotalCount(productDao.countOnSale()); // 当前页数据:SELECT * FROM product WHERE status = 1 LIMIT ?, ? // limit 的第一个参数是 offset = (currentPage - 1) * pageSize int offset = (currentPage - 1) * pageSize; page.setList(productDao.findOnSaleByPage(offset, pageSize)); return page; }PageBean里计算总页数用的是Math.ceil(totalCount * 1.0 / pageSize),向上取整,避免出现第 0 页。改动时你会发现一个问题:后台管理列表和前台列表的过滤条件本来就不同,后台要看全部商品,前台只看上架的。所以分页查询方法最好传一个status参数进去,这样两种列表都调同一个方法,条件只差一个参数,不会再出现数据对不上的问题。
6. 答辩自测与改模块:动手前先想清楚这三件事
项目能跑起来只是第一步,答辩能不能过,看的是你有没有系统地测过它。有一个笨但有效的自测顺序:先走一遍完整购物流程——注册新用户、登录、首页浏览商品、加购物车、结算下单、去后台发货、重新登录前台确认收货。这串操作覆盖了用户、商品、购物车、订单四张表的最核心链路,中间任何一步在 JSP 页面报错或者数据没落库,都是答辩前必须处理掉的问题。
想加分的话,改三个小点。第一是商品搜索,在原来“分类查看”基础上加一个按名称模糊查询的入口,SQL 用where pname like concat('%', ?, '%'),这个改动直接展示你懂动态查询而不是只会写死条件。第二是密码加盐,数据库里把密码字段从md5(明文)改成md5(明文 + 随机盐),多一个盐值字段,注册时生成盐,登录时先按用户名查出盐再比对,这个点可以讲出一段“为什么明文 MD5 不安全”的完整论述。第三是给订单列表加一个简单的多条件筛选,比如按订单状态查,一个下拉框加一个查询按钮就够了。
演示顺序也很重要。先进后台管理,给某个商品改价或者下架,保存后切到前台刷新列表,看到变化,证明后台功能和前后台数据联动正常。再走前台购物主链路,从注册到下单结账,然后切到后台把订单状态改成已发货。这比上来就演示登录注册有说服力得多,因为老师看到的是一条完整的、有业务逻辑的操作链路,而不是单个功能的堆砌。从那以后我每次帮别人看课设项目,都强制走完这条链路再做演示,至少能提前暴露出一半的隐藏问题。希望帮到你。
本文还有配套的精品资源,点击获取