简介:基于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.version>5.2.15.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> </properties> <dependencies> <!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <!-- JSON 序列化 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.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 location="classpath:db.properties"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.road.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.road.dao"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </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.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/road_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456驱动类名这里是个高频错点: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-name>contextConfigLocation</param-name> <param-value>classpath:spring-context.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>characterEncoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <force-encoding>true</force-encoding> </filter> <filter-mapping> <filter-name>characterEncoding</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-package="com.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 损害类型表 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 评定等级表 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 日常巡查记录表 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套设计有几个关键决策值得说。首先是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); List<RoadInfo> selectList(@Param("keyword") String keyword, @Param("roadLevel") String roadLevel); }<mapper namespace="com.road.dao.RoadInfoMapper"> <sql id="base_columns"> id, road_code, road_name, road_level, start_point, end_point, length_km, manager, create_time </sql> <select id="selectList" resultType="RoadInfo"> SELECT <include refid="base_columns"/> FROM road_info <where> <if test="keyword != null and keyword != ''"> AND (road_code LIKE CONCAT('%', #{keyword}, '%') OR road_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="roadLevel != 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) { List<PatrolRecordVO> list = patrolService.queryList(roadId, startDate, endDate); return Result.success(list); } }@ResponseBody负责把返回值序列化成 JSON,前端 HTML 里的 jQuery Ajax 直接拿这个 JSON 渲染表格。这套模式在 SSM 项目里是标准打法,没有页面跳转逻辑,后端只提供数据接口,前端负责展示,职责分离明确。@RequestParam(required = false)处理查询参数的可选性,不传时值为 null,MyBatis 动态 SQL 里用<if test="xxx != null">判断,拼接条件查询。
4. 日常巡查与定期检查模块实现:前端 HTML 到后端入库的完整闭环
整个系统里,日常巡查和定期检查是使用频率最高的两个业务模块。这一章从前端页面向后端一路拆,把一条巡查记录从填表到入库的完整链路走一遍,同时把 SSM 常用注解在这个模块里的实际用法对照起来讲。
4.1 前端页面:纯 HTML + jQuery 的表格和表单
前端页面直接放在 webapp 目录下,用原生的 HTML 加 jQuery 库。页面不复杂,两块核心内容:顶部的查询条件栏,中间的记录表格。查询栏和表格靠 Ajax 联动,条件是参数,表格是结果展示。
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>日常巡查记录</title> <script src="js/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 class="query-bar"> 道路名称:<input type="text" id="keyword" placeholder="输入道路名称或编号"> 损害类型: <select id="damageType"> <option value="">全部</option> <option value="1">裂缝</option> <option value="2">坑槽</option> <option value="3">沉陷</option> </select> <button onclick="loadData()">查询</button> </div> <table> <thead> <tr> <th>道路名称</th> <th>巡查日期</th> <th>巡查人</th> <th>损害类型</th> <th>损害描述</th> <th>处理状态</th> </tr> </thead> <tbody id="tableBody"></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> </html>JSP 项目里页面通常会用到 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 写入响应体,不走视图解析器。它的兄弟是@RequestBody,POST 请求时把 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 分以上是 A,70-80 是 B,60-70 是 C,60 以下是 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.driver=com.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.url=jdbc:mysql://localhost:3306/road_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8useSSL=false 关闭加密通道,本地开发和内网环境足够用。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接口全部返回 404,Network 面板显示请求路径不对或者根本没有到达后端。
原因:最常见的是项目部署容器路径和请求 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 charset,Tomcat 接收 POST 请求时没有指定字符集,MySQL 表的字符集不是 utf8mb4。任何一层没有对齐,中文传输过程中就会出现编码损失。
解决:三层逐一补齐。HTML 里加<meta charset="utf-8">,web.xml 里配置 CharacterEncodingFilter(配置方式见 2.3 节),MySQL 建表时带上DEFAULT CHARSET=utf8mb4。还有一个细节:数据库连接串里的characterEncoding=utf8要和表字符集一致,如果用 utf8mb4,连接串里写characterEncoding=utf8也能兼容,但新库建议直接统一 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”这套流程走一遍,不跳过任何一步。过程虽然略显繁琐,但能在十分钟内筛出八成的配置问题和数据问题,远远好过上线后被用户拿真数据测试。希望这套流程和上面的配置细节帮到你,让你在这套道路养护管理系统上的复现过程少一些折腾。
本文还有配套的精品资源,点击获取