feat(notify): http handler for inbox list / unread count / mark read

This commit is contained in:
2026-04-28 22:37:19 +08:00
parent 13032c1192
commit 0b323b91ee
+151
View File
@@ -0,0 +1,151 @@
// 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)
}