// Package notify 内的 handler.go 是通知 inbox 的 HTTP 接入层。它把已经 // 鉴权完成的请求路由到 Repository 的对应方法,并按统一的 response.go // 风格(Google API 风格错误体)返回。 // // 端点(统一前缀 /api/v1/notifications,全部需要登录): // // GET / → 列表,参数 ?unread=true&limit=N // GET /unread-count → 未读计数 // PATCH /read-all → 全部标已读 // PATCH /{id}/read → 单条标已读 package notify import ( "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http" "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" ) // Handler 暴露 notify 模块的 HTTP 接口;依赖 Repository 与 SessionResolver。 // 不直接持有 Dispatcher——发送通知由其它模块通过 Dispatcher 完成,handler // 仅服务 inbox 读取与状态变更。 type Handler struct { repo Repository resolver middleware.SessionResolver } // NewHandler 构造 notify 模块的 HTTP handler。 func NewHandler(repo Repository, resolver middleware.SessionResolver) *Handler { return &Handler{repo: repo, resolver: resolver} } // Mount 把 inbox 路由挂到给定 chi.Router 上。Auth 中间件在子路由内统一注入。 func (h *Handler) Mount(r chi.Router) { r.Route("/api/v1/notifications", func(r chi.Router) { r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{})) r.Get("/", h.list) r.Get("/unread-count", h.unreadCount) r.Patch("/read-all", h.readAll) r.Patch("/{id}/read", h.markRead) }) } // notificationDTO 是 inbox 单条通知的对外 JSON 形态。created_at/read_at // 统一序列化为 RFC3339 字符串;read_at 为 nil 时省略(omitempty)。 type notificationDTO struct { ID string `json:"id"` Topic string `json:"topic"` Severity string `json:"severity"` Title string `json:"title"` Body string `json:"body"` Link string `json:"link,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` ReadAt *string `json:"read_at,omitempty"` CreatedAt string `json:"created_at"` } func (h *Handler) list(w http.ResponseWriter, r *http.Request) { uid, ok := middleware.UserIDFromContext(r.Context()) if !ok { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeUnauthorized, "未认证")) return } unread := r.URL.Query().Get("unread") == "true" limit := 50 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 200 { limit = n } } msgs, err := h.repo.List(r.Context(), uid, unread, limit) if err != nil { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) return } out := make([]notificationDTO, 0, len(msgs)) for _, m := range msgs { dto := notificationDTO{ ID: m.ID.String(), Topic: m.Topic, Severity: string(m.Severity), Title: m.Title, Body: m.Body, Link: m.Link, Metadata: m.Metadata, CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339), } if m.ReadAt != nil { s := m.ReadAt.UTC().Format(time.RFC3339) dto.ReadAt = &s } out = append(out, dto) } httpx.WriteJSON(w, http.StatusOK, map[string]any{"items": out}) } func (h *Handler) unreadCount(w http.ResponseWriter, r *http.Request) { uid, ok := middleware.UserIDFromContext(r.Context()) if !ok { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeUnauthorized, "未认证")) return } n, err := h.repo.CountUnread(r.Context(), uid) if err != nil { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"count": n}) } func (h *Handler) markRead(w http.ResponseWriter, r *http.Request) { uid, ok := middleware.UserIDFromContext(r.Context()) if !ok { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeUnauthorized, "未认证")) return } id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeInvalidInput, "id 格式错误")) return } if err := h.repo.MarkRead(r.Context(), uid, id); err != nil { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) readAll(w http.ResponseWriter, r *http.Request) { uid, ok := middleware.UserIDFromContext(r.Context()) if !ok { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), errs.New(errs.CodeUnauthorized, "未认证")) return } if err := h.repo.MarkAllRead(r.Context(), uid); err != nil { httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err) return } w.WriteHeader(http.StatusNoContent) }