2e2c161770
- backed/: Go Fiber backend with cpdaily integration - frondend/: Vue 3 + Vite frontend - Dockerfile: multi-stage build (Node → Go → Alpine) - deploy/docker-compose.yml: production compose file - .woodpecker/build.yml: CI/CD with blue-green zero-downtime deploy - /health endpoint for deployment health checks - Port 8884/8885 blue-green swap via 1Panel OpenResty
171 lines
4.5 KiB
Go
171 lines
4.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"cpdaily-new/internal/database"
|
|
"cpdaily-new/internal/model"
|
|
"cpdaily-new/internal/utils"
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// CreateAnnouncementRequest 创建公告请求
|
|
type CreateAnnouncementRequest struct {
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
TargetType string `json:"targetType"` // agent / user
|
|
ForcePopup bool `json:"forcePopup"`
|
|
LoopPopup bool `json:"loopPopup"`
|
|
}
|
|
|
|
// CreateAnnouncement 创建公告
|
|
func CreateAnnouncement(c *fiber.Ctx) error {
|
|
adminID := c.Locals("adminId").(uint)
|
|
|
|
var req CreateAnnouncementRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.FailParam(c, "请求参数错误")
|
|
}
|
|
if req.Title == "" {
|
|
return utils.FailParam(c, "公告标题不能为空")
|
|
}
|
|
if req.Content == "" {
|
|
return utils.FailParam(c, "公告内容不能为空")
|
|
}
|
|
if req.TargetType != "agent" && req.TargetType != "user" {
|
|
return utils.Fail(c, 400, "目标类型必须为 agent 或 user")
|
|
}
|
|
|
|
announcement := model.Announcement{
|
|
Title: req.Title,
|
|
Content: req.Content,
|
|
TargetType: req.TargetType,
|
|
ForcePopup: req.ForcePopup,
|
|
LoopPopup: req.LoopPopup,
|
|
CreatedBy: adminID,
|
|
}
|
|
|
|
if err := database.DB.Create(&announcement).Error; err != nil {
|
|
slog.Error("创建公告失败", "error", err)
|
|
return utils.FailServer(c, "创建公告失败")
|
|
}
|
|
|
|
slog.Info("创建公告", "id", announcement.ID, "title", req.Title, "targetType", req.TargetType)
|
|
return utils.Success(c, announcement)
|
|
}
|
|
|
|
// AnnouncementListItem 公告列表项(含统计)
|
|
type AnnouncementListItem struct {
|
|
model.Announcement
|
|
ReadCount int `json:"readCount"`
|
|
UnreadCount int `json:"unreadCount"`
|
|
TotalTargets int64 `json:"totalTargets"`
|
|
}
|
|
|
|
// ListAnnouncements 公告列表(含已读/未读统计)
|
|
func ListAnnouncements(c *fiber.Ctx) error {
|
|
var announcements []model.Announcement
|
|
database.DB.Order("created_at DESC").Find(&announcements)
|
|
|
|
// 获取目标总数
|
|
var totalAgents int64
|
|
var totalUsers int64
|
|
database.DB.Model(&model.AgentUser{}).Count(&totalAgents)
|
|
database.DB.Model(&model.SysUser{}).Count(&totalUsers)
|
|
|
|
items := make([]AnnouncementListItem, 0, len(announcements))
|
|
for _, a := range announcements {
|
|
var readCount int64
|
|
database.DB.Model(&model.AnnouncementRead{}).
|
|
Where("announcement_id = ?", a.ID).
|
|
Count(&readCount)
|
|
|
|
var totalTargets int64
|
|
if a.TargetType == "agent" {
|
|
totalTargets = totalAgents
|
|
} else {
|
|
totalTargets = totalUsers
|
|
}
|
|
|
|
items = append(items, AnnouncementListItem{
|
|
Announcement: a,
|
|
ReadCount: int(readCount),
|
|
UnreadCount: int(max(0, totalTargets-readCount)),
|
|
TotalTargets: totalTargets,
|
|
})
|
|
}
|
|
|
|
return utils.Success(c, items)
|
|
}
|
|
|
|
// GetAnnouncement 公告详情(含已读用户列表)
|
|
func GetAnnouncement(c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
|
|
var announcement model.Announcement
|
|
if err := database.DB.First(&announcement, id).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return utils.Fail(c, 404, "公告不存在")
|
|
}
|
|
return utils.FailServer(c, "查询失败")
|
|
}
|
|
|
|
// 查询已读记录(带用户信息)
|
|
type ReadUser struct {
|
|
UserID uint `json:"userId"`
|
|
Username string `json:"username"`
|
|
ReadAt time.Time `json:"readAt"`
|
|
}
|
|
|
|
var reads []model.AnnouncementRead
|
|
database.DB.Where("announcement_id = ?", announcement.ID).Find(&reads)
|
|
|
|
readUsers := make([]ReadUser, 0, len(reads))
|
|
for _, r := range reads {
|
|
username := ""
|
|
if r.UserType == "agent" {
|
|
var agent model.AgentUser
|
|
if err := database.DB.First(&agent, r.UserID).Error; err == nil {
|
|
username = agent.Username
|
|
}
|
|
} else {
|
|
var user model.SysUser
|
|
if err := database.DB.First(&user, r.UserID).Error; err == nil {
|
|
username = user.Username
|
|
}
|
|
}
|
|
readUsers = append(readUsers, ReadUser{
|
|
UserID: r.UserID,
|
|
Username: username,
|
|
ReadAt: r.ReadAt,
|
|
})
|
|
}
|
|
|
|
return utils.Success(c, fiber.Map{
|
|
"announcement": announcement,
|
|
"readUsers": readUsers,
|
|
})
|
|
}
|
|
|
|
// DeleteAnnouncement 删除公告
|
|
func DeleteAnnouncement(c *fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
|
|
result := database.DB.Delete(&model.Announcement{}, id)
|
|
if result.Error != nil {
|
|
return utils.FailServer(c, "删除失败")
|
|
}
|
|
if result.RowsAffected == 0 {
|
|
return utils.Fail(c, 404, "公告不存在")
|
|
}
|
|
|
|
// 同时删除已读记录
|
|
database.DB.Where("announcement_id = ?", id).Delete(&model.AnnouncementRead{})
|
|
|
|
slog.Info("删除公告", "id", id)
|
|
return utils.Success(c, nil)
|
|
}
|