feat: add user announcement endpoints

Expose active announcements to authenticated users, track read state, and feed active database announcements into /api/status with legacy fallback.
This commit is contained in:
zizi 2026-05-20 13:15:35 +08:00
parent d9d79638c0
commit eec9f4dcae
3 changed files with 185 additions and 12 deletions

View File

@ -0,0 +1,117 @@
package controller
import (
"errors"
"strconv"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type userAnnouncementResponse struct {
model.Announcement
IsRead bool `json:"is_read"`
}
func GetUserAnnouncements(c *gin.Context) {
userId := c.GetInt("id")
unreadOnly := c.Query("unread") == "true"
var announcements []model.Announcement
var err error
if unreadOnly {
announcements, err = model.GetUnreadAnnouncements(userId)
} else {
announcements, err = model.GetActiveAnnouncements()
}
if err != nil {
common.ApiError(c, err)
return
}
result := make([]userAnnouncementResponse, 0, len(announcements))
for _, announcement := range announcements {
isRead := false
if !unreadOnly {
isRead, err = model.IsAnnouncementRead(announcement.Id, userId)
if err != nil {
common.ApiError(c, err)
return
}
}
result = append(result, userAnnouncementResponse{
Announcement: announcement,
IsRead: isRead,
})
}
common.ApiSuccess(c, result)
}
func MarkAnnouncementRead(c *gin.Context) {
announcementId, err := strconv.Atoi(c.Param("id"))
if err != nil || announcementId <= 0 {
common.ApiErrorMsg(c, "无效的公告 ID")
return
}
announcement, err := model.GetAnnouncementByID(announcementId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "公告不存在或不可见")
return
}
common.ApiError(c, err)
return
}
if !isAnnouncementVisible(announcement) {
common.ApiErrorMsg(c, "公告不存在或不可见")
return
}
userId := c.GetInt("id")
isRead, err := model.IsAnnouncementRead(announcementId, userId)
if err != nil {
common.ApiError(c, err)
return
}
if isRead {
common.ApiSuccess(c, nil)
return
}
read := &model.AnnouncementRead{
AnnouncementId: announcementId,
UserId: userId,
ReadAt: time.Now(),
}
if err := read.Insert(); err != nil {
isRead, checkErr := model.IsAnnouncementRead(announcementId, userId)
if checkErr == nil && isRead {
common.ApiSuccess(c, nil)
return
}
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
func isAnnouncementVisible(announcement *model.Announcement) bool {
if announcement.Status != announcementStatusActive {
return false
}
now := time.Now()
if announcement.StartsAt != nil && announcement.StartsAt.After(now) {
return false
}
if announcement.EndsAt != nil && announcement.EndsAt.Before(now) {
return false
}
return true
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
@ -43,10 +44,23 @@ func GetStatus(c *gin.Context) {
cs := console_setting.GetConsoleSetting()
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()
passkeySetting := system_setting.GetPasskeySettings()
legalSetting := system_setting.GetLegalSettings()
apiInfoEnabled := cs.ApiInfoEnabled
announcementsEnabled := cs.AnnouncementsEnabled
faqEnabled := cs.FAQEnabled
var apiInfo []map[string]interface{}
if apiInfoEnabled {
apiInfo = console_setting.GetApiInfo()
}
legacyAnnouncements := console_setting.GetAnnouncements()
var faq []map[string]interface{}
if faqEnabled {
faq = console_setting.GetFAQ()
}
headerNavModules := common.OptionMap["HeaderNavModules"]
sidebarModulesAdmin := common.OptionMap["SidebarModulesAdmin"]
data := gin.H{
"version": common.Version,
@ -94,14 +108,14 @@ func GetStatus(c *gin.Context) {
"stripe_unit_price": setting.StripeUnitPrice,
// 面板启用开关
"api_info_enabled": cs.ApiInfoEnabled,
"api_info_enabled": apiInfoEnabled,
"uptime_kuma_enabled": cs.UptimeKumaEnabled,
"announcements_enabled": cs.AnnouncementsEnabled,
"faq_enabled": cs.FAQEnabled,
"announcements_enabled": announcementsEnabled,
"faq_enabled": faqEnabled,
// 模块管理配置
"HeaderNavModules": common.OptionMap["HeaderNavModules"],
"SidebarModulesAdmin": common.OptionMap["SidebarModulesAdmin"],
"HeaderNavModules": headerNavModules,
"SidebarModulesAdmin": sidebarModulesAdmin,
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
@ -118,16 +132,17 @@ func GetStatus(c *gin.Context) {
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
}
common.OptionMapRWMutex.RUnlock()
// 根据启用状态注入可选内容
if cs.ApiInfoEnabled {
data["api_info"] = console_setting.GetApiInfo()
if apiInfoEnabled {
data["api_info"] = apiInfo
}
if cs.AnnouncementsEnabled {
data["announcements"] = console_setting.GetAnnouncements()
if announcementsEnabled {
data["announcements"] = getStatusAnnouncements(legacyAnnouncements)
}
if cs.FAQEnabled {
data["faq"] = console_setting.GetFAQ()
if faqEnabled {
data["faq"] = faq
}
// Add enabled custom OAuth providers
@ -166,6 +181,45 @@ func GetStatus(c *gin.Context) {
return
}
func getStatusAnnouncements(fallback []map[string]interface{}) []map[string]interface{} {
announcements, err := model.GetActiveAnnouncements()
if err == nil && len(announcements) > 0 {
result := make([]map[string]interface{}, 0, len(announcements))
for _, announcement := range announcements {
result = append(result, buildStatusAnnouncement(announcement))
}
return result
}
return fallback
}
func buildStatusAnnouncement(announcement model.Announcement) map[string]interface{} {
publishDate := announcement.CreatedAt
if announcement.StartsAt != nil {
publishDate = *announcement.StartsAt
}
if publishDate.IsZero() {
publishDate = time.Now()
}
return map[string]interface{}{
"id": announcement.Id,
"title": announcement.Title,
"content": announcement.Content,
"content_html": announcement.ContentHtml,
"type": "default",
"extra": announcement.Title,
"publishDate": publishDate.Format(time.RFC3339),
"status": announcement.Status,
"notify_mode": announcement.NotifyMode,
"json_rules": announcement.JsonRules,
"starts_at": announcement.StartsAt,
"ends_at": announcement.EndsAt,
"created_at": announcement.CreatedAt,
"updated_at": announcement.UpdatedAt,
}
}
func GetNotice(c *gin.Context) {
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()

View File

@ -22,6 +22,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/setup", controller.PostSetup)
apiRouter.GET("/status", controller.GetStatus)
apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
apiRouter.GET("/announcements", middleware.UserAuth(), controller.GetUserAnnouncements)
apiRouter.POST("/announcements/:id/read", middleware.UserAuth(), controller.MarkAnnouncementRead)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
apiRouter.GET("/notice", controller.GetNotice)