技术文档

新事的发布、可见性与媒体

新事是服务端保存的图文与短视频内容,使用独立于聊天消息的表、媒体配额和通知机制。它没有接入MLS;可见范围由每次读取时的当前关系决定。服务端支持媒体Range,但当前iOS与Android播放器路径都先完整下载后播放。

模型与响应字段

对象 字段与含义
Post id、authorId、text、visibility、createdAt、author、media、likeCount、commentCount、liked、recentComments;作者读selected帖子另有viewerIds
Media id、name、mimeType、kind=image/video、size、createdAt;原始字节另存BLOB
Comment id、postId、authorId、replyToId、text、createdAt、authorName
Notification id、actorId、actorName、postId、commentId、kind、createdAt、readAt;kind为like/comment/reply

帖子、评论、通知ID为各自的整数序列,媒体和用户为UUID;createdAt/readAt是Unix毫秒,readAt可null。完整表定义及模型组装见chat-service/src/moments-repository.mjs:8-71,149-164,204-231,277-301。

可见范围不是发布时冻结的好友快照

visibility 作者之外的读取规则
friends 当前仍是好友、没有任一方向屏蔽
selected 上述好友条件成立,且用户在该帖moments_audience中
public 已登录聊天API用户,双方未屏蔽;不是匿名公开网页
private 只有作者

个人hide优先过滤,包括作者本人隐藏自己的帖子后也不会通过普通post读取。作者通常可读自己的帖子,其他用户还会结合屏蔽与当前好友关系。selected发布须有至少一位当前未屏蔽好友,最多100、不重复,不含自己;其他范围viewerIds必须为空。删除好友后selected对象即使仍在受众表,也不再满足当前好友条件。依据:chat-service/src/moments-repository.mjs:80-89,126-145。

没有公开PATCH帖子或调整既有visibility的路由。若页面因失去可见资格而返回404,应按不可再读取处理,不应在客户端继续展示缓存为“公开可见”。

两阶段上传与发布

  1. 客户端先准备媒体,按每个文件调用POST /moments/media?name=...,Content-Type为实际MIME,body为原始字节。
  2. 上传成功得到media.id,暂时尚未绑定帖子,仅所有者可读取。
  3. 客户端收齐mediaIds后调用POST /moments:text、visibility、viewerIds、mediaIds。
  4. 服务端在事务中确认媒体为当前用户拥有、尚未绑定其他帖子,插入帖子与受众,并绑定全部媒体。
  5. 返回201 {post}后更新列表;上传成功但未发帖不等于发布成功。
项目 服务端限制
text trim后最多2000个Unicode码点;文字或媒体至少一项
图片 JPEG或PNG,单项10 MiB;PNG宽高1–2048
视频 MP4或MOV,单项30 MiB;检查ftyp标识
单帖媒体 最多9图,或仅1段视频;不能图视频混合
账号媒体 新事独立500 MiB配额;未绑定媒体超过24小时清理
JSON与文件名 发布JSON最多20000字节;name为1–120码点,无控制字符或路径分隔符

这些值见chat-service/src/server.mjs:350-365,416-434、chat-service/src/moments-repository.mjs:97-145。媒体原始字节和帖子内容都能被服务端读取;文件头检查不等于完整转码、恶意文件扫描或编解码器兼容性验收。

发布和上传没有clientId去重契约。若响应丢失,先查询自己的最新新事确认结果;重新上传可能额外占用空间,重复发帖也可能产生第二条。若上次实际发布成功,相同mediaIds再次发布会因已绑定返回INVALID_POST,而不是返回上次帖子。不能套用聊天消息的“相同clientId安全重试”。

Range读取与客户端实际行为

GET /moments/media/{mediaId}先按当前帖子权限检查,随后处理一个可选Range。支持bytes=0-1023或bytes=1024-,不支持bytes=-1024、多段或其他语法。合法范围返回206、Content-Range和Content-Length;不带Range返回200。结束位置超出文件末尾会截到末尾,起点超出大小、起点大于终点或非安全整数返回416 INVALID_RANGE。缓存为private、max-age=300,不是公开CDN许可。

合成例:对大小2048字节的有权访问媒体,请求Range: bytes=0-1023返回前1024字节;bytes=2048-返回416。首次未绑定媒体仅本人能下载,即使另一个用户知道媒体UUID也返回404。依据:chat-service/src/server.mjs:367-392、chat-service/src/moments-repository.mjs:117-123。

iOS MomentsStore.mediaData只接受200,把完整Data缓存,视频写临时mp4/mov再交AVPlayer;Android下载也只接受200,按图片10 MiB、视频30 MiB有界读取,点击视频后才下载并写缓存文件交VideoView。因此“后端支持Range”不代表当前两端已实现边下载边播放。依据:ios-app/Sources/Features/Moments/MomentsStore.swift:186-206、android-app/app/src/main/java/cn/thestarline/workbench/MomentsScreens.kt:380-438。

点赞、评论、删除和通知

PUT like为幂等点赞,DELETE like为取消;只有首次新增点赞产生通知,给自己的内容点赞不通知自己。评论1–500字,可带同帖replyToId,成功后产生comment或reply通知;回复对象缺失为INVALID_COMMENT。作者可删除自己评论,帖子作者可删除其帖下任意评论。删除被回复评论会把后续评论的replyToId置null,不会一并删掉所有回复。

只有帖子作者可DELETE帖子,数据库外键级联删除其媒体、评论、点赞与相关通知。hide只对本人隐藏,report以帖子+举报者去重并更新原因,不能举报自己的帖子。report成功只证明记录写入,不证明已有审核处理工作流。依据:chat-service/src/moments-repository.mjs:188-267。

Feed默认20、最多50条;before指向更旧帖子,返回nextBefore,不能把缺省替换成0。评论与通知固定每页30,通常倒序,nextBefore=null表示该次查询没有下一页。通知列表动态过滤屏蔽和不可见帖子,unreadCount按当前可见通知计算;mark read会将当前账号全部未读通知标记已读。新事没有在这些方法中写普通/events,客户端靠进入页面或显式刷新同步。

分页合并、互动历史与账号切换

feed在内部每次扫描最多100条候选,再逐条检查当前可见性,累积到请求数量加一才决定下一页。因此返回ID不连续、帖子数量少于候选数量都是正常情况;分页应使用nextBefore,不应自行减ID或认为被过滤内容不存在。客户端追加下一页还按帖子ID去重,避免刷新与分页重叠产生重复卡片。依据:chat-service/src/moments-repository.mjs:166-185、ios-app/Sources/Features/Moments/MomentsStore.swift:67-77。

通知记录的是过去发生的互动。取消赞只删除like关系,没有同步删除原来的点赞通知;取消后再点赞可能产生新的互动通知。当前liked状态应以帖子响应为准,不能由“收到过点赞通知”推断。评论者、帖子作者和被回复者相同时,会避免给同一人重复生成自己参与的通知;通知可见性仍在读取时重新过滤。

iOS在refresh检测到账号变化或退出时清空帖子、通知和内存媒体缓存,并删除其登记的视频临时文件;这不等于整个操作系统缓存已擦除。下载完成与刷新之间若账号发生变化,还需通过客户端生命周期测试验证旧响应不会影响新账号。本轮只记录源码机制,没有执行切换账号或缓存清除验收。依据:ios-app/Sources/Features/Moments/MomentsStore.swift:31-63。

双端差异与失败样例

场景 iOS当前实现 Android当前实现
图片准备 最长边缩到2048,以0.82质量重编码JPEG 读取选择器原始JPEG/PNG字节,最多读上限+1;不做同等重编码
文字计数 Swift String.count,按字素簇提示和禁用发布 Kotlin take(2000)/length,按UTF-16代码单元截取
指定受众 候选过滤isFriend与非isBlocked;非selected强制发送[] 候选从conversations取peer;发布固定带selected列表
发布后刷新 将返回post插入当前列表 确认提交后切回新事,再刷新列表

服务端计数是Unicode码点,包含组合字符的输入可能与两端计数不同,以服务端判定为准。Android还有源码可推导的失败场景:先选指定好友,再切到friends/public/private,selected列表未清空仍随请求发出,服务端因非selected带viewerIds拒绝400 INVALID_POST;iOS发送前清空该字段。该场景本轮未运行原生验收,不能写成已修复。依据:ios-app/Sources/Features/Moments/MomentComposerView.swift:36-62,194-236、ios-app/Sources/Features/Moments/MomentsStore.swift:91-108、android-app/app/src/main/java/cn/thestarline/workbench/MomentsScreens.kt:458-500,513-554。

正常例:friends+viewerIds=[]+一张刚上传的本人图片返回post。失败例:混传一图一视频、引用别人媒体、复用已绑定媒体或selected列表含已删除好友,均不会完成新帖子写入。详见API参考、限制与错误及验收矩阵。

源码核对范围:2026-10-23 的当前实现。本轮未执行生产业务请求或原生设备验收。

Markdown 原文完整文档文本