news 2026/9/14 13:22:51

SSM框架实战:网上报销系统源码深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM框架实战:网上报销系统源码深度解析

简介:基于Java SSM(Spring、SpringMVC、MyBatis)与MySQL实现的网上报销系统,面向毕业设计、课程设计及Java Web初学者,可用于学习SSM整合、审批流程和数据库设计。资源共303个文件,约12.17MB,包含43个Java源码、43个JAR依赖、41个Vue前端页面、30个JSON配置、11个XML映射及1个SQL脚本等,覆盖控制层、服务层、持久层、前端界面和部署脚本,便于快速搭建并理解分层架构。已有121人学习浏览。通过该资源可掌握员工登录注册、报销单创建、审批流转、财务统计等完整功能实现,同时了解Maven环境配置、Tomcat部署、单元测试与项目打包流程。代码注释清晰、目录规范,适合作为参照模板进行二次开发,也能帮助初学者理清SSM框架在真实业务中的协作方式,提升综合实践能力。

1. 为什么“网上报销系统”成了SSM入门的最佳练手项目

收到这份基于Java SSM MySQL的网上报销系统源码时,别急着双击preview.bat看效果。我见过不少人第一眼看到Spring + SpringMVC + MyBatis + MySQL的组合就发怵,觉得自己连mvn命令都没敲顺,这个项目肯定啃不动。实际上,报销系统的业务复杂度卡在一个很舒服的位置:既有员工登录、角色权限、报销单状态流转这些Web开发绕不开的核心逻辑,又不会像商城系统那样堆一大堆优惠券、库存、秒杀的边角功能。Java基础入门的人,能靠它把SSM框架的请求流转、面向接口编程、数据库事务一次串起来;工作三五年的人,也能从它的源码里反推出很多底层机制。

这个项目真正值钱的部分不在页面,而在三处:applicationContext.xml里的Bean装配方案、t_reimburse表中状态字段的流转设计、以及MyBatis XML里那些动态SQL的边界条件处理。读完这篇,你可以自己动手把登录拦截器换成Shiro,把静态数据源换成Druid连接池,哪怕答辩时被问到“如果报销单特别多怎么优化”,也能回答到点子上。

2. SSM三框架如何协同:从pom依赖到请求流转,先看懂再改代码

2.1 Spring容器管理了什么:核心配置与注解扫描

SSM项目里Spring是地基。打开pom.xml,会看到spring-contextspring-jdbcspring-webmvcmybatis-spring这些核心坐标。版本搭配方面,Spring 5.1.x对应Java 8,Spring 5.3.x也能跑在Java 8上,但换成Spring 6就必须Java 17以上。如果你用的是自带JDK 1.8的课程设计环境,我一般会把Spring锁在5.1.8.RELEASE或5.3.20,避免编译时出现UnsupportedClassVersionError

Spring的职责集中在applicationContext.xml里,主要做三件事:配置数据源、开启注解扫描、把SqlSessionFactory交给Spring托管。看一段典型的配置:

<context:component-scan base-package="com.reimburse.service" /> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/reimburse?useUnicode=true&amp;characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:mybatis-config.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.reimburse.mapper" /> </bean>

这里最容易被忽略的是MapperScannerConfigurer,它会把com.reimburse.mapper包下所有接口直接代理成MyBatis的Mapper,省去每个Mapper都要写一次getMapper()调用的模板代码。注意url&必须写成&amp;,不写的话Maven打包时会报XML解析错误。另外,serverTimezone没配的话,高版本MySQL驱动连库时直接报时区异常,我习惯在url后面追加&serverTimezone=Asia/Shanghai

2.2 SpringMVC的请求路径:DispatcherServlet与Controller映射

SpringMVC是Web层的门面。web.xml里配置了DispatcherServlet,它拦截/路径下所有请求。看一个典型配置:

<servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

load-on-startup设为1,表示Tomcat启动时就初始化SpringMVC容器,否则要等第一个请求进来才初始化,用户会明显感觉首页加载慢。spring-mvc.xml里开启<mvc:annotation-driven/>,再用<context:component-scan base-package="com.reimburse.controller"/>扫描Controller,配合内部资源视图解析器指向/WEB-INF/views/目录下的JSP。

Controller层的映射规则决定了URL结构和前端调用方式。比如报销单列表页,常见的写法是:

@Controller @RequestMapping("/reimburse") public class ReimburseController { @Resource private ReimburseService reimburseService; @RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { List<ReimburseVO> list = reimburseService.queryApplyRecord(pageNum, pageSize); model.addAttribute("list", list); return "reimburse/list"; } }

@RequestParam(defaultValue = "1")是必须的,它让前端不带pageNum参数时也能正常访问,否则直接抛MissingServletRequestParameterExceptionModel是SpringMVC向视图传数据的标准入口,对应JSP里用${list}取值。整个流程就是:浏览器请求/reimburse/list,DispatcherServlet根据@RequestMapping找到ReimburseController,方法执行结束后返回逻辑视图名,视图解析器拼出实际JSP路径,渲染HTML回给浏览器。如果你把url-pattern改成*.do,所有访问路径后面都要带上.do后缀,这是很多老项目的历史包袱。

2.3 MyBatis如何嵌入Spring:SqlSessionFactory与Mapper代理

MyBatis以Mapper接口和XML文件为操作数据库的载体。mybatis-config.xml里通常会配置驼峰映射和默认别名:

<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> <typeAliases> <package name="com.reimburse.entity" /> </typeAliases> </configuration>

开了mapUnderscoreToCamelCase后,数据库字段reimburse_no才能自动映射到Java属性reimburseNo,不用每个字段都手写resultMap。这个开关对连接查询里带别名的字段也生效,但前提是别名必须保留下划线风格,比如SELECT r.reimburse_no AS reimburse_no FROM t_reimburse r

一个Mapper接口对应一个XML文件,例如ReimburseMapper.xml里的namespace必须写接口的全限定名,id必须与接口方法名一致。这是MyBatis动态代理的约定,写错了会在调用时抛Invalid bound statement (not found),这是SSM项目里最容易排查半天的错误。检查顺序是:namespaceid、接口方法签名、parameterTyperesultType是否齐全。

层次框架核心配置典型对象
表现层SpringMVCspring-mvc.xmlController、HandlerInterceptor
业务层SpringapplicationContext.xmlService、ServiceImpl
持久层MyBatismybatis-config.xml、Mapper.xmlMapper接口、SqlSessionTemplate

三个配置文件的扫描范围绝对不能重叠:Spring只扫Service,SpringMVC只扫Controller。如果一个类同时被两个容器扫描,事务注解和AOP切面可能出现重复代理,启动时有概率报NoUniqueBeanDefinitionException

注意:同时改动pom.xml里的Spring或MyBatis版本时,务必先执行mvn clean,旧class文件残留是很多诡异报错的来源。

3. 报销系统数据库设计:从员工表到审批记录的范式权衡

3.1 核心表结构与字段类型

网上报销系统的核心表主要有员工表、报销单表、审批记录表、部门表。设计时别从完美范式出发,先画出业务流程:员工提交报销单,上级审批,财务打款,每一步留下记录。表结构可以收敛成下面这种:

CREATE TABLE t_employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL COMMENT '员工编号', emp_name VARCHAR(50) NOT NULL, dept_id INT, password VARCHAR(64) DEFAULT '123456', role TINYINT DEFAULT 0 COMMENT '0员工,1审批人,2财务', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_reimburse ( reimburse_id INT PRIMARY KEY AUTO_INCREMENT, reimburse_no VARCHAR(32) UNIQUE NOT NULL COMMENT '单据号', emp_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, reason VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '0草稿,1已提交,2审批中,3已通过,4已驳回', apply_time DATETIME, update_time DATETIME ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE t_approval_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, reimburse_id INT NOT NULL, approver_id INT NOT NULL, action TINYINT COMMENT '2提交,3通过,4驳回', comment VARCHAR(500), record_time DATETIME DEFAULT CURRENT_TIMESTAMP );

DECIMAL(10,2)是金额字段的规范选择,它比FLOAT精确,不会出现23.9变成23.899999这种二进制浮点误差。金额经过多次累加后FLOAT会差出几分钱,财务对账时非常难看。status字段用TINYINT保存状态码,让状态流转在应用层控制,数据库只负责保存最新值;审批记录单独建表,是为了能追溯每一步操作由谁做了什么决定。

3.2 外键与索引设计:保障数据一致性

t_reimburse.emp_id引用t_employee.emp_idt_approval_record.reimburse_id引用t_reimburse.reimburse_id。在这个项目里,物理外键可加可不加,但逻辑外键必须通过索引补齐。如果MySQL是5.7及以上版本,物理外键会带来锁竞争,审批记录频繁插入时每次都要检查父表,高流量下容易成为瓶颈。更好的做法是表之间只保留字段关系,索引覆盖查询条件,一致性由Service层校验保证。

索引设计关注三个维度:等值查询、排序、唯一性。员工登录走emp_no,所以建唯一索引;报销单列表按emp_id + status过滤,按apply_time排序,所以推荐组合索引:

ALTER TABLE t_reimburse ADD INDEX idx_emp_status (emp_id, status); ALTER TABLE t_approval_record ADD INDEX idx_rid_time (reimburse_id, record_time);

组合索引idx_emp_status (emp_id, status)能同时服务emp_id单独查询和emp_id + status联合查询,因为MySQL遵循最左前缀原则。如果SQL里单独查status,这个索引就失效了,此时需要额外建一个单列索引。很多初学者看到EXPLAINtype=ALL就慌,其实是没搞清楚索引方向。另一个常见的坑是把时间范围条件加在组合索引的最后面,比如(emp_id, status, apply_time),如果查询只带上emp_idapply_time这一组,索引就会退化。

3.3 MyBatis XML中的动态SQL:多条件查询报销单

报销单列表页通常有多个查询条件:单据号、状态、员工编号、金额区间。这些条件可填可不填,用MyBatis的<where>标签能优雅处理拼接问题:

<select id="search" resultType="com.reimburse.entity.ReimburseVO"> SELECT r.reimburse_no, r.amount, r.status, e.emp_name, DATE_FORMAT(r.apply_time, '%Y-%m-%d %H:%i:%s') AS apply_time FROM t_reimburse r LEFT JOIN t_employee e ON r.emp_id = e.emp_id <where> <if test="empId != null and empId != ''"> AND r.emp_id = #{empId} </if> <if test="status != null"> AND r.status = #{status} </if> <if test="minAmount != null"> AND r.amount &gt;= #{minAmount} </if> <if test="maxAmount != null"> AND r.amount &lt;= #{maxAmount} </if> </where> ORDER BY r.apply_time DESC </select>

<where>会自动去除第一个条件前面的AND,比手工写WHERE 1=1的拼接方式安全得多。&gt;=&lt;=是XML转义写法,不转义的话XML解析器会把>=理解为标签结尾。#{empId}是PreparedStatement参数占位符,能防止SQL注入;如果用${empId}直接拼字符串,登录接口上就存在被' OR 1=1突破的风险。这是Java面试题里绕不开的考点,但在这个项目里你写一次就能记住原因。

这里的ReimburseVO是视图层专用对象,它比实体类多一个empName字段,用于在前端直接展示员工姓名。很多课程设计在resultType里直接写实体类,查询完发现缺字段,再循环查一次员工表,性能就浪费在无意义的反复查询上。为列表页单独定义VO,把JOIN出来的字段都放进去,一条SQL解决展示问题,读代码的人也清爽。

4. 报销单审批流程与业务逻辑实现

4.1 登录与权限控制:拦截器还是Spring Security?

报销系统涉及员工、审批人、财务三种角色,权限控制是最容易暴露水平的模块。SSM项目里最常见的是使用SpringMVC的HandlerInterceptor实现登录拦截和角色校验,而不是一上来就引入Spring Security。Security配置链路长,要管UserDetailsService、PasswordEncoder、FilterChain,对这个体量的项目来说学习成本高于维护成本。自定义拦截器十行代码解决问题:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Employee emp = (Employee) request.getSession().getAttribute("loginUser"); if (emp == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }

spring-mvc.xml里注册拦截规则:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/reimburse/**"/> <mvc:mapping path="/employee/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> </mvc:interceptor> </mvc:interceptors>

exclude-mapping里的/static/**必须写,否则CSS、JS、图片资源全被拦截,页面会变成裸HTML,看起来像系统坏了。登录验证密码时,不要明文比对,虽然这个项目默认密码是123456,但至少要用MD5加盐存储。前面建表时把password VARCHAR(64),就是为了同时容纳MD5和SHA-256十六进制字符串,之后升级不用改表结构。角色校验可以在同一个preHandle里继续判断:从Session里取出loginUser.getRole(),然后根据handler对象的@RequiresRole注解做对应判断。如果只在Controller方法里写if判断,代码会重复很多,也不好维护。

4.2 报销单状态机:草稿、已提交、审批中、已通过/驳回

报销单的status字段不能随便改,要设计成状态机。我用0草稿、1已提交、2审批中、3已通过、4已驳回。状态迁移规则如下:

当前状态允许动作下一状态备注
0草稿提交1已提交填写不完整不给提交
1已提交审批人开始审批2审批中锁定单据,员工不可再改
2审批中通过3已通过审批意见写入记录表
2审批中驳回4已驳回员工可编辑后重新提交

在Service层实现提交动作时,要先查当前状态再更新,否则并发下两个请求同时从草稿提交,可能出现状态覆盖。加一个乐观锁字段version是常见做法:

UPDATE t_reimburse SET status = 1, version = version + 1 WHERE reimburse_id = #{id} AND status = 0 AND version = #{version}

这个SQL执行后返回的影响行数如果为0,说明状态已经不是草稿,Service层直接抛出业务异常提示“单据已被他人处理”。这是高并发场景下防止重复提交的经典解法,也是网上很多Java面试大全里说的“乐观锁应用”。注意,不能用先SELECT再UPDATE的思路,两个请求同时查到status=0,会各自执行UPDATE,产生脏写。带条件的UPDATE把校验和修改合并到一条SQL,数据库行锁会挡住第二个请求。

4.3 财务报表统计:使用SUM与GROUP BY实现聚合

财务报表模块通常要按月份和费用类型统计报销总额。费用类型可以在t_reimburse里加一列expense_type,比如1差旅、2餐饮、3办公用品。聚合查询写成:

SELECT DATE_FORMAT(apply_time, '%Y-%m') AS month, expense_type, COUNT(*) AS reimburse_count, COALESCE(SUM(amount), 0) AS total_amount FROM t_reimburse WHERE status = 3 GROUP BY month, expense_type ORDER BY month DESC, total_amount DESC;

COALESCE(SUM(amount), 0)把NULL转成0,是为了避免报表前端图表显示空白。只统计status=3即已通过的单据,把报销中不算数的金额也打进去,财务会直接找你。在MyBatis中返回这个结果,单独定义一个ReportVO,里面放monthexpenseTypereimburseCounttotalAmount四个属性。Mapper接口返回List<ReportVO>即可,不用写resultMap,前提是开篇提到的驼峰映射是开启状态。

如果是课程设计演示,还可以加一个折线图接口,SQL换成按日分组:

SELECT DATE(apply_time) AS day, SUM(amount) AS day_amount FROM t_reimburse WHERE status = 3 AND apply_time >= #{startDate} GROUP BY DATE(apply_time)

这里传给Mapper的是String类型日期,MySQL的DATE()函数会隐式转换。但为了让索引生效,我一般会把条件写成apply_time >= #{startDate} AND apply_time < DATE_ADD(#{endDate}, INTERVAL 1 DAY),避免对字段使用函数导致索引失效。这个细节在生产系统里是必须的,在这个课程设计里写进文档,也算是一个可展示的深度优化点。如果遇到慢查询,先EXPLAINtype是不是ALLindex,调整索引或改写SQL之后再看行数从几万降到几十,这个对比可以在答辩时直接展示。

5. 从源码到可运行:Maven构建、Tomcat部署与常见踩坑

5.1 环境配置顺序与验证命令

下载解压后会看到preview.batdeploy.batbuild.bat这些批处理文件。它们本质上还是把Maven命令包装成了双击执行。但建议不要直接双击,最好打开命令行手动敲,看到真实输出才能定位问题。第一步是确认JDK和Maven已经装好并配好环境变量,验证命令是:

java -version mvn -v

mvn -v如果提示找不到命令,说明MAVEN_HOME没配或者path里没有加%MAVEN_HOME%\bin。网络上一堆java环境变量配置教程,核心就三行:JAVA_HOME指向JDK解压目录,MAVEN_HOME指向Maven目录,path里追加%JAVA_HOME%\bin%MAVEN_HOME%\bin。配置完重新开一个CMD窗口验证,旧窗口不会刷新环境变量。

接着导入数据库。先在MySQL里执行CREATE DATABASE reimburse DEFAULT CHARACTER SET utf8mb4;,然后使用source导入项目里sql目录下的脚本。数据库初始化完成后,修改applicationContext.xml里的账号密码。MySQL 8默认认证插件是caching_sha2_password,老版本Maven项目中如果还是mysql-connector-java 5.1.x,会报Unable to load authentication plugin。解决办法是将驱动升级到mysql-connector-java 8.0.x,并且URL里必须写com.mysql.cj.jdbc.Driver

5.2 三个典型报错及对策

报错一:Invalid bound statement (not found)。前面提过,先检查Mapper接口全限定名与XML的namespace是否一致。很多IDE在改动XML后没有重新编译资源文件,target/classes里还是旧文件。执行mvn clean compile再启动,能解决一半这类问题。

报错二:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这是MySQL驱动与系统时区不对齐。直接在jdbc:mysql的URL末尾加上?serverTimezone=Asia/Shanghai,中文乱码再用characterEncoding=utf8控制。

报错三:Tomcat启动后访问页面返回404。先看IDEA控制台的部署上下文路径,Tomcat DeploymentApplication context如果是/reimburse,访问URL就要带上下文:http://localhost:8080/reimburse。再检查DispatcherServleturl-pattern,如果配的是*.do,而页面里引用的请求路径没有.do后缀,就会全部找不到映射。

最后一个调试习惯:启动时看到Map日志里包含ReimburseController -> /reimburse/list这种映射记录,说明Controller已被成功扫描。这个日志是判断SSM整合是否正常最快的手段,比对着页面看半天快得多。线上排查MyBatis实际执行的SQL时,把日志级别调到DEBUG,看Preparing和Parameters两个位置,比人工猜测SQL拼错要省力很多。

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

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

Scalar vs Stainless:Stainless 停运后的 SDK 生成器能力对比与迁移承接

Scalar vs Stainless&#xff1a;Stainless 停运后的 SDK 生成器能力对比与迁移承接 【免费下载链接】scalar Scalar is an open-source API platform:                                       &#x1f310; Modern REST API Client …

作者头像 李华
网站建设 2026/9/14 13:20:33

Gatus 多语言配置教程:几行配置切换状态页语言

Gatus 多语言配置教程&#xff1a;几行配置切换状态页语言 【免费下载链接】gatus Automated developer-oriented status page with alerting and incident support 项目地址: https://gitcode.com/GitHub_Trending/ga/gatus Gatus 是一款面向开发者的自动化状态监控系统…

作者头像 李华
网站建设 2026/9/14 13:20:30

Matlab实现三维路径规划:栅格地图与A*算法实战

简介&#xff1a;三维路径规划是机器人学、无人机自主导航与自动驾驶中的关键技术&#xff0c;这份资源聚焦基于蚁群算法&#xff08;ACO&#xff09;的三维路径规划问题&#xff0c;提供一套可直接运行的MATLAB实现&#xff0c;面向需要学习智能优化算法、完成毕设课设或参与机…

作者头像 李华
网站建设 2026/9/14 13:19:55

GA-PSO混合算法:用遗传变异破解粒子群早熟收敛

简介&#xff1a;本资源是一个基于遗传算法&#xff08;GA&#xff09;与粒子群优化&#xff08;PSO&#xff09;深度融合的混合智能优化算法实现项目&#xff0c;面向人工智能、运筹优化及计算智能方向的中高级学习者与研究者&#xff0c;适用于复杂函数优化、工程参数调优及机…

作者头像 李华
网站建设 2026/9/14 13:19:50

车载Android串口开发:UART/RS485稳定通信实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华