service.xpcool.com/internal/service/notice/notice.go

353 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package notice 提供统一通知服务(渠道/规则/发送/日志)。
// 发送实现Bark 走路径式 keyPOST {baseUrl}/{deviceKey} JSONpushplus 走官方 send 接口。
package notice
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"service.xpcool.com/internal/consts"
"service.xpcool.com/internal/dao"
"service.xpcool.com/internal/library/response"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/model/entity"
)
// INotice 统一通知服务接口。
type INotice interface {
ChannelList(context.Context) ([]dto.NoticeChannelVO, error)
ChannelSave(context.Context, dto.NoticeChannelInput) (uint64, error)
RuleList(context.Context, string) ([]dto.NoticeRuleVO, error)
RuleSave(context.Context, dto.NoticeRuleInput) (uint64, error)
RuleDelete(context.Context, uint64) error
LogList(context.Context, dto.NoticeLogFilter) ([]dto.NoticeLogVO, int, error)
Send(context.Context, string, map[string]string) error // 按启用规则发送通知(事件→渠道→人员)
Test(context.Context, uint64, string, string, []uint64, string, string) (bool, string, error)
}
type notice struct{}
var localNotice INotice
// New 创建通知服务实现。
func New() INotice { return &notice{} }
// Notice 返回已注册的通知服务实现。
func Notice() INotice {
if localNotice == nil {
panic("Notice implementation not registered")
}
return localNotice
}
// RegisterNotice 注册通知服务实现。
func RegisterNotice(i INotice) { localNotice = i }
// ---------------- 渠道 ----------------
func (s *notice) ChannelList(ctx context.Context) ([]dto.NoticeChannelVO, error) {
var list []entity.NoticeChannel
if err := dao.NoticeChannel.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query notice channels")
}
out := make([]dto.NoticeChannelVO, 0, len(list))
for _, v := range list {
out = append(out, dto.NoticeChannelVO{Id: v.Id, Code: v.Code, Name: v.Name, Enabled: v.Enabled, Config: v.Config, Remark: v.Remark})
}
return out, nil
}
func (s *notice) ChannelSave(ctx context.Context, in dto.NoticeChannelInput) (uint64, error) {
data := map[string]interface{}{
"code": in.Code, "name": in.Name, "enabled": in.Enabled,
"config": in.Config, "remark": in.Remark,
}
if in.Id > 0 {
if _, err := dao.NoticeChannel.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return 0, gerror.Wrap(err, "update notice channel")
}
return in.Id, nil
}
id, err := dao.NoticeChannel.Ctx(ctx).Data(data).InsertAndGetId()
if err != nil {
return 0, gerror.Wrap(err, "insert notice channel")
}
return uint64(id), nil
}
// ---------------- 规则 ----------------
func (s *notice) RuleList(ctx context.Context, eventType string) ([]dto.NoticeRuleVO, error) {
m := dao.NoticeRule.Ctx(ctx)
if eventType != "" {
m = m.Where("event_type", eventType)
}
var list []entity.NoticeRule
if err := m.OrderAsc("id").Scan(&list); err != nil {
return nil, gerror.Wrap(err, "query notice rules")
}
out := make([]dto.NoticeRuleVO, 0, len(list))
for _, v := range list {
out = append(out, dto.NoticeRuleVO{
Id: v.Id, Name: v.Name, EventType: v.EventType,
ChannelCodes: parseStrArray(v.ChannelCodes), UserIds: parseUintArray(v.UserIds),
TitleTemplate: v.TitleTemplate, BodyTemplate: v.BodyTemplate, Enabled: v.Enabled,
})
}
return out, nil
}
func (s *notice) RuleSave(ctx context.Context, in dto.NoticeRuleInput) (uint64, error) {
data := map[string]interface{}{
"name": in.Name, "event_type": in.EventType,
"channel_codes": toJSON(in.ChannelCodes), "user_ids": toJSON(in.UserIds),
"title_template": in.TitleTemplate, "body_template": in.BodyTemplate, "enabled": in.Enabled,
}
if in.Id > 0 {
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
return 0, gerror.Wrap(err, "update notice rule")
}
return in.Id, nil
}
id, err := dao.NoticeRule.Ctx(ctx).Data(data).InsertAndGetId()
if err != nil {
return 0, gerror.Wrap(err, "insert notice rule")
}
return uint64(id), nil
}
func (s *notice) RuleDelete(ctx context.Context, id uint64) error {
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", id).Delete(); err != nil {
return gerror.Wrap(err, "delete notice rule")
}
return nil
}
// ---------------- 日志 ----------------
func (s *notice) LogList(ctx context.Context, f dto.NoticeLogFilter) ([]dto.NoticeLogVO, int, error) {
m := dao.NoticeLog.Ctx(ctx)
if f.EventType != "" {
m = m.Where("event_type", f.EventType)
}
if f.ChannelCode != "" {
m = m.Where("channel_code", f.ChannelCode)
}
if f.Result > 0 {
m = m.Where("result", f.Result)
}
if f.DateFrom != "" {
m = m.WhereGTE("created_at", f.DateFrom)
}
if f.DateTo != "" {
m = m.WhereLTE("created_at", f.DateTo)
}
total, err := m.Clone().Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "count notice log")
}
var list []entity.NoticeLog
if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil {
return nil, 0, gerror.Wrap(err, "query notice log")
}
out := make([]dto.NoticeLogVO, 0, len(list))
for _, v := range list {
out = append(out, dto.NoticeLogVO{
Id: v.Id, RuleId: v.RuleId, EventType: v.EventType, ChannelCode: v.ChannelCode,
UserId: v.UserId, Target: v.Target, Title: v.Title, Body: v.Body,
Result: v.Result, Error: v.Error, CreatedAt: v.CreatedAt,
})
}
return out, total, nil
}
// ---------------- 发送 ----------------
// Send 按事件类型查启用规则,向每个渠道的每个接收用户发送,并记录日志。
func (s *notice) Send(ctx context.Context, eventType string, vars map[string]string) error {
var rules []entity.NoticeRule
if err := dao.NoticeRule.Ctx(ctx).Where("event_type", eventType).Where("enabled", 1).Scan(&rules); err != nil {
return gerror.Wrap(err, "query notice rules for send")
}
if len(rules) == 0 {
return nil // 未配置规则 = 静默
}
for _, r := range rules {
channels := parseStrArray(r.ChannelCodes)
userIds := parseUintArray(r.UserIds)
title := dto.RenderTemplate(r.TitleTemplate, vars)
body := dto.RenderTemplate(r.BodyTemplate, vars)
for _, channel := range channels {
for _, uid := range userIds {
s.deliver(ctx, r.Id, eventType, channel, uid, title, body)
}
}
}
return nil
}
// deliver 单条投递:取用户渠道凭据 → 发送 → 记日志。
func (s *notice) deliver(ctx context.Context, ruleId uint64, eventType, channel string, userId uint64, title, body string) {
var user entity.AdminUser
_ = dao.AdminUser.Ctx(ctx).Where("id", userId).Scan(&user)
var target string
switch channel {
case "bark":
target = user.BarkDeviceId
case "pushplus":
target = user.PushplusToken
default:
target = ""
}
ok, errMsg := s.sendByChannel(ctx, channel, target, title, body)
result := 0
if ok {
result = 1
}
_, _ = dao.NoticeLog.Ctx(ctx).Data(map[string]interface{}{
"rule_id": ruleId, "event_type": eventType, "channel_code": channel,
"user_id": userId, "target": target, "title": title, "body": body,
"result": result, "error": errMsg,
}).Insert()
}
// sendByChannel 按渠道发送单条;返回 (是否成功, 错误信息)。
func (s *notice) sendByChannel(ctx context.Context, channel, target, title, body string) (bool, string) {
if target == "" {
return false, "目标凭据为空(用户未配置)"
}
client := g.Client().SetTimeout(10 * time.Second)
switch channel {
case "bark":
// 渠道配置取 baseUrlJSON config.baseUrl默认容器网络内 bark:8080
baseURL := "http://bark:8080"
var ch entity.NoticeChannel
if err := dao.NoticeChannel.Ctx(ctx).Where("code", "bark").Scan(&ch); err == nil && ch.Config != "" {
var cfg struct {
BaseURL string `json:"baseUrl"`
}
_ = json.Unmarshal([]byte(ch.Config), &cfg)
if cfg.BaseURL != "" {
baseURL = cfg.BaseURL
}
}
// 路径式 keyPOST {baseUrl}/{deviceKey}body JSON {title, body}
url := strings.TrimRight(baseURL, "/") + "/" + target
resp, err := client.Post(ctx, url, map[string]string{"title": title, "body": body})
if err != nil {
return false, err.Error()
}
if resp.StatusCode != 200 {
return false, "bark http " + resp.Status
}
var out struct {
Code int `json:"code"`
}
_ = json.Unmarshal(resp.ReadAll(), &out)
if out.Code != 200 {
return false, "bark code " + resp.ReadAllString()
}
return true, ""
case "pushplus":
// 官方接口POST https://www.pushplus.plus/send {token,title,content}
resp, err := client.Post(ctx, "https://www.pushplus.plus/send", map[string]string{
"token": target, "title": title, "content": body,
})
if err != nil {
return false, err.Error()
}
if resp.StatusCode != 200 {
return false, "pushplus http " + resp.Status
}
var out struct {
Code int `json:"code"`
}
_ = json.Unmarshal(resp.ReadAll(), &out)
if out.Code != 200 {
return false, "pushplus code " + resp.ReadAllString()
}
return true, ""
}
return false, "未知渠道: " + channel
}
// Test 测试发送:优先按规则;否则手动指定渠道+目标。
func (s *notice) Test(ctx context.Context, ruleId uint64, channelCode, target string, userIds []uint64, title, body string) (bool, string, error) {
if ruleId > 0 {
var r entity.NoticeRule
if err := dao.NoticeRule.Ctx(ctx).Where("id", ruleId).Scan(&r); err != nil || r.Id == 0 {
return false, "", response.Error(consts.CodeInvalidParam, "规则不存在")
}
for _, ch := range parseStrArray(r.ChannelCodes) {
for _, uid := range parseUintArray(r.UserIds) {
s.deliver(ctx, r.Id, r.EventType, ch, uid, title, body)
}
}
return true, "已按规则投递", nil
}
// 手动:渠道+目标target 为空则按用户)
if channelCode == "" {
return false, "", response.Error(consts.CodeInvalidParam, "请指定渠道")
}
if target == "" && len(userIds) > 0 {
var user entity.AdminUser
_ = dao.AdminUser.Ctx(ctx).Where("id", userIds[0]).Scan(&user)
if channelCode == "bark" {
target = user.BarkDeviceId
} else if channelCode == "pushplus" {
target = user.PushplusToken
}
}
if target == "" {
return false, "", response.Error(consts.CodeInvalidParam, "目标凭据为空")
}
ok, msg := s.sendByChannel(ctx, channelCode, target, title, body)
_, _ = dao.NoticeLog.Ctx(ctx).Data(map[string]interface{}{
"rule_id": 0, "event_type": "test", "channel_code": channelCode,
"user_id": 0, "target": target, "title": title, "body": body,
"result": boolToInt(ok), "error": msg,
}).Insert()
return ok, msg, nil
}
// ---------------- 工具 ----------------
func parseStrArray(s string) []string {
var out []string
_ = json.Unmarshal([]byte(s), &out)
if out == nil {
out = []string{}
}
return out
}
func parseUintArray(s string) []uint64 {
var out []uint64
_ = json.Unmarshal([]byte(s), &out)
if out == nil {
out = []uint64{}
}
return out
}
func toJSON(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// NowStr 当前时间字符串(供日志)。
func NowStr() string { return gtime.Now().Format("Y-m-d H:i:s") }