📚 diary · 模组知识

HOOP 系统设计图纸 · 正文唯一真相是 docs/diary.md;改 md 再跑 ./tools/deploy_docs.sh,这页是生成的

版本 ad1bdbb5
2026-09-09 00:34
档案
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. 私密是硬的:canViewprivate 一律拒非作者(读、看图、转发进聊天都过它);日记不能被转发(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_atCOALESCE($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.goplace_db_test.go:178music_db_test.go:82music_style_fallback_db_test.goapp/test/diary_compose_date_place_0906_test.dartdiary_edit_test.dartdiary_music_plays_0907_test.dartdiary_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],canView service.go:396-412,图也过 canView service.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,56notebook_db_test.go:103app/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_bardiary_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_idGET /v1/music/browse* music_pickdiary_entry_screen
日记本 ≤ 30 本(先数再插,非原子)、名 ≤ 40 字、同名不许;封面从自己所有日记照片里挑(存 hoop:media:{id} 不存 URL);删本不删篇;一篇只属一本 /v1/diary/notebooks*PUT /v1/posts/{id}/notebook diary_v2_screendiary_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 …/visibilityPOST /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-132handler.go:266-274notebooks.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];kindFor service.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-164diary_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.md B6;守卫 app/test/diary_sticker_test.dartdiary_format_sheet_0904_test.dartdiary_move_all_0904_test.dartdiary_collage_move_0908_test.dartdiary_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,512repository.go:109-111,449-467handler.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-861model.go:122-139service.go:161-170;App 两处写 diary_compose_screen.dart:378-383,读取优先级 diary_entry_screen.dart:60-88;守卫 created_at_db_test.go:34,60diary_dates_db_test.go:76place_db_test.go:90,178music_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,407api_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-860diary_entry_screen.dart:62-86diary_entry_card.dart:123
提交成功后回读 / 回包失败 现状:Create 写库后再 Attach,SetVisibility 改完再 Get;回错不代表没写 service.go:201-219,576-582
编辑地点不校验 现状:PATCH 直接 COALESCE 进库,没有截断 / 成对 / 越界,也清不掉坐标 handler.go:119-164repository.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-473repository.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 现状:动态、软删篇都能改归本;计数排除软删(:51deleted_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.dartdocs/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:132handler.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,512repository.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,274diary_stickers.dart:8 diary_compose_date_place_0906_test.dart:195-245(顺序与默认)
新默认 🔒 写作页 stone + poppins;构造默认 ivory + serif 不动 diary_compose_screen.dart:80-81diary_style.dart:370-373 diary_compose_date_place_0906_test.dart:177-236
列表 行高 96;预览 42 / 60 字;本子 104×138(「我的」页 72) diary_entry_row.dart:52diary_style.dart:607diary_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(转发闸);App lib/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}.dartlib/widgets/diary_*.dart(14 个)、lib/widgets/music_pick.dartlib/api/diary_music_api.dartlib/models.dart(Post)、lib/models/diary_notebook.dart
  • 迁移:000022 posts 三张表、000023 可见性三值、000025 分享数、000191 受众、000269 置顶、000270 style、000275 转发、000276 @、000277 music_track_id000279 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-289JOURNAL.md:252-254 那段 6 月的「独白样式 + FAB」描述作废(那是死屏 diary_screen.dart);docs/DESIGN_SYSTEM_EXAMPLES.md §4.5 引的日记 FAB 已删(v1.709);docs/POPUP_GLASS_MIGRATION.md B6「弹窗配色跟纸走」那条并入 §3.2;docs/备忘录设计蓝图.md:16「日记是 posts 里 kind=diary 的帖子,服务端硬控私有」那半是仓里最准的一句;「发了就定格」已不完整 —— 本文核到能改正文、日期、地点、可见性,那半句别再背书;蓝图 §5.5 那行的「待核」现在可以填:后端在 post 包,没有 resource;「地点服务只在这儿用」要改成「发动态也用」;docs/00-目录.md 从没登记过日记,本文登记后是第一条。