1. 项目概述
这个基于SpringBoot+Vue的个人记账系统是我在业余时间开发的一个全栈项目,主要解决个人日常收支管理的痛点。作为一个长期被记账问题困扰的开发者,我发现市面上大部分记账工具要么功能过于复杂,要么无法满足自定义需求。于是决定自己动手开发一个轻量级但功能完备的系统。
系统采用前后端分离架构,后端使用SpringBoot提供RESTful API,前端用Vue构建响应式界面,数据库选用MySQL存储数据。这种技术组合既能保证系统的稳定性和扩展性,又能提供良好的用户体验。经过两个月的开发和测试,目前系统已经实现了基本的记账、分类统计、报表生成等功能。
提示:个人记账系统的核心价值不在于功能有多复杂,而在于能否长期坚持使用。因此我在设计时特别注重操作的便捷性和数据的可视化呈现。
2. 技术架构解析
2.1 后端技术选型
选择SpringBoot作为后端框架主要基于以下几个考虑:
-
快速开发:SpringBoot的自动配置和起步依赖大大减少了样板代码。比如集成MyBatis-Plus只需要添加一个依赖:
xml复制<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> -
内嵌容器:无需额外部署Tomcat,打包成可执行JAR后直接运行,非常适合个人项目部署。
-
生态丰富:Spring生态有完善的解决方案,比如用Spring Security做认证授权,Spring Cache做数据缓存。
-
易于测试:提供了完善的测试支持,可以方便地编写单元测试和集成测试。
实际开发中,我采用了以下关键配置:
java复制@SpringBootApplication
@MapperScan("com.ledger.mapper")
public class LedgerApplication {
public static void main(String[] args) {
SpringApplication.run(LedgerApplication.class, args);
}
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
2.2 前端技术选型
Vue.js作为前端框架的优势在于:
-
渐进式框架:可以根据需求逐步引入路由、状态管理等功能,不会一开始就增加复杂度。
-
组件化开发:将记账表单、统计图表等拆分为独立组件,提高代码复用率。例如账单列表组件:
vue复制<template> <div class="bill-list"> <el-table :data="bills" style="width: 100%"> <el-table-column prop="date" label="日期" width="180"></el-table-column> <el-table-column prop="category" label="分类" width="180"></el-table-column> <el-table-column prop="amount" label="金额"></el-table-column> </el-table> </div> </template> -
丰富的UI库:使用Element UI快速构建美观的界面,节省样式开发时间。
-
开发体验好:Vue Devtools调试方便,热重载提升开发效率。
2.3 数据库设计
MySQL数据库设计遵循第三范式,主要表结构如下:
账单表(bill)
sql复制CREATE TABLE `bill` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`type` tinyint(1) NOT NULL COMMENT '1收入 2支出',
`category_id` bigint(20) NOT NULL COMMENT '分类ID',
`amount` decimal(10,2) NOT NULL COMMENT '金额',
`date` date NOT NULL COMMENT '交易日期',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`,`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
分类表(category)
sql复制CREATE TABLE `category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`name` varchar(50) NOT NULL COMMENT '分类名称',
`type` tinyint(1) NOT NULL COMMENT '1收入 2支出',
`icon` varchar(100) DEFAULT NULL COMMENT '图标',
PRIMARY KEY (`id`),
KEY `idx_user_type` (`user_id`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:在账单表中添加了(user_id, date)联合索引,因为这是最常用的查询条件组合。
3. 核心功能实现
3.1 账单管理模块
账单管理是系统的核心功能,主要涉及以下技术要点:
-
RESTful API设计
java复制@RestController @RequestMapping("/api/bill") public class BillController { @Autowired private BillService billService; @PostMapping public Result addBill(@RequestBody BillDTO billDTO) { return billService.addBill(billDTO); } @GetMapping("/{id}") public Result getBill(@PathVariable Long id) { return Result.success(billService.getById(id)); } @GetMapping public Result listBills(@RequestParam Map<String, Object> params) { PageUtils page = billService.queryPage(params); return Result.success(page); } } -
分页查询实现
使用MyBatis-Plus的分页插件,配合前端Element UI的Pagination组件:java复制@Service public class BillServiceImpl extends ServiceImpl<BillMapper, Bill> implements BillService { @Override public PageUtils queryPage(Map<String, Object> params) { QueryWrapper<Bill> wrapper = new QueryWrapper<>(); // 构建查询条件 String date = (String) params.get("date"); if (StringUtils.isNotBlank(date)) { wrapper.eq("date", date); } // 其他条件... Page<Bill> page = this.page( new Query<Bill>().getPage(params), wrapper ); return new PageUtils(page); } } -
数据校验
使用Spring Validation进行参数校验:java复制@Data public class BillDTO { @NotNull(message = "类型不能为空") private Integer type; @NotNull(message = "分类不能为空") private Long categoryId; @DecimalMin(value = "0.01", message = "金额必须大于0") private BigDecimal amount; @NotNull(message = "日期不能为空") private LocalDate date; }
3.2 统计报表模块
统计功能使用ECharts实现数据可视化,后端提供聚合查询接口:
-
月度收支统计
java复制@GetMapping("/stats/monthly") public Result monthlyStats( @RequestParam @DateTimeFormat(pattern = "yyyy-MM") LocalDate month) { LocalDate start = month.withDayOfMonth(1); LocalDate end = month.withDayOfMonth(month.lengthOfMonth()); List<Map<String, Object>> incomeStats = billMapper.selectCategoryStats( getUserId(), 1, start, end); List<Map<String, Object>> expenseStats = billMapper.selectCategoryStats( getUserId(), 2, start, end); Map<String, Object> result = new HashMap<>(); result.put("income", incomeStats); result.put("expense", expenseStats); return Result.success(result); } -
SQL聚合查询
xml复制<select id="selectCategoryStats" resultType="map"> SELECT c.name as category, SUM(b.amount) as amount, COUNT(b.id) as count FROM bill b JOIN category c ON b.category_id = c.id WHERE b.user_id = #{userId} AND b.type = #{type} AND b.date BETWEEN #{start} AND #{end} GROUP BY b.category_id ORDER BY amount DESC </select> -
前端图表渲染
vue复制<template> <div class="chart-container"> <div ref="chart" style="width: 100%; height: 400px;"></div> </div> </template> <script> import * as echarts from 'echarts'; export default { mounted() { this.initChart(); }, methods: { async initChart() { const chart = echarts.init(this.$refs.chart); const res = await this.$http.get('/api/bill/stats/monthly', { params: { month: '2023-06' } }); const option = { tooltip: { trigger: 'item' }, series: [{ type: 'pie', data: res.data.income.map(item => ({ value: item.amount, name: item.category })) }] }; chart.setOption(option); } } } </script>
4. 开发经验与优化
4.1 前后端联调技巧
-
接口文档管理
使用Swagger生成API文档,添加依赖:xml复制<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>配置类:
java复制@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.ledger.controller")) .paths(PathSelectors.any()) .build(); } } -
跨域问题解决
后端配置全局CORS:java复制@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*"); } } -
接口调试工具
推荐使用Postman或Insomnia进行接口测试,可以保存请求历史,方便复现问题。
4.2 性能优化实践
-
缓存策略
对不常变动的分类数据使用Redis缓存:java复制@Cacheable(value = "category", key = "#userId + ':' + #type") public List<Category> listCategories(Long userId, Integer type) { return categoryMapper.selectList( new QueryWrapper<Category>() .eq("user_id", userId) .eq("type", type) ); } -
批量操作优化
使用MyBatis-Plus的批量插入功能:java复制@Transactional public void importBills(List<Bill> bills) { saveBatch(bills, 1000); // 每1000条提交一次 } -
前端懒加载
对大列表使用分页加载和虚拟滚动:vue复制<el-table :data="tableData" style="width: 100%" height="calc(100vh - 200px)" v-loading="loading"> <!-- 列定义 --> </el-table>
4.3 安全防护措施
-
SQL注入防护
坚持使用MyBatis的参数化查询,避免拼接SQL:java复制// 错误做法 wrapper.apply("date_format(date,'%Y-%m') = '" + month + "'"); // 正确做法 wrapper.apply("date_format(date,'%Y-%m') = {0}", month); -
XSS防护
前端使用vue-sanitize过滤用户输入:javascript复制import VueSanitize from "vue-sanitize"; Vue.use(VueSanitize); // 使用 this.$sanitize(userInput); -
密码安全
使用BCrypt加密存储密码:java复制@Service public class UserServiceImpl implements UserService { @Override public void register(User user) { user.setPassword( new BCryptPasswordEncoder().encode(user.getPassword()) ); save(user); } }
5. 部署与运维
5.1 本地开发环境
-
后端启动
配置application-dev.properties:properties复制spring.datasource.url=jdbc:mysql://localhost:3306/ledger?useSSL=false spring.datasource.username=root spring.datasource.password=123456 spring.jpa.hibernate.ddl-auto=update -
前端启动
创建.env.development文件:ini复制VUE_APP_API_BASE_URL=http://localhost:8080/api启动命令:
bash复制
npm run serve
5.2 生产环境部署
-
后端打包
bash复制
mvn clean package -DskipTests生成的ledger-0.0.1-SNAPSHOT.jar可直接运行:
bash复制
java -jar ledger-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod -
前端打包
bash复制
npm run build生成的dist目录可以部署到Nginx:
nginx复制server { listen 80; server_name ledger.example.com; location / { root /path/to/dist; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://localhost:8080; } } -
数据库备份
设置定时任务备份MySQL:bash复制mysqldump -uroot -p123456 ledger > /backup/ledger_$(date +%F).sql
5.3 监控与日志
-
SpringBoot Actuator
添加依赖启用健康检查:xml复制<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> -
日志配置
使用Logback按天归档日志:xml复制<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/ledger.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/ledger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>
6. 常见问题与解决方案
6.1 开发阶段问题
-
前端跨域问题
- 现象:前端请求接口时出现CORS错误
- 解决方案:
- 确保后端已配置CORS
- 开发环境可配置代理:
javascript复制module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }
-
日期时间处理
- 现象:前后端日期格式不一致
- 解决方案:
- 后端统一使用LocalDate/LocalDateTime
- 添加全局日期格式化:
java复制@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper mapper = converter.getObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); converters.add(0, converter); } }
6.2 部署阶段问题
-
静态资源404
- 现象:部署后前端页面空白或资源加载失败
- 解决方案:
- 检查Nginx配置中的root路径是否正确
- 确保Vue路由使用history模式时配置了try_files
- 前端打包时设置正确的publicPath:
javascript复制module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/ledger/' : '/' }
-
数据库连接失败
- 现象:应用启动时报数据库连接异常
- 解决方案:
- 检查数据库服务是否启动
- 确认application-prod.properties中的连接信息正确
- 确保数据库用户有远程连接权限(如果是远程数据库)
6.3 性能优化问题
-
列表加载慢
- 现象:账单列表数据量大时加载缓慢
- 解决方案:
- 后端添加分页查询,避免一次性返回大量数据
- 前端使用虚拟滚动或分页加载
- 对常用查询条件添加数据库索引
-
图表渲染卡顿
- 现象:ECharts图表数据量大时渲染卡顿
- 解决方案:
- 限制显示的数据点数,后端做数据聚合
- 使用Web Worker进行数据处理
- 对图表使用懒加载,等元素进入视口再渲染
7. 项目扩展方向
7.1 功能扩展
-
多账本支持
- 当前系统只支持个人单一账本
- 可以扩展为家庭账本或团队账本,增加账本成员管理功能
-
预算管理
- 添加月度预算设置功能
- 实现预算执行情况预警
-
数据导入导出
- 支持Excel导入历史账单
- 生成PDF格式的财务报表导出
7.2 技术扩展
-
微服务化
- 将用户服务、账单服务、统计服务拆分为独立微服务
- 使用Spring Cloud Alibaba实现服务治理
-
移动端适配
- 开发React Native或Flutter移动应用
- 或使用PWA技术实现渐进式Web应用
-
数据分析
- 集成Python数据分析能力
- 使用Apache Spark处理大规模历史数据
7.3 部署扩展
-
容器化部署
- 使用Docker打包应用
- Kubernetes集群管理
-
CI/CD流水线
- GitHub Actions自动化构建部署
- SonarQube代码质量检查
-
多环境配置
- 完善的dev/test/staging/prod环境隔离
- 配置中心统一管理
这个项目从零开始开发的过程中,我最大的体会是:一个看似简单的个人记账系统,实际上涉及了完整的前后端技术栈和软件工程实践。通过这个项目,我不仅巩固了SpringBoot和Vue的技术能力,更重要的是学会了如何将一个想法逐步实现为可用的产品。