news 2026/10/10 9:25:53

SSM+MySQL+HTML实战:道路养护管理系统的架构设计与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+MySQL+HTML实战:道路养护管理系统的架构设计与避坑指南

简介:这是一份基于SSM(Spring+SpringMVC+MyBatis)框架与MySQL数据库开发的道路养护管理系统完整项目,面向计算机相关专业毕业设计、课程设计或SSM框架学习者。系统包含道路信息、损害类型、评定等级、日常巡查、定期检查等核心模块,覆盖道路养护业务的主要流程。压缩包共894个文件,约30.45MB,内含124个Java源文件、220个编译后class文件、113个XML配置、110个JavaScript脚本、53个CSS样式及24个HTML页面,并附有SQL数据库脚本,代码结构完整,便于导入IDE运行与二次开发。已有357人学习下载,资源涵盖完整业务模块与典型SSM三层架构,可帮助读者快速理解道路养护系统的数据库设计、MVC分层与前后端交互方式,也适合作为毕业设计论文写作与功能演示的参考素材。

1. 道路养护管理系统到底在管什么:SSM+MySQL+HTML这个组合还值不值得做

道路养护管理系统,说白了就是给公路养护业务上一套"账本+流程":哪段路什么时候该巡检,巡检发现坑槽、裂缝、沉陷怎么登记,派给哪个维修队,干完没干完,验收结论如何,花了多少钱,月底怎么考核。这些事情靠微信群加Excel也能跑,但一旦路段数量过百、养护人员超过20人,信息就开始打架。用SSM+MySQL+HTML来做,是目前基层养护单位里非常典型的技术形态:Spring管对象、SpringMVC管接口、MyBatis管SQL、MySQL存数据、HTML页面负责展示和交互。它不新潮,但稳定、便宜、招人容易、部署简单。这篇笔记不探讨微服务怎么落地,就回答一个问题:这套被很多人喊过时的组合,在道路养护场景下到底怎么用、坑在哪、值不值得继续投入。

2. 架构职责与数据模型:先想清楚谁干什么

2.1 为什么选SSM而不是Spring Boot:分层边界与请求流转

先说结论:新建项目我也愿意用Spring Boot,但接手或仿照历史系统做养护管理,SSM的清晰分层往往比Spring Boot的"自动化配置"更好维护。这里的关键不是框架新旧,而是边界是否清楚。SSM的标准三层是Controller、Service、Mapper,每一层只做一件事,新人抱着代码看十分钟就能知道一次请求从哪进、从哪出。

一次典型请求是这么流转的:页面里的HTML通过Ajax发请求到SpringMVC的DispatcherServlet,DispatcherServlet根据URL匹配到Controller方法;Controller只做参数接收、调用Service、把Service结果包成统一JSON返回;Service里写业务规则,比如病害状态能不能从"维修中"直接跳到"已删除";Service再调用Mapper接口,由MyBatis把方法调用翻译成SQL发给MySQL;结果一层层返回,前端拿到JSON后渲染DOM。这套流程在任何SSM项目里都是一样的,学会一次,换项目也能用。

选型还有一个现实理由:存量系统的维护成本。很多单位前些年建的系统就是SSM,代码还躺在仓库里,运维和外包人员最熟悉的也是这套技术栈。在道路养护这种业务逻辑不复杂、并发量极低的系统里,为了"时髦"去重构,风险远大于收益。我一般只有在新起盘、团队自己可控时才会换Spring Boot,否则就顺着老架构做加法。

2.2 核心表结构:从路段到维修工单的四张关键表

做养护管理系统,第一步不是写代码,而是把表设计出来。我见过不少翻车案例,都是因为把"病害"和"维修工单"揉在一张表里,最后状态互相覆盖。常见的做法是拆成四张表:路段基础表、巡检计划表、病害台账表、维修工单表。下面是核心建表SQL。

CREATE TABLE road_section ( id BIGINT AUTO_INCREMENT PRIMARY KEY, road_code VARCHAR(32) NOT NULL COMMENT '路段编码,如ROAD-X-001', road_name VARCHAR(128) NOT NULL COMMENT '路段名称', start_km DECIMAL(8,2) NOT NULL COMMENT '起始桩号', end_km DECIMAL(8,2) NOT NULL COMMENT '结束桩号', road_level TINYINT NOT NULL COMMENT '公路等级:1高速 2国道 3省道 4县道', management_org VARCHAR(128) NOT NULL COMMENT '管养单位名称', UNIQUE KEY uk_road_code (road_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='路段基础信息表';

桩号是道路行业的里程点,用DECIMAL而不是VARCHAR,是为了后续能用区间查询——比如"我这个病害发生在K12+200到K12+300之间",直接比较数值即可。road_code加唯一索引,因为路线上传或人工录入最容易重复。管理单位和公路等级决定巡检频率和派单权重,后面生成巡检计划时会用到。

CREATE TABLE inspect_plan ( id BIGINT AUTO_INCREMENT PRIMARY KEY, road_section_id BIGINT NOT NULL COMMENT '关联路段表id', plan_date DATE NOT NULL COMMENT '计划巡检日期', inspector VARCHAR(64) NOT NULL COMMENT '巡检人', status TINYINT NOT NULL DEFAULT 0 COMMENT '0未执行 1已执行 2已关闭', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_plan_date (plan_date), CONSTRAINT fk_plan_section FOREIGN KEY (road_section_id) REFERENCES road_section(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='巡检计划表';

巡检计划表把"计划"和"执行"分开。status从0到1表示这次巡检确实有人去了,并且录入了巡查记录;2表示因恶劣天气等原因取消。plan_date上建索引,因为月底要按日期统计各路段巡检完成率。

CREATE TABLE disease_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, road_section_id BIGINT NOT NULL COMMENT '关联路段表id', disease_type TINYINT NOT NULL COMMENT '病害类型:1坑槽 2裂缝 3沉陷 4车辙', position_desc VARCHAR(256) COMMENT '位置描述,如行车道左侧', severity TINYINT NOT NULL COMMENT '严重程度:1轻微 2中等 3严重', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待派单 1已派单 2维修中 3待验收 4已验收 5已归档', photo_url VARCHAR(255) COMMENT '现场照片URL', reporter VARCHAR(64) COMMENT '上报人', report_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上报时间', repair_order_id BIGINT DEFAULT NULL COMMENT '关联维修工单id,派单后写入', KEY idx_disease_status (status), KEY idx_disease_section (road_section_id), CONSTRAINT fk_disease_section FOREIGN KEY (road_section_id) REFERENCES road_section(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='病害台账表';

病害状态是这套系统的核心状态机。单独用一列status,比用字符串"待派单/已派单"更好维护,查询和索引都方便。repair_order_id做成可空字段,派单成功后再写,避免在工单表里反向维护多对一关系时出现数据不一致。

CREATE TABLE repair_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, disease_record_id BIGINT NOT NULL COMMENT '关联病害id', assignee VARCHAR(64) NOT NULL COMMENT '维修负责人', priority TINYINT NOT NULL DEFAULT 1 COMMENT '优先级:1普通 2紧急 3特急', plan_finish_time DATETIME NOT NULL COMMENT '要求完成时间', actual_finish_time DATETIME DEFAULT NULL COMMENT '实际完成时间', cost DECIMAL(10,2) DEFAULT NULL COMMENT '维修费用', remark VARCHAR(255) COMMENT '备注', UNIQUE KEY uk_disease (disease_record_id), CONSTRAINT fk_order_disease FOREIGN KEY (disease_record_id) REFERENCES disease_record(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维修工单表';

工单表和病害表是一对一关系,所以给disease_record_id加了唯一索引。这样设计的好处是:一个病害只能派一张工单,后续不管怎么改状态,都跑不出"一条病害对应一条维修记录"的闭环。实际项目里如果允许一张工单处理多个病害,就需要拆成中间表,但大多数养护场景用一对一就够用。

下面把status字段的流转规则列出来,这是你后面写Service层时的依据:

状态值含义触发操作下一状态
0待派单上报病害后1(派单)
1已派单创建工单2(开工)
2维修中维修队开始施工3(报完工)
3待验收维修队申请验收4(验收通过)或2(验收不通过)
4已验收验收合格5(归档)
5已归档自动或手动—

2.3 前后端约定:统一JSON返回结构与第一个工具类

用HTML而不是JSP,意味着前端拿到的必须是干净的数据,而不是一段混着标签的页面。因此接口层需要一个统一的返回结构,否则前端每个页面都要写一套判空逻辑,还容易漏处理异常。我一般会定义一个Result类,结构如下。

public class Result<T> { private int code; // 0成功,非0失败 private String msg; // 提示信息 private T data; // 具体业务数据 public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } // getter / setter 省略 }

code用0表示成功的原因很简单:在JavaScript里if (res.code === 0)比if (res.code === 200)语义更直接,而且不会和后端HTTP状态码混淆。业务校验失败和系统异常都可以返回code非0,由前端弹出msg里的提示。这样一个工具类就能贯穿所有Controller方法的返回。

在实际项目里,有人喜欢把Result类放到common包下,所有Controller方法都返回Result<?>。这是个好习惯,但要注意别把Result类当垃圾桶,任何字段都往里塞。规范是:data里只放当前接口需要的对象或列表,msg只放人类能读懂的提示,debug信息不要写给用户看,放到日志里。

3. 从空目录到跑通"病害列表"接口

3.1 Maven工程目录:每一层放什么

SSM项目建好目录,后面写代码才不会乱。我一般按下面的结构组织:

road-maintenance ├── pom.xml ├── src/main/java/com/example/system │ ├── controller # SpringMVC控制器 │ ├── service # 业务接口和实现 │ ├── mapper # MyBatis Mapper接口 │ ├── entity # 实体类 │ └── common # 通用结果、异常、常量 └── src/main/resources ├── spring # applicationContext.xml和spring-mvc.xml ├── mybatis # MyBatis配置和Mapper XML ├── jdbc.properties └── webapp └── static # HTML、JS、CSS

webapp/static放HTML,SpringMVC只需要放行静态资源,不需要配置JSP视图解析器。这样做前后分离有个好处:前端出问题不用重启后端,刷新页面就能看效果;但也有代价,就是所有数据都要走接口,前端人力和后端人力都少不了一个。如果你只是单兵作战,HTML页面的工作量要想清楚,别只盯着后端。

3.2 pom.xml关键依赖与选型参数

下面是一个能跑通SSM+MySQL+HTML的基础pom依赖片段:

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.3.x</spring.version> <mybatis.version>3.5.x</mybatis.version> </properties> <dependencies> <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.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.x</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.x</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.x</version> </dependency> </dependencies>

Spring兜底的5.3.x和MyBatis 3.5.x是SSM项目里比较成熟的搭配,对应JDK 8。mybatis-spring这个适配包一定要单独引入,它负责把MyBatis的SqlSessionFactory交给Spring容器管理,让Service里可以直接注入Mapper接口,而不是自己手动写Session管理。Druid在这里既当连接池,又提供监控页面,后续排查连接池耗尽时有大用。Jackson负责把Controller返回的对象序列化成JSON,HTML的Ajax才能拿到数据。

3.3 三份核心配置:数据源、Spring容器、SpringMVC

先看applicationContext.xml,它负责Spring容器和MyBatis的整合:

<context:component-scan base-package="com.example.system"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> <property name="minIdle" value="5"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.system.mapper"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

mybatis-config.xml里至少要开启驼峰映射,否则下划线字段映射不到Java驼峰属性:

<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>

然后把spring-mvc.xml单独给DispatcherServlet用:

<mvc:annotation-driven/> <context:component-scan base-package="com.example.system.controller"/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes" value="application/json;charset=UTF-8"/> </bean> </list> </property> </bean>

最后是web.xml里最关键的两段:字符编码过滤器和DispatcherServlet映射。字符编码过滤器要放在所有Filter的最前面,否则请求参数里的中文会在进入SpringMVC之前就乱掉。DispatcherServlet的url-pattern用/,不用*.do,这样Ajax请求路径简洁,不用带后缀。

<filter> <filter-name>encodingFilter</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> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

这套配置的坑点在于:applicationContext.xml负责扫描Service和Mapper,spring-mvc.xml负责扫描Controller,两边包扫描范围不能重叠,否则事务会乱。我见过有人把Controller也扫进applicationContext.xml,结果service事务没生效,数据写一半的情况时有发生。

3.4 跑通第一个接口:从Controller到前端Ajax

后端代码按"Controller → Service → Mapper"三层走一遍。下面是一个查询病害列表的完整链路。

DiseaseController.java:

@RestController @RequestMapping("/api/disease") public class DiseaseController { @Resource private DiseaseService diseaseService; @GetMapping("/list") public Result<List<DiseaseRecord>> list(@RequestParam(required = false) Long roadSectionId, @RequestParam(required = false) Integer status) { return Result.ok(diseaseService.queryList(roadSectionId, status)); } }

DiseaseMapper.java:

public interface DiseaseMapper { List<DiseaseRecord> selectByCondition(@Param("roadSectionId") Long roadSectionId, @Param("status") Integer status); }

DiseaseMapper.xml:

<select id="selectByCondition" resultType="com.example.system.entity.DiseaseRecord"> SELECT * FROM disease_record <where> <if test="roadSectionId != null"> AND road_section_id = #{roadSectionId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY report_time DESC </select>

前端HTML里用fetch或者jQuery都行,常见做法是jQuery的$.ajax,因为老项目里兼容性和上手成本最低。记得在页面里引入jQuery的本地文件,别在生产环境依赖公共CDN:

$.ajax({ url: '/api/disease/list', method: 'GET', data: { status: 0 }, success: function (res) { if (res.code === 0) { renderList(res.data); } else { alert(res.msg); } } });

这个接口的意义是把路打通。注意Controller里用的是@RestController而不是@Controller,省得在每个方法上加@ResponseBody。参数required=false表示前端可以不传,走全量查询。MyBatis的<where>标签会自动去掉第一个多余的AND,你不需要担心条件拼错。前端所有接口都走同一套Result结构,所以renderList只要写一次,后面所有页面都能复用这套交互模式。

4. 把业务闭环做出来:巡检计划、病害上报、派单验收

4.1 按路段和周期自动生成巡检计划

业务起步阶段可以手工建计划,但路段一多就不现实。养护系统的常见做法是:每月末根据路段等级和当月天数,批量生成下月的inspect_plan。核心逻辑用一个Service方法实现:

@Service public class InspectPlanService { @Resource private RoadSectionMapper roadSectionMapper; @Resource private InspectPlanMapper inspectPlanMapper; public void generateNextMonthPlans() { List<RoadSection> sections = roadSectionMapper.selectAll(); LocalDate firstDay = LocalDate.now().plusMonths(1).withDayOfMonth(1); LocalDate lastDay = firstDay.plusMonths(1).minusDays(1); for (RoadSection section : sections) { int intervalDays = switch (section.getRoadLevel()) { case 1 -> 1; // 高速每天巡 case 2 -> 2; // 国道每两天巡 case 3 -> 7; // 省道每周巡 default -> 15; // 县道每半月巡 }; List<InspectPlan> plans = new ArrayList<>(); for (LocalDate date = firstDay; !date.isAfter(lastDay); date = date.plusDays(intervalDays)) { InspectPlan plan = new InspectPlan(); plan.setRoadSectionId(section.getId()); plan.setPlanDate(date); plan.setInspector(section.getManagementOrg() + "巡查组"); plan.setStatus(0); plans.add(plan); } inspectPlanMapper.batchInsert(plans); } } }

这段逻辑里有几个参数值得根据实际调整:intervalDays的取值代表两次巡检计划之间的天数,高速每天一次、国道两天一次、省道一周一次、县道半个月一次是我在多个项目里见过比较常见的默认值,但具体要以管养单位的考核要求为准。如果某段路是重点路段,可能需要单独维护一个特殊的巡检周期,而不只是按公路等级一刀切。batchInsert返回主键的问题也提醒一下:MyBatis的批量插入想要回填自增id,需要在Mapper XML里设置useGeneratedKeys和keyProperty,否则后续没法按计划id关联执行记录。

4.2 状态机:把病害状态流转收敛到几个方法里

最常翻车的地方,就是病害状态被到处直接update。比如维修队在开工时把status改成2,管理员验收时又改成4,中间没有任何校验,最后出现"状态5已归档的病害还能被改回1已派单"的怪事。解决办法是把所有状态转换收进Service层,控制器不能直接改status字段。

先定义状态枚举:

public enum DiseaseStatus { PENDING(0, "待派单"), DISPATCHED(1, "已派单"), REPAIRING(2, "维修中"), PENDING_VERIFY(3, "待验收"), VERIFIED(4, "已验收"), ARCHIVED(5, "已归档"); private final int code; private final String desc; DiseaseStatus(int code, String desc) { this.code = code; this.desc = desc; } }

然后Service里只暴露有限的几个方法:report、dispatch、startRepair、requestVerify、verify、archive。每个方法开头先查当前状态,不符合预期的就抛异常。下面看派单方法:

@Transactional(rollbackFor = Exception.class) public void dispatch(Long diseaseId, Long orderId) { DiseaseRecord disease = diseaseMapper.selectById(diseaseId); if (disease == null) { throw new ServiceException("病害记录不存在"); } if (disease.getStatus() != DiseaseStatus.PENDING.getCode()) { throw new ServiceException("只有待派单状态的病害才能派单"); } disease.setStatus(DiseaseStatus.DISPATCHED.getCode()); disease.setRepairOrderId(orderId); diseaseMapper.updateStatus(disease); }

@Transactional保证状态更新和工单写入在同一事务里,要么都成功,要么都回滚。rollbackFor=Exception.class很重要,因为Spring默认只回滚RuntimeException,如果你的ServiceException继承自RuntimeException那就没区别,但自定义checked exception时就会踩坑。

下面这张表是需要重点拦截的非法跳转,建议在单元测试里覆盖:

当前状态禁止操作原因
待派单直接归档没派单不可能维修完
已归档再派单闭环已结束
维修中验收还没申请完工
待验收直接归档跳过验收环节

4.3 多条件查询:MyBatis动态SQL扫台账

病害台账查询是使用频率最高的功能。养护站的人经常说"我要查某条路上所有严重病害,而且只看还没派单的",这就是多条件组合。手写JDBC拼SQL特别容易漏条件,MyBatis的<where>和<if>组合能优雅解决。

<select id="selectByCondition" resultType="com.example.system.entity.DiseaseRecord"> SELECT d.*, r.road_name AS roadName, r.road_code AS roadCode FROM disease_record d LEFT JOIN road_section r ON d.road_section_id = r.id <where> <if test="roadSectionId != null"> AND d.road_section_id = #{roadSectionId} </if> <if test="diseaseType != null"> AND d.disease_type = #{diseaseType} </if> <if test="severity != null"> AND d.severity = #{severity} </if> <if test="status != null"> AND d.status = #{status} </if> <if test="keyword != null and keyword != ''"> AND ( r.road_code LIKE CONCAT('%', #{keyword}, '%') OR d.position_desc LIKE CONCAT('%', #{keyword}, '%') ) </if> </where> ORDER BY CASE WHEN d.severity = 3 THEN 0 ELSE 1 END, d.report_time DESC </select>

这里有个容易误解的地方:<where>标签会自动去掉条件块开头的AND,但如果你把第一个条件直接写成AND d.status = #{status}而前面没有别的条件,<where>也能处理掉。CASE WHEN排序是为了让严重病害排在最前面,这是养护站的实际需求——先处理严重的,再处理轻微的,比按时间排实用得多。LEFT JOIN路段表是为了回显roadName,避免前端再单独查一次。

联调时如果觉得动态SQL拼得不对,最简单的办法是给MyBatis打开日志。在mybatis-config.xml里加一行<setting name="logImpl" value="STDOUT_LOGGING"/>,控制台就会输出==> Preparing:和==> Parameters:,能看到最终执行的SQL和参数。这也是排查动态SQL的最快手段,比猜快得多。

5. 避坑:这套系统上线前后最常遇到的5个问题

5.1 数据库连接与字符集

现象1:系统能登录,但页面上所有中文显示成问号"???",或者数据库里的中文在客户端工具里正常,在系统里乱码。

原因:MySQL连接URL里没有指定UTF-8,或者Tomcat接收请求时用了默认的ISO-8859-1解码。这两个位置只要有其中一个不对,中文就会在链路里坏掉。

解决:数据源URL加上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,注意是utf8不是utf8mb4,MySQL驱动8.x里写utf8会自动映射到utf8mb4基本没问题;Tomcat的server.xml里给Connector加URIEncoding="UTF-8"。别只在数据库建表时写utf8mb4,连接层不做一致处理照样乱码。

现象2:系统运行几天后变卡,数据库连接数持续上涨,最后报"Too many connections"。

原因:连接池没有限制最大连接数,或者每次查询都新建连接不回收。SSM项目里常见的是Druid参数没配好,还在用DriverManager手动获取连接。

解决:统一使用Druid数据源,maxActive控制在20以内,initialSize设5,并且配置testWhileIdle=true、validationQuery=SELECT 1。排查连接泄露时可以登录MySQL执行SHOW PROCESSLIST;,看哪些连接一直停留在Sleep状态且来自同一个服务IP,然后顺着代码找没释放的Connection。另外检查有没有在代码里手动调用Connection.close(),该释放的必须放在finally或try-with-resources里。

5.2 MyBatis映射与事务

现象3:查询出来的结果里,create_time、road_name这些字段全是null,但数据库里明明有值。

原因:实体类属性是驼峰命名createTime,数据库字段是下划线create_time,MyBatis默认不做驼峰转换。很多新手用了resultType=实体类就直接查SELECT *,于是全部字段映射失败。

解决:在MyBatis配置里加<setting name="mapUnderscoreToCamelCase" value="true"/>,我一般直接写在mybatis-config.xml里,这样所有Mapper XML都不用手写resultMap。如果不想全局开,就在Mapper XML里手写resultMap,把每个字段对应关系写清楚。注意:手写resultMap时一旦漏字段,一样是null,建议全局开启驼峰映射加上字段检查,两条腿走路。

现象4:Service里连续插入两条记录,第一条成功,第二条抛异常,但第一条没有回滚,数据库里留下脏数据。

原因:Spring事务没有生效。常见原因有三个:applicationContext.xml没配<tx:annotation-driven>;事务方法不是public;同类内部直接调用this.method()导致AOP代理没有介入。

解决:确认配置了DataSourceTransactionManager;把ServiceImpl所有方法都用public修饰;自调用必须通过注入自己的代理或拆分到另一个ServiceBean。排查时最简单的方法是:在事务方法里故意抛一个运行时异常,看数据库里之前插入的数据是否被回滚,不回滚就是事务没进去。

5.3 静态资源与请求路径

现象5:HTML页面在浏览器里直接双击能打开,但一点Ajax就404,后端接口单独用Postman调又正常。

原因:页面是通过file://协议打开的,Ajax的url是相对路径/api/disease/list,在file://下会被解析成本地文件路径,根本不会发到后端;另外,即使页面通过http访问,如果HTML没有放在webapp/static下,DispatcherServlet的/也会把静态资源请求拦掉。

解决:把HTML放到src/main/webapp/static下,启动后通过http://localhost:8080/static/index.html访问,不要双击打开。同时spring-mvc.xml里配置<mvc:default-servlet-handler/>,让Tomcat默认Servlet处理静态资源。页面里所有Ajax的url都拼上contextPath,或统一用相对/api/...,不要写死localhost端口,否则换环境就抓瞎。

6. 再进一步:让养护系统跑得久的好习惯

分页别手写limit。几百条数据时手写没问题,但病害台账跑上一年就有几万条,手写limit不仅麻烦还容易忘记总条数。常见做法是引入PageHelper,一行PageHelper.startPage(pageNum, pageSize)就能拿到分页结果,而且它支持从Mapper查询结果里直接包装PageInfo。要注意的是PageHelper是线程本地变量,用完后必须紧跟着执行一次查询,不能在中间穿插其他操作,否则分页参数会污染下一个查询。

日志一定要打在状态变更上。平时没人在意,但月底和养护单位对账时,如果有病害状态被改乱了,没有日志就完全不知道是谁改的、什么时候改的。我的习惯是在dispatch、verify这类状态转换方法里用logger.info记录:操作人、病害id、老状态、新状态。哪怕只两个字段,排查时也能节省一个下午。

备份命令值得写进运维手册。MySQL的备份不需要复杂工具,一条mysqldump命令加上定时任务就够了,关键是加--single-transaction让备份不锁表:

mysqldump -u root -p --single-transaction --set-gtid-purged=OFF road_maintenance > /backup/road_$(date +%F).sql

最后说个我自己的教训:以前维护一套老系统时,为了图快在Mapper里直接写UPDATE disease_record SET status = 4 WHERE id = ?,结果漏了校验,把一条还在维修中的记录验收入库了。后来我把所有状态转换都收到Service层方法里,连Controller都不许直接改status字段,这类翻车就再没出现过。技术再老,只要流程拧紧,系统就能一直好好跑。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 9:25:32

OneNET Token鉴权全解析:从APIKey到动态令牌的踩坑指南

做物联网接入的人&#xff0c;多半在 OneNET 平台上踩过同一个坑&#xff1a;明明 APIKey 申请了、设备建好了&#xff0c;调接口却一直 401&#xff1b;或者今天还好好的&#xff0c;明天程序跑起来就是各种鉴权失败。这个问题十有八九出在 Token 的获取和使用上。OneNET 作为…

作者头像 李华
网站建设 2026/10/10 9:24:05

开题报告别硬憋:广电编导人的 AI 工具搭配清单 [特殊字符]

先说一个特别典型的场景&#xff1a;你是广播电视编导专业大四学生&#xff0c;毕业作品打算拍一部非遗微纪录片&#xff0c;暂定名《一块木板的春天》&#xff0c;讲一位老木匠如何把传统木作改成年轻人愿意看的短视频内容。片子要拍&#xff0c;开题报告也得写——选题依据、…

作者头像 李华
网站建设 2026/10/10 9:22:54

ASCEND:与当地的Gemma一起,将一次小小的散步变成一次真实世界的探险

这是提交给Hacktoberfest开源人工智能挑战赛:第1周——触摸草地. 附近的公园感觉很普通。给它一扇神秘的门&#xff0c;一个小故事&#xff0c;一个在尽头等待的叶耳伴&#xff0c;同样的散步变成了一场冒险。 这就是背后的想法上升。下一层是你前门外的某个地方。 我建造的…

作者头像 李华
网站建设 2026/10/10 9:18:34

Qt C++ 坦克大战实战:从 QGraphicsScene 到完整 35 关源码解析

简介&#xff1a;面向C课程设计或大作业场景的经典坦克大战游戏项目&#xff0c;基于Qt 5.14.1与gcc 7.3.0环境开发&#xff0c;使用Qt Creator 4.11.0进行构建调试&#xff0c;完整实现单人闯关玩法。游戏共有35关&#xff0c;每关需要击败20个敌方坦克&#xff1b;玩家每关拥…

作者头像 李华
网站建设 2026/10/10 9:18:23

山海鲸可视化 VS FineReport:渲染能力与报表能力,决定项目选型边界

数字化大屏项目落地到实际业务&#xff0c;终究绕不开一个核心问题&#xff1a;项目核心诉求到底是三维场景渲染展示&#xff0c;还是复杂业务报表输出。园区 IOC、工厂数字孪生、财务统计报表、监管报送驾驶舱&#xff0c;不同项目的核心目标完全不一样&#xff0c;而山海鲸可…

作者头像 李华