资讯动态

SSM+JSP智能出差报销管理系统实战:从环境搭建到审批流落地

发布时间:2026/10/27 6:45:51 来源:尧图企业网站定制
简介这份资源是面向计算机相关专业学生与Java Web初学者的一套课程设计完整交付包主题为基于JSP与SSMSpring、Spring MVC、MyBatis框架的智能出差报销管理系统可用于课程设计参考、毕业设计选题或SSM框架实战练手。系统覆盖用户登录与权限管理、出差申请与审批、报销单创建提交与多级审批、报表统计及系统后台维护等模块按普通员工、部门主管、财务人员、管理员等角色划分功能权限。压缩包共10个文件约89.63MB包含源码压缩包、数据库SQL脚本、需求与开发报告文档、环境配置说明文本以及多张运行截图和两段实操录像便于对照理解系统结构与部署流程。目前已有47人学习下载。配套的开发报告与教程记录了需求分析、系统设计、编码实现、测试部署等环节数据库脚本可直接导入运行录像则演示了系统修改与调试过程适合希望快速掌握SSM整合开发与报销业务流程实现的读者参考借鉴。1. 从一份课程设计压缩包说起智能出差报销管理系统到底在解决什么出差回来贴发票、填单子、等审批这套流程在很多中小团队里依然是纸质加 Excel 的混合体。财务月底对账时发现金额对不上回头找当事人核实一来一回两三天就过去了。智能出差报销管理系统要解决的就是这个场景员工在线提交出差申请和报销单系统按预设规则自动校验金额与标准审批流按角色逐级流转财务端能直接导出对账数据。SSMSpring SpringMVC MyBatis是这类课程设计最常用的后端组合JSP 负责页面渲染整体结构清晰、上手门槛低适合作为毕业设计或课程大作业的技术底座。这篇笔记面向正在做类似题目的学生和刚接触 SSM 的开发者把从环境搭建到核心业务落地的完整路径拆开讲重点放在能直接复现的操作和容易翻车的地方。2. 环境搭建与 SSM 整合从零把项目跑起来2.1 开发环境选型与依赖版本锁定SSM 项目最怕的就是版本冲突尤其是 Spring 和 MyBatis 的整合包版本不匹配时启动阶段就会报NoClassDefFoundError。我一般会先把版本锁死避免后续调试时被依赖问题干扰。JDK 选 1.8Tomcat 用 8.5 或 9.0MySQL 用 5.7 或 8.0 都可以但驱动包版本要对应。Maven 的pom.xml里把 Spring 统一到 5.3.xMyBatis 用 3.5.xmybatis-spring 用 2.0.x这是经过大量项目验证的稳定组合。!-- pom.xml 关键依赖版本统一管理 -- properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version mybatis.spring.version2.0.7/mybatis.spring.version mysql.version8.0.33/mysql.version /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis.spring.version}/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency !-- JSP 相关 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies版本锁定后Maven 的依赖树会干净很多。如果用的是 MySQL 8.0连接 URL 里必须加serverTimezoneAsia/Shanghai否则启动时报时区错误。另外JSP 页面里用 JSTL 标签库时javax.servlet:jstl这个依赖不能少很多人只加了standard标签库结果页面报Unable to find taglib。2.2 Spring SpringMVC MyBatis 三层配置整合SSM 整合的核心是把 Spring 的 IoC 容器、SpringMVC 的 DispatcherServlet 和 MyBatis 的 SqlSessionFactory 串起来。常见做法是分三个配置文件applicationContext.xml管 Service 和 DAO 层spring-mvc.xml管 Controller 和视图解析mybatis-config.xml管 MyBatis 全局设置。web.xml 里配置 ContextLoaderListener 加载 Spring 容器DispatcherServlet 加载 MVC 配置。!-- web.xml 关键配置 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingapplicationContext.xml里要扫描 Service 包配置数据源和 SqlSessionFactory。数据源用 Druid 或 C3P0 都行Druid 自带监控页面调试时方便看 SQL 执行情况。SqlSessionFactory 的mapperLocations指向classpath:mapper/*.xml这样 MyBatis 的映射文件会被自动加载。!-- applicationContext.xml 核心片段 -- context:component-scan base-packagecom.reimburse.service/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/reimburse_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyour_password/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.reimburse.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.reimburse.dao/ /beanspring-mvc.xml里开启注解驱动配置视图解析器前缀后缀扫描 Controller 包。注意context:component-scan在 Spring 容器和 MVC 容器里要分开写Spring 容器扫 Service 和 DAOMVC 容器只扫 Controller否则事务注解会失效。!-- spring-mvc.xml 核心片段 -- context:component-scan base-packagecom.reimburse.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/配置完成后启动 Tomcat访问一个测试 Controller如果页面能正常返回说明整合成功。这一步如果报 404先检查url-pattern是不是/再检查 Controller 是否在扫描路径下。2.3 数据库建表与 MyBatis 逆向生成实体类出差报销系统的核心表不多但字段设计要提前想清楚。员工表、出差申请表、报销单表、审批记录表、费用明细表这五张表基本能覆盖主流程。建表时注意金额字段用DECIMAL(10,2)不要用FLOAT否则对账时会出现精度丢失。状态字段用TINYINT表示0 待审批、1 已通过、2 已驳回比字符串更省空间也更快。CREATE TABLE travel_apply ( id INT NOT NULL AUTO_INCREMENT, employee_id INT NOT NULL COMMENT 申请人ID, destination VARCHAR(100) NOT NULL COMMENT 出差目的地, start_date DATE NOT NULL, end_date DATE NOT NULL, reason VARCHAR(500) DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0待审批 1已通过 2已驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_employee (employee_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建完表后用 MyBatis Generator 逆向生成实体类和 Mapper 接口能省掉大量重复劳动。Generator 的配置文件里指定数据库连接、目标包名和表名运行mvn mybatis-generator:generate即可。生成的实体类字段名和表字段对应但注意create_time这种下划线命名的字段需要在 MyBatis 配置里开启mapUnderscoreToCamelCase否则映射不上。!-- mybatis-config.xml 开启驼峰映射 -- settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings开启 SQL 日志后控制台会打印每次执行的 SQL 和参数调试时非常有用。如果日志没输出检查logImpl是否配置正确以及是否引入了slf4j或log4j的适配包。3. 核心业务模块落地出差申请与报销审批流3.1 出差申请单的提交与表单校验出差申请是整个流程的起点员工填写目的地、起止日期、事由提交后进入待审批状态。Controller 层接收表单参数Service 层做业务校验比如结束日期不能早于开始日期、目的地不能为空。校验通过后插入travel_apply表同时往approval_record表写一条待审批记录。Controller RequestMapping(/travel) public class TravelApplyController { Autowired private TravelApplyService travelApplyService; PostMapping(/submit) public String submit(TravelApply apply, Model model) { // 基础校验日期逻辑 if (apply.getEndDate().before(apply.getStartDate())) { model.addAttribute(error, 结束日期不能早于开始日期); return travel/apply_form; } // 保存申请单状态默认为待审批 apply.setStatus(0); travelApplyService.saveApply(apply); return redirect:/travel/list; } }Service 层用Transactional注解保证申请单和审批记录同时写入避免只写了一半的情况。这里有个容易忽略的点JSP 表单里的日期字段提交上来是字符串SpringMVC 需要配置DateTimeFormat或者全局的日期转换器否则会报MethodArgumentTypeMismatchException。// 实体类日期字段加注解 DateTimeFormat(pattern yyyy-MM-dd) private Date startDate;如果项目里日期字段多建议在spring-mvc.xml里注册一个全局的FormattingConversionServiceFactoryBean统一处理日期格式比每个字段加注解更省事。3.2 多级审批流的数据库设计与状态流转审批流是报销系统的核心逻辑。常见的做法是用一张approval_record表记录每一步的审批人和审批结果travel_apply表只存当前状态。审批人按角色分两级部门经理审第一级财务审第二级。状态流转规则是0 待审批 → 1 部门通过 → 2 财务通过 → 3 已完成任意一级驳回则状态变为 9。CREATE TABLE approval_record ( id INT NOT NULL AUTO_INCREMENT, apply_id INT NOT NULL, approver_id INT NOT NULL, approver_role VARCHAR(20) NOT NULL COMMENT dept_manager / finance, result TINYINT DEFAULT 0 COMMENT 0待审 1通过 2驳回, comment VARCHAR(255) DEFAULT NULL, approve_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_apply (apply_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;审批操作在 Service 层要加锁防止两个审批人同时操作同一条记录。我一般用SELECT ... FOR UPDATE在事务里锁住申请单行再更新状态。如果并发量不大用乐观锁加版本号字段也能解决但课程设计里直接上悲观锁更简单直接。Transactional public void approve(Integer applyId, Integer approverId, Integer result, String comment) { // 锁定申请单行防止并发审批 TravelApply apply travelApplyMapper.selectForUpdate(applyId); if (apply.getStatus() 9 || apply.getStatus() 3) { throw new RuntimeException(该申请已结束不能重复审批); } // 写入审批记录 ApprovalRecord record new ApprovalRecord(); record.setApplyId(applyId); record.setApproverId(approverId); record.setResult(result); record.setComment(comment); record.setApproveTime(new Date()); approvalRecordMapper.insert(record); // 根据当前角色和审批结果更新状态 if (result 2) { apply.setStatus(9); // 驳回 } else { apply.setStatus(apply.getStatus() 1); // 进入下一级 } travelApplyMapper.updateStatus(apply); }状态流转的边界条件要写清楚如果当前状态是 1部门已通过财务审批通过后变成 2如果财务驳回直接跳到 9。前端 JSP 页面根据状态值显示不同的按钮和提示文案避免用户看到不该操作的按钮。3.3 报销单与费用明细的关联查询报销单和费用明细是一对多关系一张报销单可以包含多条费用记录比如交通费、住宿费、餐饮费。查询报销单详情时需要把明细一起查出来。MyBatis 用collection标签做嵌套查询或者用两条 SQL 分别查主表和明细表在 Service 层组装。!-- ReimburseMapper.xml 嵌套查询 -- resultMap idreimburseWithDetail typecom.reimburse.entity.Reimburse id propertyid columnid/ result propertyapplyId columnapply_id/ result propertytotalAmount columntotal_amount/ collection propertydetails ofTypecom.reimburse.entity.ReimburseDetail id propertyid columndetail_id/ result propertycategory columncategory/ result propertyamount columnamount/ result propertyinvoiceNo columninvoice_no/ /collection /resultMap select idselectWithDetail resultMapreimburseWithDetail SELECT r.id, r.apply_id, r.total_amount, d.id AS detail_id, d.category, d.amount, d.invoice_no FROM reimburse r LEFT JOIN reimburse_detail d ON r.id d.reimburse_id WHERE r.id #{id} /select嵌套查询在数据量大时会有性能问题因为 MyBatis 会执行多次 SQL。如果明细表数据不多这样写没问题如果明细上千条建议分页查询或者用JOIN一次查完在 Java 里分组。课程设计的数据量通常不大嵌套查询足够用。4. 避坑与排查SSM JSP 项目里最容易翻车的五个地方4.1 中文乱码从数据库到 JSP 页面全链路排查现象表单提交后数据库里存的是乱码或者 JSP 页面显示的问号。原因通常出在三个地方数据库连接 URL 没加characterEncodingutf8、web.xml 没配CharacterEncodingFilter、JSP 页面pageEncoding写错。解决方法是逐层检查数据库和表的字符集用utf8mb4连接 URL 加useUnicodetruecharacterEncodingutf8web.xml 里加编码过滤器JSP 头部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。四个地方都对了乱码基本不会出现。4.2 事务不生效Service 类没被 Spring 代理现象在 Service 方法上加了Transactional但异常时数据还是写进去了。原因通常是 Spring 容器和 MVC 容器的扫描范围重叠Service 被 MVC 容器扫描后事务代理没生效。解决方法是把context:component-scan拆开Spring 容器扫 Service 和 DAOMVC 容器只扫 Controller。另外Transactional方法必须是public的且不能在同类内部直接调用否则代理不生效。4.3 JSP 页面取不到 Model 数据EL 表达式被忽略现象Controller 里model.addAttribute(list, list)JSP 里${list}却显示空白。原因可能是 JSP 的web.xml版本太低EL 表达式默认被忽略。解决方法是把web.xml的web-app版本升到 3.0 以上或者在 JSP 头部加% page isELIgnoredfalse %。另外如果用了 JSTL 标签库c:forEach的items属性要写对别把${list}写成#{list}。4.4 MyBatis 映射文件不加载mapperLocations 路径写错现象启动时报Invalid bound statement (not found)说明 Mapper 接口和 XML 没对应上。原因通常是mapperLocations路径写错或者 XML 文件名和接口名不一致。解决方法是检查applicationContext.xml里mapperLocations的值确保classpath:mapper/*.xml能匹配到所有映射文件。如果 Maven 打包后 XML 没进target/classes在pom.xml的resources里把src/main/java下的 XML 也包含进去。4.5 日期格式转换失败前端传字符串后端要 Date现象表单提交时报Failed to convert property value of type String to Date。原因是 SpringMVC 默认不支持字符串到日期的自动转换。解决方法是在实体类的日期字段上加DateTimeFormat(pattern yyyy-MM-dd)或者在spring-mvc.xml里注册全局的日期转换器。如果前端用的是typedate的 input提交格式是yyyy-MM-dd和后端注解的 pattern 保持一致即可。5. 进阶技巧用拦截器做权限控制与操作日志权限控制是报销系统绕不开的需求。员工只能看自己的申请单部门经理能看本部门的财务能看所有。用 SpringMVC 的拦截器实现最直接在spring-mvc.xml里注册拦截器拦截/travel/*和/reimburse/*路径在preHandle方法里从 Session 取用户角色判断是否有权限访问目标资源。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login); return false; } // 财务角色放行所有请求 if (finance.equals(user.getRole())) { return true; } // 其他角色检查资源归属 String uri request.getRequestURI(); if (uri.contains(/travel/detail)) { Integer applyId Integer.parseInt(request.getParameter(id)); TravelApply apply travelApplyService.getById(applyId); if (!apply.getEmployeeId().equals(user.getId())) { response.sendError(403, 无权查看该申请单); return false; } } return true; } }拦截器注册时要注意排除登录页和静态资源否则用户没登录时连登录页都打不开。操作日志可以用 AOP 做在 Service 方法执行后记录操作人、操作类型和时间写入operation_log表。日志表字段不用太多user_id、action、target_id、create_time四个字段就够用。!-- spring-mvc.xml 注册拦截器 -- mvc:interceptors mvc:interceptor mvc:mapping path/travel/**/ mvc:mapping path/reimburse/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.reimburse.interceptor.AuthInterceptor/ /mvc:interceptor /mvc:interceptors验证拦截器是否生效可以用两个不同角色的账号分别登录访问同一个申请单详情页看是否返回 403。如果拦截器没起作用检查spring-mvc.xml里拦截器的注册顺序以及mvc:exclude-mapping的路径是否写对。我一般会在拦截器里加一行日志打印当前请求 URI 和用户角色调试时一目了然。这套 SSM JSP 的报销系统做完最大的收获不是代码本身而是对分层架构和事务边界的理解。刚开始做的时候我在事务不生效这个问题上卡了一整天后来才发现是扫描范围重叠导致的。希望这些踩坑记录能帮你少走弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑