
1. 项目概述与核心功能拆解做毕设选了这个SSM物流快递管理系统说实话挺有意思的。市面上大多数电商、博客、学生管理系统的毕设项目都被人做烂了答辩老师一眼就能看出你是不是自己写的。物流快递这个选题相对稍微偏门一点业务场景清晰前后端交互节点多能展示的技术点也更立体——从权限控制、多角色业务流转、快递单状态机管理到JSP页面的数据渲染都有得写。这个项目本质上是给一家中小型快递网点做的一套内部管理系统。网点老板管理快递员、客户下单寄件、快递员接单派送、前台签收登记再加上基础的数据统计和管理员后台就是整个项目的核心闭环。技术栈上用SSMSpring SpringMVC MyBatis做主框架前端用JSP JavaScript JQuery Bootstrap数据库用MySQL开发工具是IDEA Maven Tomcat。这套组合是JavaWeb时代最经典的“教科书式配置”跑起来之后对理解整个JavaWeb请求链路非常有帮助。我最初拿到这个需求时第一反应是为什么不用SpringBoot但仔细想想毕设场景下SSM反而是更好的选择。原因有三其一SSM的手写配置能体现你对框架本质的理解SpringBoot把太多东西自动化掉之后回答不好“xxx是怎么生效的”其二SSM项目同步部署到Tomcat的war包模式在现场演示时更稳当不容易出现环境不一致的问题其三绝大多数学校的毕设评分标准里SSM仍然是主流档次用SSM完成一个完整系统属于“稳扎稳打不冒风险”的选择。整个系统拆成三个角色来看业务路径是这样的管理员网点老板员工管理、快递单管理、基础数据查看、统计报表。快递员查看待派送订单、更新派送状态、确认签收。客户前台/寄件人寄件下单、快递单号查询、物流轨迹查看。如果你就是来找能跑的源码复制粘贴完成任务那么这篇博文看到这里就可以直接去部署了。但如果答辩的时候不想被问倒建议跟着我下面几节的内容走一遍尤其是第3节和第4节的代码细节我尽量把“每句配置为什么这么写”讲清楚。前几年带过的学弟问得最多的就是“老师问AOP怎么回事”、“依赖注入有什么好处”这类框架底层问题——代码不熟可以背原理不熟是真的容易当场卡住。2. SSM核心原理与项目环境准备2.1 三个框架到底各自干了什么很多同学启动SSM项目时报错第一反应是去百度报错信息但报错信息的真正原因往往在于——你不清楚哪个框架管哪一段Dispatcher拦截了不该拦截的请求或者MyBatis的Mapper接口没有扫描到错误堆栈里全是英文看着吓人其实就是某一层的配置没对上。先记住一条最简单的理解方式Spring管对象SpringMVC管请求MyBatis管数据库。Spring是整套系统的骨架它通过IoC容器把Controller、Service、Mapper这些对象统一管理起来。你不需要手动new Service、new Mapper只需要加注解或者写XML配置Spring就会在容器启动时帮你在内存中创建好这些对象并管理它们的生命周期。Service里要调用MapperSpring看到Autowired注解就会自动把容器里那个Mapper对象给你拿过来放进去——这就是“依赖注入”。好处是你写Service的时候不用关心Mapper怎么new全部解耦。SpringMVC管的是Web层。一次请求进来先是Tomcat接到然后Tomcat会把请求交给DispatcherServlet这个“大管家”大管家根据URL去找对应Controller里加过RequestMapping的方法方法执行完后返回一个字符串逻辑视图名大管家再叫InternalResourceViewResolver这个“视图解析器”帮忙拼上前缀后缀比如你返回字符串”index”解析器配置的前缀是”/WEB-INF/jsp/”后缀是”.jsp”那就跳转到/WEB-INF/jsp/index.jsp。这段就是JSP能正常跳转的底层原理面试常问。MyBatis管的是SQL。你不想写JDBC那套Connection、PreparedStatement、ResultSet的重复代码就可以写Mapper接口再加一个XML文件XML里定义SQL语句MyBatis自动帮你完成参数映射和结果集映射。比如你查询快递单列表只需要在UserMapper.xml里写一句select再在接口里定义list()方法MyBatis就会自动把数据库查出来的每一行封装成User对象塞进List里返回。2.2 开发环境配置有哪些容易踩的坑我用的是IDEA 2021版本JDK 1.8Maven 3.6.3Tomcat 8.5MySQL 5.7。这套组合我跑通不止一次算是比较稳的。MySQL用5.7而不是8.0的原因很实在5.7的驱动包连接配置更省心不需要处理8.0的SSL警告和时区报错。如果你非要用8.0记得驱动要是com.mysql.cj.jdbc.DriverURL后面要拼上serverTimezoneAsia/Shanghai否则报时区错误SSL那块要加上useSSLfalse不然控制台里全是WARNING——这个坑我后续会在第5节里展开讲。IDEA里跑一个SSM项目建议直接用Maven的Tomcat插件而不是配置本地Tomcat。为什么因为用插件方式启动IDEA会用Maven运行时把war包部署到临时Tomcat中路径更不容易出差错本地Tomcat方式经常遇到Artifact部署失败或者路径不对导致的404排查起来非常烦。配置方式是在pom.xml中加入tomcat7-maven-plugin注意没有tomcat8-maven-plugin写7就够用了然后运行tomcat7:run项目就会按你的配置启动。另外有一个版本上的坑如果你用JDK 8Maven的编译插件版本不能太老建议用maven-compiler-plugin的3.8.1版本source和target都填1.8。老版本编译器对JDK 8的模块访问会报IllegalAccessError很多同学遇到这种错误直接懵了其实就是插件版本和JDK不匹配。2.3 数据库初始化与连接配置数据库部分我直接给你一份初始化脚本的模板。建库名用express字符集必须指定utf8mb4不然存中文容易出乱码问题。主要表包括CREATE DATABASE IF NOT EXISTS express DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE express; CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL COMMENT admin/delivery/client, real_name VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_waybill ( id INT PRIMARY KEY AUTO_INCREMENT, waybill_no VARCHAR(50) UNIQUE NOT NULL, client_id INT NOT NULL, delivery_id INT, sender_name VARCHAR(50), sender_phone VARCHAR(20), sender_address VARCHAR(255), receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), status INT DEFAULT 0 COMMENT 0-待接单 1-已揽收 2-派送中 3-已签收 4-异常, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tb_track ( id INT PRIMARY KEY AUTO_INCREMENT, waybill_no VARCHAR(50) NOT NULL, info VARCHAR(255) NOT NULL, operator VARCHAR(50), track_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三张表的设计逻辑很直接tb_user管人角色区分tb_waybill管快递单主状态tb_track管物流轨迹流水。状态字段用整型而不用字符串是因为业务流转里要对status做数字判断比如只能从待接单转到已揽收不能越级整型在Java的switch和比较判断里更清晰。连接配置写在jdbc.properties里核心内容如下jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/express?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456坚持要强调这个characterEncodingutf8这个参数一定要写。不写的话数据库连接过程中如果服务器的默认编码和数据库不一致插入中文就会出现“??”乱码。Java这一侧常用的URL参数拼接方式注意要用而不是单纯的在XML配置里是特殊字符需要转义否则XML会解析报错。3. 项目整体架构与关键代码实现3.1 包结构设计与分层思路SSM项目的包结构看起来好像只是约定俗成但每一种划分背后都有职责隔离的考虑。我这边项目里用的是标准的三层架构包名如下com.express ├── controller // Web层接收请求、参数接收、返回视图 ├── service // 业务层业务逻辑、事务控制 │ └── impl ├── mapper // MyBatis的Mapper接口 ├── entity // 实体类对应数据库表 ├── common // 通用工具类、常量类、结果封装 └── interceptor // 拦截器登录验证与权限控制Controller里只做三件事接收参数、调用Service、把结果放进ModelAndView或者Model返回给页面。业务运算、状态判断、事务处理全部下沉到Service层。Mapper层就是纯粹的SQL交互。这个分层在项目小的时候看起来“多此一举”但一旦业务复杂起来你就能体会到改一处不动另一层的好处。也使得答辩时被问到“三层架构有什么好处”时有的说低耦合、维护方便、复用性强。实体类方面对应表结构写User、Waybill、Track三个类。属性命名用驼峰数据库字段用下划线这个差异通过MyBatis的驼峰映射配置解决。在mybatis-config.xml里加settings setting namemapUnderscoreToCamelCase valuetrue/ /settings加了这一行数据库里sender_name字段就能直接映射到实体类senderName属性不用在SQL里逐列写别名。3.2 Spring配置文件逐段解读SSM的配置是三个框架的整合点也是最容易写错的地方。我习惯写三个配置文件spring-mvc.xml管SpringMVCapplicationContext.xml管Spring和MyBatismybatis-config.xml管MyBatis全局设置然后在web.xml里把它们都加载进来。spring-mvc.xml中最关键的是这几段!-- 扫描Controller包 -- context:component-scan base-packagecom.express.controller/ !-- 驱动SpringMVC的注解包含请求映射、参数绑定等功能 -- mvc:annotation-driven/ !-- 放行静态资源CSS、JS、图片 -- mvc:default-servlet-handler/ !-- 视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean这个default-servlet-handler很多初学者会忽略。如果你的页面CSS和JS全部失效大概率是没配这一段。原因在于DispatcherServlet会把所有请求都拦截下来如果不放行静态资源Tomcat默认的Servlet就不会处理你的css和js文件页面就会变得光秃秃没样式。加了这个之后SpringMVC会把这些请求交还给容器默认的Servlet去处理。applicationContext.xml里负责非Web层的扫描context:component-scan base-packagecom.express context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan这里注意排除Controller因为Controller已经在spring-mvc.xml中被扫描了如果两边都扫到会对同一个Bean创建两个容器管理的对象造成重复装配的问题容易出现奇怪的效果。MyBatis整合Spring的核心配置是SqlSessionFactoryBean加Mapper扫描bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.express.mapper/ /bean我解释一下这段在做什么。第一段配置是创建SqlSessionFactory把DataSource数据源注入进去同时指定Mapper的XML文件在classpath下的mapper目录。第二段是MapperScannerConfigurer它会把com.express.mapper包下所有接口都扫描出来并且为每个接口动态生成一个代理对象放进Spring容器这样你在Service里就能直接Autowired注入Mapper接口了不需要手写实现类。电源池我直接用Alibaba Druid配置在网上查得到核心优势是监控方便可以在后台页面看到SQL执行情况。答辩的时候提一句Druid的SQL监控功能老师会觉得你用过真实项目里的东西而不是只翻过教程。Druid的配置里要注意initialSize、minIdle、maxActive几个参数初始化连接数设5最小连接数设5最大设20足够了。3.3 web.xml配置要点web.xml是这三个配置文件与Web容器之间的桥梁。比较重要的配置项包括!-- 加载Spring容器 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 配置SpringMVC前端控制器 -- servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping !-- 编码过滤器解决POST中文乱码 -- filter filter-namecharacterEncodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-namecharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping唯一需要特别注意的是DispatcherServlet的url-pattern用的是/不是/。如果用/所有JSP页面的正常渲染都会被Dispatcher拦下来处理然后SpringMVC会试图去匹配JSP路径的Controller匹配不到就会报404页面全部打不开。这个坑我见过不下十次凡是项目启动后访问页面404的优先检查这个。4. 核心业务模块与前端页面实现4.1 登录权限控制与拦截器写法物流系统有三个角色不同的角色看到的功能菜单完全不同。登录逻辑不够你还需要拦截器防止用户不登录直接通过URL访问后台页面。我搞了一个LoginInterceptor实现了HandlerInterceptor接口public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } // 管理员接口需要额外校验角色 String uri request.getRequestURI(); if (uri.contains(/admin/) !admin.equals(user.getRole())) { response.sendError(403); return false; } return true; } }然后配到spring-mvc.xml里mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:exclude-mapping path/images/**/ /mvc:interceptor /mvc:interceptorsexclude-mapping这一段很关键。放行登录、注册和静态资源其他所有请求都要经过登录检查。如果不放行静态资源页面连CSS都加载不出来——因为拦截器会拦住 /css/xxx.css 的请求并跳转登录页浏览器里就会看到样式全部丢失。登录时密码加密我用的是MD5加盐。MD5本身并不安全但作为毕设项目完全够用。实际开发中会推荐改用BCrypt或者加随机盐多次加密。做登录校验的时候把用户输入的密码做同样规则的MD5再和库里的值比对一致即通过。4.2 快递单状态流转与物流轨迹整个系统最核心的业务逻辑是快递单状态机。最初我从数据库中直接更新状态代码很随意后来重构成了下面这种校验式写法在Service层强制状态流转顺序Service public class WaybillServiceImpl implements WaybillService { Autowired private WaybillMapper waybillMapper; Autowired private TrackMapper trackMapper; Override Transactional public boolean updateStatus(String waybillNo, int targetStatus, User operator) { Waybill waybill waybillMapper.findByNo(waybillNo); if (waybill null) { throw new RuntimeException(快递单不存在); } // 状态机校验0-待接单 1-已揽收 2-派送中 3-已签收 4-异常 int current waybill.getStatus(); if (targetStatus ! current 1 targetStatus ! 4) { throw new RuntimeException(非法状态流转 current - targetStatus); } // 更新快递单状态 waybillMapper.updateStatus(waybillNo, targetStatus); // 同步添加轨迹记录 Track track new Track(); track.setWaybillNo(waybillNo); track.setInfo(getStatusDesc(targetStatus)); track.setOperator(operator.getRealName()); trackMapper.insert(track); return true; } }这里有两个值得注意的点。一是Transactional注解因为更新快递单状态和插入物流轨迹是两个操作必须保证原子性要么都成功要么都不成功。如果去掉这个注解一旦中途报错状态已经变了但轨迹没有记上数据就对不上了。这也是面试高频考点“事务是什么为什么需要事务”。二是状态机校验不是任意状态都能跳到任意状态必须按顺序流转异常单除外。这种设计保证了业务流程的严谨性答辩或后续做扩展时也能体现出你考虑问题的周全。物流轨迹查询就比较简单了前台客户输入快递单号后台按单号查tb_track表按时间倒序输出Override public ListTrack getTracks(String waybillNo) { return trackMapper.findByWaybillNoOrderByTimeDesc(waybillNo); }对应Mapper XML就是一条带order by的select。大多数查询无非就是select、where、order by的组合并不复杂关键是字段名别写错以及分页、排序这些细节。4.3 JSP页面与JS的交互实现JSP这块我想先纠正一个观念JSP早就不建议学习新技术了但你做毕设用JSP完全没毛病因为它在JavaWeb课程里依然是主流教学内容。本项目里JSP的角色是“模板视图”通过JSTL标签和EL表达式来渲染后端传过来的数据。比如管理员后台的快递单列表页面核心代码长这样% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered thead tr th单号/th th寄件人/th th收件人/th th状态/th th操作/th /tr /thead tbody c:forEach items${waybillList} varwb tr td${wb.waybillNo}/td td${wb.senderName}/td td${wb.receiverName}/td td c:choose c:when test${wb.status 0}待接单/c:when c:when test${wb.status 1}已揽收/c:when c:when test${wb.status 2}派送中/c:when c:when test${wb.status 3}已签收/c:when c:otherwise异常/c:otherwise /c:choose /td td a href${pageContext.request.contextPath}/waybill/detail/${wb.waybillNo}详情/a /td /tr /c:forEach /tbody /tableforEach就是JSTL里的循环标签items对应Controller放进Model的Listvar是循环变量名。${wb.status}就是EL表达式直接从对象的getStatus()方法取值。table的字段一一对应无论是读代码还是改代码都很直观。JSP页面一般不直接写死跳转路径我习惯用${pageContext.request.contextPath}来获取项目部署名。这样不管项目叫什么名路径都能自适应不会因为改了部署名导致全部链接失效。这是在真实项目里养成的习惯毕设里用上也会让代码规范加分。前端交互用了原生JavaScript和JQuery混着写。举一个寄件下单的校验例子表单提交前JS先校验手机号格式和收寄件人是否为空通过后才把数据提交到后台。这样避免无效请求打给服务器用户体验也好不用等后台返回错误再刷新页面。$(#submitBtn).click(function () { var senderPhone $(#senderPhone).val(); var receiverPhone $(#receiverPhone).val(); var phoneReg /^1[3-9]\d{9}$/; if (!phoneReg.test(senderPhone)) { alert(寄件人手机号格式不正确); return; } if (!phoneReg.test(receiverPhone)) { alert(收件人手机号格式不正确); return; } $(#waybillForm).submit(); });JS的正则只做前端提示拦截后端Service里还得用Java再校验一次。前端校验的目的是提升体验后端校验才是真正保护数据安全。两处都要写一个都不能省——这是Web开发的基本素养。4.4 管理员统计报表与图表展示管理员后台要能看到各个状态快递单的数量我用了一个简单的统计SQLselect idcountByStatus resultTypemap SELECT status, COUNT(*) AS cnt FROM tb_waybill GROUP BY status /select查询结果是一个ListMapString, Object每个Map里有status和cnt两个key。把这个List传给JSP页面前端用JavaScript遍历生成柱状图。图表库我用的是ECharts引入方式很简单就是页面里放一个div然后通过script标签加载ECharts的CDN文件最后写一段option配置就好。因为快递系统不是大数据可视化项目ECharts在这个场景下主要用来做几张简单的统计图——各状态快递量、近7日下单量、各快递员的派送数量。掌握ECharts的基本使用方式后续扩展图表也不是难事。JSP页面里渲染JSON数据有个小技巧Controller拿到的List不好直接用EL表达式遍历Map那就先在Java里把数据转成JSON字符串塞到Model里JSP端用script接收并且赋值给JS变量script var chartData ${statusJson}; // 然后在ECharts里用chartData做数据源 /script${statusJson}会被渲染成[{status:0,cnt:12},...]这样的JSON格式天然就是JavaScript对象直接可用。JSON转换工具我用的Fastjson在Java领域是最常见的。它的JSON.toJSONString方法一行搞定转换。5. 项目部署运行与常见问题排查5.1 从零到跑通的完整部署步骤好说到部署。很多同学源码下载下来本地跑不起来第一反应就是“代码有问题”。实际上绝大部分是环境问题部署环节有个明确顺序按这个顺序来不容易出bug安装MySQL 5.7设置root密码执行初始化SQL脚本建库建表导入数据。修改jdbc.properties里的username和password为你的MySQL实际账号密码。用IDEA打开项目等待Maven下载依赖完成。配置Maven的Runner里JRE为JDK 1.8。打开Maven面板运行tomcat7:run。浏览器访问http://localhost:8080/看到登录页即成功。用管理员账号登录在员工管理里添加快递员、客户账号然后切换角色测试完整流程。这个步骤里最容易被忽略的是第4步。IDEA的Maven默认可能用的JRE不是你安装的JDK导致编译版本不对报错信息会提到“无效的目标发行版”这时候去File → Project Structure里把Project SDK和Project language level都改成1.8Maven设置里的JDK for importer也同步改掉问题就解决了。如果是把项目放到Linux服务器上跑本质和本机跑没有区别注意MySQL的bind-address配置允许远程访问以及防火墙要放行3306端口和8080端口。Tomcat部署war包时别忘了项目名和数据库URL里的localhost要改成服务器的实际内网或公网IP。5.2 数据库连接报错速查表数据库连接是最容易翻车的地方。报错信息中有一大批其实原因都很简单我把这几年遇到的典型报错整理成了一张表报错信息可能原因解决方案Access denied for user rootlocalhostMySQL密码不对检查jdbc.properties的passwordCommunications link failureMySQL服务没启动或端口不对启动MySQL服务确认port3306Unknown database express数据库没建执行init.sql里的建库语句SSL connection errorMySQL 8.0的SSL警告URL加useSSLfalseThe server time zone value is unrecognizedMySQL 8.0时区问题URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0的认证问题URL加allowPublicKeyRetrievaltrueCannot create PoolableConnectionFactory驱动版本不对检查mysql-connector-java版本5.7用5.x驱动你自己用MySQL 5.7的时候基本上只需要注意前三行。如果你装了MySQL 8.0请务必照着表里的方案把参数补齐。另外提醒一句修改jdbc.properties后要重启Tomcat才能生效改了不重启的话你会在那边反复报错。5.3 404错误与页面打不开的原因分析页面404是SSM项目新手最容易遇到、也最折磨人的问题。根据我的经验大概有这么几种情况第一种DispatcherServlet的url-pattern写成了/*。这会导致所有JSP请求都被Dispatcher拦截它去找对应的Controller映射找不到就会404。正确写法是/把JSP的渲染交还给Servlet容器。第二种视图解析器的前缀后缀没写对。SpringMVC的Controller返回字符串”index”时InternalResourceViewResolver需要拼成/WEB-INF/jsp/index.jsp。如果后缀错了拼出来一个不存在的路径自然404。检查spring-mvc.xml里的prefix和suffix写得对不对。第三种项目上下文路径问题。直接在浏览器访问localhost:8080出现了Tomcat的默认首页说明项目没部署上去或者访问路径缺了项目名。用Maven的tomcat7插件时默认context path是你pom.xml中artifactId的值比如/ssm_express那访问地址就是http://localhost:8080/ssm_express/。这个细节不知道的话你就会一直访问默认首页一脸迷茫。第四种在JSP页面的链接和表单action里写死了路径。比如写成/waybill/list却忘了项目部署名导致404。我的项目全部用${pageContext.request.contextPath}拼接从根源上规避了这个问题。5.4 JS和JSP中文乱码根源与解决方案中文乱码在JSP程序里几乎人人遇到过分三种位置页面显示乱码、请求参数乱码、数据库存储乱码。核心原因都是编码方式不一致。我这边给出一个统一方案所有环节统一使用UTF-8。页面显示乱码JSP页面顶部要写pageEncoding% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%改成UTF-8之后字体和中文显示正常。要注意的是如果你已有页面文件直接用记事本打开另存为UTF-8格式避免IDEA的File Encoding设置为GBK导致存出来还是乱。请求参数乱码POST请求乱码用web.xml里的CharacterEncodingFilter可以解决这个是Spring提供好的过滤器。GET请求乱码比较隐蔽Tomcat 8默认的URI编码是UTF-8但如果你的Tomcat是7GET请求参数需要手动在server.xml的Connector上加URIEncodingUTF-8。我自己都在用Tomcat 8以上所以很少遇到GET乱码但如果用了8.5以上版本还乱看看是不是没有依赖Spring的characterEncodingFilter。数据库存储乱码建库时指定utf8mb4字符集连接URL带characterEncodingutf8基本可以杜绝。MySQL连接乱码最隐蔽的地方在于你单独连Navicat手动插数据没问题但程序插入就乱说明不是表结构问题而是连接参数没带编码设置。6. 项目答辩准备与实际经验分享6.1 答辩高频问题与参考回答代码跑通了之后最后一步就是过答辩。这个环节问的问题其实完全可以预判准备到位基本不慌。“为什么用SSM而不用SpringBoot”我通常这样回答SpringBoot是SSM的进一步封装和自动化用SSM是为了更好地展示对Spring、SpringMVC、MyBatis核心原理的理解。从学习角度来看能深入地掌握框架的装配过程和关键机制。“MyBatis和JDBC有什么区别”从代码层面回答即可JDBC需要手动获取连接、创建Statement、设置参数、处理结果集、关闭资源工作量大且代码重复。MyBatis把SQL写在XML中自动完成参数映射和结果集映射内置连接池管理。简单说就是MyBatis封装了JDBC的重复动作让你只关注SQL本身。“SpringMVC处理一次请求的完整过程是什么”按链路背一遍用户请求到达DispatcherServletDispatcherServlet通过HandlerMapping找到对应ControllerController调用Service处理业务并返回ModelAndViewDispatcherServlet通过ViewResolver解析视图最终渲染响应给用户。“如果快递单状态更新到一半失败了怎么办”这个就是事务考察点。我的Service里加了Transactional注解更新状态和插入轨迹记录是同一事务内两个操作若任何一个失败事务回滚。这些问题是基本面。真正体现你工作量的是系统演示时能流畅走完三个角色的完整业务不要临场发现某个按钮点了没反应。演示前用真实数据走一遍流程比背十个答疑问答都管用。6.2 我踩过的那些坑与最终体会最后分享几个这个项目里我印象最深的经验。第一个是“立刻断电不回滚”的教训。早期项目里快递状态更新后下一个操作实时查询数据发现自己改了数据库里的状态但页面一直不变。排查了半天发现是MyBatis的二级缓存问题。后来把项目里MyBatis缓存的配置直接禁用掉了缓存默认没开启但如果用了cache/配置就会踩到。毕设项目基本没有刻意缓存必要禁用缓存能让数据一致性更直观。第二个是“路径问题折磨一下午”。曾经有个页面一直跳转不到Controller前端Console报404。反复看代码、加日志最后发现是JSP里按钮onclick写死的跳转路径漏了contextPath。从那之后我所有JSP里的URL都统一使用${pageContext.request.contextPath}拼接再也不手动写死路径了。第三个是“Navicat导数据踩坑”。给客户演示前我用Navicat往数据库里导了一批测试数据结果页面显示的中文变成问号折腾了半天。原因是Navicat导入时自带的SQL文件不是UTF-8编码导入设置里面有个编码选项默认是自动改成UTF-8后重新导入就正常了。6.3 后续还能怎么扩展程序跑通、答辩搞完之后如果你对这个项目还有兴趣有几个扩展方向性价比非常高。一是把前端升级一下。现在的JSPBootstrap的组合是2015年左右的风格如果你有精力可以把前端改成Vue通过接口请求后端数据。不需要改造后端逻辑只需要写一层JSON接口前端用Vue渲染列表和表单。二是引入数据图表加强可视化。当前只有简单的ECharts统计可以增加一个物流热力图模块根据收件地址字段做区域聚合在地图上展示各区域快递量。这块视觉效果非常好答辩展示时很加分。三是加一个简单的消息推送模块。当快递状态更新时给客户手机模拟发一条通知把站内信做成一个message表登录后在页面右上角红点提示未读数。这个功能面试或答辩聊起来也挺有意思。四是最实在的——压测优化。用JMeter对登录、查询快递单、物流轨迹查询这三个接口做一个小压测观察TPS和响应时间再针对慢SQL加索引。比如tb_track的waybill_no字段加上普通索引查询速度会有肉眼可见的提升。这个属于性能优化方向写在简历上是亮点。我个人建议如果时间充裕优先做第三个消息通知和第四个压测优化因为这俩都能体现“你做了超出常规毕设范围的事”在评分和面试时都是加分项。如果时间紧张把当前版本稳定跑通、把每行代码看懂其实也足够了。这个项目的代码量不算大SSM的技术栈又是最普及的JavaWeb组合只要认真过一遍你收获的不只是一个系统还有对整套Web开发链路的扎实理解。