15 KiB
广场设计
对照:公开广场流 + 可选「仅自己 / 部分好友」;发帖形态对齐微信朋友圈
形态:Spark IM 模块化单体内的独立业务,不进会话、不走messages
状态:已实现(internal/module/square、GET/POST /v1/square/...)
硬约束:遵守docs/architecture-v3.mdC1–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 |
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=0KEY 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.godocs/architecture-v3.md