简介:基于Spring Boot与MySQL的财务管理系统完整项目源码,适合毕业设计、课程设计及需要快速搭建财务后台的开发者。系统将收入、支出、资产、负债等财务数据集中管理,通过会计科目编码实现规范记账,可自动生成资产负债表、利润表、现金流量表,并支持预算编制、执行监控、财务审批及数据统计备份等功能,界面友好且具备数据备份与恢复能力。压缩包大小约17.76MB,根据资源说明,内含全套源码、设计文档、部署说明和视频演示;文件总数未单独统计,但内容足以支撑从环境搭建到功能验证的完整学习过程。目前已有231人学习下载,代码经过测试可百分百运行,部署文档逐步引导配置MySQL与Spring Boot环境,视频演示直观展示记账、报表生成等核心操作。整体结构清晰、模块边界明确,非常适合作为课设或毕设的参考蓝本,亦可基于本源码进行功能扩展。
1. Springboot 财务管理系统:毕设级项目为什么都长这样
财务管理系统在毕业设计里的出镜率一直很高,但多数人低估了它的门槛。它真正的难点从来不是「加减乘除」,而是权限怎么控、凭证怎么保证借贷平衡、报表怎么从流水里汇总出来——这三件事恰恰是答辩组最常追问的地方。
这套资源就是一个基于 Springboot + MySQL 的完整财务管理系统,源码、设计文档、部署说明、视频演示都齐了,能完整跑通「录入凭证 → 科目汇总 → 报表查询」这条业务链。对正在做 Java 课程设计或毕设选题的人来说,它是一个可以直接拿来做二次开发的项目骨架;对想练手 Springboot 的初学者来说,它也是一个能对照着理解分层架构、事务、权限拦截的现成案例。
我拆过的毕设项目里,财务类算是最值得细看的品类之一。因为它的数据模型足够规整,业务约束清晰,很适合用来理解一套 Web 系统从表结构到接口的完整设计逻辑。下面按我实际复现的顺序,把这套资源拆开讲。
2. 数据库与权限设计:五张核心表和 RBAC 的落地方式
拿到一份 Springboot 项目源码,我习惯先看数据库脚本而不是先启动项目。因为财务系统的业务逻辑几乎全部建立在表结构之上,表设计对不对,直接决定了后面凭证、汇总、报表能写到什么程度。
这套资源的表结构是典型的「用户-角色-科目-凭证」四层模型,核心表一共五张,职责划分非常清楚。先把它拆开看明白,后面所有代码都能对上号。
2.1 五张核心表:从用户到流水的字段设计
财务系统的核心表分为系统管理域和业务域两块。系统管理域管「谁在用系统」,业务域管「账怎么记」。
| 表名 | 职责 | 核心字段 | 说明 |
|---|---|---|---|
| sys_user | 用户表 | id, username, password, real_name, role_id, status | 存登录账号,密码不能明文 |
| sys_role | 角色表 | id, role_name, role_code | 角色编码用于权限判断 |
| acc_subject | 会计科目表 | id, subject_code, subject_name, parent_id, level, type | 树形结构,支持多级科目 |
| fin_voucher | 凭证主表 | id, voucher_no, voucher_date, summary, total_debit, total_credit, create_by | 一张凭证一行 |
| fin_voucher_item | 凭证明细表 | id, voucher_id, subject_id, summary, debit_amount, credit_amount | 一个科目一行,外挂凭证 ID |
凭证拆主表和明细表是这套设计里最值得学习的地方。主表存凭证编号、日期、摘要、借贷合计这些公共信息,明细表只存科目和金额,通过 voucher_id 关联。这样设计的好处是:一张凭证无论挂多少条明细,日期和摘要都只存一份,不会冗余;统计凭证数量时直接 count 主表,统计科目金额时直接聚合明细表,各查各的,互不干扰。
科目表用 parent_id 做树形结构,level 字段标层级,type 字段区分资产、负债、权益、成本、损益五大类。报表模块就是按 type 分组汇总的,这个字段如果漏了,后面做利润表会很痛苦。值得注意的是,这套资源没有建物理外键,表之间的引用关系靠 service 层代码保证——这是毕设项目的常见做法,批量删测试数据的时候比带外键方便得多,代价是代码里必须做存在性校验。
2.2 权限模型:基于 Session 的角色拦截怎么做
财务系统的权限核心是「不同角色看到不同菜单、做不同操作」。这套资源用的是经典的 Session + 拦截器方案,权限控制落在两个层面:登录拦截保证「没登录不能进系统」,角色判断保证「登录了也不能越权」。
拦截器本身不复杂,关键是注册和放行规则的配置。
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断当前 Session 是否存有登录用户 Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { // 未登录则重定向到登录页,并拦截本次请求 response.sendRedirect("/login"); return false; } return true; } }这段代码的逻辑很直接:从 Session 里取 loginUser,取不到就说明没登录,直接重定向到登录页。真正决定权限边界的是注册时的路径规则,这部分通常在 WebMvcConfig 里配置。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) // 拦截所有接口 .addPathPatterns("/**") // 登录页、登录接口、静态资源不拦截 .excludePathPatterns("/login", "/api/login", "/css/**", "/js/**", "/images/**"); } }这里有两个细节值得注意。一是 excludePathPatterns 必须把登录接口和静态资源排除掉,否则会出现「还没登录就被拦去登录页,登录接口本身也进不去」的死循环;二是如果我们想限制管理员专属接口,可以在拦截器里再取一次用户角色做判断,或者在路径规则上单独加一层。常见做法是给管理员接口统一加/admin/前缀,配一个 AdminInterceptor 专门拦这个前缀,两个拦截器互不干扰。
关于密码存储,这套资源的 sys_user 表里有 status 字段做账号启停,密码不建议明文存放。毕设里最常见的方案是 MD5 加盐,好一点的上 BCrypt——Spring Security 的 BCryptPasswordEncoder 单独拿出来用也很方便,不用为了加密引入整套安全框架。Session 超时时间在 application.yml 里配,默认 30 分钟,财务系统建议调短一点,比如 15 分钟,避免用户在公共电脑上忘了退出。
3. 从凭证到报表:完整业务链路与 MyBatis 聚合查询
表结构理清楚之后,接下来看业务代码怎么把这些表串起来。财务系统的核心业务链路就一条:录入凭证 → 保存明细 → 汇总科目 → 查询报表。这条链路上最关键的三个技术点是事务控制、借贷平衡校验、聚合查询。
3.1 凭证录入链路:事务、主表与明细表
凭证录入是整个系统的入口,也是业务约束最密集的地方。一张凭证可以有多条明细,每一条对应一个会计科目,借方向和贷方向的金额必须相等。录入逻辑如果写不好,后面报表查出来的数一定是错的。
@Service public class VoucherService { private final VoucherMapper voucherMapper; private final VoucherItemMapper voucherItemMapper; @Transactional(rollbackFor = Exception.class) public void saveVoucher(VoucherDTO dto) { // 1. 校验借贷是否平衡:所有明细的借方合计必须等于贷方合计 BigDecimal debitSum = dto.getItems().stream() .map(VoucherItemDTO::getDebitAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal creditSum = dto.getItems().stream() .map(VoucherItemDTO::getCreditAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); if (debitSum.compareTo(creditSum) != 0) { throw new BusinessException("借贷不平衡,无法保存凭证"); } // 2. 插入凭证主表,MyBatis 回填自增主键到实体的 id 字段 voucherMapper.insert(dto.getVoucher()); Long voucherId = dto.getVoucher().getId(); // 3. 把主表 ID 挂到每条明细上,再批量插入明细表 dto.getItems().forEach(item -> item.setVoucherId(voucherId)); voucherItemMapper.batchInsert(dto.getItems()); } }这段代码里有几个点值得细说。金额计算用 BigDecimal 而不是 double,是因为二进制浮点数在十进制金额上会有精度误差,财务系统对金额的精度要求是零容忍的。借贷平衡校验放在事务最前面,一旦不等直接抛业务异常,后面的主表和明细插入都不会执行。
@Transactional(rollbackFor = Exception.class)这个写法不是随手写的。Spring 的事务默认只在抛出 RuntimeException 时才回滚,如果业务代码抛的是受检异常,事务不会回滚,主表插进去了明细没插进去,数据就残了。显式指定 rollbackFor 是最稳妥的写法。第 2 步里 MyBatis 的 useGeneratedKeys 配置也很重要,它让插入主表后能立刻拿到自增 ID,否则明细表根本挂不上主表。
3.2 科目汇总与分页:聚合 SQL 和 PageHelper 用法
凭证录进去之后,下一个核心动作是科目汇总。会计科目有层级关系,报表需要看到每个科目的借方发生额、贷方发生额和余额,这个查询用一条分组聚合 SQL 就能完成。
<select id="sumBySubject" resultType="com.example.finance.vo.SubjectSummaryVO"> SELECT s.subject_code AS subjectCode, s.subject_name AS subjectName, COALESCE(SUM(i.debit_amount), 0) AS debitTotal, COALESCE(SUM(i.credit_amount), 0) AS creditTotal FROM acc_subject s LEFT JOIN fin_voucher_item i ON s.id = i.subject_id GROUP BY s.id, s.subject_code, s.subject_name ORDER BY s.subject_code </select>这个 SQL 的关键在 LEFT JOIN 和 COALESCE。LEFT JOIN 保证「没有发生额的科目也会出现在结果里」,如果科目表有科目但凭证明细里没数据,SUM 会返回 NULL,COALESCE 把它兜成 0。这样报表展示时不会漏掉零发生额的科目,每个科目都有一行,前端渲染省去很多空值判断。
凭证列表查询的分页,这套资源用的是 PageHelper,用法上有一个高频踩坑点。
@GetMapping("/page") public PageResult<VoucherVO> page(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String keyword) { // startPage 必须紧跟查询语句,中间不能插入其他 SQL 操作 PageHelper.startPage(pageNum, pageSize); List<VoucherVO> list = voucherService.queryPage(keyword); return PageResult.of(new PageInfo<>(list)); }注意:PageHelper.startPage 只对下一条 SQL 生效,而且 set 之后一定要立刻执行查询。如果在 startPage 和 mapper 调用之间执行了别的 SQL,分页参数就会被那个查询消耗掉,导致本页数据不分页。
分页插件本身不做业务校验,pageNum 传负数或超大的 pageSize 要自己兜底。常见做法是在 controller 层做一次参数归一化:pageNum 小于 1 时重置为 1,pageSize 超过 100 时截断到 100,防止有人恶意构造超大分页参数把数据库拖垮。
4. 本地复现与参数调优:改配置、导数据、跑起来
原理看得再明白,不如把项目跑起来一遍。这一章记录我在本地完整复现这套资源的操作流程,从建库到启动,每一步都给出具体命令和参数说明。
4.1 建库与初始化:MySQL 8 下导入 SQL 文件
项目根目录的 sql 文件夹里通常会带一个初始化脚本,包含建库、建表、初始科目数据和测试账号。我在本地习惯用命令行导入,比图形工具更可控,出错信息也直白。
# 登录 MySQL,创建数据库并指定字符集 mysql -uroot -p登录后执行建库语句,字符集建议在库级别就定死:
CREATE DATABASE IF NOT EXISTS finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE finance; SOURCE /path/to/finance_init.sql;建库和导入分两步走,中间不跳。utf8mb4 而不是 utf8,是因为 utf8 在 MySQL 里最多支持三个字节,遇到生僻字和 Emoji 会直接报错或变问号;utf8mb4 是完整的四字节字符集,财务系统里客户名称、摘要信息什么字符都可能出现,用 utf8mb4 最稳。
导入也可以用命令行一行完成,适合写进部署脚本:
mysql -uroot -p --default-character-set=utf8mb4 finance < finance_init.sql--default-character-set=utf8mb4是很多人会漏掉的关键参数。如果不指定,客户端会按系统默认字符集解析 SQL 文件,文件里的中文注释和初始数据一旦被错误解析,导入后就会出现乱码。导入完成后验证一下数据完整性,确认核心表有数据再往下走:
SHOW TABLES; SELECT COUNT(*) FROM acc_subject; SELECT COUNT(*) FROM sys_user;科目表初始化一般会有几十条数据,用户表至少有一条管理员账号。如果查询结果为空,说明导入脚本没执行成功,需要回看 SOURCE 命令的输出信息,而不是急着启动项目——这一步翻车了后面全白搭。
4.2 修改 application.yml:数据源、端口、连接池参数
数据库准备好之后,改配置。Springboot 的配置文件在 src/main/resources/application.yml,核心要改的是数据源和端口。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/finance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.finance.entity这里面每个参数都不是随便写的,拆开说明:
| 配置项 | 推荐值 | 作用与注意点 |
|---|---|---|
| driver-class-name | com.mysql.cj.jdbc.Driver | MySQL 8 及以上必须用 cj 版驱动,老驱动在 8.x 会报异常 |
| useUnicode 与 characterEncoding | true / utf8 | 保证 JDBC 层中文不乱码,两个参数要同时出现 |
| serverTimezone | Asia/Shanghai | 不写的话时间会按服务器时区解析,通常差 8 小时 |
| hikari.maximum-pool-size | 20 | 连接池上限,毕设 20 够用,并发高再调大 |
| mybatis.mapper-locations | classpath:mapper/*.xml | 指向 XML 映射文件的路径,路径错了启动直接报 mapper 找不到 |
配置改完启动项目,推荐 Maven 方式:
mvn spring-boot:run启动日志里看到Tomcat started on port 8080说明已经跑起来了。浏览器访问http://localhost:8080/login,用初始化脚本里的管理员账号登录,能进系统看到菜单和数据,就算完全复现成功。
我第一次跑这个项目时在启动阶段翻过一次车:MySQL 是 8.0,配置里写的是旧版驱动类名,启动直接报ClassNotFoundException。后来统一改成 cj 版驱动才通过。如果遇到这种问题,先检查 MySQL 版本,再检查驱动和 URL 参数,这两个地方是最容易配置不一致的。
5. 部署与联调避坑:五个高频故障的排查记录
复现和联调过程中,有几个坑几乎每次都会碰到。这里按「现象 → 原因 → 解决」的记录方式整理出来,每一条都对应实际运行中能观察到的报错。
5.1 五个高频故障:现象、原因、解决
故障一:凭证日期和系统时间差 8 小时
现象:录入凭证后列表显示的时间比本地时间晚了 8 个小时,或者恰好相反。
原因:JDBC 连接串里没写 serverTimezone,MySQL 驱动按服务器默认时区解析 DATETIME 类型,本地时区是东八区,服务器默认 UTC,一进一出就差出 8 小时。
解决:在 JDBC 连接串里显式加上serverTimezone=Asia/Shanghai,改完重启服务立即生效。
故障二:MySQL 8 连接报 SSL 或公钥检索错误
现象:启动时抛Communications link failure或Public Key Retrieval is not allowed。
原因:MySQL 8 默认开启 caching_sha2_password 认证插件,JDBC 客户端首次连接需要检索服务器公钥,而 URL 里没放开这个能力。
解决:在连接串末尾追加两个参数:useSSL=false&allowPublicKeyRetrieval=true。测试环境关掉 SSL 没问题,生产环境不建议这样配。
故障三:端口被占用,Tomcat 起不来
现象:启动日志报Port already in use: 8080,甚至直接抛 BindException。
原因:上次运行的 Java 进程没退干净,端口还被占着;或者本机装了其他服务占用了同一端口。
解决:用lsof -i:8080找到占用进程的 PID,kill -9 PID清掉;急用的话可以直接改server.port换一个端口,但后面前后端联调的配置也要同步改。
故障四:导入 SQL 后中文变问号
现象:科目名称、用户姓名全是???,英文和数字正常。
原因:SQL 文件本身的编码和 MySQL 客户端解析字符集不一致。最常见的是 SQL 文件存成了 GBK,或者在导入时没指定 utf8mb4。
解决:先用文本编辑器把 .sql 文件另存为 UTF-8 编码,再执行带--default-character-set=utf8mb4的导入命令。两件事要一起做,只做一件都可能残留乱码。
故障五:MyBatis 报 Invalid bound statement
现象:启动正常,但访问 Mapper 接口方法时抛Invalid bound statement (not found)。
原因:XML 映射文件没有被扫描到。要么是mapper-locations路径写错,要么是 XML 文件没有编译到 target/classes 目录。
解决:先看target/classes/mapper/下有没有对应的 XML 文件;没有就检查 XML 是否放错了资源目录。路径配置确认无误后再检查mapper-locations的值是否与包路径一致,比如classpath:mapper/*.xml就不能在 mapper 目录下再套一层子目录。
5.2 命名映射与全局配置:两个看着像 bug 的坑
除了上面五个直接报错的故障,还有两种场景不报错,但查出来的数据全部是 null,这种静默问题更难排查。
第一个是字段映射问题。数据库字段是user_name,实体字段是userName,如果 MyBatis 没开启驼峰映射,查询结果里 userName 就是 null。解决方案是全局配置:
mybatis: configuration: map-underscore-to-camel-case: true开启后 MyBatis 会自动把user_name映射到userName,反之亦然。没开这个配置时,要么在 SQL 里写别名,要么在每个 resultMap 里手动映射,能省则省,全局开关是最干净的方案。
第二个是拦截器顺序问题。如果项目里同时配了多个拦截器,它们的执行顺序按照注册顺序决定。权限拦截器如果排在日志拦截器后面,意味着未登录请求先打日志再跳转,日志里会混入大量被拦截的访问记录。排查这类问题不需要看业务代码,直接看配置类里 addInterceptor 的调用顺序即可。
这两个问题都不抛异常,但造成的现象和真正的 bug 一模一样。遇到「接口正常但数据为空」的情况,不要急着去翻 SQL,先把这两个全局配置确认一遍,往往能省下大量时间。这也是我踩过几次之后的血泪经验。
6. 用 SQL 做财务平衡校验:验证这套系统的最后一公里
部署完成不代表系统就是对的。财务系统有一个天然的验收标准:借贷平衡。每一张凭证的借方合计必须等于贷方合计,整个系统的总借方必须等于总贷方。这个约束在代码里做了校验,但数据一旦被手工改过、或是明细删除后主表余额没重算,代码层就拦不住了。
校验方法其实很简单,用一条分组聚合 SQL 就能扫出所有不平的凭证:
SELECT voucher_id, SUM(debit_amount) AS total_debit, SUM(credit_amount) AS total_credit, SUM(debit_amount) - SUM(credit_amount) AS diff FROM fin_voucher_item GROUP BY voucher_id HAVING diff <> 0;如果这条 SQL 查出任何一行,说明系统里有不平的凭证——直接定位到凭证号,去业务里修数据,而不用干瞪眼。每月汇总校验用另一条,按月份分组核对总借贷:
SELECT DATE_FORMAT(v.voucher_date, '%Y-%m') AS month, SUM(i.debit_amount) AS total_debit, SUM(i.credit_amount) AS total_credit FROM fin_voucher i JOIN fin_voucher_item i ON i.voucher_id = i.voucher_id GROUP BY month;我实际跑这套系统时,就是用这条 SQL 发现了一个隐蔽问题:明细表里删了一条记录,但主表的 total_credit 没同步更新,凭证列表显示借贷平衡,实际明细已经不平了。这类冗余字段不一致的问题,界面操作永远发现不了,只有从明细重新聚合才能暴露。
从那以后,我每次拿到财务系统源码,不管代码写得多好,第一件事永远是先跑一遍平衡校验 SQL,确认数据模型是干净的,再往里填业务数据。这个习惯帮我挡掉了至少三次数据模型埋雷的情况,也希望帮到你。
本文还有配套的精品资源,点击获取