diff --git a/docs/specs/2026-05-15-notification-system-design.md b/docs/specs/2026-05-15-notification-system-design.md index d633b13..9169b68 100644 --- a/docs/specs/2026-05-15-notification-system-design.md +++ b/docs/specs/2026-05-15-notification-system-design.md @@ -31,6 +31,7 @@ CREATE TABLE notifications ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, -- 接收通知的用户ID + star_id BIGINT NOT NULL, -- 数据隔离(star ID) type VARCHAR(20) NOT NULL, -- 通知类型: like / system / activity title VARCHAR(200) NOT NULL, -- 通知标题 content VARCHAR(500), -- 通知内容 @@ -41,8 +42,8 @@ CREATE TABLE notifications ( read_at BIGINT, -- 阅读时间(毫秒时间戳) -- 索引 - INDEX idx_notifications_user_type_created (user_id, type, created_at DESC), - INDEX idx_notifications_user_unread (user_id, is_read, created_at DESC) + INDEX idx_notifications_user_type_created (user_id, star_id, type, 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 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, -- 点赞通知未读数 system_unread_count INT DEFAULT 0, -- 系统通知未读数 activity_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 字段示例 ```json @@ -72,7 +78,8 @@ CREATE TABLE notification_stats ( "actor_name": "张三", "actor_avatar": "https://example.com/avatar/456.png", "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") @@ -89,7 +96,8 @@ CREATE TABLE notification_stats ( "activity_title": "端午节活动", "activity_cover": "https://example.com/activity/789/cover.png", "reward_type": "badge", - "reward_name": "端午限定徽章" + "reward_name": "端午限定徽章", + "star_id": 1 } ``` @@ -118,14 +126,17 @@ CREATE TABLE notification_stats ( │ Social Service │────▶│ Notification Service │ │ (点赞业务) │ │ (存储 + 查询) │ └─────────────────┘ └─────────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Asset Service │ -│ (更新点赞数) │ -└─────────────────┘ + │ │ + ▼ ▼ +┌─────────────────┐ ┌─────────────────────┐ +│ Asset Service │ │ PostgreSQL │ +│ (更新点赞数) │ │ notifications + │ +└─────────────────┘ │ notification_stats │ + └─────────────────────┘ ``` +> **事务边界**:Notification Service 在数据库事务中同时写入 `notifications` 表和更新 `notification_stats` 表。 + --- ## 4. API 接口设计 @@ -134,12 +145,14 @@ CREATE TABLE notification_stats ( | 方法 | 描述 | 参数 | |-----|------|------| -| CreateNotification | 创建通知 | userID, type, title, content, data | -| GetNotifications | 查询通知列表 | userID, type, page, pageSize | -| GetUnreadCount | 获取未读数 | userID | -| MarkAsRead | 标记已读 | notificationID, userID | -| MarkAllAsRead | 全部标已读 | userID, type | -| DeleteNotification | 删除通知 | notificationID, userID | +| CreateNotification | 创建通知 | userID, starID, type, title, content, data | +| GetNotifications | 查询通知列表 | userID, starID, type, page, pageSize | +| GetUnreadCount | 获取未读数 | userID, starID | +| MarkAsRead | 标记已读 | notificationID, userID, starID | +| MarkAllAsRead | 全部标已读 | userID, starID, type | +| DeleteNotification | 删除通知 | notificationID, userID, starID | + +> **说明**:所有接口都需要传入 `starID`,确保数据隔离。 ### 4.2 HTTP 接口(供前端调用) @@ -173,12 +186,14 @@ GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20 1. 用户点赞 → Social Service 处理 2. Social Service 调用 Asset Service RPC 更新点赞数 3. Social Service 调用 Notification Service CreateNotification -4. Notification Service: +4. Notification Service(在事务中完成): - 写入 notifications 表 - - 更新 notification_stats 表 (+1 未读数) + - 更新 notification_stats 表 (+1 未读数,INSERT ... ON CONFLICT DO UPDATE 处理首次创建) 5. 返回成功响应 ``` +> **事务保证**:创建通知和更新统计必须在同一个事务中,确保数据一致性。 + ### 5.2 查询逻辑 ``` @@ -193,9 +208,11 @@ GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20 ### 5.3 未读数统计 -- 每次创建通知时,更新 `notification_stats` 表对应类型的未读数 -- 每次标记已读时,减少未读数 +- 每次创建通知时,在同一事务中更新 `notification_stats` 表对应类型的未读数 +- 使用 `INSERT ... ON CONFLICT DO UPDATE` 确保首次创建时自动插入记录 +- 每次标记已读时,减少对应类型的未读数 - 批量标已读时,重置对应类型的未读数为 0 +- 删除通知时,同步减少未读数 ### 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/ ├── main.go ├── repository/ -│ ├── notification_repository.go -│ └── notification_stats_repository.go +│ ├── notification_repository.go -- 通知 CRUD +│ └── notification_stats_repository.go -- 统计更新 ├── service/ -│ └── notification_service.go +│ └── notification_service.go -- 业务逻辑 + 事务处理 ├── provider/ -│ └── notification_provider.go +│ └── notification_provider.go -- RPC 接口 +├── model/ +│ └── notification.go -- 数据模型 └── client/ └── (供其他服务调用的客户端,如需要) ``` --- -## 9. 后续扩展 +## 10. 后续扩展 - 支持评论通知 (type: "comment") - 支持 @提及通知 (type: "mention") @@ -259,12 +327,14 @@ services/notificationService/ --- -## 10. 确认点 +## 11. 确认点 - [x] 统一通知表存储所有类型 - [x] 独立 Notification Service +- [x] 支持 star_id 数据隔离 - [x] 支持今日/历史 Tab 查询 - [x] 不合并点赞记录 -- [x] 支持未读数统计 +- [x] 支持未读数统计(事务保证一致性) - [x] 支持通知直达 -- [x] 支持批量操作 \ No newline at end of file +- [x] 支持批量操作 +- [x] INSERT ... ON CONFLICT DO UPDATE 处理首次创建统计 \ No newline at end of file