
1. 选题背景与项目定位“财来财往”这个名字第一次听到的时候还觉得挺有意思——它不是那种一板一眼的“智能记账本”或者“未来财务管理”而是带着一种生活气息。实际上这就是一个基于 Spring Boot 和微信小程序搭建的轻量级记账与个人财务管理应用。作为计算机毕业设计选题它的难度适中方向明确既能体现后端接口开发能力又能展示小程序端交互设计水平非常适合那些前后端都想兼顾一下、但又不希望技术栈过于庞杂的同学。这个项目要解决什么问题说白了就一个字记账。日常收入支出太零散月底总不知道钱花哪了。小程序生态成熟微信登录零门槛用户扫一扫就能用后端用 Spring Boot开发效率高、生态资料多、部署也简单毕业设计答辩时讲解起来非常顺。适合的人群很明确计算机相关专业、正在准备毕业设计或课程设计的学生尤其是想独立完成全栈小项目、但前端不想碰 Vue/React 大工程的同学。我在实际做完这个项目之后最大的感受是它不只是一个记账工具更像一个把所有核心技能串起来的综合练习。用户登录、数据建模、接口设计、图表统计、事务处理、异常拦截、部署上线每一步都有踩坑点每一步也都有成长空间。下面我按完整的开发路线展开把整个项目的设计思路、关键实现、常见坑点全部拆开讲清楚。2. 核心需求拆解与数据模型设计2.1 用户需求与功能边界划分做毕业设计最容易犯的错是一上来就堆功能结果数据库表建了二十张最后能跑通的没几个。我的建议是先圈定一个“最小可用闭环”把故事讲完整再考虑扩展。“财来财往”项目的核心闭环是微信授权登录 - 记一笔账 - 分类统计 - 生成趋势报表 - 按月份回顾。整个流程走通项目就已经完整了。我最终圈定的功能模块是这样的用户模块微信登录、个人信息维护、会话管理记账模块收入/支出记录、分类管理、备注与时间编辑统计模块按分类汇总、按月趋势、按收入/支出分项对比预算模块月度预算设置、超支提醒这个作为加分项实现没有做的东西也值得一提多账户管理、周期性账单、多人共享账本、账单导入导出这些不是不重要而是毕设周期太短做好核心比什么都做要强得多。答辩时老师问“能不能扩展”你有清晰的设计留白反而是加分项。2.2 数据库设计从需求到表结构数据库设计是整个项目的基石表建得不好后面写 SQL 和做统计时会异常痛苦。我用的是 MySQL 8.0核心表总共五张设计如下用户表useropenid、昵称、头像 URL、性别、注册时间、最后登录时间。openid 是微信侧的唯一标识直接设为唯一索引作为业务查询的关键字段。账单表bill用户 ID、收支类型0 支出 / 1 收入、金额、分类 ID、备注、记账时间、创建时间、更新时间。其中分类 ID 关联分类表记账时间单独存不直接用创建时间替代因为用户可能需要补记昨天的账。分类表category分类名称、类型收入类/支出类、图标 URL、排序值。内置的常见分类在项目启动时通过初始化数据写入比如餐饮、交通、购物、工资、红包等。预算表budget用户 ID、月份格式 YYYY-MM、预算金额、创建时间。这里用“用户月份”做唯一索引防止同一用户同一个月重复插入预算。支出明细表可选daily_expense这个表不是必须的但如果你需要做每日支出趋势图建议单独建一张聚合表或者直接基于 bill 表按日期分组后计算。小数据量下直接用 SQL 聚合即可不需要额外建表。使用 JSON 存储一些用户偏好设置也可以但我觉得对毕设来说关系型表结构更直观答辩时也更容易讲清楚设计意图。整体采用 InnoDB 引擎字符集 utf8mb4排序规则选择utf8mb4_unicode_ci保证中文数据没问题。2.3 表关系的业务含义用户与账单是一对多关系这是最基础的主外键关系。分类与账单是一对多关系但分类表本身是公共数据不分用户简化了设计。预算与用户是多对一关系每个用户每个月可以配置一条预算记录。这里有一个容易踩坑的地方外键要不要建物理外键。我的建议是不建物理外键但保留逻辑关联字段并在代码层做校验。为什么因为物理外键在后期做删除、批量导入时会带来各种约束麻烦而且小程序后端开发中物理外键对性能也有轻微影响。我在代码里通过 Service 层手动校验分类 ID 是否存在、用户 ID 是否有效逻辑上等价但灵活度更高。3. 技术选型分析与系统架构设计3.1 为什么是 Spring Boot 微信小程序先说后端。Spring Boot 现在基本上已经是 Java 后端开发的代名词了它的自动配置机制让开发者不用再做繁琐的 XML 配置内嵌 Tomcat 让部署变成“打包、运行”两步Spring Data JPA / MyBatis 都有大量现成案例。对毕业设计来说你不需要花大量时间搭建环境精力可以集中在业务逻辑上。小程序端选微信原生框架而不是 uni-app 或 Taro原因有三点。第一原生框架调微信 API 最直接少一层封装就少一层问题第二毕设项目页面量不大原生开发的代码量完全可以接受第三微信开发者工具的调试体验对新手极其友好网络请求、Storage 缓存、真机预览都一目了然。前后端分离的架构在答辩时可以这样解释前端负责视图渲染和交互反馈后端负责业务逻辑和数据持久化两者通过 JSON 格式的 HTTP 接口通信。这种模式也是当前企业开发的主流方式在简历上写“熟悉前后端分离开发模式”是站得住脚的。3.2 后端分层架构我采用经典的三层架构Controller 层接收前端请求 - Service 层处理业务逻辑 - Mapper 层进行数据库操作。实体类单独放 entity 包DTO 放 dto 包工具类放 utils 包统一返回结构放 common 包。项目启动类的写法SpringBootApplication MapperScan(com.cailaiwang.mapper) public class FinanceApplication { public static void main(String[] args) { SpringApplication.run(FinanceApplication.class, args); } }统一返回结果类我习惯用一个泛型封装public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg success; result.data data; return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.code 500; result.msg msg; return result; } }这样的好处是后端接口返回格式统一小程序端解析逻辑只需写一套。3.3 为什么用 JWT 而不是传统 Session小程序端和后端交互需要维持登录状态。传统方式是 Session Cookie但小程序端对 Cookie 的支持不友好而且 Session 存在服务器内存中服务端重启后所有用户都要重新登录。我最终选择了 JWTJSON Web Token。JWT 的机制可以这样理解用户登录后后端根据用户的 openid 生成一个 Token 字符串返回给小程序小程序把 Token 存在 Storage 里之后的每次请求都在请求头带上这个 Token。后端用一个拦截器解析 Token解析成功就能拿到用户身份。生成 Token 的代码public String generateToken(String openid) { return Jwts.builder() .setSubject(openid) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7L * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }Token 有效期我设为 7 天用户一周内重新打开小程序都不需要重新登录超过有效期则需要重新走微信授权流程。实际操作中拦截器要对OPTIONS请求直接放行否则前后端联调时 CORS 相关的问题会折腾浪费不少时间。4. 核心功能实现与关键代码解析4.1 微信登录流程的完整实现微信登录是整个项目最先要打通的功能因为后面的每一个接口都必须依赖用户身份。流程是这样的小程序端调用wx.login()获取一个临时 code把 code 发给后端后端拿着 code 去微信接口换取 openid 和 session_key拿到 openid 后查询用户表若不存在则自动注册一个新用户最后生成 JWT 返回给前端。后端的核心逻辑PostMapping(/login) public ResultString login(RequestBody LoginRequest request) { String code request.getCode(); // 调用微信接口获取 openid String url https://api.weixin.qq.com/sns/jscode2session?appid APP_ID secret APP_SECRET js_code code grant_typeauthorization_code; String result restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(result); String openid json.getString(openid); // 查询或创建用户 User user userMapper.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userMapper.insert(user); } // 生成 token 返回 String token jwtUtil.generateToken(openid); return Result.success(token); }注意这里的 APP_ID 和 APP_SECRET 不应该直接硬编码在代码里应该放到application.yml配置文件中通过Value注解注入。另外因为微信接口的 session_key 有时效性换取的 session_key 本身不用保存只有拿到 openid 就算完成目标。前端在app.js的onLaunch阶段就执行登录逻辑登录成功后将 token 存入wx.setStorageSync(token, token)后续请求从 Storage 读取。这样用户进入小程序的第一秒身份就已经就绪了。4.2 记账功能的后端实现记账是整个应用的核心高频操作后端接口设计为POST /api/bill/add。接收的参数包括收支类型、金额、分类 ID、备注、记账时间。这里有一个特别容易踩坑的地方金额类型。数据库里字段定义成DECIMAL(10,2)Java 实体类中对应使用BigDecimal绝对不要用double或float。浮点数在计算机内部是二进制表示的0.1 0.2 可能等于 0.30000000000000004账单金额一旦出现这种误差统计报表就全乱了。public ResultVoid addBill(RequestBody BillDTO billDTO, RequestHeader(Authorization) String token) { String openid jwtUtil.parseToken(token); Bill bill new Bill(); bill.setUserId(userMapper.findByOpenid(openid).getId()); bill.setType(billDTO.getType()); bill.setAmount(new BigDecimal(billDTO.getAmount())); bill.setCategoryId(billDTO.getCategoryId()); bill.setRemark(billDTO.getRemark()); bill.setBillTime(billDTO.getBillTime()); billMapper.insert(bill); return Result.success(null); }分类 ID 需要在前端通过接口查询。我在后端提供了一个GET /api/category/list接口返回全部分类。前端在记一笔页面加载时先拉取分类数据渲染成可点击的图标网格用户选中后再提交账单。实际使用时用户最频繁的操作路径是打开小程序 - 点“记一笔” - 选类型、选分类、输金额 - 保存。整个过程应该在 10 秒内完成。为了减少输入成本我在前端做了一个“最近使用的金额”快捷按钮点一下就填充上次输入的金额这个细节实测下来使用频率很高。4.3 统计模块与图表展示设计统计模块是项目里最有展示力的部分也是答辩时最能讲出深度的模块。我采用小程序端集成 ECharts 的方式实现图表渲染。选择 ECharts 是因为它功能全面社区活跃小程序的适配方案也很成熟。相比 Canvas 手工绘图ECharts 省去了大量底层绘图代码。核心统计接口有四个GET /api/statistics/type-total?month2025-04本月收入总金额和支出总金额GET /api/statistics/category-total?month2025-04type0本月各分类的支出占比GET /api/statistics/daily-trend?month2025-04每日支出趋势数据GET /api/statistics/month-compare?year2025全年每月收支对比分类占比的 SQL 是这个项目里最值得推敲的一条SELECT c.name AS categoryName, SUM(b.amount) AS totalAmount FROM bill b LEFT JOIN category c ON b.category_id c.id WHERE b.user_id #{userId} AND b.type #{type} AND DATE_FORMAT(b.bill_time, %Y-%m) #{month} GROUP BY c.id ORDER BY totalAmount DESC这里需要提醒的是如果用户在某个月对某个分类完全没有记账SUM(b.amount)的结果是 NULLJava 中映射为 null 会导致前端图表数据异常。解决方式是用IFNULL(SUM(b.amount), 0)让数据库层直接处理掉空值问题。前端 ECharts 的使用方式以饼图为例const chart echarts.init(canvas); chart.setOption({ tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 70%], data: categoryData }] });需要特别强调的是ECharts 的 echarts-for-weixin 组件要求 canvas 节点必须在页面 wxml 中提前定义好且 canvas 的宽高必须显式声明。如果图表不显示90% 是 canvas 宽高没设置或者初始化时节点还没渲染完成。用wx.nextTick包裹初始化函数可以解决这类问题。4.4 预算提醒机制预算模块的实现逻辑不算复杂但需要想清楚业务规则。我的实现是用户每月可设置一个总预算金额后端在记账接口调用完成后自动统计当前月总支出和预算对比如果超出则返回提示信息给前端由前端弹出 Toast 提醒用户。关键代码是这段BigDecimal monthExpense billMapper.sumByMonth(userId, month); BigDecimal budget budgetMapper.findByUserAndMonth(userId, month); if (budget ! null monthExpense.compareTo(budget) 0) { return Result.success(txt, 本月支出已超出预算); }这里的compareTo而不是直接运算是 BigDecimal 比较的标准做法。BigDecimal 的 equals 方法会同时比较数值和精度标度所以new BigDecimal(100.0).equals(new BigDecimal(100.00))是 false但compareTo则只比较数值大小。记账这类金额数据必须养成用compareTo的习惯。4.5 事务处理与数据一致性记账、更新统计、检查预算这几个操作必须放在同一个事务里。如果在记完账后统计查询失败用户会看到“记账失败”但数据库里其实已经插入了记录这种错误在答辩时被问到会非常尴尬。Spring Boot 中使用Transactional注解即可实现声明式事务Transactional(rollbackFor Exception.class) public ResultVoid addBillWithTransaction(BillDTO dto, String token) { addBill(dto, token); updateDailyStatistics(dto); checkBudgetAndNotify(dto); return Result.success(null); }默认情况下Spring 事务只对 RuntimeException 回滚如果方法抛出受检异常如 IOException则不会触发回滚。所以必须显式加上rollbackFor Exception.class来覆盖所有异常类型。这是很多教程不会重点强调但实际极其关键的细节。5. 小程序前端页面设计与交互实现5.1 页面架构与导航设计小程序端我规划了四个 Tab 页面首页账单流水、记账页核心操作入口、统计页图表展示、我的个人中心。TabBar 设计贴合记账工具的使用习惯——最快的路径永远是“打开即记账”所以中间鼓励为记账按钮时左右两侧分别是账单和统计。页面文件结构如下pages/ home/index // 最近账单流水按月份分组 record/index // 记一笔收入/支出切换 statistics/index // 分类占比 趋势图 mine/index // 个人信息、预算设置、关于 login/index // 登录页实际用不到自动处理首页以时间为维度展示最近 30 天的账单记录支持下拉刷新。每条记录左侧是分类图标中间是备注和分类名右侧是金额支出显示减号、收入显示加号并用颜色区分。这个页面的难点在于按日期分组渲染我采用 wxss 的 flex 布局配合wx:for循环实现根据日期字段动态生成分组头。5.2 请求封装与拦截处理小程序端的网络请求需要统一封装避免每个页面重复写wx.request。我用一个request.js文件作为唯一出口const BASE_URL https://your-domain.com/api; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }封装好后页面内调用就非常简洁const res await request(/bill/list, GET, { month: 2025-04 }); this.setData({ billList: res });5.3 交互细节与用户体验优化真正把界面做到好用的细节很多我挑几个在项目中实测最重要的说说。金额输入框必须是数字键盘。微信小程序中input typedigit可以让手机上弹出带小数点的数字键盘但要注意它和typenumber的区别number 键盘没有小数点用户输入 9.9 这种金额时会被拦下来。录入月份选择器使用微信自带的picker modedate fieldsmonth这样不用额外开发日历组件发布日期就能选择任意年月。但如果用户要补记过去某一天就需要fieldsday的日期选择器。我两个都做了默认按天选。删除账单时增加确认弹窗避免用户误触。同时提供“编辑”功能长按账单记录弹出底部操作菜单菜单项为“编辑”“删除”这个交互模式参考了主流记账应用。6. 开发环境配置与前后端部署6.1 本地开发环境准备整个项目的开发环境清单如下JDK 1.8我用的 1.8稳定为主、Maven 3.6、MySQL 8.0、微信开发者工具稳定版、Navicat数据库可视化管理工具。Spring Boot 项目的application.yml关键配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/cailaiwang?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379这里serverTimezoneAsia/Shanghai非常重要MySQL 8.0 默认时区是 UTC不加这个参数存进去的时间和取出来的时间会相差 8 小时。我在开发初期就踩过这个坑所有账单的日期全部错乱排查了大半天。6.2 后端打包部署后端打包时需要注意 Maven 配置。Spring Boot 的 Maven 插件默认生成的 jar 包可以直接用java -jar运行但需要排除测试代码避免打包失败mvn clean package -DskipTests部署到云服务器后启动命令建议用 nohup 方式nohup java -jar cailaiwang-0.0.1-SNAPSHOT.jar app.log 21 6.3 小程序的 HTTPS 与域名要求微信小程序有一个硬性要求正式环境中所有网络请求必须使用 HTTPS 协议并且域名必须在小程序后台完成 ICP 备案和业务域名配置。在开发阶段可以用“开发环境不校验合法域名”选项绕过但上线前必须配置合法域名。实际操作中我买了一台云服务器在 Nginx 中配置了 SSL 证书并将 HTTPS 请求反向代理到后端服务的 8080 端口。Nginx 配置的关键部分server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/ssl/domain.pem; ssl_certificate_key /etc/nginx/ssl/domain.key; location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }一个细节代理路径后面必须以/结尾否则 URL 前缀会被替换而不是追加。不熟悉 Nginx 的同学可以先用proxy_pass http://localhost:8080;不带路径这样配置最不容易出错。7. 常见问题与排查技巧实录7.1 微信登录失败类问题问题表现第一次进入小程序时接口返回五六百错误或者提示“code 无效”。排查思路首先小程序的 code 是一次性的前端调用wx.login()获取后必须立即发给后端不能反复使用其次后端请求微信接口需要设置较长的超时时间因为微信接口响应速度不稳定最后确认小程序的 AppID 和 AppSecret 没有写错这里最容易出错的地方是误用了测试号配置。7.2 数据库中文乱码问题表现备注、分类名称存入数据库后显示为问号。原因分析数据库、数据表、连接串三处字符集不统一。解决方式是需要三层统一为 utf8mb4。建表时可以显式指定CREATE TABLE bill ( ... ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;已经建好的表可以通过ALTER TABLE bill CONVERT TO CHARACTER SET utf8mb4修改。7.3 小程序端请求跨域问题小程序端不存在浏览器传统意义上的跨域限制但如果你用网页端联调接口就会遇到 CORS。解决方式是后端配置跨域过滤器Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }7.4 图表渲染不出来这个是最常见的坑。打开调试器看 console通常会输出chart is not defined或者canvas is undefined。解决方案是确保 echarts 的 canvas 节点已经渲染完成再初始化可以在onReady生命周期里执行而不是onLoad。另外检查 canvas 的宽高很多情况下是因为 CSS 没有给 canvas 设置宽高导致 0 尺寸无法渲染。7.5 常见问题速查表问题现象根本原因解决方案登录 code 无效code 被二次使用或过期每次登录重新调用 wx.login数据库时间差 8 小时JDBC 时区未指定连接串加 serverTimezoneAsia/Shanghai金额出现精度误差使用 double 存储金额改为 BigDecimal 类型统计查询数据为 nullSUM 聚合无结果使用 IFNULL(SUM(...), 0)小程序请求 404BASE_URL 配置错误检查域名路径和代理配置真机预览图表空白canvas 尺寸为 0为 canvas 显式设置宽高8. 答辩演示准备与项目复盘8.1 答辩演示的核心节奏毕业设计答辩时演示环节通常只有五分钟到十分钟。我的经验是“先业务、后技术”不要一上来就讲代码结构而是先用两分钟把整个系统跑起来亮个相——登录、记账、看图表、设置预算让老师对系统有直观印象然后深入到技术细节。被问到最多的问题是三类为什么选 Spring Boot、微信登录是怎么实现的、统计图表数据是如何计算的。回答这些问题不需要背诵用自己的话把实现流程说清楚即可比如“用户点登录后小程序拿到临时 code 发给后端后端用 code 去向微信服务器换 openid拿到 openid 后作为用户的唯一标识生成登录凭证”。这个表达既清晰又准确。8.2 核心亮点整理复盘这个项目最值得拿出来讲的三点是基于 JWT 的无状态用户认证、基于 ECharts 的数据可视化、基于 BigDecimal 的金额精度保障以及明确的统计聚合 SQL 设计。这些细节都是有真实业务背景的技术取舍不是简单的 CRUD而是有思考、有比较、有结论的实现方案。8.3 给我自己的经验总结做这个项目最大的收获其实是把“完整地做成一件事”变成了现实。从一个想法到一张张表、一个个接口、一页页界面再到最终部署上线每一步都会遇到问题但每一步都能找到解决问题的路径。纸上得来终觉浅绝知此事要躬行这话放在毕业设计上再合适不过。最后再分享一个小技巧代码注释不要只写“这段代码做了什么”要写“为什么这样做”。因为毕业设计提交时代码不仅要能运行还要能被看明白。注释里多写一句“这里用 compareTo 是因为 BigDecimal 的 equals 会校验精度”答辩时老师翻到这一页你自己也讲得出所以然。