456 lines
13 KiB
Go
456 lines
13 KiB
Go
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
|
||
}
|