topfans/backend/gateway/controller/notification_controller.go
2026-06-16 21:30:58 +08:00

456 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controller
import (
"context"
"net/http"
"strconv"
"time"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/gin-gonic/gin"
pbNotif "github.com/topfans/backend/pkg/proto/notification"
"github.com/topfans/backend/gateway/pkg/response"
"github.com/topfans/backend/pkg/logger"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
)
// NotificationController 通知相关控制器
type NotificationController struct {
notifService pbNotif.NotificationService
}
// NewNotificationController 创建通知控制器
func NewNotificationController(dubboClient *client.Client) (*NotificationController, error) {
notifService, err := pbNotif.NewNotificationService(dubboClient)
if err != nil {
return nil, err
}
return &NotificationController{
notifService: notifService,
}, nil
}
// parseInt 解析 query string 为 int, 失败或空返回默认值
func parseInt(s string, def int) int {
if s == "" {
return def
}
n, err := strconv.Atoi(s)
if err != nil {
return def
}
return n
}
// GetNotifications 获取通知列表
// @Summary 获取通知列表
// @Description 获取当前用户的通知列表(支持 type/tab 分页)
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param type query string false "通知类型过滤: like / system / activity"
// @Param tab query string false "列表 tab: unread / read / all"
// @Param page query int false "页码默认1"
// @Param page_size query int false "每页数量默认20"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications [get]
func (ctrl *NotificationController) GetNotifications(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.GetNotifications(ctx, &pbNotif.GetNotificationsRequest{
Type: g.Query("type"),
Tab: g.Query("tab"),
Page: int32(parseInt(g.Query("page"), 1)),
PageSize: int32(parseInt(g.Query("page_size"), 20)),
})
if err != nil {
logger.Logger.Error("GetNotifications RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
// 转换为 map 列表(Notification 含 structpb.Struct 序列化友好)
items := make([]map[string]interface{}, 0, len(resp.Items))
for _, n := range resp.Items {
items = append(items, convertNotification(n))
}
response.Success(g, gin.H{
"items": items,
"total": resp.Total,
"page": resp.Page,
"page_size": resp.PageSize,
})
}
// GetUnreadCount 获取未读通知数
// @Summary 获取未读通知数
// @Description 按类型返回未读数量(like/system/activity/total)
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/unread-count [get]
func (ctrl *NotificationController) GetUnreadCount(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.GetUnreadCount(ctx, &pbNotif.GetUnreadCountRequest{})
if err != nil {
logger.Logger.Error("GetUnreadCount RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
counts := gin.H{"like": 0, "system": 0, "activity": 0, "total": 0}
if resp.Counts != nil {
counts = gin.H{
"like": resp.Counts.Like,
"system": resp.Counts.System,
"activity": resp.Counts.Activity,
"total": resp.Counts.Total,
}
}
response.Success(g, counts)
}
// MarkAsRead 标记单条通知已读
// @Summary 标记单条通知已读
// @Description 根据通知ID标记为已读
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "通知ID"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/{id}/read [post]
func (ctrl *NotificationController) MarkAsRead(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
idStr := g.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.Error(g, http.StatusBadRequest, "参数错误: id 必须为正整数")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.MarkAsRead(ctx, &pbNotif.MarkAsReadRequest{Id: id})
if err != nil {
logger.Logger.Error("MarkAsRead RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Int64("notification_id", id),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
response.Success(g, gin.H{"id": id})
}
// MarkAsReadByTarget 按 target_id 标记已读
// @Summary 按目标ID标记已读
// @Description 将同一 target 下的所有通知标记为已读
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param target_id path int true "目标ID(如藏品ID)"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/targets/{target_id}/read [post]
func (ctrl *NotificationController) MarkAsReadByTarget(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
targetIDStr := g.Param("target_id")
targetID, err := strconv.ParseInt(targetIDStr, 10, 64)
if err != nil || targetID <= 0 {
response.Error(g, http.StatusBadRequest, "参数错误: target_id 必须为正整数")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.MarkAsReadByTarget(ctx, &pbNotif.MarkAsReadByTargetRequest{
TargetId: targetID,
})
if err != nil {
logger.Logger.Error("MarkAsReadByTarget RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Int64("target_id", targetID),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
response.Success(g, gin.H{
"target_id": targetID,
"affected": resp.Affected,
})
}
// MarkAllAsRead 全部已读
// @Summary 全部已读
// @Description 将当前用户某类型或全部通知标记为已读
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param type query string false "通知类型过滤: like / system / activity; 留空表示全部"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/read-all [post]
func (ctrl *NotificationController) MarkAllAsRead(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.MarkAllAsRead(ctx, &pbNotif.MarkAllAsReadRequest{
Type: g.Query("type"),
})
if err != nil {
logger.Logger.Error("MarkAllAsRead RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.String("type", g.Query("type")),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
response.Success(g, gin.H{
"affected": resp.Affected,
})
}
// DeleteNotification 删除单条通知
// @Summary 删除单条通知
// @Description 根据ID删除通知
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "通知ID"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/{id} [delete]
func (ctrl *NotificationController) DeleteNotification(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
idStr := g.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
response.Error(g, http.StatusBadRequest, "参数错误: id 必须为正整数")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.DeleteNotification(ctx, &pbNotif.DeleteNotificationRequest{Id: id})
if err != nil {
logger.Logger.Error("DeleteNotification RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Int64("notification_id", id),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
response.Success(g, gin.H{"id": id})
}
// DeleteByTarget 按 target_id 删除通知
// @Summary 按目标ID删除通知
// @Description 删除同一 target 下的所有通知
// @Tags notifications
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param target_id path int true "目标ID(如藏品ID)"
// @Success 200 {object} response.Response
// @Router /api/v1/notifications/targets/{target_id} [delete]
func (ctrl *NotificationController) DeleteByTarget(g *gin.Context) {
userID, _ := g.Get("user_id")
starID, _ := g.Get("star_id")
targetIDStr := g.Param("target_id")
targetID, err := strconv.ParseInt(targetIDStr, 10, 64)
if err != nil || targetID <= 0 {
response.Error(g, http.StatusBadRequest, "参数错误: target_id 必须为正整数")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
resp, err := ctrl.notifService.DeleteByTarget(ctx, &pbNotif.DeleteByTargetRequest{
TargetId: targetID,
})
if err != nil {
logger.Logger.Error("DeleteByTarget RPC failed",
zap.Int64("user_id", userID.(int64)),
zap.Int64("target_id", targetID),
zap.Error(err),
)
response.Error(g, http.StatusInternalServerError, "服务调用失败")
return
}
if resp.Base.Code != uint32(codes.OK) {
response.ErrorWithCode(g, int(resp.Base.Code), resp.Base.Message)
return
}
response.Success(g, gin.H{
"target_id": targetID,
"affected": resp.Affected,
})
}
// convertNotification 将 *pbNotif.Notification 转为前端友好的 map
// (proto 序列化时 structpb.Struct 不友好, 转成 map[string]interface{})
func convertNotification(n *pbNotif.Notification) map[string]interface{} {
if n == nil {
return nil
}
item := map[string]interface{}{
"id": n.Id,
"user_id": n.UserId,
"star_id": n.StarId,
"type": n.Type,
"title": n.Title,
"content": n.Content,
"is_read": n.IsRead,
"created_at": n.CreatedAt,
"read_at": n.ReadAt,
"target_id": n.TargetId,
"aggregated": n.Aggregated,
"total_count": n.TotalCount,
}
// data 字段: *structpb.Struct → map
if n.Data != nil {
item["data"] = n.Data.AsMap()
}
// actors 字段
if len(n.Actors) > 0 {
actors := make([]map[string]interface{}, 0, len(n.Actors))
for _, a := range n.Actors {
if a == nil {
continue
}
actors = append(actors, map[string]interface{}{
"user_id": a.UserId,
"nickname": a.Nickname,
"avatar": a.Avatar,
"liked_at": a.LikedAt,
})
}
item["actors"] = actors
}
return item
}