Merge branch 'dev' of https://git.liantu.tech/xiaoyu/topfans into feat/dev/plugin-verify

This commit is contained in:
liulong 2026-05-15 14:01:53 +08:00
commit c57bae20a9
2 changed files with 354 additions and 13 deletions

View File

@ -0,0 +1,345 @@
# 通知系统设计方案
**日期**: 2026-05-15
**状态**: 已确认
**版本**: v1.0
---
## 1. 概述
### 1.1 目标
构建统一的通知系统,支持以下通知类型:
- **点赞通知** - 用户点赞藏品时触发
- **系统通知** - 后台运营人员/系统触发
- **活动通知** - 系统活动事件触发
### 1.2 设计原则
- **统一存储** - 所有通知类型使用同一张表,通过 `type` 字段区分
- **轻量服务** - Notification Service 只负责存储和查询,不包含业务逻辑
- **扩展性** - JSON 字段存储类型特定的扩展数据,便于后续扩展新通知类型
---
## 2. 数据库设计
### 2.1 通知主表
```sql
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), -- 通知内容
data JSONB, -- 扩展数据(类型特定)
is_read BOOLEAN DEFAULT FALSE, -- 是否已读
is_deleted BOOLEAN DEFAULT FALSE, -- 是否删除(软删除)
created_at BIGINT NOT NULL, -- 创建时间(毫秒时间戳)
read_at BIGINT, -- 阅读时间(毫秒时间戳)
-- 索引
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)
);
```
### 2.2 通知统计表
用于快速查询未读数,支持 TabBar 角标显示。
```sql
CREATE TABLE notification_stats (
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, -- 更新时间
PRIMARY KEY (user_id, star_id)
);
```
> **注意**`notification_stats` 使用 `(user_id, star_id)` 作为联合主键,支持多 star 场景下各 star 独立统计未读数。
### 2.3 JSON data 字段示例
```json
// 点赞通知 (type: "like")
{
"target_type": "asset",
"target_id": 123,
"actor_id": 456,
"actor_name": "张三",
"actor_avatar": "https://example.com/avatar/456.png",
"asset_title": "我的藏品",
"asset_cover": "https://example.com/asset/123/cover.png",
"star_id": 1
}
// 系统通知 (type: "system")
{
"action_type": "url",
"action_url": "/pages/settings/detail",
"action_text": "查看详情",
"attachments": ["https://example.com/img1.jpg"]
}
// 活动通知 (type: "activity")
{
"activity_id": 789,
"activity_title": "端午节活动",
"activity_cover": "https://example.com/activity/789/cover.png",
"reward_type": "badge",
"reward_name": "端午限定徽章",
"star_id": 1
}
```
---
## 3. 服务架构
### 3.1 Notification Service 职责
| 模块 | 职责 |
|-----|-----|
| Repository | 通知记录的 CRUD查询列表 |
| Service | 业务逻辑:写入通知、更新统计、查询未读 |
| Provider | RPC 接口,供其他服务调用 |
### 3.2 服务边界
- **Notification Service** 只负责通知的存储和查询
- **业务逻辑** 由各自的服务处理(如点赞由 Social Service 处理)
- 其他服务在业务发生时调用 Notification Service 写入通知
### 3.3 调用关系
```
┌─────────────────┐ ┌─────────────────────┐
│ Social Service │────▶│ Notification Service │
│ (点赞业务) │ │ (存储 + 查询) │
└─────────────────┘ └─────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ Asset Service │ │ PostgreSQL │
│ (更新点赞数) │ │ notifications + │
└─────────────────┘ │ notification_stats │
└─────────────────────┘
```
> **事务边界**Notification Service 在数据库事务中同时写入 `notifications` 表和更新 `notification_stats` 表。
---
## 4. API 接口设计
### 4.1 RPC 接口(供内部服务调用)
| 方法 | 描述 | 参数 |
|-----|------|------|
| 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 接口(供前端调用)
| 方法 | 路径 | 描述 |
|-----|------|------|
| GET | /api/v1/notifications | 查询通知列表 |
| GET | /api/v1/notifications/unread-count | 获取未读数 |
| POST | /api/v1/notifications/:id/read | 标记单条已读 |
| POST | /api/v1/notifications/read-all | 全部标已读 |
| DELETE | /api/v1/notifications/:id | 删除通知 |
> **说明**:所有 HTTP 接口都需要通过 Header 或 Cookie 传递 `star_id` 进行数据隔离。
### 4.3 查询参数
```
GET /api/v1/notifications?type=like&tab=today&page=1&pageSize=20
参数说明:
- type: 通知类型 (like / system / activity)
- tab: 查询tab (today / history)
- page: 页码
- pageSize: 每页数量
- star_id: 数据隔离 ID从 Header 或上下文获取)
```
---
## 5. 功能详细设计
### 5.1 点赞通知生成流程
```
1. 用户点赞 → Social Service 处理
2. Social Service 调用 Asset Service RPC 更新点赞数
3. Social Service 调用 Notification Service CreateNotification
4. Notification Service在事务中完成:
- 写入 notifications 表
- 更新 notification_stats 表 (+1 未读数INSERT ... ON CONFLICT DO UPDATE 处理首次创建)
5. 返回成功响应
```
> **事务保证**:创建通知和更新统计必须在同一个事务中,确保数据一致性。
### 5.2 查询逻辑
```
今日 Tab:
WHERE type = 'like' AND user_id = ? AND star_id = ? AND created_at >= 今日零点
ORDER BY created_at DESC
历史 Tab:
WHERE type = 'like' AND user_id = ? AND star_id = ? AND created_at < 今日零点
ORDER BY created_at DESC
```
> **说明**:所有查询都需要 `star_id` 确保数据隔离。
### 5.3 未读数统计
- 每次创建通知时,在同一事务中更新 `notification_stats` 表对应类型的未读数
- 使用 `INSERT ... ON CONFLICT DO UPDATE` 确保首次创建时自动插入记录
- 每次标记已读时,减少对应类型的未读数
- 批量标已读时,重置对应类型的未读数为 0
- 删除通知时,同步减少未读数
### 5.4 通知直达
| 通知类型 | 跳转逻辑 |
|---------|---------|
| like | 跳转藏品详情页: `/pages/asset/detail?id={target_id}` |
| system | 跳转 `data.action_url` 指定页面 |
| activity | 跳转活动详情页: `/pages/activity/detail?id={activity_id}` |
---
## 6. 支持的功能
### 6.1 TabBar 角标
- 查询 `notification_stats` 表获取各类型未读数
- 前端展示: 点赞未读数、系统未读数、活动未读数
### 6.2 今日/历史 Tab
- 今日: 当天 00:00:00 至今的点赞通知
- 历史: 更早的点赞通知
### 6.3 通知直达
- 点击通知跳转到对应详情页面
### 6.4 批量操作
- 全部标为已读(支持按类型)
- 删除历史通知(软删除)
---
## 7. 记录合并规则
- **不合并记录** - 同一用户对同一藏品当天多次点赞,每条点赞都产生独立通知
- 例如: 用户A当天点赞藏品B 3次产生3条独立记录
---
## 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.3 补偿机制
如果事务提交后 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 -- 通知 CRUD
│ └── notification_stats_repository.go -- 统计更新
├── service/
│ └── notification_service.go -- 业务逻辑 + 事务处理
├── provider/
│ └── notification_provider.go -- RPC 接口
├── model/
│ └── notification.go -- 数据模型
└── client/
└── (供其他服务调用的客户端,如需要)
```
---
## 10. 后续扩展
- 支持评论通知 (type: "comment")
- 支持 @提及通知 (type: "mention")
- 支持推送功能(实时通知)
---
## 11. 确认点
- [x] 统一通知表存储所有类型
- [x] 独立 Notification Service
- [x] 支持 star_id 数据隔离
- [x] 支持今日/历史 Tab 查询
- [x] 不合并点赞记录
- [x] 支持未读数统计(事务保证一致性)
- [x] 支持通知直达
- [x] 支持批量操作
- [x] INSERT ... ON CONFLICT DO UPDATE 处理首次创建统计

View File

@ -47,16 +47,10 @@
│ nickname │ │ nickname │
│ avatar_url │ │ avatar_url │
└─────────────────┘ └─────────────────┘
```
│ LEFT JOIN
> 注:`activity_contributions.item_id` 已存储道具 ID查询时无需 JOIN activity_items 表。
┌─────────────────┐ > 前端传递 item_id后端根据 item_id 单独查询 activity_items 获取 item_name、item_icon。
│ activity_items │
│─────────────────│
│ id │
│ item_name │
│ icon_url │
└─────────────────┘
``` ```
### 2.2 表结构 ### 2.2 表结构
@ -194,15 +188,16 @@ Gateway 调用 ActivityService直接查 Repository
查询 activity_contributions 表,并联表查询用户信息和道具信息 查询 activity_contributions 表,并联表查询用户信息和道具信息
SELECT c.id, c.activity_id, c.user_id, SELECT c.id, c.activity_id, c.user_id,
u.nickname as user_nickname, u.avatar_url as user_avatar, u.nickname as user_nickname, u.avatar_url as user_avatar,
i.item_type, i.item_name, i.icon_url as item_icon, c.item_type, c.item_id,
c.quantity, c.contribution_points, c.created_at c.quantity, c.contribution_points, c.created_at
FROM activity_contributions c FROM activity_contributions c
LEFT JOIN users u ON c.user_id = u.id LEFT JOIN users u ON c.user_id = u.id
LEFT JOIN activity_items i ON c.item_id = i.id
WHERE c.activity_id = ? WHERE c.activity_id = ?
AND (c.created_at > ? OR (c.created_at = ? AND c.id > ?)) AND (c.created_at > ? OR (c.created_at = ? AND c.id > ?))
ORDER BY c.created_at DESC, c.id DESC ORDER BY c.created_at DESC, c.id DESC
LIMIT limit LIMIT limit
-- 再根据返回的 item_id 列表,单独查询 activity_items 获取 item_name、item_icon
组装响应 组装响应
@ -651,7 +646,8 @@ onHide(() => {
## 9. 实施步骤 ## 9. 实施步骤
### 9.1 后端实现 ### 9.1 后端实现
1. [x] 在 `activity_contributions` 表联表查询用户信息和道具信息LEFT JOIN users, LEFT JOIN activity_items 1. [x] 在 `activity_contributions` 表查询用户信息(仅 LEFT JOIN users
2. [ ] 根据 item_id 单独查询 activity_items 获取 item_name、item_icon
2. [ ] 在 Redis 中实现连击计数器 `combo:{user_id}:{item_type}`TTL 3秒 2. [ ] 在 Redis 中实现连击计数器 `combo:{user_id}:{item_type}`TTL 3秒
3. [ ] 在 `activity_repository.go` 添加 `GetLatestContributions` 方法 3. [ ] 在 `activity_repository.go` 添加 `GetLatestContributions` 方法
4. [ ] 在 Gateway 层添加 `contribution_controller.go` 4. [ ] 在 Gateway 层添加 `contribution_controller.go`