作为一个长期关注个人财务管理领域的开发者,我发现很多朋友在记账和理财过程中面临诸多痛点:手工记账容易遗漏、Excel表格难以多端同步、市面上的理财App功能繁杂却不够个性化。这正是我决定开发这款基于SpringBoot+Vue的个人理财系统的初衷。
这个系统采用主流的前后端分离架构,后端使用SpringBoot提供RESTful API接口,前端采用Vue.js框架构建用户界面,数据库选用MySQL存储数据。系统核心功能包括:
特别适合以下几类用户:
选择SpringBoot作为后端框架主要基于以下考虑:
数据库操作层采用MyBatis-Plus而非原生MyBatis,因为:
密码安全处理方案:
java复制// 使用BCryptPasswordEncoder进行密码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 用户注册时加密处理
user.setPassword(passwordEncoder.encode(rawPassword));
Vue.js作为前端框架的优势:
UI组件库选用Element UI的原因:
前端工程化配置要点:
javascript复制// vue.config.js 关键配置
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
css: {
loaderOptions: {
sass: {
prependData: `@import "@/styles/variables.scss";`
}
}
}
}
整体采用前后端分离架构,通信通过RESTful API实现:
code复制客户端浏览器
↑↓ HTTP/HTTPS
前端服务(Vue.js)
↑↓ AJAX/JSON
后端服务(SpringBoot)
↑↓ JDBC
MySQL数据库
关键接口设计原则:
json复制{
"code": 200,
"message": "success",
"data": {...}
}
原始设计存在改进空间,我的实际实现增加了:
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录用户名',
`password_hash` varchar(100) NOT NULL COMMENT '加密密码',
`email` varchar(50) NOT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint DEFAULT '1' COMMENT '状态(0-禁用,1-启用)',
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
考虑实际业务需求,我扩展了以下字段:
sql复制CREATE TABLE `transaction` (
`transaction_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`account_id` bigint DEFAULT NULL COMMENT '关联账户ID',
`amount` decimal(12,2) NOT NULL COMMENT '金额(正数为收入,负数为支出)',
`category_id` int NOT NULL COMMENT '分类ID',
`transaction_time` datetime NOT NULL COMMENT '交易时间',
`payment_method` varchar(20) DEFAULT NULL COMMENT '支付方式',
`status` tinyint DEFAULT '1' COMMENT '状态(0-取消,1-完成,2-计划)',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`transaction_id`),
KEY `idx_user_time` (`user_id`,`transaction_time`),
KEY `idx_user_category` (`user_id`,`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支记录表';
索引优化策略:
事务处理要点:
java复制@Transactional(rollbackFor = Exception.class)
public void addTransaction(TransactionDTO dto) {
// 1. 保存交易记录
transactionMapper.insert(dto);
// 2. 更新账户余额
accountService.updateBalance(dto.getAccountId(), dto.getAmount());
// 3. 更新预算统计
budgetService.updateBudgetStat(dto.getUserId(), dto.getCategoryId(), dto.getAmount());
}
采用JWT实现无状态认证,解决Session共享问题:
java复制public class JwtTokenUtil {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400L; // 24小时
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
// 验证逻辑...
}
前端axios拦截器配置:
javascript复制// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
axios.interceptors.response.use(response => {
if (response.data.code === 401) {
router.push('/login')
}
return response
}, error => {
return Promise.reject(error)
})
核心业务逻辑实现要点:
java复制public void validateAmount(BigDecimal amount) {
if (amount == null) {
throw new BusinessException("金额不能为空");
}
if (amount.compareTo(new BigDecimal("1000000000")) > 0) {
throw new BusinessException("单笔金额不能超过10亿");
}
if (amount.scale() > 2) {
throw new BusinessException("金额小数位不能超过2位");
}
}
java复制public List<CategoryStatVO> getCategoryStats(Long userId, LocalDate start, LocalDate end) {
return transactionMapper.selectStatsByCategory(userId,
start.atStartOfDay(),
end.plusDays(1).atStartOfDay());
}
对应的MyBatis动态SQL:
xml复制<select id="selectStatsByCategory" resultType="CategoryStatVO">
SELECT
c.category_id,
c.name,
c.icon,
SUM(t.amount) AS total,
COUNT(*) AS count
FROM transaction t
JOIN category c ON t.category_id = c.category_id
WHERE t.user_id = #{userId}
AND t.transaction_time BETWEEN #{start} AND #{end}
AND t.status = 1
GROUP BY c.category_id, c.name, c.icon
ORDER BY total DESC
</select>
预算计算的核心算法:
java复制public BudgetVO calculateBudget(Long userId, String month) {
// 1. 获取当月预算设置
Budget budget = budgetMapper.selectByUserAndMonth(userId, month);
if (budget == null) {
return new BudgetVO(month, BigDecimal.ZERO, BigDecimal.ZERO);
}
// 2. 计算实际支出
LocalDate startDate = LocalDate.parse(month + "-01");
LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth());
BigDecimal actualSpent = transactionMapper.selectTotalExpense(
userId,
startDate.atStartOfDay(),
endDate.plusDays(1).atStartOfDay());
// 3. 计算剩余额度
BigDecimal remaining = budget.getAmount().add(actualSpent); // 支出为负值
return new BudgetVO(month, budget.getAmount(), actualSpent, remaining);
}
预算超支预警实现:
java复制@Scheduled(cron = "0 0 18 * * ?") // 每天18点执行
public void checkBudgetOverrun() {
String currentMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
List<Long> userIds = userMapper.selectActiveUserIds();
for (Long userId : userIds) {
BudgetVO vo = budgetService.calculateBudget(userId, currentMonth);
if (vo.getRemaining().compareTo(BigDecimal.ZERO) < 0) {
String message = String.format("您的%s月预算已超支%.2f元",
currentMonth, vo.getRemaining().abs());
notificationService.sendBudgetWarning(userId, message);
}
}
}
前端封装ECharts组件:
vue复制<template>
<div ref="chart" style="width: 100%; height: 400px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
props: {
option: {
type: Object,
required: true
}
},
mounted() {
this.initChart();
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart);
this.chart.setOption(this.option);
// 响应窗口大小变化
window.addEventListener('resize', this.resizeHandler);
},
resizeHandler() {
this.chart && this.chart.resize();
}
},
watch: {
option: {
deep: true,
handler(newVal) {
this.chart && this.chart.setOption(newVal);
}
}
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeHandler);
this.chart && this.chart.dispose();
}
};
</script>
消费趋势图配置示例:
javascript复制{
title: {
text: '月度消费趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {
type: 'value',
name: '金额(元)'
},
series: [{
data: [1200, 2000, 1500, 800, 1200, 1800],
type: 'line',
smooth: true,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(58, 77, 233, 0.8)' },
{ offset: 1, color: 'rgba(58, 77, 233, 0.1)' }
])
}
}]
}
后端统计服务示例:
java复制public AnalysisResultVO analyzeFinancialData(Long userId, AnalysisQuery query) {
AnalysisResultVO result = new AnalysisResultVO();
// 1. 消费结构分析
result.setCategoryStats(transactionMapper.getCategoryStats(
userId,
query.getStartDate().atStartOfDay(),
query.getEndDate().plusDays(1).atStartOfDay()));
// 2. 消费趋势分析
result.setTrendStats(transactionMapper.getTrendStats(
userId,
query.getStartDate().atStartOfDay(),
query.getEndDate().plusDays(1).atStartOfDay(),
query.getGroupBy()));
// 3. 收支对比分析
result.setBalanceStats(transactionMapper.getBalanceStats(
userId,
query.getStartDate().atStartOfDay(),
query.getEndDate().plusDays(1).atStartOfDay()));
// 4. 预算执行情况
if (query.isIncludeBudget()) {
result.setBudgetStats(budgetMapper.getBudgetStats(
userId,
query.getStartDate(),
query.getEndDate()));
}
return result;
}
推荐使用Docker容器化部署:
dockerfile复制# Dockerfile 示例
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
生产环境启动命令:
bash复制docker run -d \
-p 8080:8080 \
-e "SPRING_PROFILES_ACTIVE=prod" \
-e "DATASOURCE_URL=jdbc:mysql://mysql-host:3306/finance?useSSL=false" \
-e "DATASOURCE_USERNAME=root" \
-e "DATASOURCE_PASSWORD=yourpassword" \
--name finance-backend \
finance-backend:latest
Nginx配置示例:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
SpringBoot Actuator集成:
yaml复制# application.yml 配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
prometheus:
enabled: true
配合Prometheus + Grafana监控:

java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
java复制@Cacheable(value = "user", key = "#userId")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
@CacheEvict(value = "user", key = "#user.userId")
public void updateUser(User user) {
userMapper.updateById(user);
}
javascript复制// 从cookie中获取CSRF Token
const csrfToken = document.cookie.replace(
/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/,
'$1'
);
axios.defaults.headers.common['X-XSRF-TOKEN'] = csrfToken;
在实际开发过程中,我发现有几个关键点需要特别注意:
对于想要基于此项目进行二次开发的同学,我建议先从以下方面入手:
这个项目我已经在实际生产环境运行了两年多,期间根据用户反馈进行了多次迭代。最大的体会是:财务类系统对数据准确性和一致性要求极高,任何一个小数点错误都可能造成严重后果。因此在开发过程中要特别注意: