这是一篇关于我个人项目 iTodo 的技术复盘。它是一个用 Spring Boot 4 + PostgreSQL + Redis 构建的跨端待办事项 HTTP API,同时也是我第一次系统性地用 Claude(AI 结对开发) 从规格文档一路写到腾讯云生产部署的完整实践。
这篇文章不只讲”我做了什么”,更想讲清楚三件事:架构怎么搭的、工程规范怎么定的、以及 怎么和 AI 协作把它落地的。如果你也在做后端项目、或者好奇 AI 辅助开发的真实流程,希望能给你一些可复用的思路。
iTodo 是一个面向 小程序端、网页端、App 端 的待办事项后端服务,只提供 HTTP API,不含前端界面。功能上对标成熟的任务管理产品:清单管理、任务的完成/重要/”我的一天”、子任务步骤、标签、提醒、多端增量同步、全文搜索。
它刻意做成了一个”麻雀虽小五脏俱全”的工程样板:
| 层次 | 选型 | 为什么 |
|---|---|---|
| 语言 | Java 21 LTS (Temurin) | 用上 Record、模式匹配、虚拟线程红利,长期支持 |
| 框架 | Spring Boot 4.0.6 + Spring MVC | 生态最成熟,团队/AI 都熟 |
| 安全 | Spring Security 7 + JWT (jjwt) | 无状态认证,天然适配多端 |
| 数据库 | PostgreSQL 17 + HikariCP | 强一致、TIMESTAMPTZ/INET/UUID 原生支持好 |
| ORM | MyBatis-Plus 3.5.16 | 简单 CRUD 零 SQL,复杂查询回落到 XML |
| 缓存/限流 | Redis 8.4 + Redisson | 分布式限流、令牌桶 |
| 迁移 | Flyway | 数据库变更版本化,杜绝手改生产库 |
| 文档 | springdoc-openapi | 代码即文档,自动生成 Swagger UI |
| 工具 | Lombok + MapStruct + Hutool | 减样板、DTO 映射、常用工具 |
| 观测 | Actuator + Micrometer | 健康检查与指标 |
选型的一条隐藏主线是:每一层都选”约束性强、约定优于配置”的方案。因为项目从一开始就打算和 AI 协作,工具越”有脾气”(有明确的正确用法),AI 生成的代码就越不容易跑偏。
项目没有采用传统的 controller / service / mapper 三大目录横切,而是 按业务模块垂直切分:
1
2
3
4
5
6
7
8
9
10
11
12
com.example.itodo
├── auth // 认证:登录/注册/刷新/登出、微信小程序
├── user // 用户资料
├── todo // 清单、任务、步骤、智能视图(核心域)
├── tag // 标签
├── reminder // 提醒
├── sync // 多端同步(bootstrap + changes)
├── search // 搜索
├── security // JWT、过滤器、SecurityConfig
├── infra // 基础设施,如限流 ratelimit
├── config // OpenApi / Jackson / Redisson / WebMvc / Actuator
└── common // 统一响应、错误、分页、TraceId 过滤器
每个功能包内自带 entity / mapper / dto / Service / Controller。这样做的好处是:改一个功能,相关代码都在一个包里,对人和 AI 都更友好——给 AI 的上下文可以精确到”只看 todo 包”。
一次请求的完整链路是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Client
│ Authorization: Bearer <JWT>
▼
JwtAuthenticationFilter ──► 解析 JWT,注入 CurrentUser
▼
Controller (@RestController, /api/v1/**)
│ @Valid 校验 DTO;@AuthenticationPrincipal 拿当前用户
▼
Service ──► 业务逻辑 + 权限校验(一切查询按 ownerId 收敛)
│ 写操作同时写入 sync_changes
▼
Mapper (MyBatis-Plus) ──► PostgreSQL
▲
ApiResponseAdvice ──► 出参统一包装 + 注入 traceId
两个贯穿始终的横切设计值得单独说:
统一响应结构。 所有接口出参都被包装成 ApiResponse<T>:
1
2
3
4
5
6
public record ApiResponse<T>(
boolean success,
T data,
ApiError error,
String traceId
) { ... }
Controller 里只需 return ApiResponse.ok(data),而 traceId 由 ApiResponseAdvice(一个 ResponseBodyAdvice)在写出响应前自动从 MDC 补齐。前端拿到的永远是同一个信封结构,成功看 data,失败看 error,排障看 traceId。
全链路 TraceId。 TraceIdFilter 在请求入口生成/透传 traceId 并放入 MDC,日志和响应体都带上它。线上出问题时,用户报一个 traceId 就能串起整条日志。
一个用户可以绑定多个”身份来源”(邮箱、手机号、微信),核心表结构是 users + user_identities 分离:
users 存资料,email / phone 唯一,且有一条 CHECK 约束保证三者至少有其一。user_identities 用 (provider, provider_subject) 唯一识别第三方身份,微信登录就落在这里。refresh_tokens 按设备记录,存的是 token 的哈希而非明文,支持”退出当前设备”和”退出所有设备”。安全上的几条硬规矩(写进了 CLAUDE.md):短时效 access token + 轮换 refresh token、只存 refresh token 哈希、绝不打印密码/token、不使用带凭据的通配符 CORS。
一个真实的坑:微信
jscode2session返回的Content-Type是text/plain(但 body 其实是 JSON),RestClient.body(Xxx.class)会直接抛UnknownContentTypeException。解决办法是先读成String再用ObjectMapper手动解析。这类”文档没写、只有踩了才知道”的细节,正是需要沉淀进项目记忆的东西。
同步是这个项目最有意思的部分。策略很克制但很实用:
sync_changes 追加一条记录:(user_id, resource_type, resource_id, operation, version, changed_at)。GET /sync/bootstrap 拉全量,之后调 GET /sync/changes?sinceVersion=N 拉增量。这个设计的精髓是 用一张 append-only 的变更日志表,把”多端一致性”这个复杂问题降维成”拉取比我新的记录”。代码层面所有写操作都通过一个统一的 SyncChangeService 落变更,新增资源类型只要扩展 SyncResourceType 枚举即可。
登录/注册/刷新这类接口是暴力破解的重灾区,用 Redisson 的分布式限流在 infra.ratelimit 里统一挡在前面,参数通过 RateLimitProperties 外部化配置。
所有业务表都有 deleted_at(软删除)和 created_at / updated_at。核心的 todos 表还有一个 version BIGINT,既服务于同步的版本推进,也为未来的乐观锁留了口子。
代码谁都会写,但一个项目能不能持续演进、能不能让 AI 稳定产出正确代码,靠的是规范。iTodo 把规范拆成了几套明确的约定。
CLAUDE.md 是整个 AI 协作的核心。它不是文档,而是 每次对话都会被加载的强约束,用最精炼的语言告诉 AI”在这个项目里什么必须做、什么绝不能做”。它包含几个板块:
mvn test / mvn clean package / mvn spring-boot:run,让 AI 知道怎么验证。/api/v1;进出用 DTO 绝不裸露实体;写请求必须 Jakarta Validation;schema 变更必须走 Flyway;所有用户数据查询必须按当前用户收敛权限。mvn test、确认 Flyway 干净迁移、检查 /actuator/health、保持 OpenAPI 与行为一致。写好 CLAUDE.md 的心得:规则要短、要绝对、要可验证。像”用户数据查询必须 scope by authenticated user”这种一句话规则,胜过一整页解释,因为 AI 每次生成 Controller/Service 时都会自动带上这条约束。
/api/v1 前缀,资源用复数名词,动作用子路径(如 POST /todos/{id}/complete)。ApiResponse<T>,绝不裸返实体或 Map。XxxRequest DTO + @Valid;查询用 XxxQueryRequest;输出用 XxxResponse。三者严格分离,实体永不出现在 API 边界。XxxDtoMapper)在编译期生成,零反射。PageResponse<T>。@Tag / @Operation 注解,Swagger UI 自动生成,并额外维护一份手写的 docs/openapi/openapi.yaml 作为契约。从 V1__init_schema.sql 能提炼出一套非常一致的建表约定:
UUID PRIMARY KEY DEFAULT gen_random_uuid()(靠 pgcrypto),日志类表(sync_changes / audit_logs)用 BIGSERIAL 自增。TIMESTAMPTZ(带时区),created_at / updated_at 默认 now()。deleted_at TIMESTAMPTZ。owner_id / user_id 外键,并 ON DELETE CASCADE,从数据层保证数据隔离。CHECK 尽量放到数据库层(如 ck_users_identity 保证用户至少有一种身份)。1
2
CREATE INDEX idx_todos_owner_due_date
ON todos(owner_id, due_date) WHERE deleted_at IS NULL;
查”我的一天”“重要”“按截止日期”这些智能视图时,走的都是 owner_id 打头的复合索引,且天然跳过已删除数据。
V{n}__xxx.sql,绝不手改生产库。生产环境的一次教训:早期生产库曾绕过 Flyway 手工建表,导致
flyway_schema_history记录了 V1/V2”已应用”,但实际有表缺失,微信登录直接报relation "user_identities" does not exist。修复方式是写一个幂等的CREATE TABLE IF NOT EXISTS脚本补齐、且不动 Flyway 历史。这件事把”永远走 Flyway”这条规矩从纸面变成了肌肉记忆。
项目在 docs/openspec/ 下维护了一整套 中文规格文档,在写代码之前就把系统说清楚:
project.md:目标、MVP 范围、明确的”不做什么”。feature-modules.md:功能模块总览表 + 每个模块的完整接口清单。auth.md / lists.md / tasks.md / sync.md:分域细则。development-plan.md:分 6 个阶段的开发计划,每个阶段都有明确的”交付物”和”验收标准”。这套文档还有一条硬约束:因为仓库是公开的,所有文档、示例配置里不得出现任何真实密钥,命名也用中性表达、不体现任何第三方品牌。
docs/deployment/ 下有完整的生产落地方案:itodo.service(systemd)、nginx.conf.example(HTTPS 反代 + 安全响应头)、security-checklist.md(安全清单)、restore-test.md(备份恢复演练)。核心原则:只开必要端口、数据库/Redis 不出公网、生产关闭 Swagger、密钥全走 OS 环境变量。
这可能是最多人好奇的部分。我的做法不是”让 AI 帮我写个函数”,而是把 AI 当成一个 需要明确规格和护栏的结对工程师。整个流程是这样的:
在动第一行代码之前,先和 AI 一起把 docs/openspec/ 写出来——目标、范围、模块、接口、分阶段计划。规格是 AI 的地图。有了它,后面每次让 AI 写某个模块,都能引用”按 feature-modules.md 里 任务 部分的接口清单实现”,而不是让它自由发挥。
把不可违背的规则沉淀进 CLAUDE.md(见第五章)。它会被自动加载,等于给 AI 装了一层”永远在线”的约束。这一步是把项目质量从”靠每次提示词”变成”靠制度保证”的关键。
严格按 development-plan.md 的 6 个阶段走:基础设施 → 认证 → 清单任务 → 标签提醒同步 → 部署加固 → 增强。每个阶段都有验收标准,完成一个阶段就用 mvn clean package 验证一次,再进下一个。绝不一次让 AI 生成一大坨。
这是踩坑最多、也最重要的一环。几条血泪经验:
clean 编译。 不带 clean 时 Maven 可能判定”文件没变”跳过重编,给出 假的 BUILD SUCCESS,把真实的编译错误藏起来。验证一律 mvn clean compile / clean package。.java 只能有一个 public 顶层类型。 曾经把多个 public record 塞进一个文件导致编译失败,后来每个类型独立成文件。UUID 必须 import java.util.UUID。 Todo.ownerId 是 UUID,好几个 Service 都因漏 import 在生产编译时才暴露。TodoStatus 是一组 public static final String(不是 enum),消费方不能写 TodoStatus.ACTIVE.name(),方法参数也得用 String。这类”看起来像枚举其实是字符串”的约定,必须明确告诉 AI。把上面这些坑、这些约定,持续写进项目的长期记忆里(AI 协作时的 memory 文件)。这样 同一个错误不会犯第二次——下次 AI 再碰 UUID、碰 TodoStatus、碰 Flyway,都会自动想起历史教训。这一步让 AI 协作真正产生了”复利”。
规格给方向,CLAUDE.md 立规矩,分阶段控节奏,clean 验证兜底,记忆防复发。
AI 写代码的速度不是瓶颈,”让它每次都写对”才是。而”写对”靠的不是更聪明的提示词,是更好的工程约束。
即使不用 AI,这个项目本身也是一份不错的后端工程范本。我认为最值得借鉴的几点:
ON DELETE CASCADE,把正确性尽量下沉到数据库。iTodo 对我而言不只是”又做了一个 TodoList”。它验证了一个假设:当工程规范足够清晰、验证闭环足够严密时,AI 结对开发能稳定产出生产级代码。 AI 没有替代思考,反而逼着我把每一条约定都显式化——路径规范、DTO 规范、建表规范、安全规范,全都得写下来、说清楚。而这些被显式化的规范,最终既服务了 AI,也让项目本身变得更健壮。
如果你也想试试 AI 辅助开发,我的建议是:别急着让它写代码,先花时间和它一起把规格和规矩定下来。 磨刀不误砍柴工。
项目技术栈:Java 21 · Spring Boot 4 · PostgreSQL 17 · Redis 8.4 · MyBatis-Plus · Flyway · JWT。全程采用 Spec-Driven + CLAUDE.md 护栏的 AI 结对开发模式。