new-api/controller/announcement.go
zizi d9d79638c0 feat: add announcement admin API
Add authenticated admin endpoints to create, edit, list, and archive announcements with safe HTML rendering.
2026-05-20 13:06:58 +08:00

273 lines
6.4 KiB
Go

package controller
import (
"errors"
"html"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const (
announcementStatusDraft = "draft"
announcementStatusActive = "active"
announcementStatusArchived = "archived"
announcementNotifyModeSilent = "silent"
announcementNotifyModePopup = "popup"
)
type createAnnouncementRequest struct {
Title string `json:"title"`
Content string `json:"content"`
NotifyMode string `json:"notify_mode"`
Status string `json:"status"`
JsonRules string `json:"json_rules"`
StartsAt string `json:"starts_at"`
EndsAt string `json:"ends_at"`
}
type updateAnnouncementRequest struct {
Title *string `json:"title"`
Content *string `json:"content"`
Status *string `json:"status"`
NotifyMode *string `json:"notify_mode"`
JsonRules *string `json:"json_rules"`
StartsAt *string `json:"starts_at"`
EndsAt *string `json:"ends_at"`
}
func GetAnnouncements(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
status := strings.TrimSpace(c.Query("status"))
if status != "" && !isValidAnnouncementStatus(status) {
common.ApiErrorMsg(c, "status 必须是 draft、active 或 archived")
return
}
announcements, total, err := model.GetAnnouncements(pageInfo.GetPage(), pageInfo.GetPageSize(), status)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(announcements)
common.ApiSuccess(c, pageInfo)
}
func CreateAnnouncement(c *gin.Context) {
var req createAnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
title := strings.TrimSpace(req.Title)
if title == "" {
common.ApiErrorMsg(c, "title 不能为空")
return
}
if strings.TrimSpace(req.Content) == "" {
common.ApiErrorMsg(c, "content 不能为空")
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = announcementStatusDraft
}
if !isValidAnnouncementStatus(status) {
common.ApiErrorMsg(c, "status 必须是 draft、active 或 archived")
return
}
notifyMode := strings.TrimSpace(req.NotifyMode)
if notifyMode == "" {
notifyMode = announcementNotifyModeSilent
}
if !isValidAnnouncementNotifyMode(notifyMode) {
common.ApiErrorMsg(c, "notify_mode 必须是 silent 或 popup")
return
}
startsAt, err := parseAnnouncementTime(req.StartsAt)
if err != nil {
common.ApiErrorMsg(c, "starts_at 必须是 RFC3339 格式")
return
}
endsAt, err := parseAnnouncementTime(req.EndsAt)
if err != nil {
common.ApiErrorMsg(c, "ends_at 必须是 RFC3339 格式")
return
}
announcement := &model.Announcement{
Title: title,
Content: req.Content,
ContentHtml: renderAnnouncementContentHTML(req.Content),
Status: status,
NotifyMode: notifyMode,
JsonRules: req.JsonRules,
StartsAt: startsAt,
EndsAt: endsAt,
CreatedBy: c.GetInt("id"),
}
if err := announcement.Insert(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, announcement)
}
func GetAnnouncement(c *gin.Context) {
announcement, ok := getAnnouncementFromParam(c)
if !ok {
return
}
common.ApiSuccess(c, announcement)
}
func UpdateAnnouncement(c *gin.Context) {
announcement, ok := getAnnouncementFromParam(c)
if !ok {
return
}
var req updateAnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
common.ApiErrorMsg(c, "title 不能为空")
return
}
announcement.Title = title
}
if req.Content != nil {
if strings.TrimSpace(*req.Content) == "" {
common.ApiErrorMsg(c, "content 不能为空")
return
}
announcement.Content = *req.Content
announcement.ContentHtml = renderAnnouncementContentHTML(*req.Content)
}
if req.Status != nil {
status := strings.TrimSpace(*req.Status)
if !isValidAnnouncementStatus(status) {
common.ApiErrorMsg(c, "status 必须是 draft、active 或 archived")
return
}
announcement.Status = status
}
if req.NotifyMode != nil {
notifyMode := strings.TrimSpace(*req.NotifyMode)
if !isValidAnnouncementNotifyMode(notifyMode) {
common.ApiErrorMsg(c, "notify_mode 必须是 silent 或 popup")
return
}
announcement.NotifyMode = notifyMode
}
if req.JsonRules != nil {
announcement.JsonRules = *req.JsonRules
}
if req.StartsAt != nil {
startsAt, err := parseAnnouncementTime(*req.StartsAt)
if err != nil {
common.ApiErrorMsg(c, "starts_at 必须是 RFC3339 格式")
return
}
announcement.StartsAt = startsAt
}
if req.EndsAt != nil {
endsAt, err := parseAnnouncementTime(*req.EndsAt)
if err != nil {
common.ApiErrorMsg(c, "ends_at 必须是 RFC3339 格式")
return
}
announcement.EndsAt = endsAt
}
if err := announcement.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, announcement)
}
func DeleteAnnouncement(c *gin.Context) {
announcement, ok := getAnnouncementFromParam(c)
if !ok {
return
}
announcement.Status = announcementStatusArchived
if err := announcement.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
func getAnnouncementFromParam(c *gin.Context) (*model.Announcement, bool) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return nil, false
}
announcement, err := model.GetAnnouncementByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "公告不存在")
return nil, false
}
common.ApiError(c, err)
return nil, false
}
return announcement, true
}
func parseAnnouncementTime(value string) (*time.Time, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, err
}
return &parsed, nil
}
func renderAnnouncementContentHTML(content string) string {
escaped := html.EscapeString(content)
return strings.ReplaceAll(escaped, "\n", "<br>\n")
}
func isValidAnnouncementStatus(status string) bool {
switch status {
case announcementStatusDraft, announcementStatusActive, announcementStatusArchived:
return true
default:
return false
}
}
func isValidAnnouncementNotifyMode(notifyMode string) bool {
switch notifyMode {
case announcementNotifyModeSilent, announcementNotifyModePopup:
return true
default:
return false
}
}