Add authenticated admin endpoints to create, edit, list, and archive announcements with safe HTML rendering.
273 lines
6.4 KiB
Go
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
|
|
}
|
|
}
|