
简介基于SSM框架、MySQL数据库和HTML前端技术构建的道路养护管理系统完整项目主要面向需要完成课程设计、毕业设计或希望参考成熟管理系统进行实战演练的Java学习者。系统涵盖道路信息管理、损害类型管理、评定等级管理、日常巡查管理、定期检查管理等核心功能业务模块划分清晰有助于理解SSM整合开发流程与分层设计思想。整套资料共894个文件压缩包大小30.45MB内含124个Java源码文件、220个编译后的class文件、113个XML配置文件、110个JavaScript脚本、53个CSS样式以及24个HTML页面同时包含27个依赖JAR包和数据库SQL脚本便于还原运行环境。目前已有357人学习下载受到需要做类似管理系统的开发者关注。从后台Java代码到前端页面、从MyBatis映射到数据库结构均有完整呈现可直接用于功能扩展、二次开发或作为毕业设计参考资料帮助快速掌握道路养护相关业务的信息化实现方式。1. 这套道路养护管理系统Maven 拉起来就能跑的后台台账做道路养护的同学应该都有体会日常巡查记录和定期检查台账是最容易乱的两块东西纸质的容易丢Excel 的版本一多就分不清哪份是最新的。这套基于 SSM MySQL HTML 的道路养护管理系统就是把“道路信息、损害类型、评定等级、日常巡查、定期检查”这五块数据统一收进一个 Web 后台里浏览器打开就能录入、查询、维护不用装客户端。系统的技术栈很典型Spring SpringMVC MyBatis 做后端MySQL 存数据前端就是纯 HTML 加 jQuery没有引入 Vue 或 React 那套重型东西。适合两类人一类是想找个完整 SSM 项目练手的学生或者转行开发者代码结构清晰Maven 拉完依赖配置好数据库就能跑另一类是基层养护单位的信息员需要一套轻量的台账工具不想上太重的大型系统。整个系统体量不大但 SSM 该有的分层、配置、注解、Mapper 映射都齐能跑通就说明你对这套框架的基本盘已经掌握了。2. SSM 与 MySQL 的整合配置不把依赖和驱动理清后面全是坑SSM 项目第一道坎不是业务代码而是环境整合。Spring、SpringMVC、MyBatis 三个框架各自独立通过配置文件粘合在一起任何一环版本不对或者配置路径写错启动时就会报一堆让人摸不着头脑的异常。这一章把配置层面的关键点拆开讲照着做能少走很多弯路。2.1 Maven 依赖版本搭配固定版本组合别追新SSM 整合最忌讳的就是每个框架都拿最新版三个框架各自的更新节奏不一样拼接起来容易出现接口不兼容。我一般用这套经过大量项目验证的版本组合Spring 5.2.x、MyBatis 3.5.6、MyBatis-Spring 2.0.6、MySQL Connector/J 8.0.2x、Jackson 2.12.x。这套组合在 Tomcat 8.5 和 9.0 上都跑得很稳。properties spring.version5.2.15.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- JSON 序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.5/version /dependency /dependencies这里有个容易翻车的细节如果你本机 MySQL 是 5.7用 mysql-connector-java 8.0.28 没问题驱动会自动兼容但反过来你用老驱动连 MySQL 8.0 就会报 SSL 连接错误。所以驱动版本跟数据库版本要匹配8.x 驱动向上兼容 5.7这是最稳的选择。Spring 版本这里统一用 5.2.x 而不是 4.x因为 MyBatis-Spring 2.0 系列要求 Spring 5 以上版本太低直接启动失败。2.2 Spring 与 MyBatis 的核心配置数据源、事务、扫描路径依赖拉好之后核心的工作就是写 Spring 配置。SSM 项目通常至少有两个配置文件一个是 spring-context.xml 负责数据源、事务、MyBatis 整合另一个是 spring-mvc.xml 负责注解驱动和 Controller 扫描。!-- spring-context.xml -- context:property-placeholder locationclasspath:db.properties/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.road.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.road.dao/ /bean tx:annotation-driven transaction-managertransactionManager/ bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean注意这里我加了mapUnderscoreToCamelCase开启下划线自动转驼峰。数据库字段如果叫road_code实体类里写roadCode开启这个配置后 MyBatis 会自动完成映射不用每个字段都手动写 resultMap。不过开启的前提是数据库字段命名规范统一用下划线实体类统一用驼峰如果表里有rId这种混搭命名还是老老实实写 resultMap。数据源我用的DriverManagerDataSource这是最轻量的方式适合学习和小型项目。生产环境建议换成 druid 或 HikariCP配置方式一样把 bean 的 class 换掉即可jdbc.url、jdbc.username这些属性名大同小异。db.properties 里需要注意 MySQL 8.0 的 url 必须带 serverTimezone 和 useSSL 参数否则连不上jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/road_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456驱动类名这里是个高频错点MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver不是 5.x 时代的com.mysql.jdbc.Driver。如果你用的是 8.x 驱动但类名写成旧的启动直接抛 ClassNotFoundException。很多教程还在复制老代码这块踩坑率很高。2.3 web.xml 与 SpringMVC 配置请求从浏览器到 Controller 的管道web.xml 承担的是容器初始化职责配置两个关键监听器Spring 容器监听器和 SpringMVC 前端控制器 DispatcherServlet。容易漏的是 DispatcherServlet 的初始化参数必须指定 spring-mvc.xml 的位置否则框架找不到 Controller。context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-context.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener 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 filter filter-namecharacterEncoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param force-encodingtrue/force-encoding /filter filter-mapping filter-namecharacterEncoding/filter-name url-pattern/*/url-pattern /filter-mapping字符集过滤器必须放在所有 Filter 的最前面并且force-encoding要设成 true否则如果请求头里带了其他编码信息过滤器不会强制覆盖。这个 filter 直接影响后面表单提交的中文数据很多系统上线后乱码问题就出在这一步。servlet-mapping 的url-pattern用的/把所有请求都交给 SpringMVC 处理包括静态资源所以 spring-mvc.xml 里还需要配资源映射mvc:annotation-driven/ mvc:default-servlet-handler/ context:component-scan base-packagecom.road.controller/mvc:default-servlet-handler是让 Tomcat 默认的 servlet 处理 HTML、JS、CSS 这些静态文件否则你写好的前端页面会全部 404。这个配置在 SSM 项目里太容易漏了漏掉的表现就是接口调用正常但浏览器打开页面全挂。3. 数据模型与表结构设计道路、损害类型、评定等级怎么落到 MySQL系统能不能用一半看表设计合不合理。道路养护管理涉及的数据实体不多但彼此之间有外键关联设计不好就会出现冗余和查询混乱。这一章把表结构拆开讲同时给出 MyBatis 的持久层写法。3.1 五张核心表的结构字段、类型、外键关系我把表分成三组基础档案道路、损害类型、评定等级业务流水日常巡查、定期检查。基础档案是维度表业务流水是事实表查询时通过外键关联维度信息。下面是完整的建表 DDL字段已经去掉多余的部分保留业务真正需要的-- 道路信息表 CREATE TABLE road_info ( id int NOT NULL AUTO_INCREMENT, road_code varchar(32) NOT NULL COMMENT 道路编号, road_name varchar(128) NOT NULL COMMENT 道路名称, road_level varchar(16) DEFAULT NULL COMMENT 道路等级高速/国道/省道/县道, start_point varchar(128) DEFAULT NULL COMMENT 起点桩号, end_point varchar(128) DEFAULT NULL COMMENT 终点桩号, length_km decimal(8,2) DEFAULT NULL COMMENT 里程(公里), manager varchar(64) DEFAULT NULL COMMENT 管养负责人, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_road_code (road_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 损害类型表 CREATE TABLE damage_type ( id int NOT NULL AUTO_INCREMENT, type_code varchar(32) NOT NULL COMMENT 类型编码, type_name varchar(64) NOT NULL COMMENT 损害类型名称裂缝/坑槽/沉陷/车辙, description varchar(255) DEFAULT NULL COMMENT 类型说明, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评定等级表 CREATE TABLE assess_level ( id int NOT NULL AUTO_INCREMENT, level_code varchar(16) NOT NULL COMMENT 等级编码A/B/C/D, level_name varchar(32) NOT NULL COMMENT 等级名称优/良/中/差, score_range varchar(32) DEFAULT NULL COMMENT 分值区间, description varchar(255) DEFAULT NULL COMMENT 评定说明, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 日常巡查记录表 CREATE TABLE patrol_record ( id int NOT NULL AUTO_INCREMENT, road_id int NOT NULL COMMENT 关联道路ID, patrol_date date NOT NULL COMMENT 巡查日期, patrol_person varchar(64) NOT NULL COMMENT 巡查人, weather varchar(32) DEFAULT NULL COMMENT 天气, damage_type_id int DEFAULT NULL COMMENT 损害类型ID, damage_desc varchar(255) DEFAULT NULL COMMENT 损害描述, damage_level varchar(16) DEFAULT NULL COMMENT 损害程度轻微/中等/严重, handle_status tinyint DEFAULT 0 COMMENT 处理状态0未处理/1已处理, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_road_date (road_id, patrol_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套设计有几个关键决策值得说。首先是road_code加了唯一索引因为道路编号在业务上是唯一标识但不适合做物理主键用自增 id 做主键业务编号做唯一键这样避免了主键被业务数据牵制。其次是联合索引idx_road_date日常巡查最常见的查询是“某条路在某段时间内的记录”联合索引直接命中不产生全表扫描。最后是handle_status用了 tinyint 而不是 varchar状态字段用数字存查询时用WHERE handle_status 0比字符串比较效率高代码里做一层常量映射即可。损害类型和评定等级两张表是典型的维度表数据量小但又经常被引用。损害类型表里的 type_name 包括了裂缝、坑槽、沉陷、车辙这些道路常见病害这张表的意义在于把巡查记录里的“损害描述”归一化统计时按类型分组才能算出哪种病害占比最高。评定等级表的 score_range 字段存的是“85-100”“70-84”这类区间字符串方便前端直接展示但如果你要做分值范围的条件查询建议拆成 score_min 和 score_max 两个字段SQL 里用BETWEEN就方便了这是设计时的一个取舍点。3.2 MyBatis 持久层Mapper 接口与 XML 的对应关系表设计好了接下来是数据访问层。我习惯用 Mapper 接口加 XML 文件的方式不用注解写 SQL因为复杂的关联查询和动态条件在 XML 里维护更清晰。接口负责定义方法XML 负责写 SQL两者通过 namespace 和方法名绑定。public interface RoadInfoMapper { int insert(RoadInfo road); int update(RoadInfo road); int deleteById(Integer id); RoadInfo selectById(Integer id); ListRoadInfo selectList(Param(keyword) String keyword, Param(roadLevel) String roadLevel); }mapper namespacecom.road.dao.RoadInfoMapper sql idbase_columns id, road_code, road_name, road_level, start_point, end_point, length_km, manager, create_time /sql select idselectList resultTypeRoadInfo SELECT include refidbase_columns/ FROM road_info where if testkeyword ! null and keyword ! AND (road_code LIKE CONCAT(%, #{keyword}, %) OR road_name LIKE CONCAT(%, #{keyword}, %)) /if if testroadLevel ! null and roadLevel ! AND road_level #{roadLevel} /if /where ORDER BY road_code /select /mapper这里有几个 MyBatis 的常用写法值得抠一下。where标签会自动处理掉第一个多余的 AND 或 OR这是 MyBatis 动态 SQL 里最实用的特性避免了你手动拼接字符串时的“AND 开头报错”问题。sql标签定义公共字段列多张表查询或者重复查询时不用每次写一遍完整字段列表修改表结构只改一处。CONCAT(%, #{keyword}, %)是标准模糊查询写法注意 #{keyword} 用了预编译参数不会产生 SQL 注入问题这是 MyBatis 对比直接拼字符串的绝对优势。关于 resultType 和 resultMap 的选择开启了下划线转驼峰后单表查询用 resultType 就够了MyBatis 自动把road_code映射到roadCode。如果你有多表关联查询比如巡查记录关联道路名称和损害类型名称返回的字段不在任何一张实体表里这时候需要自定义一个 VO 类在 XML 里用 resultMap 手动映射每个字段。3.3 业务层与控制器注解驱动的事务和 JSON 接口Service 层负责业务逻辑我习惯以“表操作”为粒度拆类。日常巡查这个模块的 Service 有一个典型的业务场景新增巡查记录时如果损害等级是“严重”需要同时更新道路的评定等级。这类跨表更新必须加事务。Service public class PatrolRecordService { Autowired private PatrolRecordMapper patrolRecordMapper; Autowired private RoadInfoMapper roadInfoMapper; Transactional(rollbackFor Exception.class) public void addPatrol(PatrolRecord record) { if (record.getPatrolDate() null) { record.setPatrolDate(new Date()); } patrolRecordMapper.insert(record); // 损害严重时更新道路评定等级为 D if (严重.equals(record.getDamageLevel())) { RoadInfo road roadInfoMapper.selectById(record.getRoadId()); if (road ! null) { road.setAssessLevel(D); roadInfoMapper.update(road); } } } }Transactional注解是 Spring 声明式事务的核心rollbackFor Exception.class指定所有异常都触发回滚。这里踩过坑的人都知道如果只写Transactional不指定 rollbackFor默认只在 RuntimeException 时回滚检查异常比如 IOException不会回滚数据就会处于半提交状态。这个小细节在面试里也是高频考点。Controller 层就是标准的注解流转Controller RequestMapping(/patrol) public class PatrolController { Autowired private PatrolRecordService patrolService; RequestMapping(value /add, method RequestMethod.POST) ResponseBody public Result add(RequestBody PatrolRecord record) { patrolService.addPatrol(record); return Result.success(添加成功); } RequestMapping(/list) ResponseBody public Result list(RequestParam(required false) Integer roadId, RequestParam(required false) String startDate, RequestParam(required false) String endDate) { ListPatrolRecordVO list patrolService.queryList(roadId, startDate, endDate); return Result.success(list); } }ResponseBody负责把返回值序列化成 JSON前端 HTML 里的 jQuery Ajax 直接拿这个 JSON 渲染表格。这套模式在 SSM 项目里是标准打法没有页面跳转逻辑后端只提供数据接口前端负责展示职责分离明确。RequestParam(required false)处理查询参数的可选性不传时值为 nullMyBatis 动态 SQL 里用if testxxx ! null判断拼接条件查询。4. 日常巡查与定期检查模块实现前端 HTML 到后端入库的完整闭环整个系统里日常巡查和定期检查是使用频率最高的两个业务模块。这一章从前端页面向后端一路拆把一条巡查记录从填表到入库的完整链路走一遍同时把 SSM 常用注解在这个模块里的实际用法对照起来讲。4.1 前端页面纯 HTML jQuery 的表格和表单前端页面直接放在 webapp 目录下用原生的 HTML 加 jQuery 库。页面不复杂两块核心内容顶部的查询条件栏中间的记录表格。查询栏和表格靠 Ajax 联动条件是参数表格是结果展示。!DOCTYPE html html langzh-cn head meta charsetutf-8 title日常巡查记录/title script srcjs/jquery-3.5.1.min.js/script style .query-bar { margin-bottom: 15px; } .query-bar input, .query-bar select { margin-right: 10px; padding: 4px 8px; } table { width: 100%; border-collapse: collapse; } table th, table td { border: 1px solid #ddd; padding: 6px 10px; font-size: 14px; } /style /head body div classquery-bar 道路名称input typetext idkeyword placeholder输入道路名称或编号 损害类型 select iddamageType option value全部/option option value1裂缝/option option value2坑槽/option option value3沉陷/option /select button onclickloadData()查询/button /div table thead tr th道路名称/th th巡查日期/th th巡查人/th th损害类型/th th损害描述/th th处理状态/th /tr /thead tbody idtableBody/tbody /table script function loadData() { $.ajax({ url: /road/patrol/list, type: GET, data: { keyword: $(#keyword).val(), damageType: $(#damageType).val() }, dataType: json, success: function(res) { if (res.code 200) { renderTable(res.data); } else { alert(res.msg); } }, error: function() { alert(请求失败请检查网络); } }); } function renderTable(data) { var html ; for (var i 0; i data.length; i) { var item data[i]; var statusText item.handleStatus 0 ? 未处理 : 已处理; html tr td item.roadName /td td item.patrolDate /td td item.patrolPerson /td td item.damageTypeName /td td item.damageDesc /td td statusText /td /tr; } $(#tableBody).html(html); } $(function() { loadData(); }); /script /body /htmlJSP 项目里页面通常会用到 EL 表达式和 JSTL 标签但这套系统用 HTML Ajax 的方式意味着静态页面在浏览器里直接执行 JS 去请求后端接口和后台工程解耦。如果项目部署在 Tomcat 中注意页面路径里要带上项目上下文名称比如/项目名/road/patrol/list或者配置 Tomcat 的 Context 路径为/否则接口会 404。页面渲染用字符串拼接显然不够优雅但在这个体量的系统里完全够用。如果你想优化可以引入 Vue 的 CDN 版本在 HTML 里声明el绑定一个容器data里存数组配合v-for渲染表格代码会比字符串拼接清晰不少。这不影响 SSM 后端结构前端层引入 Vue CDN 是低成本提升体验的做法。4.2 SSM 常用注解在这个模块里的实际落位一套 SSM 项目跑下来注解是绕不开的话题。把日常巡查模块里用到的注解系统过一遍比对着框架文档死记硬背高效得多。Controller标注在 PatrolController 上声明这是一个 SpringMVC 控制器组件扫描到之后接管对应的 URL 路由。RequestMapping标注在类上和方法的双重路由配置。类上是/patrol前缀方法上再拼接/add、/list完整 URL 就是/patrol/add和/patrol/list。method 属性限定 GET 或 POST加了之后请求方式不对直接 405。ResponseBody把方法返回值序列化成 JSON 写入响应体不走视图解析器。它的兄弟是RequestBodyPOST 请求时把 JSON 反序列化成对象两个配在一起就是完整的前后端 JSON 交互通道。RequestParam绑定 URL 查询参数到方法入参。required false表示这个参数可以不传适用于查询条件可选的场景。Autowired按类型注入依赖。解决的是 Spring 容器里 bean 的依赖关系避免手动 new 对象导致容器管理失效。Transactional事务边界控制。加在 Service 层的公共方法上保证跨表操作要么全部成功要么全部回滚。MapperScan如果在 spring-context.xml 里配置了 MapperScannerConfigurer这个注解可以不写二选一即可。实际项目里经常有人把ResponseBody和Controller合写成RestController这在 SpringBoot 项目里是很常见的写法。但 SSM 项目里我建议还是拆开写因为有些方法要返回视图名比如跳转页面有些要返回 JSON拆开更灵活。不要把 RESTful 风格的写法生搬硬套到 SSM 里。4.3 定期检查模块与巡查模块的联动逻辑定期检查是比日常巡查更正式的环节抽样频率低、检查项目多两者在数据上是互补关系日常巡查发现问题定期检查做全面评估。系统里定期检查模块的核心是“评定等级”逻辑检查结束后要给这条路打一个 A/B/C/D 的等级分。Service public class InspectRecordService { Autowired private InspectRecordMapper inspectMapper; Autowired private AssessLevelMapper assessLevelMapper; Transactional(rollbackFor Exception.class) public void addInspect(InspectRecord record) { if (record.getInspectDate() null) { record.setInspectDate(new Date()); } // 插入检查记录 inspectMapper.insert(record); // 关联评定等级 AssessLevel level assessLevelMapper.selectByScore(record.getTotalScore()); if (level ! null) { record.setLevelId(level.getId()); inspectMapper.updateLevel(record); } } }这里selectByScore是打分到等级的自然映射80 分以上是 A70-80 是 B60-70 是 C60 以下是 D。把这条规则放数据库表里而不是硬编码在 Java 代码里好处是等级标准调整时只改表数据不需要重新发版。这个思路在养护单位里很实用评定标准经常被上级部门调整硬编码意味着每次都要改代码重新部署放数据库里一行 update 就搞定。用户反馈里最常见的需求是“按道路和日期区间导出台账”这需要把查询结果输出成 Excel。系统里如果没做这个功能可以加一个导出接口用 Apache POI 生成 xls 文件。这一步和 SSM 的 MVC 结构不冲突Controller 返回一个response.getOutputStream()的流式响应前端在 jQuery 里用window.location.href触发下载即可。5. 避坑清单从连不上数据库到 Tomcat 中文乱码的五个血泪记录SSM 项目开发过程中踩过的坑很多不是业务逻辑复杂造成的而是配置层和环境层的问题。这些坑特征明显、重复率高遇到一次后面就认得它了。下面五条是我在跑这套系统时真实遇到的每一条都按现象、原因、解决来写。5.1 启动报 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动时抛出ClassNotFoundException: com.mysql.jdbc.Driver项目直接启动失败。原因MySQL Connector/J 8.x 版本的驱动类名改成了com.mysql.cj.jdbc.Driver旧的类名虽然在 8.x 里还保留着兼容性入口但如果你的依赖是 5.1.x 版本类名就是旧的。关键矛盾是驱动包是 8.x但代码里jdbc.driver配置的类名却是抄教程里的旧写法com.mysql.jdbc.Driver。这个不匹配在连接建立时才会触发。解决确认 pom.xml 里 mysql-connector-java 版本是 8.0.2x同时 db.properties 里jdbc.drivercom.mysql.cj.jdbc.Driver。两个地方对齐就一次通过。5.2 MySQL 8.0 连接报 SSL 和时区错误现象驱动能加载但创建连接时抛异常提示The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者Connections using insecure transport are prohibited while requiring SSL。原因MySQL 8.0 默认启用 SSL 连接同时要求客户端指定时区。中文环境下的系统时区名 MySQL 驱动解析不了就报出这样一串乱码。这不是代码问题是连接参数缺失。解决JDBC 连接串里补两个参数jdbc.urljdbc:mysql://localhost:3306/road_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse 关闭加密通道本地开发和内网环境足够用。serverTimezone 指定 Asia/Shanghai避免驱动读系统默认时区解析出乱码。加完这两个参数问题基本就绝迹了。5.3 Mapper 接口和 XML 绑定失败现象启动时项目能起来调用任何 DAO 方法时抛org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。原因三种常见情况第一种mapper XML 文件的 namespace 没写或者写错导致 MyBatis 找不到对应的接口路径第二种XML 文件名和接口名不一致MyBatis 约定文件放在classpath:mapper/下文件名必须和接口类名完全匹配第三种spring-context.xml 里mapperLocations配置的路径不对比如你文件实际放在com.road.dao.mapper包下但配置写的是classpath:mapper/*.xml。解决先用一个笨办法排查启动日志里看 MyBatis 打印的加载日志确认哪些 XML 被加载了。如果 XML 没有加载优先检查mapperLocations路径和文件实际位置是否一一对应。namespace 写错比较隐蔽建议直接打开 XML 文件核对namespace的属性值是否和接口全限定名一致前缀多一个或少一个字符都会出问题。5.4 前端页面能打开但接口全部 404现象浏览器输入地址能访问到静态 HTML 页面但调用$.ajax接口全部返回 404Network 面板显示请求路径不对或者根本没有到达后端。原因最常见的是项目部署容器路径和请求 URL 前缀不匹配。Tomcat 默认的访问路径是http://localhost:8080/项目名/但静态页面里的 Ajax 请求写成了http://localhost:8080/patrol/list少了项目上下文名称自然 404。另一个可能就是 servlet-mapping 的 url-pattern 写成了*.do之类的后缀匹配但页面请求没有带这个后缀。解决两个选择把请求 URL 前面加项目上下文名或者在 Tomcat 的 server.xml 里把 Context 的 path 设置成/部署后直接用根路径访问。开发调试阶段我建议用后者省心但上线部署还是要按服务器实际情况调整。5.5 表单提交中文乱码数据库里全是问号现象前端在输入框里填的中文内容提交后数据库里存的是????英文和数字正常。原因字符集链路断了三层页面没有声明 meta charsetTomcat 接收 POST 请求时没有指定字符集MySQL 表的字符集不是 utf8mb4。任何一层没有对齐中文传输过程中就会出现编码损失。解决三层逐一补齐。HTML 里加meta charsetutf-8web.xml 里配置 CharacterEncodingFilter配置方式见 2.3 节MySQL 建表时带上DEFAULT CHARSETutf8mb4。还有一个细节数据库连接串里的characterEncodingutf8要和表字符集一致如果用 utf8mb4连接串里写characterEncodingutf8也能兼容但新库建议直接统一 utf8mb4。三层全设对中文不会再出幺蛾子。6. 验证系统跑通的一套流程从 SQL 初始化到页面全链路走查整套系统部署完成不代表业务正确我一般会强制自己走一遍完整的验证流程确保每一步都有数据产出而不是只看项目能启动就交差。这套验证流程相当于给系统做一次“体检”每个模块都要有一点真实数据。第一步是初始化数据库。把建表 SQL 文件导进 MySQL检查 5 张表是否都建好表结构字段类型是否符合预期。用SHOW TABLES和DESC road_info快速核对重点看create_time默认值、外键字段的索引有没有生效。这一步做完数据库侧的基础设施就确认了。第二步是启动项目后看日志。Tomcat 启动完成后重点看有没有 MyBatis 的 mapper XML 加载日志Spring 容器初始化时有没有 bean 创建失败的信息。没有异常不代表 SQL 就是对的但异常出现一定是配置链路某个环节有偏差。第三步是业务功能全链路验证。打开道路信息管理页面新增一条测试道路填写编号、名称、等级、起终点桩号新增一条损害类型比如“路面裂缝”新增一条评定等级记录。这三条基础数据分别对应三张维度表。然后做日常巡查记录选刚才建的道路关联损害类型填巡查人和损害描述提交后查看列表出现新记录。最后做定期检查记录给这条路打 85 分验证自动关联到 A 等级。全链路下来系统的核心逻辑就走通了。第四步是写一条统计 SQL 验证数据关联是否正确SELECT r.road_name, COUNT(p.id) AS patrol_count, SUM(CASE WHEN p.handle_status 0 THEN 1 ELSE 0 END) AS pending_count FROM road_info r LEFT JOIN patrol_record p ON r.id p.road_id GROUP BY r.id, r.road_name ORDER BY pending_count DESC;这条 SQL 能查出每条路的巡查总量和未处理数量LEFT JOIN 保证了没巡查记录的道路也会显示统计结果能反过来验证之前录入的数据外键关联是否准确。如果发现 patrol_count 是 0说明关联条件写错或者巡查记录里的 road_id 没对上。从那以后我每拿到一套 SSM 类型的项目都强制自己把“初始化数据库 → 启动看日志 → 全链路功能验证 → 写统计 SQL”这套流程走一遍不跳过任何一步。过程虽然略显繁琐但能在十分钟内筛出八成的配置问题和数据问题远远好过上线后被用户拿真数据测试。希望这套流程和上面的配置细节帮到你让你在这套道路养护管理系统上的复现过程少一些折腾。本文还有配套的精品资源点击获取