ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot+Vue财务系统源码部署与改造:从跑通到避坑指南

Spring Boot+Vue财务系统源码部署与改造:从跑通到避坑指南

简介:基于SpringBoot与Vue的财务管理系统完整源码包,聚焦中小型企业财务信息化场景,适合Java方向的毕业设计、课程设计以及需要快速搭建可演示项目的开发者。系统功能覆盖首页、个人中心、员工管理、部门管理、员工工资与工资调整、资产类别、固定资产、经营信息、序时账、年度利润以及系统管理,业务链路完整、模块边界清晰,便于在论文写作或答辩演示时对照展开。资源共449个文件、约17.71MB,包含123个Java后端源码、47个Vue前端页面、161个SVG图标,另附SQL数据库脚本、SpringBoot配置文件和说明文档,整体结构便于按模块检索。目前已有138人学习/下载。包内还提供1-install、2-run、3-build等批处理脚本,配合说明文档可快速完成环境部署与项目启动;读者既可将其作为毕业设计的完整参考,也能基于源码进行二次改造,实现固定资产、工资核算等业务模块的个性化扩展。

1. 这个 springboot + vue 财务系统源码包,先别急着解压:它解决什么问题

基于springboot的财务管理系统源代码(java+vue+mysql+说明文档+LW).zip 这类压缩包,在毕业设计圈和公司内部工具仓库里都很常见。收到包的第一反应不应该是双击解压,而是先想清楚:我要拿它做什么?如果只是交作业,跑通演示就行;如果要给真实财务团队用,凭证、权限、对账逻辑才是核心,页面 UI 反而是最不重要的部分。Spring Boot + Vue + MySQL 这套技术栈本身门槛不高,一天内跑通属于正常操作;容易卡住你的,往往是数据库初始化、接口联调和金额精度这些“旁边”的问题。这个方向适合三类人:准备毕设/课设的在校生、需要快速搭一个内部记账系统的小团队、想读一份完整前后端分离项目源码的初级开发者。下面按环境准备、启动服务、改造凭证流程、避坑、验收的顺序,把一条可复现的落地路径讲清楚。

2. 跑通最小闭环:从 zip 压缩包到浏览器出现登录页

2.1 解压后先别跑:这四类文件分别是什么

压缩包名字里的“说明文档+LW”,在毕业设计场景下一般指配套的论文或设计报告,里面通常有 ER 图、功能截图、启动步骤。解压后先看文档,比瞎猜后端端口省时间。常见目录结构是后端代码、前端代码、数据库脚本、文档四件套。

unzip 基于springboot的财务管理系统源代码*.zip -d finance-system cd finance-system ls -la

用unzip解压时,中文文件名加空格容易触发参数解析问题,建议直接按 Tab 补全完整文件名。-d finance-system是解压到指定目录,避免 zip 内文件散落一地。ls -la后重点关注四类东西:含pom.xml的后端目录、含package.json的前端目录、含.sql的数据库脚本目录、以及 pdf 或 word 格式的说明文档。如果压缩包没有顶层目录,全部是散文件,就先mkdir finance-system && mv * finance-system把它们拢起来,后面操作路径清晰很多。

2.2 环境准备:JDK、Maven、Node、MySQL 版本先对齐

这个环节最容易翻车的是版本错位。Spring Boot 2.x 配 JDK 8 或 11 没问题;但如果 pom.xml 里是 Spring Boot 3.x,JDK 最低要 17,别在 JDK 8 上死磕。前端如果是 Vue 2,Node 14/16 比较稳;Vue 3 则建议 Node 16 以上。MySQL 5.7 和 8.0 都能用,但驱动类和连接串写法不一样,后文会单独说。

java -version mvn -v node -v npm -v mysql --version

先跑一遍这五条命令确认本机基线。java -version看的是 JDK 版本,mvn -v里能看到 Maven 使用哪个 JDK,如果系统装了多个 JDK,Maven 可能没走你期望的那个。node -v和npm -v是前端打包工具链的基础,npm跟 node 版本强绑定,版本过旧会出现依赖安装失败的莫名错误。mysql --version决定你后面连数据库时用哪套驱动配置。我一般会把版本信息截图或写下来,再去对照源码里的pom.xml和package.json,确认两边的预期一致再动手。

2.3 导入数据库:先建库,再执行 SQL 脚本

源码包里的 SQL 脚本不一定包含建库语句,很多只建表。如果你直接source某些只含CREATE TABLE的脚本,会报“未选择数据库”的错误。所以稳妥做法是手动建库,再导入。

mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS finance_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p finance_db < sql/finance_db.sql

第一条命令里-e是执行一段 SQL 并退出,utf8mb4必须显式指定,否则表里存中文和 emoji 都容易出乱码。COLLATE utf8mb4_general_ci是排序规则,对中文环境够用。第二条命令把脚本喂给finance_db库。执行成功后会看到 SQL 源文件里每一条语句的执行反馈,没报错就说明表和初始化数据进去了。检查一下表数量:

mysql -u root -p finance_db -e "SHOW TABLES;"

这时候你大概能看到voucher、voucher_detail、account_balance、sys_user之类的核心表。如果一张表都没有,说明脚本路径不对,或者脚本里本身带了建库语句,需要打开.sql文件前几行确认。

2.4 启动后端:先改数据库密码,再 mvn spring-boot:run

后端配置集中在src/main/resources/application.yml。打开后先看spring.datasource这一段,密码、库名、时区都要改成本机的。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/finance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

serverTimezone=Asia/Shanghai是必写参数,MySQL 8 的驱动会强校验时区,漏掉它启动直接报错。useUnicode=true&characterEncoding=utf8保证中文读写正常。密码字段改成你本机 MySQL 的实际密码。改完后,进入后端目录启动。

cd backend mvn clean spring-boot:run

第一次运行 Maven 会比较慢,因为要下载大量依赖。如果公司内网需要走代理,或者下载超时,就在~/.m2/settings.xml里配置阿里云镜像。看到类似Started ...Application in xx seconds的日志,说明后端起来了。这时候先用浏览器访问http://localhost:8080,如果返回空白或者 401,说明 Spring Boot 正常但拦截器挡了静态资源,这是好事;就怕连接拒绝。

2.5 启动前端:npm install 慢是常态,先配镜像源

前端启动相对简单,但npm install下载慢会消耗耐心。建议直接指定镜像源。

cd frontend npm install --registry=https://registry.npmmirror.com npm run serve

--registry参数只对当前命令生效,不改全局配置,比较干净。npm install结束后,npm run serve会把 vue-cli 的开发服务器拉起来。终端会打印一个 Local 地址,通常是http://localhost:8080,如果后端已经占了 8080,前端会自动切到 8081。打开地址看到登录页,说明最小闭环已经走通。这时候你还没真正前后端联调,只是把两个服务都跑起来了。下一步要做的,是看懂账务核心表,再去处理接口代理和登录鉴权。

3. 看懂账再改代码:财务核心表设计与凭证数据流

3.1 三张绕不开的表:凭证、明细、余额

财务系统不管前端写成什么样,后端的核心永远绕不开三张表:凭证主表、凭证明细表、科目余额表。凭证主表记录“这一笔单据是什么”,明细表记录“这笔单据里哪些科目借了多少钱、贷了多少钱”,余额表记录“每个科目到某个期间为止的累计数”。源码包里的表结构可能字段名不同,但语义基本一致。

CREATE TABLE voucher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_no VARCHAR(32) NOT NULL UNIQUE, voucher_date DATE NOT NULL, status TINYINT DEFAULT 0 COMMENT '0草稿 1已审核 2已过账', operator_id BIGINT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE voucher_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_id BIGINT NOT NULL, account_code VARCHAR(32) NOT NULL, summary VARCHAR(255), debit_amount DECIMAL(12,2) DEFAULT 0, credit_amount DECIMAL(12,2) DEFAULT 0, KEY idx_voucher (voucher_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE account_balance ( account_code VARCHAR(32) PRIMARY KEY, period VARCHAR(10) NOT NULL, begin_balance DECIMAL(14,2), current_debit DECIMAL(14,2), current_credit DECIMAL(14,2), end_balance DECIMAL(14,2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意voucher_detail里的金额字段用的是DECIMAL,不是double或float。这是财务系统的底线:浮点数的二进制表示在加减乘除后会产生 0.0000001 级误差,账面上会差一分钱。voucher_no设置唯一约束,是为了防止同一张凭证号被重复录入。account_balance用period字段区分月份,比如2025-07,这样期末结账可以只更新当前期间,不用全表扫描。很多新手直接把period设计成日期类型,反而难写按月查询。

如果源码包里带科目表,一般长这样:

CREATE TABLE account_subject ( account_code VARCHAR(32) PRIMARY KEY, account_name VARCHAR(100) NOT NULL, parent_code VARCHAR(32), level INT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

parent_code和level用来维护科目的层级关系,比如“1002 银行存款”是“1002.01 工行存款”的父级。报表模块做科目汇总时,通常需要根据层级决定是否把子科目余额向上归集。

3.2 录一张报销单,系统里发生了什么

一张报销单从前端提交到后端,至少要经过“校验借贷平衡 → 写凭证主表 → 写明细表 → 更新科目余额”四步。核心逻辑在 Service 层。源码里的实现可能更简单,但你要改业务时,最终一定会遇到这段。

@Transactional public Long createVoucher(VoucherDTO dto) { BigDecimal totalDebit = dto.getDetails().stream() .map(VoucherDetailDTO::getDebitAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalCredit = dto.getDetails().stream() .map(VoucherDetailDTO::getCreditAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); if (totalDebit.compareTo(totalCredit) != 0) { throw new BizException("借贷不平衡,无法保存"); } Voucher voucher = new Voucher(); voucher.setVoucherNo(dto.getVoucherNo()); voucher.setVoucherDate(dto.getVoucherDate()); voucher.setStatus(0); voucherMapper.insert(voucher); for (VoucherDetailDTO detail : dto.getDetails()) { VoucherDetail po = new VoucherDetail(); po.setVoucherId(voucher.getId()); po.setAccountCode(detail.getAccountCode()); po.setSummary(detail.getSummary()); po.setDebitAmount(detail.getDebitAmount()); po.setCreditAmount(detail.getCreditAmount()); voucherDetailMapper.insert(po); } // 更新 account_balance,按科目+期间累加 balanceService.refresh(voucher.getVoucherDate()); return voucher.getId(); }

这里@Transactional保证了凭证主表、明细表、余额表要么一起成功,要么一起回滚。compareTo是BigDecimal的比较方式,不能用equals,因为equals会比较小数位,比如1.00和1会返回 false。balanceService.refresh根据凭证日期找到对应期间,重新计算每个科目的借方累计、贷方累计和期末余额。这种做法实现简单,但并发高时容易把余额算重。改进方案是给account_balance加唯一索引(account_code, period),然后在更新时使用INSERT ... ON DUPLICATE KEY UPDATE做原子累加。

另一个细节:明细表里每条记录应该只填借方或贷方一个字段,不要两个都填。很多源码为了省事,允许同时填,结果做试算平衡时同一笔被算了两次。你在检查已有代码时,可以用后面第 3.4 节的 SQL 跑一遍,凡是借贷都不为 0 的记录,基本都埋了雷。

3.3 权限不复杂:用户、角色、菜单三张表决定你能看到什么

财务系统的权限比普通管理系统更讲究。出纳和会计不能看到同样的菜单,审核人员不能修改已过账凭证。源码包里如果用了 Spring Security 或 Shiro,表结构通常是用户、角色、菜单三张主表加两张关联表。

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(50) NOT NULL ); CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, menu_name VARCHAR(50), parent_id BIGINT, path VARCHAR(100), component VARCHAR(100), visible TINYINT DEFAULT 1 ); CREATE TABLE sys_user_role ( user_id BIGINT, role_id BIGINT ); CREATE TABLE sys_role_menu ( role_id BIGINT, menu_id BIGINT );

前端动态路由的数据来源,就是根据当前用户查sys_user_role到sys_role_menu,最后拿到sys_menu里的path和component。需要提醒的是:菜单表控制“能看到什么”,Controller 上的权限注解才控制“能操作什么”。很多毕设项目只做了菜单隐藏,后端接口仍然裸奔,任何人登录后直接调/api/voucher/save都能新增凭证。你如果要把这套系统用于真实场景,一定要给关键接口加@PreAuthorize("hasRole('FINANCE')")之类的注解。

3.4 试算平衡查询:每个财务系统都必须有的体检 SQL

验证数据是否正确,有一个通用 SQL:按凭证分组统计借方合计和贷方合计,把不平的凭证捞出来。这个脚本可以直接在 MySQL 里跑,不用改代码。

SELECT d.voucher_id, SUM(d.debit_amount) AS total_debit, SUM(d.credit_amount) AS total_credit FROM voucher_detail d GROUP BY d.voucher_id HAVING total_debit <> total_credit;

如果这个查询返回空,说明所有凭证都借贷平衡。如果返回了数据,就要去检查该凭证的明细,大概率是某条分录只填了借方,或者借贷两边金额不一致。我一般把这个查询写成 SQL 文件保存下来,每次改动完账务逻辑都跑一遍,它比看页面上的演示数据靠谱得多。

4. 把 Vue 和 Spring Boot 真正粘起来:联调三处必调参数

4.1 前端代理:devServer.proxy 里的 target、pathRewrite 和 changeOrigin

前端的开发服务器和后端 Spring Boot 通常不在同一个端口。浏览器直接访问前端的 8081,再请求http://localhost:8080/api/xxx会跨域。解决跨域最省事的方法不是配 CORS,而是在前端 vue.config.js 里做代理。

// frontend/vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

target是后端真实地址,changeOrigin: true会把请求头里的 Host 改成 target 域名,这样后端识别不到跨域来源,省去在后端配 CORS 的麻烦。pathRewrite是重写路径的开关:如果后端接口路径是/voucher/list,而前端统一加了个/api前缀,就需要用pathRewrite把/api剥掉;如果后端本来就是/api/voucher/list,那这行就不要写,写了反而变成/voucher/list,直接 404。这也是联调时最常翻车的地方。改完代理配置后,必须重启npm run serve,vue.config.js 不会热更新。

4.2 axios 拦截器:Authorization 头与 401 跳转

前后端打通后,第二步是把登录返回的 token 塞进每次请求。常见源码里用的是 JWT,后端过滤器会读取Authorization: Bearer <token>。axios 拦截器是放 token 最合适的位置。

// frontend/src/utils/request.js import axios from 'axios' import router from '@/router' axios.defaults.baseURL = '/api' axios.interceptors.request.use(config => { const token = sessionStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) axios.interceptors.response.use( res => res.data, error => { if (error.response.status === 401) { sessionStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

config.headers['Authorization'] = 'Bearer ' + token是 JWT 的标准写法,注意 Bearer 后面有一个空格。有的源码后端用的是自定义 token,不认 Bearer 前缀,那你得打开后端过滤器的源码确认,否则请求会一直 401。401 跳转是页面层的兜底,但如果后端返回的是 200 加错误码,这里的拦截器就不会触发,需要结合后端统一返回体的设计再判断。baseURL和 vue.config.js 里的代理前缀要保持一致,这里设置/api,代理里就代理/api,不要写两套。

4.3 生产打包:Vue 构建后塞进 Spring Boot 的 static,路由要配 fallback

开发联调完,很多人会想把 Vue 打包后放进 Spring Boot,做成一个单 jar。这个思路没问题,但有一个很大的坑:如果前端路由用的是history模式,直接访问http://localhost:8080/voucher/list,Spring Boot 不知道这个路径对应哪个 Controller,会返回 404。

cd frontend npm run build rm -rf ../backend/src/main/resources/static/* cp -r dist/* ../backend/src/main/resources/static/

打包命令本身简单,关键是后端要补一个转发规则,把不属于/api的路径全部转发到index.html。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}") .forwardTo("/index.html"); } }

这个配置里的正则[^\\.]*表示“路径中不包含点号”。因为静态资源如js/app.js、css/app.css都带后缀,应该走静态文件解析,只有干净的路径才应该转发到index.html。如果你用了 hash 路由,URL 里会带#,其实不需要这个转发;但为了地址好看,很多项目会选择 history 模式,那这段配置就是必须的。还有一个常见问题是static目录重复:你拷贝前必须先清空,否则旧文件会和新文件混在一起,出现改了代码但页面没变化的假象。如果想做 jar 包整体交付,把后端打成 jar 后,用java -jar启动即可,静态资源会自动从 classpath 读取。

5. 避坑:Spring Boot + Vue + MySQL 联调的 5 条翻车记录

5.1 页面出来了,但所有请求都是 404

现象:前端登录页正常打开,提交登录后控制台显示POST http://localhost:8081/voucher/login 404,后端日志没有任何新东西。

原因:前后端接口前缀不一致。前端请求路径是/voucher/login,后端实际接口是/api/voucher/login,而代理配置里没有做路径重写,请求落到了前端的 devServer 上,自然 404。

解决:先在后端 Controller 上确认@RequestMapping的值。如果后端是/api/voucher/login,要么把前端请求都加上/api前缀,要么在代理里配置:

proxy: { '/voucher': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/voucher': '/api/voucher' } } }

改完必须重启npm run serve。以后遇到联调 404,第一件事不是看代码,而是看后端接口实际路径和前端 proxy 的映射关系。

5.2 后端启动报错:MySQL 时区乱码

现象:执行mvn spring-boot:run后,日志里出现java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone。

原因:这是 MySQL 8 驱动对时区的强校验。数据库返回的时区名称是CST,但 JVM 的默认时区解析不了,于是出现了 GBK 乱码。

解决:在application.yml的 URL 后面加上serverTimezone=Asia/Shanghai。如果还报错,加上useSSL=false去掉 SSL 连接警告。注意useUnicode=true不要删。

url: jdbc:mysql://localhost:3306/finance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

这个坑只在本地环境首次跑时出现,一旦补上就不会再犯,但每次换电脑都要再撞一次,所以我会把这段 URL 写在说明文档的首页。

5.3 springboot 版本太高,JDK 8 编译不过

现象:mvn clean package报错java: invalid target release: 17,或者一堆cannot find symbol指向jakarta.servlet。

原因:pom.xml 里的spring-boot-starter-parent是 3.x 版本。Spring Boot 3 默认要求 JDK 17,并且把包名从javax.*换成jakarta.*。如果源码还在用javax.servlet,或者本机只有 JDK 8,自然编不过。

解决:先看本机 JDK。如果你坚持用 JDK 8,就把 pom 里的 Spring Boot 版本降回 2.7.x。注意javax和jakarta的代码要对应 Spring Boot 2.x,否则代码里的import javax.annotation.PostConstruct会变成编译错误。如果你本机已经装了 JDK 17,那直接mvn clean也能跑,但很多毕业设计源码里引用的第三方库在 Jakarta 下需要额外改写,反而更费劲。

5.4 登录成功后刷新白屏

现象:前端用 history 路由,跳转到/dashboard后一切正常,但按 F5 刷新就白屏,控制台报Cannot GET /dashboard。

原因:浏览器向 Spring Boot 发起了真实 HTTP 请求,后端没有对应这个路径的 Controller,所以返回 404。hash 路由不存在这个问题,因为#后面的路径不会发到服务器。

解决:后端加上静态资源 forwarding 配置,把非接口请求都转发到index.html。前面 4.3 节里的WebMvcConfig就是标准解法。如果你最终没做单 jar 部署,而是用 Nginx,也在 Nginx 配置里加一行:

location / { try_files $uri $uri/ /index.html; }

这个问题本质上是前端路由和后端资源解析的边界问题,理解了原理,换什么部署方式都不会慌。

5.5 对账不平:金额差了一分钱

现象:试算平衡 SQL 跑出来,某些凭证的借贷合计差 0.01,甚至差出负数。前端页面看着是 100.00,数据库里存的是 99.999999。

原因:数据库表字段用了float或double,Java 实体类用了Double。浮点数在二进制下无法精确表示 0.1,累加后误差被放大。

解决:把所有金额字段改为DECIMAL(14,2),Java 类型改成BigDecimal,实体类的 setter 不要用Double。从 JSON 接收金额时,前端传字符串,后端用new BigDecimal(value)而不是Double.parseDouble。这个坑不一定在初次联调时暴露,但等到月底对账时一定会爆,属于早晚要还的债。

6. 从“能跑”到“敢用”:财务数据的验证清单与习惯

跑通源码包只是起点,敢不敢把数据交给它,取决于你做了几层验证。我会按以下清单过一遍,每一条都能在半小时内完成。

第一层是接口层验证:用 Swagger 或 Postman 登录,拿到 token 后调用新增凭证接口,故意构造一组不平的借贷数据,看后端是否拒绝。如果源码没有做校验并仍然保存成功,说明账务核心逻辑不完整,需要自己补上。

第二层是数据层验证:跑一遍试算平衡 SQL,要求结果为空。再用余额表做核对,检查每个科目期末余额是否等于期初加上本期发生额:

SELECT account_code, begin_balance, current_debit, current_credit, end_balance, (begin_balance + current_debit - current_credit) AS expected_balance FROM account_balance WHERE period = '2025-07' AND end_balance <> (begin_balance + current_debit - current_credit);

这个查询把所有余额计算不一致的科目直接捞出来。如果你改过余额更新逻辑,一定跑一次。

第三层是权限层验证:用普通操作员账号登录,确认看不到菜单;再用管理员账号登录,尝试直接访问后端接口 URL,比如/api/user/list,如果返回数据,说明权限只控制前端,没控制后端,需要补注解。

最后一层是备份验证:财务系统最怕丢数据,我会在结账前跑一次mysqldump,并把备份文件加上日期后缀。

mysqldump -u root -p finance_db > finance_db_$(date +%Y%m%d).sql

这套系统能不能真正投入,不取决于它是不是基于 springboot、用没用 vue,而取决于你有没有把期末结账、凭证审核、权限隔离这些边界补完整。我做这类项目的一个习惯是:每次改完账务逻辑,先拿一组不平衡数据和一组平衡数据分别跑一遍,确认系统该拒绝就拒绝、该通过就通过,再往下做页面。这种验证方式虽然土,但能帮你避开月底对账时的手忙脚乱。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进