ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaWeb商城项目源码实践:SSM架构、数据库设计与避坑指南

JavaWeb商城项目源码实践:SSM架构、数据库设计与避坑指南 简介这是一份基于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 购物商城”这个名字在不同人手里可能指向三种完全不同的实现最早的纯 JSPServletJDBC中间几年铺天盖地的 SSMSpringSpringMVCMyBatis以及现在越来越常见的 Spring Boot。纯 JSPServlet 的好处是贴近课本你能看清 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一个接口配一个 XMLSQL 都集中在 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.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/shop_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456如果你的 MySQL 是 5.7驱动类名可以继续用com.mysql.jdbc.Driver如果是 8.0必须用com.mysql.cj.jdbc.Driver并且 URL 里一定要带serverTimezoneAsia/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.10.2 不等于 0.3做过支付的都知道这是血泪教训。购物车表t_cart_item连接用户和商品核心是(user_id, product_id)的联合唯一键。这个设计能从数据库层面保证同一个用户重复加购同一件商品时记录只有一条数量累加而不是插入多条省去大量额外判断。订单表t_order负责记录整单信息包括总额、状态、收件人。这里有个设计要点收件人姓名、电话、地址要冗余在订单表里而不是下单时临时去查用户表。因为用户以后可能修改自己的收货地址但历史订单必须保留下单那一刻的收件信息。订单项表t_order_item同理商品名称、图片、价格都要以快照形式存一份。商品改价或者改名之后订单里显示的仍然是成交时的数据。3.2 建表 SQLMySQL 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 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名, sort INT DEFAULT 0 COMMENT 排序值,越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 idfindByUsername resultTypeUser 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?error1; } session.setAttribute(loginUser, user); return redirect:/product/list; }这个方法的逻辑很直白登录成功就放一个loginUser到 Session 里登录失败就带个error1参数回到登录页。userService.login内部做的事情是先按用户名查出用户再把输入密码做 MD5 跟查出来的密文比对。参数username、password是 SpringMVC 自动从请求表单绑定的表单里的 name 属性必须跟它们同名这是新手最容易忽略的坑。有了 Session 之后未登录用户不能访问购物车和订单页面需要一层拦截器。常见的写法是实现HandlerInterceptor在preHandle里判断session.getAttribute(loginUser)是否为空为空就重定向到登录页。这个机制答辩时经常被问能说清楚“Session 是服务端保存的会话状态Cookie 里只存了 sessionId”这句话基本就算过关。4.2 商品列表与分页手写 LIMIT 并算清总页数商品列表是商城首页的主体不能把所有商品一次性查出渲染到页面上数据量大了页面会非常慢。分页的标准做法是 SQL 里用 LIMIT 做物理分页只查当前页需要的十几条数据。select idfindPage resultTypeProduct SELECT id, category_id, name, subtitle, main_image, price FROM t_product WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC LIMIT #{offset}, #{size} /selectGetMapping(/list) public String list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 8) Integer size, Integer categoryId, Model model) { int offset (page - 1) * size; ListProduct 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) { ListCartItem 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 iddecreaseStock UPDATE t_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} /updateWHERE 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 头部的pageEncodingUTF-8再看数据库连接 URL 里有没有characterEncodingutf8mb4最后看 Tomcat 的请求过滤器。解决在 web.xml 里加一个 Spring 提供的字符编码过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncoding设为 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 末尾加serverTimezoneAsia/Shanghai用户名密码换成你自己的。如果还连不上先用 Navicat 或命令行单独测一下localhost:3306能不能通排除 MySQL 服务本身没启动或者端口被占用的问题。端口冲突的经典特征是没有报 driver 错误而是报Connection refused。5.4 下单成功但库存没减事务和 UPDATE 条件现象页面上订单生成了购物车也清空了但去数据库看t_product.stock还是原数或者商品卖超了库存变成负数。原因两个。一是 Service 方法上漏了Transactional下单过程中某一步失败后订单回滚了但库存扣减没有或者反过来二是库存扣减的 SQL 写成了set stock stock - 1不带stock quantity条件两次并发下单同时读到 stock1都以为还能卖结果库存被扣成负数。解决把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 iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value6000/ property namevalidationQuery valueSELECT 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 路径配错。把环境一项项列出来核对比盯着控制台日志猜一个小时管用得多。这套流程我帮别人调过不下十次属实能救急希望帮到你让你把省下来的时间花在真正能加分的设计上。本文还有配套的精品资源点击获取
返回列表