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) }