乐于分享
好东西不私藏

挑战用 AI 开发 App 从 0 到上架全过程 | 第 02 篇 · 给 AI 一份能看懂的需求:

挑战用 AI 开发 App 从 0 到上架全过程 | 第 02 篇 · 给 AI 一份能看懂的需求:

从用户故事到接口契约

「AI 看得懂中文,但 AI 更看得懂结构化数据。」

这是写完这篇文档后,我最深的一句话感悟。

写在前面

上一篇我聊了「为什么一个人开发也要写需求文档」,那篇收尾时我说了一句话:

 在 AI 编程时代,需求文档不是写给人看的,是写给 AI 看的。

但写完产品定位文档后,我发现一个尴尬的事实:这份文档 AI 其实「看不太懂」。

我对 Trae 说:「按这份需求文档帮我生成 note_text 表的 CRUD 接口」。它给我返回了一段代码,字段是 idtitlecontentcreateTime——看起来没毛病,但完全不符合我的业务。我文档里明明写了要 categorytagswordCountaiOrganizedIds,它一个都没加。

后来我才明白:AI 看得懂中文,但 AI 更看得懂结构化数据。产品定位文档解决的是「做什么」,而 AI 写代码需要的是「怎么做」——它需要知道:

  • 用户会做什么操作(用户故事)

  • 这些操作对应哪些模块(模块清单)

  • 每个模块对外暴露什么接口(接口契约)

  • 接口背后是怎么存数据的(表结构)

这篇就专门讲我是怎么把这四件事写清楚的。最终输出了一份 pig-note后端模块规划设计.md,这份文档后来成了 Trae 生成后端代码的「圣经」。

一、用户故事:从「模糊想法」到「可执行描述」

1.1 为什么需要用户故事

一开始我直接写功能清单:

- 文本笔记:增删改查 - 语音笔记:增删改查 - 图片笔记:增删改查

但当我把这份清单丢给 AI 时,它问我:

  • 文本笔记删除是物理删除还是逻辑删除?

  • 语音笔记新增时需要传音频文件还是 URL?

  • 图片笔记的图片是单张还是多张?

我答不上来。于是我开始写用户故事。

1.2 用户故事模板

用户故事的标准格式是:

作为一个 <角色>, 我想要 <做什么>, 以便于 <达到什么目的>。

比如:

 作为一个注册用户, 我想要在首页看到今日记录数量和最近 5 条笔记预览, 以便于快速了解自己今天的记录情况。

1.3 星尘笔记的 12 个核心用户故事

我把 App 拆成了 12 个用户故事,这里挑几个完整的写出来:

用户故事 1 · 快速记录灵感

作为注册用户,我想要在首页点击浮动按钮后选择记录方式(文字/语音/图片/手绘/AI对话),以便于在 30 秒内完成一次灵感记录。

用户故事 2 · 语音转文字

作为注册用户,我想要录完音后自动转写成文字,以便于之后搜索和阅读,不用每次都听音频。

用户故事 3 · AI 整理笔记

作为注册用户,我想要对任意一条原始笔记一键调用 AI 整理,以便于得到结构化的标题、摘要、标签和待办事项。

用户故事 4 · 跨类型查找

作为注册用户,我想要通过自然语言问 AI「上周记过关于账单的笔记」,以便于不用翻列表就能找到老笔记。

用户故事 5 · 待办管理

作为注册用户,我想要把笔记里提到的事项标记为待办,以便于之后提醒自己执行。

用户故事 6 · 记账统计

作为注册用户,我想要按月查看我的支出/收入/结余统计,以便于了解自己的财务状况。

完整的 12 个用户故事我都写在了 docs/产品需求规划-灵感笔记.md 里,这里不一一列出。重点是:每个用户故事都对应一个或多个功能模块,可以直接映射成接口。

二、从用户故事拆出 10 大业务模块

2.1 拆解方法:动词 + 名词

把每个用户故事里的动词和名词拆出来:

用户故事
动词
名词
对应模块
快速记录灵感
记录
灵感
文本/语音/图片/手绘笔记
语音转文字
转写
语音
语音笔记模块
AI 整理
整理
笔记
AI 整理模块
跨类型查找
查找
笔记
AI 对话 + 聚合索引
待办管理
管理
待办
待办事项模块
记账统计
统计
账单
记账模块

2.2 10 大业务模块清单

最终拆出了 10 个后端业务模块,每个模块对应一张或多张表:

序号
模块名
类名
对应表
核心职责
1
文本笔记
NoteTextnote_text
文字类笔记 CRUD
2
语音笔记
NoteVoicenote_voice
录音 + 转写 + 音频存储
3
图片笔记
NotePhotonote_photo
多图 JSON + OCR
4
手绘笔记
NoteDrawingnote_drawing
画布图片存储
5
AI 整理
NoteAiOrganizednote_ai_organized
多版本结构化整理
6
AI 对话
NoteAiChatnote_ai_chat_message
聊天消息历史
7
待办事项
NoteTodonote_todo
状态流转 + 重复 + 提醒
8
记账
NoteAccountnote_account
收支 + 分类 + 统计
9
礼单
NoteGiftnote_gift
收礼随礼 + 往来统计
10
用户设置
NoteUserSettingnote_user_setting
通知/自动保存/流式开关

2.3 加上 2 张基础设施表

除了 10 个业务表,我还规划了 2 张「基础设施」表:

  • note_base:聚合索引表,解决多表分页查询的性能问题(每张子表增删改时同步维护)

  • note_view_history:浏览历史表,用于「最近看过」功能

总共 12 张业务表。这个规模一个人扛,必须靠 AI——这也是为什么我要把每个模块的接口契约都写得这么细。

三、接口契约:用 Markdown 表格画路径 / 方法 / 入参 / 返回

3.1 接口设计的 5 条约定

在动手写接口表之前,我给自己定了 5 条硬性约定:

  1. 路径前缀统一:所有业务接口都以 /note 开头,避免和 pig 框架自带的 /admin 冲突

  2. 返回结构统一:所有接口返回 R<T>,其中 R 是 pig 封装的统一响应体(code / msg / data

  3. HTTP 方法语义化:GET 查询、POST 新增、PUT 修改、DELETE 删除,不混用

  4. 分页统一用 MyBatis Plus 的 IPage:前端传 current / size,后端返回 records / total / current / size

  5. 批量删除用 body 传 ID 数组:而不是 query 拼接,避免 URL 过长

这 5 条约定写在文档最前面,Trae 生成代码时就会按这套规范来。

3.2 接口契约表(节选)

文档里我为每个模块都画了一张接口表。这里展示文本笔记模块的完整契约:

文本笔记 /note/text

方法
路径
入参
返回
说明
GET
/note/text/page
current, size, title?, category?
IPage<NoteText>
分页查询
GET
/note/text/{id}
id
NoteText
查询详情
POST
/note/text
NoteText 对象
R<Void>
新增
PUT
/note/text
NoteText 对象
R<Void>
修改
DELETE
/note/text
id[]
R<Void>
批量删除
GET
/note/text/count
-
R<Integer>
统计数量

这张表丢给 Trae,它能直接生成 NoteTextController 的完整骨架,连 @PreAuthorize 权限注解都帮你加上。

3.3 统一返回结构

我在文档里明确写了返回结构的样子:

{   "code": 0,   "msg": "success",   "data": {     "records": [...],     "total": 100,     "current": 1,     "size": 10   } }

这样 AI 生成前端 API 封装时,能正确解构 res.data.records,不会出现「数据出来了但页面空白」的诡异问题。

3.4 几个有意思的接口设计

接口 1 · 待办状态切换

PUT /note/todo/toggle 入参:{ id: 123, status: "completed" } 返回:R<Void>

为什么不用 PUT /note/todo 整体更新?因为状态切换是高频操作,每次都传整个 todo 对象太浪费带宽,而且容易把其他字段覆盖掉。单独抽一个 toggle 接口,更轻量也更安全。

接口 2 · 首页聚合统计

GET /note/home/stats 

返回:{   

    todayCount: 3,        // 今日记录数   

    totalCount: 128,       // 总记录数   

    pendingTodoCount: 5,  // 待办数   

    todayExpense: 56.50   // 今日支出 

}

这个接口要聚合 5 张笔记表 + 待办表 + 记账表的统计。如果按常规思路,每个模块查一次,首页加载会卡。所以我用了 note_base 聚合索引表,一次查询搞定。

接口 3 · AI 整理

POST /note/ai-organized/organize 入参:{   sourceId: 123,   sourceType: "voice",   style: "detail" } 返回:R<NoteAiOrganized>

这个接口的特殊之处在于:它不是简单的 CRUD,而是触发一次 AI 调用。后端要先查原始笔记 → 拼接 Prompt → 调用 Spring AI → 解析返回 → 写入 note_ai_organized 表。是整个项目最复杂的接口。

四、数据表骨架:10 张核心表的关系图

4.1 设计原则

我在写数据表设计之前,定了 4 条原则:

  1. 审计字段统一:所有表都带 create_by / update_by / create_time / update_time / del_flag,用 pig 的 MybatisBaseEntity 自动填充

  2. 主键统一雪花算法:bigint 类型,避免自增 ID 在分表时的冲突

  3. 逻辑删除统一用 del_flag0 正常,-1 删除,不物理删除用户数据

  4. JSON 字段慎用:只有当字段是变长数组时才用 JSON(如 note_photo.images),其他情况拆字段

4.2 表关系图

12 张表之间不是孤立的,它们有引用关系。我画了一张 ASCII 关系图:

                   ┌─────────────────┐                     │   note_base     │  聚合索引表                     │  (note_type +   │  (查询入口)                     │   note_id)      │                     └────────┬────────┘                              │ 同步维护        ┌─────────────────────┼─────────────────────┐        │                     │                     │        ▼                     ▼                     ▼ ┌─────────────┐      ┌─────────────┐      ┌─────────────┐ │ note_text   │      │ note_voice  │      │ note_photo   │ └──────┬──────┘      └──────┬──────┘      └──────┬──────┘        │                    │                    │        └──────────┬─────────┴──────────┬─────────┘                   │ source_id          │                   │ source_type        │                   ▼                    ▼             ┌─────────────────────────────┐             │   note_ai_organized         │  AI 整理表             │   (多版本 version)           │             └──────────────┬──────────────┘                            │ todos JSON                            ▼                     ┌─────────────┐                     │  note_todo  │  待办事项                     └─────────────┘  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │note_account │  │ note_gift   │  │note_ai_chat_msg │  独立模块 └─────────────┘  └─────────────┘  └─────────────────┘  ┌─────────────────────┐ │ note_user_setting   │  用户偏好(1用户1行) └─────────────────────┘  ┌─────────────────────┐ │ note_view_history   │  浏览历史 └─────────────────────┘

4.3 字段设计的几个关键决策

决策 1 · note_photo.images 用 JSON 数组而不是拆表

我考虑过建一张 note_photo_item 子表存每张图片,但最终选了 JSON 数组。原因:

  • 一条图片笔记通常 1-9 张图片,不会上百张

  • JSON 数组读取一次 IO,子表要 JOIN

  • 查询时不需要按图片字段筛选

但如果未来要做「按图片内容搜索」(比如找所有含白板的图片),就要拆表加索引了。

决策 2 · note_ai_organized 用 source_id + source_type 而不是外键

为什么不用外键?因为来源可能是 note_textnote_voicenote_photo 三张表中的任意一张,没法建外键约束。用 source_type 字段标识来源表,应用层负责校验。

决策 3 · note_todo.due_date 用 bigint 时间戳而不是 datetime

因为我用的是 MyBatis Plus,bigint 时间戳在前端展示时更灵活,可以按不同时区格式化。pig 框架里所有业务表的时间字段都用时间戳。

决策 4 · note_account 用 record_date 而不是 create_time

create_time 是审计字段(记录什么时候入库的),record_date 是业务字段(用户实际记账的日期)。这两个时间可能差几天——比如用户补记昨天的账单。

4.4 字段示例:note_ai_organized

这里展示最复杂的一张表的完整字段:

字段
类型
说明
id
bigint(20)
主键,雪花算法
title
varchar(120)
整理后标题
content
text
整理后正文
summary
text
摘要
category
varchar(32)
推荐分类
tags
varchar(255)
推荐标签,逗号分隔
todos
text
识别到的待办,JSON 数组
source_id
bigint(20)
来源笔记 ID
source_type
varchar(32)
来源类型:text/voice/image
style
varchar(32)
整理风格:detail/summary/meeting/todo/study
version
int
版本号
create_by
varchar(64)
创建人
update_by
varchar(64)
更新人
create_time
datetime
创建时间
update_time
datetime
更新时间
del_flag
char(2)
删除标记

这张表的设计要点:

  • source_id + source_type 实现多态关联

  • version 支持同一原始笔记多次整理,对比不同风格

  • tags 用逗号分隔字符串而不是 JSON,方便 SQL LIKE 查询

  • todos 必须用 JSON,因为待办是结构化对象数组

五、把文档喂给 AI 的实战

5.1 第一次喂文档的失败

写完文档后,我兴奋地把整个 pig-note后端模块规划设计.md 丢给 Trae,对它说:

  按这份文档帮我生成所有模块的 Controller / Service / Mapper / Entity 代码。

结果它确实按模块生成了 Entity、Mapper、Service、Controller 一整套文件,分包也分得挺规整。但我一跑起来就发现问题——代码能跑,但细节全是"裸奔"状态

  • Controller 把 App 端接口和管理端接口混在一个类里,没区分 /note/text/* 和 /admin/note/text/*

  • 管理端 Controller 一个 @PreAuthorize 权限注解都没加,谁都能调

  • 管理页面的分类、状态字段直接写死 <el-option>,没走 useDict 字典

  • App 端查询没加 createBy = 当前用户 的数据隔离

我意识到:整份文档丢进去,AI 能生成"形似"的代码,但"神不似"——缺的是业务细节约束

5.2 改进:分模块喂 + 补细节约束

后来我改了策略,不是"一个模块一个模块地喂"那么简单,而是分模块喂的同时,把每个模块的业务约束讲清楚

  1. 先说:「生成 note_text 模块,App 端接口和管理端接口要拆成两个 ControllerNoteTextController 放 /note/textAdminNoteTextController 放 /admin/note/text

  2. 再说:「管理端 Controller 每个 endpoint 都要加 @PreAuthorize("@pms.hasPermission('note_text_view')") 这类权限注解,权限码对应 sys_menu 里的 permission 字段」

  3. 然后说:「App 端查询必须 .eq(NoteText::getCreateBy, SecurityUtils.getUser().getUsername()) 做数据隔离,管理端才能查全部」

  4. 最后说:「管理页面的 category 字段从字典 dict_note_type 获取,用 useDict hook 渲染下拉,不要写死 <el-option>

这样分步走,每个文件都准确无误,生成的代码直接能上线,不用自己再返工调一遍。一个完整的文本笔记模块(App 端 + 管理端 + 前端页面),10 分钟就生成完了。

5.3 一个意外收获:文档成了「活字典」

写完文档后,我发现自己开发时也经常翻这份文档。比如:

  • 写前端 API 封装时,查接口路径和入参

  • 写 SQL 脚本时,查字段类型和默认值

  • 和自己讨论「这个字段叫什么名字」时,翻文档定夺

这份文档后来变成了项目的「活字典」,无论是 AI 还是人,都靠它对齐认知。

六、写完文档后我学到的事

6.1 AI 时代的「文档」是什么

以前写需求文档是为了给产品经理、设计师、开发、测试看,所以重点是「人能读懂」。

AI 时代,文档多了一个读者——AI。AI 读文档的方式和人不同:

  • 人看文字描述就能脑补

  • AI 需要结构化数据(表格、代码块、JSON 示例)

所以我的写法是:文字描述写给人看,表格和示例写给 AI 看。一份文档兼顾两类读者。

6.2 不要追求"完美文档"

写第一版文档时,我纠结了一晚上:要不要画 UML 类图?要不要写时序图?要不要用 PlantUML?

后来想通了:能跑的文档就是好文档。Markdown 表格 + ASCII 图 + 代码示例,足够 AI 理解。UML 这些重型工具,等团队规模大了再用。

6.3 文档要随代码一起迭代

写完文档不代表结束。我后来开发过程中改了好几次设计:

  • note_todo 表加了一个 completed_time 字段(一开始忘了)

  • note_account 表的 category_icon 字段是从字典 remarks 字段读的(一开始想直接存 emoji)

  • note_ai_organized 表加了 style 字段(一开始只有版本号,后来发现需要风格区分)

每次改动我都会同步更新文档。Git 里这份文档的 commit 历史比代码还多。

七、互动

写完这篇,我想问大家一个问题:

 你在用 AI 编程时,是直接对着 AI 说需求,还是先写一份文档再喂给它?

我自己从「直接说」到「先写文档」的转变,是从一次 AI 自由发挥的翻车开始的。欢迎在评论区聊聊你的做法。

下一篇预告

《03 - Todo 功能详细规划:从模糊想法到可执行的开发清单》

这个模块是整个项目最复杂的,状态流转、重复规则、提醒模型,我吃了不少坑,值得单独开一篇细讲。

「挑战用 AI 开发 App 从 0 到上架全过程」系列文章

· 第 02 篇 · 灵感笔记 ·