☰
JavaWeb商城项目源码实践:SSM架构、数据库设计与避坑指南
2026/10/7 17:01:16 网站建设 项目流程

简介:这是一份基于JavaWeb的商城购买项目源码与数据库文件,源自个人毕业设计,曾获答辩评审98分。项目围绕网上购物场景,覆盖前台商品展示、登录注册、购物车及下单购买流程,同时包含后台管理模块,适合计算机、通信、自动化等专业学生用作期末课程设计、课程大作业或毕业设计的参考模板,也便于有基础者在此基础上按需扩展改造。

压缩包共252个文件,大小约26.35MB,核心为38个java源文件、43个jar依赖库、1个sql数据库脚本及jsp页面,并配以33个js、23个css、9个html等前端资源,以及多张jpg/png页面截图,目录结构清晰,便于按模块查看。数据库脚本可用于快速初始化购物系统所需的数据表,前端引入Bootstrap等常用组件,工程结构完整,可导入开发工具进行运行与二次调整。

目前已有1969人学习下载。源码、页面及数据库文件均经过调试测试,确认可运行;借助这套材料,可系统理解JavaWeb项目从页面展示到数据存取的整体脉络,包括分层结构、前后端交互与关键业务流的实现方式,尤其适合在课程设计或毕业设计阶段快速搭建一个完整可演示的购物商城系统,具有较好的学习借鉴价值。

1. JavaWeb网上购物商城项目:源码+数据库,值得自己跑一遍吗

如果你搜过“JavaWeb 商城购买”“网上购物项目源码 数据库”,大概率是在做课程设计或毕业设计。这类项目的价值从来不是“真的能买东西”,而是用一套浏览、加购、下单、扣库存的闭环,把 JavaWeb 里最高频的考点全部串起来:Session、拦截器、分页、事务、多表查询、数据库表设计。我调试过不少这类源码,一个很现实的情况是——能顺利从下载跑到浏览器的不足一半,卡点几乎都在数据库脚本和 IDEA 运行配置上。

这篇文章不聊花活,就按“选型 → 数据库 → 购买闭环 → 避坑 → 演示优化”往下推。目标是让你拿到任何一套 SSM 结构的商城源码,都能在两小时内跑起来,并且能讲清楚每个模块为什么这么写,而不是对着黑匣子瞎猜。

2. 先定技术选型:SSM 组合为什么是商城项目源码里的绝对主力

2.1 三条技术路线,选错一步后面全是坑

“JavaWeb 购物商城”这个名字,在不同人手里可能指向三种完全不同的实现:最早的纯 JSP+Servlet+JDBC,中间几年铺天盖地的 SSM(Spring+SpringMVC+MyBatis),以及现在越来越常见的 Spring Boot。

纯 JSP+Servlet 的好处是贴近课本,你能看清 HTTP 请求是怎么被 Servlet 接住、怎么转发到 JSP 的,适合课时短的入门作业。但坏处也很直接:业务一多,所有代码都堆在 Servlet 里,购物车和下单这种多表联动要写大量重复代码。而你搜到的“JavaWeb 项目完整案例 MySQL”,十套里有七八套是 SSM 骨架,原因就是 SSM 的分层足够清晰,Spring 管对象和事务,SpringMVC 管路由,MyBatis 管 SQL,写起来比分页和订单这种逻辑时心里有底。

Spring Boot 当然更新,但很多学校的 JavaWeb 课程还没覆盖到它。如果你拿到的源码是 Spring Boot 版,往往还带着前后端分离的 Vue 工程和 Security 权限配置,对只学过 JavaWeb 基础的人来说,黑匣子反而更大。所以结论很简单:源码是什么就顺着什么改,如果你是零基础自己选型,选 SSM 最稳,资料多、改起来快、答辩也好讲架构。

2.2 读懂 SSM 商城目录结构:打开黑匣子,几分钟就能定位代码

拿到一套源码,先别急着点启动,花十分钟把目录结构看一遍。SSM 商城项目的目录长这样:

shop/ ├── pom.xml └── src/main/ ├── java/com/shop/ │ ├── controller/ # 接收请求,返回页面或跳转 │ ├── service/ # 业务接口 │ │ └── impl/ # 业务实现,事务基本都加在这里 │ ├── mapper/ # MyBatis 的 Mapper 接口 │ ├── entity/ # 和数据库表对应的实体类 │ └── util/ # 工具类,比如 MD5、订单号生成 ├── resources/ │ ├── jdbc.properties # 数据库连接配置,跑不起来先看它 │ ├── spring-mybatis.xml # Spring 整合 MyBatis,数据源在这 │ ├── springmvc.xml # 视图解析器、拦截器、静态资源 │ └── mapper/ # UserMapper.xml、ProductMapper.xml 等 └── webapp/ ├── WEB-INF/web.xml # 项目入口配置,字符编码过滤器一般在这 ├── jsp/ # 首页、商品列表、购物车、订单页 └── static/ # css、js、图片

这个结构对应的是标准的三层架构:Controller 只做参数接收和页面跳转,不写业务;业务逻辑在 Service 层,尤其是下单这种需要多个步骤的操作,事务注解一定写在 Service 实现类上;最底层是 Mapper,一个接口配一个 XML,SQL 都集中在 resources/mapper 下。

调试的时候,按这个思路找问题能省一半时间:页面报错先看 Controller 的返回值,SQL 报错直接看对应的 XML,数据库连不上就先翻 jdbc.properties。很多新手上来就在 JSP 里翻代码,结果看了半天发现问题是 Mapper 里的 SQL 写错了,方向完全反了。

2.3 IDEA 运行 JavaWeb 项目的最小配置:Maven、Tomcat、数据库连接三件套

把这个项目跑起来,需要的东西比普通 Java 程序多两个:Tomcat 和一个数据库。按下面的顺序做,每一步都不要跳。

第一步,用 IDEA 打开项目后,先确认 Maven。右下角如果一直在转圈下载依赖,就等它跑完;如果没反应,右键点 pom.xml,选 Maven → Reimport。依赖没下全,后面 Tomcat 一启动就会报各种各样的 ClassNotFoundException,这是最常见的翻车点。

第二步,配置 Tomcat。菜单 Run → Edit Configurations → 点左上角加号 → Tomcat Server → Local。在 Application server 一栏选你本机装的 Tomcat 目录,选 Tomcat 的根目录,不是 lib 目录。然后切到 Deployment 选项卡,点加号选 Artifact,选形如shop:war exploded的那个。Application context 建议改成/shop,访问地址就是http://localhost:8080/shop/。这一步最容易出错的是选了带:war的打包格式,那会导致每次改代码都要重新打 war 包,开发效率极低,一定要选 exploded 模式。

第三步,改数据库连接。找到 resources/jdbc.properties,这是整个项目跟数据库通信的唯一入口:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/shop_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

如果你的 MySQL 是 5.7,驱动类名可以继续用com.mysql.jdbc.Driver;如果是 8.0,必须用com.mysql.cj.jdbc.Driver,并且 URL 里一定要带serverTimezone=Asia/Shanghai,否则驱动初始化会直接报时区错误。用户名和密码换成你自己的。注意 URL 中间的shop_db就是数据库名,对应下一章要执行的建库脚本。

最后启动:点绿色运行按钮,控制台出现 “Server startup” 就算成功。如果启动后访问是 404,先看 URL 是不是/shop/,再看 Tomcat 配置里的 Application context 是不是和 URL 对得上。这一套配置对所有 SSM 结构的老项目都通用,我后来换过好几台电脑搭环境,基本就是这个流程走一遍,十分钟以内能搞定。

3. 数据库设计:商城项目的 6 张核心表,以及一份能直接跑的 MySQL 脚本

3.1 为什么一张大表跑不通商城:6 张核心表各自的职责

很多新手拿到源码后的第一个想法是“把商品、订单、用户塞一张表里不就行了”,这正是后续翻车的根源。网上购物这个场景,最少需要 6 张表来承担不同职责。

用户表t_user存账号密码和收件信息。密码字段一定要存 MD5 之后的密文,不要存明文,这是所有毕设答辩都会问的点。

分类表t_category和商品表t_product是一对多关系。商品表里category_id保存分类 ID,价格字段用DECIMAL(10,2)而不用FLOAT,是因为浮点类型在金额计算上存在精度误差,比如 0.1+0.2 不等于 0.3,做过支付的都知道这是血泪教训。

购物车表t_cart_item连接用户和商品,核心是(user_id, product_id)的联合唯一键。这个设计能从数据库层面保证同一个用户重复加购同一件商品时,记录只有一条,数量累加而不是插入多条,省去大量额外判断。

订单表t_order负责记录整单信息,包括总额、状态、收件人。这里有个设计要点:收件人姓名、电话、地址要冗余在订单表里,而不是下单时临时去查用户表。因为用户以后可能修改自己的收货地址,但历史订单必须保留下单那一刻的收件信息。订单项表t_order_item同理,商品名称、图片、价格都要以快照形式存一份。商品改价或者改名之后,订单里显示的仍然是成交时的数据。

3.2 建表 SQL:MySQL 5.7 / 8.0 都能直接执行的 DDL

这套建表脚本我按 MySQL 5.7 和 8.0 的兼容性来写,字符集统一用 utf8mb4,避免 emoji 和生僻字入库变问号。先建前四张表:

CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE shop_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,MD5后存储', nickname VARCHAR(50) DEFAULT '' COMMENT '昵称', phone VARCHAR(20) DEFAULT '' COMMENT '手机号', address VARCHAR(255) DEFAULT '' COMMENT '收货地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID', name VARCHAR(50) NOT NULL COMMENT '分类名', sort INT DEFAULT 0 COMMENT '排序值,越小越靠前' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表'; CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', category_id INT NOT NULL COMMENT '分类ID', name VARCHAR(100) NOT NULL COMMENT '商品名称', subtitle VARCHAR(255) DEFAULT '' COMMENT '副标题/卖点', main_image VARCHAR(255) DEFAULT '' COMMENT '主图URL', price DECIMAL(10,2) NOT NULL COMMENT '售价', stock INT NOT NULL DEFAULT 0 COMMENT '库存', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; CREATE TABLE t_cart_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '购物车项ID', user_id INT NOT NULL COMMENT '用户ID', product_id INT NOT NULL COMMENT '商品ID', quantity INT NOT NULL DEFAULT 1 COMMENT '数量', checked TINYINT NOT NULL DEFAULT 1 COMMENT '1勾选 0不勾选', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';

这里两个细节对新手很友好:一是t_cart_item的联合唯一键uk_user_product,它让“加购同一商品自动累加数量”变成了数据库本身的行为,而不是靠应用层先查再改;二是外键我没有物理创建,只保留了KEY idx_category这样的普通索引。物理外键在项目里容易造成删除受限和死锁,实际开发更多用逻辑外键,查询靠category_id关联,由应用层保证数据一致性,这在答辩时也是加分项。

再建订单相关的两张表:

CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', order_no VARCHAR(32) NOT NULL COMMENT '业务订单号', user_id INT NOT NULL COMMENT '下单用户ID', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', receiver_name VARCHAR(50) DEFAULT '' COMMENT '收件人', receiver_phone VARCHAR(20) DEFAULT '' COMMENT '收件电话', receiver_address VARCHAR(255) DEFAULT '' COMMENT '收件地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '明细ID', order_id INT NOT NULL COMMENT '订单ID', product_id INT NOT NULL COMMENT '商品ID', product_name VARCHAR(100) NOT NULL COMMENT '商品名称快照', product_image VARCHAR(255) DEFAULT '' COMMENT '商品图片快照', price DECIMAL(10,2) NOT NULL COMMENT '成交单价快照', quantity INT NOT NULL COMMENT '购买数量', KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

订单表用order_no做业务上的唯一标识,而不是直接用自增主键,因为自增 ID 本质是个内部编号,在业务单据里直接暴露给用户不合适。t_order_item里每个字段都带“快照”注释,这一点我在 3.1 里强调过——商品信息会变,但订单记录不能变。

3.3 演示数据:没有商品数据,商城首页就是个空壳

建完表下一步是灌数据。很多源码包自带的数据库脚本只建表不插数据,你启动起来看到空荡荡的首页,还以为自己没配好。这里给一组可以直接跑的演示数据:

INSERT INTO t_category (name, sort) VALUES ('手机数码', 1), ('电脑办公', 2), ('服饰鞋包', 3); INSERT INTO t_user (username, password, nickname) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '超级管理员'); INSERT INTO t_product (category_id, name, subtitle, main_image, price, stock) VALUES (1, '小米 14 Ultra', '徕卡光学镜头 骁龙8Gen3', '/static/img/phone1.jpg', 5999.00, 50), (1, '华为 Mate 60 Pro', '卫星通话 昆仑玻璃', '/static/img/phone2.jpg', 6999.00, 40), (2, 'ThinkPad X1 Carbon', '14英寸轻薄商务本', '/static/img/laptop1.jpg', 9999.00, 20), (3, 'Nike 经典跑鞋', '轻量缓震 透气网面', '/static/img/shoe1.jpg', 499.00, 100);

注意这里的admin密码是e10adc3949ba59abbe56e057f20f883e,也就是明文123456的 MD5 值。登录页输入 123456 才能进去,这正好呼应了 3.1 说的密文存储。插入顺序也有讲究:先插分类得到分类 ID,再插商品引用这个 ID;先插用户再插购物车。如果你把购物车数据先插,user 和 product 还没存在,逻辑外键虽然不会在数据库层面报错,但查询时会查出一堆空关联,所以演示数据的顺序不要乱。

4. 从注册到下单:把“购买”闭环一步步写出来

4.1 注册登录模块:Session 会话与登录拦截

购物商城的第一步是让用户登录,否则购物车不知道记在谁名下。SSM 项目里登录最常见的做法是:Service 层校验用户名密码,Controller 层把用户对象塞进 Session,后续请求通过拦截器判断 Session 里有没有人。

先看 Mapper 层,登录只需要一个查询和一个插入:

public interface UserMapper { User findByUsername(String username); int insert(User user); }
<select id="findByUsername" resultType="User"> SELECT id, username, password, nickname, phone, address FROM t_user WHERE username = #{username} </select>

注意查询条件只写用户名,不要带密码。为什么?因为密码需要在 Java 代码里先做 MD5 再比对,如果在 SQL 里拼密码,绕了一圈不说,还容易让日志里带上明文密码。Controller 层拿到用户后校验:

@PostMapping("/login") public String login(String username, String password, HttpSession session) { User user = userService.login(username, password); if (user == null) { return "redirect:/login.jsp?error=1"; } session.setAttribute("loginUser", user); return "redirect:/product/list"; }

这个方法的逻辑很直白:登录成功就放一个loginUser到 Session 里,登录失败就带个error=1参数回到登录页。userService.login内部做的事情是先按用户名查出用户,再把输入密码做 MD5 跟查出来的密文比对。参数username、password是 SpringMVC 自动从请求表单绑定的,表单里的 name 属性必须跟它们同名,这是新手最容易忽略的坑。

有了 Session 之后,未登录用户不能访问购物车和订单页面,需要一层拦截器。常见的写法是实现HandlerInterceptor,在preHandle里判断session.getAttribute("loginUser")是否为空,为空就重定向到登录页。这个机制答辩时经常被问,能说清楚“Session 是服务端保存的会话状态,Cookie 里只存了 sessionId”这句话,基本就算过关。

4.2 商品列表与分页:手写 LIMIT 并算清总页数

商品列表是商城首页的主体,不能把所有商品一次性查出渲染到页面上,数据量大了页面会非常慢。分页的标准做法是 SQL 里用 LIMIT 做物理分页,只查当前页需要的十几条数据。

<select id="findPage" resultType="Product"> SELECT id, category_id, name, subtitle, main_image, price FROM t_product WHERE status = 1 <if test="categoryId != null"> AND category_id = #{categoryId} </if> ORDER BY create_time DESC LIMIT #{offset}, #{size} </select>
@GetMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "8") Integer size, Integer categoryId, Model model) { int offset = (page - 1) * size; List<Product> productList = productService.findPage(categoryId, offset, size); int total = productService.count(categoryId); int totalPages = (total + size - 1) / size; model.addAttribute("currentPage", page); model.addAttribute("totalPages", totalPages); model.addAttribute("productList", productList); return "product/list"; }

两个参数要配合着理解:page是前端传过来的当前页码,size是每页条数。SQL 里的offset不能直接用页码,要先算成偏移量,也就是(page - 1) * size。比如第 2 页每页 8 条,偏移量就是 8,意思是跳过前 8 条从第 9 条开始取。totalPages 的计算公式(total + size - 1) / size是个小技巧,等价于向上取整,避免最后一页只有几条数据时页码数不对。

JSP 页面里只需要拿到currentPage和totalPages渲染页码链接,上一页下一页都带上page参数。很多源码里分页是接的 PageHelper 插件,那个更省事,但手写一次 LIMIT 能让你分清楚页码和偏移量的区别,答辩时被问到“分页原理”也不慌。

4.3 购物车:加购去重、数量累加、勾选结算

购物车的核心操作就三个:加购、改数量、勾选。我见过太多源码把购物车放在 Session 里,用户没登录也能加购,看起来方便,但一关浏览器车就空了,而且多浏览器完全没法同步,这类项目答辩时很吃亏。用数据库表存购物车,登录用户加购就落表,逻辑更完整。

public void add(Integer userId, Integer productId, Integer quantity) { CartItem existing = cartItemMapper.findByUserIdAndProductId(userId, productId); if (existing != null) { existing.setQuantity(existing.getQuantity() + quantity); cartItemMapper.updateQuantity(existing); } else { CartItem item = new CartItem(); item.setUserId(userId); item.setProductId(productId); item.setQuantity(quantity); item.setChecked(1); cartItemMapper.insert(item); } }

这段代码的逻辑顺序很重要:先查,存在就累加,不存在就新插入。但这里有个并发问题:如果用户同时点了两次加购,两个请求都查不到记录,就会各插一条,破坏 3.2 里那个联合唯一键的唯一性。所以更稳妥的做法是在插入时用INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity),一条 SQL 直接完成“存在就更新,不存在就插入”的原子操作。你可以保留上面的 Java 逻辑方便阅读,也可以换成这个 SQL 来兜底,两种都是常见写法。

购物车列表页的核心是渲染所有勾选状态的商品,并累加总金额。金额累加用BigDecimal,不要用 double 加减,前面说过浮点精度问题,这里就是正式的翻车现场。加购之后跳回到购物车列表,让用户能看到“这次加了什么、加了几个、合计多少钱”,整个闭环才完整。

4.4 提交订单:事务回滚与条件扣库存

下单是整个项目里最考验设计的一步,它的流程比看起来长:生成订单主表、生成订单项、扣减库存、清空购物车。四个动作任何一个失败,都不能留下半成品订单。

@Transactional(rollbackFor = Exception.class) public Order createOrder(Integer userId, String receiverName, String receiverPhone, String receiverAddress) { List<CartItem> checkedItems = cartItemMapper.findCheckedByUserId(userId); if (checkedItems.isEmpty()) { throw new RuntimeException("购物车为空,无法下单"); } // 遍历勾选项,用商品当前价格累加总额 BigDecimal total = BigDecimal.ZERO; for (CartItem item : checkedItems) { Product product = productMapper.findById(item.getProductId()); total = total.add(product.getPrice() .multiply(new BigDecimal(item.getQuantity()))); } // 生成订单主表 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); order.setReceiverName(receiverName); order.setReceiverPhone(receiverPhone); order.setReceiverAddress(receiverAddress); orderMapper.insert(order); // 逐条生成订单项,并扣减库存 for (CartItem item : checkedItems) { Product product = productMapper.findById(item.getProductId()); OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(product.getId()); oi.setProductName(product.getName()); oi.setProductImage(product.getMainImage()); oi.setPrice(product.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); int rows = productMapper.decreaseStock(product.getId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException( "商品[" + product.getName() + "]库存不足,下单已回滚"); } } // 清空已下单的购物车项 cartItemMapper.deleteCheckedByUserId(userId); return order; }

这段代码有三个关键设计。第一个是方法上的@Transactional(rollbackFor = Exception.class),它声明了事务边界:方法内任何一步抛出异常,前面所有数据库操作全部回滚。注意rollbackFor一定要带,因为 Spring 默认只对运行时异常回滚,如果你抛的是普通 Exception,事务不会滚,就可能出现“订单生成了但库存没扣”的严重问题。

第二个是扣库存用的不是set stock = stock - 1,而是带条件判断的条件更新。对应的 SQL 是这样:

<update id="decreaseStock"> UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>

WHERE id = ? AND stock >= ?保证了库存充足才会更新成功,返回行数 rows 是 0 就说明库存不够。这比“先查库存再拼 update”更可靠,因为并发场景下查到的数字可能已经过期。把判断下推到 SQL 里,数据库本身就帮我们挡了一次并发放行。

第三个是订单状态status = 0,表示待支付。完整的商城流程还有支付、发货、收货这些节点,但对毕设来说,把“从购物车走到生成订单+减库存”这步做扎实,核心打分点就已经拿到。订单号生成我一般用时间戳加用户 ID 拼接,比如20241107 + userId + 毫秒数,保证唯一性和长度适中。

5. 避坑:JavaWeb 商城项目最常见的 6 个翻车现场

5.1 Tomcat 启动报 ClassNotFoundException:依赖没有进 lib

现象:Tomcat 启动到一半,控制台抛ClassNotFoundException: org.springframework.web.context.ContextLoaderListener,或者类似的 Spring 类找不到。

原因:这张翻车现场绝大多数是 Maven 依赖虽然下载了,但没有被 IDEA 打包进发布目录。项目是 war 包结构,类库必须进入WEB-INF/lib才能被 Tomcat 识别,IDEA 的 Artifacts 配置没处理好,依赖就不会自动带过去。

解决:先右键 pom.xml → Maven → Reimport,强制刷新依赖;然后打开 File → Project Structure → Artifacts,选中你的 web exploded 包,在右侧列表里确认Available Elements里的依赖已经右键放入WEB-INF/lib。最省心的一步是回到 Run → Edit Configurations,勾选Build on startup,让每次启动前强制重新构建。

5.2 页面和数据库全变问号:字符编码三件套缺一不可

现象:商品名显示成???,或者 JSP 页面整片乱码,控制台日志也带中文乱码。

原因:字符编码要打通三个环节:IDE 文件编码、Tomcat 请求编码、数据库连接编码。很多老源码用的是 GBK 写页面,你的 IDEA 默认 UTF-8,一打开就乱一半;数据库没连 utf8mb4,又乱另一半。这个问题的排查顺序是:先看 JSP 头部的pageEncoding="UTF-8",再看数据库连接 URL 里有没有characterEncoding=utf8mb4,最后看 Tomcat 的请求过滤器。

解决:在 web.xml 里加一个 Spring 提供的字符编码过滤器:

<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> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

forceEncoding设为 true 是关键,它会同时覆盖请求和响应的编码,只配这一处就能解决大半乱码。

5.3 数据库连不上:MySQL 8.0 驱动类名和时区

现象:启动或第一次访问接口时报Caused by: java.sql.SQLException: The server time zone value ...或者ClassNotFoundException: com.mysql.jdbc.Driver。

原因:绝大多数是 MySQL 版本和驱动不匹配。老源码用的驱动类是com.mysql.jdbc.Driver,这个类在 MySQL 8.0 的驱动包里已经被移除,改成了com.mysql.cj.jdbc.Driver;另外 8.0 强制要求 URL 带时区参数。

解决:按 2.3 里的 jdbc.properties 改一遍。驱动类名换成带cj的那个,URL 末尾加serverTimezone=Asia/Shanghai,用户名密码换成你自己的。如果还连不上,先用 Navicat 或命令行单独测一下localhost:3306能不能通,排除 MySQL 服务本身没启动或者端口被占用的问题。端口冲突的经典特征是没有报 driver 错误,而是报Connection refused。

5.4 下单成功但库存没减:事务和 UPDATE 条件

现象:页面上订单生成了,购物车也清空了,但去数据库看t_product.stock还是原数,或者商品卖超了库存变成负数。

原因:两个。一是 Service 方法上漏了@Transactional,下单过程中某一步失败后,订单回滚了但库存扣减没有,或者反过来;二是库存扣减的 SQL 写成了set stock = stock - 1不带stock >= quantity条件,两次并发下单同时读到 stock=1,都以为还能卖,结果库存被扣成负数。

解决:把@Transactional(rollbackFor = Exception.class)加到下单方法上,并把扣库存 SQL 改成第 4.4 节那种带条件判断的版本。改完之后做个验证:把某商品库存改成 1,用两个浏览器同时下单 1 件,看最终库存是不是 0 而不是 -1。这一个并发场景讲清楚,答辩的分数能拉开一大截。

5.5 上传的图片重启后消失:Tomcat 虚拟路径

现象:商品图片在开发环境传上去能显示,但每次重启 Tomcat 或重新部署,图片就 404,数据库里记录的路径还在。

原因:IDEA 里以 exploded 方式部署时,上传文件默认写到了 Tomcat 临时发布目录下,这个目录每次部署都会被清空重建。文件一旦写到 target 临时包里,生命周期就跟着一次部署走,重启即消失。

解决:最常见的做法是把上传目录映射到项目外部的固定磁盘路径。比如在application.properties或配置类里定义一个upload.path=/Users/you/shop-upload/,上传时用这个绝对路径保存,访问时通过 Tomcat 的虚拟路径映射,把/upload/**指向本地那个目录。IDEA 里也可以直接配置 Deployment 的外部源(External Source),把本地目录挂到/upload上下文下。要点就一句:上传文件永远不要落到项目的 target 或 webapp 里,否则就是一次性的。

5.6 分页页码对不上:LIMIT 参数的顺序陷阱

现象:点第 2 页,出来的数据是第 1 页的内容,或者第一页就重复,翻到底多出一堆空白页。

原因:MySQL 的LIMIT支持两种写法:LIMIT offset, size和LIMIT size OFFSET offset。很多源码手写 SQL 时用的是LIMIT #{size}, #{offset},参数顺序反了,宏替换之后查出来的自然就是错乱的。另一种情况是 JSP 里拿到的page参数是字符串,跟整数做运算时被拼成了错误的偏移量。

解决:统一 SQL 写成LIMIT #{offset}, #{size},并保证 Controller 里offset = (page - 1) * size。如果项目用的是 PageHelper 插件,就不用关心这些,因为插件会自己拼 SQL;但手写 LIMIT 时,顺序和偏移量计算要作为一组来检查,单看哪边都对,合起来就错。

6. 从能跑到能演示:上线前必做的优化和自查清单

6.1 把默认连接池换成 Druid:参数怎么调

SSM 老项目大多配置的是 C3P0 或 DBCP,性能上不是不能用,但始终不太透明。我会把数据源统一换成 Druid,原因就是它的连接池状态可在页面监控,方便答辩时展示。在 spring-mybatis.xml 里替换数据源:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> <property name="maxWait" value="6000"/> <property name="validationQuery" value="SELECT 1"/> </bean>

参数就调三个重点:initialSize是启动时建立的连接数,一般 5 足够;maxActive是并发峰值能撑住的连接上限,毕设项目 20 绰绰有余;maxWait是拿不到连接时最长等待毫秒数,6000 就合理。其他高级参数比如防 SQL 注入的 filter,新手不建议乱加,加了反而看不懂监控页的数据。

6.2 验收/答辩前的自测清单

我把这几年帮人调试项目踩过的坑,整理成一张几分钟就能过完一遍的检查表:

检查项预期结果
Tomcat 访问地址http://localhost:8080/shop/首页正常加载,无 404
数据库版本匹配驱动类名和时区参数与你的 MySQL 5.7 / 8.0 对应
演示账号admin / 123456 能登录,登录后右上角显示昵称
加购流程商品可加购、重复加购数量累加、购物车总额正确
下单与库存下单成功生成订单号和订单项,t_product.stock减少
异常处理库存设为 1 后连续下单两次,第二次提示库存不足
中文显示JSP、商品名、订单收件人信息全部正常无乱码

每一条都对应本文前面解过的坑。最后说一句过来人的话:我见过太多人栽在同一个地方——拿到源码先急着点启动,报错后就开始盲改,改完还是报错,最后才发现是数据库脚本没执行,或者 Tomcat context 路径配错。把环境一项项列出来核对,比盯着控制台日志猜一个小时管用得多。这套流程我帮别人调过不下十次,属实能救急,希望帮到你,让你把省下来的时间花在真正能加分的设计上。

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

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

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

立即咨询