基于SSM+MySQL的道路养护管理系统设计与实现

发布时间:2026/9/15 23:10:09
基于SSM+MySQL的道路养护管理系统设计与实现 简介集中面向道路养护信息化管理项目开发者的SSMMySQLHTML后台管理系统源码包覆盖道路信息管理、损害类型管理、评定等级管理、日常巡查与定期检查等核心模块适合高校毕业设计、课程设计或中小型市政养护系统二次开发参考。压缩包内共894个文件大小30.45MB主要包含Java业务代码、MyBatis映射XML、JSP/HTML页面、JavaScript/CSS前端资源及数据库SQL脚本另有Maven工程配置与部署相关文件便于直接导入开发环境查看项目结构与运行逻辑。目前已有357人学习资源提供完整功能展示对应的可运行工程读者可结合博文详情了解各模块设计思路快速掌握SSM框架在道路养护场景下的分层实现方式也可根据实际需求扩展巡检记录与等级评定流程。1. 道路养护管理系统为什么还选 ssmmysqlhtml公路养护部门日常要做的事说白了就是三件巡检发现病害、派单维修、验收归档。这活儿看着简单真干起来数据量不小——每天上百条巡查记录每一条都要关联路段桩号、病害类型、严重程度、处理状态最后还要生成月底统计报表。多数小型养护单位到现在还用 Excel 登记查一条历史工单要翻半天文件更别说按路段、按病害类型做统计了。这个标题给的就是一条比较务实的路前端用 html 页面直接渲染和交互后端用 ssm 框架Spring SpringMVC MyBatis组织业务逻辑数据落在 mysql 里。它适合两类人一是正在做毕设、需要找到一个完整可落地的 JavaWeb 项目骨架的学生二是想用最低成本给班组搭一套内部工单系统的运维或养护管理人员。这套组合没有微服务、没有前后端分离但恰恰因为它结构简单、依赖少、部署直观反而能稳稳跑完“录入-流转-统计”这条主线。下面从数据库设计开始把一套能跑的养护管理系统拆开讲。2. 先把 mysql 表结构定好再搭 ssm 工程骨架2.1 道路养护核心表设计字段和索引一次想清楚道路养护管理系统里最核心的实体不是“用户”而是“病害”和“工单”。我一般先画数据流巡检员在某个路段发现坑槽或龟裂上报一条病害记录管理员看到记录后生成养护工单派给施工队施工队处理完回填结果管理员验收关闭。围绕这条线至少要建四张表路段表、病害上报表、养护工单表、状态变更日志表。以下是建表 SQL 的核心部分。CREATE TABLE t_road_section ( id INT PRIMARY KEY AUTO_INCREMENT, road_code VARCHAR(32) NOT NULL COMMENT 路段编码, road_name VARCHAR(64) NOT NULL COMMENT 路段名称, start_km DECIMAL(8,2) NOT NULL COMMENT 起点桩号, end_km DECIMAL(8,2) NOT NULL COMMENT 终点桩号, manager VARCHAR(32) COMMENT 路段负责人, remark VARCHAR(255) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_disease_report ( id INT PRIMARY KEY AUTO_INCREMENT, road_id INT NOT NULL COMMENT 关联路段, disease_type VARCHAR(16) NOT NULL COMMENT 病害类型坑槽/龟裂/沉陷/车辙, disease_level TINYINT NOT NULL DEFAULT 0 COMMENT 0轻度 1中度 2重度, position_desc VARCHAR(128) COMMENT 具体位置描述, report_user VARCHAR(32) NOT NULL COMMENT 上报人, report_time DATETIME NOT NULL COMMENT 上报时间, photo_url VARCHAR(255) COMMENT 现场照片路径, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待派单 1已派单 2施工中 3待验收 4已完工 5已驳回, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, KEY idx_road_time (road_id, report_time), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_maintenance_order ( id INT PRIMARY KEY AUTO_INCREMENT, report_id INT NOT NULL COMMENT 关联病害上报, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 工单号, assign_user VARCHAR(32) COMMENT 派单人, assign_time DATETIME COMMENT 派单时间, execute_team VARCHAR(64) COMMENT 施工班组, plan_finish_date DATE COMMENT 计划完成日期, actual_cost DECIMAL(10,2) DEFAULT 0 COMMENT 实际费用, finish_time DATETIME COMMENT 完工时间, accept_user VARCHAR(32) COMMENT 验收人, accept_time DATETIME COMMENT 验收时间, accept_result VARCHAR(255) COMMENT 验收意见, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待施工 1施工中 2待验收 3已完工 4已驳回, version INT NOT NULL DEFAULT 0, KEY idx_report (report_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型上有一个容易被忽略的点病害等级用 TINYINT 而不是 VARCHAR因为后续要按等级做统计和排序数字类型在范围查询和索引上比字符串更友好。金额类字段用 DECIMAL(10,2)避免 FLOAT 的精度问题。桩号字段用 DECIMAL(8,2)可以精确到厘米级。索引方面idx_road_time (road_id, report_time)是典型的联合索引直接服务“查某路段最近三个月的病害记录”这个高频查询单独给 status 建索引是因为列表页默认会按状态筛选待办工单。2.2 用 Maven 搭 ssm 骨架依赖版本是个隐形坑建完表后开始搭工程。这个标题组合最常见的工程结构是 Maven 的 war 包项目目录上分成 controller、service、mapper、entity 四层webapp 目录下放 HTML、CSS、JS 静态资源。pom.xml 里需要引入的核心依赖有五个spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid 连接池。版本上我一般固定用 Spring 5.3.x 搭配 MyBatis 3.5.x不要追最新大版本因为 Spring 6 和 jakarta 命名空间会让很多老教程失效。以下是一个可直接用的依赖清单片段。properties spring.version5.3.30/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency /dependencies版本搭配上有一个值得记住的组合mysql-connector-java 8.x 的驱动类名是com.mysql.cj.jdbc.DriverURL 必须带serverTimezoneAsia/Shanghai否则本地测试时会报时区错误。jackson-databind 用来把 Controller 返回的对象序列化成 JSON 给前端 html 的 Ajax 调用这个依赖经常被忘记加导致页面能打开但数据始终加载不出来。2.3 三层配置Spring 管对象SpringMVC 管请求MyBatis 管 SQL工程搭好后配置是 ssm 项目最容易卡壳的地方。我习惯把配置拆成三个文件spring-mvc.xml 负责扫描 controller 包和静态资源放行spring-mybatis.xml 负责数据源、SqlSessionFactory、Mapper 扫描和事务管理器web.xml 里配置 DispatcherServlet 和字符编码过滤器。下面给出 spring-mybatis.xml 的关键部分因为它是整个数据链路的中枢。context:component-scan base-packagecom.road.service / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver / property nameurl valuejdbc:mysql://localhost:3306/road_maintain?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai / property nameusername valueroot / property namepassword valueroot / property nameinitialSize value5 / property namemaxActive value20 / /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property namemapperLocations valueclasspath:mapper/*.xml / property nametypeAliasesPackage valuecom.road.entity / property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue / /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.road.mapper / /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource / /bean配置里有三个细节决定能不能跑通mapUnderscoreToCamelCase设为 true 后数据库的report_user字段能自动映射到实体类的reportUser属性少写几十行 resultMapmapperLocations里的classpath:mapper/*.xml是把 SQL 语句统一收进 resources 目录写 XML 文件而不是写在注解里这样复杂动态 SQL 更容易维护连接池参数里initialSize和maxActive按照内部系统二十人同时在线的规模设置就够不需要调大。事务管理器这一行很多人落下落下之后Transactional注解会静默失效数据写到一半出错时不会回滚工单状态就乱了。3. Controller 到 Mapper 的查询链路用动态 SQL 做病害列表筛选3.1 三层代码怎么写请求从哪里进、数据怎么出查询链路按 ssm 的标准走法执行浏览器发 HTTP 请求到 SpringMVC 的 DispatcherServlet它根据 RequestMapping 找到对应的 Controller 方法Controller 调 Service 接口Service 实现类里调 Mapper 接口Mapper 接口绑定同名 XML 文件里的 SQL把结果集映射成实体类再逐层返回最后由 Controller 用 ResponseBody 转成 JSON 交给前端。以病害列表查询为例Controller 层代码大致是这样。Controller RequestMapping(/disease) public class DiseaseController { Autowired private DiseaseService diseaseService; RequestMapping(/list) ResponseBody public Result list(Integer pageNum, Integer pageSize, String roadName, Integer diseaseLevel, String startTime, String endTime) { PageHelper.startPage(pageNum null ? 1 : pageNum, pageSize null ? 10 : pageSize); ListDiseaseVO list diseaseService.queryPage(roadName, diseaseLevel, startTime, endTime); PageInfoDiseaseVO pageInfo new PageInfo(list); return Result.ok(pageInfo); } }这段代码的逻辑分三段理解PageHelper.startPage是分页插件的入口它通过 MyBatis 拦截器把下一条查询 SQL 自动拼上LIMIT ?pageNum 从 1 开始DiseaseVO是视图对象里面除了病害字段还加了roadName这是通过查询时 JOIN 路段表带出来的避免前端再单独发请求查路段名Result.ok是一个统一返回体包含 code、message、data 三个字段前端 html 里的 Ajax 拿到后先判断 code 再做渲染。参数上有个约定pageSize 上限在 Service 里要限制到 100防止有人一次拉走全表数据。3.2 Mapper 里的动态 SQL 是检索的核心条件拼接按需生效Service 层一般不写复杂逻辑真正的筛选逻辑在 Mapper XML 里。下面的 SQL 是病害分页查询的核心支持按路段名模糊匹配、按病害等级精确匹配、按上报时间范围筛选。SELECT r.id, r.disease_type, r.disease_level, r.position_desc, r.report_time, r.report_user, r.status, s.road_name FROM t_disease_report r LEFT JOIN t_road_section s ON r.road_id s.id WHERE 1 1 if testroadName ! null and roadName ! AND s.road_name LIKE CONCAT(%, #{roadName}, %) /if if testdiseaseLevel ! null AND r.disease_level #{diseaseLevel} /if if teststartTime ! null and startTime ! AND r.report_time gt; #{startTime} /if if testendTime ! null and endTime ! AND r.report_time lt; #{endTime} /if ORDER BY r.report_time DESC写这段 SQL 时有几个习惯值得保持WHERE 11看着不优雅但它是 MyBatis 动态 SQL 最稳妥的解法——如果去掉它第一个if不成立而第二个成立拼出来的 SQL 就是WHERE AND disease_level ?直接语法错误时间字段用gt;和lt;是因为 XML 里不能裸写大于号和小于号CONCAT(%, #{roadName}, %)比在 Java 代码里拼好%值%再传进来更安全没有 SQL 注入的写法问题。排序固定用report_time DESC保证新上报的病害排在前面这个排序字段也是表里的索引列数据量大时不会额外产生 filesort。3.3 前端 html 页面如何消费 JSON表格渲染和翻页怎么做标题里特别提到了 html说明前端不走 JSP 也不做前后端分离而是直接用静态页面加 Ajax。我在 webapp 目录下建一个diseaseList.html页面里面有筛选表单、表格容器和翻页按钮数据通过fetch获取。一个通用的渲染函数骨架如下。script async function loadPage(pageNum) { const params new URLSearchParams({ pageNum: pageNum, pageSize: 10, roadName: document.getElementById(roadName).value, diseaseLevel: document.getElementById(diseaseLevel).value }); const resp await fetch(/disease/list? params.toString()); const body await resp.json(); if (body.code ! 200) { alert(加载失败: body.message); return; } renderTable(body.data.list); renderPager(body.data.pageNum, body.data.pages); } /scriptfetch 的 GET 请求参数用 URLSearchParams 生成比手工拼字符串更规范返回体里body.data.list是当前页数据集合body.data.pages是总页数这两个字段来自 PageInfo 的序列化结果。要注意 PageHelper 返回的 PageInfo 里字段名是pageNum、pageSize、total、pages、list前端字段名要和它对齐写错一个就渲染不出来。renderTable 函数内部用document.createElement(tr)动态建行再通过textContent赋值不用 innerHTML 拼数据避免 XSS 注入风险——病害描述字段是用户输入不能直接当 HTML 解析。4. 养护工单的状态流转事务和乐观锁是 ssm 落地成败的关键4.1 用状态机表设计守住业务边界六种状态不许乱跳道路养护的工单流转不是随便改个状态值那么简单。一个病害从上报到归档状态必须按固定路径走待派单 → 已派单 → 施工中 → 待验收 → 已完工。驳回是一个分支从待派单可以直接回到待派单并重置派单信息。如果代码里谁都能随便改状态就会出现施工队还没干活工单就已经验收完的乱象。状态机定义用一张表约束是 ssm 和 mysql 项目里最廉价可靠的方案。status 值含义允许进入的操作前置状态0待派单上报病害后默认状态无1已派单管理员派给施工队02施工中施工队确认开工13待验收施工队提交完工说明24已完工管理员验收通过35已驳回验收不合格 / 派单信息有误1, 3判定状态是否允许跳转的逻辑写在 Service 层用一个 Map 配置合法的状态转移路径而不是散落在大段的 if-else 里。例如Map.of(0, new Integer[]{1}, 1, new Integer[]{2, 5})这样新加一个状态只改 Map 不动业务代码后续审计也一目了然。4.2 Transactional 在派单操作上的正确用法和常见失败场景派单操作同时要更新病害表的 status、创建工单记录、写入一条状态变更日志三个动作必须在一个事务里完成。以下这段代码是 Service 层实现派单的核心逻辑。Service public class MaintenanceServiceImpl implements MaintenanceService { Autowired private DiseaseReportMapper diseaseReportMapper; Autowired private MaintenanceOrderMapper orderMapper; Autowired private StatusLogMapper statusLogMapper; Override Transactional(rollbackFor Exception.class) public void assignOrder(AssignDTO dto) { DiseaseReport report diseaseReportMapper.selectByIdForUpdate(dto.getReportId()); if (report null) { throw new BusinessException(病害记录不存在); } if (report.getStatus() ! 0) { throw new BusinessException(当前状态不允许派单); } int updated diseaseReportMapper.compareAndSetStatus( dto.getReportId(), 0, 1, report.getVersion()); if (updated ! 1) { throw new BusinessException(操作冲突请刷新后重试); } MaintenanceOrder order new MaintenanceOrder(); order.setReportId(report.getId()); order.setOrderNo(generateOrderNo()); order.setExecuteTeam(dto.getExecuteTeam()); order.setPlanFinishDate(dto.getPlanFinishDate()); order.setStatus(0); orderMapper.insert(order); statusLogMapper.insert(StatusLog.of(report.getId(), report.getStatus(), 1, 管理员派单)); } }这段代码有四个关键点。selectByIdForUpdate是加了FOR UPDATE的查询它在 mysql 的 InnoDB 引擎下会对这一行加排他锁第二个管理员同时点派单时会被阻塞等锁超时后由compareAndSetStatus返回 0 触发“操作冲突”提示。compareAndSetStatus对应的 SQL 是UPDATE t_disease_report SET status #{newStatus}, version version 1 WHERE id #{id} AND status #{oldStatus} AND version #{oldVersion}用受影响行数判断是否更新成功。Transactional(rollbackFor Exception.class)里的 rollbackFor 必须写因为 Spring 默认只回滚 RuntimeException而自定义的 BusinessException 常常继承 Exception不加这个参数事务死活不生效。generateOrderNo()生成工单号我常用DateUtil.format(new Date(), yyyyMMddHHmmss) 四位随机数保证并发情况下不撞号。4.3 施工和验收的消息提醒怎么用 html 页面定时轮询待办数量养护人员打开系统首页时最关心的是有多少病害等着派单、多少工单等着验收。这个需求在 ssmhtml 架构下不用 WebSocket直接前端定时轮询接口即可。html 页面里写一个setInterval每 30 秒请求一次/dashboard/todoCount接口返回待办数量后在导航栏的角标元素上更新数字。实现很简单但有一个参数值得注意轮询间隔要大于后端处理时间还要考虑多人同时轮询的压力。内部系统几十人同时在线30 秒一次完全够用如果间隔小于 10 秒mysql 的 QPS 会被这种无效查询白白消耗掉。5. 给 mysql 做一次慢查询走查参数和索引该调的地方就调这套系统上线前我最常做的一件事是把巡检数据灌到十万条以上然后逐个页面点一遍开着 mysql 的慢查询日志看哪些 SQL 拖后腿。慢查询日志的开关在 my.cnf 里slow_query_log ONlong_query_time 1等于说执行超过 1 秒的 SQL 全部记录到日志文件里。灌测试数据用 mysql 的递归 CTE 可以一次生成上万条巡检记录例如设置一个WITH RECURSIVE seq AS (SELECT 1 AS n UNION ALL SELECT n 1 FROM seq WHERE n 10000)再配合DATE_ADD生成递增的时间批量 INSERT 进巡检表不用写存储过程就能快速制造数据。实际走查中遇到最多的三类问题都在索引上。第一类是隐式类型转换t_disease_report表的report_user是 VARCHAR 类型页面筛选条件传过来的是数字 123mysql 就会把字段值全部转成数字再比较索引直接失效。排查方法很简单看WHERE report_user 123的执行计划如果 type 是 ALL 就中招了。第二类是函数包裹索引列比如按月份查巡检记录时写了WHERE DATE_FORMAT(report_time, %Y-%m) 2024-06这会让idx_road_time用不上应该改写为report_time 2024-06-01 AND report_time 2024-07-01。第三类是排序和查询条件没有复合索引页面默认按时间倒序查某路段纪录但 SQL 先按 road_id 过滤再排序单独建 road_id 索引或者时间索引都不够必须用(road_id, report_time)这样的联合索引才能兼顾过滤和排序。关于连接池参数还有一个容易忽略的指标Druid 的maxWait默认是 -1意思是拿不到连接时不等待直接抛异常。内部系统经常出现早晨上班那一刻 20 个人同时打开页面连接池被占满后后面的请求全部报错。把maxWait设为 5000配合testWhileIdle为 true让线程排队等待 5 秒而不是立刻失败运维压力会小很多。最后检查一遍 mysql 的innodb_buffer_pool_size如果机器内存是 8G 就设到 2G默认的 128M 缓存区在灌了测试数据后连索引都放不下再好的 SQL 也跑不出性能。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询