You've already forked agentic-coding-workflow
feat(notify): http handler for inbox list / unread count / mark read
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user