Spring Boot财务管理系统:权限设计、凭证链路与部署避坑
简介基于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 idsumBySubject resultTypecom.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 会返回 NULLCOALESCE 把它兜成 0。这样报表展示时不会漏掉零发生额的科目每个科目都有一行前端渲染省去很多空值判断。凭证列表查询的分页这套资源用的是 PageHelper用法上有一个高频踩坑点。GetMapping(/page) public PageResultVoucherVO page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword) { // startPage 必须紧跟查询语句中间不能插入其他 SQL 操作 PageHelper.startPage(pageNum, pageSize); ListVoucherVO list voucherService.queryPage(keyword); return PageResult.of(new PageInfo(list)); }注意PageHelper.startPage 只对下一条 SQL 生效而且 set 之后一定要立刻执行查询。如果在 startPage 和 mapper 调用之间执行了别的 SQL分页参数就会被那个查询消耗掉导致本页数据不分页。分页插件本身不做业务校验pageNum 传负数或超大的 pageSize 要自己兜底。常见做法是在 controller 层做一次参数归一化pageNum 小于 1 时重置为 1pageSize 超过 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-setutf8mb4 finance finance_init.sql--default-character-setutf8mb4是很多人会漏掉的关键参数。如果不指定客户端会按系统默认字符集解析 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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-namecom.mysql.cj.jdbc.DriverMySQL 8 及以上必须用 cj 版驱动老驱动在 8.x 会报异常useUnicode 与 characterEncodingtrue / utf8保证 JDBC 层中文不乱码两个参数要同时出现serverTimezoneAsia/Shanghai不写的话时间会按服务器时区解析通常差 8 小时hikari.maximum-pool-size20连接池上限毕设 20 够用并发高再调大mybatis.mapper-locationsclasspath: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 连接串里没写 serverTimezoneMySQL 驱动按服务器默认时区解析 DATETIME 类型本地时区是东八区服务器默认 UTC一进一出就差出 8 小时。解决在 JDBC 连接串里显式加上serverTimezoneAsia/Shanghai改完重启服务立即生效。故障二MySQL 8 连接报 SSL 或公钥检索错误现象启动时抛Communications link failure或Public Key Retrieval is not allowed。原因MySQL 8 默认开启 caching_sha2_password 认证插件JDBC 客户端首次连接需要检索服务器公钥而 URL 里没放开这个能力。解决在连接串末尾追加两个参数useSSLfalseallowPublicKeyRetrievaltrue。测试环境关掉 SSL 没问题生产环境不建议这样配。故障三端口被占用Tomcat 起不来现象启动日志报Port already in use: 8080甚至直接抛 BindException。原因上次运行的 Java 进程没退干净端口还被占着或者本机装了其他服务占用了同一端口。解决用lsof -i:8080找到占用进程的 PIDkill -9 PID清掉急用的话可以直接改server.port换一个端口但后面前后端联调的配置也要同步改。故障四导入 SQL 后中文变问号现象科目名称、用户姓名全是???英文和数字正常。原因SQL 文件本身的编码和 MySQL 客户端解析字符集不一致。最常见的是 SQL 文件存成了 GBK或者在导入时没指定 utf8mb4。解决先用文本编辑器把 .sql 文件另存为 UTF-8 编码再执行带--default-character-setutf8mb4的导入命令。两件事要一起做只做一件都可能残留乱码。故障五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确认数据模型是干净的再往里填业务数据。这个习惯帮我挡掉了至少三次数据模型埋雷的情况也希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →