Files
iotlcm/admin-svr/server/internal/logic/warningnotify/message_center_sender.go
T

589 lines
16 KiB
Go

package warningnotify
import (
"context"
"sort"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
const (
messageTypeSystem = 1
sendUserTypeAgent = 1
sendUserTypeTerminal = 2
simTerminalTable = "t_sim_terminal"
terminalTable = "t_terminal"
defaultPlatformTitle = "预警提醒"
defaultPlatformContent = "您有新的预警消息,请登录平台查看。"
)
type platformRecipientGroup struct {
SendUserType int
UserIDs []int64
}
type platformDelivery struct {
Group platformRecipientGroup
Batch Batch
}
type terminalUserCardRow struct {
UserID int64 `orm:"user_id"`
ICCID string `orm:"iccid"`
MSISDN string `orm:"msisdn"`
}
// MessageCenterSender writes platform warnings through the message center service.
// Email and SMS warnings are sent through their configured external services.
type MessageCenterSender struct {
Fallback Sender
}
func init() {
UseMessageCenterSender()
}
func UseMessageCenterSender() {
SetSender(MessageCenterSender{Fallback: LogSender{}})
}
func (s MessageCenterSender) Send(ctx context.Context, batch Batch) error {
if ctx == nil {
ctx = context.Background()
}
if len(batch.Items) == 0 {
return nil
}
switch batch.Method {
case MethodPlatform:
return s.sendPlatform(ctx, batch)
case MethodEmail:
return s.sendEmail(ctx, batch)
case MethodSMS:
return s.sendSMS(ctx, batch)
case MethodWechat:
return s.sendWechat(ctx, batch)
default:
return s.fallback().Send(ctx, batch)
}
}
func (s MessageCenterSender) sendPlatform(ctx context.Context, batch Batch) error {
if err := s.writePlatform(ctx, batch); err != nil {
g.Log().Errorf(
ctx,
"WarningNotify platform message write failed, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d, err:%+v",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
len(batch.Items),
err,
)
return err
}
return nil
}
func (s MessageCenterSender) sendEmail(ctx context.Context, batch Batch) error {
if err := sendWarningEmail(ctx, batch); err != nil {
g.Log().Errorf(
ctx,
"WarningNotify email send failed, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, recipients:%s, count:%d, err:%+v",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
strings.Join(batch.Recipients, ","),
len(batch.Items),
err,
)
return nil
}
return nil
}
func (s MessageCenterSender) sendSMS(ctx context.Context, batch Batch) error {
if err := sendWarningSMS(ctx, batch); err != nil {
g.Log().Errorf(
ctx,
"WarningNotify SMS send failed, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, recipients:%s, count:%d, err:%+v",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
strings.Join(batch.Recipients, ","),
len(batch.Items),
err,
)
return nil
}
return nil
}
func (s MessageCenterSender) sendWechat(ctx context.Context, batch Batch) error {
if err := sendWarningWechat(ctx, batch); err != nil {
g.Log().Errorf(
ctx,
"WarningNotify wechat subscribe send failed, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d, err:%+v",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
len(batch.Items),
err,
)
return err
}
return nil
}
func (s MessageCenterSender) fallback() Sender {
if s.Fallback != nil {
return s.Fallback
}
return LogSender{}
}
func (s MessageCenterSender) writePlatform(ctx context.Context, batch Batch) error {
deliveries, err := resolvePlatformDeliveries(ctx, batch)
if err != nil {
return err
}
if len(deliveries) == 0 {
g.Log().Warningf(
ctx,
"WarningNotify platform message skipped, no recipients, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
len(batch.Items),
)
return nil
}
messageCtx := platformMessageContext(ctx, batch)
for _, delivery := range deliveries {
if len(delivery.Group.UserIDs) == 0 || len(delivery.Batch.Items) == 0 {
continue
}
messageBatch := delivery.Batch
applyContent(&messageBatch)
if err := service.CommonTMessageLog().Edit(messageCtx, platformMessageParam(messageBatch, delivery.Group)); err != nil {
return err
}
}
g.Log().Infof(
ctx,
"WarningNotify platform message submitted, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, groups:%d, count:%d",
batch.Business,
batch.TenantID,
batch.DeptID,
batch.Category,
batch.WarnStage,
len(deliveries),
len(batch.Items),
)
return nil
}
func resolvePlatformDeliveries(ctx context.Context, batch Batch) ([]platformDelivery, error) {
if batch.prepared != nil {
return []platformDelivery{{Group: *batch.prepared, Batch: batch}}, nil
}
deliveries := make([]platformDelivery, 0, 2)
agentUserIDs, err := queryAgentUserIDs(ctx, batch.TenantID, batch.DeptID, batch.CreatedBy, batch.Business)
if err != nil {
return nil, err
}
if len(agentUserIDs) > 0 {
deliveries = append(deliveries, platformDelivery{
Group: platformRecipientGroup{
SendUserType: sendUserTypeAgent,
UserIDs: agentUserIDs,
},
Batch: batch,
})
} else if batch.Business == BusinessTrafficCutoff {
g.Log().Warningf(ctx, "WarningNotify traffic cutoff skipped department recipients, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d", batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, len(batch.Items))
}
if hasCardWarningItems(batch.Items) {
deliveries = append(deliveries, resolveTerminalPlatformDeliveries(ctx, batch)...)
}
return deliveries, nil
}
func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, createdBy int64, business string) ([]int64, error) {
deptUserIDs := make([]int64, 0)
creatorUserID := int64(0)
if strings.TrimSpace(tenantID) == "" {
return deptUserIDs, nil
}
columns := dao.SysUser.Columns()
if deptID > 0 {
var users []entity.SysUser
if err := dao.SysUser.Ctx(ctx).
Fields(columns.UserId).
Where(columns.TenantId, tenantID).
Where(columns.DeptId, deptID).
Where(columns.UserType, consts.SysUserTypeSys).
Where(columns.Status, consts.SysDeptStatusNormal).
Scan(&users); err != nil {
return nil, err
}
for _, user := range users {
deptUserIDs = append(deptUserIDs, user.UserId)
}
}
if business != BusinessTrafficCutoff && createdBy > 0 {
var creator *entity.SysUser
if err := dao.SysUser.Ctx(ctx).
Fields(columns.UserId).
Where(columns.TenantId, tenantID).
Where(columns.UserId, createdBy).
Where(columns.UserType, consts.SysUserTypeSys).
Where(columns.Status, consts.SysDeptStatusNormal).
Scan(&creator); err != nil {
return nil, err
}
if creator != nil {
creatorUserID = creator.UserId
}
}
return platformAgentRecipientIDs(business, deptUserIDs, creatorUserID), nil
}
func platformAgentRecipientIDs(business string, deptUserIDs []int64, createdBy int64) []int64 {
userIDs := make([]int64, 0, len(deptUserIDs)+1)
seen := make(map[int64]struct{})
for _, userID := range deptUserIDs {
userIDs = appendUniqueInt64(userIDs, seen, userID)
}
if business != BusinessTrafficCutoff {
userIDs = appendUniqueInt64(userIDs, seen, createdBy)
}
return userIDs
}
func queryCreatedTerminalUserIDs(ctx context.Context, tenantID string, createdBy int64) ([]int64, error) {
tenantID = strings.TrimSpace(tenantID)
if tenantID == "" || createdBy <= 0 {
return nil, nil
}
columns := dao.SysUser.Columns()
var creator *entity.SysUser
if err := dao.SysUser.Ctx(ctx).
Fields(columns.UserId).
Where(columns.TenantId, tenantID).
Where(columns.UserId, createdBy).
Where(columns.UserType, consts.SysUserTypeAPP).
Where(columns.Status, consts.SysDeptStatusNormal).
Scan(&creator); err != nil {
return nil, err
}
if creator == nil || creator.UserId <= 0 {
return nil, nil
}
return []int64{creator.UserId}, nil
}
func resolveTerminalPlatformDeliveries(ctx context.Context, batch Batch) []platformDelivery {
if batch.prepared != nil {
return []platformDelivery{{Group: *batch.prepared, Batch: batch}}
}
itemsByUser := make(map[int64][]Event)
seenByUser := make(map[int64]map[int]struct{})
appendCreatedTerminalEvents(ctx, batch, itemsByUser, seenByUser)
if err := appendRelatedTerminalEvents(ctx, batch, itemsByUser, seenByUser); err != nil {
g.Log().Warningf(ctx, "WarningNotify query terminal users failed, tenantId:%s, deptId:%d, err:%+v", batch.TenantID, batch.DeptID, err)
}
userIDs := make([]int64, 0, len(itemsByUser))
for userID := range itemsByUser {
userIDs = append(userIDs, userID)
}
sort.Slice(userIDs, func(i, j int) bool {
return userIDs[i] < userIDs[j]
})
deliveries := make([]platformDelivery, 0, len(userIDs))
for _, userID := range userIDs {
items := itemsByUser[userID]
if userID <= 0 || len(items) == 0 {
continue
}
terminalBatch := batch
terminalBatch.Items = append([]Event(nil), items...)
terminalBatch.WarnStage = highestStage(terminalBatch.Items)
deliveries = append(deliveries, platformDelivery{
Group: platformRecipientGroup{
SendUserType: sendUserTypeTerminal,
UserIDs: []int64{userID},
},
Batch: terminalBatch,
})
}
return deliveries
}
type indexedWarningEvent struct {
Index int
Event Event
}
func appendCreatedTerminalEvents(ctx context.Context, batch Batch, itemsByUser map[int64][]Event, seenByUser map[int64]map[int]struct{}) {
itemsByCreator := make(map[int64][]indexedWarningEvent)
for index, item := range batch.Items {
if item.ItemType != 1 {
continue
}
createdBy := item.CreatedBy
if createdBy <= 0 {
createdBy = batch.CreatedBy
}
if createdBy <= 0 {
continue
}
itemsByCreator[createdBy] = append(itemsByCreator[createdBy], indexedWarningEvent{Index: index, Event: item})
}
creatorIDs := make([]int64, 0, len(itemsByCreator))
for createdBy := range itemsByCreator {
creatorIDs = append(creatorIDs, createdBy)
}
sort.Slice(creatorIDs, func(i, j int) bool {
return creatorIDs[i] < creatorIDs[j]
})
for _, createdBy := range creatorIDs {
userIDs, err := queryCreatedTerminalUserIDs(ctx, batch.TenantID, createdBy)
if err != nil {
g.Log().Warningf(ctx, "WarningNotify query created terminal user failed, tenantId:%s, userId:%d, err:%+v", batch.TenantID, createdBy, err)
continue
}
for _, userID := range userIDs {
for _, item := range itemsByCreator[createdBy] {
appendTerminalEvent(itemsByUser, seenByUser, userID, item.Index, item.Event)
}
}
}
}
func appendRelatedTerminalEvents(ctx context.Context, batch Batch, itemsByUser map[int64][]Event, seenByUser map[int64]map[int]struct{}) error {
tenantID := strings.TrimSpace(batch.TenantID)
if tenantID == "" {
return nil
}
iccidIndexes, msisdnIndexes := terminalCardEventIndexes(batch.Items)
if len(iccidIndexes) == 0 && len(msisdnIndexes) == 0 {
return nil
}
if len(iccidIndexes) > 0 {
rows, err := queryTerminalUserCardRowsByColumn(ctx, tenantID, "st.iccid", mapKeys(iccidIndexes))
if err != nil {
return err
}
appendTerminalRows(itemsByUser, seenByUser, batch.Items, rows, iccidIndexes, msisdnIndexes)
}
if len(msisdnIndexes) > 0 {
rows, err := queryTerminalUserCardRowsByColumn(ctx, tenantID, "st.msisdn", mapKeys(msisdnIndexes))
if err != nil {
return err
}
appendTerminalRows(itemsByUser, seenByUser, batch.Items, rows, iccidIndexes, msisdnIndexes)
}
return nil
}
func queryTerminalUserCardRowsByColumn(ctx context.Context, tenantID string, column string, values []string) ([]terminalUserCardRow, error) {
rows := make([]terminalUserCardRow, 0)
if strings.TrimSpace(tenantID) == "" || len(values) == 0 {
return rows, nil
}
for offset := 0; offset < len(values); offset += 500 {
if err := ctx.Err(); err != nil {
return nil, err
}
end := min(offset+500, len(values))
var page []terminalUserCardRow
if err := g.DB().Model(simTerminalTable+" st").Ctx(ctx).Safe().
LeftJoin(terminalTable+" t", "t.terminal_id = st.terminal_id AND t.tenant_id = st.tenant_id").
LeftJoin(dao.SysUser.Table()+" u", "(u.user_id = t.leader OR u.user_id = st.terminal_id) AND u.tenant_id = st.tenant_id").
Fields("DISTINCT u.user_id, st.iccid, st.msisdn").
Where("st.tenant_id", tenantID).
Where("u.user_type", consts.SysUserTypeAPP).
Where("u.status", consts.SysDeptStatusNormal).
WhereIn(column, values[offset:end]).
Scan(&page); err != nil {
return nil, err
}
rows = append(rows, page...)
}
return rows, nil
}
func hasCardWarningItems(items []Event) bool {
for _, item := range items {
if item.ItemType == 1 {
return true
}
}
return false
}
func terminalCardEventIndexes(items []Event) (map[string][]int, map[string][]int) {
iccidIndexes := make(map[string][]int)
msisdnIndexes := make(map[string][]int)
for index, item := range items {
if item.ItemType != 1 {
continue
}
itemCode := strings.TrimSpace(item.ItemCode)
msisdn := strings.TrimSpace(item.MSISDN)
if itemCode != "" {
iccidIndexes[itemCode] = append(iccidIndexes[itemCode], index)
}
if msisdn != "" {
msisdnIndexes[msisdn] = append(msisdnIndexes[msisdn], index)
}
}
return iccidIndexes, msisdnIndexes
}
func appendTerminalRows(itemsByUser map[int64][]Event, seenByUser map[int64]map[int]struct{}, items []Event, rows []terminalUserCardRow, iccidIndexes map[string][]int, msisdnIndexes map[string][]int) {
for _, row := range rows {
if row.UserID <= 0 {
continue
}
for _, index := range iccidIndexes[strings.TrimSpace(row.ICCID)] {
appendTerminalEvent(itemsByUser, seenByUser, row.UserID, index, items[index])
}
for _, index := range msisdnIndexes[strings.TrimSpace(row.MSISDN)] {
appendTerminalEvent(itemsByUser, seenByUser, row.UserID, index, items[index])
}
}
}
func appendAllCardEvents(itemsByUser map[int64][]Event, seenByUser map[int64]map[int]struct{}, userID int64, items []Event) {
for index, item := range items {
if item.ItemType != 1 {
continue
}
appendTerminalEvent(itemsByUser, seenByUser, userID, index, item)
}
}
func appendTerminalEvent(itemsByUser map[int64][]Event, seenByUser map[int64]map[int]struct{}, userID int64, index int, item Event) {
if userID <= 0 {
return
}
seen := seenByUser[userID]
if seen == nil {
seen = make(map[int]struct{})
seenByUser[userID] = seen
}
if _, exists := seen[index]; exists {
return
}
seen[index] = struct{}{}
itemsByUser[userID] = append(itemsByUser[userID], item)
}
func mapKeys(values map[string][]int) []string {
keys := make([]string, 0, len(values))
for key := range values {
key = strings.TrimSpace(key)
if key != "" {
keys = append(keys, key)
}
}
sort.Strings(keys)
return keys
}
func platformMessageContext(ctx context.Context, batch Batch) context.Context {
return contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: batch.CreatedBy,
DeptId: batch.DeptID,
TenantId: strings.TrimSpace(batch.TenantID),
}}})
}
func platformMessageParam(batch Batch, group platformRecipientGroup) *model.TMessageLogEditParam {
messageType := uint(messageTypeSystem)
sendUserType := uint(group.SendUserType)
title := platformTitle(batch)
content := platformContent(batch)
link := platformLink(batch)
isRecall := uint(0)
return &model.TMessageLogEditParam{
MessageType: &messageType,
SendUserType: &sendUserType,
UserIds: gjson.New(group.UserIDs),
MessageTitle: &title,
MessageContent: &content,
MessageLink: &link,
IsRecall: &isRecall,
}
}
func platformTitle(batch Batch) string {
title := strings.TrimSpace(batch.PlatformTitle)
if title == "" {
return defaultPlatformTitle
}
return title
}
func platformContent(batch Batch) string {
content := strings.TrimSpace(batch.PlatformContent)
if content == "" {
return defaultPlatformContent
}
return content
}
func platformLink(batch Batch) string {
return ""
}
func appendUniqueInt64(values []int64, seen map[int64]struct{}, value int64) []int64 {
if value <= 0 {
return values
}
if _, ok := seen[value]; ok {
return values
}
seen[value] = struct{}{}
return append(values, value)
}