59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
559 lines
15 KiB
Go
559 lines
15 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 nil
|
|
}
|
|
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) {
|
|
deliveries := make([]platformDelivery, 0, 2)
|
|
|
|
agentUserIDs, err := queryAgentUserIDs(ctx, batch.TenantID, batch.DeptID, batch.CreatedBy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(agentUserIDs) > 0 {
|
|
deliveries = append(deliveries, platformDelivery{
|
|
Group: platformRecipientGroup{
|
|
SendUserType: sendUserTypeAgent,
|
|
UserIDs: agentUserIDs,
|
|
},
|
|
Batch: batch,
|
|
})
|
|
}
|
|
|
|
if hasCardWarningItems(batch.Items) {
|
|
deliveries = append(deliveries, resolveTerminalPlatformDeliveries(ctx, batch)...)
|
|
}
|
|
|
|
return deliveries, nil
|
|
}
|
|
|
|
func queryAgentUserIDs(ctx context.Context, tenantID string, deptID int64, createdBy int64) ([]int64, error) {
|
|
userIDs := make([]int64, 0)
|
|
seen := make(map[int64]struct{})
|
|
if strings.TrimSpace(tenantID) == "" {
|
|
return userIDs, 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 {
|
|
userIDs = appendUniqueInt64(userIDs, seen, user.UserId)
|
|
}
|
|
}
|
|
|
|
if 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 {
|
|
userIDs = appendUniqueInt64(userIDs, seen, creator.UserId)
|
|
}
|
|
}
|
|
|
|
return userIDs, nil
|
|
}
|
|
|
|
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 {
|
|
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
|
|
}
|
|
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).
|
|
Scan(&rows)
|
|
return rows, err
|
|
}
|
|
|
|
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)
|
|
}
|