SSM+JSP母婴电商网站实战:分层架构、数据库设计与避坑指南
简介面向Java毕业设计学生的一套基于SSM与JSP的母婴用品网站项目包包含完整项目源码、数据库脚本和部署工具覆盖商品展示、购物车、订单管理等电商核心模块。代码中加入了详细注释新手也能读懂关键逻辑适合作为毕业设计、课程设计或期末大作业的参考蓝本。压缩包内共有1371个文件整体约25.45MB。主要构成包括169个JSP页面、127个Java类、367个JavaScript脚本以及大量CSS、HTML、图片素材完整覆盖前端展示与后端业务处理另附MySQL建表脚本和部署说明配合IDEA、Tomcat、Maven环境即可快速运行。项目经过严格调试界面简洁、操作顺畅同时按控制层、业务层、数据访问层分层组织目录结构直观便于二次开发。目前已调试无误可直接用于答辩演示。资源包内还收录了开发工具与教程文档能帮助节省从零搭建环境的时间。目前已有65人学习下载对于需要快速完成一个“前台电商后台管理”题目的学生是一份省时省力的高质量参考。1. 这不是“过时项目”SSMJSP 母婴用品网站为什么还值得认真做看到这个标题你大概已经在毕业设计的选题堆里翻过一轮了。SSMJSP 听起来像上一个时代的组合但恰恰是它最适合在几个月内把一个完整业务闭环做出来并讲清楚。母婴用品网站这个外壳并不重要真正重要的是里面的商品管理、用户登录、购物车结算、订单流转这一整套流程正好覆盖了 Spring、SpringMVC、MyBatis 三门课的核心考点。这个项目能解决的实际问题很直接让你从“会写 Servlet”跨到“能搭一个分层 Web 项目”并且数据库设计、接口调用、页面渲染都能在答辩时拿出实物。适合两类人一类是时间紧、需要快速交付可运行系统的毕业生另一类是刚学完框架、想找一个小而全的案例练手的人。我给你的建议是不要一上来就解压源码开 IDEA那样你只会得到一个跑起来但讲不清的“黑匣子”。先把技术组合的来龙去脉摸透再动手改代码最后用一套验收清单确认自己真能接住老师的提问。2. 先搞清楚技术组合为什么是 SSMJSP而不是 SpringBootVue2.1 SSM 与 JSP 的职责划分谁在干活谁只是画页面很多第一次接触这个组合的人最大的误区是把 JSP 当成“写 Java 的地方”。翻开旧项目你会发现登录逻辑写在 JSP 里数据库查询也写在 JSP 里页面一多就变成一团乱线。其实在 SSM 架构里JSP 的职责只有一个把后台准备好的数据渲染成 HTML。它属于视图层不应该出现业务判断更不应该出现 JDBC 代码。职责划分应该是这样的Spring 容器统一管理 Service、Mapper 这些对象解决对象创建和依赖注入的问题SpringMVC 负责接收浏览器请求、解析参数、调用 Service、把返回值交给视图MyBatis 负责把 Java 方法和 SQL 语句做映射处理结果集到对象的转换。整个请求链路是“JSP 页面发起请求 → Controller 接收参数 → Service 处理业务 → Mapper 执行 SQL → 返回 ModelAndView → JSP 渲染”。为什么不直接用 SpringBootVue不是不能而是单人在短周期内做毕设前后端分离意味着你要同时维护两个工程、处理跨域、定义接口文档、解决前端构建环境任何一个环节出问题都在消耗有限的答辩准备时间。SSMJSP 的前后端一体化部署意味着你只需要一个 Tomcat 就能把整个系统跑起来调试时打断点也少绕一层。2.2 环境准备JDK、Tomcat、MySQL 的版本搭配与最小配置环境搭错是这类项目的第一个拦路虎。我见过有人用 JDK17 去跑一个基于 JDK8 编译的老框架结果 Tomcat 启动到一半就报 UnsupportedClassVersionError然后开始怀疑代码有问题。实际上框架本身对高版本 JDK 的支持并不好所以最稳妥的组合是 JDK8 系列搭配 Tomcat 8.5 或 9.0 系列。Tomcat 版本也要留意Tomcat 10 之后把包名从 javax.* 换成了 jakarta.*而 SSM 老项目里全是 javax.servlet 开头直接部署会编译不过。所以别在 Tomcat 版本上追求新8.5/9.0 对这套技术栈是经验上最顺的。Maven 用 3.6 系列就够它能正常解析 Spring 相关依赖没必要上最新版。环境变量配置在 Linux/macOS 下是常见的做法Windows 用户在系统属性里配置同样的两项即可export JAVA_HOME/usr/local/jdk1.8.0_xxx export PATH$JAVA_HOME/bin:$PATH export MAVEN_HOME/usr/local/apache-maven-3.6.x export PATH$MAVEN_HOME/bin:$PATH java -version mvn -versionJAVA_HOME 指向 JDK 安装根目录不是 bin 目录。PATH 里加上 bin 是为了让命令行直接识别 java 和 mvn 命令。最后用 java -version 和 mvn -version 验证如果输出的版本和你安装的一致说明环境没问题。这里有一个实际教训如果你的电脑之前装过多个 JDK最好在 IDEA 的项目结构里单独指定 Project SDK 为 1.8同时把 Tomcat 的 JRE 路径也指到 1.8避免 IDE 默认用了高版本。2.3 数据库初始化与 jdbc.properties两种 MySQL 版本下的连接串差异数据库是这个项目的重头戏。MySQL 5.7 和 8.0 都能用但连接配置有明显区别。建库的时候就要指定字符集否则后面的中文乱码会让你排查到怀疑人生。我一般用这条 SQL 建库CREATE DATABASE IF NOT EXISTS baby_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4 比 utf8 多支持了表情符号和一些特殊字符商品名里万一有特殊符号也不会报错。collate 选 general_ci 表示不区分大小写排序对中文环境足够用。建好库之后JDBC 连接串写在 src/main/resources/jdbc.properties 里这是整个项目第一个要核对的地方jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/baby_mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456如果你是 MySQL 8.0第一行的驱动类要改成 com.mysql.cj.jdbc.Driver这是官方在 8.0 系列里推荐的驱动类名。URL 里的 characterEncodingutf8 表示以 UTF-8 字符集传输数据serverTimezoneAsia/Shanghai 是必须加的不加的话高版本驱动会拿服务器默认时区去算经常报时间差错误。useSSLfalse 是关掉 SSL 加密握手本地开发没必要开启。提示如果连接串忘了写 useSSLfalseMySQL 8.0 启动时会有一大段 SSL 告警不影响运行但很吓人真正会报错的往往是没写 serverTimezone。Spring 加载这个配置文件时要确认 spring-context.xml 里用了context:property-placeholder locationclasspath:jdbc.properties/并且在 dataSource 的 bean 里用 ${jdbc.driver} 这种占位符取值。很多项目本地跑不通不是代码问题而是配置文件里的用户名密码和本机 MySQL 对不上。拿到一个陌生源码包时第一件事是先看配置文件再启动项目。3. 把母婴用品网站拆成可落地的功能模块分层设计与核心代码3.1 用户模块注册登录、MD5加盐和 Session 状态维持用户模块是所有电商项目的地基它解决的是“谁在使用系统”的问题。注册时要检查用户名是否重复登录时要校验密码登录成功后把用户对象放进 Session之后访问购物车、订单等页面时再判断用户是否登录。SSM 里这个模块能很好地演示三层调用关系。密码存储不能是明文这是个常识。但毕设项目里也没必要上 BCrypt常见做法是 MD5 加固定盐。虽然固定盐的安全强度有限但至少能防止别人直接看到数据库里的原始密码答辩时你能说出“用盐避免彩虹表直接命中”就已经及格了。如果想让方案更完整可以在用户表里加一个 salt 字段每个用户注册时生成随机盐登录时用同一个盐去校验。登录的核心代码在 Controller 层代码量不大但很有代表性Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { String encryptedPwd DigestUtils.md5DigestAsHex( (password SALT).getBytes(StandardCharsets.UTF_8)); User user userService.login(username, encryptedPwd); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/index; } }这里 Controller 只做参数接收和视图跳转密码加密放在 Controller 里其实不算理想更合理的做法是放到 Service 层让 Controller 保持轻量。DigestUtils.md5DigestAsHex来自 Spring 自带的工具包它接收字节数组所以要把字符串先转成 UTF-8 字节再传入。SALT 是一个常量字符串在真实项目里会是一个随机盐但毕设里统一盐省去了存盐字段的复杂度。用户表密码字段要设计成 varchar(32)因为 MD5 输出固定是 32 位十六进制字符。有人设计成 varchar(20)注册时密码一加密就超长SQL 插入直接报错。这个细节会在 5.1 节的避坑里再展开。3.2 商品与分类模块首页导航、分页列表和详情页的数据流母婴用品网站的商品模块一般包含分类导航、商品列表、商品详情三个页面。首页顶部是分类菜单点击某个分类进入该分类下的商品列表再点击商品卡片进入详情。数据流是“页面携带参数 → Controller 接收 → Service 查询 → 返回 Model → JSP 渲染”。列表页必须考虑分页因为商品数量一多一次性查出来会让页面卡顿。SSM 项目里 PageHelper 是使用最广的分页插件用法是在查询前调用静态方法启动分页GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(required false) Integer categoryId, Model model) { PageHelper.startPage(pageNum, 12); ListProduct products productService.listByCategory(categoryId); PageInfoProduct pageInfo new PageInfo(products); model.addAttribute(pageInfo, pageInfo); model.addAttribute(categoryId, categoryId); return productList; }PageHelper.startPage(pageNum, 12)的意思是只对下一条查询生效查询结束后自动拼接 limit 语句pageNum 是当前页码12 是每页条数。PageInfo会把查询到的 List 包装成分页对象它内部包含了总条数、总页数、上一页、下一页这些信息JSP 里直接通过${pageInfo.total}和${pageInfo.pages}取用。categoryId是可选的传了就在 SQL 里拼 where 条件不传就查询全部商品。对应的 MyBatis 查询语句里要写动态 SQL使用where标签可以自动去掉多余的 and 关键字select idlistByCategory resultTypecom.example.entity.Product SELECT id, name, price, main_image, category_id FROM mall_product where if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC /select这个if是 MyBatis 动态 SQL 最常用的分支它能根据参数是否为空决定是否拼接条件。这里要注意一个细节如果多个条件都用if拼接第二个条件前面的 and 必须写在if标签内部否则条件为空时 SQL 会残留一个多余的 and导致查询失败。商品详情的逻辑相对简单就是根据 id 查一条数据放到 Model 里。详情页面的图片加载坑比较多因为商品表里存的通常是一个相对路径而部署环境不同图片解析方式也不一样。这个问题我留到第 5 章专门说。3.3 购物车与订单模块事务边界和防超卖库存扣减购物车和订单是母婴用品网站里业务最密集的部分也是答辩时最容易被追问的地方。购物车表记录用户和商品的对应关系以及数量下单时把购物车里选中的商品插入订单表和订单明细表。报表层面这就是一个典型的“多表关联 事务一致性”案例。先看下单的核心逻辑。这里有一个非常容易踩的坑Transactional 注解默认只回滚 RuntimeException 和 Error如果你在事务方法里抛了个受检查异常事务不会回滚订单插进去一半就是脏数据。所以我在实际项目里习惯显式指定 rollbackForTransactional(rollbackFor Exception.class) public Long createOrder(Order order, ListCartItem cartItems) { order.setOrderNo(MO System.currentTimeMillis()); order.setStatus(0); orderMapper.insert(order); for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(item.getPrice()); orderItemMapper.insert(orderItem); int rows productMapper.deductStock( item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足商品ID item.getProductId()); } } cartMapper.deleteByUserId(order.getUserId()); return order.getId(); }这个方法的执行顺序是先生成订单主记录拿到自增主键再循环插入订单明细每插入一条明细就扣减一次库存全部成功后才清空购物车。任何一步失败都会触发事务回滚保证不会出现“订单有了但库存没扣”或者“库存扣了但订单没了”的数据不一致。库存扣减的 SQL 是整个下单流程的关键update iddeductStock UPDATE mall_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} /update条件里加stock #{quantity}是一种乐观锁思路。如果没有这个条件两个请求同时下单都读到库存 5一个扣 3 一个扣 4最后库存会变成 -2。而带上这个条件后数据库的行锁会让第二个更新等待等第一个提交后第二个再执行时发现 stock 已经不满足条件影响行数为 0代码里就抛出异常回滚。这一句 SQL 就能在答辩时解释“如何防止超卖”比泛泛说“用了事务”有说服力得多。下单完成后订单主表的状态字段用数字表示0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。不要用字符串比如“未付款”因为字符串状态容易写错且存储空间更大。这个状态设计在 4.1 节的表结构里会继续讲。4. 数据库设计是这道毕设题的灵魂核心表结构与数据初始化4.1 母婴用品业务需要哪些表从用户表到订单明细表如果问一个 SSM 毕业设计项目哪里最容易露怯十个人里有八个会说是数据库设计。母婴用品网站的数据库并不复杂但五脏俱全。我按业务依赖顺序给出完整表清单配套的表结构如下表名核心字段作用mall_userid, username, password, nickname, phone前台用户账号mall_addressid, user_id, receiver, phone, detail收货地址mall_categoryid, name, parent_id, sort商品分类支持两级mall_productid, category_id, name, price, stock, main_image, status商品主表mall_cartid, user_id, product_id, quantity购物车mall_orderid, order_no, user_id, total_price, status, create_time订单主表mall_order_itemid, order_id, product_id, product_name, price, quantity订单明细用户表不要放地址字段因为一个用户可能有多条收货地址下单时选一个地址放子表可以记录历史地址。订单表里冗余一个 total_price 是合理的因为明细表里的商品价格可能会因促销或后台改价而变化主表保存下单那一刻的总额才准确。订单明细表冗余 product_name 和 price 也是一个常见设计防止商品以后改了名或下了架历史订单仍然能显示正确的商品信息。建表时外键关系建议用逻辑关联而不是物理外键。也就是说order 表里的 user_id 字段是普通索引但不要真的去建 FOREIGN KEY 约束。理由很务实物理外键会在插入、删除时产生额外校验成本而毕设里操作数据经常要手工改 SQL如果外键约束太严删数据时会反复报错打断思路。你在答辩时可以说“用逻辑外键保持灵活查询时通过 join 关联”这是被广泛接受的做法。核心订单表的建表语句值得仔细看CREATE TABLE mall_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成 4已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;amount 字段选了 DECIMAL(10,2) 而不是 FLOAT 或 DOUBLE这是金融数据的基本常识浮点数在二进制里无法精确表示十进制小数累加时会出现 0.1 0.2 不等于 0.3 的问题。DECIMAL 是字符串存储的数字精度可控。create_time 用 DEFAULT CURRENT_TIMESTAMP 可以让数据库自动记录下单时间少写一行 Java 代码。4.2 MyBatis 映射文件与驼峰映射少写一半 resultMap很多新手在写 MyBatis 时有个坏习惯每一张表都先写一个完整的 resultMap把每个字段的 column 和 property 都列一遍。其实 MyBatis 有一个全局开关叫驼峰映射能把数据库字段的下划线风格自动转成 Java 属性的驼峰风格。在 mybatis-config.xml 里加一行配置settings setting namemapUnderscoreToCamelCase valuetrue/ /settings开启后数据表里的 category_id 会自动映射到实体类的 categoryId 属性user_name 映射到 userName不需要你写任何映射规则。这是 MyBatis 项目里最省事的配置之一也几乎是毕设项目的标配。如果没开这个开关你写的 SQL 里的查询列就得用 AS 起别名否则结果集里全是 null排查半天才发现是映射没对上。只有那些查询结果里带有额外字段、需要做关联映射的场景才值得手写 resultMap。比如查询订单明细时要把关联的商品图片也查出来而订单明细表里并没有图片字段resultMap idOrderItemWithProductMap typeOrderItem id columnid propertyid/ result columnproduct_name propertyproductName/ result columnquantity propertyquantity/ result columnprice propertyprice/ association propertyproduct javaTypeProduct id columnproduct_id propertyid/ result columnmain_image propertymainImage/ /association /resultMap select idselectItemsWithProduct resultMapOrderItemWithProductMap SELECT oi.id, oi.product_name, oi.quantity, oi.price, p.main_image FROM mall_order_item oi LEFT JOIN mall_product p ON oi.product_id p.id WHERE oi.order_id #{orderId} /select这个例子演示的是“订单明细中嵌入商品信息”的典型关联场景。association用来处理“多个明细对应一个商品”的引用关系它会创建一个嵌套的 Product 对象。对于毕设来说能用出一个 association 就已经说明你理解对象关系和 SQL 关联查询了不用刻意去写复杂的 collection 嵌套。4.3 初始化数据注意点图片路径、价格单位和分类层级数据库里放什么初始数据决定了项目第一次启动时页面好不好看。很多项目导入 SQL 后能跑但首页空空如也原因是初始化 SQL 里没有商品图片或者图片路径指向了本机绝对路径。母婴用品的商品图片路径有一个原则只存相对路径不存 http:// 开头的完整地址。我一般会在 mall_product 表里把 main_image 存成/upload/products/xxx.jpg这种形式然后在 JSP 页面用${pageContext.request.contextPath}${product.mainImage}去拼接出完整访问路径。这样做的好处是换一台电脑部署时只要把图片目录整体拷贝过去路径不用改。如果你存了http://localhost:8080/upload/xxx.jpg别人拿到项目后访问就变成访问他的 localhost图片直接裂掉。商品价格字段用 DECIMAL(10,2)单价不要用元单位存在数据库里。你可以把价格理解为“元”的十进制数查询时直接显示就行。分类层级用 parent_id 实现两级分类parent_id 为 0 表示一级分类比如“奶粉”“纸尿裤”一级分类下的子分类的 parent_id 指向对应的一级分类 id。这个设计比单独靠 name 区分层级清晰得多也给后续做“分类下子分类数量统计”留了余地。初始化用户表的密码一定要存加密后的值。你写了注册功能又想在测试时直接用 SQL 造一个管理员账号那 SQL 里的 password 字段就不能是明文 123456否则代码里用 MD5 加密后去查永远匹配不上。先用代码或命令行工具算好 123456 的 MD5 值再放进 INSERT 语句里。5. SSMJSP 母婴网站避坑指南本地能跑不等于没有问题5.1 页面能打开却登录不了Session 失效与 Cookie 路径的坑现象项目启动正常首页能打开但输入正确的用户名密码后页面跳回登录页或者登录成功后点几下又弹出登录框。原因最常见的是部署路径带了项目名比如访问地址是http://localhost:8080/baby_mall/而登录成功后重定向写的是redirect:/index这个路径不带项目名Tomcat 会把它解析成http://localhost:8080/index于是请求根本到不了你的项目上下文里Session 里的登录状态自然无效。另一个原因是登录成功后的页面里用了相对路径跳转浏览器去请求了错误地址。解决所有 Controller 里的重定向都写redirect:/项目名/xxx或者更稳妥的写法是redirect:${pageContext.request.contextPath}/index。在 JSP 里也不用写死/baby_mall/login统一用${pageContext.request.contextPath}拼接。排查时可以按一下 F12 看网络请求的 URL如果跳过去的地址里少了项目名原因基本就是这个。还要检查 web.xml 里的欢迎页路径是否指向/index如果欢迎页写的是静态 JSP 文件登录拦截逻辑会跳过。5.2 添加购物车后数量对不上过滤器顺序、包扫描和主键回填现象购物车页面能打开但点击“加入购物车”后列表里总是同一个商品变成两条记录或者数量永远是 1要么就是添加后数据库里根本没写入数据。原因这个现象背后有三个独立问题混在一起。第一个是 SpringMVC 的 CharacterEncodingFilter 没有配置导致中文商品名乱码Service 层用乱码名去查重每次都当成新商品。第二个是 Spring 的包扫描范围不对比如context:component-scan base-packagecom.example/只扫到了 controller没扫到 service结果依赖注入失败异常在运行时才暴露表现很奇怪。第三个是插入购物车记录时 MyBatis 没有开启主键回填购物车表里 user_id 和 product_id 都是成功写入的但 order_item 表需要拿主键去关联时拿到的是 null。解决把过滤器配置为第一个过滤器并且设置 encoding 为 UTF-8。检查 spring-context.xml 里包扫描的 base-package确保 Controller、Service、Mapper 都在一个根包下。插入语句里显式配置主键回填insert idinsert parameterTypeCartItem useGeneratedKeystrue keyPropertyid INSERT INTO mall_cart(user_id, product_id, quantity) VALUES (#{userId}, #{productId}, #{quantity}) /insertuseGeneratedKeys 告诉 MyBatis 使用JDBC的 getGeneratedKeys 方法拿到自增主键keyProperty 指定把值赋给对象的哪个属性。不配这个属性插入后对象的 id 还是 null后续拿这个 id 做关联操作就会出问题。排查思路是先在数据库里手动执行一次 INSERT确认中文能正常写入再在 Service 层打日志看参数是否完整传过来最后看插入后对象 id 是否被赋值。5.3 部署到服务器后图片打不开上传目录与项目目录分离现象本地开发时商品图片显示正常但把项目打成 war 包丢到服务器上之后图片全部 404或者重启一次 Tomcat 图片就没了。原因开发时写入的图片路径是request.getServletContext().getRealPath(/upload)这个路径在开发工具里指向项目的 webapp 目录看起来能用。但打成 war 包部署后Tomcat 会把 war 解压到临时目录这个目录会在重启时被清理你上传的图片跟着一起没了。这是“把运行时数据放在程序目录里”的经典反模式运行时产生的文件必须落在程序目录之外。解决在服务器上建一个独立目录比如 /data/uploadSpringMVC 里把上传目录改成配置项同时配置静态资源映射mvc:resources mapping/upload/** locationfile:/data/upload/ /这句配置的意思是浏览器访问/upload/xxx.jpg时SpringMVC 直接去服务器磁盘的/data/upload/xxx.jpg找文件返回。这样上传文件不再经过 Tomcat 的部署目录重启不会丢失也不会在重新部署时被覆盖。商品表里存的 main_image 不需要改仍然存/upload/xxx.jpg只是它被映射到的磁盘位置变了。5.4 数据库中文乱码从建库到连接串的字符集三层排查现象通过 JSP 页面新增的商品名称显示为问号或者直接用 SQL 文件导入数据后项目里查出来的中文全是乱码。原因乱码问题几乎都是“写入时用一种编码读取时用另一种编码”造成的。这一条链路里有三个关卡数据库本身的字符集、JDBC 连接串里的 characterEncoding、JSP 文件里声明的 pageEncoding。任何一层不一致都会导致中文在某一个环节被转坏。解决按顺序做三层检查。第一层数据库中执行SHOW CREATE DATABASE baby_mall;确认是 utf8mb4如果是 latin1 就把建库语句里的字符集改掉。第二层检查 jdbc.properties 的 URL 是否包含characterEncodingutf8没有就加上。第三层打开 JSP 文件看顶部声明是否是% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %pageEncoding 负责告诉 JSP 编译器这个文件本身是什么编码contentType 告诉浏览器用什么编码解析响应内容。这两个必须一致且都写成 UTF-8。还有一个小坑如果页面里有中文是从数据库传上来的而页面本身没有加这一行声明浏览器会按默认编码解析出现“数据库对但页面乱”的假象。排查时先在浏览器里手动切换编码试试如果切换后正常就是页面响应头没写明编码优先补页面声明。6. 别急着交项目用一张验收清单和三个扩展点给自己加分项目跑起来之后你要做的不只是截几张图放进论文里而是站在答辩老师的角度重新走一遍。我给你一张平时自己验收 SSM 电商项目时用的清单照着过一遍基本能兜住 80% 的提问维度验收动作检查点用户侧注册一个新账号并登录用户名重复提示、错误密码提示是否正常商品侧从首页分类点进列表再进详情分页页码是否准确、图片是否正常加载订单侧加入购物车、下单、清空购物车库存是否扣减、订单明细是否完整部署侧打包成 war 放到另一台电脑图片是否还能打开、数据库连接是否可改验收的目的是帮你找到“本地能跑但换环境就废”的隐患。比如检查图片路径是不是相对路径、数据库连接配置是不是可替换、配置文件里有没有写死本机 IP。你可以在答辩前把项目清空所有数据重新执行一遍 SQL 初始化脚本再按用户流程走一遍能做到这一步说明你的交付物是可复现的这是很多项目拿不到高分的原因。如果还有余力我建议你加一个登录拦截器而不是在每个 Controller 里重复判断 Session。写一个 HandlerInterceptor在 preHandle 里判断 Session 是否为空未登录就重定向到登录页再在 spring-context.xml 里配置拦截路径为/cart/**和/order/**。这个改动大概几十行代码但能让代码结构上一个档次。另一个加分扩展是引入一个简单的数据统计页比如用 AdminLTE 模板或 ECharts 画一个“最近七天订单数量”的折线图。背后的查询也不复杂就是按天分组统计 mall_order 表里的数据但它能证明你不只是会写增删改查还能把数据变成有用的信息。答辩时间有限你只需要做那一个图表不用铺开。我之前指导一个 A 同学做类似项目时他功能全跑通了最后却被问了“数据库为什么没有物理外键”而卡壳。从那以后我养成了一个习惯在 SQL 文件里给每张表写上设计注释把自己当时的取舍理由留在注释里——这样无论什么时候被问到你都能说出设计的意图而不是靠临场硬猜。这个习惯希望帮到你。本文还有配套的精品资源点击获取