☰
Spring Boot+微信小程序记账应用开发实战:从设计到部署全解析
2026/10/11 18:55:10 网站建设 项目流程

1. 选题背景与项目定位

“财来财往”这个名字,第一次听到的时候还觉得挺有意思——它不是那种一板一眼的“智能记账本”或者“未来财务管理”,而是带着一种生活气息。实际上,这就是一个基于 Spring Boot 和微信小程序搭建的轻量级记账与个人财务管理应用。作为计算机毕业设计选题,它的难度适中,方向明确,既能体现后端接口开发能力,又能展示小程序端交互设计水平,非常适合那些前后端都想兼顾一下、但又不希望技术栈过于庞杂的同学。

这个项目要解决什么问题?说白了就一个字:记账。日常收入支出太零散,月底总不知道钱花哪了。小程序生态成熟,微信登录零门槛,用户扫一扫就能用;后端用 Spring Boot,开发效率高、生态资料多、部署也简单,毕业设计答辩时讲解起来非常顺。适合的人群很明确:计算机相关专业、正在准备毕业设计或课程设计的学生,尤其是想独立完成全栈小项目、但前端不想碰 Vue/React 大工程的同学。

我在实际做完这个项目之后最大的感受是:它不只是一个记账工具,更像一个把所有核心技能串起来的综合练习。用户登录、数据建模、接口设计、图表统计、事务处理、异常拦截、部署上线,每一步都有踩坑点,每一步也都有成长空间。下面我按完整的开发路线展开,把整个项目的设计思路、关键实现、常见坑点全部拆开讲清楚。

2. 核心需求拆解与数据模型设计

2.1 用户需求与功能边界划分

做毕业设计最容易犯的错,是一上来就堆功能,结果数据库表建了二十张,最后能跑通的没几个。我的建议是先圈定一个“最小可用闭环”,把故事讲完整,再考虑扩展。

“财来财往”项目的核心闭环是:微信授权登录 -> 记一笔账 -> 分类统计 -> 生成趋势报表 -> 按月份回顾。整个流程走通,项目就已经完整了。我最终圈定的功能模块是这样的:

  • 用户模块:微信登录、个人信息维护、会话管理
  • 记账模块:收入/支出记录、分类管理、备注与时间编辑
  • 统计模块:按分类汇总、按月趋势、按收入/支出分项对比
  • 预算模块:月度预算设置、超支提醒(这个作为加分项实现)

没有做的东西也值得一提:多账户管理、周期性账单、多人共享账本、账单导入导出,这些不是不重要,而是毕设周期太短,做好核心比什么都做要强得多。答辩时老师问“能不能扩展”,你有清晰的设计留白,反而是加分项。

2.2 数据库设计:从需求到表结构

数据库设计是整个项目的基石,表建得不好,后面写 SQL 和做统计时会异常痛苦。我用的是 MySQL 8.0,核心表总共五张,设计如下:

用户表user:openid、昵称、头像 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 Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.msg = "success"; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 500; result.msg = msg; return result; } }

这样的好处是后端接口返回格式统一,小程序端解析逻辑只需写一套。

3.3 为什么用 JWT 而不是传统 Session

小程序端和后端交互,需要维持登录状态。传统方式是 Session + Cookie,但小程序端对 Cookie 的支持不友好,而且 Session 存在服务器内存中,服务端重启后所有用户都要重新登录。我最终选择了 JWT(JSON 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 Result<String> 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_type=authorization_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 Result<Void> 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?month=2025-04:本月收入总金额和支出总金额
  • GET /api/statistics/category-total?month=2025-04&type=0:本月各分类的支出占比
  • GET /api/statistics/daily-trend?month=2025-04:每日支出趋势数据
  • GET /api/statistics/month-compare?year=2025:全年每月收支对比

分类占比的 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)的结果是 NULL,Java 中映射为 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 Result<Void> 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 type="digit">可以让手机上弹出带小数点的数字键盘,但要注意它和type="number"的区别:number 键盘没有小数点,用户输入 9.9 这种金额时会被拦下来。

录入月份选择器使用微信自带的<picker mode="date" fields="month">,这样不用额外开发日历组件,发布日期就能选择任意年月。但如果用户要补记过去某一天,就需要fields="day"的日期选择器。我两个都做了,默认按天选。

删除账单时增加确认弹窗,避免用户误触。同时提供“编辑”功能,长按账单记录弹出底部操作菜单,菜单项为“编辑”“删除”,这个交互模式参考了主流记账应用。

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?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379

这里serverTimezone=Asia/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 2>&1 &

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 ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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 时区未指定连接串加 serverTimezone=Asia/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 会校验精度”,答辩时老师翻到这一页,你自己也讲得出所以然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询