| 档案 | |
|---|---|
module |
diary |
summary |
日记:没有自己的包 —— 一篇日记就是 posts 表里 visibility='private' 的一行,kind='diary' 由可见性算出来、客户端传不了;唯一自己的表是日记本 diary_notebooks(删本不删篇,外键 SET NULL);纸 / 字 / 花纹 / 贴纸 / 排版全在 App 里定义,后端只存一段 ≤ 2048 字节的 style JSON 且不解释;日期由客户端给、库时钟兜底,格式错整条拒;地点是用户打的字(可带设备坐标,不做地理编码);配乐帖级只存歌 id,读时联表,歌没了帖级派生字段全空,但老日记 style 里的快照仍会被画、仍会试播;发帖没有幂等,先传图再写库再回读:传图中断留已传的几张、库事务失败留孤儿对象、提交成功后回读失败记录已在库里;编辑只改动过的字段、照片即点即存;一篇日记离开日记本本文核了三条路 —— 改可见性(kind 跟着翻成 moment)、拍成图片发一条新帖、导出 PDF(移本 / 移出本 / 删本另算);日记页只拉第一页 21 条、没有加载更多,搜索 / 按本 / 按月都在这些条里算,日历只拉时间点;App 端没有本地存档、没有草稿、没有离线。 |
updated |
2026-09-08 |
verified_ref |
f4c7da310cbf746a7c9369baa62e016c9d926ecb |
source_ref |
[backend/internal/domain/post/{service,repository,handler,notebooks,mentions,model}.go, backend/migrations/000022 000270 000277 000279 000291, app/lib/screens/diary_.dart, app/lib/util/{diary_style,diary_stickers,diary_open,diary_share,diary_export,diary_months}.dart, app/lib/widgets/diary_.dart, app/lib/widgets/music_pick.dart, app/lib/api/diary_music_api.dart, app/lib/services/place_service.dart(只引)] |
status |
current |
review_after |
2026-12-07 |
owner |
nova |
supersedes |
[无旧图纸(蓝图 §5.5 标「没有图纸,只有 CHANGELOG → 新写」);TASKS.md 274-289 与 JOURNAL.md 252-254 那段 6 月的「独白样式 + FAB」日记描述已作废;DESIGN_SYSTEM_EXAMPLES §4.5 引的 diary_screen.dart FAB 是死屏] |
这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:
[code@f4c7da31 文件:行]= 那棵树上的实现;[prod@日期,谁核]= 生产机当天核过;[decision@日期,谁]= 拍板过的决定。代码证明「那一版怎么做的」,证明不了「线上是这样」和「应该是这样」。 边界:帖子系统本体(朋友圈 / 广场 / 点赞评论 / 受众)归 post;播放器和自动播放策略归 music,本文只写日记怎么挂歌、怎么放;地点服务place_service是「我现在在哪」的唯一出处,发动态也用它(蓝图写「只在这儿用」不准确),本文只写日记存什么;备忘录(HOOP SPACE Notes)是另一张表,明写「不复用 posts」。两位提需求的人:Lisa(lisagao)和李敏是两个人,CHANGELOG 08-24 那六条曾把 Lisa 写成李敏、后来自己更正过,本文按更正后的算。
0. 给接手的人:三分钟读懂日记
先认五个词:日记(entry) = posts 表一行,visibility='private',kind='diary' 是后端从可见性算的,客户端传不了 kind;日记本(notebook) = diary_notebooks 一行,一篇最多属一本,删本不删篇;纸与字(style) = 一段 ≤ 2048 字节的 JSON(纸、花纹、笔、墨、排版、字号、正文位置、照片位置、贴纸、配乐 id),后端只存不解释,画的人是 App;配乐 = posts.music_track_id 一个歌 id(老客户端只写在 style 里),标题 / 封面读时联 songs;三扇门 = 本文核过的三条离开日记本的路(移本 / 移出本 / 删本不在此列):改可见性(kind 翻成 moment)、拍成图片发一条新帖、导出 PDF。
它保证到什么程度:
1. 私密是硬的:canView 对 private 一律拒非作者(读、看图、转发进聊天都过它);日记不能被转发(not_repostable);@ 到的人不会因为日记收到通知(通知前再过一次 canView)。但改可见性那一刻,已存的坐标随新的受众范围可见,没人抹。
2. 写入不是原子的,也没有幂等,失败分三种:先逐张把图传到 R2、再一个事务写 posts + post_media、再回读 Attach 回包。传图中断 = 已传的几张留在 R2、没日记;事务失败 = 图全成了孤儿对象(注释说「清道夫收得走」,仓里没找到清道夫);提交成功后回读失败 = 接口回错但日记已在库里;超时重试 = 可能重复,看断在哪一步。日期格式错 = 整条 400(不静默存成今天)。
3. 日期是用户的,不是服务器的:created_at 客户端给、库 now() 兜底;编辑时不许改到明天以后(24 小时余量给时区),建帖时不判;没有 edited_at,改过日期日历上的点就跟着走。
4. 日记本删了篇还在:外键 ON DELETE SET NULL;同一人本名不重复(索引);≤ 30 本(先数再插,不是原子上限,并发可越)、名 ≤ 40 字。移本只查「帖是你的且本是你的」,不查 kind 也不查 deleted_at,所以能改自己的动态或软删篇的归本;本子计数排除软删篇,但不排除未删的动态。
5. 换新默认不改老日记的样式解释:新默认(Stone 纸、Poppins 字)只挂在写作页的初始样式上,DiaryStyle 构造默认值(ivory / serif)不动,因为它同时是「style 空或坏时怎么解释」;花纹 / 字体 / 贴纸只往后追加、不插入。渲染器和字体本身跟当前 App 版本走(阅读页还会把 none 花纹补成横线),不是「永远长当年的样子」。
6. 配乐跟着歌走,但老快照还在:帖级只存 id,歌改名标题跟着变;歌没了 Attach 把四个派生字段和帖级 id 清空、日记照常读,可 style 里的快照不清:阅读页帖级标题为空就回落 style 的标题和 id 去造播放对象、卡片按 style.hasMusic(只看 id)画歌行 —— 旧标题仍可能显示、仍会试播,能不能出声看音乐端点。读时 COALESCE(music_track_id, style->>'music_id') 兼容老客户端,三处查询共用。改歌没有通道:帖级已有 A 时,重写 style 成 B 或清空都盖不过 COALESCE 里的 A,编辑接口也没有清 A 的字段;只有帖级为空的老记录靠改 style 生效。
7. App 端没有存档:每次打开重拉 GET /v1/posts/mine?kind=diary,没有快照、没有草稿、没有离线,离开写作页内容就没了。
当前已核实没做的(§4 全表,先说四条):按本拉篇的接口(App 只拉第一页 21 条、不传 before、不循环,再本地过滤,超出的篇看不见);后端搜索(App 只在拿到的 ≤ 21 条里搜正文和地点);改歌的通道(帖级已有 id 的记录改不了);R2 孤儿对象的清道夫(仓里没有)。
出了问题先查哪:§5 —— 「日记发到朋友圈了」看可见性是不是被改过;「照片没了」分软删和对象;「日历没点」看时区和 400 天窗;「配乐不响」看 id 在哪一列、歌在不在。
1. 两段流程的一生
1.1 写一篇,再读到它
你看到的:「我的」页 ➕ 或 HOOP SPACE 抽屉「写日记」→ 一张纸:顶上一行日期 / 时间 / 地点,正文,最多 4 张照片,底栏一排按钮(照片、字体、颜色、纸、花纹、链接、音乐、贴纸)→ 点保存先进预览,能拖正文和照片、改宽度、贴贴纸 → 存好回到日记本,列表一行一篇(统一一支笔),点进去一页一篇像信纸,翻页换日,挂了歌的自动响。
背后发生的:
1. 写:写作页初始样式 stone 纸 + poppins 字;日期只取一次 DateTime.now(),改日期只动年月日、时分保留;地点是一个普通文本框,不问权限、不定位、不存坐标(定位那套只有发动态在用);照片 ≤ 4 张,改过的按 PNG 原样传、没改的压到 1600 / q82;配乐走三处共用的选歌面板(GET /v1/music/browse → /browse/{slug}),选中后写两处:style 里三个字段(离线快照)+ 帖级 music_track_id(后端解析 ver 和封面用)。能存 = 正文非空或有图。没有草稿、没有自动保存。
2. 预览:同一个卡片组件画,不是仿制品;预览不落盘,只把改过的样式带回来;退出也保留挪过的位置。
3. 存 POST /v1/posts(multipart ≤ 100 MB):后端不读 kind 也不读 notebook_id;visibility='private' → kindFor 算出 diary;非日记的 style 直接丢;空串写 NULL;created_at 用 COALESCE($14, now()),坏格式 400;地点名 ≤ 120 字截断,坐标不成对或越界就丢坐标留名字;music_track_id 先查 SongPublished,不在 / 未发布 / 查询报错就静默清掉帖级 id、style 原样保留、帖照发(之后 Attach 仍按 style 回落)。顺序:先逐张 PutObject(posts/{postID}/{mediaID}{ext} + 缩略图)→ 再一个事务 INSERT posts + post_media → 通知(@ 到的人和订阅者,各再过一次 canView,私密帖到不了别人,失败只记日志)→ 回读 Attach 回包。回读失败时记录已提交、通知调用也已发生。
4. 列表:GET /v1/posts/mine?kind=diary,created_at DESC LIMIT 21 游标翻页(无总数);App 只请求第一页(不传 before、不循环、没有加载更多),不重排;按本、按月、搜索都在这最多 21 条里做;三种空态分开画(拉失败 / 从没写过 / 这个月没有)。
5. 读:整个当前列表当一叠传进阅读页,垂直 PageView 一页一篇;顶栏只剩日历 + ⋯(编辑 / 导出 / 分享收进 ⋯),日历只亮这叠里有的日子;配乐复用广场的 PostMusicAutoplay,帖级歌优先、老日记回落 style 三字段(没 ver 没封面所以不缓存),翻页先停上一首再起下一首,只有这一屏把浮窗留着(hideMiniBar: false,不推翻 Mike 08-28「自动播但不展示播放器」那条,那条说的是流)。
6. 改:PATCH /v1/posts/{id} 只发改过的字段(nil = 不动,否则会重写 created_at);照片即点即存(加 POST …/media、删 DELETE …/media/{mid}),文字 / 日期 / 地点 / 样式一起在「保存」;删照片只删库行不删对象;贴纸在编辑页只画不可拖;「排版」按钮不要求改过字。
哪一步会断、留下什么:第 1 步离开页面 = 全没了;第 3 步传图中断 = 已传的几张留在 R2、没日记;图全传了库没写 = 孤儿对象、没日记;库写成回读失败 = 接口报错但日记已在、通知调用也已发生;超时重试 = 可能重复,看断在哪步;music_track_id 不合法 = 帖发了、帖级没歌、style 快照还在、无提示;日期坏 = 整条 400;第 4 步超出第一页 21 条的篇 = 列表、按本、搜索都看不到它;第 5 步歌没了 = 帖级不画,老日记的 style 快照仍可能画并试播;第 6 步照片存了、文字没存 = 两条路本来就分开,照片留着。
出处:kind 派生
[code@f4c7da31 backend/internal/domain/post/service.go:529],建帖service.go:89-211(style 只给日记:139-141,图 ≤ 4:132,正文 5000 截断:118-120,歌静默清:161-170,通知:208-211),地点消毒service.go:50-71,先传图后写库service.go:471-497;仓储[code@f4c7da31 backend/internal/domain/post/repository.go:63-146](空串写 NULL:73-106,库时钟:109-111),列表:189-257,编辑:1542-1563(无edited_at:1561-1562),删:1527,删图不删对象:1618-1623;处理器[code@f4c7da31 backend/internal/domain/post/handler.go:29,119-153,214-313,365-372,504-514,807-834];表[code@f4c7da31 backend/migrations/000022_posts.up.sql:1-11]、[code@f4c7da31 backend/migrations/000270_posts_style.up.sql:12]、[code@f4c7da31 backend/migrations/000277_post_music.up.sql:9-19]、[code@f4c7da31 backend/migrations/000279_post_place.up.sql:9-24];App 写作[code@f4c7da31 app/lib/screens/diary_compose_screen.dart:76-96,256-294,311-389],元信息行[code@f4c7da31 app/lib/widgets/diary_meta_line.dart:56-138],预览[code@f4c7da31 app/lib/screens/diary_preview_screen.dart:22-23,159,181-183],列表[code@f4c7da31 app/lib/screens/diary_v2_screen.dart:57-117,404-423,549-592,655-685],阅读[code@f4c7da31 app/lib/screens/diary_entry_screen.dart:60-88,166-207,223-246,269-295,475-533],编辑[code@f4c7da31 app/lib/screens/diary_edit_screen.dart:33-39,121-125,184-212,235,280,474-498],选歌[code@f4c7da31 app/lib/widgets/music_pick.dart:24-43,252-257]、[code@f4c7da31 app/lib/api/diary_music_api.dart:8-45];守卫post/created_at_db_test.go、place_db_test.go:178、music_db_test.go:82、music_style_fallback_db_test.go、app/test/diary_compose_date_place_0906_test.dart、diary_edit_test.dart、diary_music_plays_0907_test.dart、diary_header_calendar_0908_test.dart[decision@2026-08-21,Jeff:看完一篇往前滑就是前一天,不用退出来][decision@2026-08-25,Lisa:一篇 = 一整页信纸;列表统一一支笔][decision@2026-08-26,Jeff:六件(改 / 长按 / 删 / 搜 / 封面 / 照片即存)][decision@2026-09-03,Lisa:预览画真组件、不落盘;正文可整块挪、锚在左上][decision@2026-09-06,李敏:写作页要先定日期时间、可加地点][decision@2026-09-07,李敏:默认 Stone 纸 + Poppins;老日记不动][decision@2026-09-07,李敏:读日记时配乐要响;可以留浮窗只留唱片][decision@2026-09-08,李敏:顶栏改日历,只亮有日记的天;导出分享收进 ⋯]。
1.2 一篇日记离开日记本:本文核过的三条路
你看到的:① 帖子的「⋯ → 谁能看」把一篇日记改成朋友可见,它就从日记本消失、出现在朋友圈;反过来把一条动态改成「只有自己」,它进日记本。② 阅读页「⋯ → 分享」选朋友或全网,把这一页拍成图片发成一条新帖,日记本身还在、还是私密。③ 「⋯ → 导出」把这一页拍成图片装进 PDF,交给系统分享。
背后发生的:
1. 改可见性 PATCH /v1/posts/{id}/visibility:同一条 UPDATE 里 kind 跟着翻(private ↔ diary,其余 ↔ moment);目标不是 friends 时同一事务清受众和排除名单(改成 private 也清,private → friends 不清);style 不清(建帖时非日记会丢 style,这条路不丢,留下一段没人画的死数据);坐标不清,已存的坐标随新受众范围可见。「只能放宽到原帖上限」只管转发到 network 的根帖检查,自己原创的日记三档随便选。改完再回读,回读失败不代表没改。日记不能转发:repost_of 指向 private 帖 → 403 not_repostable;转发进聊天非作者也拒。
2. 拍图发新帖:captureDiaryCard 把当前页的 RepaintBoundary 按 3 倍像素拍成 PNG → createPost(visibility: friends|network, body:'', images:[png]) 一条新 moment;原日记不动。分享和导出故意是两颗按钮(Lisa 08-25:合成一颗她想存手机就得先发朋友圈)。
3. 导出 PDF:同一张图 → buildDiaryPdf(页面尺寸跟图不跟 A4)→ 写到临时目录 → SharePlus;只导当前这一页;文件名 HOOP Diary 2026-08-24.pdf。
4. 删除:DELETE /v1/posts/{id} 只是 deleted_at = now()(作者自己、未删过才 200,否则 404);没有硬删、没有恢复端点;已核的删除路径只改库 / 删关联行,不主动删对象,对象长期留存与仓外清理未核。移本、移出本、删本保留篇,也是离开某本的路,不在上面三条里。删是这一块唯一不可逆的动作,App 端必须确认并说后果。删日记本 DELETE /v1/diary/notebooks/{id} 是真删,回 entries_kept: true。
哪一步会断、留下什么:门① 改成朋友可见 = 坐标和 style 一起出去;门② 拍图失败 = 不发请求;发帖失败要分上传前 / 途中失败、库事务失败、提交后回读或回包失败 —— 可能无帖、可能留孤儿对象、也可能新帖已存在,不能只凭报错判断(同 §1.1);门③ 写临时目录失败 = 没文件;删 = 行还在库里,只软删。
出处:可见性翻 kind
[code@f4c7da31 backend/internal/domain/post/repository.go:1327-1359],canViewservice.go:396-412,图也过canViewservice.go:993,1011,不能转发handler.go:869-873,转发进聊天[code@f4c7da31 backend/internal/domain/message/service.go:1592];拍图[code@f4c7da31 app/lib/util/diary_share.dart:17-18],分享diary_entry_screen.dart:341-390,398-400,导出[code@f4c7da31 app/lib/util/diary_export.dart:23-69];删本[code@f4c7da31 backend/internal/domain/post/notebooks.go:113-119,283-290];守卫post/visibility_db_test.go:26,56、notebook_db_test.go:103、app/test/diary_share_test.dart[decision@2026-08-2x,作者:定向发的帖一律不许转,日记在内][decision@2026-08-24,Lisa:删本不删篇,这句话不说清没人敢按][decision@2026-08-25,Lisa:分享是发新帖,导出是拿出 App,两颗按钮][decision@2026-08-26,Jeff:删除要确认并说后果][decision@2026-08-27,Tora4:改可见性 kind 跟着翻,判据抽成 kindFor 和发帖共用]。
2. 用户能看到的能力(怎么用 / 限制 / 入口)
| 能力 | 怎么用 / 限制 | 后端入口 | App 落点 |
|---|---|---|---|
| 写日记 | 正文 ≤ 5000 字(截断)、≤ 4 图或 1 视频;日期不晚于明天(只在编辑时判);没草稿 | POST /v1/posts |
diary_compose_screen(入口:我的 ➕、HOOP SPACE 抽屉、日记本 ➕) |
| 纸 / 字 / 花纹 / 墨 / 排版 / 字号 | 9 纸(7 浅 2 深)、28 笔(12 支中文优先)、13 花纹、6 墨 + 跟纸、3 排版、字号 0.8–1.6;全 App 端 | style 列 ≤ 2048 B | diary_style_bar、diary_format_sheet |
| 贴纸 | 24 种矢量(3 动),每页 ≤ 16(2048 字节算出来的);动画只在写 / 预览 / 单篇,列表不动 | 同上 | diary_sticker_layer |
| 预览与排版 | 拖正文、拖照片、宽度滑杆(0.3–1.0,没挪过时灰);预览不落盘 | — | diary_preview_screen |
| 地点 | 手打文字;不定位不存坐标(发动态那条路才有坐标) | place_name |
diary_meta_line |
| 配乐 | 从 HOOP Music 分类里选(三处共用面板);读时自动播、留浮窗 | music_track_id、GET /v1/music/browse* |
music_pick、diary_entry_screen |
| 日记本 | ≤ 30 本(先数再插,非原子)、名 ≤ 40 字、同名不许;封面从自己所有日记照片里挑(存 hoop:media:{id} 不存 URL);删本不删篇;一篇只属一本 |
/v1/diary/notebooks*、PUT /v1/posts/{id}/notebook |
diary_v2_screen、diary_notebook_row |
| 列表 / 网格 / 按月 / 搜索 | 后端 21 条一页游标,当前 App 只拉第一页、没有加载更多;按本、按月、搜正文和地点全在这些条里;网格瓦片不带 ❤💬↗ | GET /v1/posts/mine?kind=diary |
diary_v2_screen |
| 日历 | 有日记的天亮绿点(她说的是「red dot or anyways」,例子不是要求);写作页日历 2000 年起到今天 | GET /v1/posts/diary/dates?from=&to=(回时间点,≤ 400 天) |
diary_date_picker |
| 阅读 | 一页一篇垂直翻;顶栏日历 + ⋯;单篇时日历禁用 | GET /v1/posts/{id} |
diary_entry_screen |
| 编辑 | 文字 / 日期 / 地点 / 样式一次 PATCH;照片即点即存;B I U S 与聊天同一套标记 | PATCH /v1/posts/{id}、…/media |
diary_edit_screen |
| 三扇门 | 改可见性 / 拍图发朋友圈 / 导出 PDF | PATCH …/visibility、POST /v1/posts、— |
diary_entry_screen ⋯ 菜单 |
| 删除 | 软删,不可恢复;确认并说后果 | DELETE /v1/posts/{id} |
diary_open(confirmAndDeleteDiary) |
| 「我的」页日记格 | 预览 8 条;格子是 DiaryGridTile 不是帖子瓦片;这张卡是 DiaryV2Screen 全 App 唯一入口 |
— | profile_screen |
出处:路由
[code@f4c7da31 backend/internal/domain/post/handler.go:31-106]、notebooks.go:223-316;上限service.go:118-132、handler.go:266-274、notebooks.go:34-38;App 目录[code@f4c7da31 app/lib/util/diary_style.dart:21-39,46-95,148-182,274,298-302,382-384,456-474]、[code@f4c7da31 app/lib/util/diary_stickers.dart:8-38,74-139];入口[code@f4c7da31 app/lib/screens/profile_screen.dart:76,1971-1988,2080-2082,2137,2174]、[code@f4c7da31 app/lib/screens/home_screen.dart:1786-1789]、[code@f4c7da31 app/lib/widgets/hoop_space_drawer.dart:322-327];封面存法diary_v2_screen.dart:220-222,290,324,339-341;日历[code@f4c7da31 app/lib/widgets/diary_date_picker.dart:44,110-135,261,305-315]。
3. 内部怎么运作
3.1 一张表加一个筐:日记就是私密的帖
怎么做的:posts.kind CHECK ('moment','diary'),由 kindFor(visibility, repostOf) 算:非转发且 private → diary;visibility CHECK ('private','friends','network');日记从 Feed(只取 friends)和 Plaza(只取 network)天然排除。日记本 diary_notebooks(id, owner_id, name, cover_url, sort, created_at, updated_at),posts.notebook_id UUID … ON DELETE SET NULL,索引 (owner_id, lower(name)) 唯一、(owner_id, sort, created_at)、idx_posts_notebook(notebook_id, created_at DESC) WHERE notebook_id IS NOT NULL(ListNotebooks 按 notebook_id 关联计数能匹配它,真选不选看执行计划,未核;按本拉篇的接口没做)。30 本是先 Count 再 Insert,不是原子上限。updated_at 只写不读;cover_url 只 TrimSpace 不校验。备忘录(000359)是另一张表,明写不复用 posts。
出处:
[code@f4c7da31 backend/migrations/000022_posts.up.sql:6-11]、[code@f4c7da31 backend/migrations/000023_post_visibility_network.up.sql:4]、[code@f4c7da31 backend/migrations/000291_diary_notebooks.up.sql:7-40];kindForservice.go:529;Feed / Plaza 过滤repository.go:166,363;日记本仓储notebooks.go:21-51,98,139-152,175-179,190-210;备忘录不复用[code@f4c7da31 backend/internal/domain/note/model.go:6];守卫notebook_db_test.go:45-164、diary_dates_db_test.go:38-105。
3.2 纸与字:后端一段 JSON,App 一张目录
用户看到的:换纸整页立刻变、换字整页立刻变,没有「预览卡」;老日记的 style 按当年的解释画,渲染器是现在的。
怎么做的:style 编码键 paper, pattern, font, ink, layout, fs(字号,1.0 不写)、tp(正文位置)、pp / pw(照片位置、宽度)、stk(贴纸,坐标 3 位小数)、music_id/title/artist;解码器全部不抛,坏 / 空 → 默认(ivory / serif);目录只追加不插入(她认熟的位置不能推走);Poppins 加在枚举末尾(有测试按下标取第 4 个);中文字体的判据是中文主字族,不是拉丁字族带回落;Flutter 字体回落是静默的,「加了字体看不出差别」不会有红灯,12 支中文字里 11 支没在真机上见过。贴纸上限 16 是从 2048 字节算的,不是审美挑的。列表层统一一支笔(kDiaryListPen),每篇自己的笔只在阅读页 —— 这条推翻了 Jeff 08-21「列表跟每篇的笔」,老测试红了是「指令不是故障」。弹窗配色跟着纸走(DiaryPaper.isDark),不跟主题。
出处:
[code@f4c7da31 app/lib/util/diary_style.dart:90-95,148-153,178-182,253-258,298-302,370-373,456-474,491-560];贴纸diary_stickers.dart:74-93,117,139;新默认只在写作页diary_compose_screen.dart:76-81;列表一支笔diary_v2_screen.dart:621-649;弹窗跟纸docs/POPUP_GLASS_MIGRATION.mdB6;守卫app/test/diary_sticker_test.dart、diary_format_sheet_0904_test.dart、diary_move_all_0904_test.dart、diary_collage_move_0908_test.dart、diary_v2_golden_test.dart[decision@2026-08-24,Lisa:韩式信纸 + 贴照片,倾斜角是固定表不是随机;贴纸矢量不 PNG][decision@2026-08-25,Lisa:列表统一一支笔,推翻 Jeff 08-21][decision@2026-09-03,Lisa:花纹 7→13、字体 5→10,只追加不插入][decision@2026-09-04,Lisa:贴纸 8→24 含 3 动;中文字要中文主字族][decision@2026-09-04,李敏:排版面板照 Apple 那套;对齐按钮故意不画]。
3.3 日期、地点、配乐:三样挂件各自的存法
怎么做的:日期 —— App 一次取 now()、日历改年月日保留时分、时间另一个系统选择器;后端 COALESCE($14::timestamptz, now()),坏格式整条 400;编辑不许晚于 now()+24h,建帖不判(代码注释声称两边一样,实际不一样);日历端点回 时间点 不回日期(哪一天只有手机时区知道),窗 ≤ 400 天、≤ 1000 行,空月回 []。地点 —— 日记页是文本框,只存 place_name;place_lat/lng 列存在(发动态那条路会存设备低精度定位),消毒只在新建:名空则坐标丢、坐标不成对或越界丢坐标留名、名 ≤ 120 字;编辑 PATCH 把地点三字段直接 COALESCE 进库,没有截断 / 成对 / 越界检查,也没法传 null 清已有坐标;没索引、没查询、没粗化;改可见性时不抹。配乐 —— 建帖验 SongPublished 否则静默清;读时 musicTrackExpr = COALESCE(NULLIF(p.music_track_id,''), p.style->>'music_id') 用在 Attach / BySong / SongUseCount 三处,少一处界面就自相矛盾;派生的标题 / 艺人 / 封面 / ver 读时另一条查询联 songs,歌没了四个都空且帖级 id 也清,style 不清(阅读页和卡片会回落 style,见 §0 第 6 条);没有改歌的编辑字段:帖级为空的老记录靠重写 style 让回落路捡起来,帖级已有 id 的改不了;这条回落要留到老客户端退役。
出处:日期
service.go:193-194,259-265,512、repository.go:109-111,449-467、handler.go:301-313,395-410;地点service.go:50-71、[code@f4c7da31 backend/internal/domain/post/model.go:144-157]、[code@f4c7da31 app/lib/services/place_service.dart:4-19,63](发动态用);配乐repository.go:25-49,153,402,438,821-861、model.go:122-139、service.go:161-170;App 两处写diary_compose_screen.dart:378-383,读取优先级diary_entry_screen.dart:60-88;守卫created_at_db_test.go:34,60、diary_dates_db_test.go:76、place_db_test.go:90,178、music_style_fallback_db_test.go:81-180[decision@2026-08-26,作者:日记日期不晚于明天,24 小时给时区][decision@2026-09-07,李敏:「There's still no music」→ 根因歌只在 style 里,修在 SQL 源头][decision@2026-09-07,李敏:日历点绿不红,「red dot or anyways」是例子不是要求]。
3.4 App 端:一个入口、无存档、三种空态
怎么做的:DiaryV2Screen 全 App 只从「我的」页那张卡进(有守卫真去点);旧 DiaryScreen 是死屏(没人 push,只有两条测试还引它);列表每次进只拉第一页(≤ 21 条)、没有加载更多,没有 PageSnapshot、没有 Hive、没有退避,只有手动重试和下拉刷新;日记本拉失败故意吞掉,篇照样画;错误一律 friendlyError + toast,notebook_name_taken / notebook_limit 有翻译;解码器都不抛;日历点按月缓存、失败把月标记拿掉好重试;选歌面板三种空分开(拉失败 / 分类空 / 没搜到),接口失败回 null 不回假空表;没有 WS 分支,日记全是 HTTP。
出处:
diary_v2_screen.dart:72-117,150-155,553-608;唯一入口守卫app/test/diary_entry_reachable_test.dart:1-4;死屏[code@f4c7da31 app/lib/screens/diary_screen.dart:29-32,100-104];日历缓存diary_date_picker.dart:110-135;选歌三种空music_pick.dart:252-257。
4. 已知缺口与待核事项(逐行看状态)
| 项 | 状态 | 出处 |
|---|---|---|
| 按本拉篇的接口 / 加载更多 | 没做:App 只请求第一页(21 条,不传 before、不循环),按本 / 按月 / 搜索都在这些条里;超出的篇看不见 | diary_v2_screen.dart:94-107,407、api_client.dart:3152-3159 |
| 后端搜索 | 没做:App 只在拿到的 ≤ 21 条里搜正文 + 地点;记录了「一次拉不完了」再改后端 | diary_v2_screen.dart:410-415 |
| 改配乐 | 没做:EditFields 无 music 字段;帖级已有 id 的记录改不了(style 盖不过 COALESCE),帖级为空的靠重写 style |
repository.go:49,1542 |
| 歌没了旧快照仍画仍试播 | 现状:Attach 只清帖级派生字段不清 style;阅读页回落 style、卡片按 style.hasMusic |
repository.go:832-860、diary_entry_screen.dart:62-86、diary_entry_card.dart:123 |
| 提交成功后回读 / 回包失败 | 现状:Create 写库后再 Attach,SetVisibility 改完再 Get;回错不代表没写 |
service.go:201-219,576-582 |
| 编辑地点不校验 | 现状:PATCH 直接 COALESCE 进库,没有截断 / 成对 / 越界,也清不掉坐标 | handler.go:119-164、repository.go:1563-1578 |
| style 反序列化没查 nil | 现状:JSON null 也过 2048 字节 + map 那道闸 |
handler.go:140-148,266-274 |
| 30 本非原子 | 现状:先 Count 再 Insert,并发可越 | notebooks.go:172-197 |
| 发帖幂等 / 限流 | 没做:超时重试可能重复,看断在哪步 | service.go:89 |
| R2 孤儿对象清道夫 | 仓里没有;注释假设有;R2 生命周期规则在不在仓外未核 | service.go:471-473、repository.go:1618-1623 |
| 硬删 / 恢复 | 没做:只有 deleted_at;已核路径不主动删对象,留存与仓外清理未核 |
repository.go:1527,1618-1623 |
| 改可见性不抹坐标、不清 style | 现状:已存坐标随新受众可见;style 留成死数据;清受众的条件是目标 ≠ friends | repository.go:1327-1359 |
| 建帖不判未来日期 | 现状:只有编辑判;注释说两边一样 | service.go:193-194,512 |
| 移本不查 kind、不查 deleted_at | 现状:动态、软删篇都能改归本;计数排除软删(:51 有 deleted_at IS NULL),不排除动态 |
notebooks.go:51,139-146 |
cover_url 不校验、updated_at 只写不读 |
现状 | notebooks.go:50,98,190,210 |
| App 端存档 / 草稿 / 离线 | 没做 | diary_v2_screen.dart:72-88 |
| 安卓 / 网页版真机验字体与配乐 | 待核:12 支中文字 11 支没见过真机;配乐测试里播放器是 null,「真听到声」只能真机 | CHANGELOG v1.787 / v1.891 |
| 对齐按钮 | 故意不画,等有段落属性模型 | CHANGELOG v1.817 |
| 日记本顶栏那颗唱片 | 李敏划掉了;那是全局播放器,等她一句话 | CHANGELOG v1.709 / WIP:1582 |
| 桌面底色(v1.077 ③)、温柔版(v1.282)、编辑页重画(v1.755) | 各等 Lisa / Lisa / Mike 看图点头,没记录到回复 | CHANGELOG |
| 「我的」页本子行 72×96 vs 日记本页 104×138 | 故意不同,要一样说一声 | CHANGELOG v1.787 ④ |
死屏 diary_screen.dart 与它的快照键、DESIGN_SYSTEM_EXAMPLES §4.5 引它 |
待清理 | diary_screen.dart、docs/DESIGN_SYSTEM_EXAMPLES.md:189 |
16 个无人调用的翻译键(含说反了的 diary_edit_photos_note) |
待清理 | app/assets/translations/en.json |
| CHANGELOG v1.195 整段重复两遍 | 待清理 | CHANGELOG:25894 / 26001 |
| 000291 与 09-07 之后的读路径改动是否已上生产库 | 未核:LEDGER 的 schema_migrations 抽查停在 270 |
backend/migrations/LEDGER.md |
| 别人主页不显示日记格(TASKS 那条) | 未核是否已做 | TASKS.md:289 |
5. 出问题先查哪里 · 动它之前
排障一条路:
1. 日记跑到朋友圈了 / 动态跑进日记本了:看 posts.visibility,kind 是它算出来的;PATCH …/visibility 是唯一会翻的路;日记本里出现动态 = 移本没查 kind。
2. 照片没了:行 deleted_at 非空 = 软删;post_media 行没了但对象还在 = 删图只删行;有对象没行 = 传图中断或写库失败的孤儿;接口报错但行在 = 回读失败。
3. 日历没点 / 点在隔壁天:端点回的是时间点,天由手机时区算;窗 > 400 天 400;改过 created_at 点会挪。
4. 配乐不响:先看 music_track_id 列还是 style->>'music_id'(老客户端只写后者,读时 COALESCE);再看歌 status='published' 且没被删(没了帖级全空,但 style 快照仍可能画、仍试播);帖级已有 id 时改 style 不生效;测试里播放器是 null,只能真机听。
5. 写着写着没了:没有草稿,离开就丢;不是 bug 是现状。
6. 篇看不见:App 只拉第一页 21 条,没有加载更多;按本 / 按月 / 搜索都在这 21 条里。
7. 发了两篇一样的:超时重试可能重复,没有幂等;先看断在传图、写库还是回读。
改它之前:
- 动 style:后端只管 ≤ 2048 字节 + 能反序列化成 map(没查 nil,JSON null 也过);目录只追加不插入;新默认只挂写作页初始样式,构造默认值(ivory / serif)不动;贴纸上限跟字节走。
- 动配乐:musicTrackExpr 三处一起改;回落留到老客户端退役;阅读页回落 style、卡片按 style.hasMusic,和后端那三处口径要一起动;要做改歌先给编辑加清帖级 id 的字段。
- 动日期 / 地点:库时钟兜底,格式错整条拒;日历回时间点不回日期;地点消毒只在新建,编辑那条要补。
- 动可见性:kindFor 和发帖共用;放宽时想抹坐标 / 清 style 要在同一条 UPDATE 里做。
- 动日记本:删本必须 SET NULL;移本如果要查 kind,先想清动态被塞进本子的存量。
- 动阅读页顶栏:两条守卫成对(「顶栏只剩两颗」+「三件事都还在 ⋯ 里」),不然最便宜的修法是把导出删掉。
- 动浮窗:hideMiniBar 默认 true 不动,只有日记阅读页传 false;别顺手翻默认。
- 别信的旧记录:TASKS 274-289 / JOURNAL 252-254 是 6 月的独白样式 + FAB 版(已死);DESIGN_SYSTEM_EXAMPLES 的日记 FAB 是死屏;蓝图「地点服务只在这儿用」不准;CHANGELOG 08-24 六条署名 Lisa 不是李敏。
6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)
🔒 = Jeff / Lisa / 李敏明确锁定的;其余是普通配置值,改之前把守卫一起改。
| 数 | 当前值 | 实现出处(f4c7da31) | 守卫 / 未覆盖 |
|---|---|---|---|
| style 上限 🔒 | 2048 字节且能反序列化成 map(JSON null 也过) | handler.go:266-274,140-148 |
App 侧 diary_sticker_test.dart;后端未覆盖 |
| 贴纸上限 🔒 | 16 / 页;缩放 0.06–0.42;坐标 3 位小数 | diary_stickers.dart:74,93,117,139 |
diary_sticker_test.dart |
| 图 / 视频 | ≤ 4 图或 1 视频;图 15 MB、视频 80 MB、缩略 2 MB、multipart 100 MB | service.go:132、handler.go:29,279-294 |
未覆盖 |
| 正文 | 5000 字截断 | service.go:118-120 |
未覆盖 |
| 地点名 | 新建时 120 字截断、越界丢;编辑不校验 | service.go:52-71 |
place_db_test.go:178 |
| 日期 | 编辑不晚于 now+24h;日历窗 400 天 / 1000 行 | service.go:263,512、repository.go:467 |
未覆盖 |
| 日记本 | 30 本(先数再插,非原子);名 40 字;同名唯一 | notebooks.go:34-38、000291:33 |
唯一索引有;上限未覆盖 |
| 分页 | 21 / 页(kind)、31 / 页(filter) | repository.go:189-257 |
未覆盖 |
| 字号 | 0.8–1.6,步 0.1,1.0 不写 | diary_style.dart:298-302 |
diary_format_sheet_0904_test.dart |
| 正文 / 照片宽 | 默认 0.78 / 0.62,夹 0.3–1.0 | diary_style.dart:382-384,544 |
diary_collage_move_0908_test.dart |
| 目录 🔒 | 9 纸 / 28 笔 / 13 花纹 / 6 墨 / 3 排版 / 24 贴纸;只追加 | diary_style.dart:21,46,153,178,274、diary_stickers.dart:8 |
diary_compose_date_place_0906_test.dart:195-245(顺序与默认) |
| 新默认 🔒 | 写作页 stone + poppins;构造默认 ivory + serif 不动 | diary_compose_screen.dart:80-81、diary_style.dart:370-373 |
diary_compose_date_place_0906_test.dart:177-236 |
| 列表 | 行高 96;预览 42 / 60 字;本子 104×138(「我的」页 72) | diary_entry_row.dart:52、diary_style.dart:607、diary_notebook_row.dart:58-68 |
goldens |
| 压图 | 1600 / q82;改过的 PNG 原样 | diary_compose_screen.dart:350-363 |
未覆盖 |
| 拍图 | pixelRatio 3.0 | diary_share.dart:18 |
只测「拍到了」 |
| 「我的」页预览 | 8 条 | profile_screen.dart:76 |
profile_diary_tab_test.dart |
7. 出处与旧记录去向
- 代码(全部
[code@f4c7da31]):backend/internal/domain/post/{service,repository,handler,notebooks,mentions,model}.go+ 9 个_db_test.go(要真 Postgres,没有就 Skip,「Skip 不是绿」);backend/internal/domain/message/service.go(转发闸);Applib/screens/{diary_v2_screen,diary_compose_screen,diary_edit_screen,diary_entry_screen,diary_preview_screen}.dart(diary_screen.dart已死)、lib/util/{diary_style,diary_stickers,diary_open,diary_share,diary_export,diary_months}.dart、lib/widgets/diary_*.dart(14 个)、lib/widgets/music_pick.dart、lib/api/diary_music_api.dart、lib/models.dart(Post)、lib/models/diary_notebook.dart。 - 迁移:000022 posts 三张表、000023 可见性三值、000025 分享数、000191 受众、000269 置顶、000270 style、000275 转发、000276 @、000277 music_track_id、000279 place 三列、000291 diary_notebooks + notebook_id。本模组最新 000291(09-07 之后的配乐、日历都是读路径,没新迁移);仓库最新 000364。
- Redis:无。WS:无。Meilisearch:无。
- 生产:本文没有在生产机核任何项(000291 是否已跑、R2 是否有生命周期规则、真机字体与配乐)→
[prod@ 待核]。 - 决定(按更正后的署名):08-21 Jeff 翻页看前一天、列表跟每篇的笔(后被推翻);08-21 李敏 帖子要能加地点(卡 576df6e3);08-2x 作者 定向帖不许转;08-24 Lisa 日记格改本子 + 一行一篇、韩式信纸固定倾斜、贴纸矢量且 ≤ 16、删本不删篇、自动封面只挑浅纸(这六条 CHANGELOG 曾署李敏,08-24 Nova4 更正为 Lisa);08-24 李敏 每天一页要像自己的页;08-25 Lisa 列表一支笔、一篇一整页信纸、分享导出两颗按钮;08-26 Jeff 六件;08-26 作者 日期不晚于明天、照片即存、删图不删对象;08-27 Tora4 可见性翻 kind;08-27 Lisa 温柔版;08-28 李敏 侧栏书本图标;08-28 Mike 自动播不展示播放器;09-02 李敏 「我的」➕ 三项、格子不带 ❤💬↗、搜索常驻、B I U S 与聊天同一套、排版面板照 Apple;09-03 Lisa 花纹字体只追加、正文整块可挪、预览真组件不落盘;09-03 Mike 编辑页照新建页画;09-04 Lisa 贴纸 24、中文主字族、本子 104×138;09-04 李敏 字号只作用正文、对齐不画;09-06 李敏 先定日期时间可加地点;09-07 李敏 Stone + Poppins、老日记不动、读日记要有配乐、留浮窗只留唱片、日历绿点;09-08 李敏 照片可挪且在字下、一个滑杆、顶栏日历 + ⋯、「What more decision do you need from me?」→ 网页版不再等她点头。
- 未核清单:① 生产未核(见上);② R2 生命周期规则在不在仓外;③ 12 支中文字真机长什么样、配乐真机响不响;④ 三件等看图点头的(v1.077 ③ / v1.282 / v1.755)有没有回复;⑤ 别人主页不显示日记格是否已做;⑥
MoveToNotebook塞进动态 / 软删篇的存量有没有;⑦idx_posts_notebook执行计划里选不选;⑧ R2 对象是否长期留存。 - 旧记录去向:没有旧图纸可替代。
TASKS.md:274-289、JOURNAL.md:252-254那段 6 月的「独白样式 + FAB」描述作废(那是死屏diary_screen.dart);docs/DESIGN_SYSTEM_EXAMPLES.md§4.5 引的日记 FAB 已删(v1.709);docs/POPUP_GLASS_MIGRATION.mdB6「弹窗配色跟纸走」那条并入 §3.2;docs/备忘录设计蓝图.md:16「日记是 posts 里 kind=diary 的帖子,服务端硬控私有」那半是仓里最准的一句;「发了就定格」已不完整 —— 本文核到能改正文、日期、地点、可见性,那半句别再背书;蓝图 §5.5 那行的「待核」现在可以填:后端在post包,没有resource;「地点服务只在这儿用」要改成「发动态也用」;docs/00-目录.md从没登记过日记,本文登记后是第一条。