feat: 公告系统 — Phase 2 完成
- Model/Repo/Service/Controller 全栈实现 - CRUD + ListActive(生效时间范围过滤) - 管理后台独立页面 /admin/announcements(列表+弹窗CRUD) - 前台首页 Hero 下方公告栏(info/success/warning/error 四色) - Moderator+ 权限可管理公告
This commit is contained in:
102
internal/controller/admin/announcement_controller.go
Normal file
102
internal/controller/admin/announcement_controller.go
Normal file
@ -0,0 +1,102 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"metazone.cc/mce/internal/common"
|
||||
"metazone.cc/mce/internal/model"
|
||||
"metazone.cc/mce/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AnnouncementController 公告管理控制器
|
||||
type AnnouncementController struct {
|
||||
svc announcementUseCase
|
||||
}
|
||||
|
||||
type announcementUseCase interface {
|
||||
Create(a *model.Announcement) error
|
||||
Update(a *model.Announcement) error
|
||||
Delete(id uint) error
|
||||
List(offset, limit int) ([]model.Announcement, int64, error)
|
||||
ListActive() ([]model.Announcement, error)
|
||||
}
|
||||
|
||||
// NewAnnouncementController 构造函数
|
||||
func NewAnnouncementController(svc announcementUseCase) *AnnouncementController {
|
||||
return &AnnouncementController{svc: svc}
|
||||
}
|
||||
|
||||
// Page 公告管理页面
|
||||
func (ac *AnnouncementController) Page(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "admin/announcements/index.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "公告管理",
|
||||
"ExtraCSS": "/admin/static/css/announcements.css",
|
||||
"ExtraJS": "/admin/static/js/announcements.js",
|
||||
"Types": service.AnnouncementTypes,
|
||||
}))
|
||||
}
|
||||
|
||||
// List 公告列表 API
|
||||
func (ac *AnnouncementController) List(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
list, total, err := ac.svc.List(offset, pageSize)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "查询失败")
|
||||
return
|
||||
}
|
||||
common.Ok(c, gin.H{"list": list, "total": total})
|
||||
}
|
||||
|
||||
// Create 创建公告 API
|
||||
func (ac *AnnouncementController) Create(c *gin.Context) {
|
||||
var req model.Announcement
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := ac.svc.Create(&req); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "创建失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "公告已创建")
|
||||
}
|
||||
|
||||
// Update 更新公告 API
|
||||
func (ac *AnnouncementController) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的 ID")
|
||||
return
|
||||
}
|
||||
var req model.Announcement
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
req.ID = uint(id)
|
||||
if err := ac.svc.Update(&req); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "更新失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "公告已更新")
|
||||
}
|
||||
|
||||
// Delete 删除公告 API
|
||||
func (ac *AnnouncementController) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的 ID")
|
||||
return
|
||||
}
|
||||
if err := ac.svc.Delete(uint(id)); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "删除失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "公告已删除")
|
||||
}
|
||||
17
internal/model/announcement.go
Normal file
17
internal/model/announcement.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Announcement 公告
|
||||
type Announcement struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title"`
|
||||
Content string `gorm:"type:text;not null" json:"content"`
|
||||
Type string `gorm:"type:varchar(20);default:info" json:"type"` // info / warning / success / error
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
StartAt *time.Time `json:"start_at,omitempty"`
|
||||
EndAt *time.Time `json:"end_at,omitempty"`
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
73
internal/repository/announcement_repo.go
Normal file
73
internal/repository/announcement_repo.go
Normal file
@ -0,0 +1,73 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"metazone.cc/mce/internal/model"
|
||||
"metazone.cc/mce/internal/service"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AnnouncementRepo 公告数据访问
|
||||
type AnnouncementRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAnnouncementRepo 构造函数
|
||||
func NewAnnouncementRepo(db *gorm.DB) *AnnouncementRepo {
|
||||
return &AnnouncementRepo{db: db}
|
||||
}
|
||||
|
||||
// WithTx 基于事务创建新实例
|
||||
func (r *AnnouncementRepo) WithTx(tx *gorm.DB) service.AnnouncementStore {
|
||||
return &AnnouncementRepo{db: tx}
|
||||
}
|
||||
|
||||
// Create 创建公告
|
||||
func (r *AnnouncementRepo) Create(a *model.Announcement) error {
|
||||
return r.db.Create(a).Error
|
||||
}
|
||||
|
||||
// Update 更新公告
|
||||
func (r *AnnouncementRepo) Update(a *model.Announcement) error {
|
||||
return r.db.Save(a).Error
|
||||
}
|
||||
|
||||
// Delete 删除公告
|
||||
func (r *AnnouncementRepo) Delete(id uint) error {
|
||||
return r.db.Delete(&model.Announcement{}, id).Error
|
||||
}
|
||||
|
||||
// FindByID 按 ID 查询
|
||||
func (r *AnnouncementRepo) FindByID(id uint) (*model.Announcement, error) {
|
||||
var a model.Announcement
|
||||
err := r.db.First(&a, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// List 分页查询全部公告(管理后台用)
|
||||
func (r *AnnouncementRepo) List(offset, limit int) ([]model.Announcement, int64, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.Announcement{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var list []model.Announcement
|
||||
err := r.db.Order("sort ASC, created_at DESC").Offset(offset).Limit(limit).Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// ListActive 查询当前生效中的公告(前台用,按 sort 排序)
|
||||
func (r *AnnouncementRepo) ListActive() ([]model.Announcement, error) {
|
||||
now := time.Now()
|
||||
var list []model.Announcement
|
||||
err := r.db.Where("is_active = true").
|
||||
Where("start_at IS NULL OR start_at <= ?", now).
|
||||
Where("end_at IS NULL OR end_at >= ?", now).
|
||||
Order("sort ASC, created_at DESC").
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
@ -32,6 +32,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
adminPages.GET("/energy/logs", d.adminEnergyCtrl.EnergyLogPage)
|
||||
adminPages.GET("/energy/fund-logs", d.adminEnergyCtrl.FundLogPage)
|
||||
adminPages.GET("/comments", d.adminCommentCtrl.CommentsPage)
|
||||
adminPages.GET("/announcements", d.announcementCtrl.Page)
|
||||
}
|
||||
|
||||
// --- 管理后台 API ---
|
||||
@ -99,4 +100,17 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
adminCommentAPI.GET("", d.adminCommentCtrl.ListComments)
|
||||
adminCommentAPI.DELETE("/:id", d.adminCommentCtrl.Delete)
|
||||
}
|
||||
|
||||
// --- 公告管理 API(moderator+) ---
|
||||
announcementAPI := r.Group("/api/admin/announcements")
|
||||
announcementAPI.Use(d.authMdw.Required())
|
||||
announcementAPI.Use(middleware.RequireMinRole(model.RoleModerator))
|
||||
announcementAPI.Use(d.authMdw.BannedWriteGuard())
|
||||
announcementAPI.Use(middleware.CSRF(cfg))
|
||||
{
|
||||
announcementAPI.GET("", d.announcementCtrl.List)
|
||||
announcementAPI.POST("", d.announcementCtrl.Create)
|
||||
announcementAPI.PUT("/:id", d.announcementCtrl.Update)
|
||||
announcementAPI.DELETE("/:id", d.announcementCtrl.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,8 +43,10 @@ type dependencies struct {
|
||||
followCtrl *controller.FollowController
|
||||
favoriteCtrl *controller.FavoriteController
|
||||
favoriteService *service.FavoriteService
|
||||
trendsService *service.TrendsService
|
||||
homeCache *cache.HomePageCache
|
||||
trendsService *service.TrendsService
|
||||
homeCache *cache.HomePageCache
|
||||
announcementService *service.AnnouncementService
|
||||
announcementCtrl *adminCtrl.AnnouncementController
|
||||
}
|
||||
|
||||
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) (
|
||||
|
||||
@ -103,6 +103,11 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
|
||||
maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr)
|
||||
|
||||
// 公告系统
|
||||
announcementRepo := repository.NewAnnouncementRepo(db)
|
||||
announcementService := service.NewAnnouncementService(announcementRepo)
|
||||
announcementCtrl := adminCtrl.NewAnnouncementController(announcementService)
|
||||
|
||||
// 首页内存缓存(30s TTL),文章变更时失效
|
||||
homeCache := cache.NewHomePageCache(30 * time.Second)
|
||||
postService.WithHomePageInvalidator(func() { homeCache.Invalidate() })
|
||||
@ -126,7 +131,9 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
||||
followCtrl: followCtrl,
|
||||
favoriteCtrl: favoriteCtrl,
|
||||
favoriteService: favoriteService,
|
||||
trendsService: trendsService,
|
||||
homeCache: homeCache,
|
||||
trendsService: trendsService,
|
||||
homeCache: homeCache,
|
||||
announcementService: announcementService,
|
||||
announcementCtrl: announcementCtrl,
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,11 +33,13 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
||||
d.homeCache.Set(posts, total)
|
||||
}
|
||||
}
|
||||
announcements, _ := d.announcementService.ListActive()
|
||||
c.HTML(http.StatusOK, "home/index.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "首页",
|
||||
"ExtraCSS": "/static/css/home.css",
|
||||
"Posts": posts,
|
||||
"PostCount": total,
|
||||
"Title": "首页",
|
||||
"ExtraCSS": "/static/css/home.css",
|
||||
"Posts": posts,
|
||||
"PostCount": total,
|
||||
"Announcements": announcements,
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
59
internal/service/announcement_service.go
Normal file
59
internal/service/announcement_service.go
Normal file
@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import "metazone.cc/mce/internal/model"
|
||||
|
||||
// AnnouncementTypes 公告类型及其显示名称和颜色
|
||||
var AnnouncementTypes = map[string]struct {
|
||||
Label string
|
||||
Color string
|
||||
}{
|
||||
"info": {"信息", "#3b82f6"},
|
||||
"success": {"成功", "#22c55e"},
|
||||
"warning": {"警告", "#f59e0b"},
|
||||
"error": {"错误", "#ef4444"},
|
||||
}
|
||||
|
||||
// AnnouncementStore 公告仓库接口(ISP)
|
||||
type AnnouncementStore interface {
|
||||
Create(a *model.Announcement) error
|
||||
Update(a *model.Announcement) error
|
||||
Delete(id uint) error
|
||||
FindByID(id uint) (*model.Announcement, error)
|
||||
List(offset, limit int) ([]model.Announcement, int64, error)
|
||||
ListActive() ([]model.Announcement, error)
|
||||
}
|
||||
|
||||
// AnnouncementService 公告业务逻辑
|
||||
type AnnouncementService struct {
|
||||
repo AnnouncementStore
|
||||
}
|
||||
|
||||
// NewAnnouncementService 构造函数
|
||||
func NewAnnouncementService(repo AnnouncementStore) *AnnouncementService {
|
||||
return &AnnouncementService{repo: repo}
|
||||
}
|
||||
|
||||
// Create 创建公告
|
||||
func (s *AnnouncementService) Create(a *model.Announcement) error {
|
||||
return s.repo.Create(a)
|
||||
}
|
||||
|
||||
// Update 更新公告
|
||||
func (s *AnnouncementService) Update(a *model.Announcement) error {
|
||||
return s.repo.Update(a)
|
||||
}
|
||||
|
||||
// Delete 删除公告
|
||||
func (s *AnnouncementService) Delete(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
// List 分页列表
|
||||
func (s *AnnouncementService) List(offset, limit int) ([]model.Announcement, int64, error) {
|
||||
return s.repo.List(offset, limit)
|
||||
}
|
||||
|
||||
// ListActive 生效中的公告
|
||||
func (s *AnnouncementService) ListActive() ([]model.Announcement, error) {
|
||||
return s.repo.ListActive()
|
||||
}
|
||||
Reference in New Issue
Block a user