feat(server/notices): GET /v1/notices(合并+unread_count)+ POST /read + openapi 扩展

This commit is contained in:
wangjia
2026-07-13 13:43:50 +08:00
parent ed1f805cbc
commit 0325603470
4 changed files with 170 additions and 5 deletions
+1
View File
@@ -218,6 +218,7 @@ func (a *AccountAPI) ListPlans(w http.ResponseWriter, r *http.Request) {
// ─── GET /v1/notices ─────────────────────────────────────────────────────────
// ListNotices handles GET /v1/notices. Returns an empty list for the MVP.
// TODO(Task7): 由 notices.Handler 替换后删除(main.go 路由挂载在 Task 7 统一装配)。
func (a *AccountAPI) ListNotices(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"notices": []any{}})
+61
View File
@@ -0,0 +1,61 @@
package notices
import (
"encoding/json"
"net/http"
"time"
"github.com/wangjia/pangolin/server/internal/apierr"
"github.com/wangjia/pangolin/server/internal/auth"
)
// listLimit 是 GET /v1/notices 单次返回的最大条数(合并广播+定向后按 published_at 倒序截断)。
const listLimit = 50
// Handler 承载 /v1/notices、/v1/notices/read 两个受保护端点。
type Handler struct {
st *Store
}
func NewHandler(st *Store) *Handler {
return &Handler{st: st}
}
// List handles GET /v1/notices: 返回该用户可见的通知(广播 ∪ 定向)与未读数。
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid, ok := auth.UserIDFromContext(ctx)
if !ok {
apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
return
}
items, unreadCount, err := h.st.ListForUser(ctx, uid, time.Now().UTC(), listLimit)
if err != nil {
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
return
}
if items == nil {
items = []Notice{}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{
"notices": items,
"unread_count": unreadCount,
})
}
// MarkRead handles POST /v1/notices/read: 把用户已读水位推进到当前时间。
func (h *Handler) MarkRead(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid, ok := auth.UserIDFromContext(ctx)
if !ok {
apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
return
}
if err := h.st.MarkRead(ctx, uid, time.Now().UTC()); err != nil {
apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
+64
View File
@@ -0,0 +1,64 @@
package notices
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/wangjia/pangolin/server/internal/codes"
)
func TestListAndMarkRead(t *testing.T) {
db := openDB(t)
seedU(t, db, 1, "u1")
st := NewStore(db)
_, _ = st.InsertBroadcast(context.Background(), "news", "hello", "hello", "", "", "", time.Now().UTC(), nil)
h := NewHandler(st)
req := httptest.NewRequest(http.MethodGet, "/v1/notices", nil)
req = req.WithContext(context.WithValue(req.Context(), codes.CtxKeyUserID, int64(1)))
w := httptest.NewRecorder()
h.List(w, req)
if w.Code != 200 {
t.Fatalf("code=%d body=%s", w.Code, w.Body)
}
var got struct {
Notices []map[string]any `json:"notices"`
UnreadCount int `json:"unread_count"`
}
_ = json.Unmarshal(w.Body.Bytes(), &got)
if len(got.Notices) != 1 || got.UnreadCount != 1 {
t.Fatalf("got %+v", got)
}
if got.Notices[0]["type"] != "news" || got.Notices[0]["unread"] != true {
t.Fatalf("字段契约: %+v", got.Notices[0])
}
// read → 再查 unread 清零
req2 := httptest.NewRequest(http.MethodPost, "/v1/notices/read", nil)
req2 = req2.WithContext(context.WithValue(req2.Context(), codes.CtxKeyUserID, int64(1)))
w2 := httptest.NewRecorder()
h.MarkRead(w2, req2)
if w2.Code != 200 {
t.Fatalf("read code=%d", w2.Code)
}
w3 := httptest.NewRecorder()
h.List(w3, req)
_ = json.Unmarshal(w3.Body.Bytes(), &got)
if got.UnreadCount != 0 {
t.Fatalf("read 后 unread_count=%d", got.UnreadCount)
}
}
func TestListUnauthorized(t *testing.T) {
db := openDB(t)
h := NewHandler(NewStore(db))
w := httptest.NewRecorder()
h.List(w, httptest.NewRequest(http.MethodGet, "/v1/notices", nil))
if w.Code != http.StatusUnauthorized {
t.Fatalf("code=%d want 401", w.Code)
}
}