最近在整理手头的源码项目,发现这套个人理财系统挺有代表性——SpringBoot+Vue前后端分离,MyBatis负责持久层,MySQL存数据,标准的企业级管理系统打法。比起那些动辄几十张表的ERP,这个系统业务边界清晰,该有的东西一样不少:登录鉴权、收支记账、预算管理、账单报表、分类统计,做完之后对整套技术栈的理解会非常通透。
这篇文章我想把整个项目的拆解过程完整记录下来,从架构选型思路、数据库表设计,到后端接口实现、前端页面联动,再到最后怎么打包部署上线,都尽量讲透。同时也会把我在实际开发中踩过的坑、优化过的细节一并写出来。适合正在学SpringBoot+Vue整合开发的朋友,或者准备做管理系统类项目的同学,拿来当参考骨架非常合适。
1. 项目定位与技术选型的关键考量
1.1 为什么是SpringBoot+Vue,而不是别的组合
个人理财系统听起来简单,但其实业务链条挺长的:账户管理、收支流水、分类统计、预算控制、目标储蓄、报表分析,每个模块都有独立的逻辑和交互需求。选技术栈的时候,我首先考虑的是团队协作成本和后期维护难度。
后端选SpringBoot是没什么悬念的,SpringBoot能把项目从配置地狱里解放出来,内嵌Tomcat,打jar包就能跑,部署成本极低。而且Spring生态在企业级项目里的统治地位短时间不会动摇,无论是安全框架Spring Security、事务管理还是监控体系Actuator,都能无缝集成。
前端我用的是Vue。Vue对中小型团队极其友好,模板语法直白,单文件组件组织代码清晰,且生态里的Element UI组件库几乎可以覆盖后台管理系统的全部UI需求——表格、表单校验、弹窗、日期选择器、分页组件都是现成的,开发效率非常高。
MyBatis作为持久层框架,最大的优势是SQL可控。理财系统里有大量统计类SQL——按天汇总、按月分类、区间聚合,这类复杂查询用MyBatis的@Select注解或者XML文件写原生SQL,比全自动ORM好调试得多。而且MyBatis的缓存机制、动态SQL在写“根据不同条件查询流水列表”这类需求时特别顺手。
MySQL没什么好多说的,数据量级在百万以内,MySQL 5.7/8.0完全够用。如果后续数据量上来了,还可以平滑迁移到MySQL集群或云数据库,架构上不需要推翻重来。
1.2 这套架构解决的核心问题
先说一个很多人容易忽视的点:技术选型不是选最新的,而是选最稳妥的、团队上手最快的。这套架构的定位就是“务实”。
举几个具体的例子。项目要支持多用户使用,所以必然涉及权限控制。SpringBoot + Spring Security + JWT做无状态认证,前端拿到token存起来,每次请求带上即可,后端接口通过拦截器验token,这样既支持PC端也能无缝扩展到小程序端。
再看数据层面,理财系统的核心是账目,任何一笔金额的增删改都必须保证准确性。Spring注解式事务处理转账、记账类场景非常成熟,配合MySQL的InnoDB引擎行级锁,基本不会出现并发问题。
再比如报表模块。Vue端用ECharts做图表展示,后端提供聚类的JSON数据接口,前端拿到数据直接渲染。这种前后端分离的模式,后端只负责输出结构化数据,前端专注交互和可视化,实际上是在用一套后端同时支撑了Web端和管理端的需求。
2. 核心功能拆解与数据库设计思路
2.1 功能模块全景图
个人理财系统的功能我把它们归为五大块:用户中心、账务管理、预算管理、报表统计、系统配置。
用户中心是基础,涉及注册登录、个人信息、密码修改。账务管理是重头戏,包括账户体系(银行卡、现金、微信钱包、支付宝)、收支记录、转账记录、分类管理。预算管理要处理预算规则的制定和超支预警逻辑。报表统计则包括月度收支趋势、分类占比、账户余额变化等可视化图表。系统配置主要管理数据字典,比如交易类型、币种、分类标签。
整体上,这些模块都围绕一个核心实体展开——流水记录(transaction)。理解了这一点,数据库设计就能抓住主线。
2.2 数据库表结构的设计要点
建表的时候我遵循了几个原则:主键用自增id、金额用decimal(10,2)、所有表带create_time和update_time、业务唯一键用联合唯一索引控制。下面说几张核心表的结构思路。
用户表:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码密文(BCrypt)', `email` varchar(64) DEFAULT NULL, `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码存储这里特别提醒一句,千万别用明文,也别用MD5直接加密。我用的BCrypt,同一密码每次生成的hash都不一样,自带盐值,防彩虹表攻击的能力强得多。
账户表要记录用户下挂了哪些资产账户。关键字段包括账户名称、类型(1银行卡、2现金、3微信、4支付宝)、余额、初始余额、图标编码。这里有个小细节:账户余额不应该直接存储为实时余额,而是根据所有流水的收入减支出计算得出。如果硬存余额字段,一旦某笔流水被修改或删除,余额就要联动更新,事务控制稍有不慎就会出现账实不符的问题。
流水表是整张核心表:
CREATE TABLE `transaction` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `account_id` int(11) NOT NULL COMMENT '账户ID', `category_id` int(11) NOT NULL COMMENT '分类ID', `type` tinyint(1) NOT NULL COMMENT '类型 1收入 2支出', `amount` decimal(10,2) NOT NULL COMMENT '金额', `transaction_date` date NOT NULL COMMENT '交易日期', `note` varchar(255) DEFAULT NULL COMMENT '备注', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_date` (`user_id`,`transaction_date`), KEY `idx_user_category` (`user_id`,`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意我建立了idx_user_date这个联合索引。因为报表统计几乎都是按“某用户在某个时间范围”过滤,这个索引能直接命中查询,避免全表扫描。在数据量多起来之后,索引的好坏直接影响页面响应速度,这是数据库设计阶段就要提前考虑的事,不能等出了问题再补。
预算表、分类表之类的结构相对简单,但预算表要特别注意唯一约束——同一个月同一个分类只能有一条预算记录。用联合唯一索引uk_user_month_category建好,就能在数据库层面挡住重复数据,不需要在应用层再做一遍判断。
2.3 为什么不引入Redis做缓存
热词里有关于MyBatis缓存的讨论,正好说一下我的取舍。MyBatis自带的二级缓存默认是关闭的,需要手动开启,而且它的策略是namespace级别的,如果跨表查询、多表关联复杂,缓存命中率会很差,还可能读到脏数据。
这套理财系统我特意没有加Redis。原因很简单:数据量级和访问并发都不高,个人或小团队使用,QPS不会超过每秒几十次。在这种场景下引入Redis,反而多了一个中间件要部署、要维护,成本大于收益。所有的实时统计都走MySQL计算,加上合理索引,查询都在几十毫秒内完成,这是够用的。
如果你的场景真的要做成SaaS产品,用户量上来了,那可以在报表接口前加一层Redis缓存,key设计成report:user:{userId}:{dateRange},过期时间设置成5分钟。但这是后话,现在的架构阶段不需要。
3. 后端落地:从登录鉴权到流水报表的实现要点
3.1 SpringSecurity+JWT无状态登录的实现思路
项目里我选用Spring Security做认证授权,但并没有使用它默认的Session机制,而是整合了JWT实现无状态登录。这个组合的流程大致是:用户提交用户名密码 → 后端认证管理器校验 → 校验通过生成JWT返回前端 → 前端存储token并放在请求头的Authorization字段里 → 后端过滤器解析token并设置SecurityContext。
核心配置上,要重写WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法,关闭csrf、放行登录注册接口、其他所有请求都要认证。然后自定义一个JwtAuthenticationFilter,继承OncePerRequestFilter,在doFilterInternal里解析token,从Redis或者数据库里加载用户信息,写入SecurityContext。
具体代码逻辑大概是:
String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); String username = jwtUtil.getUsernameFromToken(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } }这里有个常用的小坑:Spring Boot版本太高或者Spring Security的版本API有变动,WebSecurityConfigurerAdapter在Spring Security 5.7之后标记为废弃,新版本推荐用SecurityFilterChain的Bean方式配置。如果你用的是Spring Boot 2.7以下版本,WebSecurityConfigurerAdapter还可以正常用;如果上了3.x,就需要改成组件注册风格了。这个在排错章节还会详细说。
3.2 记账核心接口的事务处理
记账是理财系统最核心的操作,一笔新增流水需要同时做两件事:向transaction表插入流水记录,再更新account表的余额。这两个操作必须在一个事务里,否则插入成功了但余额没更新,账就平不了。
我用@Transactional(rollbackFor = Exception.class)注解在Service方法上实现。
@Transactional(rollbackFor = Exception.class) public void addTransaction(TransactionAddDTO dto) { // 构建流水实体,写入transaction表 transactionMapper.insert(transaction); // 根据收入/支出类型,更新账户余额 Account account = accountMapper.selectById(dto.getAccountId()); if (transaction.getType() == 1) { account.setBalance(account.getBalance().add(dto.getAmount())); } else { account.setBalance(account.getBalance().subtract(dto.getAmount())); } accountMapper.updateById(account); }金额计算这块,我必须强调一下:所有金额字段都用BigDecimal,禁止用double或float。double的二进制存储机制决定了它无法精确表达某些十进制小数,比如0.1+0.2=0.30000000000000004。放在财务场景里这是不可饶恕的错误。
实际代码里,新增流水和删除流水还要考虑上下游依赖。比如你删掉了一笔收入流水,那账户余额也应该随之减少相应的金额。所以删除接口也要走事务,先删流水、再更新余额。如果删除的时候这个分类下的流水已经被报表引用了,就要用逻辑删除或者软删除的方式,避免统计出错。
3.3 报表统计的SQL聚合写法
报表模块是理财系统数据价值最直接的体现。月度收支统计、分类占比、账户余额趋势,这些都是典型的场景。
以“某个月份的分类支出统计”为例,SQL大概是:
SELECT c.category_name, SUM(t.amount) AS total_amount FROM transaction t LEFT JOIN category c ON t.category_id = c.id WHERE t.user_id = #{userId} AND t.type = 2 AND t.transaction_date BETWEEN #{startDate} AND #{endDate} GROUP BY c.category_name ORDER BY total_amount DESC这种SQL用MyBatis直接写XML映射非常直观,比JPA生成的JPQL好调试得多。数据量不大时性能完全没问题,有个几十万条流水后稍微优化下索引也够用。
月度收支对比折线图的数据,我后端直接返回12个月的收支汇总。大体思路是按月份聚合,但要注意没有流水的月份要补零,否则前端渲染曲线图会跳到坐标断点。这个补零逻辑我建议放在SQL层做,用一张月份数字表左连接流水数据,比在后端Java里循环填充更简洁。
3.4 MyBatis初始化与SQL日志配置
热词里提到了mybatis中xmlconfigbuilser,正好聊两句原理相关的东西。MyBatis启动时,XMLConfigBuilder负责解析MyBatis的全局配置文件(比如mybatis-config.xml),包括环境配置、映射器注册、别名设置等。如果用了Spring Boot的mybatis-spring-boot-starter,它会自动加载SqlSessionFactory,把Mapper接口注册成Spring Bean,这块的初始化原理对排查“Mapper找不到”“SQL语法报错”类问题非常重要。
开发阶段建议把MyBatis的SQL日志打印出来,配置很简单:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台就能看到每个接口执行的完整SQL和参数值,排查SQL错误、分析慢查询都非常直观。生产环境记得关掉,有日志量膨胀风险。
4. 前端落地:Vue页面结构与核心交互设计
4.1 前端项目的目录组织和路由规划
前端用Vue 2 + Element UI + Vue Router + Axios + ECharts,这是目前中小型管理系统最高效的套装。如果你现在新起项目,可以直接用Vue 3 + Element Plus,写法会有些差异,但整体思路完全一致。
路由设计上,我采用了嵌套路由和动态路由结合的方式。基础路由包括登录页、注册页;登录后的主框架路由下挂载各个功能页。这里说下动态路由的考量:虽然当前系统只有“普通用户”一种角色,但为了后续扩展成有管理员角色的多用户系统,我按权限码动态加载路由表。定制一个路由生成函数,根据后端返回的权限列表过滤出该用户可见的页面。
典型的路由结构:
{ "path": "/dashboard", "component": "Layout", "children": [ { "path": "overview", "name": "Overview", "component": "views/Overview" }, { "path": "accounts", "name": "Accounts", "component": "views/Accounts" }, { "path": "transactions", "name": "Transactions", "component": "views/Transactions" }, { "path": "budget", "name": "Budget", "component": "views/Budget" }, { "path": "reports", "name": "Reports", "component": "views/Reports" } ] }4.2 记账页面的表单交互细节
记账页面是用户使用频率最高的页面,交互设计上有一个细节很关键:用户录入金额、选择分类、填写备注后点击“保存并继续记账”。我通过重置表单部分字段、保留账户和分类选择状态来实现连续记账场景下的高效录入。如果用户每天有十几笔零碎消费,这个操作节奏能节省大量时间。
另外一个细节是金额输入的体验优化。前端input绑定的是字符串类型的值,在提交前做格式校验:允许小数点后最多两位,不允许负数,超过两位小数自动截断。校验逻辑放在前端后,配合后端@DecimalMin、@Digits注解做二次校验,形成双层保护。
分类选择器我用的是级联选择组件。数据结构上做两层:父级是大类(餐饮、交通、购物、居住、娱乐等),子级是细分类(餐饮-早餐/午餐/外卖/零食)。后端返回的是一棵树,前端渲染成下拉树结构。选型时用“最后一级分类的id”入库,保存为category_id。设计这个层级结构时要注意,报表统计时要能按父级分类聚合,所以SQL里要么做递归查询,要么在分类表里把父级编号带上,用LEFT(category_code, 2)截取前缀来聚合,简单高效。
4.3 Axios请求封装与拦截器处理
前后端分离模式下,Axios拦截器是任何一个项目都必须做好的基础设施。我在request.js里统一做了三件事:请求拦截器自动挂载token、响应拦截器统一处理错误码、特定状态码强制跳转登录页。
// 请求拦截器 service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); // 响应拦截器 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error => { Message.error(error.response?.data?.message || '服务器异常'); return Promise.reject(error); } );这里有个已踩过的坑:后端接口返回数据格式必须统一。我定义了一套标准响应体:{ code: 200, message: "success", data: {...} },所有Controller通过Result类包装返回,这样前端拦截器只需要处理一种结构,可维护性大幅提升。
4.4 ECharts图表数据的对接
报表页面的图表通过ECharts实现。关键是后端返回的数据结构要能直接被ECharts的组件消费。
比如“月度收支趋势”图,后端返回:
{ "months": ["2025-01", "2025-02", "2025-03"], "income": [3450.00, 5600.00, 2300.00], "expense": [2345.00, 3210.00, 4560.00] }前端拿到数据后,直接映射到ECharts的配置项:
option = { xAxis: { type: 'category', data: data.months }, yAxis: { type: 'value' }, series: [ { name: '收入', type: 'bar', data: data.income }, { name: '支出', type: 'bar', data: data.expense } ] };分类占比这一块,我用的是环形图,后端按分类聚合金额,前端把{ name, value }数组直接塞进去就可以了。ECharts的常用图表基本都支持Array格式的数据转换,前端不需要做任何加工。
5. 从开发到上线:环境配置与打包部署的完整路径
5.1 本地开发环境的搭建
你得先把基础环境准备好。JDK使用1.8/11均可,Maven选择一个3.6+版本,Node.js建议14以上。MySQL的安装配置网上的资料很多,但有几个比较容易出问题的地方我提醒一下:
- Windows下安装MySQL 5.7或8.0时,记得安装完执行
mysql_install_db或者通过服务安装工具初始化,否则启动服务后会报table 'mysql.user' doesn't exist之类的错误。 - root用户的加密规则:MySQL 8.0默认使用
caching_sha2_password,而很多老版本连接驱动用的是mysql_native_password,版本不匹配会导致应用连接失败。解决方案是在MySQL里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';,或者直接升级驱动到最新版。
后端配置文件application.yml的大致内容:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/personal_finance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.finance.entity数据库初始化SQL文件我会放在项目根目录的db/init.sql下,首次建库建表直接执行这个脚本即可。
5.2 前后端分离部署的三种方案
开发过程中,前后端是分别启动的,前端npm run dev起在localhost:8081,后端SpringBoot起在8080,两者通过代理方式联通。但上线就涉及部署策略选择,我总结了三套方案:
方案一:前后端完全分离部署
前端构建后部署到Nginx,后端打jar包放服务器上跑。前端请求接口通过Nginx配置反向代理到后端端口。这是最主流、最灵活的方式,适合前后端有独立上线节奏的团队。
方案二:前端打包后放入SpringBoot静态目录
这个方案严格说不算前后端分离,但适合“没人管运维”的场景。前端构建后的静态资源直接放到SpringBoot的src/main/resources/static下面,打成一个大jar包,只开一个8080端口就完事。缺点是每次前端改版都要重新打包后端,不太灵活。好处是真的省事,内网小项目很实用。
热词里有一个“vue打包放进springboot中”,说的就是这种玩法。具体操作是:前端执行npm run build生成dist目录,拷贝里面的所有内容到后端的src/main/resources/static目录,重新打包后端即可。
方案三:Docker Compose整合部署
在服务器上用docker-compose启动MySQL容器、后端容器、Nginx容器、前端。
前两种方案在小型项目里最常见,方案三属于进阶玩法,等真正有需求了再研究也不迟。
5.3 一个重要的安全细节:数据库密码不要硬编码
我在第一版源码里犯过一个低级错误,数据库密码直接写在application.yml里,直接传到了git仓库里。后来虽然改了,但密码已经留在提交历史里了。
建议使用Spring Security的加密属性或者配置中心的方案。最简单的方式是使用环境变量:
spring: datasource: password: ${DB_PASSWORD}这样本地开发时在IDEA的Environment variables里配置,部署上线时在服务器环境变量里配置,密码不会进入代码仓库,安全性提高一个档次。
5.4 跨域问题的处理
前后端分离开发中,跨域是一道绕不开的门槛。开发环境我用Vue CLI的devServer代理解决:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这样前端请求/api/login时,开发服务器会代理到http://localhost:8080/login,避免了浏览器的跨域限制。生产环境如果走Nginx反向代理,同样在location /api里配置proxy_pass。
如果后端要直接支持跨域,也可以在Spring Boot里配置全局CorsFilter,允许指定域名访问,但要注意不要把allow-origin设置成*,否则前端带了Cookie就失效了。
6. 常见问题与排查技巧实录
6.1 热词问题速查:从MyBatis到MySQL的实战排错
做这个项目的过程中,我遇到的高频问题不少。整理成一张速查表:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
启动报Failed to configure a DataSource | 没配置数据源或者配置格式不对 | 检查application.yml中spring.datasource.url/username/password是否完整 |
| 访问接口返回401 | token过期或者没带token | 检查前端是否在请求头带Authorization,检查JWT过期时间设置 |
| 查询报表很慢 | 缺少索引或索引没用上 | 用EXPLAIN分析SQL,给where条件字段建联合索引 |
| 保存流水后账户余额不对 | 事务没有生效 | 检查Service方法是不是被同类内部调用导致@Transactional失效(Spring事务代理机制),以及是不是catch了异常导致回滚失败 |
前端构建报This dependency was not found | 依赖缺失 | 删除node_modules,重新执行npm install |
| MyBatis控制台不打SQL | 日志级别配置缺失 | 按上文配置log-impl: org.apache.ibatis.logging.stdout.StdOutImpl |
| 中文乱码 | 字符集配置不一致 | 数据库连接URL加characterEncoding=utf8,数据库表用utf8mb4 |
| MySQL连接报SSL错误 | MySQL 8.0默认开启SSL | 连接URL后面加sslMode=DISABLED或useSSL=false |
6.2 事务不生效的那个隐蔽坑
上面表格里提到“事务不生效”,我一定要展开说一下。这个问题的现象是:新增流水成功,但账户余额没变,日志里还能看到更新余额的SQL执行了,可数据就是不对。排查到最后发现,问题出在Service方法的调用上。
假设我写的代码是TransactionServiceImpl里的addTransaction()调用了同类里的updateAccountBalance(),而updateAccountBalance()标了@Transactional,它是不会生效的。原因是Spring事务是基于AOP代理实现的,只有通过代理对象调用方法时,事务增强才会生效。而同一个类内部调用,走的是this.method(),没有经过代理,注解无效。
解决办法有两个:要么把updateAccountBalance挪到另一个Service类里,要么直接在addTransaction这个方法上标注@Transactional,保证整个流程都在一个事务里。我最后选的是后者,因为整个“插入流水+更新余额”本来就应该是原子操作。
6.3 SpringBoot 3.x带来的升级影响
热词里有人问“springboot版本太高”怎么处理,这确实是个真问题。如果你从网上下载的源码是基于Spring Boot 2.x写的,直接换成Spring Boot 3.x,大概率会报一堆错——不是代码逻辑错了,而是API变动导致的编译错误。
Spring Boot 3.x有几个关键变化:
javax.servlet改成jakarta.servlet,所有import语句都要换- Spring Security 6.x中
WebSecurityConfigurerAdapter废弃,改成组件化的SecurityFilterChain配置 - 内置Tomcat升级到10.1
- MyBatis需要切换
mybatis-spring-boot-starter到3.x版本
如果你只是想跑通代码,最简单的方式是保持源码原有的Spring Boot版本不动,不要刻意升级。如果确实要升级,就在升级之前充分测试所有接口,逐个排查编译错误。
6.4 MyBatis一级缓存的一个小毛病
虽然说过没引入二级缓存,但MyBatis的一级缓存是默认开启的,作用域是SqlSession。在Spring整合环境下,每个Mapper操作都创建新的SqlSession,一级缓存基本失效。但如果同一个Service方法里同一个Mapper执行了两次带参数的相同SQL,第二次会直接走缓存,不会重新查库。
这本身不是问题,唯一要注意的是:同一SqlSession中,如果先执行了增删改操作,MyBatis会清空一级缓存,避免脏读。如果非要手动控制缓存行为,可以在Mapper接口方法上标注@Options(flushCache = FlushCachePolicy.TRUE)或flushCache="true",但绝大多数场景下不需要动这些默认行为。
6.5 启动端口冲突的快速定位
热词里有一个“idea 2026 怎么配置springboot服务 编辑配置数据比如启动端口”,这里也顺手说一下。IDEA里启动Spring Boot项目时,如果你想修改启动端口,有两种方法:
- 在
application.yml里改server.port - 在IDEA的Run Configuration里Environment variables添加
SERVER_PORT=8081(注意要用下划线,Spring Boot会把环境变量里的SERVER_PORT映射成server.port)
如果你启动时端口被占用,报Port 8080 was already in use,先在命令行查占用:
netstat -ano | findstr 8080看到进程PID之后:
taskkill /f /pid PID号如果是Linux服务器,就用lsof -i:8080和kill -9 PID。
6.6 前端页面样式错乱与缓存问题
实际部署中,前端页面容易出现“明明改了代码刷新却不生效”的尴尬。这是浏览器缓存在作祟。解决方案有两个:一是在Nginx里对静态资源关闭缓存:
location /static/ { expires -1; add_header Cache-Control 'no-store, no-cache, must-revalidate'; }二是构建时给文件打hash指纹。Vue CLI打包后的JS和CSS文件名自带hash值,文件名变了浏览器自然会加载新文件。如果用了Vite,构建逻辑是一样的。比较推荐第一种直接在Nginx层面解决,省力且稳定。
7. 更多扩展方向的思考
最后想聊聊这套系统后续可能的演进方向。
虽然基础的个人记账、账户管理、预算、报表都比较完整了,但按企业级标准看,还有不少可以扩展的空间。比如家庭共享记账,几个人共用一个账本,每个人添加流水后其他成员都能实时看到——这个场景需要在现有表结构上增加“家庭组”的概念,涉及多租户改造,工作量不小但业务价值很高。
再比如支付导入功能。现在很多用户的流水都分散在支付宝、微信、银行卡里,手动录入容易漏记。如果增加Excel模板导入,用户每月导出支付宝账单、Excel整理后一次性导入系统,再配合分类规则自动匹配,体验会好很多。
对接一些基础的数据分析能力也是方向之一,比如基于历史流水数据的消费趋势预测——这用简单的时间序列模型就能做,Python训练好模型后,通过HTTP接口暴露给Java后端调用,是典型的多语言系统集成场景。
不过这些都是锦上添花的部分了。现在的版本已经把核心主线和工程质量做扎实了,扩展起来是有清晰路径的,不会推翻重来。
这套系统我从设计到落地,前前后后花了大约一个月,大部分时间都花在数据库表设计和报表SQL优化上。写这篇文章一方面是对整个项目的一次复盘总结,另一方面也希望刚接触前后端分离开发的朋友能少走一些弯路。如果你们在跑源码或者开发过程中遇到什么问题,欢迎在评论里交流讨论,我看到的都会认真回复。