spark-server/docs/square.md
DESKTOP-L8OUL9F\gfl0034 c26d61fd26 feat(media): 镜像装 ffmpeg,并补齐压缩配置与文档
机器人视频压缩依赖 ffmpeg;补 MediaCompressOptions、广场机器人说明与当日变动清单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 23:53:56 +08:00

15 KiB
Raw Blame History

广场设计

对照:公开广场流 + 可选「仅自己 / 部分好友」;发帖形态对齐微信朋友圈
形态:Spark IM 模块化单体内的独立业务,不进会话、不走 messages
状态:已实现(internal/module/square、GET/POST /v1/square/...)
硬约束:遵守 docs/architecture-v3.md C1–C7。对照已落地朋友圈见 docs/moments.md。

广场是独立于朋友圈的内容频道:默认发帖经审核通过后进入全站广场 Feed;发帖可选 仅自己、部分好友可看(白名单)、不给谁看(黑名单,其余登录用户可看)。私密/白名单帖不进全站 Feed。与好友 拉黑 关系任一侧存在时,默认双方互相看不到对方广场内容(无需写进 audience_uids)。载体与朋友圈一致。分表、分 REST、前缀 perm/square/。


0. 需求摘要

# 需求 设计要点
1 发送方式与朋友圈类似 同一套 content JSON;square_image / square_video
2 默认审核通过;公开类帖进全站 Feed audit_status;v1 即 approved;全站 Feed 拉 visibility∈{公开,不给谁看},再按读者过滤
3 仅自己 / 部分好友 / 不给谁看 visibility 0/1/2/3 + square_audience(2=白名单能看,3=黑名单不能看);名单须双向好友,最多 200
3b 拉黑好友默认不给看 任意可见性下,friend.BlockedEither(作者,读者) → 不可见、不可赞评
4 广场列表分页 cursor + limit,默认 20、上限 50(与朋友圈一致,不改朋友圈默认)
6 v1 就要赞/评通知 square_notices + 列表/已读/未读角标 API(对齐朋友圈,不进会话 inbox)

0.1 做 / 不做

做(v1)

能力 说明
发帖 公开 / 仅自己 / 部分好友 / 不给谁看(黑名单)
全站广场 Feed visibility∈{0,3} + 审核通过 + 未删;读时剔除拉黑对与黑名单命中
作者主页流 GET /v1/square/users/{uid}/posts:按可见性对当前浏览者过滤
单帖详情 有权读者可读(见 §3.5)
点赞 / 取消赞 幂等;写 square_notices(赞)
评论 / 回复 / 删评 全量展示评论(无共同好友过滤);写通知(评/回复)
互动通知 列表、已读、未读红点(不进 messages / mailbox)
作者删帖 软删 + 删对象
审核字段 默认通过

不做(v1)

能力 原因
朋友圈式「不看他 / 可见天数」 属 moment_*(广场无 hide 边)
广场帖写进 messages / 会话 inbox C1/C7
全站 TCP 推送每条新帖 非聊天;赞评可选二期 deliver Push
推荐算法、话题、@ 后续
未登录刷广场 需 Bearer

机器人海外媒体(已实现)

能力 说明
定时发帖 square_bot.enabled;进程内 ticker,默认 5 分钟;随机 botNNN@spark.bot
内容 海外主流媒体 公开 RSS 标题+短摘要+原文链接;URL 去重(t_square_bot_links)
TTL 机器人帖 expires_at 默认 15 天;到期软删 + Feed/详情不可见
配图 media.bot_compress:enclosure 图下载后压到 ≤1080 再 Put

详见 docs/media.md。


0.2 与朋友圈的差异

维度 朋友圈 广场
主 Feed 好友时间线 全站流(visibility 公开 + 不给谁看,读时过滤)
可见性默认 好友公开 全站公开(审核通过后)
不给谁看 黑名单 visibility=3 同枚举;仍进 Feed 拉取,名单内不可见
拉黑 Feed 过滤 默认双方互不可见(无需写 audience)
私密/白名单 不进好友 Feed 不进全站 Feed;主页流按权限
评论规则 共同好友 全部展示
通知 moment_notices square_notices(v1 同等能力)
表 / API moment_*、/v1/moments square_*、/v1/square

内容 JSON:与 moment/entity 的 Content 字段兼容;logic 不跨模块 import model。


0.3 与 V3 的关系

约束 广场
C1 Snowflake square_posts.id
C2 HTTP → square/logic
C5 发帖 HTTP 200;非 SEND_ACK
C6 限流用 reqctx.RealIP
C7 通知不进会话 Redis ZSET

1. 总览

客户端
  │  HTTPS /v1/square
  │  POST /v1/files  (square_image / square_video)
  ▼
Gin  →  square/logic
          │
          ├─ MySQL    square_posts / square_audience / square_likes
          │           square_comments / square_notices
          ├─ friend   校验 audience、读权限(双向好友)
          ├─ RustFS   perm/square/...
          └─ 可选二期  deliver:queue(赞评在线 Push,朋友圈亦未强制)

2. 模块与目录

internal/module/square/
├── entity/          Post、Visibility、AuditStatus、Notice、Content…
├── model/           一表一文件 + repo
└── logic/           publish、feed、album、get、like、comment、notice、delete、visible
internal/transport/http/handler/square/
migrations/00xx_square.sql

依赖:

依赖 用途
friend audience_uids 须双向好友;读帖时校验关系(拉黑则不可见/不可互动)
user 作者卡片
upload / objectstore / id.Generator 同前

3. 数据模型

3.1 square_posts

列 类型 说明
id BIGINT PK Snowflake;游标
user_id BIGINT 作者
kind SMALLINT 1 文 2 图 3 视频
text VARCHAR(1000) 可空;与媒体至少一项
content VARBINARY(16384) NOT NULL 序列化 JSON 字节(json.Marshal → []byte 入库);图/视频只存 object_key,禁止 MySQL BLOB 类型、禁止 VARCHAR/TEXT 存 JSON 字符串、禁止媒体二进制
poi_* 可选地点
visibility SMALLINT 0 全站公开 1 仅自己 2 白名单 3 黑名单(不给谁看)
audit_status SMALLINT 0 待审 1 通过 2 拒绝;v1 发帖 1
audit_note VARCHAR(256)
audited_at DATETIME(3) NULL
status SMALLINT 0 正常 1 软删
like_count / comment_count INT
created_at DATETIME(3)

索引:

  • PRIMARY KEY (id)
  • KEY idx_public_feed (audit_status, visibility, id) — 全站 Feed:audit_status=1 AND visibility IN (0,3) AND status=0
  • KEY idx_user (user_id, id) — 作者主页流

visibility(枚举与朋友圈 moment_posts.visibility 同值同义,广场语义见下表)

值 名称 进全站 Feed 拉取 读者(在未拉黑前提下)
0 public 是 任意登录用户
1 private 否 仅作者
2 include 否 作者 + 白名单 square_audience
3 exclude 是 除黑名单 square_audience 外的登录用户

发帖默认:visibility=0。

content 列(与 messages.content 语义一致,物理类型不同)

层 约定
应用层 entity.Content → Marshal() 得到 []byte,写入 Post.Content
GORM Content []byte + gorm:"column:content;type:varbinary(16384);..."
MySQL VARBINARY(16384),不用 BLOB/MEDIUMBLOB
上限 9 图 + 视频元数据 JSON 远小于 16KB;超限返回 40001

朋友圈 moment_posts.content 历史为 BLOB;广场新表按上表实现,后续若要统一 moment 可单独 migration。

拉黑(全局,默认):不论上表哪一行,若 BlockedEither(作者, viewer),则 viewer 不可见、不可赞评;实现于 visible 逻辑最前,不写入 square_audience。

3.2 square_audience

visibility=2(白名单)或 visibility=3(黑名单)时有行。语义与 moment_audience 一致:

visibility peer_id 含义
2 include 能看 的好友 uid
3 exclude 不能看 的好友 uid(其余登录用户能看,仍受拉黑规则约束)
列 说明
post_id 帖 id
peer_id 好友 uid
PK (post_id, peer_id)

单帖最多 200 人;audience_uids 去重;每人须与作者 双向好友(friend 校验);不含作者自己。

3.3 square_likes / square_comments

与 moment_likes / moment_comments 同构。评论列表不做共同好友过滤。

3.4 square_notices(v1 必做)

结构对齐 moment_notices:

列 说明
id Snowflake PK,列表游标
to_user_id 接收人(帖作者或被回复者)
from_user_id 点赞人/评论人
kind 1 赞 2 评论 3 回复
post_id 帖 id
comment_id 赞为 0
read_at NULL=未读
created_at

索引:KEY idx_to_user (to_user_id, id)。

写入时机(与朋友圈一致,同事务或紧接 COMMIT):

动作 通知
点赞 to=帖作者,kind=赞;自己赞自己不写
直接评论 to=帖作者,kind=评论
回复评论 to=被回复用户,kind=回复;若与帖作者不同可各写一条(同 moment)

取消赞不删历史通知(同朋友圈)。

未读水位(v1)

独立表 square_notice_state(或单列进用户扩展,推荐独立表与 moment 解耦):

列 说明
user_id PK
last_read_notice_id 广场互动通知已读水位,只增不减
updated_at

GET /v1/square/notices/unread:latest_id > last_read_notice_id 则 has_new=true(实现可抄 moment UnreadFeed)。


3.5 读权限(单帖 / 赞 / 评 / 通知跳转)

对浏览者 viewer、帖 post(status=正常,audit_status=通过):

第一步(默认):若 viewer != post.user_id 且 BlockedEither(post.user_id, viewer) → 拒绝(拉黑好友不给看)。

第二步:visibilityOK(与 moment/logic/visible.go 同构,公开帖在广场表示「除黑名单外全员」):

visibility 允许读
public (0) 任意登录用户
private (1) 仅作者
include (2) 作者,或 viewer 在白名单
exclude (3) 作者,或 viewer 不在 黑名单

全站 Feed:visibility IN (0,3) 分页拉取后,对每条执行上述过滤;不足 limit 时 overfetch(策略同朋友圈 Feed,Overfetch/OverfetchRounds 可复用常量)。


4. 分页约定

cursor = 上一页最后 post_id,limit 默认 20、max 50(常量与朋友圈 entity.ListDefault / ListMax 对齐);响应 items、next_cursor、has_more。

4.1 全站 Feed SQL(示意)

SELECT ... FROM square_posts
WHERE audit_status = 1 AND visibility IN (0, 3) AND status = 0
  AND (:cursor = 0 OR id < :cursor)
ORDER BY id DESC
LIMIT :batch;   -- batch > limit,供拉黑/黑名单过滤

4.2 作者主页流

GET /v1/square/users/{uid}/posts?cursor=&limit=

  • 拉取 user_id = uid 的帖(id 游标),在 logic 层对 viewer 做 §3.5 过滤(overfetch 策略可抄朋友圈 album)。
  • 用于:看自己的私密/部分好友帖;好友看对方允许自己看的广场帖。

4.3 与朋友圈分页

朋友圈 保持 GET /v1/moments?cursor=&limit= 默认 20(internal/module/moment/entity/kind.go 的 ListDefault、OpenAPI 不变)。广场复用同一套默认/上限常量或复制相同数值(20/50)。


5. HTTP API(v1)

均需 Bearer。前缀 /v1/square。

帖子

方法 路径 说明
GET /v1/square/posts 全站公开 Feed;limit 默认 20
POST /v1/square/posts 发帖(见下)
GET /v1/square/posts/{id} 单帖(权限 §3.5)
DELETE /v1/square/posts/{id} 作者删帖
GET /v1/square/users/{uid}/posts 某用户广场帖(按可见性过滤)

互动

方法 路径 说明
PUT/DELETE /v1/square/posts/{id}/like 赞 / 取消
GET/POST/DELETE /v1/square/posts/{id}/comments… 评论列表/发评/删评

通知(v1)

方法 路径 说明
GET /v1/square/notices cursor、limit(默认 20)
GET /v1/square/notices/unread 角标
POST /v1/square/notices/read body { "cursor": 0 } 标记已读到该水位

发帖 Body

{
  "text": "今天天气不错",
  "images": [{ "key": "perm/square/image/20260922/a.jpg", "w": 1080, "h": 720 }],
  "visibility": 0,
  "audience_uids": [2101923649870106624],
  "poi": { "name": "外滩", "lat": 31.24, "lng": 121.49 }
}
字段 说明
visibility 0 公开 / 1 仅自己 / 2 白名单 / 3 不给谁看
audience_uids visibility=2 或 3 时必填,最多 200,双向好友

校验:媒体规则同朋友圈;visibility 为 2/3 时 audience_uids 非空(entity.Visibility.NeedsAudience())。


6. 上传与对象存储

不变:square_image / square_video → perm/square/...。


7. 权限与安全

动作 规则
全站 Feed 公开+不给谁看类帖;按读者过滤拉黑/黑名单
发帖 登录 + 限流
读单帖/主页流 §3.5 + 拉黑
赞评 须能读该帖;限流;写 square_notices
删帖 作者

8. 实现复用

项 策略
客户端 可见范围:公开 / 仅自己 / 选好友可看 / 选好友不给看
visibility 校验 与 moment 同枚举 0–3;validate.square.*
visible 复用 visibilityOK 语义 + 优先 BlockedEither
notice 抄 moment/logic/notice.go、like.go、comment.go 的 notify 模式
GORM 嵌入结构体须 导出(MessageRow 教训)

9. 实施阶段

Phase 1(本期)

  • migration:square_posts、square_audience、square_likes、square_comments、square_notices、square_notice_state
  • module + HTTP + OpenAPI + i18n
  • upload purpose
  • 发帖:公开 / 仅自己 / 白名单 / 黑名单(不给谁看) + 拉黑默认过滤
  • Feed + 用户主页流 + 分页默认 20
  • 赞评 + 通知列表/未读/已读
  • 测试:可见性、Feed 不含私密帖、赞评通知、拉黑不可互动

Phase 2

  • 审核 pending + 运营后台
  • deliver:queue 赞评 Push(在线设备)
  • 机审 / 敏感词

10. 测试要点

场景 期望
发 visibility=0 出现在 GET /square/posts
发 visibility=1 不出现在全站 Feed;作者主页可见
发 visibility=2 + audience 仅作者与名单好友可读;全站 Feed 无
发 visibility=3 + audience 全站 Feed 有;名单内好友不可见
A 拉黑 B B 刷 Feed 不见 A 的帖;B 单帖 GET 403/404
A 公开帖 + B 在黑名单 仅 B 不可见
B 赞 A 的公开帖 A 的 square_notices 增一条;unread 为 true
B 评 A 同;自己评自己不通知
回复 C 的评论 C 收到回复通知;A 收到评论通知(若规则同 moment)
非好友不在 audience GET 单帖 404 或 403
GORM INSERT 全列写入

11. 参考

  • docs/moments.md、internal/module/moment/logic/notice.go
  • docs/architecture-v3.md