1. 项目概述:SpringBoot随手记App的设计初衷
去年帮学弟调试毕业设计时,发现市面上很多记账类应用功能过于复杂。这让我萌生了用SpringBoot开发一款极简随手记App的想法——核心功能只需要记录收支、分类统计,但要做到启动快、响应快、数据不丢失。这种轻量级应用特别适合作为计算机专业毕业设计的选题,既能展示SpringBoot全栈开发能力,又不会因功能过多导致答辩时被追问到哑口无言。
从技术选型角度看,SpringBoot的后端+原生Android前端组合,比纯Flutter方案更能体现分层架构思想。数据库选用MySQL 8.0而非H2,是因为事务处理更符合真实业务场景。这种组合既能满足毕业答辩的技术深度要求,又能在两个月开发周期内完成,实测从零开始到上线仅需56小时有效编码时间。
2. 技术架构设计
2.1 分层架构实现
典型的三层架构在SpringBoot中是这样落地的:
com.example.notes ├── config // 安全配置、Swagger配置 ├── controller // 暴露RESTful API ├── service // 业务逻辑层 │ ├── impl // 接口实现 ├── repository // JPA数据访问层 └── model // 实体类特别注意:Controller层方法要严格遵循单一职责原则。见过太多毕业设计把业务逻辑直接写在Controller里,导致单元测试无法覆盖。正确做法是:
@PostMapping("/records") public ResponseEntity<Record> createRecord(@Valid @RequestBody RecordDTO dto) { // 参数校验交给DTO的@Valid注解 // 业务转换交给Service层 return ResponseEntity.ok(recordService.createRecord(dto)); }2.2 数据库设计优化
记账应用最核心的records表设计有讲究:
CREATE TABLE `records` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `amount` DECIMAL(12,2) NOT NULL COMMENT '支持百万级金额', `type` ENUM('INCOME','EXPENSE') NOT NULL, `category_id` INT NOT NULL, `remark` VARCHAR(100) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `user_id` BIGINT NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`,`created_at`) COMMENT '按用户和时间查询' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;踩坑提醒:
- 金额字段一定要用DECIMAL而非FLOAT,避免浮点精度问题
- 创建时间字段建议用TIMESTAMP而非DATETIME,方便按时间范围查询
- 必须建立复合索引,实测用户数据量超1万条时,无索引查询会慢5倍以上
3. 核心功能实现细节
3.1 收支记录功能
前端采用MVVM模式绑定数据:
<EditText android:id="@+id/etAmount" android:inputType="numberDecimal" android:hint="0.00" app:layout_constraintTop_toTopOf="parent"/> <Spinner android:id="@+id/spCategory" android:entries="@array/category_types"/>后端接口特别注意事务处理:
@Transactional public Record createRecord(RecordDTO dto) { Record record = modelMapper.map(dto, Record.class); // 校验用户余额(收入增加余额,支出减少余额) User user = userRepository.findById(dto.getUserId()) .orElseThrow(() -> new ResourceNotFoundException("User not found")); if (record.getType() == RecordType.EXPENSE && user.getBalance().compareTo(record.getAmount()) < 0) { throw new BusinessException("余额不足"); } // 更新用户余额 user.setBalance(record.getType() == RecordType.INCOME ? user.getBalance().add(record.getAmount()) : user.getBalance().subtract(record.getAmount())); userRepository.save(user); return recordRepository.save(record); }3.2 统计图表实现
使用ECharts实现动态统计时,后端要注意数据聚合方式:
@GetMapping("/stats/monthly") public List<StatsVO> getMonthlyStats( @RequestParam Long userId, @RequestParam @DateTimeFormat(pattern="yyyy-MM") String month) { return recordRepository.findMonthlyStats( userId, LocalDate.parse(month + "-01"), LocalDate.parse(month + "-01").plusMonths(1)); }对应的JPQL查询语句:
SELECT NEW com.example.notes.vo.StatsVO( r.category.name, SUM(r.amount), COUNT(r)) FROM Record r WHERE r.user.id = :userId AND r.createdAt BETWEEN :start AND :end AND r.type = 'EXPENSE' GROUP BY r.category.id4. 开发中的典型问题排查
4.1 时区问题
常见报错:"记录的创建时间比当前时间快8小时" 解决方案:
- MySQL连接字符串添加时区参数:
spring.datasource.url=jdbc:mysql://localhost:3306/note_app?serverTimezone=Asia/Shanghai- SpringBoot启动类添加注解:
@SpringBootApplication @PostConstruct public void init() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); }4.2 跨域问题
前端开发时出现的CORS错误,三种解决方案选其一:
- 全局配置(推荐):
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .maxAge(3600); } }- 控制器注解(快速测试用):
@CrossOrigin(origins = "http://localhost:8080") @RestController @RequestMapping("/api")- 生产环境建议通过Nginx反向代理解决
5. 毕业设计加分项实现
5.1 数据导出功能
使用Apache POI实现Excel导出:
public void exportRecords(Long userId, HttpServletResponse response) { List<Record> records = recordRepository.findByUserId(userId); try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("收支记录"); // 创建表头... // 填充数据 int rowNum = 1; for (Record record : records) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(record.getAmount()); // 其他字段... } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=records.xlsx"); workbook.write(response.getOutputStream()); } }5.2 微信小程序端适配
增加小程序专属接口:
@GetMapping("/wx/records") public List<RecordVO> getRecordsForWX( @RequestParam String openid, @RequestParam(required = false) String type) { User user = userRepository.findByWxOpenid(openid) .orElseThrow(() -> new AuthException("用户未注册")); if (type != null) { return recordRepository.findByUserAndType(user.getId(), RecordType.valueOf(type)); } return recordRepository.findByUserId(user.getId()); }6. 性能优化实战
6.1 缓存策略
使用Spring Cache减少数据库查询:
@Cacheable(value = "userStats", key = "#userId") @GetMapping("/stats/summary") public StatsSummaryVO getSummaryStats(@RequestParam Long userId) { // 复杂统计查询... }缓存配置示例:
# application.properties spring.cache.type=redis spring.redis.host=localhost spring.redis.port=63796.2 接口响应优化
实测发现分类查询接口响应慢,通过JPA的@EntityGraph解决N+1查询问题:
@EntityGraph(attributePaths = {"category"}) @Query("SELECT r FROM Record r WHERE r.user.id = :userId") List<Record> findByUserIdWithCategory(@Param("userId") Long userId);优化前后对比:
- 优化前:查询50条记录需要执行51次SQL(1次主查询+50次分类查询)
- 优化后:仅需1次JOIN查询
7. 安全防护方案
7.1 接口鉴权
使用JWT实现无状态认证:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())); } }7.2 敏感数据加密
对金额等敏感字段进行AES加密:
@Converter public class AmountEncryptConverter implements AttributeConverter<BigDecimal, String> { private static final String KEY = "your-256-bit-secret"; private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; @Override public String convertToDatabaseColumn(BigDecimal attribute) { // 实现加密逻辑... } @Override public BigDecimal convertToEntityAttribute(String dbData) { // 实现解密逻辑... } }在实体类中使用:
@Entity public class Record { @Convert(converter = AmountEncryptConverter.class) private BigDecimal amount; }8. 部署上线实战
8.1 Docker容器化
Dockerfile最佳实践:
FROM openjdk:11-jre-slim VOLUME /tmp ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]构建命令:
# 先打包SpringBoot项目 mvn clean package -DskipTests # 构建镜像 docker build -t note-app . # 运行容器 docker run -d -p 8080:8080 --name note-app \ -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-server:3306/note_app \ -e SPRING_DATASOURCE_USERNAME=root \ note-app8.2 性能监控
集成Spring Boot Actuator:
# application.properties management.endpoints.web.exposure.include=health,info,metrics,prometheus management.metrics.export.prometheus.enabled=true配合Grafana监控看板,可以实时查看:
- JVM内存使用
- 接口响应时间
- 数据库连接池状态
- 系统负载
9. 答辩准备技巧
9.1 演示数据准备
使用Spring Boot的CommandLineRunner预置测试数据:
@Bean public CommandLineRunner initData(UserRepository userRepo, RecordRepository recordRepo) { return args -> { User user = userRepo.save(new User("test", "123456")); List<Record> records = Arrays.asList( new Record(user, LocalDateTime.now(), new BigDecimal("100.00"), RecordType.INCOME), new Record(user, LocalDateTime.now().minusDays(1), new BigDecimal("50.00"), RecordType.EXPENSE) ); recordRepo.saveAll(records); }; }9.2 常见答辩问题预演
根据多年指导经验,评委最爱问的5个问题:
为什么选用SpringBoot而不是其他框架?
- 标准答案:约定优于配置、内嵌服务器、丰富的Starter依赖
如何保证数据一致性?
- 展示@Transactional注解的使用场景
用户量增大后系统如何扩展?
- 讨论数据库分库分表、Redis缓存、负载均衡
安全方面做了哪些措施?
- 演示JWT鉴权、SQL注入防护、XSS过滤
项目的创新点在哪里?
- 突出极简设计、响应速度、数据加密方案