作为一个长期跟踪家庭财务管理需求的开发者,我发现市面上大多数记账工具要么功能过于简单,要么操作复杂难以坚持使用。这次基于SpringBoot和SSM框架开发的家庭收支管理系统,正是为了解决这个痛点而设计的。
这个系统的核心价值在于:
技术选型上,后端采用SpringBoot+SSM的经典组合:
前端部分则选择了:
提示:技术栈选择时特别考虑了学习成本和社区支持度,这些都是中小型项目快速上线的关键因素
系统采用典型的三层架构:
code复制src/main/java
├── config # Spring配置类
├── controller # MVC控制器
├── service # 业务逻辑
│ ├── impl # 服务实现
├── dao # MyBatis Mapper接口
├── entity # 实体类
└── util # 工具类
核心表结构设计如下:
sql复制CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `transaction` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`amount` decimal(10,2) NOT NULL,
`type` enum('income','expense') NOT NULL,
`category_id` int NOT NULL,
`remark` varchar(255) DEFAULT NULL,
`transaction_date` date NOT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `category` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`icon` varchar(50) DEFAULT NULL,
`type` enum('income','expense') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:金额字段使用DECIMAL(10,2)确保精度,避免浮点数计算问题
采用Spring Security实现安全的认证授权:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.and()
.logout()
.logoutSuccessUrl("/login");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
使用MyBatis的动态SQL实现灵活查询:
java复制@Mapper
public interface TransactionMapper {
@SelectProvider(type = TransactionSqlProvider.class, method = "findByCriteria")
List<Transaction> findByCriteria(@Param("userId") Integer userId,
@Param("type") String type,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
@Param("categoryId") Integer categoryId);
// 其他CRUD方法...
}
public class TransactionSqlProvider {
public String findByCriteria(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("transaction");
WHERE("user_id = #{userId}");
if (params.get("type") != null) {
WHERE("type = #{type}");
}
if (params.get("startDate") != null) {
WHERE("transaction_date >= #{startDate}");
}
if (params.get("endDate") != null) {
WHERE("transaction_date <= #{endDate}");
}
if (params.get("categoryId") != null) {
WHERE("category_id = #{categoryId}");
}
ORDER_BY("transaction_date DESC");
}}.toString();
}
}
使用ECharts实现消费分类饼图:
javascript复制function initCategoryPieChart(data) {
const chart = echarts.init(document.getElementById('category-chart'));
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: data.categories
},
series: [
{
name: '消费比例',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.series
}
]
};
chart.setOption(option);
}
对高频访问的分类数据使用Redis缓存:
java复制@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_KEY = "user:categories:";
@Override
@Cacheable(value = "categories", key = "#userId")
public List<Category> findByUserId(Integer userId) {
return categoryMapper.findByUserId(userId);
}
@Override
@CacheEvict(value = "categories", key = "#userId")
public void clearCache(Integer userId) {
// 缓存由注解自动清除
}
}
使用Spring声明式事务确保数据一致性:
java复制@Service
@Transactional
public class TransactionServiceImpl implements TransactionService {
@Autowired
private TransactionMapper transactionMapper;
@Autowired
private AccountService accountService;
@Override
public void addTransaction(Transaction transaction) {
// 记录交易
transactionMapper.insert(transaction);
// 更新账户余额
if (transaction.getType() == TransactionType.INCOME) {
accountService.increaseBalance(transaction.getUserId(), transaction.getAmount());
} else {
accountService.decreaseBalance(transaction.getUserId(), transaction.getAmount());
}
}
}
使用Spring Profile管理不同环境配置:
yaml复制# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/finance_dev
username: devuser
password: devpass
redis:
host: localhost
port: 6379
yaml复制# application-prod.yml
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db:3306/finance_prod
username: ${DB_USER}
password: ${DB_PASS}
redis:
host: redis-prod
port: 6379
创建Dockerfile实现容器化部署:
dockerfile复制FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/finance-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
使用docker-compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: finance_prod
MYSQL_USER: appuser
MYSQL_PASSWORD: dbpass
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
db_data:
MyBatis枚举处理:
java复制@MappedTypes(TransactionType.class)
public class TransactionTypeHandler extends BaseTypeHandler<TransactionType> {
// 实现类型转换方法
}
日期时间处理:
yaml复制spring:
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
分页查询优化:
java复制@GetMapping("/transactions")
public PageInfo<Transaction> listTransactions(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
PageHelper.startPage(page, size);
List<Transaction> list = transactionService.findByUserId(getCurrentUserId());
return new PageInfo<>(list);
}
Excel导出性能:
java复制@GetMapping("/export")
public void exportExcel(HttpServletResponse response) {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=transactions.xlsx");
EasyExcel.write(response.getOutputStream(), Transaction.class)
.sheet("交易记录")
.doWrite(transactionService.findAllForExport(getCurrentUserId()));
}
前端表单验证:
javascript复制$("#transaction-form").validate({
rules: {
amount: {
required: true,
number: true,
min: 0.01
},
transactionDate: {
required: true,
date: true
}
},
messages: {
amount: "请输入有效的金额",
transactionDate: "请选择有效的日期"
}
});
在开发过程中,最大的教训是要尽早建立完整的测试体系。我建议采用以下测试策略:
重要提示:数据库迁移考虑使用Flyway或Liquibase,避免手动执行SQL脚本导致的环境不一致问题