☰
SSM+JSP母婴电商网站实战:分层架构、数据库设计与避坑指南
2026/10/14 2:45:56 网站建设 项目流程

简介:面向Java毕业设计学生的一套基于SSM与JSP的母婴用品网站项目包,包含完整项目源码、数据库脚本和部署工具,覆盖商品展示、购物车、订单管理等电商核心模块。代码中加入了详细注释,新手也能读懂关键逻辑,适合作为毕业设计、课程设计或期末大作业的参考蓝本。压缩包内共有1371个文件,整体约25.45MB。主要构成包括169个JSP页面、127个Java类、367个JavaScript脚本,以及大量CSS、HTML、图片素材,完整覆盖前端展示与后端业务处理;另附MySQL建表脚本和部署说明,配合IDEA、Tomcat、Maven环境即可快速运行。项目经过严格调试,界面简洁、操作顺畅,同时按控制层、业务层、数据访问层分层组织,目录结构直观,便于二次开发。目前已调试无误,可直接用于答辩演示。资源包内还收录了开发工具与教程文档,能帮助节省从零搭建环境的时间。目前已有65人学习下载,对于需要快速完成一个“前台电商+后台管理”题目的学生,是一份省时省力的高质量参考。

1. 这不是“过时项目”:SSM+JSP 母婴用品网站为什么还值得认真做

看到这个标题,你大概已经在毕业设计的选题堆里翻过一轮了。SSM+JSP 听起来像上一个时代的组合,但恰恰是它,最适合在几个月内把一个完整业务闭环做出来并讲清楚。母婴用品网站这个外壳并不重要,真正重要的是里面的商品管理、用户登录、购物车结算、订单流转这一整套流程,正好覆盖了 Spring、SpringMVC、MyBatis 三门课的核心考点。

这个项目能解决的实际问题很直接:让你从“会写 Servlet”跨到“能搭一个分层 Web 项目”,并且数据库设计、接口调用、页面渲染都能在答辩时拿出实物。适合两类人:一类是时间紧、需要快速交付可运行系统的毕业生,另一类是刚学完框架、想找一个小而全的案例练手的人。

我给你的建议是:不要一上来就解压源码开 IDEA,那样你只会得到一个跑起来但讲不清的“黑匣子”。先把技术组合的来龙去脉摸透,再动手改代码,最后用一套验收清单确认自己真能接住老师的提问。

2. 先搞清楚技术组合:为什么是 SSM+JSP,而不是 SpringBoot+Vue

2.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 渲染”。

为什么不直接用 SpringBoot+Vue?不是不能,而是单人在短周期内做毕设,前后端分离意味着你要同时维护两个工程、处理跨域、定义接口文档、解决前端构建环境,任何一个环节出问题都在消耗有限的答辩准备时间。SSM+JSP 的前后端一体化部署,意味着你只需要一个 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 -version

JAVA_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.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/baby_mall?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

如果你是 MySQL 8.0,第一行的驱动类要改成 com.mysql.cj.jdbc.Driver,这是官方在 8.0 系列里推荐的驱动类名。URL 里的 characterEncoding=utf8 表示以 UTF-8 字符集传输数据,serverTimezone=Asia/Shanghai 是必须加的,不加的话高版本驱动会拿服务器默认时区去算,经常报时间差错误。useSSL=false 是关掉 SSL 加密握手,本地开发没必要开启。

提示:如果连接串忘了写 useSSL=false,MySQL 8.0 启动时会有一大段 SSL 告警,不影响运行但很吓人;真正会报错的往往是没写 serverTimezone。

Spring 加载这个配置文件时,要确认 spring-context.xml 里用了<context:property-placeholder location="classpath: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); List<Product> products = productService.listByCategory(categoryId); PageInfo<Product> 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 id="listByCategory" resultType="com.example.entity.Product"> SELECT id, name, price, main_image, category_id FROM mall_product <where> <if test="categoryId != 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,如果你在事务方法里抛了个受检查异常,事务不会回滚,订单插进去一半就是脏数据。所以我在实际项目里习惯显式指定 rollbackFor:

@Transactional(rollbackFor = Exception.class) public Long createOrder(Order order, List<CartItem> 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 id="deductStock"> 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 name="mapUnderscoreToCamelCase" value="true"/> </settings>

开启后,数据表里的 category_id 会自动映射到实体类的 categoryId 属性,user_name 映射到 userName,不需要你写任何映射规则。这是 MyBatis 项目里最省事的配置之一,也几乎是毕设项目的标配。如果没开这个开关,你写的 SQL 里的查询列就得用 AS 起别名,否则结果集里全是 null,排查半天才发现是映射没对上。

只有那些查询结果里带有额外字段、需要做关联映射的场景,才值得手写 resultMap。比如查询订单明细时要把关联的商品图片也查出来,而订单明细表里并没有图片字段:

<resultMap id="OrderItemWithProductMap" type="OrderItem"> <id column="id" property="id"/> <result column="product_name" property="productName"/> <result column="quantity" property="quantity"/> <result column="price" property="price"/> <association property="product" javaType="Product"> <id column="product_id" property="id"/> <result column="main_image" property="mainImage"/> </association> </resultMap> <select id="selectItemsWithProduct" resultMap="OrderItemWithProductMap"> 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. SSM+JSP 母婴网站避坑指南:本地能跑不等于没有问题

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-package="com.example"/>只扫到了 controller,没扫到 service,结果依赖注入失败,异常在运行时才暴露,表现很奇怪。第三个是插入购物车记录时 MyBatis 没有开启主键回填,购物车表里 user_id 和 product_id 都是成功写入的,但 order_item 表需要拿主键去关联时拿到的是 null。

解决:把过滤器配置为第一个过滤器,并且设置 encoding 为 UTF-8。检查 spring-context.xml 里包扫描的 base-package,确保 Controller、Service、Mapper 都在一个根包下。插入语句里显式配置主键回填:

<insert id="insert" parameterType="CartItem" useGeneratedKeys="true" keyProperty="id"> INSERT INTO mall_cart(user_id, product_id, quantity) VALUES (#{userId}, #{productId}, #{quantity}) </insert>

useGeneratedKeys 告诉 MyBatis 使用JDBC的 getGeneratedKeys 方法拿到自增主键,keyProperty 指定把值赋给对象的哪个属性。不配这个属性,插入后对象的 id 还是 null,后续拿这个 id 做关联操作就会出问题。排查思路是:先在数据库里手动执行一次 INSERT,确认中文能正常写入;再在 Service 层打日志看参数是否完整传过来;最后看插入后对象 id 是否被赋值。

5.3 部署到服务器后图片打不开:上传目录与项目目录分离

现象:本地开发时商品图片显示正常,但把项目打成 war 包丢到服务器上之后,图片全部 404,或者重启一次 Tomcat 图片就没了。

原因:开发时写入的图片路径是request.getServletContext().getRealPath("/upload"),这个路径在开发工具里指向项目的 webapp 目录,看起来能用。但打成 war 包部署后,Tomcat 会把 war 解压到临时目录,这个目录会在重启时被清理,你上传的图片跟着一起没了。这是“把运行时数据放在程序目录里”的经典反模式,运行时产生的文件必须落在程序目录之外。

解决:在服务器上建一个独立目录,比如 /data/upload,SpringMVC 里把上传目录改成配置项,同时配置静态资源映射:

<mvc:resources mapping="/upload/**" location="file:/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 是否包含characterEncoding=utf8,没有就加上。第三层,打开 JSP 文件,看顶部声明是否是:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-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 文件里给每张表写上设计注释,把自己当时的取舍理由留在注释里——这样无论什么时候被问到,你都能说出设计的意图,而不是靠临场硬猜。这个习惯希望帮到你。

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

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

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

立即咨询