简介:面向大学生消费场景的智能记账系统毕业设计项目,基于Java语言与SpringBoot框架构建后端服务,前端使用Vue渐进式框架,数据库采用MySQL,实现了前后端分离的完整架构。压缩包内包含项目源码、数据库脚本、论文文档以及开发工具配置,经过严格调试,下载后可直接运行,非常适合作为毕业设计、课程设计或期末大作业参考。整个资源包共374个文件,大小约15.51MB,文件类型包括Java源码、Vue组件、SVG图标、PNG图片、SQL数据库脚本、bat启动脚本等,目录组织清晰,便于读者快速定位代码模块并开展二次开发。系统功能覆盖消费记账、账户管理、统计分析等核心模块,界面设计简洁美观,操作流程顺畅,能够以图表直观展示个人消费习惯与财务状况,有助于大学生培养理性消费意识。目前已有72人学习下载,作为高分毕业设计项目,无论是用于完整项目学习,还是作为实际财务工具,都具有良好的参考与应用价值。1. 大学生智能消费记账:从"记了白记"到"越记越省"的高分毕设工程大学生记账这事,十个人里有八个坚持不过两周,原因倒不是懒,而是市面上的记账 App 对校园场景的适配太弱:食堂刷卡、外卖红包、宿舍水电分摊这类高频低额消费,记录成本高,月底复盘又看不出钱到底花在哪。这套基于 SpringBoot+Vue+MySQL 的智能消费记账系统,把「AI 消费分类 + 预算超支预警 + 月度趋势分析」做成了完整的闭环,后端是标准的三层架构,前端是 Vue 组件化页面,数据库落六张核心表,答辩演示时能直接拿出「分类占比图」「预算消耗进度条」「月度对比折线图」三样硬货。适合两类人:一类是把这套源码当毕业设计基座、想在两周内凑齐完整系统的同学;另一类是已经写过单体 CRM 或商城、想看看前后端分离项目怎么把业务模块串起来的从业者。下面按实际拆解顺序,把技术选型、关键代码、踩坑点和验收路径逐层讲透。2. 整体架构与数据设计:六张表怎么扛住核心业务闭环2.1 前后端分离架构里的"职责边界"打开工程目录第一眼,会发现它没有把 Controller 和页面堆在一起,而是严格分成了 backend 和 frontend 两个独立模块。这是我推荐所有毕设项目都遵循的边界:SpringBoot 只负责暴露 RESTful 接口,Vue 只负责渲染和交互,两者通过 JSON 通信。这套系统的接口前缀统一走/api,前端在vue.config.js里配了 devServer 代理,开发环境下把/api转发到localhost:8080,后端不关心页面长什么样,前端也不关心 SQL 怎么执行。这么做的好处有两个:一是答辩时能讲清楚「低耦合」这个概念,而不是含糊地说"用了 SpringBoot 和 Vue";二是后期你想换掉前端框架或者改成小程序端,后端一套接口可以原样复用。实际项目中,某跨平台系统就是把这套 Vue 页面替换成了 uni-app 实现,接口只改了个跨域配置,业务逻辑一行没动。2.2 六张核心表的字段设计与关系梳理数据库脚本在sql/目录下,初始化脚本里一共有六张表,大部分表之间靠逻辑关联而非物理外键。很多同学拿到手第一件事就是删掉外键约束怕报错,这个做法不算错,但你要知道为什么这么设计——核心业务表的写入频率高,物理外键在插入时会多一次约束校验,对性能不友好,所以这系统只在事务层面保证一致性。用户表存账号密码和基础信息,密码用了 MD5 加盐存储,表里那个salt字段很多同学会忽略,答辩时主动提一句"密码不是明文存储"是加分项。账单表是绝对的核心,字段包含bill_type(1 支出 2 收入)、category_id、amount、bill_time、remark,其中amount用 DECIMAL(10,2) 而不用 FLOAT,避免浮点精度问题。分类表里内置二十种预设分类,数据在初始化脚本里已经灌好,前端加载时会按icon字段渲染对应的图标。预算表是预警功能的数据源,按月存储,budget_amount和used_amount两个字段配合计算进度。提醒表存储预算超支后的通知记录,共享表则用来处理 AA 制场景下的多人消费分摊。如果你想把项目做得更有亮点,我建议在现有基础上加一张tag表做消费标签,比如"奶茶""打车""学习资料",这属于低成本高回报的优化——不用动现有表结构,加张关联表即可。3. 后端核心模块落地:三大功能从请求到 SQL 的完整链路3.1 登录鉴权的 Session 与拦截器实现登录模块是整个系统的入口,技术选型走的是传统 Session + 拦截器方案,没有引入 JWT 或 Spring Security。对毕设级别项目来说这个选择很务实——代码量少,答辩时能把逻辑讲透,出问题也好排查。核心拦截器如下:@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } UserContext.setCurrentUser(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }这段代码里有两个容易被忽略的点。第一是UserContext,我用 ThreadLocal 存当前登录用户,这样在 Service 层任何地方都能直接取到 userId,不用把用户对象一层层往下传。第二是拦截器里设了 401 状态码而不是 200,这样前端 axios 响应拦截器能统一跳转到登录页,而不是每个接口都去判断返回码。参数说明:如果改造成 JWT,核心变化是把HttpSession session = request.getSession()换成从 Header 里取 token,其他业务代码基本不用动,这个点可以在答辩时作为"项目可扩展性"的论述素材。3.2 账单单条新增与批量导入的双通道设计账单管理模块同时支持单条录入和批量导入,单条录入走常规的 Controller-Service-Mapper 链路,批量导入则接收前端上传的 Excel 文件,用 EasyExcel 解析。@PostMapping("/bill/import") public Result importBills(@RequestParam("file") MultipartFile file) { Long userId = UserContext.getCurrentUser().getId(); try { List<BillImportDTO> dtoList = EasyExcel.read(file.getInputStream()) .head(BillImportDTO.class) .sheet() .doReadSync(); if (dtoList.size() > 500) { return Result.error("单次导入不能超过500条"); } int successCount = 0; List<String> failReasons = new ArrayList<>(); for (BillImportDTO dto : dtoList) { try { Bill bill = new Bill(); bill.setUserId(userId); bill.setAmount(dto.getAmount()); bill.setCategoryId(categoryService.findByUserAndName(userId, dto.getCategory())); bill.setBillTime(DateUtils.parseDateTime(dto.getTime())); bill.setBillType(dto.getType()); bill.setRemark(dto.getRemark()); billService.save(bill); successCount++; } catch (Exception e) { failReasons.add("第" + (dtoList.indexOf(dto) + 1) + "行导入失败:" + e.getMessage()); } } return Result.success("导入完成,成功" + successCount + "条,失败" + failReasons.size() + "条", failReasons); } catch (Exception e) { log.error("Excel解析失败", e); return Result.error("Excel格式错误,请使用系统提供的模板"); } }这段代码为什么用doReadSync()而不用监听器方式?因为同步读完后直接返回统计信息,交互上对用户更友好——能看到成功失败明细,而不是异步抛个"导入中请稍后"然后没下文。setCategoryId这行的逻辑值得注意,用户 Excel 里填的是分类名称,系统需要按 userId 和名称映射到 ID;找不到分类时findByUserAndName会抛异常,被内层 catch 捕获后计入失败列表,保证一整批文件不会因为一行脏数据全部回滚。3.3 预算预警:定时任务与"超支扣减"的临时一致性方案预算模块的实现有两条线:查询时实时计算和定时任务扫描生成提醒。实时计算走的是聚合 SQL,预算详情页需要展示「本月预算、已用金额、剩余比例」三个数字,这不能逐条把账单 load 进内存再累加,而是用一条分组查询。SELECT category_id, SUM(amount) AS used_amount FROM bill WHERE user_id = #{userId} AND bill_type = 1 AND bill_time BETWEEN #{startTime} AND #{endTime} GROUP BY category_id重点说一下定时任务那条线,@Scheduled(cron = "0 0 2 * * ?")每天凌晨两点跑一次,扫描所有预算表的记录,找出used_amount / budget_amount > 0.9的项,生成一条提醒记录。这里有个常见的理解误区:定时任务读的是预算表里的used_amount字段,而不是实时去账单表聚合,所以每晚两点前这个字段可能不是最新的。维护方式是在账单保存后同步updateBudgetUsedAmount(),如果改造成更严谨的方案,可以加事务消息或事件驱动,但毕设场景下「保存时更新 + 凌晨对账」已经够稳。提醒模块还做了「连续超卖保护」——同一天内同一分类已生成超支提醒时,不再重复生成,避免用户每次登录都看到五条相同的提醒。这个去重逻辑用了一条带date字段的唯一索引,简单但有效。4. 前端工程化落地:Vue 组件树、状态管理与 ECharts 图表联动4.1 从路由守卫到页面骨架的初始化链路前端技术栈是 Vue 2 + Element UI + ECharts,main.js里注册了全局组件和 axios 实例,路由配置里做了一层全局前置守卫:router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token'); if (to.path !== '/login' && !token) { next('/login'); } else { next(); } });这一段看起来简单,但定位 401 响应需要两处配合:路由守卫挡「没登录就访问」的场景,axios 响应拦截器挡「登录过期后继续操作」的场景。很多同学只在路由层做了拦截,结果用户登录状态失效后,在页面上点击任何按钮都没反应,还以为是系统的 bug。页面骨架采用经典的「左侧菜单 + 右侧内容区」布局,菜单项根据路由表自动生成,activeMenu从this.$route.path动态取,刷新页面不会菜单高亮错乱。顶栏右侧有用户头像下拉菜单,退出登录时调/logout接口并sessionStorage.clear()。4.2 账单页面的条件查询与分页联动账单列表页是业务逻辑最密集的页面,搜索条件有「时间范围、分类、金额区间、类型」,每个条件变化都要重新拉取数据。这里用 watch 监听条件对象,变化后重置页码为 1 再触发查询,避免用户在第三页筛选时跳到空白页。data() { return { queryParams: { pageNum: 1, pageSize: 10, categoryId: null, type: null, startDate: null, endDate: null, minAmount: null, maxAmount: null }, tableData: [], loading: false }; }, watch: { queryParams: { handler() { this.queryParams.pageNum = 1; this.fetchList(); }, deep: true } }deep: true这一步很关键,对象内部字段变化不会触发浅层监听。分页组件用 Element UI 的el-pagination,@current-change事件触发查询;如果你发现切换页码时查询条件丢失,检查有没有把queryParams传到后端——这里的写法是每个查询条件都作为独立参数拼接到查询字符串,后端用@RequestParam(required = false)接收。4.3 ECharts 三张图的响应式适配技巧「分类占比」「月度趋势」「预算进度」这三张图是答辩演示的颜值担当,全部在Statistics.vue里。ECharts 的图表容器必须要有明确高度,style="height: 360px"写在div上;初始化时机放在mounted里,但数据是异步加载的,所以要先初始化图表再setOption,数据回来后再setOption一次。窗口自适应这块有个容易踩的坑:直接监听window.resize后调用chart.resize(),如果页面上多个图表会卡顿。实操上我用了一个轻量方案——在resize回调里加debounce300ms,同时用document.querySelectorAll('.chart-container')定位所有图表实例统一调整。另外页面隐藏再显示时,tab 切换会导致图表宽度计算成 0,所以用nextTick重新调用chart.resize(),配合keep-alive保留图表状态。5. 常见问题排查:从跨域到金额精度的五个真实踩坑记录5.1 CORS 跨域配置导致登录后接口全部 401现象:前端能打开登录页,但登录成功后所有业务接口都报「跨域错误」,浏览器控制台显示CORS policy红字。原因:前端开发服务器在 8081 端口,后端接口在 8080,浏览器拦截了跨域请求。很多同学以为在vue.config.js配了代理就完事,但代理只在开发环境生效,你直接输入http://localhost:8080/api访问还是会被拦。解决:后端加一个 WebMvcConfigurer 配置类,允许指定来源和携带凭证。注意allowCredentials(true)时allowedOriginPatterns不能用"*",必须写具体域名或使用allowedOriginPatterns("*")来适配。5.2 前端金额显示 0.30000000000000004现象:账单列表里金额明明是 9.9 元,显示成 9.899999999999999。原因:MySQL 字段类型用了 FLOAT,Java 实体类用了 Float/Double,二进制浮点运算产生了精度误差。解决:数据库amount字段改成 DECIMAL(10,2),Java 用BigDecimal,前端展示直接拿后端返回的数字字符串,不要再做乘除运算。所有涉及金额聚合统计的地方都走 SQL 的SUM(amount),不要在 Java 里逐个累加。5.3 时区问题:账单时间总是差八小时现象:前端录入晚上八点的账单,查询列表时变成凌晨零点。原因:MySQL 连接串没加serverTimezone=Asia/Shanghai,SpringBoot 默认按 UTC 时区解析时间字段。解决:JDBC URL 追加?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8,同时实体类里时间字段统一用java.util.Date,前端传参时用timestamp字符串(new Date().getTime()),避免字符串格式解析出歧义。5.4 自定义分类删除后旧账单显示空白现象:删除一个分类,历史账单的分类名称显示为空。原因:账单表只存了category_id,没冗余存分类名,删除分类后 JOIN 查不到数据。解决:删除分类时做逻辑删除,给category表加is_delete字段,查询账单时 LEFT JOIN 分类表并过滤is_delete = 0;如果执意物理删除,就把分类名冗余到账单表的category_name字段,查询后直接展示不用 JOIN。5.5 打包后前端访问/api404现象:本地开发一切正常,npm run build后部署到 Nginx,页面能打开但所有接口报 404。原因:Nginx 没有做/api反向代理到后端服务,开发环境的 proxy 配置不会跟着打包产物走。解决:Nginx location 配置加一段proxy_pass http://127.0.0.1:8080;,注意斜杠路径匹配规则。另外打包后用nginx -c /usr/local/nginx/conf/nginx.conf验证配置语法,改完执行nginx -s reload。这套流程我在多个项目中都用同样的套路,基本不会再遇到打包后接口不通的问题。6. 验收自测清单与两个低成本功能扩展从拿到源码到确认系统可运行,我建议新设备上按顺序做一遍完整验证:数据库导入初始化脚本 → 启动后端 → 启动前端 → 注册一个测试账号 → 手动录入十条不同分类的支出数据 → 检查分类占比图和月度趋势图是否多出对应点位 → 把月预算调低到 500 元 → 继续录满到超过 500 → 观察首页是否弹出超支提醒。这个过程如果顺利,说明环境配置、数据表和核心业务链路都没有被改坏。验收之后如果想加亮点,优先做「月度对比视图」,在统计页加一个同比选项,SQL 用DATE_FORMAT(bill_time, '%Y-%m')做维度分组,前后端各加一个小模块即可,比新做一个模块省时间但展示效果好。共享账单功能如果原始模块不够完善,可以做简化版:一张共享表记录账单 ID 和用户 ID 的映射,查询时用IN子句拼 userId 列表,也够应付演示。这套系统对我最大的启发是:毕设项目不需要追求技术栈多新,把 SpringBoot 的拦截器、MyBatis-Plus 的 CRUD、Vue 的生命周期这几样吃透,再配合合理的表结构,就能搭出一套逻辑自洽的完整系统。从那以后我每次评审毕设项目都强制走一遍上面的验收清单——登录、增删改查、统计图表、预算预警各摸一遍,有问题的模块在当下暴露,总比答辩现场翻车好。这套源码和配套论文我一直留在本地工程库里,希望帮到正在做类似选题的你。本文还有配套的精品资源,点击获取
简介:面向大学生消费场景的智能记账系统毕业设计项目,基于Java语言与SpringBoot框架构建后端服务,前端使用Vue渐进式框架,数据库采用MySQL,实现了前后端分离的完整架构。压缩包内包含项目源码、数据库脚本、论文文档以及开发工具配置,经过严格调试,下载后可直接运行,非常适合作为毕业设计、课程设计或期末大作业参考。整个资源包共374个文件,大小约15.51MB,文件类型包括Java源码、Vue组件、SVG图标、PNG图片、SQL数据库脚本、bat启动脚本等,目录组织清晰,便于读者快速定位代码模块并开展二次开发。系统功能覆盖消费记账、账户管理、统计分析等核心模块,界面设计简洁美观,操作流程顺畅,能够以图表直观展示个人消费习惯与财务状况,有助于大学生培养理性消费意识。目前已有72人学习下载,作为高分毕业设计项目,无论是用于完整项目学习,还是作为实际财务工具,都具有良好的参考与应用价值。1. 大学生智能消费记账:从"记了白记"到"越记越省"的高分毕设工程大学生记账这事,十个人里有八个坚持不过两周,原因倒不是懒,而是市面上的记账 App 对校园场景的适配太弱:食堂刷卡、外卖红包、宿舍水电分摊这类高频低额消费,记录成本高,月底复盘又看不出钱到底花在哪。这套基于 SpringBoot+Vue+MySQL 的智能消费记账系统,把「AI 消费分类 + 预算超支预警 + 月度趋势分析」做成了完整的闭环,后端是标准的三层架构,前端是 Vue 组件化页面,数据库落六张核心表,答辩演示时能直接拿出「分类占比图」「预算消耗进度条」「月度对比折线图」三样硬货。适合两类人:一类是把这套源码当毕业设计基座、想在两周内凑齐完整系统的同学;另一类是已经写过单体 CRM 或商城、想看看前后端分离项目怎么把业务模块串起来的从业者。下面按实际拆解顺序,把技术选型、关键代码、踩坑点和验收路径逐层讲透。2. 整体架构与数据设计:六张表怎么扛住核心业务闭环2.1 前后端分离架构里的"职责边界"打开工程目录第一眼,会发现它没有把 Controller 和页面堆在一起,而是严格分成了 backend 和 frontend 两个独立模块。这是我推荐所有毕设项目都遵循的边界:SpringBoot 只负责暴露 RESTful 接口,Vue 只负责渲染和交互,两者通过 JSON 通信。这套系统的接口前缀统一走/api,前端在vue.config.js里配了 devServer 代理,开发环境下把/api转发到localhost:8080,后端不关心页面长什么样,前端也不关心 SQL 怎么执行。这么做的好处有两个:一是答辩时能讲清楚「低耦合」这个概念,而不是含糊地说"用了 SpringBoot 和 Vue";二是后期你想换掉前端框架或者改成小程序端,后端一套接口可以原样复用。实际项目中,某跨平台系统就是把这套 Vue 页面替换成了 uni-app 实现,接口只改了个跨域配置,业务逻辑一行没动。2.2 六张核心表的字段设计与关系梳理数据库脚本在sql/目录下,初始化脚本里一共有六张表,大部分表之间靠逻辑关联而非物理外键。很多同学拿到手第一件事就是删掉外键约束怕报错,这个做法不算错,但你要知道为什么这么设计——核心业务表的写入频率高,物理外键在插入时会多一次约束校验,对性能不友好,所以这系统只在事务层面保证一致性。用户表存账号密码和基础信息,密码用了 MD5 加盐存储,表里那个salt字段很多同学会忽略,答辩时主动提一句"密码不是明文存储"是加分项。账单表是绝对的核心,字段包含bill_type(1 支出 2 收入)、category_id、amount、bill_time、remark,其中amount用 DECIMAL(10,2) 而不用 FLOAT,避免浮点精度问题。分类表里内置二十种预设分类,数据在初始化脚本里已经灌好,前端加载时会按icon字段渲染对应的图标。预算表是预警功能的数据源,按月存储,budget_amount和used_amount两个字段配合计算进度。提醒表存储预算超支后的通知记录,共享表则用来处理 AA 制场景下的多人消费分摊。如果你想把项目做得更有亮点,我建议在现有基础上加一张tag表做消费标签,比如"奶茶""打车""学习资料",这属于低成本高回报的优化——不用动现有表结构,加张关联表即可。3. 后端核心模块落地:三大功能从请求到 SQL 的完整链路3.1 登录鉴权的 Session 与拦截器实现登录模块是整个系统的入口,技术选型走的是传统 Session + 拦截器方案,没有引入 JWT 或 Spring Security。对毕设级别项目来说这个选择很务实——代码量少,答辩时能把逻辑讲透,出问题也好排查。核心拦截器如下:@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } UserContext.setCurrentUser(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }这段代码里有两个容易被忽略的点。第一是UserContext,我用 ThreadLocal 存当前登录用户,这样在 Service 层任何地方都能直接取到 userId,不用把用户对象一层层往下传。第二是拦截器里设了 401 状态码而不是 200,这样前端 axios 响应拦截器能统一跳转到登录页,而不是每个接口都去判断返回码。参数说明:如果改造成 JWT,核心变化是把HttpSession session = request.getSession()换成从 Header 里取 token,其他业务代码基本不用动,这个点可以在答辩时作为"项目可扩展性"的论述素材。3.2 账单单条新增与批量导入的双通道设计账单管理模块同时支持单条录入和批量导入,单条录入走常规的 Controller-Service-Mapper 链路,批量导入则接收前端上传的 Excel 文件,用 EasyExcel 解析。@PostMapping("/bill/import") public Result importBills(@RequestParam("file") MultipartFile file) { Long userId = UserContext.getCurrentUser().getId(); try { List<BillImportDTO> dtoList = EasyExcel.read(file.getInputStream()) .head(BillImportDTO.class) .sheet() .doReadSync(); if (dtoList.size() > 500) { return Result.error("单次导入不能超过500条"); } int successCount = 0; List<String> failReasons = new ArrayList<>(); for (BillImportDTO dto : dtoList) { try { Bill bill = new Bill(); bill.setUserId(userId); bill.setAmount(dto.getAmount()); bill.setCategoryId(categoryService.findByUserAndName(userId, dto.getCategory())); bill.setBillTime(DateUtils.parseDateTime(dto.getTime())); bill.setBillType(dto.getType()); bill.setRemark(dto.getRemark()); billService.save(bill); successCount++; } catch (Exception e) { failReasons.add("第" + (dtoList.indexOf(dto) + 1) + "行导入失败:" + e.getMessage()); } } return Result.success("导入完成,成功" + successCount + "条,失败" + failReasons.size() + "条", failReasons); } catch (Exception e) { log.error("Excel解析失败", e); return Result.error("Excel格式错误,请使用系统提供的模板"); } }这段代码为什么用doReadSync()而不用监听器方式?因为同步读完后直接返回统计信息,交互上对用户更友好——能看到成功失败明细,而不是异步抛个"导入中请稍后"然后没下文。setCategoryId这行的逻辑值得注意,用户 Excel 里填的是分类名称,系统需要按 userId 和名称映射到 ID;找不到分类时findByUserAndName会抛异常,被内层 catch 捕获后计入失败列表,保证一整批文件不会因为一行脏数据全部回滚。3.3 预算预警:定时任务与"超支扣减"的临时一致性方案预算模块的实现有两条线:查询时实时计算和定时任务扫描生成提醒。实时计算走的是聚合 SQL,预算详情页需要展示「本月预算、已用金额、剩余比例」三个数字,这不能逐条把账单 load 进内存再累加,而是用一条分组查询。SELECT category_id, SUM(amount) AS used_amount FROM bill WHERE user_id = #{userId} AND bill_type = 1 AND bill_time BETWEEN #{startTime} AND #{endTime} GROUP BY category_id重点说一下定时任务那条线,@Scheduled(cron = "0 0 2 * * ?")每天凌晨两点跑一次,扫描所有预算表的记录,找出used_amount / budget_amount > 0.9的项,生成一条提醒记录。这里有个常见的理解误区:定时任务读的是预算表里的used_amount字段,而不是实时去账单表聚合,所以每晚两点前这个字段可能不是最新的。维护方式是在账单保存后同步updateBudgetUsedAmount(),如果改造成更严谨的方案,可以加事务消息或事件驱动,但毕设场景下「保存时更新 + 凌晨对账」已经够稳。提醒模块还做了「连续超卖保护」——同一天内同一分类已生成超支提醒时,不再重复生成,避免用户每次登录都看到五条相同的提醒。这个去重逻辑用了一条带date字段的唯一索引,简单但有效。4. 前端工程化落地:Vue 组件树、状态管理与 ECharts 图表联动4.1 从路由守卫到页面骨架的初始化链路前端技术栈是 Vue 2 + Element UI + ECharts,main.js里注册了全局组件和 axios 实例,路由配置里做了一层全局前置守卫:router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token'); if (to.path !== '/login' && !token) { next('/login'); } else { next(); } });这一段看起来简单,但定位 401 响应需要两处配合:路由守卫挡「没登录就访问」的场景,axios 响应拦截器挡「登录过期后继续操作」的场景。很多同学只在路由层做了拦截,结果用户登录状态失效后,在页面上点击任何按钮都没反应,还以为是系统的 bug。页面骨架采用经典的「左侧菜单 + 右侧内容区」布局,菜单项根据路由表自动生成,activeMenu从this.$route.path动态取,刷新页面不会菜单高亮错乱。顶栏右侧有用户头像下拉菜单,退出登录时调/logout接口并sessionStorage.clear()。4.2 账单页面的条件查询与分页联动账单列表页是业务逻辑最密集的页面,搜索条件有「时间范围、分类、金额区间、类型」,每个条件变化都要重新拉取数据。这里用 watch 监听条件对象,变化后重置页码为 1 再触发查询,避免用户在第三页筛选时跳到空白页。data() { return { queryParams: { pageNum: 1, pageSize: 10, categoryId: null, type: null, startDate: null, endDate: null, minAmount: null, maxAmount: null }, tableData: [], loading: false }; }, watch: { queryParams: { handler() { this.queryParams.pageNum = 1; this.fetchList(); }, deep: true } }deep: true这一步很关键,对象内部字段变化不会触发浅层监听。分页组件用 Element UI 的el-pagination,@current-change事件触发查询;如果你发现切换页码时查询条件丢失,检查有没有把queryParams传到后端——这里的写法是每个查询条件都作为独立参数拼接到查询字符串,后端用@RequestParam(required = false)接收。4.3 ECharts 三张图的响应式适配技巧「分类占比」「月度趋势」「预算进度」这三张图是答辩演示的颜值担当,全部在Statistics.vue里。ECharts 的图表容器必须要有明确高度,style="height: 360px"写在div上;初始化时机放在mounted里,但数据是异步加载的,所以要先初始化图表再setOption,数据回来后再setOption一次。窗口自适应这块有个容易踩的坑:直接监听window.resize后调用chart.resize(),如果页面上多个图表会卡顿。实操上我用了一个轻量方案——在resize回调里加debounce300ms,同时用document.querySelectorAll('.chart-container')定位所有图表实例统一调整。另外页面隐藏再显示时,tab 切换会导致图表宽度计算成 0,所以用nextTick重新调用chart.resize(),配合keep-alive保留图表状态。5. 常见问题排查:从跨域到金额精度的五个真实踩坑记录5.1 CORS 跨域配置导致登录后接口全部 401现象:前端能打开登录页,但登录成功后所有业务接口都报「跨域错误」,浏览器控制台显示CORS policy红字。原因:前端开发服务器在 8081 端口,后端接口在 8080,浏览器拦截了跨域请求。很多同学以为在vue.config.js配了代理就完事,但代理只在开发环境生效,你直接输入http://localhost:8080/api访问还是会被拦。解决:后端加一个 WebMvcConfigurer 配置类,允许指定来源和携带凭证。注意allowCredentials(true)时allowedOriginPatterns不能用"*",必须写具体域名或使用allowedOriginPatterns("*")来适配。5.2 前端金额显示 0.30000000000000004现象:账单列表里金额明明是 9.9 元,显示成 9.899999999999999。原因:MySQL 字段类型用了 FLOAT,Java 实体类用了 Float/Double,二进制浮点运算产生了精度误差。解决:数据库amount字段改成 DECIMAL(10,2),Java 用BigDecimal,前端展示直接拿后端返回的数字字符串,不要再做乘除运算。所有涉及金额聚合统计的地方都走 SQL 的SUM(amount),不要在 Java 里逐个累加。5.3 时区问题:账单时间总是差八小时现象:前端录入晚上八点的账单,查询列表时变成凌晨零点。原因:MySQL 连接串没加serverTimezone=Asia/Shanghai,SpringBoot 默认按 UTC 时区解析时间字段。解决:JDBC URL 追加?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8,同时实体类里时间字段统一用java.util.Date,前端传参时用timestamp字符串(new Date().getTime()),避免字符串格式解析出歧义。5.4 自定义分类删除后旧账单显示空白现象:删除一个分类,历史账单的分类名称显示为空。原因:账单表只存了category_id,没冗余存分类名,删除分类后 JOIN 查不到数据。解决:删除分类时做逻辑删除,给category表加is_delete字段,查询账单时 LEFT JOIN 分类表并过滤is_delete = 0;如果执意物理删除,就把分类名冗余到账单表的category_name字段,查询后直接展示不用 JOIN。5.5 打包后前端访问/api404现象:本地开发一切正常,npm run build后部署到 Nginx,页面能打开但所有接口报 404。原因:Nginx 没有做/api反向代理到后端服务,开发环境的 proxy 配置不会跟着打包产物走。解决:Nginx location 配置加一段proxy_pass http://127.0.0.1:8080;,注意斜杠路径匹配规则。另外打包后用nginx -c /usr/local/nginx/conf/nginx.conf验证配置语法,改完执行nginx -s reload。这套流程我在多个项目中都用同样的套路,基本不会再遇到打包后接口不通的问题。6. 验收自测清单与两个低成本功能扩展从拿到源码到确认系统可运行,我建议新设备上按顺序做一遍完整验证:数据库导入初始化脚本 → 启动后端 → 启动前端 → 注册一个测试账号 → 手动录入十条不同分类的支出数据 → 检查分类占比图和月度趋势图是否多出对应点位 → 把月预算调低到 500 元 → 继续录满到超过 500 → 观察首页是否弹出超支提醒。这个过程如果顺利,说明环境配置、数据表和核心业务链路都没有被改坏。验收之后如果想加亮点,优先做「月度对比视图」,在统计页加一个同比选项,SQL 用DATE_FORMAT(bill_time, '%Y-%m')做维度分组,前后端各加一个小模块即可,比新做一个模块省时间但展示效果好。共享账单功能如果原始模块不够完善,可以做简化版:一张共享表记录账单 ID 和用户 ID 的映射,查询时用IN子句拼 userId 列表,也够应付演示。这套系统对我最大的启发是:毕设项目不需要追求技术栈多新,把 SpringBoot 的拦截器、MyBatis-Plus 的 CRUD、Vue 的生命周期这几样吃透,再配合合理的表结构,就能搭出一套逻辑自洽的完整系统。从那以后我每次评审毕设项目都强制走一遍上面的验收清单——登录、增删改查、统计图表、预算预警各摸一遍,有问题的模块在当下暴露,总比答辩现场翻车好。这套源码和配套论文我一直留在本地工程库里,希望帮到正在做类似选题的你。本文还有配套的精品资源,点击获取