docs: 更新通知系统设计方案

主要修改:
- 添加 star_id 数据隔离字段到 notifications 和 notification_stats 表
- 更新索引包含 star_id
- 添加数据一致性保证章节(事务边界、点赞通知防重复唯一约束)
- 所有 API 接口参数增加 starID
- JSON data 示例增加 star_id 字段

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zheng020 2026-05-15 11:25:29 +08:00
parent 20f90d7120
commit ec46004551

View File

@ -31,6 +31,7 @@
CREATE TABLE notifications ( CREATE TABLE notifications (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, -- 接收通知的用户ID user_id BIGINT NOT NULL, -- 接收通知的用户ID
star_id BIGINT NOT NULL, -- 数据隔离star ID
type VARCHAR(20) NOT NULL, -- 通知类型: like / system / activity type VARCHAR(20) NOT NULL, -- 通知类型: like / system / activity
title VARCHAR(200) NOT NULL, -- 通知标题 title VARCHAR(200) NOT NULL, -- 通知标题
content VARCHAR(500), -- 通知内容 content VARCHAR(500), -- 通知内容
@ -41,8 +42,8 @@ CREATE TABLE notifications (
read_at BIGINT, -- 阅读时间(毫秒时间戳) read_at BIGINT, -- 阅读时间(毫秒时间戳)
-- 索引 -- 索引
INDEX idx_notifications_user_type_created (user_id, type, created_at DESC), INDEX idx_notifications_user_type_created (user_id, star_id, type, created_at DESC),
INDEX idx_notifications_user_unread (user_id, is_read, created_at DESC) INDEX idx_notifications_user_unread (user_id, star_id, is_read, created_at DESC)
); );
``` ```
@ -52,15 +53,20 @@ CREATE TABLE notifications (
```sql ```sql
CREATE TABLE notification_stats ( CREATE TABLE notification_stats (
user_id BIGINT PRIMARY KEY, -- 用户ID user_id BIGINT NOT NULL, -- 用户ID
star_id BIGINT NOT NULL, -- 数据隔离star ID
like_unread_count INT DEFAULT 0, -- 点赞通知未读数 like_unread_count INT DEFAULT 0, -- 点赞通知未读数
system_unread_count INT DEFAULT 0, -- 系统通知未读数 system_unread_count INT DEFAULT 0, -- 系统通知未读数
activity_unread_count INT DEFAULT 0, -- 活动通知未读数 activity_unread_count INT DEFAULT 0, -- 活动通知未读数
total_unread_count INT DEFAULT 0, -- 总未读数 total_unread_count INT DEFAULT 0, -- 总未读数
updated_at BIGINT NOT NULL -- 更新时间 updated_at BIGINT NOT NULL, -- 更新时间
PRIMARY KEY (user_id, star_id)
); );
``` ```
> **注意**`notification_stats` 使用 `(user_id, star_id)` 作为联合主键,支持多 star 场景下各 star 独立统计未读数。
### 2.3 JSON data 字段示例 ### 2.3 JSON data 字段示例
```json ```json
@ -72,7 +78,8 @@ CREATE TABLE notification_stats (
"actor_name": "张三", "actor_name": "张三",
"actor_avatar": "https://example.com/avatar/456.png", "actor_avatar": "https://example.com/avatar/456.png",
"asset_title": "我的藏品", "asset_title": "我的藏品",
"asset_cover": "https://example.com/asset/123/cover.png" "asset_cover": "https://example.com/asset/123/cover.png",
"star_id": 1
} }
// 系统通知 (type: "system") // 系统通知 (type: "system")
@ -89,7 +96,8 @@ CREATE TABLE notification_stats (
"activity_title": "端午节活动", "activity_title": "端午节活动",
"activity_cover": "https://example.com/activity/789/cover.png", "activity_cover": "https://example.com/activity/789/cover.png",
"reward_type": "badge", "reward_type": "badge",
"reward_name": "端午限定徽章" "reward_name": "端午限定徽章",
"star_id": 1
} }
``` ```
@ -118,14 +126,17 @@ CREATE TABLE notification_stats (
│ Social Service │────▶│ Notification Service │ │ Social Service │────▶│ Notification Service │
│ (点赞业务) │ │ (存储 + 查询) │ │ (点赞业务) │ │ (存储 + 查询) │
└─────────────────┘ └─────────────────────┘ └─────────────────┘ └─────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Asset Service │ │ Asset Service │ │ PostgreSQL │
│ (更新点赞数) │ │ (更新点赞数) │ │ notifications + │
└─────────────────┘ └─────────────────┘ │ notification_stats │
└─────────────────────┘
``` ```
> **事务边界**Notification Service 在数据库事务中同时写入 `notifications` 表和更新 `notification_stats` 表。
--- ---
## 4. API 接口设计 ## 4. API 接口设计
@ -134,12 +145,14 @@ CREATE TABLE notification_stats (
| 方法 | 描述 | 参数 | | 方法 | 描述 | 参数 |
|-----|------|------| |-----|------|------|
| CreateNotification | 创建通知 | userID, type, title, content, data | | CreateNotification | 创建通知 | userID, starID, type, title, content, data |
| GetNotifications | 查询通知列表 | userID, type, page, pageSize | | GetNotifications | 查询通知列表 | userID, starID, type, page, pageSize |
| GetUnreadCount | 获取未读数 | userID | | GetUnreadCount | 获取未读数 | userID, starID |
| MarkAsRead | 标记已读 | notificationID, userID | | MarkAsRead | 标记已读 | notificationID, userID, starID |
| MarkAllAsRead | 全部标已读 | userID, type | | MarkAllAsRead | 全部标已读 | userID, starID, type |
| DeleteNotification | 删除通知 | notificationID, userID | | DeleteNotification | 删除通知 | notificationID, userID, starID |
> **说明**:所有接口都需要传入 `starID`,确保数据隔离。
### 4.2 HTTP 接口(供前端调用) ### 4.2 HTTP 接口(供前端调用)
@ -173,12 +186,14 @@ GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20
1. 用户点赞 → Social Service 处理 1. 用户点赞 → Social Service 处理
2. Social Service 调用 Asset Service RPC 更新点赞数 2. Social Service 调用 Asset Service RPC 更新点赞数
3. Social Service 调用 Notification Service CreateNotification 3. Social Service 调用 Notification Service CreateNotification
4. Notification Service: 4. Notification Service(在事务中完成):
- 写入 notifications 表 - 写入 notifications 表
- 更新 notification_stats 表 (+1 未读数) - 更新 notification_stats 表 (+1 未读数INSERT ... ON CONFLICT DO UPDATE 处理首次创建)
5. 返回成功响应 5. 返回成功响应
``` ```
> **事务保证**:创建通知和更新统计必须在同一个事务中,确保数据一致性。
### 5.2 查询逻辑 ### 5.2 查询逻辑
``` ```
@ -193,9 +208,11 @@ GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20
### 5.3 未读数统计 ### 5.3 未读数统计
- 每次创建通知时,更新 `notification_stats` 表对应类型的未读数 - 每次创建通知时,在同一事务中更新 `notification_stats` 表对应类型的未读数
- 每次标记已读时,减少未读数 - 使用 `INSERT ... ON CONFLICT DO UPDATE` 确保首次创建时自动插入记录
- 每次标记已读时,减少对应类型的未读数
- 批量标已读时,重置对应类型的未读数为 0 - 批量标已读时,重置对应类型的未读数为 0
- 删除通知时,同步减少未读数
### 5.4 通知直达 ### 5.4 通知直达
@ -233,25 +250,76 @@ GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20
--- ---
## 8. 项目结构 ## 8. 数据一致性保证
### 8.1 事务边界
创建通知和更新统计必须在同一个数据库事务中完成:
```go
func (s *NotificationService) CreateNotification(ctx context.Context, ...) error {
return s.db.Transaction(func(tx *sql.Tx) error {
// 1. 写入 notifications 表
_, err := tx.Exec("INSERT INTO notifications (...) VALUES (...)", ...)
if err != nil {
return err
}
// 2. 更新 notification_stats 表(使用 INSERT ... ON CONFLICT DO UPDATE
_, err = tx.Exec(`
INSERT INTO notification_stats (user_id, star_id, like_unread_count, updated_at)
VALUES (?, ?, 1, ?)
ON CONFLICT (user_id, star_id) DO UPDATE SET
like_unread_count = notification_stats.like_unread_count + 1,
total_unread_count = notification_stats.total_unread_count + 1,
updated_at = ?
`, userID, starID, now, now)
return err
})
}
```
### 8.2 点赞通知防重复
由于藏品可下架再上架,同一用户对同一藏品当天可能产生多条点赞通知。为避免重复:
```sql
-- 为点赞通知添加唯一约束(可选,取决于业务需求)
CREATE UNIQUE INDEX idx_like_notification_daily
ON notifications (user_id, star_id, type, data->>'target_id', data->>'actor_id', date_trunc('day', to_timestamp(created_at/1000)));
```
> **说明**:如果业务上允许同一天多条点赞通知(每条都展示),则不需要此唯一约束。
### 8.2 补偿机制
如果事务提交后 RPC 调用方未收到响应,调用方会重试。此时:
- 使用唯一约束 `UNIQUE (user_id, star_id, type, target_type, target_id, actor_id, date)` 防止重复创建点赞通知
- 或者通过幂等性设计(如每次点赞生成唯一 notification_id处理重复
---
## 9. 项目结构
``` ```
services/notificationService/ services/notificationService/
├── main.go ├── main.go
├── repository/ ├── repository/
│ ├── notification_repository.go │ ├── notification_repository.go -- 通知 CRUD
│ └── notification_stats_repository.go │ └── notification_stats_repository.go -- 统计更新
├── service/ ├── service/
│ └── notification_service.go │ └── notification_service.go -- 业务逻辑 + 事务处理
├── provider/ ├── provider/
│ └── notification_provider.go │ └── notification_provider.go -- RPC 接口
├── model/
│ └── notification.go -- 数据模型
└── client/ └── client/
└── (供其他服务调用的客户端,如需要) └── (供其他服务调用的客户端,如需要)
``` ```
--- ---
## 9. 后续扩展 ## 10. 后续扩展
- 支持评论通知 (type: "comment") - 支持评论通知 (type: "comment")
- 支持 @提及通知 (type: "mention") - 支持 @提及通知 (type: "mention")
@ -259,12 +327,14 @@ services/notificationService/
--- ---
## 10. 确认点 ## 11. 确认点
- [x] 统一通知表存储所有类型 - [x] 统一通知表存储所有类型
- [x] 独立 Notification Service - [x] 独立 Notification Service
- [x] 支持 star_id 数据隔离
- [x] 支持今日/历史 Tab 查询 - [x] 支持今日/历史 Tab 查询
- [x] 不合并点赞记录 - [x] 不合并点赞记录
- [x] 支持未读数统计 - [x] 支持未读数统计(事务保证一致性)
- [x] 支持通知直达 - [x] 支持通知直达
- [x] 支持批量操作 - [x] 支持批量操作
- [x] INSERT ... ON CONFLICT DO UPDATE 处理首次创建统计