From 06dc3798fc9ecdcfdaa1ad9056f28bc1199b5c52 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 5 Mar 2026 12:02:56 +0800 Subject: [PATCH] feat: add delete env ref tasks chekc --- internal/controllers/env_controller.go | 25 +++++- internal/router/router.go | 1 + internal/services/env_service.go | 44 +++++++++- web/src/api/index.ts | 9 +- web/src/views/environments/Environments.vue | 97 ++++++++++++++++++--- 5 files changed, 158 insertions(+), 18 deletions(-) diff --git a/internal/controllers/env_controller.go b/internal/controllers/env_controller.go index 9ac375f..d41c29b 100644 --- a/internal/controllers/env_controller.go +++ b/internal/controllers/env_controller.go @@ -117,11 +117,32 @@ func (ec *EnvController) DeleteEnvVar(c *gin.Context) { return } - success := ec.envService.DeleteEnvVar(id) + force := c.Query("force") == "true" + success, associatedTasks := ec.envService.DeleteEnvVar(id, force) + + if len(associatedTasks) > 0 { + c.JSON(200, utils.Response{ + Code: 409, + Msg: "该环境变量已被任务引用,请先在任务中删除引用或选择强制删除", + Data: vo.ToTaskVOListFromModels(associatedTasks), + }) + return + } + if !success { - utils.NotFound(c, "环境变量不存在") + utils.NotFound(c, "环境变量不存在或删除失败") return } utils.SuccessMsg(c, "删除成功") } + +func (ec *EnvController) GetAssociatedTasks(c *gin.Context) { + id := c.Param("id") + if id == "" { + utils.BadRequest(c, "无效的环境变量ID") + return + } + tasks := ec.envService.GetAssociatedTasks(id) + utils.Success(c, vo.ToTaskVOListFromModels(tasks)) +} diff --git a/internal/router/router.go b/internal/router/router.go index 0c72e7a..664bb4b 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -139,6 +139,7 @@ func Setup(c *Controllers) *gin.Engine { env.GET("", c.Env.GetEnvVars) env.GET("/all", c.Env.GetAllEnvVars) env.GET("/:id", c.Env.GetEnvVar) + env.GET("/:id/tasks", c.Env.GetAssociatedTasks) env.PUT("/:id", c.Env.UpdateEnvVar) env.DELETE("/:id", c.Env.DeleteEnvVar) } diff --git a/internal/services/env_service.go b/internal/services/env_service.go index 411f2a2..7e42cda 100644 --- a/internal/services/env_service.go +++ b/internal/services/env_service.go @@ -6,6 +6,8 @@ import ( "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/models" "github.com/engigu/baihu-panel/internal/utils" + + "gorm.io/gorm" ) type EnvService struct{} @@ -72,9 +74,47 @@ func (es *EnvService) UpdateEnvVar(id string, name, value, remark string, hidden return &env } -func (es *EnvService) DeleteEnvVar(id string) bool { +func (es *EnvService) GetAssociatedTasks(id string) []models.Task { + var associatedTasks []models.Task + query := "envs = ? OR envs LIKE ? OR envs LIKE ? OR envs LIKE ?" + database.DB.Where(query, id, id+",%", "%,"+id, "%,"+id+",%").Find(&associatedTasks) + return associatedTasks +} + +func (es *EnvService) DeleteEnvVar(id string, force bool) (bool, []models.Task) { + associatedTasks := es.GetAssociatedTasks(id) + + if len(associatedTasks) > 0 && !force { + return false, associatedTasks + } + + if force { + err := database.DB.Transaction(func(tx *gorm.DB) error { + // Update tasks to remove this env ID + for _, task := range associatedTasks { + ids := splitEnvIDs(task.Envs) + var newIDs []string + for _, eid := range ids { + if eid != id { + newIDs = append(newIDs, eid) + } + } + newEnvs := strings.Join(newIDs, ",") + if err := tx.Model(&task).Update("envs", newEnvs).Error; err != nil { + return err + } + } + // Delete the env var + if err := tx.Where("id = ?", id).Delete(&models.EnvironmentVariable{}).Error; err != nil { + return err + } + return nil + }) + return err == nil, nil + } + result := database.DB.Where("id = ?", id).Delete(&models.EnvironmentVariable{}) - return result.RowsAffected > 0 + return result.RowsAffected > 0, nil } // GetEnvVarsByIDs 根据逗号分隔的ID字符串获取环境变量列表,返回 NAME=VALUE 格式 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index a97ba83..56a0e76 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -89,9 +89,16 @@ export const api = { return request(`/env?${query}`) }, all: () => request('/env/all'), + tasks: (id: string) => request(`/env/${id}/tasks`), create: (data: Partial) => request('/env', { method: 'POST', body: JSON.stringify(data) }), update: (id: string, data: Partial) => request(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) }), - delete: (id: string) => request(`/env/${id}`, { method: 'DELETE' }) + delete: (id: string, force?: boolean) => { + const query = force ? '?force=true' : '' + return fetch(`${API_BASE_URL}/env/${id}${query}`, { + method: 'DELETE', + credentials: 'include' + }).then(res => res.json() as Promise>) + } }, execute: { command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }), diff --git a/web/src/views/environments/Environments.vue b/web/src/views/environments/Environments.vue index a172151..42a8032 100644 --- a/web/src/views/environments/Environments.vue +++ b/web/src/views/environments/Environments.vue @@ -2,12 +2,12 @@ import { ref, onMounted, watch } from 'vue' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' +import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import Pagination from '@/components/Pagination.vue' -import { Plus, Pencil, Trash2, Eye, EyeOff, Search } from 'lucide-vue-next' +import { Plus, Pencil, Trash2, Eye, EyeOff, Search, AlertTriangle, Terminal } from 'lucide-vue-next' import TextOverflow from '@/components/TextOverflow.vue' import { api, type EnvVar } from '@/api' import { toast } from 'vue-sonner' @@ -23,6 +23,8 @@ const isEdit = ref(false) const showValues = ref>({}) const showDeleteDialog = ref(false) const deleteEnvId = ref(null) +const associatedTasks = ref([]) +const isDeleting = ref(false) const filterName = ref('') const currentPage = ref(1) @@ -84,22 +86,49 @@ async function saveEnv() { } catch { toast.error('保存失败') } } -function confirmDelete(id: string) { +async function confirmDelete(id: string) { deleteEnvId.value = id - showDeleteDialog.value = true + try { + const res = await api.env.tasks(id) + associatedTasks.value = res || [] + showDeleteDialog.value = true + } catch { + toast.error('检查变量引用失败') + } } -async function deleteEnv() { +async function deleteEnv(force = false) { if (!deleteEnvId.value) return + isDeleting.value = true try { - await api.env.delete(deleteEnvId.value) + const res = await api.env.delete(deleteEnvId.value, force) + if (res.code === 409) { + associatedTasks.value = res.data || [] + isDeleting.value = false + return + } + if (res.code !== 200) { + toast.error(res.msg || '删除失败') + isDeleting.value = false + return + } toast.success('变量已删除') loadEnvVars() - } catch { toast.error('删除失败') } - showDeleteDialog.value = false - deleteEnvId.value = null + showDeleteDialog.value = false + } catch { + toast.error('网络错误,删除失败') + } finally { + isDeleting.value = false + } } +watch(showDeleteDialog, (val) => { + if (!val) { + associatedTasks.value = [] + deleteEnvId.value = null + } +}) + function toggleShow(id: string) { showValues.value[id] = !showValues.value[id] } @@ -208,12 +237,54 @@ onMounted(loadEnvVars) 确认删除 - 确定要删除此环境变量吗?此操作无法撤销。 + +
+
+ +
+

环境变量正在使用中

+

+ 该变量已被以下任务引用,直接删除可能导致任务运行失败。建议先移除引用或选择“强制删除”。 +

+
+
+ +
+
+

关联任务 ({{ associatedTasks.length }})

+
+
+
+
+ + {{ task.name }} +
+ {{ task.id }} +
+
+
+ +
+

+ 提示:选择强制删除将自动解除以上任务对该变量的绑定并执行物理删除。 +

+
+
+

确定要删除此环境变量吗?此操作无法撤销,请谨慎操作。

+
- 取消 - 删除 - + 取消 + +