feat(notification): 新增全部标记已读接口
This commit is contained in:
@@ -60,6 +60,22 @@ func (d *NotificationDao) NotificationUpdateReadByUUIDIfUnread(ctx context.Conte
|
|||||||
return res.RowsAffected > 0, nil
|
return res.RowsAffected > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotificationUpdateReadAllByUserID 将该用户全部未读通知标记为已读,返回影响行数。
|
||||||
|
// 条件带 is_read = 1,保证只统计本次真正从未读变为已读的条数(幂等,重复调用返回 0)。
|
||||||
|
func (d *NotificationDao) NotificationUpdateReadAllByUserID(ctx context.Context, userID int64) (int64, error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.NotificationUpdateReadAllByUserID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
res := d.db.WithContext(ctx).Model(&mysqlmodel.UserNotification{}).
|
||||||
|
Where("user_id = ? AND is_read = ?", userID, int8(1)).
|
||||||
|
Update("is_read", int8(2))
|
||||||
|
if res.Error != nil {
|
||||||
|
span.RecordError(res.Error)
|
||||||
|
return 0, res.Error
|
||||||
|
}
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NotificationGetByUUID 按 UUID 查询通知
|
// NotificationGetByUUID 按 UUID 查询通知
|
||||||
func (d *NotificationDao) NotificationGetByUUID(ctx context.Context, notificationUUID string) (*mysqlmodel.UserNotification, error) {
|
func (d *NotificationDao) NotificationGetByUUID(ctx context.Context, notificationUUID string) (*mysqlmodel.UserNotification, error) {
|
||||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.NotificationGetByUUID")
|
ctx, span := global.Tracer.Start(ctx, "mysqldao.NotificationGetByUUID")
|
||||||
|
|||||||
@@ -77,6 +77,22 @@ func (h *NotificationHandler) Get(c *gin.Context) {
|
|||||||
response.ToResponse(out)
|
response.ToResponse(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadAll 将当前用户全部通知标记为已读(前端进入通知页时调用)。
|
||||||
|
func (h *NotificationHandler) ReadAll(c *gin.Context) {
|
||||||
|
response := app.NewResponse(c)
|
||||||
|
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||||
|
if !ok {
|
||||||
|
response.ToErrorResponse(errcode.Unauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, e := h.svc.ReadAll(c.Request.Context(), userID)
|
||||||
|
if e != nil {
|
||||||
|
response.ToErrorResponse(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ToResponse(out)
|
||||||
|
}
|
||||||
|
|
||||||
// Read 标记通知已读(uuid 来自路径参数)。
|
// Read 标记通知已读(uuid 来自路径参数)。
|
||||||
func (h *NotificationHandler) Read(c *gin.Context) {
|
func (h *NotificationHandler) Read(c *gin.Context) {
|
||||||
response := app.NewResponse(c)
|
response := app.NewResponse(c)
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ func NewRouter() *gin.Engine {
|
|||||||
// 通知
|
// 通知
|
||||||
auth.GET("/notification", notificationHandler.List)
|
auth.GET("/notification", notificationHandler.List)
|
||||||
auth.GET("/notification/unread", notificationHandler.Unread)
|
auth.GET("/notification/unread", notificationHandler.Unread)
|
||||||
|
// 静态路径 /notification/read-all 与 /notification/:uuid/read 段数不同,不会互相抢占
|
||||||
|
auth.POST("/notification/read-all", notificationHandler.ReadAll)
|
||||||
auth.GET("/notification/:uuid", notificationHandler.Get)
|
auth.GET("/notification/:uuid", notificationHandler.Get)
|
||||||
auth.POST("/notification/:uuid/read", notificationHandler.Read)
|
auth.POST("/notification/:uuid/read", notificationHandler.Read)
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,26 @@ func (s *NotificationService) Read(ctx context.Context, userID int64, request *d
|
|||||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadAll 将当前用户全部未读通知标记为已读,并回填未读数缓存。
|
||||||
|
// 与单条已读一致:以 MySQL 为权威重新统计后返回,避免 Redis 计数偏差。
|
||||||
|
func (s *NotificationService) ReadAll(ctx context.Context, userID int64) (*dto.NotificationUnreadResponse, *errcode.Error) {
|
||||||
|
ctx, span := global.Tracer.Start(ctx, "svc.NotificationReadAll")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
affected, err := s.notificationDao.NotificationUpdateReadAllByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "mark all notifications read failed", "user_id", userID, "error", err)
|
||||||
|
return nil, errcode.DatabaseError
|
||||||
|
}
|
||||||
|
slog.InfoContext(ctx, "mark all notifications read", "user_id", userID, "affected", affected)
|
||||||
|
|
||||||
|
cnt, cerr := s.unreadCount(ctx, userID)
|
||||||
|
if cerr != nil {
|
||||||
|
return nil, cerr
|
||||||
|
}
|
||||||
|
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Unread 获取当前用户未读数(以 MySQL 为权威)。
|
// Unread 获取当前用户未读数(以 MySQL 为权威)。
|
||||||
func (s *NotificationService) Unread(ctx context.Context, userID int64) (*dto.NotificationUnreadResponse, *errcode.Error) {
|
func (s *NotificationService) Unread(ctx context.Context, userID int64) (*dto.NotificationUnreadResponse, *errcode.Error) {
|
||||||
ctx, span := global.Tracer.Start(ctx, "svc.NotificationUnread")
|
ctx, span := global.Tracer.Start(ctx, "svc.NotificationUnread")
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ Authorization: Bearer <access_token>
|
|||||||
| POST | `/like` | 点赞 / 取消点赞 |
|
| POST | `/like` | 点赞 / 取消点赞 |
|
||||||
| GET | `/like/:targetUUID` | 查询目标点赞状态与数量 |
|
| GET | `/like/:targetUUID` | 查询目标点赞状态与数量 |
|
||||||
|
|
||||||
**通知(4)**
|
**通知(5)**
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -342,6 +342,7 @@ Authorization: Bearer <access_token>
|
|||||||
| GET | `/notification/unread` | 未读通知数 |
|
| GET | `/notification/unread` | 未读通知数 |
|
||||||
| GET | `/notification/:uuid` | 通知详情 |
|
| GET | `/notification/:uuid` | 通知详情 |
|
||||||
| POST | `/notification/:uuid/read` | 标记单条已读 |
|
| POST | `/notification/:uuid/read` | 标记单条已读 |
|
||||||
|
| POST | `/notification/read-all` | 全部标记已读(前端进入通知页时调用) |
|
||||||
|
|
||||||
**订单(4)**
|
**订单(4)**
|
||||||
|
|
||||||
@@ -1637,7 +1638,7 @@ Authorization: Bearer <access_token>
|
|||||||
**业务逻辑**
|
**业务逻辑**
|
||||||
|
|
||||||
- **条件更新**(仅"未读→已读"),防并发重复扣减未读数;已读时直接返回当前未读数,幂等。
|
- **条件更新**(仅"未读→已读"),防并发重复扣减未读数;已读时直接返回当前未读数,幂等。
|
||||||
- ⚠️ **没有"全部已读"接口**,需要全标已读时前端需逐条调用。
|
- ⚠️ 本接口只影响该条;需要**全部已读**请用 `POST /notification/read-all`(见下节),无需逐条调用。
|
||||||
|
|
||||||
**请求**(path)
|
**请求**(path)
|
||||||
|
|
||||||
@@ -1651,6 +1652,25 @@ Authorization: Bearer <access_token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### POST `/notification/read-all` — 全部标记已读(鉴权)
|
||||||
|
|
||||||
|
**业务逻辑**
|
||||||
|
|
||||||
|
- 将当前用户**全部未读**通知一次性置为已读(`is_read` 1 → 2);条件更新(仅未读→已读),**幂等**,重复调用不影响结果。
|
||||||
|
- ⚠️ **无请求体、无 query 参数**,作用于当前登录用户的全部通知,不受列表页 `msg_type` / `is_read` 筛选影响。
|
||||||
|
- 以 MySQL 为权威重新统计未读数并回填 Redis 缓存后返回,前端可直接用返回值刷新红点,**无需再调** `GET /notification/unread`。
|
||||||
|
- 前端约定:`pages/notification` 在**列表渲染完成后**调用(先拉列表保留本次进入的未读标记,再清零服务端状态与红点),实现「点开通知页即全部已读」。
|
||||||
|
|
||||||
|
**响应** `data`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| unread_count | int64 | 标记后的未读通知数(正常为 `0`) |
|
||||||
|
|
||||||
|
**错误码**:`19001` 数据库错误 · `19002` 缓存错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 5.11 商品模块(公开,无需登录)
|
### 5.11 商品模块(公开,无需登录)
|
||||||
|
|
||||||
**`goods_type` 枚举**:`1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包
|
**`goods_type` 枚举**:`1`=会员包 `2`=上传包 `3`=AI 推荐包 `4`=衣柜分享包 `5`=搭配分享包
|
||||||
|
|||||||
Reference in New Issue
Block a user