简介本资源为基于SSM框架、MySQL数据库与HTML前端构建的道路养护管理系统完整源码包面向计算机专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者可帮助理解企业级信息管理系统的分层架构与业务实现。系统围绕道路养护业务展开涵盖道路信息管理、损害类型信息管理、评定等级信息管理、日常巡查信息管理与定期检查信息管理等核心模块功能结构贴近实际养护工作流程。压缩包共894个文件约30.45MB包含124个java源文件、220个class编译文件、113个xml配置、110个js脚本、53个css样式及24个html页面另有jar依赖、sql脚本与war部署包覆盖从后端逻辑到前端交互的完整链路。目前已有357人学习下载适合通过阅读源码掌握SSM整合配置、数据库表设计与增删改查实现也可作为二次开发与功能扩展的基础工程。1. 道路养护管理系统从 SSM 到 MySQL 的落地路线图市政道路养护这行有个特点路面病害、巡查记录、维修工单、验收归档四件事永远在循环。很多养护单位还在用 Excel 加微信群的方式管这些事一条坑槽从发现到修补完成中间要经过拍照、上报、派工、施工、验收五六个环节信息全靠人传人月底统计的时候对不上账是常事。基于 SSMMySQLHTML 的道路养护管理系统解决的就是这个链条上的信息断层问题把病害上报、工单派发、维修记录、统计报表串成一条数据流让每个环节的状态可查、可追溯。这套技术栈的选择其实很务实。SSM 指 Spring SpringMVC MyBatis是国内 Java Web 教学和中小型项目里最成熟的组合资料多、坑位明确、招人好招。MySQL 负责持久化HTML 加少量 CSS/JS 做前端页面不引入前后端分离的复杂度部署一台普通服务器就能跑。适合谁适合做课程设计的学生、需要快速交付内部管理系统的开发者以及想用一套完整业务练手 SSM 全链路的工程师。下面从环境搭建一路讲到工单状态流转和统计查询的落地细节。2. 环境搭建与 SSM 骨架把项目跑起来的最小闭环2.1 JDK、Tomcat、Maven 的版本搭配SSM 项目翻车最多的地方不是代码是版本。Spring 5.x 配 JDK 8 最稳JDK 11 以上会遇到部分反射和模块化问题新手不建议碰。Tomcat 用 8.5 或 9.0Tomcat 10 把 javax 包名改成了 jakartaSSM 老项目直接报 ClassNotFound。Maven 用 3.6 以上即可。组件推荐版本说明JDK1.8Spring 5.x 兼容性最好Tomcat9.0.x不要用 10包名不兼容Maven3.6.3依赖管理MySQL5.7 或 8.08.0 注意驱动和时区Spring5.3.x与 JDK8 搭配稳定MySQL 安装这一步Windows 下建议直接下官网的 MSI 安装包勾选 MySQL Server 和 MySQL Workbench安装时设置 root 密码字符集选 utf8mb4。Linux 下用 apt 或 yum 装完记得执行mysql_secure_installation。装完先验证连接mysql -u root -p # 输入密码后进入 mysql 命令行 SHOW DATABASES; # 能看到 information_schema、mysql 等库说明连接正常如果报ERROR 2002 (HY000): Cant connect to local MySQL server through socket八成是服务没启动Windows 去服务列表启动 MySQL 服务Linux 执行systemctl start mysql。这个报错在搜索引擎里出现频率极高本质就是客户端找不到服务端 socket 文件。2.2 数据库建表养护业务的核心表结构道路养护系统的数据模型围绕「路段—病害—工单—维修记录」展开。核心表至少四张路段表 road_section、病害上报表 disease_report、工单表 work_order、维修记录表 maintenance_record。下面给出关键建表语句CREATE DATABASE road_maintenance DEFAULT CHARACTER SET utf8mb4; USE road_maintenance; -- 路段表 CREATE TABLE road_section ( id INT PRIMARY KEY AUTO_INCREMENT, section_name VARCHAR(100) NOT NULL COMMENT 路段名称, start_point VARCHAR(100) COMMENT 起点, end_point VARCHAR(100) COMMENT 终点, road_level VARCHAR(20) COMMENT 道路等级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 病害上报表 CREATE TABLE disease_report ( id INT PRIMARY KEY AUTO_INCREMENT, section_id INT NOT NULL COMMENT 所属路段, disease_type VARCHAR(50) COMMENT 病害类型坑槽/裂缝/沉陷, severity TINYINT DEFAULT 1 COMMENT 严重程度 1-3, description VARCHAR(500) COMMENT 描述, image_url VARCHAR(255) COMMENT 现场照片路径, report_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0待处理 1已派工 2已完成, FOREIGN KEY (section_id) REFERENCES road_section(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 工单表 CREATE TABLE work_order ( id INT PRIMARY KEY AUTO_INCREMENT, report_id INT NOT NULL COMMENT 关联病害上报, assignee VARCHAR(50) COMMENT 施工负责人, plan_start DATE COMMENT 计划开工, plan_end DATE COMMENT 计划完工, order_status TINYINT DEFAULT 0 COMMENT 0待接单 1施工中 2待验收 3已完结, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (report_id) REFERENCES disease_report(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status和order_status用 TINYINT 而不是字符串是为了查询和索引效率。severity默认值设为 1避免插入时漏传导致 NULL。外键约束保证病害一定挂在某个路段下工单一定关联某条病害上报这是数据一致性的底线。实际项目里如果追求写入性能可以把外键去掉改由应用层保证但中小型系统保留外键更省心。2.3 Maven 依赖与 SSM 整合配置pom.xml 里核心依赖就几块spring-context、spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、druid 连接池、jstl。版本对齐很关键mybatis-spring 的版本要和 mybatis 匹配否则启动时报NoClassDefFoundError。dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.30/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesMySQL 8.0 的驱动类名是com.mysql.cj.jdbc.DriverURL 里必须带时区和 SSL 参数否则启动报时区错误jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/road_maintenance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码serverTimezoneAsia/Shanghai解决The server time zone value is unrecognized报错useSSLfalse在本地开发时避免 SSL 握手警告。生产环境建议开启 SSL 并配置证书但内网系统关掉也无妨。连接池用 Druid在 spring-dao.xml 里配置 initialSize、maxActive、validationQueryvalidationQuerySELECT 1保证连接有效性检测。3. 病害上报与工单流转把业务状态机写对3.1 病害上报的 Controller 与文件上传病害上报是系统的入口养护巡查人员发现坑槽、裂缝后拍照上传填写路段、类型、严重程度。Controller 接收 multipart 请求先存图片再落库。Controller RequestMapping(/disease) public class DiseaseController { Autowired private DiseaseService diseaseService; PostMapping(/report) public String report(RequestParam(file) MultipartFile file, DiseaseReport report, HttpServletRequest request) throws IOException { if (!file.isEmpty()) { // 生成唯一文件名避免覆盖 String fileName UUID.randomUUID() _ file.getOriginalFilename(); String uploadDir request.getServletContext().getRealPath(/upload/); File dest new File(uploadDir fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); report.setImageUrl(/upload/ fileName); } report.setStatus(0); // 初始状态待处理 diseaseService.saveReport(report); return redirect:/disease/list; } }UUID.randomUUID()加原始文件名做前缀防止同名文件互相覆盖这是血泪经验——早期直接用原文件名两个人同时传IMG_001.jpg就丢图了。getRealPath(/upload/)拿到的是 webapp 下的物理路径部署到 Tomcat 后文件存在项目目录里重新部署 war 包会丢生产环境要改成外部挂载目录。report.setStatus(0)显式设置初始状态不依赖数据库默认值逻辑更清晰。3.2 工单状态流转从派工到验收工单状态机是整个系统最容易写乱的地方。状态定义0 待接单、1 施工中、2 待验收、3 已完结。每次状态变更都要校验前置状态不能从 0 直接跳到 3。Service public class WorkOrderService { Autowired private WorkOrderMapper workOrderMapper; // 状态流转规则key 是当前状态value 是允许的下一状态 private static final MapInteger, ListInteger TRANSITION new HashMap(); static { TRANSITION.put(0, Arrays.asList(1)); // 待接单 - 施工中 TRANSITION.put(1, Arrays.asList(2)); // 施工中 - 待验收 TRANSITION.put(2, Arrays.asList(3)); // 待验收 - 已完结 } public void changeStatus(Integer orderId, Integer targetStatus) { WorkOrder order workOrderMapper.selectById(orderId); if (order null) { throw new RuntimeException(工单不存在); } ListInteger allowed TRANSITION.get(order.getOrderStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new RuntimeException(非法状态流转 order.getOrderStatus() - targetStatus); } order.setOrderStatus(targetStatus); workOrderMapper.updateStatus(order); } }用 Map 定义流转规则比一堆 if-else 清晰得多加状态只改 Map。changeStatus先查再校验再更新三步缺一不可。并发场景下两个管理员同时操作同一工单可能都读到旧状态然后都通过校验解决办法是在 update 的 SQL 里加AND order_status #{oldStatus}做乐观锁更新影响行数为 0 就说明状态已被改抛异常让前端刷新。3.3 MyBatis 映射与动态 SQL 查询工单列表页需要按路段、状态、时间范围组合筛选用 MyBatis 的动态 SQL 最合适。select idselectByCondition resultMapworkOrderMap SELECT w.*, d.disease_type, s.section_name FROM work_order w LEFT JOIN disease_report d ON w.report_id d.id LEFT JOIN road_section s ON d.section_id s.id where if testsectionId ! null AND s.id #{sectionId} /if if testorderStatus ! null AND w.order_status #{orderStatus} /if if teststartDate ! null AND w.create_time gt; #{startDate} /if if testendDate ! null AND w.create_time lt; #{endDate} /if /where ORDER BY w.create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签自动处理第一个条件前的 AND避免手写WHERE 11的丑陋写法。gt;和lt;是 XML 里大于小于的转义直接写和会解析报错。LIMIT #{offset}, #{pageSize}做分页offset 由页码减一乘每页条数算出。注意 MyBatis 的#{}是预编译占位符能防注入${}是字符串拼接有注入风险排序字段如果要用${}必须在 Java 层做白名单校验。4. 统计报表与前端页面让数据能看能用4.1 按路段和病害类型的统计 SQL管理层最关心的是「哪个路段病害最多」「哪类病害占比最高」。用 GROUP BY 聚合查询直接出结果。-- 各路段病害数量统计 SELECT s.section_name, COUNT(d.id) AS disease_count FROM road_section s LEFT JOIN disease_report d ON s.id d.section_id GROUP BY s.id, s.section_name ORDER BY disease_count DESC; -- 病害类型分布 SELECT disease_type, COUNT(*) AS cnt, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM disease_report), 2) AS percent FROM disease_report GROUP BY disease_type;第一条用 LEFT JOIN 保证没有病害的路段也出现在结果里数量为 0否则管理层会以为路段漏了。第二条算百分比时乘 100.0 而不是 100因为整数除法会截断成 0这个坑很多人踩过。ROUND(..., 2)保留两位小数。4.2 HTML 页面与 JSTL 渲染前端不搞前后端分离用 JSP JSTL 直接渲染简单直接。列表页核心是遍历和状态显示。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 title工单列表/title /head body table border1 tr th工单号/thth路段/thth病害类型/thth状态/thth操作/th /tr c:forEach items${page.list} varorder tr td${order.id}/td td${order.sectionName}/td td${order.diseaseType}/td td c:choose c:when test${order.orderStatus 0}待接单/c:when c:when test${order.orderStatus 1}施工中/c:when c:when test${order.orderStatus 2}待验收/c:when c:otherwise已完结/c:otherwise /c:choose /td tda href/order/detail?id${order.id}详情/a/td /tr /c:forEach /table /body /htmlc:choose做状态码到中文的映射比在 Java 里拼字符串灵活。${order.id}是 EL 表达式注意 JSP 页面顶部contentType和pageEncoding都要设 UTF-8否则中文乱码。如果页面出现乱码先查三处JSP 的 contentType、web.xml 的 CharacterEncodingFilter、数据库连接 URL 的 characterEncoding。4.3 分页查询的封装分页是列表页标配封装一个 PageResult 类包含 list、total、pageNum、pageSize。public class PageResultT { private ListT list; private int total; private int pageNum; private int pageSize; public int getTotalPages() { return (total pageSize - 1) / pageSize; // 向上取整 } // getter/setter 省略 }(total pageSize - 1) / pageSize是向上取整的经典写法total10、pageSize3 时算出 4 页。Service 层先查 count 再查 list两次查询之间数据可能变化导致页码对不上对一致性要求高的场景可以用同一事务包住。5. 避坑与排查SSM 项目里最容易翻车的五件事5.1 中文乱码从请求到响应全链路排查现象表单提交后数据库里存的是问号或者页面显示乱码。原因通常有三层请求编码、数据库连接编码、响应编码。解决web.xml 里配 CharacterEncodingFilter 设 forceEncoding 为 trueJDBC URL 加characterEncodingutf8数据库和表都用 utf8mb4JSP 页面 contentType 设 UTF-8。四层都对齐才彻底解决只改一处往往还有残留。5.2 事务不生效Service 没加 Transactional 或方法非 public现象工单状态更新了但维修记录没插入数据不一致。原因Transactional 加在了 Controller 上或者加在了 private 方法上Spring AOP 代理不到。解决注解加在 Service 的 public 方法上且确保该类被 Spring 管理。另外同类内部方法调用不会触发事务因为不走代理需要注入自身或拆到另一个 Service。5.3 MyBatis 返回 null字段名和属性名没对上现象查询结果里某个字段一直是 nullSQL 单独执行有值。原因数据库列名section_name和 Java 属性sectionName没配映射MyBatis 默认不开驼峰转换。解决在 mybatis-config.xml 里加setting namemapUnderscoreToCamelCase valuetrue/或者手写 resultMap 显式映射。推荐开驼峰转换省事。5.4 连接池耗尽连接没关或 maxActive 太小现象系统跑一段时间后报wait millis 60000, active 20请求全部卡住。原因手写 JDBC 代码忘了关 Connection或者 maxActive 设太小扛不住并发。解决用 MyBatis 和 Spring 管理连接会自动关闭如果手写 JDBC 必须放 finally 里 close。maxActive 根据并发量调一般 20 到 50配合maxWait设置等待超时。5.5 图片上传后访问 404路径和虚拟目录没配现象上传成功但页面显示裂图。原因文件存到了 webapp 外部目录Tomcat 没有映射该路径。解决要么存到 webapp 内部但重新部署会丢要么在 Tomcat 的 server.xml 里配 Context 的 docBase 指向外部目录要么用 Nginx 做静态资源代理。生产环境推荐外部目录加 Nginx开发和部署分离。6. 进阶技巧用存储过程和索引把统计查询压到毫秒级数据量上来之后统计报表会变慢。我一般做两件事给高频查询字段加索引把复杂统计逻辑下沉到存储过程。索引方面disease_report表的section_id、status、report_time是查询高频字段建联合索引idx_section_status_time (section_id, status, report_time)。注意联合索引的最左前缀原则查询条件必须从 section_id 开始才能用上。work_order表的order_status和create_time也建议建索引。存储过程适合把「按路段统计各状态工单数」这类多步聚合封装起来DELIMITER // CREATE PROCEDURE stat_order_by_section(IN start_date DATE, IN end_date DATE) BEGIN SELECT s.section_name, SUM(CASE WHEN w.order_status 0 THEN 1 ELSE 0 END) AS wait_count, SUM(CASE WHEN w.order_status 1 THEN 1 ELSE 0 END) AS doing_count, SUM(CASE WHEN w.order_status 3 THEN 1 ELSE 0 END) AS done_count FROM road_section s LEFT JOIN disease_report d ON s.id d.section_id LEFT JOIN work_order w ON d.id w.report_id WHERE w.create_time BETWEEN start_date AND end_date GROUP BY s.id, s.section_name; END // DELIMITER //调用时CALL stat_order_by_section(2024-01-01, 2024-12-31)。用SUM(CASE WHEN ...)做条件计数比多次查询再拼装快得多。DELIMITER //是为了让 MySQL 把整个存储过程当成一个语句不然遇到内部分号就截断了。验证优化效果用 EXPLAINEXPLAIN SELECT s.section_name, COUNT(d.id) FROM road_section s LEFT JOIN disease_report d ON s.id d.section_id GROUP BY s.id;看 type 列是不是 ref 或 rangekey 列有没有用上索引rows 列扫描行数是否合理。如果 type 是 ALL 说明全表扫描得检查索引。我自己的习惯是每加一个统计查询就先 EXPLAIN 一遍别等上线了被用户投诉慢再回头查那时候改索引要停服后悔药没地方买。这套系统我前后搭过几次最大的体会是业务状态机一定要在动手写代码前画清楚状态定义和流转规则定死了后面写 Service 就是填空题定不清楚写到一半改状态前面所有查询和页面都得跟着改。另外数据库字段的默认值和约束能加就加别指望应用层每次都传对数据库是最后一道防线。希望帮到你。本文还有配套的精品资源点击获取
