☰
SSM警务信息管理系统实战:从登录权限到部署避坑全解析
2026/10/31 10:17:22 网站建设 项目流程

简介:面向Java相关专业毕业设计场景,这份基于SSM框架的警务信息管理系统可用于毕设选题、课程实训或项目二次开发参考。系统覆盖管理员、领导、人力资源、行政人员、警员、警车信息及借车记录七大管理模块,贴合警务单位日常管理流程;前端采用Vue构建界面,后端以Java实现业务逻辑,配套MySQL数据库。

资源包共454个文件,约14.51MB,以Java源码、Vue前端组件、SQL建表脚本、XML配置及部署脚本为主,另含SVG图标、样式表与说明文档。压缩包内附install、run、build等一键部署脚本,以及《部署说明》《表结构设计》文档,便于快速搭建运行环境、理解数据表关系,完整掌握从下载到运行的各环节,目录结构清晰。目前已有52人学习浏览,适合正在筹备毕业设计或需要参考SSM整合方案、警务信息化业务建模的读者。

1. 从值班室的Excel台账说起:SSM警务信息管理系统到底解决什么问题

如果你去基层派出所值班室转一圈,大概率还能看到两三台电脑轮流抢着录入警情、案件信息的场景。谁接的警、录入到哪一步、案件现在流转到哪个民警手里、超时没处理,全靠一本Excel台账加微信群吼。这不是技术落后,是信息管理没跟上。基于SSM的警务信息管理系统,本质就是把这类散落在Excel和口头交接里的数据,收进一个规范化、可追踪的Web系统里:SSM负责把请求、业务逻辑、数据库操作一条线串起来,让你能在浏览器里完成用户管理、警情登记、案件审批、值班安排和统计查询。这套东西适合两类人动手:一类是需要完成毕业设计的Java方向学生,想找一个技术栈正统、业务不复杂但五脏俱全的练手项目;另一类是确实想把业务流程整理成系统的开发者,想看看SSM这套经典Java组合在管理类系统里是怎么落地的。接下来我按自己做这类系统的习惯,从选型、建表、写代码到部署排查,一条线讲清楚。

2. 选型定模块:为什么是SSM,以及警情与案件模块怎么拆

在动代码之前,先明确技术栈选择。SSM是Spring、SpringMVC、MyBatis三个框架的组合:Spring管对象生命周期和事务,SpringMVC管HTTP请求的路由分发,MyBatis把Java方法和SQL语句映射起来。这套组合在2015年到2020年之间的Java Web项目里几乎就是标准答案,放到毕业设计语境下,它有三个不可替代的优势:第一,分层清楚,Controller、Service、Mapper三层结构让答辩时的逻辑讲解特别顺畅;第二,MyBatis让你能精确控制SQL,写复杂查询时不至于被ORM框架的规则绑住手脚;第三,Spring的IoC和AOP是Java面试必问的高频知识点,答辩时从项目里顺手引出来就是加分项。

2.1 技术栈选型与版本搭配:不要为省事去下载最新的版本

很多新手直接用最新版Spring和JDK,结果跑了几天都在调兼容性。常见做法是选择经过大量项目验证的稳定组合:JDK 1.8、Tomcat 8.5、MySQL 5.7、Maven 3.6。Spring用4.x版本,MyBatis用3.4.x,MyBatis-Spring用1.3.x。为什么不追新?因为SSM时代的资料、博客、毕设参考代码几乎都是在这套版本下跑的,遇到问题搜到的解决方案能直接抄。JDK 1.8和Spring 4.x搭配,连javax.servlet的包名都不用纠结。

组件推荐版本说明
JDK1.8长期维护,兼容性最好,环境变量配置教程最多
Tomcat8.5支持Servlet 3.1,对应SpringMVC的注解配置
MySQL5.7InnoDB默认引擎,事务支持稳定
Maven3.6.x依赖管理,构建war包
Spring4.3.xIoC、AOP、事务管理
MyBatis3.4.x动态SQL能力强

这个版本组合在Maven中央仓库里都能找到,不需要修改镜像源。构建工具用Maven而不是Gradle,因为绝大多数学校和部署环境默认支持Maven项目结构,导出war包后丢进Tomcat的webapps目录就能运行,省去Gradle在Tomcat里额外配置的麻烦。

2.2 功能模块与数据库表设计:一张权限表管住三类角色

警务信息管理系统不是一个模糊概念的堆砌,它在毕设题目里的标准解读是把派出所日常业务信息管理起来。按最常见的需求拆分,核心模块至少包括:用户管理、警情管理、案件管理、值班安排、操作日志。角色一般分三类:管理员、民警、领导。管理员管用户和权限,民警录警情和处理案件,领导做查看和审批。

我在建表阶段强烈建议把警情和案件拆成两张表,而不是混在一张表里带个type字段。警情是事件发生的原始记录,案件是警情经过初查后符合立案条件的正式记录。两者的生命周期完全不同:警情可能处理完就结案归档,案件则要经历立案、侦查、破案、移送起诉多个阶段。混在一起会导致后续的状态字段逻辑混乱。拆开之后,警情表和案件表用case_no字段做关联,案件表里记录from_alert_no指向源警情编号。

建表是这套系统里最值得花时间的部分。我列出基础的五张核心表结构,字段根据你的业务可以增减,但类型和命名建议保持一致风格。

-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) DEFAULT 'police', dept VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 警情表 CREATE TABLE alert_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, alert_no VARCHAR(20) NOT NULL COMMENT '警情编号', title VARCHAR(100), content TEXT, address VARCHAR(200), reporter VARCHAR(50), report_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已办结', assign_user_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 案件表 CREATE TABLE case_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, case_no VARCHAR(20) NOT NULL UNIQUE, from_alert_no VARCHAR(20), case_name VARCHAR(100), case_type VARCHAR(50), status TINYINT DEFAULT 0 COMMENT '0初查 1已立案 2侦办中 3已破案 4移送起诉', handle_user_id BIGINT, approve_status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

这三张表的生产语句里有几个细节要说明。第一,user表和企鹅的命名我全部用下划线小写风格,避免Linux服务器上表名大小写敏感带来的坑。第二,status字段用TINYINT而不是VARCHAR,因为状态值在代码里用常量或枚举比较,比字符串更不容易写错。比如我见过有人把状态值写成"处理中",数据库里录入成"处理中 "带了个空格,等查询时怎么都匹配不上。第三,关联字段assign_user_id、handle_user_id不建外键约束,只用普通索引。外键在并发写入时会让锁范围扩大,而且毕设项目的数据量用不到数据库层的完整性约束,应用层代码控制逻辑,数据库只存数据。

2.3 分层的工程结构:让答辩官一眼看到你的架构意识

工程目录结构在IDEA里按Maven标准布局创建,包名写清楚层级,不要把所有类扔进一个包里。我习惯的划分是:

com.police.system ├── controller # SpringMVC控制层 ├── service # 业务逻辑层接口 │ └── impl # 接口实现 ├── mapper # MyBatis数据访问接口 ├── entity # 数据库实体类(POJO) ├── interceptor # 登录拦截器 ├── config # 配置类(或XML配置) └── common # 通用工具、常量、响应封装

Controller只接收参数并调用Service,不在Controller里写业务判断;Service层不出现HttpServletRequest这样的Servlet对象,保持业务逻辑与Web容器解耦;Mapper接口方法名和XML里的statement id一一对应,命名规则统一用insertXxx、selectXxxByCondition。这样分层在做敏捷答辩时可以很自然地说清楚"请求从Controller进来,经Service处理事务,由Mapper操作数据库",逻辑链条完整。实际代码量控制在三千行以内就能完成核心功能,这个量级对毕设来说不多不少,太少了显得单薄,太多了后面改需求会改到怀疑人生。

3. 用SSM实现用户登录与权限控制:从建表到拦截器的一串代码

用户登录和权限控制是这套系统里唯一贯穿所有模块的公共逻辑,也是答辩时最容易被追问的部分。很多人在这个环节翻车,就是因为在Controller里写了一大堆if判断当前用户是否有权限,代码又臭又长,还没法复用。正确做法是用SpringMVC的拦截器统一处理权限校验,业务代码里完全不用关心用户是否已登录。

3.1 登录接口与密码加密:密码不要明文存数据库

先写登录的Service实现。密码存储,强烈建议不要用MD5直接加密,直接用加盐的方式。所谓加盐,就是在用户密码后面拼一段随机的字符串再计算哈希,这样即便两个用户设置了相同的密码,存库的结果也不一样,防止彩虹表撞库。Spring自带的DigestUtils里封装了MD5方法,但加盐逻辑需要自己拼。我这里用更简单的处理:MD5加密密码加上用户名拼接后再次加密,作为简单盐值。

@Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override public User login(String username, String password) { // 数据库里存的是用户名+密码拼接后做MD5的结果 String encodedPwd = DigestUtils.md5DigestAsHex( (username + password).getBytes(StandardCharsets.UTF_8)); return userMapper.selectByUsernameAndPwd(username, encodedPwd); } }

这段代码逻辑很简单,关键在于MD5计算的对象是用户名和密码的组合。两次不同用户的密码哪怕相同,因为盐值用了用户名,所以算出来的哈希值也不同。但要注意一个隐藏细节:StandardCharsets.UTF_8必须显式指定字符集,不要用默认编码,否则在Windows上开发正常,部署到Linux服务器上中文密码会匹配不上,这是经典的中文乱码坑。

控制器接收登录参数,成功后把用户信息放进Session,然后跳转主页:

@Controller public class LoginController { @Resource private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST) public String doLogin(String username, String password, HttpSession session) { User user = userService.login(username, password); if (user != null) { session.setAttribute("loginUser", user); return "redirect:/index"; } return "redirect:/login?error=true"; } }

用户对象直接存入Session即可。不要放一个空的userId再每次查库,这会多一次无谓的数据库访问。页面跳转用redirect而不是forward,防止用户刷新页面时表单重复提交。登录接口的信息承载用参数接收而不是@RequestBody的JSON对象,是为了降低前端的对接成本,直接在表单里写username和password两个name属性就能提交。

3.2 登录拦截器与角色权限控制:面向切面而不是if-else

拦截器是这套系统的权限核心。扩展SpringMVC的HandlerInterceptorAdapter,在preHandle方法里判断Session中是否存在用户。如果没有就重定向到登录页,有就放行。对于管理员才允许访问的路径,比如用户管理,还需要额外判断角色字段。

public class LoginInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员接口校验 String uri = request.getRequestURI(); if (uri.contains("/user/") && !"admin".equals(loginUser.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); return false; } return true; } }

这段拦截器把权限判断收敛到了一个地方。用户管理相关的路径以/user/开头,只有角色为admin的用户能通过。普通民警访问管理接口时会收到403响应。不要在Controller里再用if判断角色,那会让权限逻辑散落各处,后面想调整权限规则时改到吐血。

拦截器注册需要在SpringMVC配置文件中完成。如果项目用注解配置类,就实现WebMvcConfigurer接口重写addInterceptors方法:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/css/**", "/js/**", "/images/**"); } }

这里最关键的参数是excludePathPatterns。你系统的静态资源路径和登录接口必须排除,否则刷新页面时CSS和JS全被拦截,页面会变成没有样式的纯HTML,而且登录接口被拦截后用户连登录都做不到。SSM的常用注解在这一步体现得比较集中:@Configuration、@Controller、@Service、@Resource、@RequestMapping,每个注解各司其职,没有多余的装饰。这是新手最容易忽略的地方,也是查错时第一眼看的地方。

3.3 权限模型的边界:角色不等于权力

这套用拦截器做权限控制的方案只适用于角色数量少且权限边界清晰的系统。警务管理系统里三类角色够用,但如果你后面加了"协警"或"文职"角色,且他们能访问的菜单组合各不相同,角色字段的方式就不够用了。那时候需要引入RBAC模型,加一张权限表和一张角色权限关联表,通过查表判断是否有权限。但那种复杂度对毕设来说偏重了。骨架期先用角色字段撑起来,答辩时如果老师问"权限如何扩展",你就回答引入RBAC表结构,把拦截器从查角色改成查权限码即可,不需要真的把扩展代码写进去。

4. 走通案件登记与分页查询:MyBatis动态SQL与状态流转的落地写法

案件管理模块是整个系统的业务中枢,它最考验代码设计能力的地方不是增删改查本身,而是两个点:一个多条件组合查询的性能与写法,另一个是案件状态在各环节的流转控制。前者用MyBatis动态SQL解决,后者用状态字段加Service层校验解决。

4.1 案件登记接口:事务、参数绑定与返回结果封装

案件登记由民警操作,录入案件名称、类型、来源警情编号等信息。它涉及一次插入案件表、一次更新警情表的状态,所以在Service方法上加@Transactional确保两步操作要么都成功,要么都回滚。

@Transactional(rollbackFor = Exception.class) @Override public boolean registerCase(CaseInfo caseInfo) { // 生成案件编号:日期 + 4位随机数 String caseNo = "AJ" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + String.format("%04d", (int)(Math.random() * 10000)); caseInfo.setCaseNo(caseNo); caseInfo.setStatus(0); // 初查 int insertResult = caseMapper.insertCase(caseInfo); // 同步更新警情表状态为已转案件 AlertInfo alertInfo = new AlertInfo(); alertInfo.setAlertNo(caseInfo.getFromAlertNo()); alertInfo.setStatus(2); // 已办结 int updateResult = alertMapper.updateStatusByAlertNo(alertInfo); return insertResult > 0 && updateResult > 0; }

@Transactional注解默认只回滚RuntimeException,受检异常不会触发回滚。加rollbackFor = Exception.class是明确告诉Spring所有异常都回滚,防止异常被捕获后数据处于一半写入的尴尬状态。

Controller接收页面表单提交的数据,SpringMVC会把表单字段自动绑定到CaseInfo对象中同名属性上。这一步的坑在于前端表单里input的name必须和CaseInfo的属性一一对应,比如caseName对应<input name="caseName">,否则绑定一个空对象进去,数据库里全是NULL。登记成功后跳转到列表页。

@RequestMapping("/case/register") public String register(CaseInfo caseInfo, Model model) { boolean success = caseService.registerCase(caseInfo); if (success) { return "redirect:/case/list"; } model.addAttribute("errorMsg", "案件登记失败,请检查警情编号"); return "case/register"; }

操作成功后用重定向避免表单重复提交,失败时返回表单页并携带错误信息。这里的错误信息字段名和页面里${errorMsg}对应,模板引擎用JSP的情况下,Model中的属性会直接被EL表达式取到。如果这里不写Model model参数,页面就取不到errorMsg,用户看到的只是死板的重定向回表单页,不知道失败原因。

4.2 多条件分页查询:MyBatis动态SQL的三种场景与PageHelper的整合

列表页几乎必然带查询条件:按案件编号、案件类型、状态、登记时间范围。如果每个条件单独写一个Mapper方法,组合起来就是地狱。MyBatis的动态SQL在这里发挥最大优势,一个方法覆盖所有组合场景。

<select id="selectCaseListByCondition" resultType="com.police.system.entity.CaseInfo"> SELECT * FROM case_info <where> <if test="caseNo != null and caseNo != ''"> AND case_no LIKE CONCAT('%', #{caseNo}, '%') </if> <if test="caseType != null and caseType != ''"> AND case_type = #{caseType} </if> <if test="status != null"> AND status = #{status} </if> <if test="beginTime != null"> AND create_time &gt;= #{beginTime} </if> <if test="endTime != null"> AND create_time &lt;= #{endTime} </if> </where> ORDER BY create_time DESC </select>

<where>标签会自动处理掉第一个AND前缀,这是MyBatis的一个细节,忘了用它,SQL拼接时很容易出现WHERE AND case_type=...的语法错误。<if>标签判断空字符串和null都必须写,否则前端传一个空字符串过来,MyBatis会把它当成有值拼进SQL导致查不出数据。

分页用PageHelper拦截器。它底层是通过MyBatis拦截器在SQL执行前自动拼接LIMIT子句,无需手动写分页SQL。在MyBatis配置中注册插件,然后在Service调用前静态设置页码和每页条数。

@Override public PageInfo<CaseInfo> getCasePage(int pageNum, int pageSize, CaseInfo condition) { PageHelper.startPage(pageNum, pageSize); List<CaseInfo> caseList = caseMapper.selectCaseListByCondition(condition); return new PageInfo<>(caseList); }

PageHelper.startPage后紧跟着的第一条查询会被拦截处理。这里有个严重警告:startPage和Mapper调用之间绝对不能有其他数据库操作,比如插入日志,否则分页会应用错SQL上。我见过一个同学在startPage后先执行了一次字典表查询,结果给字典查询也加了LIMIT,页面数据直接显示不全。

PageInfo对象里自带total、pageNum、pageSize、pages等分页属性,在JSP页面用${pageInfo.total}就能显示总条数,不需要再额外查COUNT。

4.3 状态流转的赋值逻辑:只靠前端按钮控制就是灾难

案件状态从初查到移送起诉共五个阶段,前端列表页每个操作按钮控制状态值。如果只靠前端传status值来更新,用户直接改请求参数就能把案件从初查改成移送起诉,绕过了中间环节,这是典型的越权更新。Service层必须校验状态转移的合法性。

public static final Map<Integer, List<Integer>> ALLOWED_TRANSITIONS = new HashMap<>(); static { ALLOWED_TRANSITIONS.put(0, Arrays.asList(1)); // 初查 -> 立案 ALLOWED_TRANSITIONS.put(1, Arrays.asList(2)); // 立案 -> 侦办 ALLOWED_TRANSITIONS.put(2, Arrays.asList(3)); // 侦办 -> 破案 ALLOWED_TRANSITIONS.put(3, Arrays.asList(4)); // 破案 -> 移送起诉 } public boolean updateCaseStatus(Long caseId, Integer oldStatus, Integer newStatus) { List<Integer> allowedNext = ALLOWED_TRANSITIONS.get(oldStatus); if (allowedNext == null || !allowedNext.contains(newStatus)) { throw new BusinessException("非法的案件状态流转"); } return caseMapper.updateStatus(caseId, oldStatus, newStatus); }

状态流转时,Mapper的更新SQL必须带上oldStatus作为条件,防止并发情况下两个人同时操作同一案件导致状态覆盖。

<update id="updateStatus"> UPDATE case_info SET status = #{newStatus} WHERE id = #{caseId} AND status = #{oldStatus} </update>

UPDATE语句影响行数为0时,说明数据已经被别人改掉了,此时抛出并发冲突异常让用户重试。这个方案是用数据库的行锁机制做乐观锁,不需要额外加version字段,是最轻量的并发控制做法。

5. 部署避坑:war包与MySQL的五个高频故障排查

毕设项目做到能跑只是第一步,真正折磨人的是部署环节。开发环境一切正常,到了别人的机器上就是跑不起来,这类问题我统称为玄学问题。实际上没有玄学,所有怪异的报错都有明确的触发原因,只是你没往那个方向想。这一章把警务信息管理系统部署中最高频的五个故障逐个拆开,每条都按"现象到原因到解决"的排查方式写,这些情况我基本每次部署都会遇到一两个。

5.1 用Maven构建war包:别跳过测试

构建命令很简单,但参数有讲究:

mvn clean package -DskipTests -Pprod

clean清掉target目录下的旧产物,package执行完整构建流程生成war包,-DskipTests跳过单元测试(这个项目未必写了测试,但保留参数能防止代码里无效的测试类阻塞构建),-Pprod激活prod环境的Maven profile。Profile功能用于区分开发环境和生产环境的配置,数据库地址、账号密码、日志级别通常在pom.xml里配置成多个profile,激活prod时使用生产配置。如果你只有一个环境,这个参数可以不写。

构建成功后在target目录下找到police-system.war,把这个文件复制到Tomcat的webapps目录下,启动Tomcat即可自动解压部署。不要手动解压war包,Tomcat会自动处理,手动解压反而可能因为目录结构不对而报错。

5.2 数据库配置的三种写法:properties、JNDI与Druid连接池

SSM项目的数据库配置一般写在jdbc.properties里,被Spring的配置文件加载。我们的版本组合里用Druid连接池做数据库连接管理。

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/police_system?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=root

URL里的四个参数必须齐全。useUnicode=true&characterEncoding=utf-8保证中文不乱码,useSSL=false跳过SSL握手从而避免MySQL 5.7里因为证书问题报错,serverTimezone=Asia/Shanghai必须显式指定时区,否则MySQL驱动会报连接超时或CannotGetJdbcConnection异常。

Druid连接池在Spring的XML配置里如下注册:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <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="minIdle" value="5"/> <property name="maxActive" value="20"/> </bean>

initialSize是连接池启动时的初始连接数,minIdle是最小空闲数,maxActive是最大活跃数。毕设场景数据量不大,这三个参数保持5、5、20即可。如果你在服务器上发现连接数一直涨且不回落,把minIdle调高一些,这是Druid的常见行为。

5.3 中文乱码:Linux上最常见的部署杀手

现象:Windows上开发运行全部正常,部署到Linux服务器后,页面显示正常,但插入数据库的中文全部变成问号。

原因分三处:Linux系统默认字符集不是UTF-8、Tomcat请求响应字符集设置缺失、数据库表字符集不对。系统字符集只要LANG环境变量是UTF-8就行,这通常不是主因。Tomcat方面,确保server.xml里的Connector加了URIEncoding="UTF-8":

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>

这里最容易忽略的是,Spring的CharacterEncodingFilter是否在web.xml里配置。它负责把请求体的字符集设置为UTF-8,如果不配置,POST表单提交的中文在进入Controller前就已经乱码。

<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>

forceEncoding设为true表示请求和响应都强制UTF-8编码。只设encoding为UTF-8时,如果客户端没有显式带Content-Type,请求体可能还是按平台默认编码解析,所以forceEncoding必须写。数据库方面,建库语句要显式指定utf8mb4字符集,utf8mb4比utf8能多存一些特殊字符,是MySQL推荐的标准写法。

5.4 Invalid bound statement、NoClassDefFoundError等启动期崩溃

现象:Tomcat部署后启动,几分钟后访问页面,直接报500错误,查看catalina.out日志发现Invalid bound statement (not found): com.police.system.mapper.CaseMapper.selectCaseListByCondition。

原因:Mapper接口找到了,但对应的XML文件没有被MyBatis加载。Spring配置MyBatis的SqlSessionFactoryBean时,mapperLocations属性没有指向resources下的mapper目录。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean>

如果用了classpath:mapper/*.xml,确认src/main/resources/mapper/下面确实存在XML文件且文件名和Mapper接口的完全限定名对应。还有一个隐蔽坑:MyBatis的XML文件放在java目录下而不是resources目录下时,Maven默认不会把XML打包进war包。在pom.xml的build节点里补充资源插件配置,把xml文件也纳入打包范围。

NoClassDefFoundError通常意味着jar包版本冲突或缺失,比如spring-web和spring-webmvc版本不一致。检查pom.xml里Spring各依赖版本号是否统一,这个错误本质是类加载器找不到对应的类,大多数情况是版本漂移导致的。

5.5 数据库密码特殊字符导致的连接失败

现象:本地MySQL密码是root,正常。部署到服务器,密码设置成P@ssw0rd!,修改jdbc.properties后启动报错Access denied for user。

原因:@和!在properties文件里有特殊含义。@在Spring的占位符${}里可能被误解析,!在某些环境中会被shell转义,密码里的特殊字符破坏了配置加载。

解决:把密码里的特殊字符去掉,或者用Properties文件的Unicode转义写法,再或者密码值用单引号包裹。实际上最省事的方式是设置一个纯字母数字的数据库密码,数据库服务器的安全策略允许的情况下,这是毕设场景下最不做不必要的麻烦的选择。

5.6 Druid监控页面的生产关闭

Druid自带一个监控页面,可以查看SQL执行情况,开发调试很有用。但部署到公网服务器时务必关闭,否则任何人都能通过访问/druid/index.html查看你的数据库连接信息和SQL语句,这是数据泄露级别的风险。在web.xml里设置Druid的allow和deny参数:

<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <param-name>allow</param-name> <param-value>127.0.0.1</param-value> </init-param> <init-param> <param-name>deny</param-name> <param-value>all</param-value> </init-param> </servlet>

允许访问的IP只留本机。但这只能挡住HTTP层,如果服务器本身配置了公网端口转发,还是要注意防火墙规则。对毕设而言,如果你是部署在学校机房内网,问题不大;如果放在有公网IP的云服务器上,这步必须做。

6. 上线前的验证与改进:给自己一张不翻车的验收清单

系统开发完成后,不要急着写答辩PPT,先用一张验收清单把所有功能过一遍。这个习惯我从第一次做毕设项目养成,到后来做企业项目也一直沿用。清单覆盖功能流、边界条件、数据一致性三个层面,测试时按清单逐条确认,发现问题就记录在案。

检查项测试方法预期结果
登录成功输入正确账号密码跳转主页,Session生成
登录失败输入错误密码回到登录页并提示错误
未登录访问页面直接访问/index重定向到/login
非管理员访问用户管理用民警账号访问/user/list返回403页面
警情转案件录入合法警情编号案件生成,警情状态变已办结
案件非法状态流转初查案件直接调移送起诉抛出异常提示非法流转
分页参数异常页码传0或负数PageHelper自动修正为第一页
多条件查询案件名称模糊+类型精确返回交集结果
中文数据插入中文姓名和地址页面和数据库均无乱码
并发状态更新两个窗口同时更新同一案件只有一个成功

这里提一个所有SSM项目都通用的坑:Controller直接返回对象时,默认使用Jackson的序列化配置,日期字段会变成时间戳或格式错乱的字符串。如果你有Ajax接口返回JSON,需要在实体类的日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解,再在Spring配置里配置Jackson的ObjectMapper,统一日期格式。这个细节不处理,前端拿到的日期就是"2024-05-01T10:30:00.000+00:00"这种难看的字符串。

事务配置验证还要看一个常见坑:Service实现类里内部方法调用,比如register方法里调用了同一个类的updateAlertStatus方法,updateAlertStatus方法上的@Transactional不会生效。因为Spring的AOP代理默认基于接口代理,内部调用绕过了代理对象。解决方式要么把两个方法拆到不同的Service类,要么在启动类上使用@EnableTransactionManagement后把内部方法改成在另一个注入的Bean里调用。这个细节可能是答辩老师最喜欢问的深入题,答上来就是加分项。

后续改进方向上,如果时间充裕,优先做两件事:一是操作日志切面,用Spring AOP记录每个用户的关键操作,这个需求在答辩时容易出彩;二是接入Redis缓存热点查询,把案件列表的查询结果缓存起来,这个点能体现你对性能的思考。两件事都涉及新增技术栈,但代码改动量都不大,一两天能写完。做不完也没关系,在论文里把思路写清楚,答辩时说明白扩展方向即可,不用追求代码全部落地。

这套SSM系统我自己前前后后带人做过好几遍,从建表到部署踩过的坑基本都记在这里。最深刻的教训是:依赖版本能用稳定组合就不要尝鲜,权限校验必须在服务端做而不能依赖前端控制,部署环境的变化优先查字符集和配置而不是怀疑代码逻辑。每一步照着这个流程走,这门毕业设计不仅能安稳落地,还会在答辩时比较从容。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询