feat: add announcement admin API
Add authenticated admin endpoints to create, edit, list, and archive announcements with safe HTML rendering.
This commit is contained in:
parent
6a409b7724
commit
d9d79638c0
272
controller/announcement.go
Normal file
272
controller/announcement.go
Normal file
@ -0,0 +1,272 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -304,6 +304,15 @@ func SetApiRouter(router *gin.Engine) {
|
||||
promoCodeRoute.DELETE("/:id", controller.DeletePromoCode)
|
||||
promoCodeRoute.GET("/:id/usages", controller.GetPromoCodeUsages)
|
||||
}
|
||||
announcementAdminRoute := apiRouter.Group("/admin/announcements")
|
||||
announcementAdminRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
announcementAdminRoute.GET("", controller.GetAnnouncements)
|
||||
announcementAdminRoute.POST("", controller.CreateAnnouncement)
|
||||
announcementAdminRoute.GET("/:id", controller.GetAnnouncement)
|
||||
announcementAdminRoute.PUT("/:id", controller.UpdateAnnouncement)
|
||||
announcementAdminRoute.DELETE("/:id", controller.DeleteAnnouncement)
|
||||
}
|
||||
logRoute := apiRouter.Group("/log")
|
||||
logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs)
|
||||
logRoute.DELETE("/", middleware.AdminAuth(), controller.DeleteHistoryLogs)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user