简介:JavaWeb购物商城是一套基于JSP、Servlet与MySQL实现的小型在线商店项目源码,主要面向正在学习JavaWeb开发的学生、需要完成课程设计或毕业设计的开发者,也适合想快速搭建商城演示项目的初学者。项目将用户界面、请求控制、业务逻辑与数据访问明确分层,包含用户注册登录、商品分类浏览、购物车增删改、订单提交确认、后台商品管理等功能模块,并覆盖待支付、处理中、已发货等状态流转场景,能够帮助读者打通从页面提交到数据库操作的完整调用链路。资源包采用RAR压缩格式,共270个文件,大小约7.08MB,内含Java源码与编译后的class文件、JSP动态页面、XML配置文件、CSS与JavaScript前端资源、商品图片素材以及数据库脚本,覆盖了项目部署运行所需的工程结构,目录组织清晰便于按需查阅。目前该资源已有1287人学习浏览,具备一定的参考热度。借助这份完整工程,不仅可以直观理解商品、订单、用户等核心数据表的MySQL设计思路,还能对照源码掌握请求转发、会话管理、JDBC增删改查等关键Web开发技能,对系统化巩固JavaWeb知识框架很有帮助。
1. 为什么这份 javaweb 购物商城能当课设救急
如果你正在为一门 javaweb 课程设计找一款能跑通、能讲清、改起来不吃力的购物商城项目,这份资源大概率能帮你省掉至少一周的从零编码时间。它不是只有页面和假数据的演示壳子——按 SSM(Spring + SpringMVC + MyBatis)三层结构搭的完整商城,登录注册、商品分类、购物车、下单结算这些主线功能都带得齐,SQL 脚本和部署说明也一并配好。适合两类人:一类是被课程设计或实训卡住的学生,想拿到一个结构清楚、答辩能讲出逻辑的完整案例;另一类是准备转 Java 开发,需要一个能写进简历又能本地真实跑通的业务系统。虽然偏向教学,但 Session 登录态、购物车合并、库存扣减踩到的坑,和真实商城几乎一样,这也是值得拆一遍的原因。
2. 技术选型与工程骨架:SSM 分层与数据库五张表
2.1 为什么是 SSM:课程设计场景下的务实选型
很多高校的 javaweb 课设题目本身就写着“不使用 Spring Boot”,于是 SSM 成了兼容性最好的折中方案:比纯 Servlet + JDBC 开发快得多,又比 Spring Boot 更能展示手写配置的能力。Spring 管对象、SpringMVC 管路由、MyBatis 管 SQL,三者各司其职。拿着这套结构去答辩,老师问“Spring 容器什么时候初始化”,你可以直接指向 web.xml 里 DispatcherServlet 的 load-on-startup 配置,而不是笼统说“框架都帮我们弄好了”。
拿到这类项目包之后,我一般会建议先别急着启动 Tomcat,先把 src/main/java 下面的包结构过一遍。常见做法是按 controller、service、mapper、entity 四层拆:controller 只做参数接收和页面跳转,service 层管事务和业务规则,mapper 层写 SQL 与表字段映射,entity 对应数据库表。这样从一个页面请求追到数据库执行,只需要三步,排查问题的时候路径非常短。
还有一个检查点会在第一遍跑项目时浪费很多时间:看 JSP 页面放在哪个目录。如果项目的 JSP 统一在 /WEB-INF/view 下面,spring-mvc.xml 里的视图解析器会配成 prefix=/WEB-INF/view/、suffix=.jsp;如果页面散落在 webapp 根目录或自定义路径,这份配置必须同步改。很多新手只改页面不发布配置,结果访问路径全部 404,这不是代码的问题,是视图解析器在“按图索骥”时找不到图。
2.2 Maven 工程骨架与关键配置
这个项目类型的典型 Maven 依赖长这样,版本号是按 JDK 8 + Tomcat 8.5 + MySQL 5.7 的老牌组合配的:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies>Spring 5.2 配 JDK 8 是稳定组合,spring-webmvc 会自动带出 spring-web 和 spring-core,不需要重复声明。mysql-connector 用 5.1.47 是为了兼容 MySQL 5.7;如果你本地装的是 MySQL 8,需要把驱动类名从 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver,并升级连接器版本为 8.0.x,否则启动时大概率报 ClassNotFoundException。javax.servlet-api 的 scope 是 provided,意思是部署时由 Tomcat 提供,war 包里不要打包进去,否则 Tomcat lib 下的 servlet-api 会起冲突。
下面是 web.xml 里最容易被忽略的两个配置,字符编码过滤器和 DispatcherServlet:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>CharacterEncodingFilter 不是随便放一个位置就行,它必须排在所有过滤器最前面。有的项目里还有登录拦截器或权限过滤器,如果登录过滤器先执行,POST 请求此时还没走编码转换,后面做登录校验时用户名密码就已经乱码了,会得到“用户不存在”的错觉。load-on-startup 的值填 1,意思是应用启动时就初始化 Spring 容器,而不是等第一个请求进来再初始化,这样做能尽早暴露配置错误。
2.3 数据库设计:五张核心表怎么建
购物商城主线功能只需五张表撑起来:用户表、分类表、商品表、订单表和订单明细表。订单必须单独拆明细表,不能把商品明细塞进订单表里,否则一个订单买三件商品时就没法存。建表 SQL 我按最常见的写法给你:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL, `password` varchar(64) NOT NULL, `nickname` varchar(32) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `sort_order` int(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `goods` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) DEFAULT NULL, `name` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL, `stock` int(11) NOT NULL DEFAULT '0', `image_url` varchar(255) DEFAULT NULL, `description` text, `status` tinyint(4) DEFAULT '1', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_sn` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_sn` (`order_sn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `order_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_id` int(11) NOT NULL, `goods_id` int(11) NOT NULL, `goods_name` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL, `quantity` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;订单表用 order_sn 而不是自增 ID 直接当订单号给用户看,是因为自增 ID 容易被遍历猜测,也能体现“订单号要唯一且不可预测”的业务意识。order_item 表冗余了 goods_name 和 price 两个字段,下单后商品就算改名甚至下架,明细里依然保留了下单时的交易快照,这个设计在答辩时是加分项。商品表的 price 用 decimal(10,2) 而不用 float/double,前者是精确数值,后者是浮点近似值,会对账不符的坑能从这里直接避免。
3. 登录与购物车:Session 状态管理和购物车合并逻辑
3.1 登录态怎么存:Session 与 Cookie 的边界
HTTP 协议本身是无状态的,商城想认出“当前登录的是谁”,只能靠浏览器每次请求带上凭证。这个凭证有两种落位:放浏览器端就是 Cookie,放服务端内存就是 Session。教学项目里登录态基本都走 Session,因为 Session 存在 Tomcat 内存里,用户直接改不了;Cookie 虽然也能存用户 ID,但用户可以在浏览器里手动改值,安全性完全不可控。
两种方案的边界我用一个对比说明:
| 对比项 | Cookie | Session |
|---|---|---|
| 数据存放 | 浏览器本机 | Tomcat 内存 |
| 能否篡改 | 能 | 不能 |
| 过期策略 | Expires/Max-Age 控制 | 默认 30 分钟 |
| 典型用途 | 记住用户名、购物车草稿 | 登录态、验证码 |
Cookie 也不是没用,商城里的购物车草稿和“下次自动登录”一般还是会用到 Cookie,只是登录主体凭证必须放在 Session。如果某个项目源码里把用户 ID 原样塞进 Cookie,没有签名也没有过期时间,答辩时被问“安全性怎么保证”就会很难往下讲。
3.2 登录接口与拦截器代码
登录逻辑的核心不是比对密码本身,而是登录成功后如何把用户对象放进 Session,以及如何让后续请求能拦截未登录用户。先看登录接口处理方法:
@Controller public class LoginController { @Autowired private UserService userService; @PostMapping("/login") public String login(String username, String password, HttpSession session, Model model, @RequestParam(defaultValue = "/") String redirect) { User user = userService.login(username, MD5Util.encode(password)); if (user == null) { model.addAttribute("error", "用户名或密码错误"); return "login"; } session.setAttribute("currentUser", user); return "redirect:" + redirect; } }密码一定不能明文存,建表时 password 字段长度我写了 64,就是给 MD5 加密后的 32 位再加盐值的空间。登录成功后的 redirect 参数设计得比较实用:未登录用户访问购物车时,会被拦截器带到登录页并带上原目标地址,登录完成后直接跳回去,而不是每次都要重新点一遍。这个参数必须做白名单校验,如果发现 redirect 不是站内路径,就强制跳回首页,否则会有开放重定向的安全问题。
拦截器要单独注册才生效,写法如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("currentUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login?redirect=" + URLEncoder.encode(request.getRequestURI(), "UTF-8")); return false; } return true; } }在 spring-mvc.xml 里注册时要注意放行路径:静态资源 css/js/images、登录接口、注册接口、商品列表和商品详情是要开放的,购物车、订单、结算这些路径全部拦截。很多项目把拦截范围写成 “/*”,结果登录页的 css 也被拦住,页面看起来就是一团没有样式的纯文本。正确做法是注册时配 exclude-mapping 把静态目录全部排掉。
3.3 购物车合并:未登录与已登录的切换
商城购物车最麻烦的场景不是加购,而是用户未登录时往 Cookie 里塞了几件商品,后来点登录,这两批商品怎么合并。常见做法是未登录使用 Cookie 存一个 cart JSON,登录完成后,把 Cookie 里的购物车数据并进当前用户的 Session 购物车,然后清掉 Cookie。加购逻辑的核心代码:
public void addToCart(Cart cart, Goods goods, int quantity) { List<CartItem> items = cart.getItems(); for (CartItem item : items) { if (item.getGoodsId().equals(goods.getId())) { item.setQuantity(item.getQuantity() + quantity); return; } } CartItem newItem = new CartItem(); newItem.setGoodsId(goods.getId()); newItem.setGoodsName(goods.getName()); newItem.setPrice(goods.getPrice()); newItem.setQuantity(quantity); items.add(newItem); }用 goodsId 判断购物车里是否已存在这个商品,存在就把数量叠加,不存在就新增一行。这里有个边界要讲清楚:购物车条目里的 price 应该取“加入购物车那一刻”的商品价格,而不是每次查询实时价格。如果下单结算时商品临时调价,价格以购物车快照为准,否则用户看到的价格和最后付款价格不一致,就是大麻烦。
登录合并的逻辑是模拟合并:遍历 Cookie 购物车的每一项,逐一调用 addToCart 追加到 Session 购物车。有一类坑经常出现——合并时没判断 Cookie 购物车里和 Session 购物车里是否有同一件商品,直接用 add 覆盖,导致数量变少。用 addToCart 方法天然规避了这个问题,因为方法内部先做存在性判断。
4. 部署与避坑:Tomcat、MySQL、Session 的三类疑难
4.1 拿到资源后的部署三步走
先把环境对齐再动手。这套项目跑起来只需要三个前置:JDK 8、Tomcat 8.5(或 9.0)、MySQL 5.7(或 8.0)。部署流程我习惯按下面的顺序走,每做一步就确认一次结果,避免最后堆在一起排错。
- 先建空库,用 Navicat 或命令行把项目里带 .sql 脚本导入,确认五张表全部生成,顺序执行一遍不要跳跃。
- 修改 db.properties 连接配置,重点看 URL 里的 database 名、用户名、密码三处,编码统一用 UTF-8。
- 用 Maven 打包成 war,丢进 Tomcat 的 webapps 目录,启动后访问 contextPath 对应的首页。
这三步看着简单,但每一步都可能埋坑:数据库导入时报Unknown collation、连接配置改了但没重新打包导致没生效、war 包名称和访问地址不一致导致 404。下面的踩坑记录都是我在复现这类项目时真实遇到过的。
4.2 数据库连接报时区错误
现象:Tomcat 启动时控制台报错,核心内容包含The server time zone value 'XXX' is unrecognized,应用起不来或首次访问 500。
原因:MySQL 5.7 的时区配置在某些版本里默认不规范,而数据库驱动在建立连接时会做时区校验,校验不过就直接拒绝连接。
解决:这跟代码没关系,是连接串的问题。在 db.properties 的 JDBC URL 后面追加时区参数:
jdbc.url=jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai如果你的数据库是 MySQL 8,除了时区参数还要把驱动类改成com.mysql.cj.jdbc.Driver,否则启动阶段就会报 ClassNotFound。
4.3 POST 表单提交后中文全部乱码
现象:注册页面填中文昵称,提交后数据库里存的是å¼ ä¸‰一类无法辨认的字符;登录用户名是中文时,每次都提示“用户名不存在”。
原因:Tomcat 8.0 之后的版本对 POST 请求默认使用 ISO-8859-1 解码,而页面编码是 UTF-8,两边不一致就乱码。这是操作系统的区域设置、页面编码和 Tomcat 默认编码三方不一致造成的,单纯改 JSP 页面编码没用。
解决:按第 2 章说的在 web.xml 里配置 CharacterEncodingFilter,并且把<url-pattern>配置在最外层。配置完必须重启 Tomcat,因为过滤器加载发生在容器初始化阶段。如果过滤器配了还是乱码,检查 spring-mvc.xml 里是否也配置了请求编码转换,两者叠加会重复解码,反而会把已经转好的 UTF-8 字节再转一次。
4.4 EL 表达式和 JSTL 标签不生效
现象:商品列表页 JSP 里${goods.name}原样显示成一段文本,<c:forEach>标签也没有循环出数据,但页面上其它静态内容正常渲染。
原因:两个层级的问题:一是 web.xml 没有引入 JSTL 标签库描述文件,二是 Servlet 版本声明和 Tomcat 实际版本不匹配,导致 EL 表达式解析被禁用。
解决:先确认项目的 web.xml 根节点是 Servlet 3.1 规范,再看 pom.xml 里是否声明了 jstl 1.2 依赖。如果两者都正常,检查 JSP 页面顶部是否漏了这两行:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page isELIgnored="false" %>最后一行是关键,Servlet 3.1 的 web.xml 默认允许 EL 表达式,但如果项目里还混着旧的 Servlet 2.5 声明,EL 就被当成普通文本直接输出。
4.5 登录成功后页面跳转循环
现象:用未登录身份访问购物车,被拦截器带到登录页,登录成功后没有进入购物车,而是在登录页和购物车之间反复跳转,最终浏览器提示“重定向了太多次”。
原因:登录成功后的 redirect 参数是登录页路径,拦截器又只判断 Session 是否有 currentUser,登录成功时写 Session 的代码没执行到,或者写进去了但不同请求拿到的 Session 不是同一个,就会造成“登录了等于没登录”。
解决:在登录成功的处理代码里加日志,打印 user 和 session.getId(),确认是否是同一个 Session。常见场景包括:项目被部署到两个 Tomcat 实例、浏览器禁用了 Cookie、应用路径用了 IP 加不同端口访问。如果是单机部署且只开了一个 Tomcat,优先检查登录成功后是不是用 sendRedirect 写了绝对路径,比如把/login写成了/shop/login,导致不同 contextPath 下两次请求的 Session 不同。修复方式是统一使用 request.getContextPath() 拼接站内路径,不要手写死路径。
5. 进阶改造:并发下单防超卖与验证方法
5.1 库存扣减为什么不能用“先查再减”
如果直接把这段代码给出的商城主线功能当成简历项目,面试官大概率会追问“多个人同时下单同一件商品,库存只有 1 件,会不会超卖”。很多课设代码是这样写的:先查库存,剩余量大于 0 就执行 update。这在单用户操作时没问题,但两个请求同时读到库存为 1,同时通过校验,同时执行减一,库存就变负了。这是典型的并发竞态,靠 Java 代码里的 if 判断拦不住。
最简洁可讲的方案是把“校验库存”和“扣减库存”合并成一条条件更新 SQL:
UPDATE goods SET stock = stock - 1 WHERE id = #{goodsId} AND stock > 0;MyBatis 里对应的 mapper 方法返回 int,这个值就是受影响行数。受影响行数为 1 说明扣减成功,可以继续创建订单;受影响行数为 0 说明库存已经被别的请求扣完,此时直接提示“库存不足”并取消下单。
@Transactional public boolean createOrder(Long userId, Long goodsId, int quantity) { int rows = goodsMapper.deductStock(goodsId); if (rows == 0) { throw new RuntimeException("库存不足"); } orderMapper.insert(...); return true; }UPDATE 在 InnoDB 引擎下会对命中行加行锁,同一时刻只有一个事务能改这行数据,所以条件更新天然防护了超卖。注意事务边界:扣减库存和插入订单必须在同一个事务里,createOrder 方法上的 @Transactional 不能省,否则出现“库存扣了但订单没生成”的不一致状态。这个方法只防超卖不解决排队优化,但作为课设和简历项目已经够用。
5.2 并发验证的土办法
没有压测工具也能验证方案有没有生效。打开两个无痕浏览器窗口,用两个账号同时登录商城,把同一件商品库存改为 1,然后两个窗口同时点下单。如果扣减逻辑正确,必然是一个成功、一个提示库存不足;如果两边都下单成功,说明代码里还在用查库存再减的旧逻辑。更严格的做法是在数据库里用两条连接同时执行上面的 UPDATE,因为验证的是 SQL 行为,不走界面也能复现。
从那以后我每次改库存相关代码,不管只是改了一个 SQL 空格,还是会强制自己走一遍“两个窗口同时下单同一商品”的验证流程,再看日志里受影响行数。这个土办法拦住了我好几次自以为没问题的翻车。希望帮到你。
本文还有配套的精品资源,点击获取