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
212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"cpdaily-new/internal/database"
|
|
"cpdaily-new/internal/model"
|
|
"cpdaily-new/internal/utils"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// getCurrentUser 从上下文中获取当前用户信息(支持普通用户和代理/管理员)
|
|
func getCurrentUser(c *fiber.Ctx) (userID uint, userType string, err error) {
|
|
if adminID, ok := c.Locals("adminId").(uint); ok {
|
|
return adminID, "agent", nil
|
|
}
|
|
if userID, ok := c.Locals("userId").(uint); ok {
|
|
return userID, "user", nil
|
|
}
|
|
return 0, "", fmt.Errorf("未认证")
|
|
}
|
|
|
|
// UserAnnouncementItem 用户视角的公告项
|
|
type UserAnnouncementItem struct {
|
|
model.Announcement
|
|
IsRead bool `json:"isRead"`
|
|
ReadAt *time.Time `json:"readAt,omitempty"`
|
|
}
|
|
|
|
// ListUserAnnouncements 获取当前用户的公告列表(含已读状态)
|
|
func ListUserAnnouncements(c *fiber.Ctx) error {
|
|
userID, userType, err := getCurrentUser(c)
|
|
if err != nil {
|
|
return utils.FailAuth(c, "未认证")
|
|
}
|
|
|
|
var announcements []model.Announcement
|
|
database.DB.Where("target_type = ?", userType).
|
|
Order("created_at DESC").
|
|
Find(&announcements)
|
|
|
|
// 查询该用户的已读记录
|
|
readIDs := make(map[uint]time.Time)
|
|
var reads []model.AnnouncementRead
|
|
database.DB.Where("user_id = ? AND user_type = ?", userID, userType).Find(&reads)
|
|
for _, r := range reads {
|
|
readIDs[r.AnnouncementID] = r.ReadAt
|
|
}
|
|
|
|
items := make([]UserAnnouncementItem, 0, len(announcements))
|
|
for _, a := range announcements {
|
|
readAt, isRead := readIDs[a.ID]
|
|
item := UserAnnouncementItem{
|
|
Announcement: a,
|
|
IsRead: isRead,
|
|
}
|
|
if isRead {
|
|
item.ReadAt = &readAt
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
return utils.Success(c, items)
|
|
}
|
|
|
|
// GetUnreadCount 获取未读公告数
|
|
func GetUnreadCount(c *fiber.Ctx) error {
|
|
userID, userType, err := getCurrentUser(c)
|
|
if err != nil {
|
|
return utils.FailAuth(c, "未认证")
|
|
}
|
|
|
|
var totalCount int64
|
|
database.DB.Model(&model.Announcement{}).
|
|
Where("target_type = ?", userType).
|
|
Count(&totalCount)
|
|
|
|
var readCount int64
|
|
database.DB.Model(&model.AnnouncementRead{}).
|
|
Where("user_id = ? AND user_type = ?", userID, userType).
|
|
Count(&readCount)
|
|
|
|
unread := int(totalCount) - int(readCount)
|
|
if unread < 0 {
|
|
unread = 0
|
|
}
|
|
|
|
return utils.Success(c, fiber.Map{
|
|
"total": totalCount,
|
|
"read": readCount,
|
|
"unread": unread,
|
|
})
|
|
}
|
|
|
|
// PendingPopupItem 待弹窗公告项
|
|
type PendingPopupItem struct {
|
|
ID uint `json:"id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
ForcePopup bool `json:"forcePopup"`
|
|
LoopPopup bool `json:"loopPopup"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
// MyAnnouncementInfo 我的公告信息聚合
|
|
type MyAnnouncementInfo struct {
|
|
UnreadCount int `json:"unreadCount"`
|
|
PopupQueue []PendingPopupItem `json:"popupQueue"` // 需要弹窗的公告(依次弹出)
|
|
}
|
|
|
|
// GetMyAnnouncementInfo 获取当前用户的公告聚合信息(未读数 + 待弹窗队列)
|
|
func GetMyAnnouncementInfo(c *fiber.Ctx) error {
|
|
userID, userType, err := getCurrentUser(c)
|
|
if err != nil {
|
|
return utils.FailAuth(c, "未认证")
|
|
}
|
|
|
|
// 1. 总公告数
|
|
var totalCount int64
|
|
database.DB.Model(&model.Announcement{}).
|
|
Where("target_type = ?", userType).
|
|
Count(&totalCount)
|
|
|
|
// 2. 已读数
|
|
var readCount int64
|
|
database.DB.Model(&model.AnnouncementRead{}).
|
|
Where("user_id = ? AND user_type = ?", userID, userType).
|
|
Count(&readCount)
|
|
|
|
unread := int(totalCount) - int(readCount)
|
|
if unread < 0 {
|
|
unread = 0
|
|
}
|
|
|
|
// 3. 待弹窗队列:forcePopup=true 且 (loopPopup=true 或 未读)
|
|
var forceAnnouncements []model.Announcement
|
|
database.DB.Where("target_type = ? AND force_popup = 1", userType).
|
|
Order("created_at ASC").
|
|
Find(&forceAnnouncements)
|
|
|
|
// 查已读记录
|
|
readIDs := make(map[uint]bool)
|
|
var reads []model.AnnouncementRead
|
|
database.DB.Where("user_id = ? AND user_type = ?", userID, userType).Find(&reads)
|
|
for _, r := range reads {
|
|
readIDs[r.AnnouncementID] = true
|
|
}
|
|
|
|
popupQueue := make([]PendingPopupItem, 0, len(forceAnnouncements))
|
|
for _, a := range forceAnnouncements {
|
|
if a.LoopPopup || !readIDs[a.ID] {
|
|
popupQueue = append(popupQueue, PendingPopupItem{
|
|
ID: a.ID,
|
|
Title: a.Title,
|
|
Content: a.Content,
|
|
ForcePopup: a.ForcePopup,
|
|
LoopPopup: a.LoopPopup,
|
|
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
}
|
|
|
|
return utils.Success(c, MyAnnouncementInfo{
|
|
UnreadCount: unread,
|
|
PopupQueue: popupQueue,
|
|
})
|
|
}
|
|
|
|
// MarkAnnouncementRead 标记公告为已读
|
|
func MarkAnnouncementRead(c *fiber.Ctx) error {
|
|
userID, userType, err := getCurrentUser(c)
|
|
if err != nil {
|
|
return utils.FailAuth(c, "未认证")
|
|
}
|
|
|
|
announcementID := c.Params("id")
|
|
|
|
// 验证公告存在
|
|
var announcement model.Announcement
|
|
if err := database.DB.First(&announcement, announcementID).Error; err != nil {
|
|
return utils.Fail(c, 404, "公告不存在")
|
|
}
|
|
|
|
// 验证公告目标类型匹配
|
|
if announcement.TargetType != userType {
|
|
return utils.Fail(c, 403, "无权操作此公告")
|
|
}
|
|
|
|
// 检查是否已读
|
|
var existing model.AnnouncementRead
|
|
result := database.DB.Where("announcement_id = ? AND user_id = ? AND user_type = ?",
|
|
announcementID, userID, userType).First(&existing)
|
|
if result.Error == nil {
|
|
// 已读过了
|
|
return utils.Success(c, fiber.Map{"alreadyRead": true})
|
|
}
|
|
|
|
// 创建已读记录
|
|
read := model.AnnouncementRead{
|
|
AnnouncementID: announcement.ID,
|
|
UserID: userID,
|
|
UserType: userType,
|
|
ReadAt: time.Now(),
|
|
}
|
|
if err := database.DB.Create(&read).Error; err != nil {
|
|
return utils.FailServer(c, "标记失败")
|
|
}
|
|
|
|
return utils.Success(c, fiber.Map{"alreadyRead": false})
|
|
}
|