news 2026/10/28 2:07:53

基于SSM+SpringBoot的新能源汽车充电桩管理系统源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM+SpringBoot的新能源汽车充电桩管理系统源码解析

简介:这套新能源汽车充电桩系统毕业设计资料面向高校计算机相关专业学生与课程设计开发者,围绕SSM/SpringBoot框架、B/S模式与MySQL数据库展开,覆盖首页、个人中心、用户管理、维修员管理、电桩类别管理、充电桩管理、充电桩报修与维修回复、系统管理等核心模块,可用于毕设选题、系统方案撰写、功能模块划分和数据库设计参考。资源包共1个docx文件,大小约4.62MB,整合毕业论文说明书与源码整理内容,包含题目、中英文摘要、目录以及系统分析、设计与实现等章节,结构完整,方便按章节查阅写作框架、业务闭环与技术要点。目前已有65人学习下载,适合需要梳理充电桩管理系统权限控制、故障报修流程与可扩展性设计思路的读者参考。

1. 一个值班表引发的重写:这套充电桩管理系统到底解决什么

去年冬天帮朋友看一个充电站的排班表,Excel 里记着十几个桩的编号、谁去修的、修完有没有回填,月底对账时编号和人对不上。这套基于 SSM 的新能源汽车充电桩管理系统源码,解决的正是这件事:把充电桩档案、用户报修、维修员回复三条线收进一个后台,让「几号桩什么时候坏的、谁去修的、修完回复了什么」有据可查。它不是运营平台,没有支付、没有实时监控、没有硬件对接,定位非常明确——一套业务流程完整、能跑通、能讲清的管理后台。对正在找毕业设计题目或者想拿一套完整 CRUD 范本练手的开发者来说,它覆盖了权限、分页、文件上传、状态流转这些老生常谈但又绕不开的点,拆开看比抄一遍有用得多。

2. 技术栈与目录结构:SSM 三件套在这套源码里怎么落地

2.1 选型拆解:为什么「SSM」和 SpringBoot 两种说法都对

先把这个矛盾点说清楚,因为很多人下载下来第一眼就懵了:标题写的是 SSM,代码里跑的却是 SpringBoot。SSM 指 Spring + SpringMVC + MyBatis 这套经典组合,SpringBoot 只是把这三者用 starter 粘起来、把 XML 配置换成注解和 yml 的脚手架,底层还是同一个 IOC 容器、同一个 DispatcherServlet、同一个 SqlSessionFactory。所以叫它「基于 SSM 的 SpringBoot 项目」没毛病,答辩时说清这一层,比背概念管用。

选型上这套代码走的是最稳妥的路线。Web 层用 spring-boot-starter-web,自带 Jackson 做 JSON 序列化;持久层用 MyBatis-Plus,本质还是 MyBatis,SQL 可见可改,答辩时能指着 SQL 讲表关系,这一点比 JPA/Hibernate 强——后者生成的 SQL 是黑匣子,出了问题只能翻日志猜。数据库 MySQL,字符集 utf8mb4;服务器不用单独装 Tomcat,SpringBoot 内置容器打成可执行 jar,java -jar就能起,省掉了在 IDE 里配 Server Runtime 的一堆事。

<dependencies> <!-- Web 层:内嵌 Tomcat + SpringMVC + Jackson --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 持久层:MyBatis-Plus 在 MyBatis 上包了一层,分页和条件构造器开箱即用 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <!-- 版本以你本地仓库能拉到的为准,父工程里统一管理更省心 --> </dependency> <!-- 驱动:MySQL 8 用 com.mysql:mysql-connector-j,5.7 用 mysql:mysql-connector-java --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 实体类省掉 getter/setter,注意 IDE 要装 Lombok 插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这段依赖里有两个容易踩的点值得先记住:scope为 runtime 的驱动包在编译期不可见,别在代码里 import 它的类;Lombok 是optional的,打包后不传递,如果换个 IDE 打开发现实体类全是红波浪线,八成是没装插件或者没开注解处理,去 Settings 里勾上 Annotation Processors 就行。

架构模式是标准的 B/S 三层:浏览器发请求,Controller 接参校验,Service 处理业务和事务,Mapper 落到 SQL,结果原路返回 JSON 由前端渲染。没有微服务、没有消息队列、没有 Redis,对于一套管理后台来说这是优点不是缺点——依赖越少,别人拿到手能跑起来的概率越高。

2.2 目录结构:controller、service、mapper 三层的职责边界

源码的包结构基本是模板化的,看懂一层等于看懂全部。典型的目录树长这样:

charging-pile/ ├── src/main/java/com/example/charging/ │ ├── ChargingApplication.java # 启动类,@MapperScan 在这里 │ ├── config/ # MybatisPlusConfig、WebMvcConfig │ ├── interceptor/ # LoginInterceptor 登录拦截 │ ├── controller/ # 对外接口,只做接参和返回 │ ├── service/ # 接口定义 │ │ └── impl/ # 业务实现,事务加在这一层 │ ├── mapper/ # 继承 BaseMapper 的接口 │ ├── entity/ # 与表一一对应的实体 │ └── common/ # Result、TokenUtil、Md5Util ├── src/main/resources/ │ ├── application.yml # 数据源、端口、上传路径 │ ├── mapper/ # 复杂查询的 XML │ └── static/ # 前端页面和静态资源 └── pom.xml

分层的关键在于职责别串味。Controller 里只做三件事:接参数、调 Service、包装Result返回;一旦有人在 Controller 里写if判断业务状态,后面加需求就会失控。Service 层负责事务边界,凡是「写两张表」的动作都必须挂@Transactional,充电桩报修转维修回复就是典型的两表操作。Mapper 层继承BaseMapper<T>就白拿一套单表增删改查,只有跨表统计、复杂条件才写 XML,别为了「显得规范」把简单查询也塞进 XML。

实体类直接用表字段命名,这套源码的字段是纯拼音,比如dianzhuangbianhao、suochuweizhi,一开始看着别扭,但它有个好处:前端表单的name属性、后端实体属性、数据库列名三者完全一致,联调时不用来回查映射表。

@Data @TableName("dianzhuang") public class ChargingPile implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String dianzhuangbianhao; // 电桩编号,业务主键,列表页的查询条件 private String dianzhuangleibie; // 电桩类别,与类别表做逻辑关联而非外键 private String tupian; // 图片,只存文件名,展示时拼 /upload/ 前缀 private String shurudianya; // 输入电压 private String shuchugonglv; // 输出功率 private String suochuweizhi; // 所处位置 private Float meishidanjia; // 每时单价,注意是 float 不是 double }

@TableName显式指定表名,避免实体类名和表名大小写不一致导致 Linux 上找不到表;IdType.AUTO对应数据库自增,如果你导出的 SQL 建表语句里主键是bigint NOT NULL AUTO_INCREMENT,这里就必须是 AUTO,否则插入后拿不到回填的 id。meishidanjia用 Float 是这类源码的常见写法,真正做计费建议换成BigDecimal,浮点累加会掉精度,这个问题后面改造章节还会提。

2.3 七张表的字段设计:从拼音字段名读出业务约束

数据库设计是这套系统里最值得细看的部分,因为它把业务规则直接写进了字段。核心七张表的关系是:用户和维修员各自独立建表,管理员走统一的users表并带role字段;充电桩表存档案;报修表存故障描述;回复表存维修结论。电桩类别没有做成外键关联,而是把类别名以字符串冗余在桩表和报修表里,这是典型的「查询优先」取舍——列表页不用 join,代价是改了类别名历史数据不同步。

表名关键字段设计意图
dianzhuangdianzhuangbianhao、dianzhuangleibie、shurudianya、shuchugonglv、suochuweizhi、meishidanjia充电桩档案,编号是业务主键也是查询入口
dianzhuangbaoxiudianzhuangbianhao、guzhangmiaoshu、yonghuzhanghao、baoxiushijian、baoxiuzhuangtai报修单,记录故障描述与提交人
weixiuhuifudianzhuangbianhao、weixiugonghao、weixiuzhuangtai、weixiudengji、dengjishijian维修回复,维修员登记处理结果
yonghuyonghuzhanghao、yonghuxingming、mima、xingbie、nianling、lianxidianhua普通用户,账号唯一
weixiuyuanweixiugonghao、weixiuxingming、mima、shenfenzheng、jiatingzhuzhi维修员,工号唯一
dianzhuangleibiedianzhuangleibie(类别名)下拉选项的数据源
usersusername、password、role后台登录,role 区分管理员与普通角色

报修表和回复表里都冗余了yonghuxingming、weixiuxingming这类姓名字段,看 E-R 图会觉得没必要(有账号就能查出来),但只要写过列表页就明白:报修列表要一次显示几十条,每条都去 join 用户表查姓名,SQL 复杂度上来,前端还要处理 null。冗余姓名的代价是用户改名后历史单据显示旧名,对维修记录来说,「当时是谁报的」比「现在他叫什么」更重要,这个取舍是合理的。

主键统一用bigint自增而不是 UUID,是因为这套系统的 id 会直接出现在 URL 参数里(/api/pile/3),自增数字短、好调试;如果面向公网,建议换成雪花 ID 或者对外暴露编号而不是主键,避免被人遍历。

3. 核心链路拆解:一条报修单从提交到回复怎么走完

3.1 登录与角色分流:三种身份共用一个入口

登录是这套系统里唯一一个「三种角色共用一张表」的地方。users表只有username、password、role三个有效字段,登录成功后前端根据role决定跳哪个后台:admin 进管理端,普通用户进个人中心,维修员进工单列表。这种设计的优点是登录逻辑只有一份,缺点是角色一多就要改代码,好在业务里就三种。

@RestController @RequestMapping("/api") public class LoginController { @Autowired private UsersService usersService; @PostMapping("/login") public Result login(@RequestBody LoginForm form, HttpServletRequest request) { // 1. 按账号查人,注意这里不校验状态字段,改造时建议补一个 enabled Users user = usersService.lambdaQuery() .eq(Users::getUsername, form.getUsername()) .one(); // 2. 密码必须用密文比对,明文存库是这类源码最常见的扣分项 if (user == null || !Md5Util.md5(form.getPassword()).equals(user.getPassword())) { return Result.error("账号或密码不正确"); } // 3. 会话与令牌同时下发,前端跳转用 role,后续鉴权用 token request.getSession().setAttribute("loginUser", user); Map<String, Object> data = new HashMap<>(); data.put("token", TokenUtil.sign(user.getId(), user.getRole())); data.put("role", user.getRole()); data.put("username", user.getUsername()); return Result.ok(data); } }

@RequestBody说明前端提交的是 JSON 而不是表单,改动前端调用方式时这个注解不能丢,否则参数全是 null。密码比对用 MD5 是为了和存量数据兼容,新项目别再用 MD5,改成 BCrypt 更稳妥。TokenUtil.sign用 id 和 role 生成令牌,把角色塞进令牌里,后面的拦截器就不用每次查库。

拦截器的写法值得单独看一眼,因为它决定了哪些接口免登录:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行静态资源和预检请求,否则页面 CSS 都加载不出来 if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("token"); if (token == null || !TokenUtil.verify(token)) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录已失效,请重新登录\"}"); return false; } return true; } }

handler instanceof HandlerMethod这行判断是血泪经验:拦截器默认会把静态资源请求也拦下来,导致登录页自己的 js 被 401,页面白屏但控制台不报错,排查半天。返回 401 而不是重定向到登录页,是因为前后端分离下 ajax 拿到 302 会静默失败,只能用状态码驱动前端跳转。拦截器在WebMvcConfig里注册时记得排除/api/login、/upload/**这些路径。

3.2 充电桩档案的增删改查与分页

充电桩管理是整套系统的数据底座,其余模块都围着dianzhuangbianhao转。分页查询是使用频率最高的接口,条件构造器写法如下:

@GetMapping("/pile/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, String dianzhuangbianhao, String dianzhuangleibie) { Page<ChargingPile> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<ChargingPile> wrapper = new LambdaQueryWrapper<>(); // 编号做模糊查询,类别做精确匹配,条件为空时不拼接,避免 where 1=1 wrapper.like(StringUtils.hasText(dianzhuangbianhao), ChargingPile::getDianzhuangbianhao, dianzhuangbianhao) .eq(StringUtils.hasText(dianzhuangleibie), ChargingPile::getDianzhuangleibie, dianzhuangleibie) .orderByDesc(ChargingPile::getId); return Result.ok(pileService.page(page, wrapper)); }

pageNum和pageSize给了默认值,前端不传也不会 500;like和eq的第一个参数是布尔条件,为空就跳过这个条件,这比手拼 SQL 安全得多,也顺手防住了 SQL 注入。orderByDesc(id)保证新增的数据排在前面,符合管理后台的使用习惯。

真正让分页生效的是这个配置类,很多源码里它被漏掉了:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 不注册这个内部拦截器,page 查询会退化成全表查询,数据量一大直接卡死 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

DbType.MYSQL决定了生成LIMIT ?,?的方言,换成其他数据库要改这里。这个坑非常隐蔽——接口不报错,返回的total却是全表行数,前端页码全是 1 页,数据量小的时候根本发现不了。

新增和修改走同一套校验逻辑,编号唯一性检查必须放在 Service 里而不是数据库唯一索引上,因为报错信息要友好:

public boolean savePile(ChargingPile pile) { // 同一个编号只能存在一条,修改时要排除自身 id long count = pileMapper.selectCount(new LambdaQueryWrapper<ChargingPile>() .eq(ChargingPile::getDianzhuangbianhao, pile.getDianzhuangbianhao()) .ne(pile.getId() != null, ChargingPile::getId, pile.getId())); if (count > 0) { throw new RuntimeException("该电桩编号已存在"); } return pile.getId() == null ? pileMapper.insert(pile) > 0 : pileMapper.updateById(pile) > 0; }

ne的布尔条件在新增时为 false 直接跳过,修改时生效,一个方法覆盖两条路径,省得写两遍。

3.3 报修单到维修回复:状态怎么流转

这条链路是整套系统的业务核心,也是最容易写乱的地方。流程是:用户提交报修(写报修表,状态为待处理)→ 维修员在工单列表看到未处理记录 → 处理完在回复表登记维修结果 → 系统把对应报修单标记为已处理。

@Service public class RepairServiceImpl implements RepairService { @Autowired private ChargingRepairMapper repairMapper; @Autowired private RepairReplyMapper replyMapper; @Override @Transactional(rollbackFor = Exception.class) public void reply(RepairReply reply) { // 1. 回复记录先落库,登记时间由服务端生成,不信任前端传值 reply.setDengjishijian(new Date()); replyMapper.insert(reply); // 2. 按编号 + 报修人定位最近一条报修单,这里不用 id 是因为回复表单里没有报修单 id ChargingRepair repair = repairMapper.selectOne( new LambdaQueryWrapper<ChargingRepair>() .eq(ChargingRepair::getDianzhuangbianhao, reply.getDianzhuangbianhao()) .eq(ChargingRepair::getYonghuzhanghao, reply.getYonghuzhanghao()) .eq(ChargingRepair::getBaoxiuzhuangtai, "待处理") .orderByDesc(ChargingRepair::getId) .last("limit 1")); if (repair == null) { throw new RuntimeException("未找到对应的待处理报修单"); } repair.setBaoxiuzhuangtai("已处理"); repairMapper.updateById(repair); } }

@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省,Spring 默认只对运行时异常回滚,抛受检异常时两张表会写一半。状态字段用中文字符串「待处理/已处理」是这类源码的通用做法,直观但没法约束,脏数据很难发现;改造时换成枚举或者整型状态码,查询效率也更高。.last("limit 1")是拼接在 SQL 末尾的,用之前一定确认传入内容是自己写的常量,别把用户输入拼进去。

定位报修单的方式依赖「编号 + 账号」两个字段,如果同一个用户对同一台桩报了两次故障,只会关掉最新那条,旧的永远挂着。这是设计上的漏洞,二次开发时最好在回复表单里带上报修单 id,一步到位。

3.4 图片上传与静态资源访问

充电桩档案里要传图片,用于列表页展示和维修员现场比对。上传接口本身很短,但配置不对就会出现「上传成功、浏览器 404」的情况:

@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error("上传文件为空"); } // 1. 取扩展名,用 UUID 重命名,避免中文名和重复覆盖 String original = file.getOriginalFilename(); String suffix = original.substring(original.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; // 2. 目录不存在先创建,transferTo 不会帮你建父目录 File dest = new File(uploadPath, fileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 3. 只返回文件名,数据库里存的也是文件名,路径由前端拼接 return Result.ok(fileName); }

@RequestParam("file")的名字必须和前端<input type="file" name="file">或者 FormData 里的 key 一致。文件名用 UUID 重命名而不是保留原名,一是防中文乱码,二是防同名覆盖,三是防有人传.jsp之类的东西上来。数据库只存文件名这个约定很重要,将来换存储(本地换 OSS)只需要改展示拼接的前缀,不用批量改数据。

配套的静态资源映射:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 访问 /upload/xxx.jpg 时映射到磁盘目录,结尾的斜杠不能少 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); } }

addResourceLocations的路径必须以斜杠结尾,否则最后一段文件名会被拼错;Windows 下用file:D:/upload/,Linux 下是file:/data/upload/,两边都建议写在 yml 里由配置注入,别写死在代码里。

前端调用这块,页面里通常是一段 jQuery ajax,翻源码的时候可以先找这段确认接口约定:

// 分页查询,token 放在 header 里给拦截器校验 $.ajax({ url: '/api/pile/page', type: 'GET', data: { pageNum: 1, pageSize: 10, dianzhuangbianhao: $('#bh').val() }, headers: { token: localStorage.getItem('token') }, success: function (res) { // res.data.records 是 MyBatis-Plus 固定返回结构 renderTable(res.data.records, res.data.total); } });

res.data.records和res.data.total是Page对象的固定字段名,自己封装返回体时别改这两个 key,否则所有列表页都要跟着改。headers里的 token 名字要和拦截器里getHeader("token")保持一致,大小写敏感。

4. 本地跑通:从 SQL 脚本到浏览器首页

4.1 环境版本先对齐,别急着编译

这套源码最初是在老版本 IDE 里写的,文档里提到的 JDK 1.7 和现在 SpringBoot 2.x 的基线要求不匹配,直接照着配必翻车。稳妥的组合是 JDK 8(或 11)、Maven 3.6 以上、MySQL 5.7 或 8.0、IDEA 社区版就够用。SpringBoot 2.7 之后最低要求 JDK 8,3.x 要 17,先看pom.xml里spring-boot-starter-parent的版本再决定 JDK,别反过来。

MySQL 8 和 5.7 的驱动坐标、连接参数都不一样,8.0 要用com.mysql.cj.jdbc.Driver,连接串必须带serverTimezone,否则启动时报「The server time zone value is unrecognized」。字符集统一 utf8mb4,建库语句里显式指定,别指望服务器默认值。

4.2 导入数据与改配置

拿到源码先找*.sql文件,通常是一个建库脚本加一份初始数据。用 Navicat 或者命令行执行都行,执行完确认三件事:库名和配置文件里一致、表都建出来了、users表里有一条管理员账号。命令行导入的写法:

# 先建库,再导入,避免脚本里没写 create database mysql -u root -p -e "CREATE DATABASE charging_pile DEFAULT CHARSET utf8mb4;" mysql -u root -p charging_pile < db/charging_pile.sql # 确认表数量,正常应该在七张左右 mysql -u root -p -e "USE charging_pile; SHOW TABLES;"

配置文件的每一项都要对得上,尤其是数据源和上传路径:

server: port: 8080 spring: datasource: # 8.0 驱动是 com.mysql.cj.jdbc.Driver,5.7 是 com.mysql.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/charging_pile?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: # 复杂查询的 XML 放在 resources/mapper 下,这里不配会报 Invalid bound statement mapper-locations: classpath:mapper/*.xml configuration: # 表字段是纯拼音无下划线,实体属性同名,所以关掉驼峰映射反而更省事 map-underscore-to-camel-case: false file: upload-path: D:/upload/

allowPublicKeyRetrieval=true是 MySQL 8 用 caching_sha2_password 认证时的必需品,不加会报公钥检索失败。max-file-size默认只有 1MB,传张手机拍的照片就超了,报错信息还很含糊,提前调大省事。map-underscore-to-camel-case这条要看实体命名,如果字段里出现weixiu_gonghao这种下划线风格就必须打开,纯拼音就关掉,两种约定混用是最容易查不出数据的元凶。

4.3 启动后的自测顺序

启动类跑起来看到 Tomcat started on port 8080 只是第一步,功能是否通要按顺序验证,别一上来就点遍所有菜单。先登录:用 SQL 里的管理员账号登进去,确认能进后台首页;再验一条完整 CRUD:新增一台充电桩,看列表页是否出现,改一下单价再刷新看是否生效,删掉确认消失;然后走业务闭环:用普通用户账号提交一条报修,切维修员账号在工单列表里看到它,填回复提交,最后回用户端确认状态变成已处理;最后验上传:传一张图,F12 看返回的文件名,再直接访问/upload/文件名确认能打开。

这四步走完,说明数据源、事务、权限、静态资源四条线都通了,剩下的页面样式问题都是小事。如果卡在某一步,直接跳到下一章的排查清单,按现象对号入座比重头读代码快得多。

5. 常见问题排查:这套源码最容易翻车的五个地方

5.1 启动直接退出,报 Failed to configure a DataSource

现象是控制台打印Failed to configure a DataSource: 'url' attribute is not specified,应用还没起来就退出。原因有两种:一是application.yml根本没被加载,比如文件放在了resources之外,或者名字拼成了application.yaml之外的其他形式;二是启动类的包扫描路径和 Mapper 不在同一棵树下,@MapperScan写错了包名。解决办法是先确认target/classes下有没有 yml 文件,没有就是资源没打进构建;再看启动类的@MapperScan("com.example.charging.mapper")是否和实际包路径一致,多一层少一层都会导致 Mapper 注入失败。

5.2 数据库有数据,页面一条都查不出来

接口返回 200,total是 0,但用 Navicat 查同一张表明明有记录。原因大概率是分页插件没注册,或者前端传的参数名和后端@RequestParam对不上。先看返回的total:如果是全表行数而不是过滤后的数量,就是MybatisPlusConfig里漏了PaginationInnerInterceptor;如果total直接是 0,把 MyBatis 的 SQL 日志打开(logging.level.xxx.mapper: debug),看实际执行的 where 条件里带了什么,参数名不匹配时后端收到的是 null,条件被跳过,返回的应该是全量数据而不是 0,所以更可能是前端把pageNum传成了page。这种问题看日志比读代码快十倍。

5.3 中文变问号,时间差八小时

两个独立问题经常一起出现。中文乱码要同时满足三处:数据库和表是 utf8mb4、连接串带characterEncoding=utf8、响应头Content-Type是application/json;charset=UTF-8,缺一处就出问号。时间差八小时是时区问题,serverTimezone必须和服务器所在时区一致,国内写Asia/Shanghai;另外实体里Date类型返回给前端默认是时间戳,想显示成yyyy-MM-dd HH:mm:ss就在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),注意timezone属性也要写,只写 pattern 在某些版本下仍然会偏。

5.4 图片上传成功,但访问路径 404

上传接口返回了文件名,说明文件确实写到磁盘了,问题出在静态资源映射。常见原因有三个:addResourceLocations("file:" + uploadPath)里uploadPath结尾没有斜杠,导致拼出来的路径少一层;磁盘目录和配置的路径不是同一个(配置写D:/upload/,实际存到了项目根目录下的upload);或者路径里带了反斜杠,在 Linux 上直接失效。排查方法是先在服务器上ls确认文件真实位置,再用浏览器直接访问/upload/文件名,看返回的是 404 还是 403,404 是映射没匹配上,403 是路径对了但权限不够。

5.5 管理员能登录,但业务数据查不到

这套源码里存在两套用户体系:users表负责登录鉴权(username/password/role),yonghu表负责业务数据(yonghuzhanghao/yonghuxingming)。管理员在users里,普通用户在yonghu里,两边的账号没有同步机制。表现就是登录成功了,但个人中心查不到自己的信息,或者报修列表里没有自己提交的单子。原因是用users里的 username 去查yonghu表,查不到。解决方式有两种:改造时把两张表合并,用 role 字段区分,业务表只留外键;或者保守一点,在用户注册时同步往两张表各写一条,用事务包住。这类「两套账号」的设计在老毕设里非常普遍,接手第一件事就该把它摸清楚,否则后面所有权限相关的需求都会卡住。

6. 二次开发:把「能跑的毕设」改成「能讲的系统」

这套代码跑通不难,难的是怎么在它基础上做出自己的东西。我的建议是别一上来堆功能,先做三件投入产出比最高的事。

第一,把状态字段从字符串换成枚举。baoxiuzhuangtai、weixiuzhuangtai现在是中文硬编码,散落在各个 Service 里,改一个字要全局搜索。抽一个枚举,把状态值和流转规则收进去,查询和判断都引用枚举常量,将来加「维修中」「已取消」只改一处。

public enum RepairStatus { PENDING("待处理"), DONE("已处理"), CANCELED("已取消"); private final String label; RepairStatus(String label) { this.label = label; } public String getLabel() { return label; } }

第二,补一张订单/计费表。现在的meishidanjia只是个展示字段,没有真正计费。加一张charge_order,字段至少包含用户账号、电桩编号、开始时间、结束时间、时长、金额(用decimal(10,2)不要用 float)、支付状态。这一步做完,你的系统就从「设备台账」升级成「有经营数据的系统」,答辩或者面试时能讲的东西完全不一样。

第三,权限从拦截器升级到注解。现在是一个拦截器管所有接口,任何登录用户都能调管理员接口。加一个自定义注解@RequireRole("admin"),在拦截器里读方法上的注解比对 token 里的 role,改造成本半天,但能挡住「普通用户直接调删除接口」这种明显漏洞。

改造点大致工作量带来的收益
状态枚举化2 小时流转规则集中,加状态不改业务代码
订单计费表1 天从台账系统变成有经营数据的系统
注解式权限半天接口级鉴权,堵住越权调用
密码 BCrypt1 小时告别 MD5,简历上不留硬伤

验证方式也别靠手点。每改完一个模块,先用 Postman 把那组接口跑一遍正常流和异常流(不传参、传错类型、越权调用),再写两三个 JUnit 用例覆盖核心 Service,尤其是带事务的那几个方法。跑完再看一眼数据库,确认失败的操作没有留下半条脏数据,这比页面点一百遍有效。

从那以后我每接一份这类源码,都强制自己走一遍「先跑通登录、再跑通一条完整业务闭环、最后才动页面」的顺序,中间不跳步。跳步的代价是出了问题分不清是环境、数据还是代码的锅,白白耗掉一晚上。希望帮到你。

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

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

基于LSTM与模糊综合评价的高校教学质量量化方案

简介&#xff1a;这份资源是2022年江苏省研究生数学建模科研创新实践大赛B题的完整赛题PDF&#xff0c;面向备战研究生数学建模竞赛的高校学生与指导教师&#xff0c;聚焦高校课堂教学质量数据的量化分析问题。赛题围绕专家评语量化、课程分类、教学团队授课质量排序、专家评价…

作者头像 李华
网站建设 2026/10/28 2:07:27

植物叶片图像识别:OpenCV轮廓处理全流程与抗干扰特征提取

简介&#xff1a;本资源是一份面向Python初学者与计算机视觉入门者的OpenCV图像处理实践教程&#xff0c;聚焦植物叶片识别这一典型形状分析任务&#xff0c;帮助读者掌握图像分割、轮廓检测与几何特征提取等核心技能。资源为单个Word文档&#xff08;.doc&#xff09;&#xf…

作者头像 李华
网站建设 2026/10/28 2:07:26

会议纪要哪个软件总结精准?7款主流工具统一环境实测与选型清单

一、先把痛点说清楚&#xff1a;为什么你导出的纪要靠反复听录音才能补全会开完了&#xff0c;录音还在手机里躺着。等到真要整理纪要&#xff0c;才发现转出来的文字是一大段缺少逻辑结构的流水账&#xff0c;谁说的、结论是什么、谁负责、什么时候交&#xff0c;全都得自己从…

作者头像 李华
网站建设 2026/10/28 2:06:52

Windows 系统录音小工具:置顶浮窗、实时波形、MP3/WAV,无需安装

Windows 系统录音小工具&#xff1a;置顶浮窗、实时波形、MP3/WAV&#xff0c;无需安装只录电脑正在播放的声音&#xff0c;不使用麦克风。适合录制视频声音、在线课程、会议回放以及需要导入剪映继续处理的音频。![外链图片转存失败,源站可能有防盗链机制,建议将![请添加图片描…

作者头像 李华
网站建设 2026/10/28 2:06:52

内容为什么发完还会掉:新鲜度衰减与可复核的复测节奏

一、引用不是一次性的&#xff0c;是一条会褪色的曲线 一篇内容第一次被 AI 引用&#xff0c;只代表它在当时的候选集里排到了前面。模型不会把这个结果存成永久结论&#xff0c;它会周期性地重新评估信源。评估里有一项权重叫新鲜度。同样两个都能回答问题的内容&#xff0c;引…

作者头像 李华
网站建设 2026/10/28 2:06:51

马克思四环五角星——政府忙碌律B结构性必然·独立化盲区

BSD 六系马克思五环五角星与独立化盲区同构定律——去掉科技建设显五角星佛道儒独立天道致五行牛顿独立时间中国以经济建设为中心独立科技建设古代现代当代完全同构马克思原本四环不成星0 元本体论大一统Step330​核心突破​&#xff08;作者裁定八条从几何实证到历史同构定律&…

作者头像 李华