401 lines
15 KiB
Markdown
401 lines
15 KiB
Markdown
# 广场设计
|
||
|
||
> 对照:公开广场流 + 可选「仅自己 / 部分好友」;发帖形态对齐微信朋友圈
|
||
> 形态: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 |
|
||
|
||
---
|
||
|
||
# 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. 总览
|
||
|
||
```text
|
||
客户端
|
||
│ 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. 模块与目录
|
||
|
||
```text
|
||
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(示意)
|
||
|
||
```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
|
||
|
||
```json
|
||
{
|
||
"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`
|