Files
team-api/internal/logic/admin/notification.go
T

420 lines
11 KiB
Go

package admin
import (
"bytes"
"context"
"fmt"
"text/template"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
)
// ListTemplates 获取通知模板列表(分页)
func (s *sAdmin) ListTemplates(ctx context.Context, req *v1.TemplateListReq) (*v1.TemplateListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
var total int
items := make([]*v1.TemplateItem, 0)
err := dao.NtfTemplates.Ctx(ctx).
OrderAsc("code").
Page(page, pageSize).
ScanAndCount(&items, &total, false)
if err != nil {
return nil, err
}
return &v1.TemplateListRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// GetTemplate 获取单个通知模板
func (s *sAdmin) GetTemplate(ctx context.Context, req *v1.TemplateGetReq) (*v1.TemplateGetRes, error) {
record, err := dao.NtfTemplates.Ctx(ctx).
Where("code", req.Code).
One()
if err != nil {
return nil, err
}
if record == nil {
return nil, common.NewNotFoundError("template")
}
return &v1.TemplateGetRes{Data: record.Map()}, nil
}
// UpdateTemplate 更新通知模板
func (s *sAdmin) UpdateTemplate(ctx context.Context, req *v1.TemplateUpdateReq) (*v1.TemplateUpdateRes, error) {
data := do.NtfTemplates{}
if req.Subject != "" {
data.Subject = req.Subject
}
if req.BodyTemplate != "" {
data.BodyTemplate = req.BodyTemplate
}
if req.Channel != "" {
data.Channel = req.Channel
}
_, err := dao.NtfTemplates.Ctx(ctx).
Where("code", req.Code).
Data(data).
Update()
if err != nil {
return nil, err
}
return nil, nil
}
// TestTemplate 用测试变量渲染模板,返回渲染结果(不发送)
func (s *sAdmin) TestTemplate(ctx context.Context, req *v1.TemplateTestReq) (*v1.TemplateTestRes, error) {
var tpl struct {
Subject string `json:"subject"`
BodyTemplate string `json:"body_template"`
Channel string `json:"channel"`
}
err := dao.NtfTemplates.Ctx(ctx).
Where("code", req.Code).
Scan(&tpl)
if err != nil {
return nil, err
}
if tpl.BodyTemplate == "" {
return nil, common.NewNotFoundError("template")
}
type renderResult struct {
Original string `json:"original"`
Rendered string `json:"rendered"`
Error string `json:"error,omitempty"`
}
subjectRendered, subjectErr := renderGoTemplate(tpl.Subject, req.Variables)
bodyRendered, bodyErr := renderGoTemplate(tpl.BodyTemplate, req.Variables)
result := map[string]any{
"subject": renderResult{
Original: tpl.Subject,
Rendered: subjectRendered,
},
"body": renderResult{
Original: tpl.BodyTemplate,
Rendered: bodyRendered,
},
"channel": tpl.Channel,
}
if subjectErr != nil {
result["subject"] = renderResult{
Original: tpl.Subject,
Error: subjectErr.Error(),
}
}
if bodyErr != nil {
result["body"] = renderResult{
Original: tpl.BodyTemplate,
Error: bodyErr.Error(),
}
}
return &v1.TemplateTestRes{Data: result}, nil
}
// SendMessage 创建手动站内消息并推送 WebSocket 通知
func (s *sAdmin) SendMessage(ctx context.Context, req *v1.MessageSendReq) (*v1.MessageSendRes, error) {
if req.Title == "" || req.Content == "" {
return nil, common.NewBadRequestError("title and content are required")
}
engine := common.NewNotificationEngine()
if req.UserID > 0 {
return nil, engine.SendMessage(ctx, req.TenantID, req.UserID, "system", req.Title, req.Content)
}
// 未指定用户时发送广播
return nil, engine.SendBroadcastMessage(ctx, req.TenantID, "system", req.Title, req.Content, "")
}
// SendBroadcast 创建广播消息并推送 WebSocket 通知
func (s *sAdmin) SendBroadcast(ctx context.Context, req *v1.MessageBroadcastReq) (*v1.MessageBroadcastRes, error) {
if req.Title == "" || req.Content == "" {
return nil, common.NewBadRequestError("title and content are required")
}
engine := common.NewNotificationEngine()
if req.TenantID != nil && *req.TenantID > 0 {
return nil, engine.SendBroadcastMessage(ctx, *req.TenantID, "system", req.Title, req.Content, req.TargetRoles)
}
// 广播到所有活跃租户
var tenants []struct {
Id int64 `json:"id"`
}
err := dao.TntTenants.Ctx(ctx).Where("status", "active").Fields("id").Scan(&tenants)
if err != nil {
return nil, err
}
for _, t := range tenants {
if e := engine.SendBroadcastMessage(ctx, t.Id, "system", req.Title, req.Content, req.TargetRoles); e != nil {
g.Log().Warningf(ctx, "SendBroadcast to tenant %d failed: %v", t.Id, e)
}
}
return nil, nil
}
// ListMessages 获取所有消息列表(管理后台,支持过滤)
func (s *sAdmin) ListMessages(ctx context.Context, req *v1.MessageListReq) (*v1.MessageListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.NtfMessages.Ctx(ctx).
LeftJoin("tnt_tenants t", "t.id = ntf_messages.tenant_id").
LeftJoin("tnt_users u", "u.id = ntf_messages.user_id").
Fields("ntf_messages.*, t.name as tenant_name, u.display_name as user_name")
if req.TenantID > 0 {
query = query.Where("ntf_messages.tenant_id", req.TenantID)
}
if req.Type != "" {
query = query.Where("ntf_messages.type", req.Type)
}
var total int
items := make([]*v1.MessageItem, 0)
err := query.OrderDesc("ntf_messages.created_at").
Page(page, pageSize).
ScanAndCount(&items, &total, false)
if err != nil {
return nil, err
}
return &v1.MessageListRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// GetMessageReadStats 获取广播消息的已读统计
func GetMessageReadStats(ctx context.Context, messageID int64) (map[string]any, error) {
var msg struct {
IsBroadcast int `json:"is_broadcast"`
TenantID int64 `json:"tenant_id"`
}
err := dao.NtfMessages.Ctx(ctx).
Where("id", messageID).
Fields("is_broadcast, tenant_id").
Scan(&msg)
if err != nil {
return nil, err
}
if msg.IsBroadcast != 1 {
return nil, common.NewBadRequestError("message is not a broadcast")
}
totalMembers, err := dao.TntUsers.Ctx(ctx).
Where("tenant_id", msg.TenantID).
Where("status", "active").
Count()
if err != nil {
return nil, err
}
readCount, err := dao.NtfReadStatus.Ctx(ctx).
Where("message_id", messageID).
Count()
if err != nil {
return nil, err
}
unreadCount := totalMembers - readCount
if unreadCount < 0 {
unreadCount = 0
}
readRate := float64(0)
if totalMembers > 0 {
readRate = float64(readCount) / float64(totalMembers) * 100
}
return map[string]any{
"message_id": messageID,
"total_members": totalMembers,
"read_count": readCount,
"unread_count": unreadCount,
"read_rate": fmt.Sprintf("%.1f%%", readRate),
}, nil
}
// CreateAnnouncement 创建公告
func (s *sAdmin) CreateAnnouncement(ctx context.Context, req *v1.AnnouncementCreateReq) (*v1.AnnouncementCreateRes, error) {
if req.Title == "" || req.Content == "" {
return nil, common.NewBadRequestError("title and content are required")
}
annType := req.Type
if annType == "" {
annType = "info"
}
status := req.Status
if status == "" {
status = "draft"
}
displayPosition := req.DisplayPosition
if displayPosition == "" {
displayPosition = "console"
}
// Get admin user ID from context
adminUserID := common.GetCtxUserID(ctx)
data := do.NtfAnnouncements{
Title: req.Title,
Type: annType,
Content: req.Content,
Status: status,
IsPinned: req.IsPinned,
DisplayPosition: displayPosition,
CreatedBy: adminUserID,
}
if req.EffectiveAt != "" {
if t, err := time.Parse(time.RFC3339, req.EffectiveAt); err == nil {
data.EffectiveAt = gtime.NewFromTime(t)
}
}
if req.ExpiresAt != "" {
if t, err := time.Parse(time.RFC3339, req.ExpiresAt); err == nil {
data.ExpiresAt = gtime.NewFromTime(t)
}
}
result, err := dao.NtfAnnouncements.Ctx(ctx).Insert(data)
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return &v1.AnnouncementCreateRes{ID: id}, nil
}
// UpdateAnnouncement 更新公告
func (s *sAdmin) UpdateAnnouncement(ctx context.Context, req *v1.AnnouncementUpdateReq) (*v1.AnnouncementUpdateRes, error) {
data := do.NtfAnnouncements{}
if req.Title != "" {
data.Title = req.Title
}
if req.Type != "" {
data.Type = req.Type
}
if req.Content != "" {
data.Content = req.Content
}
if req.Status != "" {
data.Status = req.Status
}
if req.IsPinned != nil {
data.IsPinned = *req.IsPinned
}
if req.DisplayPosition != "" {
data.DisplayPosition = req.DisplayPosition
}
if req.EffectiveAt != "" {
if t, err := gtime.StrToTime(req.EffectiveAt); err == nil {
data.EffectiveAt = t
}
}
if req.ExpiresAt != "" {
if t, err := gtime.StrToTime(req.ExpiresAt); err == nil {
data.ExpiresAt = t
}
}
_, err := dao.NtfAnnouncements.Ctx(ctx).
Where("id", req.Id).
Data(data).
Update()
if err != nil {
return nil, err
}
return nil, nil
}
// ListAnnouncements 获取公告列表(分页)
func (s *sAdmin) ListAnnouncements(ctx context.Context, req *v1.AnnouncementListReq) (*v1.AnnouncementListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.NtfAnnouncements.Ctx(ctx)
if req.Status != "" {
query = query.Where("status", req.Status)
}
var total int
items := make([]*v1.AnnouncementItem, 0)
err := query.OrderDesc("is_pinned").
OrderDesc("created_at").
Page(page, pageSize).
ScanAndCount(&items, &total, false)
if err != nil {
return nil, err
}
return &v1.AnnouncementListRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// PublishAnnouncement 发布公告
func (s *sAdmin) PublishAnnouncement(ctx context.Context, req *v1.AnnouncementPublishReq) (*v1.AnnouncementPublishRes, error) {
_, err := dao.NtfAnnouncements.Ctx(ctx).
Where("id", req.Id).
Data(do.NtfAnnouncements{
Status: "published",
}).Update()
if err != nil {
return nil, err
}
return nil, nil
}
// ArchiveAnnouncement 归档公告
func (s *sAdmin) ArchiveAnnouncement(ctx context.Context, req *v1.AnnouncementArchiveReq) (*v1.AnnouncementArchiveRes, error) {
_, err := dao.NtfAnnouncements.Ctx(ctx).
Where("id", req.Id).
Data(do.NtfAnnouncements{
Status: "archived",
}).Update()
if err != nil {
return nil, err
}
return nil, nil
}
// renderGoTemplate renders a Go template string with variables.
func renderGoTemplate(tplStr string, vars map[string]any) (string, error) {
if tplStr == "" {
return "", nil
}
t, err := template.New("").Parse(tplStr)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := t.Execute(&buf, vars); err != nil {
return "", err
}
return buf.String(), nil
}