☰
SpringBoot极简记账App开发实战与毕业设计指南
2026/10/27 17:28:37 网站建设 项目流程

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;

踩坑提醒:

  1. 金额字段一定要用DECIMAL而非FLOAT,避免浮点精度问题
  2. 创建时间字段建议用TIMESTAMP而非DATETIME,方便按时间范围查询
  3. 必须建立复合索引,实测用户数据量超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.id

4. 开发中的典型问题排查

4.1 时区问题

常见报错:"记录的创建时间比当前时间快8小时" 解决方案:

  1. MySQL连接字符串添加时区参数:
spring.datasource.url=jdbc:mysql://localhost:3306/note_app?serverTimezone=Asia/Shanghai
  1. SpringBoot启动类添加注解:
@SpringBootApplication @PostConstruct public void init() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); }

4.2 跨域问题

前端开发时出现的CORS错误,三种解决方案选其一:

  1. 全局配置(推荐):
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .maxAge(3600); } }
  1. 控制器注解(快速测试用):
@CrossOrigin(origins = "http://localhost:8080") @RestController @RequestMapping("/api")
  1. 生产环境建议通过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=6379

6.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-app

8.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个问题:

  1. 为什么选用SpringBoot而不是其他框架?

    • 标准答案:约定优于配置、内嵌服务器、丰富的Starter依赖
  2. 如何保证数据一致性?

    • 展示@Transactional注解的使用场景
  3. 用户量增大后系统如何扩展?

    • 讨论数据库分库分表、Redis缓存、负载均衡
  4. 安全方面做了哪些措施?

    • 演示JWT鉴权、SQL注入防护、XSS过滤
  5. 项目的创新点在哪里?

    • 突出极简设计、响应速度、数据加密方案

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

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

立即咨询