diff --git a/agent/app/repo/alert.go b/agent/app/repo/alert.go
index e62d1fa02..015a9b0eb 100644
--- a/agent/app/repo/alert.go
+++ b/agent/app/repo/alert.go
@@ -1,6 +1,9 @@
package repo
import (
+ "encoding/json"
+ "strings"
+
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -353,56 +356,203 @@ var singletonTypes = map[string]bool{
func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
tx := global.AlertDB.Begin()
+ if tx.Error != nil {
+ return tx.Error
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ tx.Rollback()
+ panic(r)
+ }
+ }()
+
var oldConfigs []model.AlertConfig
- _ = tx.Find(&oldConfigs).Error
+ if err := tx.Find(&oldConfigs).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ usedConfigIDs, err := loadUsedAlertConfigIDs(tx)
+ if err != nil {
+ tx.Rollback()
+ return err
+ }
+
oldConfigMap := make(map[string]uint)
- nonSingletonTypes := make(map[string]struct{})
+ oldConfigByType := make(map[string][]model.AlertConfig)
+ oldConfigByKey := make(map[string][]model.AlertConfig)
+ consumedConfigIDs := make(map[uint]struct{})
for _, item := range oldConfigs {
if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item.ID
continue
}
- nonSingletonTypes[item.Type] = struct{}{}
- }
- for _, item := range data {
- if !singletonTypes[item.Type] {
- nonSingletonTypes[item.Type] = struct{}{}
- }
- }
- for itemType := range nonSingletonTypes {
- if err := tx.Where("type = ?", itemType).Delete(&model.AlertConfig{}).Error; err != nil {
- tx.Rollback()
- return err
- }
+ oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
+ oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
}
for _, item := range data {
if singletonTypes[item.Type] {
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
+ consumedConfigIDs[item.ID] = struct{}{}
} else {
item.ID = 0
}
- if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil {
+ if item.ID == 0 {
+ if err := tx.Create(&item).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+ } else if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
continue
}
+
+ key := alertConfigSyncKey(item)
+ if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
+ item.ID = matched.ID
+ consumedConfigIDs[item.ID] = struct{}{}
+ if err := tx.Save(&item).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+ deleteAlertConfigByID(oldConfigByType, matched.ID)
+ continue
+ }
+
+ if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
+ item.ID = matched.ID
+ consumedConfigIDs[item.ID] = struct{}{}
+ if err := tx.Save(&item).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+ continue
+ }
+
item.ID = 0
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
}
}
- for _, id := range oldConfigMap {
- if err := tx.Where("id = ?", id).Delete(&model.AlertConfig{}).Error; err != nil {
+ for _, item := range oldConfigs {
+ if _, used := usedConfigIDs[item.ID]; used {
+ continue
+ }
+ if _, kept := consumedConfigIDs[item.ID]; kept {
+ continue
+ }
+ if err := tx.Where("id = ?", item.ID).Delete(&model.AlertConfig{}).Error; err != nil {
tx.Rollback()
return err
}
}
if err := tx.Commit().Error; err != nil {
+ tx.Rollback()
return err
}
return nil
}
+
+func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
+ var alerts []model.Alert
+ if err := tx.Select("method").Find(&alerts).Error; err != nil {
+ return nil, err
+ }
+
+ usedIDs := make(map[uint]struct{})
+ for _, alert := range alerts {
+ for _, item := range strings.Split(alert.Method, ",") {
+ item = strings.TrimSpace(item)
+ if item == "" {
+ continue
+ }
+ id, err := strconv.ParseUint(item, 10, 64)
+ if err != nil {
+ continue
+ }
+ usedIDs[uint(id)] = struct{}{}
+ }
+ }
+ return usedIDs, nil
+}
+
+func alertConfigSyncKey(item model.AlertConfig) string {
+ return item.Type + "::" + normalizeAlertConfigJSON(item.Config)
+}
+
+func normalizeAlertConfigJSON(config string) string {
+ trimmed := strings.TrimSpace(config)
+ if trimmed == "" {
+ return ""
+ }
+
+ var data any
+ if err := json.Unmarshal([]byte(trimmed), &data); err != nil {
+ return trimmed
+ }
+ buf, err := json.Marshal(data)
+ if err != nil {
+ return trimmed
+ }
+ return string(buf)
+}
+
+func popAlertConfigByKey(configMap map[string][]model.AlertConfig, key string) (model.AlertConfig, bool) {
+ items := configMap[key]
+ if len(items) == 0 {
+ return model.AlertConfig{}, false
+ }
+
+ item := items[0]
+ if len(items) == 1 {
+ delete(configMap, key)
+ } else {
+ configMap[key] = items[1:]
+ }
+ return item, true
+}
+
+func popUnusedAlertConfigByType(configMap map[string][]model.AlertConfig, usedConfigIDs map[uint]struct{}, configType string) (model.AlertConfig, bool) {
+ items := configMap[configType]
+ if len(items) == 0 {
+ return model.AlertConfig{}, false
+ }
+
+ for idx, item := range items {
+ if _, used := usedConfigIDs[item.ID]; used {
+ continue
+ }
+ if idx == 0 {
+ if len(items) == 1 {
+ delete(configMap, configType)
+ } else {
+ configMap[configType] = items[1:]
+ }
+ } else {
+ configMap[configType] = append(items[:idx], items[idx+1:]...)
+ }
+ return item, true
+ }
+ return model.AlertConfig{}, false
+}
+
+func deleteAlertConfigByID(configMap map[string][]model.AlertConfig, id uint) {
+ for key, items := range configMap {
+ for idx, item := range items {
+ if item.ID != id {
+ continue
+ }
+ if len(items) == 1 {
+ delete(configMap, key)
+ } else {
+ configMap[key] = append(items[:idx], items[idx+1:]...)
+ }
+ return
+ }
+ }
+}
diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go
index 3d847c78b..f8a8c6950 100644
--- a/agent/app/service/alert.go
+++ b/agent/app/service/alert.go
@@ -512,6 +512,9 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err
}
+ if err := a.checkAlertConfigSMSPhoneUnique(req); err != nil {
+ return err
+ }
if req.ID != 0 {
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -538,6 +541,29 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
return nil
}
+func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
+ if req.Type != constant.SMSConfig {
+ return nil
+ }
+
+ phone := alertConfigSMSPhone(req.Config)
+ configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type))
+ if err != nil {
+ return err
+ }
+
+ for _, config := range configs {
+ if req.ID != 0 && config.ID == req.ID {
+ continue
+ }
+ if alertConfigSMSPhone(config.Config) == phone {
+ return buserr.New("ErrAlertConfigPhoneExist")
+ }
+ }
+
+ return nil
+}
+
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" {
@@ -604,7 +630,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
func alertConfigDisplayName(configType, configData string) string {
switch configType {
- case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark:
+ case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
var cfg struct {
DisplayName string `json:"displayName"`
}
@@ -617,6 +643,16 @@ func alertConfigDisplayName(configType, configData string) string {
}
}
+func alertConfigSMSPhone(configData string) string {
+ var cfg struct {
+ Phone string `json:"phone"`
+ }
+ if err := json.Unmarshal([]byte(configData), &cfg); err != nil {
+ return ""
+ }
+ return strings.TrimSpace(cfg.Phone)
+}
+
func (a AlertService) DeleteAlertConfig(id uint) error {
_, err := alertRepo.GetConfigById(id)
if err != nil {
diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml
index 520e03568..f3b0dbd10 100644
--- a/agent/i18n/lang/en.yaml
+++ b/agent/i18n/lang/en.yaml
@@ -78,6 +78,7 @@ ErrBackupCheck: 'Backup account connectivity test failed: {{ .err }}'
ErrBackupLocalDelete: 'Local backup accounts cannot be deleted'
ErrBackupLocalCreate: 'Local backup accounts cannot be created'
ErrAlertConfigInUse: 'Alert configuration is in use and cannot be deleted'
+ErrAlertConfigPhoneExist: 'Phone number already exists'
#app
ErrPortInUsed: '{{ .detail }} port is already occupied!'
diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml
index baa70e4ed..8fa23c76f 100644
--- a/agent/i18n/lang/es-ES.yaml
+++ b/agent/i18n/lang/es-ES.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
ErrBackupLocalDelete: 'No se puede eliminar cuentas de respaldo locales'
ErrBackupLocalCreate: 'No se pueden crear cuentas de respaldo locales'
ErrAlertConfigInUse: 'La configuración de alertas está en uso y no se puede eliminar'
+ErrAlertConfigPhoneExist: 'El numero de telefono ya existe'
ErrPortInUsed: 'El puerto {{ .detail }} ya está ocupado'
ErrAppLimit: 'El número de aplicaciones instaladas ha superado el límite'
ErrNotInstall: 'Aplicación no instalada'
diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml
index f2eefde1c..1a3ac5a2c 100644
--- a/agent/i18n/lang/ja.yaml
+++ b/agent/i18n/lang/ja.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
ErrBackupLocalDelete: 'ローカルバックアップは削除できません'
ErrBackupLocalCreate: 'ローカルバックアップは作成できません'
ErrAlertConfigInUse: 'アラート設定は使用中のため削除できません'
+ErrAlertConfigPhoneExist: '電話番号はすでに存在します'
ErrPortInUsed: '{{ .detail }} ポートはすでに使用されています'
ErrAppLimit: 'インストールされているアプリケーションの数が制限を超えました'
ErrNotInstall: 'アプリケーションがインストールされていません'
diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml
index 853974c60..0c04a83b1 100644
--- a/agent/i18n/lang/ko.yaml
+++ b/agent/i18n/lang/ko.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
ErrBackupLocalDelete: '로컬 백업은 삭제할 수 없습니다'
ErrBackupLocalCreate: '로컬 백업은 만들 수 없습니다'
ErrAlertConfigInUse: '경고 설정이 사용 중이므로 삭제할 수 없습니다'
+ErrAlertConfigPhoneExist: '전화번호가 이미 존재합니다'
ErrPortInUsed: '{{ .detail }} 포트가 이미 사용 중입니다'
ErrAppLimit: '설치된 애플리케이션 수가 한도를 초과했습니다'
ErrNotInstall: '응용 프로그램이 설치되지 않았습니다'
diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml
index a4ac2aea7..d26c77439 100644
--- a/agent/i18n/lang/ms.yaml
+++ b/agent/i18n/lang/ms.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
ErrBackupLocalDelete: 'Tidak boleh padam sandaran tempatan'
ErrBackupLocalCreate: 'Tidak boleh buat sandaran tempatan'
ErrAlertConfigInUse: 'Konfigurasi amaran sedang digunakan dan tidak boleh dipadamkan'
+ErrAlertConfigPhoneExist: 'Nombor telefon sudah wujud'
ErrPortInUsed: 'Port {{ .detail }} sudah diduduki'
ErrAppLimit: 'Bilangan aplikasi yang dipasang telah melebihi had'
ErrNotInstall: 'Aplikasi tidak dipasang'
diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml
index a00023cd7..70b1798a9 100644
--- a/agent/i18n/lang/pt-BR.yaml
+++ b/agent/i18n/lang/pt-BR.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
ErrBackupLocalDelete: 'Não é permitido excluir contas locais'
ErrBackupLocalCreate: 'Não é permitido criar contas locais'
ErrAlertConfigInUse: 'A configuração de alerta está em uso e não pode ser excluída'
+ErrAlertConfigPhoneExist: 'O numero de telefone ja existe'
ErrPortInUsed: 'A porta {{ .detail }} já está ocupada'
ErrAppLimit: 'O número de aplicativos instalados excedeu o limite'
ErrNotInstall: 'Aplicativo não instalado'
diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml
index 81721dbce..aecdb256b 100644
--- a/agent/i18n/lang/ru.yaml
+++ b/agent/i18n/lang/ru.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: 'Проверка подключения не удалась: {{
ErrBackupLocalDelete: 'Нельзя удалить локальные бэкапы'
ErrBackupLocalCreate: 'Нельзя создать локальные бэкапы'
ErrAlertConfigInUse: 'Конфигурация оповещений используется и не может быть удалена'
+ErrAlertConfigPhoneExist: 'Номер телефона уже существует'
ErrPortInUsed: '{{ .detail }} порт уже занят'
ErrAppLimit: 'Количество установленных приложений превысило лимит'
ErrNotInstall: 'Приложение не установлено'
diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml
index 3f8ac9c7e..6fd8dd047 100644
--- a/agent/i18n/lang/tr.yaml
+++ b/agent/i18n/lang/tr.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
ErrBackupLocalDelete: 'Yerel yedek silme yok'
ErrBackupLocalCreate: 'Yerel yedek oluşturma yok'
ErrAlertConfigInUse: 'Uyarı yapılandırması kullanımda ve silinemez'
+ErrAlertConfigPhoneExist: 'Telefon numarasi zaten mevcut'
ErrPortInUsed: '{{ .detail }} portu zaten kullanılıyor'
ErrAppLimit: 'Yüklenen uygulama sayısı sınırı aştı'
ErrNotInstall: 'Uygulama yüklenmedi'
diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml
index 1d6a9864f..e8e1c270a 100644
--- a/agent/i18n/lang/zh-Hant.yaml
+++ b/agent/i18n/lang/zh-Hant.yaml
@@ -79,6 +79,7 @@ ErrBackupCheck: '備份帳號測試連線失敗{{ .err }}'
ErrBackupLocalDelete: '暫時不支援刪除本機伺服器備份帳號'
ErrBackupLocalCreate: '暫時不支援建立本機伺服器備份帳號'
ErrAlertConfigInUse: '告警配置正在使用中,無法刪除'
+ErrAlertConfigPhoneExist: '手機號碼已存在'
ErrPortInUsed: '{{ .detail }} 連接埠已被佔用!'
ErrAppLimit: '應用程式超出安裝數量限制'
ErrNotInstall: '應用程式未安裝'
diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml
index 4b4b3a9c2..395e5f803 100644
--- a/agent/i18n/lang/zh.yaml
+++ b/agent/i18n/lang/zh.yaml
@@ -78,6 +78,7 @@ ErrBackupCheck: "备份账号测试连接失败 {{ .err }}"
ErrBackupLocalDelete: "暂不支持删除本地服务器备份账号"
ErrBackupLocalCreate: "暂不支持创建本地服务器备份账号"
ErrAlertConfigInUse: "告警配置正在使用中,无法删除"
+ErrAlertConfigPhoneExist: "手机号已存在"
#app
ErrPortInUsed: "{{ .detail }} 端口已被占用!"
diff --git a/frontend/src/api/modules/alert.ts b/frontend/src/api/modules/alert.ts
index 181187f53..78f60dfc5 100644
--- a/frontend/src/api/modules/alert.ts
+++ b/frontend/src/api/modules/alert.ts
@@ -9,7 +9,7 @@ const alertConfigHiddenTypes = ['sms'];
const resolveAlertConfigExcludeTypes = (excludeTypes: string[] = []) => {
const globalStore = GlobalStore();
const types = new Set(excludeTypes);
- if (globalStore.isIntl || globalStore.isEE) {
+ if (globalStore.isIntl || globalStore.isEE || !globalStore.isProductPro) {
alertConfigHiddenTypes.forEach((type) => types.add(type));
}
return Array.from(types);
diff --git a/frontend/src/views/setting/alert/dash/task/index.vue b/frontend/src/views/setting/alert/dash/task/index.vue
index 81954818d..dba77f910 100644
--- a/frontend/src/views/setting/alert/dash/task/index.vue
+++ b/frontend/src/views/setting/alert/dash/task/index.vue
@@ -275,15 +275,15 @@
class="flex items-center flex-row md:flex-nowrap sm:flex-nowrap flex-wrap justify-between gap-2 w-full"
>
-
- {{ $t('commons.units.minute') }}11
+
+ {{ $t('commons.units.minute') }}
-
+
{{ $t('xpack.alert.loginFail') }}
-
+
{{ $t('commons.units.time') }}
@@ -398,7 +398,7 @@ import { routerToName } from '@/utils/router';
import { checkCidr, checkCidrV6, checkIpV4V6 } from '@/utils/validate';
import { useGlobalStore } from '@/composables/useGlobalStore';
-const { isMaster, isProductPro, isEE } = useGlobalStore();
+const { isMaster, isProductPro, isEE, isIntl } = useGlobalStore();
const alertConfigs = ref([]);
const loadAlertConfigs = async () => {
@@ -423,7 +423,7 @@ const configOptions = computed(() => {
type: c.type,
disabled:
c.status !== 'Enable' ||
- (!isProductPro.value && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(c.type)),
+ ((isIntl.value || !isProductPro.value) && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(c.type)),
}))
.sort((a, b) => Number(a.disabled) - Number(b.disabled));
});
diff --git a/frontend/src/views/setting/alert/setting/drawer/index.vue b/frontend/src/views/setting/alert/setting/drawer/index.vue
index 03c8fcc9d..be9ee76f6 100644
--- a/frontend/src/views/setting/alert/setting/drawer/index.vue
+++ b/frontend/src/views/setting/alert/setting/drawer/index.vue
@@ -156,7 +156,7 @@ const emailRules = {
const smsRules = {
smsDisplayName: [Rules.requiredInput, { validator: checkSmsDisplayNameDuplicate, trigger: 'blur' }],
- smsPhone: [Rules.requiredInput, Rules.phone, { validator: checkPhoneDuplicate, trigger: 'blur' }],
+ smsPhone: [Rules.phone, { validator: checkPhoneDuplicate, trigger: 'blur' }],
smsDailyAlertNum: [Rules.integerNumber, checkNumberRange(20, 100)],
};
@@ -302,10 +302,6 @@ function checkSmsDisplayNameDuplicate(_rule: unknown, value: string, callback: (
function checkPhoneDuplicate(_rule: unknown, value: string, callback: (error?: Error) => void) {
const currentValue = normalizeDisplayName(value);
- if (!currentValue) {
- callback();
- return;
- }
const duplicated = alertConfigs.value.some((item) => {
if (item.type !== 'sms') {
diff --git a/frontend/src/views/setting/alert/setting/index.vue b/frontend/src/views/setting/alert/setting/index.vue
index f387bcb48..005ae8ada 100644
--- a/frontend/src/views/setting/alert/setting/index.vue
+++ b/frontend/src/views/setting/alert/setting/index.vue
@@ -151,7 +151,7 @@ import { MsgSuccess } from '@/utils/message';
import AlertDrawer from '@/views/setting/alert/setting/drawer/index.vue';
import { Alert } from '@/api/interface/alert';
-const { docsUrl, isMaster, isMobile, isProductPro, isEE } = useGlobalStore();
+const { docsUrl, isMaster, isMobile, isProductPro, isEE, isIntl } = useGlobalStore();
const loading = ref(false);
const alertDrawerRef = ref();
@@ -483,7 +483,7 @@ const buttons = computed(() => [
openEditDrawer(row);
},
disabled: (row: Alert.AlertConfigInfo) =>
- !isProductPro.value && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(row.type),
+ (isIntl.value || !isProductPro.value) && ['weCom', 'dingTalk', 'feiShu', 'sms'].includes(row.type),
},
{
label: i18n.global.t('commons.button.delete'),