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 }