chore: 初始开源发布
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 已说明。
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
package warningnotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const warningEmailHTMLSignature = `data-warning-email="iotlcm"`
|
||||
|
||||
func applyContent(batch *Batch) {
|
||||
if batch == nil {
|
||||
return
|
||||
}
|
||||
batch.SMSContent = buildSMSContent(*batch)
|
||||
batch.EmailSubject = buildEmailSubject(*batch)
|
||||
batch.EmailBody = buildEmailBody(*batch)
|
||||
batch.PlatformTitle = buildPlatformTitle(*batch)
|
||||
batch.PlatformContent = buildPlatformContent(*batch)
|
||||
}
|
||||
|
||||
func buildSMSContent(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
summary := batchObjectSummary(batch)
|
||||
title := warningTitle(batch)
|
||||
action := ""
|
||||
if hasAffectOperation(batch.Items) {
|
||||
action = ",涉及停机或断网操作"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"【物联网】%s触发%s%s,请登录控制台查看。",
|
||||
summary,
|
||||
title,
|
||||
action,
|
||||
)
|
||||
}
|
||||
|
||||
func buildEmailSubject(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
if count == 0 {
|
||||
return "物联网预警提醒"
|
||||
}
|
||||
return fmt.Sprintf("%s(%d条)", warningTitle(batch), count)
|
||||
}
|
||||
|
||||
func buildEmailBody(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(`<div ` + warningEmailHTMLSignature + ` style="font-family:Arial,'Microsoft YaHei',sans-serif;font-size:14px;line-height:1.6;color:#1f2937;">`)
|
||||
fmt.Fprintf(&builder, `<p style="margin:0 0 12px;">%s,共 %d 条,最高级别:%s。</p>`, htmlEscape(warningTitle(batch)), count, htmlEscape(levelText(highestLevel(batch.Items))))
|
||||
if hasAffectOperation(batch.Items) {
|
||||
builder.WriteString(`<p style="margin:0 0 12px;color:#b42318;">本次预警包含停机或断网类操作,请重点确认。</p>`)
|
||||
}
|
||||
builder.WriteString(`<table style="width:100%;border-collapse:collapse;border:1px solid #d0d5dd;">`)
|
||||
builder.WriteString(`<thead><tr style="background:#f2f4f7;">`)
|
||||
builder.WriteString(emailHeaderCell(emailObjectHeader(batch)))
|
||||
builder.WriteString(emailHeaderCell("预警类型"))
|
||||
builder.WriteString(emailHeaderCell("预警原因"))
|
||||
builder.WriteString(emailHeaderCell("预警时间"))
|
||||
builder.WriteString(`</tr></thead><tbody>`)
|
||||
for _, item := range batch.Items {
|
||||
builder.WriteString(`<tr>`)
|
||||
builder.WriteString(emailBodyCell(displayObjectCode(item)))
|
||||
builder.WriteString(emailBodyCell(emailWarningType(item, batch)))
|
||||
builder.WriteString(emailBodyCell(emailWarningReason(item)))
|
||||
builder.WriteString(emailBodyCell(emailWarnTime(item)))
|
||||
builder.WriteString(`</tr>`)
|
||||
}
|
||||
builder.WriteString(`</tbody></table></div>`)
|
||||
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
func buildPlatformTitle(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
return "预警提醒"
|
||||
}
|
||||
if title := expireDayPlatformTitle(batch); title != "" {
|
||||
return title
|
||||
}
|
||||
return warningTitle(batch)
|
||||
}
|
||||
|
||||
func buildPlatformContent(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := buildPlatformSummaryContent(batch)
|
||||
if hasAffectOperation(batch.Items) {
|
||||
content += "本次提醒包含停机或断网类操作,请及时确认。"
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func buildPlatformSummaryContent(batch Batch) string {
|
||||
if isExpireDayBatch(batch) {
|
||||
return fmt.Sprintf(
|
||||
"%s有%d张卡临近到期,最高档位:%s,请登录控制台查看。",
|
||||
firstObjectCode(batch),
|
||||
len(batch.Items),
|
||||
buildPlatformTitle(batch),
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s触发%s,最高级别:%s,请登录控制台查看。",
|
||||
batchObjectSummary(batch),
|
||||
buildPlatformTitle(batch),
|
||||
levelText(highestLevel(batch.Items)),
|
||||
)
|
||||
}
|
||||
|
||||
func warningTitle(batch Batch) string {
|
||||
category := warningCategory(batch)
|
||||
return categoryStageTitle(category, batch.WarnStage)
|
||||
}
|
||||
|
||||
func warningCategory(batch Batch) string {
|
||||
category := strings.TrimSpace(batch.Category)
|
||||
if category == "" {
|
||||
category = "预警"
|
||||
}
|
||||
return category
|
||||
}
|
||||
|
||||
func categoryStageTitle(category string, stage int) string {
|
||||
category = strings.TrimSpace(category)
|
||||
if category == "" {
|
||||
category = "预警"
|
||||
}
|
||||
if stage > 0 {
|
||||
return category + stageText(stage)
|
||||
}
|
||||
if strings.HasSuffix(category, "提醒") {
|
||||
return category
|
||||
}
|
||||
return category + "提醒"
|
||||
}
|
||||
|
||||
func expireDayPlatformTitle(batch Batch) string {
|
||||
if !isExpireDayBatch(batch) {
|
||||
return ""
|
||||
}
|
||||
threshold := expireDayPlatformThreshold(batch)
|
||||
if threshold <= 0 {
|
||||
return "距离到期时间预警"
|
||||
}
|
||||
return fmt.Sprintf("距离到期时间小于%.0f天", threshold)
|
||||
}
|
||||
|
||||
func isExpireDayBatch(batch Batch) bool {
|
||||
category := strings.TrimSpace(batch.Category)
|
||||
for _, item := range batch.Items {
|
||||
if strings.TrimSpace(item.Kind) == "expire_day" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(category, "到期") && strings.TrimSpace(item.UnitText) == "天" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func expireDayPlatformThreshold(batch Batch) float64 {
|
||||
for _, item := range batch.Items {
|
||||
if item.WarnStage == batch.WarnStage && item.Threshold > 0 {
|
||||
return item.Threshold
|
||||
}
|
||||
}
|
||||
for _, item := range batch.Items {
|
||||
if item.Threshold > 0 {
|
||||
return item.Threshold
|
||||
}
|
||||
}
|
||||
switch batch.WarnStage {
|
||||
case 1:
|
||||
return 30
|
||||
case 2:
|
||||
return 15
|
||||
case 3:
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func itemDetailSummary(item Event) string {
|
||||
return itemSummary(item)
|
||||
}
|
||||
|
||||
func batchObjectSummary(batch Batch) string {
|
||||
count := len(batch.Items)
|
||||
if count == 0 {
|
||||
return ""
|
||||
}
|
||||
code := firstObjectCode(batch)
|
||||
if count == 1 {
|
||||
return code
|
||||
}
|
||||
return fmt.Sprintf("%s等%d%s", code, count, objectCountUnit(batch.Items[0]))
|
||||
}
|
||||
|
||||
func firstObjectCode(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
return "--"
|
||||
}
|
||||
return displayObjectCode(batch.Items[0])
|
||||
}
|
||||
|
||||
func objectCountUnit(item Event) string {
|
||||
switch item.ItemType {
|
||||
case 2:
|
||||
return "个流量池"
|
||||
default:
|
||||
return "张卡"
|
||||
}
|
||||
}
|
||||
|
||||
func metricSummary(item Event) string {
|
||||
unit := strings.TrimSpace(item.UnitText)
|
||||
switch unit {
|
||||
case "天", "月":
|
||||
parts := []string{
|
||||
"剩余" + formatMetric(item.Actual, unit),
|
||||
}
|
||||
if item.Threshold > 0 {
|
||||
parts = append(parts, "阈值"+formatMetric(item.Threshold, unit))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
case "%":
|
||||
parts := []string{
|
||||
"阈值" + formatMetric(item.Threshold, unit),
|
||||
"实际" + formatMetric(item.Actual, unit),
|
||||
}
|
||||
if shouldShowRemain(item) {
|
||||
parts = append(parts, "剩余"+remainMetric(item))
|
||||
}
|
||||
if item.TotalFlowMB > 0 {
|
||||
parts = append(parts, "总量"+formatMetric(item.TotalFlowMB, "MB"))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
default:
|
||||
parts := []string{
|
||||
"阈值" + formatMetric(item.Threshold, item.UnitText),
|
||||
"实际" + formatMetric(item.Actual, item.UnitText),
|
||||
}
|
||||
if shouldShowRemain(item) {
|
||||
parts = append(parts, "剩余"+remainMetric(item))
|
||||
}
|
||||
if item.TotalFlowMB > 0 {
|
||||
parts = append(parts, "总量"+formatMetric(item.TotalFlowMB, "MB"))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
}
|
||||
|
||||
func itemSummary(item Event) string {
|
||||
parts := make([]string, 0, 1)
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", itemLabel(item), displayObjectCode(item)))
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func itemLabel(item Event) string {
|
||||
switch item.ItemType {
|
||||
case 2:
|
||||
return "流量池编号"
|
||||
default:
|
||||
return "ICCID"
|
||||
}
|
||||
}
|
||||
|
||||
func displayObjectCode(item Event) string {
|
||||
itemCode := strings.TrimSpace(item.ItemCode)
|
||||
if itemCode == "" {
|
||||
return "--"
|
||||
}
|
||||
return itemCode
|
||||
}
|
||||
|
||||
func stageText(stage int) string {
|
||||
if stage <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("第%d次提醒", stage)
|
||||
}
|
||||
|
||||
func levelText(level int) string {
|
||||
switch level {
|
||||
case 1:
|
||||
return "提示"
|
||||
case 2:
|
||||
return "注意"
|
||||
case 3:
|
||||
return "警告"
|
||||
case 4:
|
||||
return "危险"
|
||||
case 5:
|
||||
return "紧急"
|
||||
default:
|
||||
return "提醒"
|
||||
}
|
||||
}
|
||||
|
||||
func highestLevel(items []Event) int {
|
||||
level := 0
|
||||
for _, item := range items {
|
||||
if item.WarnLevel > level {
|
||||
level = item.WarnLevel
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func hasAffectOperation(items []Event) bool {
|
||||
for _, item := range items {
|
||||
if item.AffectOperation {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldShowRemain(item Event) bool {
|
||||
if item.TotalFlowMB > 0 {
|
||||
return true
|
||||
}
|
||||
unit := strings.TrimSpace(item.UnitText)
|
||||
return unit == "MB" || unit == "%"
|
||||
}
|
||||
|
||||
func remainMetric(item Event) string {
|
||||
if !shouldShowRemain(item) {
|
||||
return "--"
|
||||
}
|
||||
return formatMetric(item.RemainFlowMB, "MB")
|
||||
}
|
||||
|
||||
func formatMetric(value float64, unit string) string {
|
||||
unit = strings.TrimSpace(unit)
|
||||
switch unit {
|
||||
case "%":
|
||||
return fmt.Sprintf("%.2f%%", value)
|
||||
case "天", "月":
|
||||
return fmt.Sprintf("%.0f%s", value, unit)
|
||||
case "MB":
|
||||
return fmt.Sprintf("%.3f MB", value)
|
||||
default:
|
||||
if unit == "" {
|
||||
return fmt.Sprintf("%.3f", value)
|
||||
}
|
||||
return fmt.Sprintf("%.3f %s", value, unit)
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(value time.Time) string {
|
||||
return value.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func emailObjectHeader(batch Batch) string {
|
||||
if len(batch.Items) == 0 {
|
||||
return "ICCID"
|
||||
}
|
||||
for _, item := range batch.Items {
|
||||
if item.ItemType != 2 {
|
||||
return "ICCID"
|
||||
}
|
||||
}
|
||||
return "流量池编号"
|
||||
}
|
||||
|
||||
func emailWarningType(item Event, batch Batch) string {
|
||||
category := strings.TrimSpace(item.Category)
|
||||
if category != "" {
|
||||
return category
|
||||
}
|
||||
return warningCategory(batch)
|
||||
}
|
||||
|
||||
func emailWarningReason(item Event) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if item.WarnStage > 0 {
|
||||
parts = append(parts, stageText(item.WarnStage))
|
||||
}
|
||||
if summary := metricSummary(item); summary != "" {
|
||||
parts = append(parts, summary)
|
||||
}
|
||||
if item.OperationText != "" {
|
||||
parts = append(parts, item.OperationText)
|
||||
}
|
||||
if len(parts) == 0 && item.Remark != "" {
|
||||
parts = append(parts, item.Remark)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func emailWarnTime(item Event) string {
|
||||
if item.WarnTime.IsZero() {
|
||||
return "--"
|
||||
}
|
||||
return formatTime(item.WarnTime)
|
||||
}
|
||||
|
||||
func emailHeaderCell(value string) string {
|
||||
return `<th style="padding:8px 10px;border:1px solid #d0d5dd;text-align:left;font-weight:600;color:#344054;">` + htmlEscape(value) + `</th>`
|
||||
}
|
||||
|
||||
func emailBodyCell(value string) string {
|
||||
return `<td style="padding:8px 10px;border:1px solid #d0d5dd;vertical-align:top;color:#1f2937;">` + htmlEscape(value) + `</td>`
|
||||
}
|
||||
|
||||
func htmlEscape(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "--"
|
||||
}
|
||||
return html.EscapeString(value)
|
||||
}
|
||||
Reference in New Issue
Block a user