公司动态

Spring Boot实战:从零搭建个人想法管理系统

📅 2026/8/30 23:20:02
Spring Boot实战:从零搭建个人想法管理系统
记录灵感看起来是一件小事真正开发起来才知道 kris idea 这个标题可以承载一整套工程把零散的技术想法收集起来给它们加状态、排优先级、评估工作量最后变成可推进的任务。下面以 kris idea 作为项目名从零搭建一个轻量级个人想法管理系统先跑通 REST API再把数据落到数据库最后补上生产环境必须考虑的鉴权、校验、日志和扩展点。kris idea 这个名字很容易让人误以为只是个备忘录脚本但一旦你开始设计表结构、接口和状态流转就会发现它已经是一个标准的 CRUD 加业务规则的项目。这样的项目非常适合作为 Spring Boot 入门练习因为它覆盖了实体映射、Repository 查询、Service 事务、Controller 参数校验、H2 调试、MySQL 迁移和 Spring Security 集成等一整套开发链路。这篇文章会按实际开发顺序展开先定义问题边界再准备环境然后设计表结构接着编写核心接口再用 H2 控制台和 curl 验证最后讨论生产环境需要补哪些能力。学习阶段可以用 H2 内存库快速跑通正式开发阶段再切 MySQL 和数据库迁移工具。1. 先想清楚 kris idea 这套项目要解决什么问题1.1 灵感记录为什么不能只依赖备忘录很多人记录技术灵感时第一反应是打开备忘录或聊天框把一段话丢进去。这种方式的问题不是“不能记”而是“记了之后没法处理”。备忘录里的条目没有统一字段没有优先级没有状态。一个想法是已经做完了还是已经放弃还是仍然值得投入全靠你重新读一遍才能判断。时间一长里面的内容就会堆积成无人清理的回收站。kris idea 这个项目要解决的问题就是把“随手记下的文字”升级成“有结构、可流转、能推进的想法列表”。在项目里一个想法不再只是一段文字而是包含标题、描述、状态、优先级、预计工时、创建时间和更新时间的结构化记录。这样你就能随时回答三个问题现在有多少想法是待办的哪些想法值得优先做哪些想法已经被放弃。1.2 从想法到需求给记录加状态机想法和任务之间最大的区别是生命周期。一个任务通常只有未开始、进行中、已完成三种状态而一个想法可能长期停留在草稿阶段也可能在评估后被放弃。kris idea 的最小状态机可以设计为四个状态状态含义典型流转方向DRAFT刚记录还没整理清楚DRAFT - ACTIVEDRAFT - DISCARDEDACTIVE已经确认值得推进ACTIVE - DONEACTIVE - DISCARDEDDONE已经落地完成终态不再流转DISCARDED经过评估后放弃终态不再流转优先级也建议至少分四档避免所有想法都是“高优”导致排序失效。优先级含义建议处理节奏URGENT最近几天就要推进当天进入 ACTIVEHIGH本周内值得处理本周进入 ACTIVEMEDIUM有潜力但不急排入后续计划LOW只记录暂不投入保持 DRAFT状态和优先级分开管理是因为它们描述的是两个维度状态表示“这个想法走到哪一步了”优先级表示“这个想法值不值得先做”。实际项目中不要把两者合并成一个字段否则筛选和排序都会变得很别扭。1.3 最小可用范围把这个阶段的目标锁定做一个项目最怕一开始就铺开所有功能。kris idea 第一版可以只做五件事创建想法提交标题、描述、优先级默认状态为 DRAFT。查询列表支持按状态筛选按更新时间倒序排列。查询详情通过 ID 拿单条记录。修改状态把 DRAFT 流转到 ACTIVE、DONE 或 DISCARDED。删除想法删除已经确认无用的记录。登录、权限、标签、全文搜索、前端页面这些能力第一版先不进入实现范围。它们会在后面的生产环境章节讨论但不要在起步阶段拖慢学习节奏。注意第一版用 REST API 验证功能不代表项目最终形态不需要前端。先让数据链路完整可验证再补界面排查问题时会更轻松。2. 环境准备和项目骨架要先对齐否则后面全在白费功夫2.1 本机环境要求kris idea 后端使用 Spring Boot 3.x实体映射和数据访问使用 Spring Data JPA本地调试数据库使用 H2生产环境再切换 MySQL。开始之前先确认本机环境满足以下要求。组件版本建议检查命令说明JDK17 及以上java -versionSpring Boot 3 要求 JDK 17 起步Maven3.6 及以上mvn -version用于依赖管理和构建IDEIntelliJ IDEA 或 Eclipse无推荐安装 Lombok 插件如果使用手动 getter/setter 则不需要curl任意版本curl --version用于接口验证Postman可选无替代 curl 的图形化调试工具如果本机没有安装 JDK 和 Maven建议先配置 JAVA_HOME 和 MAVEN_HOME 环境变量确保命令行里能直接执行java和mvn。这一步没有做好后面跑mvn spring-boot:run时会频繁出现找不到命令或版本不匹配的问题。2.2 用 Spring Initializr 或 Maven 创建工程推荐直接使用 Spring Initializr 生成基础工程。项目名由于带有英文单引号不适合直接用作 Maven artifactId 和 Java 包名因此工程目录叫kris-ideagroupId 用com.krisidea包名使用com.krisidea.idea。curl https://start.spring.io/starter.tgz \ -d dependenciesweb,data-jpa,validation,h2 \ -d groupIdcom.krisidea \ -d artifactIdkris-idea \ -d namekris-idea \ -d packageNamecom.krisidea.idea \ -d javaVersion17 \ -d typemaven-project \ | tar -xzvf -如果你习惯使用 IDE 内置的 Spring Initializr也可以手动选择 Spring Web、Spring Data JPA、Validation 和 H2 Database 四个依赖。生成完成后工程结构大致如下。kris-idea ├── pom.xml └── src ├── main │ ├── java │ │ └── com/krisidea/idea │ │ ├── KrisIdeaApplication.java │ │ ├── entity │ │ ├── repository │ │ ├── service │ │ └── controller │ └── resources │ └── application.yml └── test └── java/com/krisidea/ideapom.xml 是依赖管理的关键文件。下面是一个最小可运行的依赖清单。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent groupIdcom.krisidea/groupId artifactIdkris-idea/artifactId version0.0.1-SNAPSHOT/version properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency /dependencies不要直接复制一个不确定版本的 Spring Boot 父依赖到生产项目落地前要根据本机 JDK 和依赖仓库里的版本确认。版本号只写 3.2.5 是示例实际项目以 Maven 仓库里可用的稳定版本为准。2.3 application.yml 里的配置要区分学习和生产学习阶段用 H2 内存库最方便项目启动时自动建表进程关闭后数据消失不会留下脏数据。下面是一份学习环境配置。server: port: 8080 spring: application: name: kris-idea datasource: url: jdbc:h2:mem:krisidea;DB_CLOSE_DELAY-1;DATABASE_TO_UPPERfalse driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false这里有几个关键点要注意。DB_CLOSE_DELAY-1让 H2 内存数据库在连接关闭时不会立刻销毁否则调试时可能遇到“第一次请求正常第二次请求表不存在”的现象。DATABASE_TO_UPPERfalse让 H2 保留小写表名和字段名这能减少与 MySQL 行为不一致带来的困惑。ddl-auto: update只能在学习和开发阶段使用。它会根据实体类自动修改表结构但在生产环境一旦表结构变更不可控就可能出现字段丢失或索引重建的严重问题。生产环境应该切换到 MySQL并使用 Flyway 或 Liquibase 管理数据库迁移。open-in-view: false是 Spring Boot 3 的推荐设置。默认的 open-in-view 为 true 时HTTP 请求全程保持数据库连接初学者感觉不到问题但容易出现连接被长时间占用的情况。关闭它倒逼你在 Service 层把关联数据查询完整而不是在 Controller 或视图层依赖懒加载。3. 用一张 ideas 表把想法变成结构化数据3.1 表结构设计说明kris idea 第一版只需要一张idea表。字段设计要围绕“记录、筛选、排序、推进”四个动作展开。字段类型约束说明idBIGINT主键自增每条想法的唯一标识titleVARCHAR(120)非空想法标题控制长度避免超长文本撑爆列表descriptionVARCHAR(1000)可空想法详细描述statusVARCHAR(30)非空状态枚举值存字符串不存数字priorityVARCHAR(20)非空优先级枚举值estimated_hoursINT可空预计投入工时便于评估排期created_atTIMESTAMP非空创建时间updated_atTIMESTAMP非空最后修改时间updated_at是列表排序的关键字段。第一版查询列表按它倒序排列刚更新过的想法排在最前面这比按创建时间排序更符合使用直觉因为你最关心的永远是最近处理过的内容。3.2 实体、枚举和数据库映射创建两个枚举类一个表示状态一个表示优先级。package com.krisidea.idea.entity; public enum IdeaStatus { DRAFT, ACTIVE, DONE, DISCARDED }package com.krisidea.idea.entity; public enum IdeaPriority { LOW, MEDIUM, HIGH, URGENT }实体类对应idea表。为了让示例代码便于阅读这里省略了 getter 和 setter实际代码必须补全或者使用 Lombok 的Getter和Setter。package com.krisidea.idea.entity; import jakarta.persistence.*; import java.time.LocalDateTime; Entity Table(name idea) public class Idea { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 120) private String title; Column(length 1000) private String description; Enumerated(EnumType.STRING) Column(nullable false, length 30) private IdeaStatus status; Enumerated(EnumType.STRING) Column(nullable false, length 20) private IdeaPriority priority; Column(name estimated_hours) private Integer estimatedHours; Column(name created_at, nullable false, updatable false) private LocalDateTime createdAt; Column(name updated_at, nullable false) private LocalDateTime updatedAt; PrePersist public void prePersist() { LocalDateTime now LocalDateTime.now(); if (createdAt null) { createdAt now; } updatedAt now; } PreUpdate public void preUpdate() { updatedAt LocalDateTime.now(); } }Enumerated(EnumType.STRING)非常关键。如果使用默认的EnumType.ORDINAL数据库里存的是枚举下标数字一旦你在枚举中间插入新值所有历史数据的含义都会错位。存字符串虽然会占用更多空间但在小项目里完全值得。3.3 为什么状态和优先级要用枚举而不是字符串直接在实体里写一个普通 String 字段也能跑通但代价是约束缺失。调用方可以传任意字符串比如把一个状态写成pending数据库不会报错查询时却永远匹配不到数据。枚举把可选值限制在编译期的固定集合里IDE 会自动提示解析异常也能在入口处快速暴露。另一个好处是 Controller 在接收请求参数时Spring 会尝试把字符串自动转换成枚举传入未知值时直接返回 400而不是把脏数据写进数据库。不过枚举也不是没有缺点。它适合值集合稳定或变化很慢的字段。如果业务上经常需要临时新增状态后端每次都要改代码重新发布此时可以考虑用字典表代替枚举。对于 kris idea 这种个人项目枚举是更简单的选择。4. 实现核心 API创建、查询、修改、删除、状态流转4.1 Repository 只写接口就行但要选对查询方式Spring Data JPA 的 Repository 层不需要手写实现类继承JpaRepository后基础的增删改查方法就自动存在。package com.krisidea.idea.repository; import com.krisidea.idea.entity.Idea; import com.krisidea.idea.entity.IdeaStatus; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface IdeaRepository extends JpaRepositoryIdea, Long { ListIdea findByStatusOrderByUpdatedAtDesc(IdeaStatus status); }findByStatusOrderByUpdatedAtDesc是 Spring Data JPA 的派生查询方法框架会根据方法名自动生成 SQL。方法名拆解下来是按status字段过滤。结果按updatedAt字段倒序排列。只要字段名和实体属性名一致这个方法名就能正确解析。如果把updatedAt写成updateTime或者把status写错项目启动时就会抛出方法名解析异常。这类错误的特点是非常明确启动日志里会直接告诉你哪个属性找不到。4.2 Service 层负责业务规则不要让 Controller 直接操作数据第一版如果直接把 Repository 注入 Controller代码会短很多但业务规则会散落在接口层。比如“创建想法时标题不能为空”“默认状态是 DRAFT”“默认优先级是 MEDIUM”这些规则放在 Service 层更容易复用和测试。package com.krisidea.idea.service; import com.krisidea.idea.entity.Idea; import com.krisidea.idea.entity.IdeaPriority; import com.krisidea.idea.entity.IdeaStatus; import com.krisidea.idea.repository.IdeaRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Service public class IdeaService { private final IdeaRepository ideaRepository; public IdeaService(IdeaRepository ideaRepository) { this.ideaRepository ideaRepository; } Transactional public Idea create(Idea idea) { if (idea.getTitle() null || idea.getTitle().trim().isEmpty()) { throw new IllegalArgumentException(title 不能为空); } if (idea.getStatus() null) { idea.setStatus(IdeaStatus.DRAFT); } if (idea.getPriority() null) { idea.setPriority(IdeaPriority.MEDIUM); } return ideaRepository.save(idea); } Transactional public Idea changeStatus(Long id, IdeaStatus status) { Idea idea ideaRepository.findById(id) .orElseThrow(() - new IllegalArgumentException(idea 不存在: id)); idea.setStatus(status); return idea; } Transactional public void delete(Long id) { ideaRepository.deleteById(id); } }注意changeStatus方法中没有显式调用save因为idea是从持久化上下文里查出来的托管实体在事务方法内修改字段后事务提交时 Hibernate 会自动执行 UPDATE。这个机制叫脏检查初学阶段容易忽略但它能避免很多重复的 save 调用。4.3 Controller 暴露 REST 接口并统一返回结构Controller 负责把 HTTP 请求转换成 Service 调用再负责把结果序列化成 JSON。下面先给出最小可运行版本直接返回实体对象。package com.krisidea.idea.controller; import com.krisidea.idea.entity.Idea; import com.krisidea.idea.entity.IdeaStatus; import com.krisidea.idea.repository.IdeaRepository; import com.krisidea.idea.service.IdeaService; import jakarta.validation.Valid; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.Map; RestController RequestMapping(/api/ideas) public class IdeaController { private final IdeaService ideaService; private final IdeaRepository ideaRepository; public IdeaController(IdeaService ideaService, IdeaRepository ideaRepository) { this.ideaService ideaService; this.ideaRepository ideaRepository; } GetMapping public ListIdea list(RequestParam(required false) String status) { if (status null || status.isBlank()) { return ideaRepository.findAll( Sort.by(Sort.Direction.DESC, updatedAt)); } IdeaStatus ideaStatus IdeaStatus.valueOf(status.toUpperCase()); return ideaRepository.findByStatusOrderByUpdatedAtDesc(ideaStatus); } GetMapping(/{id}) public Idea detail(PathVariable Long id) { return ideaRepository.findById(id) .orElseThrow(() - new ResponseStatusException( HttpStatus.NOT_FOUND, idea not found)); } PostMapping ResponseStatus(HttpStatus.CREATED) public Idea create(Valid RequestBody Idea idea) { return ideaService.create(idea); } PatchMapping(/{id}/status) public Idea changeStatus(PathVariable Long id, RequestBody MapString, String body) { IdeaStatus status IdeaStatus.valueOf( body.get(status).toUpperCase()); return ideaService.changeStatus(id, status); } DeleteMapping(/{id}) ResponseStatus(HttpStatus.NO_CONTENT) public void delete(PathVariable Long id) { ideaService.delete(id); } }这个版本有两个问题要解释。第一IdeaStatus.valueOf(status.toUpperCase())会在字符串无法匹配枚举时抛出IllegalArgumentExceptionSpring 不会自动把它转成 400 响应所以更稳妥的做法是捕获异常或使用自定义异常处理器。第二直接返回实体对象会把所有字段暴露给前端第一版为了简洁可以接受正式项目建议用 DTO 拆分请求对象和响应对象。如果你希望所有接口返回统一结构可以定义一个通用响应体并把 Controller 的返回类型整体替换。package com.krisidea.idea.common; public class ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT ok(T data) { ApiResponseT response new ApiResponse(); response.code 0; response.message ok; response.data data; return response; } // getters and setters }统一响应体最直接的好处是前端可以按固定格式解析异常时也能拿到同样的结构。代价是每个 Controller 方法都要包装一层代码会稍微啰嗦。具体是否引入取决于你是只做后端接口还是需要跟前端约定协议。4.4 用 curl 验证核心流程项目启动后打开第二个终端执行以下命令。创建一个想法curl -X POST http://localhost:8080/api/ideas \ -H Content-Type: application/json \ -d { title: 把想法管理工具做成 REST 服务, description: 先跑通增删改查再考虑前端, status: DRAFT, priority: HIGH, estimatedHours: 3 }预期响应里会包含生成的 id 和默认填充的创建时间。查询列表curl http://localhost:8080/api/ideas按状态筛选curl http://localhost:8080/api/ideas?statusACTIVE查询详情curl http://localhost:8080/api/ideas/1修改状态curl -X PATCH http://localhost:8080/api/ideas/1/status \ -H Content-Type: application/json \ -d {status:ACTIVE}删除curl -X DELETE http://localhost:8080/api/ideas/1删除成功后状态码应该是 204没有响应体。再次查询这条记录时应返回 404。注意不要只验证接口能返回 200。创建、查询、修改、删除四个流程都跑一遍才说明数据链路是完整的。5. 验证结果H2 控制台、日志和 Postman 三种方式5.1 启动项目并确认接口可访问在工程根目录执行mvn spring-boot:run看到类似下面的日志说明 Spring Boot 已经启动成功。Tomcat started on port 8080 (http) Started KrisIdeaApplication in 2.3 seconds然后访问curl http://localhost:8080/api/ideas第一次查询时如果返回[]说明接口可用但数据库还没有数据。这是正常现象先用前面的 POST 接口创建一条数据再回头看。5.2 通过 H2 控制台查看数据落库情况H2 控制台是学习阶段最直观的调试工具。浏览器访问http://localhost:8080/h2-console登录时填写以下配置配置项值JDBC URLjdbc:h2:mem:krisideaUser NamesaPassword留空即可连接成功后执行下面的 SQL 查看数据。SELECT id, title, priority, status, estimated_hours, created_at, updated_at FROM idea ORDER BY updated_at DESC;如果能看到刚创建的数据说明 JPA 的表映射和字段命名都没有问题。如果表名为空或字段为空优先检查application.yml里的DATABASE_TO_UPPERfalse是否生效以及实体类上的Table注解是否写错。5.3 通过日志和 HTTP 状态码判断问题出在哪一层application.yml里开启了show-sql: true所以每次请求数据库时控制台都会打印实际执行的 SQL。当接口返回结果和预期不一致时先看 SQL 再判断问题层次。现象请求层业务层数据层返回 400提示参数错误是否否返回 404ID 不存在是是否返回 200但数据查不到否是是SQL 明显不对字段名错误否否是例如查询列表时返回空数组但 H2 里明明有数据。这时先刷新浏览器看日志里是否有 SELECT 语句再判断 SQL 里的表名和字段名是否和实际表一致。如果根本没有 SELECT 日志问题可能在参数解析或 Controller 路由上。Postman 的优势在于可以保存请求历史、管理环境变量、查看响应头和响应体适合在做前端联调之前先把接口契约调试稳定。它和 curl 本质上是同一种验证方式选择哪一种取决于个人习惯。6. 常见问题排查接口通了但数据没写入这类问题怎么查6.1 错误现象、可能原因和检查路径表kris idea 这类项目在初学阶段遇到的大部分问题都集中在数据库连接、JPA 映射、JSON 序列化三块。下面是比较常见的场景。问题现象常见原因检查方式处理建议启动报表不存在H2 内存库被重建或 ddl-auto 配置不对查看启动日志中的建表语句确认ddl-auto: update不要乱改 URLPOST 返回 500日志显示字段为空实体字段没有赋值或请求 JSON 字段名不对打印请求体核对字段名检查前端请求的字段名是否与实体属性一致列表查不到刚插入的数据使用了不同数据库连接或事务未提交查看 H2 控制台连接地址统一使用jdbc:h2:mem:krisidea修改状态后没有生效没有在事务方法内操作实体查看日志是否打印 UPDATE把状态修改逻辑放进 Service 事务方法LocalDateTime 返回数组格式默认序列化没有配置日期格式查看 JSON 响应配置spring.jackson.date-format或用JsonFormat接口名带 status 参数时 500枚举解析失败检查请求参数大小写捕获异常并返回 4006.2 JPA 字段命名与数据库关键字冲突idea表本身不是关键字但如果你把表名改成user、order或者在字段里使用desc、level这类词H2 和 MySQL 都可能报语法错误。例如你有一个字段叫descHibernate 生成的 SQL 会变成select idea0_.desc from idea idea0_这在部分数据库里会直接报错因为DESC是排序关键字。解决办法是在实体字段上明确指定列名避免和保留字冲突。Column(name description_of_idea) private String description;更稳妥的做法是建表前先在数据库客户端里执行一次测试 SQL确认列名不会引起语法错误。学习阶段用 H2生产阶段用 MySQL两个数据库的保留字集合不完全一致上线前要在目标数据库里重新验证一遍。6.3 LocalDateTime 序列化格式异常如果响应里的时间变成一大段数字或数组问题通常出在 Jackson 对LocalDateTime的默认序列化上。这是因为 Java 8 时间类型默认会被序列化成包含年月日时分秒的对象结构而不是常见的字符串。解决方式有两种。一种是在配置里统一指定日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai另一种是在字段上使用JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime createdAt;需要注意spring.jackson.date-format对LocalDateTime不一定总是生效它主要影响java.util.Date。因此推荐对实体里的时间字段显式加JsonFormat效果更可控。6.4 事务不生效或更新失败一个最容易被忽略的问题是同一个 Controller 里直接调用 Service 的私有方法事务注解不生效。Spring 的事务代理是基于 AOP 的只有通过 Spring 容器注入的 Service 外部调用Transactional才会被代理拦截。另一种情况是更新时先findById再修改但没有把修改后的实体重新保存。前文已经说过只要实体处于持久化上下文中事务提交时会自动更新。如果你对一个从数据库查出来的实体执行了修改最后却调用saveAndFlush可能暴露出 Hibernate 在事务边界上的混淆。推荐的做法是事务边界放在 Service 层公开方法上。查询和修改在同一个事务方法内完成。不要在一个事务方法内调用同类中的另一个事务方法。如果需要立即看到 UPDATE 效果可以使用saveAndFlush但要理解它只是强制提交 SQL并不代表事务结束。7. 生产环境要考虑的事迁移、鉴权、分页和日志7.1 从 H2 换到 MySQL 要调整什么学习阶段用 H2 只是为了快速验证正式部署前必须切换到 MySQL 或 PostgreSQL。切换过程不是只改一个连接 URL 那么简单至少要做以下调整。在 pom.xml 中加入 MySQL 驱动。新增application-prod.yml配置文件。把ddl-auto改为validate或none。引入 Flyway 管理数据库迁移脚本。检查字段类型是否兼容例如 MySQL 的 TEXT 对应 H2 的 CLOB。下面是application-prod.yml的示例。spring: datasource: url: jdbc:mysql://localhost:3306/kris_idea?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: kris_app password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate open-in-view: false flyway: enabled: true locations: classpath:db/migration数据库密码不能写死在配置文件里这里用${DB_PASSWORD}从环境变量读取。生产环境还要关闭 H2 控制台避免运维接口暴露。Flyway 的迁移脚本是一组按版本号递增的 SQL 文件。第一个迁移脚本可以这样写CREATE TABLE idea ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(120) NOT NULL, description VARCHAR(1000), status VARCHAR(30) NOT NULL, priority VARCHAR(20) NOT NULL, estimated_hours INT, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL );ddl-auto: validate的作用是启动时让 Hibernate 检查实体类和表结构是否一致不一致就启动失败。这样可以防止代码和数据库结构悄悄脱节。7.2 给接口加身份认证kris idea 如果只部署在本地不加认证也没有大问题。但一旦部署到公网任何人都有可能调用你的接口增删改查。生产环境至少要加一层认证。最简单的做法是引入 Spring Security先跑通 HTTP Basic 认证。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency然后提供一个最小安全配置。package com.krisidea.idea.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/**).authenticated() .anyRequest().permitAll() ) .httpBasic(Customizer.withDefaults()); return http.build(); } }这个配置会要求所有/api/**请求都必须认证。访问时需要带上用户名和密码curl -u kris:password http://localhost:8080/api/ideasHTTP Basic 只适合内部系统或快速验证。对外服务建议使用 JWT 或 OAuth2把用户名、角色、过期时间放在令牌里避免每个请求都查数据库验证密码。安全方案的选型取决于你的部署场景这里只给出一个能够继续扩展的起点。7.3 分页查询和输入校验第一版的列表查询直接返回所有记录。当想法数量增长到几百上千条接口响应会越来越大前端渲染也会变慢。生产环境要改成 Spring Data 自带的分页方式。Repository 里增加分页查询方法import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface IdeaRepository extends JpaRepositoryIdea, Long { PageIdea findByStatus(IdeaStatus status, Pageable pageable); }Controller 里接收 page 和 size 参数GetMapping public PageIdea list(RequestParam(required false) String status, RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size) { Pageable pageable PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, updatedAt)); if (status null || status.isBlank()) { return ideaRepository.findAll(pageable); } IdeaStatus ideaStatus IdeaStatus.valueOf(status.toUpperCase()); return ideaRepository.findByStatus(ideaStatus, pageable); }输入校验要在创建和修改接口上完整暴露。实体字段上可以使用注解NotBlank(message 标题不能为空) Size(max 120, message 标题不能超过 120 字) private String title;Controller 上已经有ValidSpring 会在进入 Service 之前完成校验。校验失败返回 400而不是把错误数据写入数据库。7.4 日志和监控生产环境不能只靠控制台输出判断问题。至少要做到以下几件事。使用 Logback 或 Log4j2 写入文件按天滚动保留最近 30 天日志。在 Service 层记录关键操作例如创建想法、修改状态、删除想法带上操作对象 ID。使用 Spring Boot Actuator 暴露健康检查接口由监控系统定期探测。对异常请求返回统一错误结构并在日志里记录 traceId方便把用户看到的报错和日志对应起来。数据库连接池参数要按项目规模调整确定最大连接数避免默认值不够或过高。management: endpoints: web: exposure: include: health,info,metricsActuator 的/actuator/health返回 UP 时说明应用本身是健康的。但它只代表应用进程存活不代表业务逻辑正常。真正的健康检查要结合数据库连接、关键接口探测和告警规则综合判断。8. 从 kris idea 延伸出去下一步可以做什么8.1 把状态机做完整第一版的状态修改很自由任意状态都能跳到任意状态。比如 DONE 之后还可以改回 DRAFT这在业务语义上就不合理。后续可以定义一个状态流转表只允许合法流转。当前状态允许流转到DRAFTACTIVEDISCARDEDACTIVEDONEDISCARDEDDONE无DISCARDED无Service 层在changeStatus里校验流转合法性不合法时抛出业务异常。这个改动能让你提前理解状态机设计比直接引入复杂工作流引擎更有价值。8.2 增加标签、搜索和关联任务单一想法列表之后自然需要分类能力。可以增加tag表把想法和标签做成多对多关系。查询时按标签过滤能更精准地聚焦某个技术方向的灵感。再往后可以把想法和任务打通。想法变成任务时复制标题、描述、优先级同时保留关联的 ideaId这样能追踪“这个需求是从哪个想法演化而来”的完整链路。对于个人技术项目管理这是一个非常实用的扩展点。8.3 可复用的开发检查清单最后整理一份 checklist既可以用于 kris idea 项目也可以复用到其他 Spring Boot 项目。检查项学习环境生产环境数据库H2 内存库MySQL 或 PostgreSQL外置配置建表方式ddl-auto: updateFlyway 迁移脚本密码配置可写死环境变量或密钥管理服务分页查询直接全量返回Page Pageable接口认证无Spring Security JWT 或 OAuth2输入校验可以不写Valid 统一异常处理日志控制台输出文件滚动 traceId健康检查不必须Actuator 或自研探活接口备份不需要数据库定期备份kris idea 看起来只是一个个人想法管理系统但它把 Spring Boot 开发里最常用的一整套能力都串起来了。从表结构设计、JPA 映射、REST 接口到事务边界、枚举设计、数据库迁移、安全认证、分页和日志每一步都可以继续加深。初学者最好的做法是先按本文顺序把最小版本跑通再选择一个扩展点深入做下去。真正把想法变成一个可持续推进项目的过程本身就是在实践 kris idea 这个名字。