feat: 定时任务编辑入口全栈打通 + 修复ApprovalFlow编译错误 + 完成大文件拆分重构

- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
  复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
  保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
  (getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
   approveInstance→approveProcess, rejectInstance→rejectProcess)
  + 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
  ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
  user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
  // Logan.Li
This commit is contained in:
attacker committed 2026-06-15 01:09:49 +08:00
1 parent 8be63ac168
commit a7e2351ad2
37 files changed
+7374 -4891

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,343 @@
package handlers
import (
"net/http"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
)
// ListCronJobs 获取定时任务列表
// @Summary 获取定时任务列表
// @Description 获取所有定时任务列表
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /cron-jobs [get]
func (h *AutomationHandler) ListCronJobs(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.CronJob{}).Count(&total)
var jobs []models.CronJob
h.db.Preload("Script").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&jobs)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: jobs,
Total: total,
},
})
}
// GetCronJob 获取定时任务详情
// @Summary 获取定时任务详情
// @Description 获取指定定时任务的详细信息
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id} [get]
func (h *AutomationHandler) GetCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.Preload("Script").First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: job,
})
}
// CreateCronJob 创建定时任务
// @Summary 创建定时任务
// @Description 创建新定时任务
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateCronJobRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs [post]
func (h *AutomationHandler) CreateCronJob(c *gin.Context) {
var req models.CreateCronJobRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
job := models.CronJob{
Name: req.Name,
Description: req.Description,
CronExpr: req.CronExpr,
Type: req.Type,
Content: req.Content,
ScriptID: req.ScriptID,
TargetType: req.TargetType,
Timeout: req.Timeout,
NotifyOn: req.NotifyOn,
Enabled: true,
}
if job.Timeout == 0 {
job.Timeout = 300
}
if job.NotifyOn == "" {
job.NotifyOn = "failure"
}
if err := h.db.Create(&job).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: job,
})
}
// UpdateCronJob 更新定时任务
// @Summary 更新定时任务
// @Description 更新指定定时任务
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Param request body models.CreateCronJobRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id} [put]
func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateCronJobRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"cron_expr": req.CronExpr,
"type": req.Type,
"content": req.Content,
"script_id": req.ScriptID,
"target_type": req.TargetType,
"timeout": req.Timeout,
"notify_on": req.NotifyOn,
}
if err := h.db.Model(&job).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Script").First(&job, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: job,
})
}
// DeleteCronJob 删除定时任务
// @Summary 删除定时任务
// @Description 删除指定定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response
// @Router /cron-jobs/{id} [delete]
func (h *AutomationHandler) DeleteCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.CronJob{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ToggleCronJob 切换定时任务状态
// @Summary 切换定时任务状态
// @Description 启用或禁用定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id}/toggle [post]
func (h *AutomationHandler) ToggleCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
job.Enabled = !job.Enabled
if err := h.db.Save(&job).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "操作失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "操作成功",
Data: job,
})
}
// ExecuteCronJob 执行定时任务
// @Summary 执行定时任务
// @Description 手动执行定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.TaskExecution}
// @Router /cron-jobs/{id}/execute [post]
func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
// Resolve the content to execute. For type=script, load the linked Script.
content := job.Content
scriptType := "shell"
if job.Type == "script" && job.ScriptID != nil {
var script models.Script
if err := h.db.First(&script, *job.ScriptID).Error; err == nil {
content = script.Content
scriptType = script.Type
h.db.Model(&script).UpdateColumn("run_count", script.RunCount+1)
}
}
now := time.Now()
execution := models.TaskExecution{
TaskType: "cron",
TaskID: uint(id),
TaskName: job.Name,
Status: "running",
TargetType: job.TargetType,
Targets: job.Targets,
StartedAt: &now,
}
if err := h.db.Create(&execution).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建执行记录失败",
})
return
}
// Execute locally on the AIOps host.
output, exitCode, duration := runLocal(scriptType, content, job.Timeout)
finishedAt := time.Now()
status := "success"
if exitCode != 0 {
status = "failed"
}
h.db.Model(&execution).Updates(map[string]interface{}{
"status": status,
"output": output,
"duration": duration,
"finished_at": finishedAt,
})
// Update cron job statistics.
statsUpdates := map[string]interface{}{
"last_run_at": now,
"last_status": status,
"run_count": job.RunCount + 1,
}
if status == "success" {
statsUpdates["success_count"] = job.SuccessCount + 1
} else {
statsUpdates["fail_count"] = job.FailCount + 1
}
h.db.Model(&job).Updates(statsUpdates)
execution.Output = output
execution.Status = status
execution.Duration = duration
execution.FinishedAt = &finishedAt
msg := "执行成功"
if status == "failed" {
msg = "执行失败"
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: execution,
})
}
@@ -0,0 +1,358 @@
package handlers
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"os/exec"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
)
// runPlaybookLocal executes an Ansible playbook YAML locally on the AIOps host.
// It writes the content to a temporary file and invokes `ansible-playbook`
// against localhost with a local connection. If the ansible-playbook binary is
// missing the exec error is captured in the output. Returns combined
// stdout+stderr, the exit code (-1 on failure) and duration in milliseconds.
func runPlaybookLocal(content, inventory string, timeoutSec int) (output string, exitCode int, durationMs int) {
start := time.Now()
timeout := time.Duration(timeoutSec) * time.Second
if timeout <= 0 {
timeout = 600 * time.Second
}
// Write the playbook YAML to a temporary file.
tmpFile, err := os.CreateTemp("", "ezops-playbook-*.yml")
if err != nil {
return fmt.Sprintf("创建临时文件失败: %v", err), -1, 0
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return fmt.Sprintf("写入临时文件失败: %v", err), -1, 0
}
tmpFile.Close()
// Write an optional inventory file.
inventoryArg := "localhost,"
if inventory != "" {
invFile, err := os.CreateTemp("", "ezops-inventory-*.ini")
if err == nil {
if _, werr := invFile.WriteString(inventory); werr == nil {
inventoryArg = invFile.Name()
defer os.Remove(invFile.Name())
}
invFile.Close()
}
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
args := []string{"-i", inventoryArg}
if inventoryArg == "localhost," {
args = append(args, "--connection=local")
}
args = append(args, tmpFile.Name())
cmd := exec.CommandContext(ctx, "ansible-playbook", args...)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
runErr := cmd.Run()
durationMs = int(time.Since(start).Milliseconds())
output = out.String()
if ctx.Err() == context.DeadlineExceeded {
output += fmt.Sprintf("\n[超时] 执行超过 %d 秒被终止", timeoutSec)
return output, -1, durationMs
}
if runErr != nil {
if exitError, ok := runErr.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
} else {
// Binary not found or other exec error (e.g. ansible-playbook not
// installed). Provide a clear hint instead of an empty output.
exitCode = -1
if output == "" {
output = fmt.Sprintf("无法执行 ansible-playbook:%v\n(请确认 AIOps 主机已安装 ansible:yum install -y ansible / pip install ansible)", runErr)
}
}
return output, exitCode, durationMs
}
return output, 0, durationMs
}
// ListPlaybooks 获取Playbook列表
// @Summary 获取Playbook列表
// @Description 获取所有Ansible Playbook列表
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /playbooks [get]
func (h *AutomationHandler) ListPlaybooks(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.Playbook{}).Count(&total)
var playbooks []models.Playbook
h.db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&playbooks)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: playbooks,
Total: total,
},
})
}
// GetPlaybook 获取Playbook详情
// @Summary 获取Playbook详情
// @Description 获取指定Playbook的详细信息
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "Playbook ID"
// @Success 200 {object} models.Response{data=models.Playbook}
// @Router /playbooks/{id} [get]
func (h *AutomationHandler) GetPlaybook(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var playbook models.Playbook
if err := h.db.First(&playbook, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "Playbook不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: playbook,
})
}
// CreatePlaybook 创建Playbook
// @Summary 创建Playbook
// @Description 创建新的Ansible Playbook
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreatePlaybookRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.Playbook}
// @Router /playbooks [post]
func (h *AutomationHandler) CreatePlaybook(c *gin.Context) {
var req models.CreatePlaybookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
playbook := models.Playbook{
Name: req.Name,
Code: randomCode("pb"),
Description: req.Description,
Content: req.Content,
Category: req.Category,
Tags: req.Tags,
Variables: req.Variables,
Inventory: req.Inventory,
Timeout: req.Timeout,
}
if playbook.Category == "" {
playbook.Category = "deployment"
}
if playbook.Timeout == 0 {
playbook.Timeout = 600
}
if err := h.db.Create(&playbook).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: playbook,
})
}
// UpdatePlaybook 更新Playbook
// @Summary 更新Playbook
// @Description 更新指定Playbook
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Playbook ID"
// @Param request body models.CreatePlaybookRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Playbook}
// @Router /playbooks/{id} [put]
func (h *AutomationHandler) UpdatePlaybook(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreatePlaybookRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var playbook models.Playbook
if err := h.db.First(&playbook, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "Playbook不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"content": req.Content,
"category": req.Category,
"tags": req.Tags,
"variables": req.Variables,
"inventory": req.Inventory,
"timeout": req.Timeout,
}
if err := h.db.Model(&playbook).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&playbook, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: playbook,
})
}
// DeletePlaybook 删除Playbook
// @Summary 删除Playbook
// @Description 删除指定Playbook
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "Playbook ID"
// @Success 200 {object} models.Response
// @Router /playbooks/{id} [delete]
func (h *AutomationHandler) DeletePlaybook(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Playbook{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ExecutePlaybook 执行Playbook
// @Summary 执行Playbook
// @Description 在AIOps主机上本地执行Ansible Playbook
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Playbook ID"
// @Success 200 {object} models.Response{data=models.TaskExecution}
// @Router /playbooks/{id}/execute [post]
func (h *AutomationHandler) ExecutePlaybook(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var playbook models.Playbook
if err := h.db.First(&playbook, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "Playbook不存在",
})
return
}
now := time.Now()
execution := models.TaskExecution{
TaskType: "playbook",
TaskID: uint(id),
TaskName: playbook.Name,
Status: "running",
StartedAt: &now,
}
if err := h.db.Create(&execution).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建执行记录失败",
})
return
}
// Execute the playbook locally on the AIOps host.
output, exitCode, duration := runPlaybookLocal(playbook.Content, playbook.Inventory, playbook.Timeout)
finishedAt := time.Now()
status := "success"
if exitCode != 0 {
status = "failed"
}
h.db.Model(&execution).Updates(map[string]interface{}{
"status": status,
"output": output,
"duration": duration,
"finished_at": finishedAt,
})
h.db.Model(&playbook).UpdateColumn("run_count", playbook.RunCount+1)
execution.Output = output
execution.Status = status
execution.Duration = duration
execution.FinishedAt = &finishedAt
msg := "执行成功"
if status == "failed" {
msg = "执行失败"
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: execution,
})
}
@@ -0,0 +1,322 @@
package handlers
import (
"net/http"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
)
// ListScripts 获取脚本列表
// @Summary 获取脚本列表
// @Description 获取所有脚本列表
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /scripts [get]
func (h *AutomationHandler) ListScripts(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.Script{}).Count(&total)
var scripts []models.Script
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&scripts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: scripts,
Total: total,
},
})
}
// GetScript 获取脚本详情
// @Summary 获取脚本详情
// @Description 获取指定脚本的详细信息
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts/{id} [get]
func (h *AutomationHandler) GetScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: script,
})
}
// CreateScript 创建脚本
// @Summary 创建脚本
// @Description 创建新脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateScriptRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts [post]
func (h *AutomationHandler) CreateScript(c *gin.Context) {
var req models.CreateScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
script := models.Script{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Type: req.Type,
Content: req.Content,
Params: req.Params,
Timeout: req.Timeout,
}
if script.Type == "" {
script.Type = "shell"
}
if script.Timeout == 0 {
script.Timeout = 300
}
if err := h.db.Create(&script).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: script,
})
}
// UpdateScript 更新脚本
// @Summary 更新脚本
// @Description 更新指定脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Param request body models.CreateScriptRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts/{id} [put]
func (h *AutomationHandler) UpdateScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"type": req.Type,
"content": req.Content,
"params": req.Params,
"timeout": req.Timeout,
"version": script.Version + 1,
}
if err := h.db.Model(&script).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&script, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: script,
})
}
// DeleteScript 删除脚本
// @Summary 删除脚本
// @Description 删除指定脚本
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Success 200 {object} models.Response
// @Router /scripts/{id} [delete]
func (h *AutomationHandler) DeleteScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Script{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ExecuteScript 执行脚本
// @Summary 执行脚本
// @Description 在目标服务器上执行脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Param request body models.ExecuteScriptRequest true "执行请求"
// @Success 200 {object} models.Response{data=models.TaskExecution}
// @Router /scripts/{id}/execute [post]
func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
// The request body is optional — when omitted we execute locally.
var req models.ExecuteScriptRequest
_ = c.ShouldBindJSON(&req)
now := time.Now()
execution := models.TaskExecution{
TaskType: "script",
TaskID: uint(id),
TaskName: script.Name,
Status: "running",
TargetType: req.TargetType,
StartedAt: &now,
}
if err := h.db.Create(&execution).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建执行记录失败",
})
return
}
// Execute locally on the AIOps host.
output, exitCode, duration := runLocal(script.Type, script.Content, script.Timeout)
finishedAt := time.Now()
status := "success"
if exitCode != 0 {
status = "failed"
}
h.db.Model(&execution).Updates(map[string]interface{}{
"status": status,
"output": output,
"duration": duration,
"finished_at": finishedAt,
})
h.db.Model(&script).UpdateColumn("run_count", script.RunCount+1)
execution.Output = output
execution.Status = status
execution.Duration = duration
execution.FinishedAt = &finishedAt
msg := "执行成功"
if status == "failed" {
msg = "执行失败"
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: execution,
})
}
// ListExecutions 获取任务执行记录列表
// @Summary 获取任务执行记录
// @Description 获取脚本/定时任务的执行历史记录(支持 task_type / task_id 筛选)
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param task_type query string false "任务类型 (cron/script)"
// @Param task_id query string false "任务ID"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /task-executions [get]
func (h *AutomationHandler) ListExecutions(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.TaskExecution{})
if taskType := c.Query("task_type"); taskType != "" {
db = db.Where("task_type = ?", taskType)
}
if taskID := c.Query("task_id"); taskID != "" {
db = db.Where("task_id = ?", taskID)
}
var total int64
db.Count(&total)
var execs []models.TaskExecution
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&execs)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: execs,
Total: total,
},
})
}
@@ -0,0 +1,202 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// CloudAccountHandler 云账号处理器
type CloudAccountHandler struct {
db *gorm.DB
}
// NewCloudAccountHandler 创建云账号处理器
func NewCloudAccountHandler(db *gorm.DB) *CloudAccountHandler {
return &CloudAccountHandler{db: db}
}
// ListCloudAccounts 获取云账号列表
// @Summary 获取云账号列表
// @Description 获取所有云账号列表
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /cloud-accounts [get]
func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) {
var accounts []models.CloudAccount
h.db.Find(&accounts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: accounts,
Total: int64(len(accounts)),
},
})
}
// createCloudAccountRequest 创建云账号请求(独立 request struct,避免 CloudAccount.AccessKey/SecretKey 的 json:"-" 导致无法反序列化)
type createCloudAccountRequest struct {
Name string `json:"name"`
Provider string `json:"provider"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
MonthlyCost float64 `json:"monthly_cost"`
Status string `json:"status"`
}
// CreateCloudAccount 创建云账号
// @Summary 创建云账号
// @Description 创建新云账号
// @Tags 云账号
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body createCloudAccountRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.CloudAccount}
// @Router /cloud-accounts [post]
func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
var req createCloudAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Provider) == "" {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "账号名称和云平台不能为空",
})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
account := models.CloudAccount{
Name: strings.TrimSpace(req.Name),
Provider: strings.TrimSpace(req.Provider),
AccessKey: req.AccessKey,
SecretKey: req.SecretKey,
MonthlyCost: req.MonthlyCost,
Status: status,
}
if err := h.db.Create(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: account,
})
}
// UpdateCloudAccount 更新云账号
// @Summary 更新云账号
// @Description 更新指定云账号信息
// @Tags 云账号
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "云账号ID"
// @Param request body createCloudAccountRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.CloudAccount}
// @Router /cloud-accounts/{id} [put]
func (h *CloudAccountHandler) UpdateCloudAccount(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req createCloudAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var account models.CloudAccount
if err := h.db.First(&account, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "云账号不存在",
})
return
}
// 只在提供非空值时更新凭证字段
if strings.TrimSpace(req.Name) != "" {
account.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Provider) != "" {
account.Provider = strings.TrimSpace(req.Provider)
}
if strings.TrimSpace(req.AccessKey) != "" {
account.AccessKey = req.AccessKey
}
if strings.TrimSpace(req.SecretKey) != "" {
account.SecretKey = req.SecretKey
}
if strings.TrimSpace(req.Status) != "" {
account.Status = strings.TrimSpace(req.Status)
}
account.MonthlyCost = req.MonthlyCost
if err := h.db.Save(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: account,
})
}
// DeleteCloudAccount 删除云账号
// @Summary 删除云账号
// @Description 删除指定云账号
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Param id path int true "云账号ID"
// @Success 200 {object} models.Response
// @Router /cloud-accounts/{id} [delete]
func (h *CloudAccountHandler) DeleteCloudAccount(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.CloudAccount{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
@@ -0,0 +1,230 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DomainHandler 域名处理器
type DomainHandler struct {
db *gorm.DB
}
// NewDomainHandler 创建域名处理器
func NewDomainHandler(db *gorm.DB) *DomainHandler {
return &DomainHandler{db: db}
}
// ListDomains 获取域名列表
// @Summary 获取域名列表
// @Description 获取所有域名列表
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /domains [get]
func (h *DomainHandler) ListDomains(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.Domain{})
if query.Keyword != "" {
db = db.Where("name LIKE ?", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var domains []models.Domain
db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&domains)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: domains,
Total: total,
},
})
}
// CreateDomain 创建域名
// @Summary 创建域名
// @Description 创建新域名
// @Tags 域名
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.Domain true "创建请求"
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains [post]
func (h *DomainHandler) CreateDomain(c *gin.Context) {
var req updateDomainRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if strings.TrimSpace(req.Name) == "" {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "域名不能为空",
})
return
}
status := req.Status
if status == "" {
status = "active"
}
domain := models.Domain{
Name: strings.TrimSpace(req.Name),
Registrar: req.Registrar,
Status: status,
ExpireDate: parseDateStr(req.ExpireDate),
DNSProvider: req.DNSProvider,
RecordCount: req.RecordCount,
SSLEnabled: req.SSLEnabled,
SSLExpireDate: parseDateStr(req.SSLExpireDate),
}
if err := h.db.Create(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: domain,
})
}
// updateDomainRequest 域名创建/更新请求(日期字段用字符串接收,避免 RFC3339 解析失败)
type updateDomainRequest struct {
Name string `json:"name"`
Registrar string `json:"registrar"`
Status string `json:"status"`
ExpireDate string `json:"expire_date"`
DNSProvider string `json:"dns_provider"`
RecordCount int `json:"record_count"`
SSLEnabled bool `json:"ssl_enabled"`
SSLExpireDate string `json:"ssl_expire_date"`
}
// parseDateStr 将 YYYY-MM-DD 格式字符串解析为 *time.Time,空串返回 nil
func parseDateStr(s string) *time.Time {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return nil
}
return &t
}
// DeleteDomain 删除域名
// @Summary 删除域名
// @Description 删除指定域名
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param id path int true "域名ID"
// @Success 200 {object} models.Response
// @Router /domains/{id} [delete]
func (h *DomainHandler) DeleteDomain(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Domain{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// UpdateDomain 更新域名
// @Summary 更新域名
// @Description 更新指定域名信息
// @Tags 域名
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "域名ID"
// @Param request body updateDomainRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains/{id} [put]
func (h *DomainHandler) UpdateDomain(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req updateDomainRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var domain models.Domain
if err := h.db.First(&domain, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "域名不存在",
})
return
}
if strings.TrimSpace(req.Name) != "" {
domain.Name = strings.TrimSpace(req.Name)
}
domain.Registrar = req.Registrar
domain.Status = req.Status
if domain.Status == "" {
domain.Status = "active"
}
domain.ExpireDate = parseDateStr(req.ExpireDate)
domain.DNSProvider = req.DNSProvider
domain.RecordCount = req.RecordCount
domain.SSLEnabled = req.SSLEnabled
domain.SSLExpireDate = parseDateStr(req.SSLExpireDate)
if err := h.db.Save(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: domain,
})
}
@@ -0,0 +1,132 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ==================== 云平台(Platform / Cloud)====================
// PlatformHandler 云平台处理器
type PlatformHandler struct {
db *gorm.DB
}
func NewPlatformHandler(db *gorm.DB) *PlatformHandler {
return &PlatformHandler{db: db}
}
// platformRequest 云平台创建/更新请求
type platformRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Type string `json:"type"`
Status string `json:"status"`
Website string `json:"website"`
Description string `json:"description"`
}
// ListPlatforms 获取云平台列表
func (h *PlatformHandler) ListPlatforms(c *gin.Context) {
var platforms []models.Cloud
h.db.Find(&platforms)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: platforms,
Total: int64(len(platforms)),
},
})
}
// CreatePlatform 创建云平台
func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "平台名称和编码不能为空"})
return
}
cloudType := strings.TrimSpace(req.Type)
if cloudType == "" {
cloudType = "public"
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
platform := models.Cloud{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Type: cloudType,
Status: status,
Website: req.Website,
Description: req.Description,
}
if err := h.db.Create(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: platform})
}
// UpdatePlatform 更新云平台
func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var platform models.Cloud
if err := h.db.First(&platform, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云平台不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
platform.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
platform.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Type) != "" {
platform.Type = strings.TrimSpace(req.Type)
}
if strings.TrimSpace(req.Status) != "" {
platform.Status = strings.TrimSpace(req.Status)
}
platform.Website = req.Website
platform.Description = req.Description
if err := h.db.Save(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: platform})
}
// DeletePlatform 删除云平台
func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Cloud{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
@@ -0,0 +1,127 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ==================== 区域管理(Region)====================
// RegionHandler 区域处理器
type RegionHandler struct {
db *gorm.DB
}
func NewRegionHandler(db *gorm.DB) *RegionHandler {
return &RegionHandler{db: db}
}
// regionRequest 区域创建/更新请求
type regionRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Provider string `json:"provider"`
Location string `json:"location"`
Status string `json:"status"`
Endpoint string `json:"endpoint"`
}
// ListRegions 获取区域列表
func (h *RegionHandler) ListRegions(c *gin.Context) {
var regions []models.Region
h.db.Find(&regions)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: regions,
Total: int64(len(regions)),
},
})
}
// CreateRegion 创建区域
func (h *RegionHandler) CreateRegion(c *gin.Context) {
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "区域名称和编码不能为空"})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
region := models.Region{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Provider: strings.TrimSpace(req.Provider),
Location: req.Location,
Status: status,
Endpoint: req.Endpoint,
}
if err := h.db.Create(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: region})
}
// UpdateRegion 更新区域
func (h *RegionHandler) UpdateRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var region models.Region
if err := h.db.First(&region, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "区域不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
region.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
region.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Provider) != "" {
region.Provider = strings.TrimSpace(req.Provider)
}
region.Location = req.Location
if strings.TrimSpace(req.Status) != "" {
region.Status = strings.TrimSpace(req.Status)
}
region.Endpoint = req.Endpoint
if err := h.db.Save(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: region})
}
// DeleteRegion 删除区域
func (h *RegionHandler) DeleteRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Region{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
-496
View File
@@ -12,7 +12,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@@ -318,498 +317,3 @@ func (h *LDAPHandler) TestLDAPConnection(c *gin.Context) {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"}})
}
// ListLDAPUsers 列出LDAP用户
// POST /api/v1/admin/ldap-config/users
func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
var req struct {
OuDn string `json:"ou_dn"`
}
c.ShouldBindJSON(&req)
var cfg models.LDAPConfig
if err := h.db.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
defer conn.Close()
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
searchBase := cfg.BaseDN
if req.OuDn != "" {
searchBase = req.OuDn
}
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
baseFilter = strings.ReplaceAll(sf, "%s", "*")
} else {
baseFilter = sf
}
}
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
200, 0, false, baseFilter,
[]string{"uid", "cn", "mail", "dn", "telephoneNumber", "title", "department"},
nil,
))
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
type LdapUser struct {
DN string `json:"dn"`
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
Phone string `json:"phone"`
Title string `json:"title"`
Department string `json:"department"`
}
var users []LdapUser
for _, e := range sr.Entries {
users = append(users, LdapUser{
DN: e.DN,
UID: e.GetAttributeValue("uid"),
CN: e.GetAttributeValue("cn"),
Mail: e.GetAttributeValue("mail"),
Phone: e.GetAttributeValue("telephoneNumber"),
Title: e.GetAttributeValue("title"),
Department: e.GetAttributeValue("department"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: users})
}
// BatchImportLDAP 批量导入LDAP用户
// POST /api/v1/admin/ldap-config/import
func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
var req struct {
Users []struct {
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
} `json:"users"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
imported := 0
skipped := 0
for _, lu := range req.Users {
uid := lu.UID
if uid == "" {
uid = lu.Username
}
cn := lu.CN
if cn == "" {
cn = lu.Nickname
}
mail := lu.Mail
if mail == "" {
mail = lu.Email
}
if uid == "" {
skipped++
continue
}
// Check if already exists
var existing models.User
if err := h.db.Where("username = ?", uid).First(&existing).Error; err == nil {
// Already exists, update info
updates := map[string]interface{}{
"auth_source": "ldap",
}
if cn != "" {
updates["real_name"] = cn
}
if mail != "" {
updates["email"] = mail
}
h.db.Model(&existing).Updates(updates)
imported++
continue
}
// Create new LDAP user
user := models.User{
Username: uid,
Password: "-", // LDAP users don't have local password
RealName: cn,
Email: mail,
Status: 1,
AuthSource: "ldap",
}
if err := h.db.Create(&user).Error; err != nil {
skipped++
continue
}
imported++
}
// bcrypt import to avoid unused import error
_ = bcrypt.MinCost
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: fmt.Sprintf("导入完成: %d 个用户成功, %d 个跳过", imported, skipped),
Data: gin.H{"imported": imported, "skipped": skipped},
})
}
// ListLDAPOUs 列出 LDAP 组织单元
// POST /api/v1/admin/ldap-config/ou-list
func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
searchReq := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=organizationalUnit)",
[]string{"ou", "description"},
nil,
)
sr, err := conn.Search(searchReq)
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
type OU struct {
DN string `json:"dn"`
Name string `json:"name"`
Description string `json:"description"`
}
var ous []OU
for _, e := range sr.Entries {
ous = append(ous, OU{
DN: e.DN,
Name: e.GetAttributeValue("ou"),
Description: e.GetAttributeValue("description"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ous})
}
// CreateLDAPOU 创建组织单元
// POST /api/v1/admin/ldap-config/create-ou
func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
var req struct {
OUName string `json:"ou_name"`
ParentDN string `json:"parent_dn"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.ParentDN == "" {
req.ParentDN = cfg.BaseDN
}
ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OUName), req.ParentDN)
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(ouDN, nil)
addReq.Attribute("objectClass", []string{"organizationalUnit", "top"})
addReq.Attribute("ou", []string{req.OUName})
if req.Description != "" {
addReq.Attribute("description", []string{req.Description})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建OU失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
}
// CreateLDAPUser 在LDAP中创建用户
// POST /api/v1/admin/ldap-config/create-user
func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
OUDN string `json:"ou_dn"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "用户名和密码不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.OUDN == "" {
req.OUDN = cfg.BaseDN
}
userDN := fmt.Sprintf("uid=%s,%s", ldap.EscapeFilter(req.Username), req.OUDN)
// Parse user mapping
mapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"}
if cfg.UserMapping != nil {
_ = json.Unmarshal([]byte(*cfg.UserMapping), &mapping)
}
uidAttr := mapping["username"]
if uidAttr == "" {
uidAttr = "uid"
}
mailAttr := mapping["email"]
if mailAttr == "" {
mailAttr = "mail"
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(userDN, nil)
addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
addReq.Attribute(uidAttr, []string{req.Username})
addReq.Attribute("cn", []string{req.DisplayName})
addReq.Attribute("sn", []string{req.DisplayName})
if req.Email != "" {
addReq.Attribute(mailAttr, []string{req.Email})
}
addReq.Attribute("userPassword", []string{req.Password})
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建用户失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}})
}
// DeleteLDAPUser 删除LDAP条目(用户或OU)
// POST /api/v1/admin/ldap-config/delete-user
func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) {
var req struct {
DN string `json:"dn"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
delReq := ldap.NewDelRequest(req.DN, nil)
if err := conn.Del(delReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
// UpdateLDAPUser 修改 LDAP 用户属性
// POST /api/v1/admin/ldap-config/update-user
func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
var req struct {
DN string `json:"dn"`
CN string `json:"cn"`
Mail string `json:"mail"`
Phone string `json:"phone"`
Title string `json:"title"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
// 先查当前值,做 diff
sr, err := conn.Search(ldap.NewSearchRequest(
req.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
1, 0, false, "(objectClass=*)",
[]string{"cn", "mail", "telephoneNumber", "title"},
nil,
))
if err != nil || len(sr.Entries) == 0 {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "用户不存在"})
return
}
entry := sr.Entries[0]
modReq := ldap.NewModifyRequest(req.DN, nil)
// cn
oldCN := entry.GetAttributeValue("cn")
if req.CN != "" && req.CN != oldCN {
modReq.Replace("cn", []string{req.CN})
modReq.Replace("sn", []string{req.CN}) // sn 同步更新
}
// mail
oldMail := entry.GetAttributeValue("mail")
if req.Mail != oldMail {
if req.Mail == "" {
oldVals := entry.GetAttributeValues("mail")
if len(oldVals) > 0 {
modReq.Delete("mail", oldVals)
}
} else {
modReq.Replace("mail", []string{req.Mail})
}
}
// telephoneNumber
oldPhone := entry.GetAttributeValue("telephoneNumber")
if req.Phone != oldPhone {
if req.Phone == "" {
oldVals := entry.GetAttributeValues("telephoneNumber")
if len(oldVals) > 0 {
modReq.Delete("telephoneNumber", oldVals)
}
} else {
modReq.Replace("telephoneNumber", []string{req.Phone})
}
}
// title
oldTitle := entry.GetAttributeValue("title")
if req.Title != oldTitle {
if req.Title == "" {
oldVals := entry.GetAttributeValues("title")
if len(oldVals) > 0 {
modReq.Delete("title", oldVals)
}
} else {
modReq.Replace("title", []string{req.Title})
}
}
if len(modReq.Changes) > 0 {
if err := conn.Modify(modReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "修改失败: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "修改成功"})
}
+122
View File
@@ -0,0 +1,122 @@
package handlers
import (
"fmt"
"net/http"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
)
// ListLDAPOUs 列出 LDAP 组织单元
// POST /api/v1/admin/ldap-config/ou-list
func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
searchReq := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=organizationalUnit)",
[]string{"ou", "description"},
nil,
)
sr, err := conn.Search(searchReq)
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
type OU struct {
DN string `json:"dn"`
Name string `json:"name"`
Description string `json:"description"`
}
var ous []OU
for _, e := range sr.Entries {
ous = append(ous, OU{
DN: e.DN,
Name: e.GetAttributeValue("ou"),
Description: e.GetAttributeValue("description"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ous})
}
// CreateLDAPOU 创建组织单元
// POST /api/v1/admin/ldap-config/create-ou
func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
var req struct {
OUName string `json:"ou_name"`
ParentDN string `json:"parent_dn"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.ParentDN == "" {
req.ParentDN = cfg.BaseDN
}
ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OUName), req.ParentDN)
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(ouDN, nil)
addReq.Attribute("objectClass", []string{"organizationalUnit", "top"})
addReq.Attribute("ou", []string{req.OUName})
if req.Description != "" {
addReq.Attribute("description", []string{req.Description})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建OU失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
}
+397
View File
@@ -0,0 +1,397 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3"
"golang.org/x/crypto/bcrypt"
)
// ListLDAPUsers 列出LDAP用户
// POST /api/v1/admin/ldap-config/users
func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
var req struct {
OuDn string `json:"ou_dn"`
}
c.ShouldBindJSON(&req)
var cfg models.LDAPConfig
if err := h.db.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
defer conn.Close()
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
searchBase := cfg.BaseDN
if req.OuDn != "" {
searchBase = req.OuDn
}
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
baseFilter = strings.ReplaceAll(sf, "%s", "*")
} else {
baseFilter = sf
}
}
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
200, 0, false, baseFilter,
[]string{"uid", "cn", "mail", "dn", "telephoneNumber", "title", "department"},
nil,
))
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
type LdapUser struct {
DN string `json:"dn"`
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
Phone string `json:"phone"`
Title string `json:"title"`
Department string `json:"department"`
}
var users []LdapUser
for _, e := range sr.Entries {
users = append(users, LdapUser{
DN: e.DN,
UID: e.GetAttributeValue("uid"),
CN: e.GetAttributeValue("cn"),
Mail: e.GetAttributeValue("mail"),
Phone: e.GetAttributeValue("telephoneNumber"),
Title: e.GetAttributeValue("title"),
Department: e.GetAttributeValue("department"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: users})
}
// BatchImportLDAP 批量导入LDAP用户
// POST /api/v1/admin/ldap-config/import
func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
var req struct {
Users []struct {
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
} `json:"users"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
imported := 0
skipped := 0
for _, lu := range req.Users {
uid := lu.UID
if uid == "" {
uid = lu.Username
}
cn := lu.CN
if cn == "" {
cn = lu.Nickname
}
mail := lu.Mail
if mail == "" {
mail = lu.Email
}
if uid == "" {
skipped++
continue
}
// Check if already exists
var existing models.User
if err := h.db.Where("username = ?", uid).First(&existing).Error; err == nil {
// Already exists, update info
updates := map[string]interface{}{
"auth_source": "ldap",
}
if cn != "" {
updates["real_name"] = cn
}
if mail != "" {
updates["email"] = mail
}
h.db.Model(&existing).Updates(updates)
imported++
continue
}
// Create new LDAP user
user := models.User{
Username: uid,
Password: "-", // LDAP users don't have local password
RealName: cn,
Email: mail,
Status: 1,
AuthSource: "ldap",
}
if err := h.db.Create(&user).Error; err != nil {
skipped++
continue
}
imported++
}
// bcrypt import to avoid unused import error
_ = bcrypt.MinCost
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: fmt.Sprintf("导入完成: %d 个用户成功, %d 个跳过", imported, skipped),
Data: gin.H{"imported": imported, "skipped": skipped},
})
}
// CreateLDAPUser 在LDAP中创建用户
// POST /api/v1/admin/ldap-config/create-user
func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
OUDN string `json:"ou_dn"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
if req.Username == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "用户名和密码不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
if req.OUDN == "" {
req.OUDN = cfg.BaseDN
}
userDN := fmt.Sprintf("uid=%s,%s", ldap.EscapeFilter(req.Username), req.OUDN)
// Parse user mapping
mapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"}
if cfg.UserMapping != nil {
_ = json.Unmarshal([]byte(*cfg.UserMapping), &mapping)
}
uidAttr := mapping["username"]
if uidAttr == "" {
uidAttr = "uid"
}
mailAttr := mapping["email"]
if mailAttr == "" {
mailAttr = "mail"
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
addReq := ldap.NewAddRequest(userDN, nil)
addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
addReq.Attribute(uidAttr, []string{req.Username})
addReq.Attribute("cn", []string{req.DisplayName})
addReq.Attribute("sn", []string{req.DisplayName})
if req.Email != "" {
addReq.Attribute(mailAttr, []string{req.Email})
}
addReq.Attribute("userPassword", []string{req.Password})
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建用户失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}})
}
// DeleteLDAPUser 删除LDAP条目(用户或OU)
// POST /api/v1/admin/ldap-config/delete-user
func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) {
var req struct {
DN string `json:"dn"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
delReq := ldap.NewDelRequest(req.DN, nil)
if err := conn.Del(delReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
// UpdateLDAPUser 修改 LDAP 用户属性
// POST /api/v1/admin/ldap-config/update-user
func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
var req struct {
DN string `json:"dn"`
CN string `json:"cn"`
Mail string `json:"mail"`
Phone string `json:"phone"`
Title string `json:"title"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
return
}
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"})
return
}
cfg := configs[0]
port := 389
if cfg.LDAPPort != nil {
port = *cfg.LDAPPort
}
conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
return
}
// 先查当前值,做 diff
sr, err := conn.Search(ldap.NewSearchRequest(
req.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
1, 0, false, "(objectClass=*)",
[]string{"cn", "mail", "telephoneNumber", "title"},
nil,
))
if err != nil || len(sr.Entries) == 0 {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "用户不存在"})
return
}
entry := sr.Entries[0]
modReq := ldap.NewModifyRequest(req.DN, nil)
// cn
oldCN := entry.GetAttributeValue("cn")
if req.CN != "" && req.CN != oldCN {
modReq.Replace("cn", []string{req.CN})
modReq.Replace("sn", []string{req.CN}) // sn 同步更新
}
// mail
oldMail := entry.GetAttributeValue("mail")
if req.Mail != oldMail {
if req.Mail == "" {
oldVals := entry.GetAttributeValues("mail")
if len(oldVals) > 0 {
modReq.Delete("mail", oldVals)
}
} else {
modReq.Replace("mail", []string{req.Mail})
}
}
// telephoneNumber
oldPhone := entry.GetAttributeValue("telephoneNumber")
if req.Phone != oldPhone {
if req.Phone == "" {
oldVals := entry.GetAttributeValues("telephoneNumber")
if len(oldVals) > 0 {
modReq.Delete("telephoneNumber", oldVals)
}
} else {
modReq.Replace("telephoneNumber", []string{req.Phone})
}
}
// title
oldTitle := entry.GetAttributeValue("title")
if req.Title != oldTitle {
if req.Title == "" {
oldVals := entry.GetAttributeValues("title")
if len(oldVals) > 0 {
modReq.Delete("title", oldVals)
}
} else {
modReq.Replace("title", []string{req.Title})
}
}
if len(modReq.Changes) > 0 {
if err := conn.Modify(modReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "修改失败: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "修改成功"})
}
-641
View File
@@ -3,8 +3,6 @@ package handlers
import (
"net/http"
"strconv"
"strings"
"time"
"ezops/internal/models"
@@ -370,642 +368,3 @@ func (h *ServerHandler) GetServerStats(c *gin.Context) {
Data: stats,
})
}
// CloudAccountHandler 云账号处理器
type CloudAccountHandler struct {
db *gorm.DB
}
// NewCloudAccountHandler 创建云账号处理器
func NewCloudAccountHandler(db *gorm.DB) *CloudAccountHandler {
return &CloudAccountHandler{db: db}
}
// ListCloudAccounts 获取云账号列表
// @Summary 获取云账号列表
// @Description 获取所有云账号列表
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /cloud-accounts [get]
func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) {
var accounts []models.CloudAccount
h.db.Find(&accounts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: accounts,
Total: int64(len(accounts)),
},
})
}
// createCloudAccountRequest 创建云账号请求(独立 request struct,避免 CloudAccount.AccessKey/SecretKey 的 json:"-" 导致无法反序列化)
type createCloudAccountRequest struct {
Name string `json:"name"`
Provider string `json:"provider"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
MonthlyCost float64 `json:"monthly_cost"`
Status string `json:"status"`
}
// CreateCloudAccount 创建云账号
// @Summary 创建云账号
// @Description 创建新云账号
// @Tags 云账号
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body createCloudAccountRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.CloudAccount}
// @Router /cloud-accounts [post]
func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
var req createCloudAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Provider) == "" {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "账号名称和云平台不能为空",
})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
account := models.CloudAccount{
Name: strings.TrimSpace(req.Name),
Provider: strings.TrimSpace(req.Provider),
AccessKey: req.AccessKey,
SecretKey: req.SecretKey,
MonthlyCost: req.MonthlyCost,
Status: status,
}
if err := h.db.Create(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: account,
})
}
// UpdateCloudAccount 更新云账号
// @Summary 更新云账号
// @Description 更新指定云账号信息
// @Tags 云账号
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "云账号ID"
// @Param request body createCloudAccountRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.CloudAccount}
// @Router /cloud-accounts/{id} [put]
func (h *CloudAccountHandler) UpdateCloudAccount(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req createCloudAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var account models.CloudAccount
if err := h.db.First(&account, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "云账号不存在",
})
return
}
// 只在提供非空值时更新凭证字段
if strings.TrimSpace(req.Name) != "" {
account.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Provider) != "" {
account.Provider = strings.TrimSpace(req.Provider)
}
if strings.TrimSpace(req.AccessKey) != "" {
account.AccessKey = req.AccessKey
}
if strings.TrimSpace(req.SecretKey) != "" {
account.SecretKey = req.SecretKey
}
if strings.TrimSpace(req.Status) != "" {
account.Status = strings.TrimSpace(req.Status)
}
account.MonthlyCost = req.MonthlyCost
if err := h.db.Save(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: account,
})
}
// DeleteCloudAccount 删除云账号
// @Summary 删除云账号
// @Description 删除指定云账号
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Param id path int true "云账号ID"
// @Success 200 {object} models.Response
// @Router /cloud-accounts/{id} [delete]
func (h *CloudAccountHandler) DeleteCloudAccount(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.CloudAccount{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// DomainHandler 域名处理器
type DomainHandler struct {
db *gorm.DB
}
// NewDomainHandler 创建域名处理器
func NewDomainHandler(db *gorm.DB) *DomainHandler {
return &DomainHandler{db: db}
}
// ListDomains 获取域名列表
// @Summary 获取域名列表
// @Description 获取所有域名列表
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /domains [get]
func (h *DomainHandler) ListDomains(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.Domain{})
if query.Keyword != "" {
db = db.Where("name LIKE ?", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var domains []models.Domain
db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&domains)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: domains,
Total: total,
},
})
}
// CreateDomain 创建域名
// @Summary 创建域名
// @Description 创建新域名
// @Tags 域名
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.Domain true "创建请求"
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains [post]
func (h *DomainHandler) CreateDomain(c *gin.Context) {
var req updateDomainRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if strings.TrimSpace(req.Name) == "" {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "域名不能为空",
})
return
}
status := req.Status
if status == "" {
status = "active"
}
domain := models.Domain{
Name: strings.TrimSpace(req.Name),
Registrar: req.Registrar,
Status: status,
ExpireDate: parseDateStr(req.ExpireDate),
DNSProvider: req.DNSProvider,
RecordCount: req.RecordCount,
SSLEnabled: req.SSLEnabled,
SSLExpireDate: parseDateStr(req.SSLExpireDate),
}
if err := h.db.Create(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: domain,
})
}
// updateDomainRequest 域名创建/更新请求(日期字段用字符串接收,避免 RFC3339 解析失败)
type updateDomainRequest struct {
Name string `json:"name"`
Registrar string `json:"registrar"`
Status string `json:"status"`
ExpireDate string `json:"expire_date"`
DNSProvider string `json:"dns_provider"`
RecordCount int `json:"record_count"`
SSLEnabled bool `json:"ssl_enabled"`
SSLExpireDate string `json:"ssl_expire_date"`
}
// parseDateStr 将 YYYY-MM-DD 格式字符串解析为 *time.Time,空串返回 nil
func parseDateStr(s string) *time.Time {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return nil
}
return &t
}
// DeleteDomain 删除域名
// @Summary 删除域名
// @Description 删除指定域名
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param id path int true "域名ID"
// @Success 200 {object} models.Response
// @Router /domains/{id} [delete]
func (h *DomainHandler) DeleteDomain(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Domain{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// UpdateDomain 更新域名
// @Summary 更新域名
// @Description 更新指定域名信息
// @Tags 域名
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "域名ID"
// @Param request body updateDomainRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains/{id} [put]
func (h *DomainHandler) UpdateDomain(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req updateDomainRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var domain models.Domain
if err := h.db.First(&domain, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "域名不存在",
})
return
}
if strings.TrimSpace(req.Name) != "" {
domain.Name = strings.TrimSpace(req.Name)
}
domain.Registrar = req.Registrar
domain.Status = req.Status
if domain.Status == "" {
domain.Status = "active"
}
domain.ExpireDate = parseDateStr(req.ExpireDate)
domain.DNSProvider = req.DNSProvider
domain.RecordCount = req.RecordCount
domain.SSLEnabled = req.SSLEnabled
domain.SSLExpireDate = parseDateStr(req.SSLExpireDate)
if err := h.db.Save(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: domain,
})
}
// ==================== 云平台(Platform / Cloud)====================
// PlatformHandler 云平台处理器
type PlatformHandler struct {
db *gorm.DB
}
func NewPlatformHandler(db *gorm.DB) *PlatformHandler {
return &PlatformHandler{db: db}
}
// platformRequest 云平台创建/更新请求
type platformRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Type string `json:"type"`
Status string `json:"status"`
Website string `json:"website"`
Description string `json:"description"`
}
// ListPlatforms 获取云平台列表
func (h *PlatformHandler) ListPlatforms(c *gin.Context) {
var platforms []models.Cloud
h.db.Find(&platforms)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: platforms,
Total: int64(len(platforms)),
},
})
}
// CreatePlatform 创建云平台
func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "平台名称和编码不能为空"})
return
}
cloudType := strings.TrimSpace(req.Type)
if cloudType == "" {
cloudType = "public"
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
platform := models.Cloud{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Type: cloudType,
Status: status,
Website: req.Website,
Description: req.Description,
}
if err := h.db.Create(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: platform})
}
// UpdatePlatform 更新云平台
func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req platformRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var platform models.Cloud
if err := h.db.First(&platform, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云平台不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
platform.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
platform.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Type) != "" {
platform.Type = strings.TrimSpace(req.Type)
}
if strings.TrimSpace(req.Status) != "" {
platform.Status = strings.TrimSpace(req.Status)
}
platform.Website = req.Website
platform.Description = req.Description
if err := h.db.Save(&platform).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: platform})
}
// DeletePlatform 删除云平台
func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Cloud{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
// ==================== 区域管理(Region)====================
// RegionHandler 区域处理器
type RegionHandler struct {
db *gorm.DB
}
func NewRegionHandler(db *gorm.DB) *RegionHandler {
return &RegionHandler{db: db}
}
// regionRequest 区域创建/更新请求
type regionRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Provider string `json:"provider"`
Location string `json:"location"`
Status string `json:"status"`
Endpoint string `json:"endpoint"`
}
// ListRegions 获取区域列表
func (h *RegionHandler) ListRegions(c *gin.Context) {
var regions []models.Region
h.db.Find(&regions)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: regions,
Total: int64(len(regions)),
},
})
}
// CreateRegion 创建区域
func (h *RegionHandler) CreateRegion(c *gin.Context) {
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "区域名称和编码不能为空"})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
region := models.Region{
Name: strings.TrimSpace(req.Name),
Code: strings.TrimSpace(req.Code),
Provider: strings.TrimSpace(req.Provider),
Location: req.Location,
Status: status,
Endpoint: req.Endpoint,
}
if err := h.db.Create(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: region})
}
// UpdateRegion 更新区域
func (h *RegionHandler) UpdateRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req regionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
var region models.Region
if err := h.db.First(&region, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "区域不存在"})
return
}
if strings.TrimSpace(req.Name) != "" {
region.Name = strings.TrimSpace(req.Name)
}
if strings.TrimSpace(req.Code) != "" {
region.Code = strings.TrimSpace(req.Code)
}
if strings.TrimSpace(req.Provider) != "" {
region.Provider = strings.TrimSpace(req.Provider)
}
region.Location = req.Location
if strings.TrimSpace(req.Status) != "" {
region.Status = strings.TrimSpace(req.Status)
}
region.Endpoint = req.Endpoint
if err := h.db.Save(&region).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: region})
}
// DeleteRegion 删除区域
func (h *RegionHandler) DeleteRegion(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Region{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
-344
View File
@@ -1,10 +1,7 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"ezops/internal/api/middleware"
"ezops/internal/config"
@@ -361,344 +358,3 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
Message: "密码修改成功",
})
}
// ListUsers 获取用户列表
// @Summary 获取用户列表
// @Description 获取所有用户列表(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
var query models.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
db := h.db.Model(&models.User{})
if query.Keyword != "" {
db = db.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var users []models.User
db.Preload("Roles").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&users)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: users,
Total: total,
},
})
}
// GetUser 获取用户详情
// @Summary 获取用户详情
// @Description 获取指定用户的详细信息
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var user models.User
if err := h.db.Preload("Roles").First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: user,
})
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建新用户(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateUserRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 检查用户名是否已存在
var existingUser models.User
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "用户名已存在",
})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "密码加密失败",
})
return
}
user := models.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
Status: 1,
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建用户失败",
})
return
}
// 关联角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Append(roles)
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: user,
})
}
// UpdateUser 更新用户
// @Summary 更新用户
// @Description 更新指定用户信息(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Param request body models.UpdateUserRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
updates := map[string]interface{}{}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := h.db.Model(&user).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
// 更新角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Replace(roles)
}
h.db.Preload("Roles").First(&user, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: user,
})
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Description 删除指定用户(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response
// @Router /users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// BatchImportUsers 批量导入用户
// POST /api/v1/admin/users/batch-import
func (h *UserHandler) BatchImportUsers(c *gin.Context) {
var req struct {
Users []struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
Role string `json:"role"`
} `json:"users"`
SkipExisting bool `json:"skip_existing"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
imported := 0
skipped := 0
var errors []string
for i, u := range req.Users {
if u.Username == "" {
errors = append(errors, fmt.Sprintf("第%d行: 用户名为空", i+1))
continue
}
// Check if exists
var existing models.User
if err := h.db.Where("username = ?", u.Username).First(&existing).Error; err == nil {
if req.SkipExisting {
skipped++
continue
}
errors = append(errors, fmt.Sprintf("第%d行: 用户 %s 已存在", i+1, u.Username))
skipped++
continue
}
// Default password
pwd := u.Password
if pwd == "" {
pwd = "admin123"
}
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 密码加密失败", i+1))
continue
}
user := models.User{
Username: u.Username,
Password: string(hashed),
Email: u.Email,
Phone: u.Phone,
RealName: u.Nickname,
Status: 1,
AuthSource: "local",
}
if err := h.db.Create(&user).Error; err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 创建用户 %s 失败", i+1, u.Username))
continue
}
imported++
}
msg := fmt.Sprintf("导入完成: %d 成功, %d 跳过", imported, skipped)
if len(errors) > 0 {
msg += fmt.Sprintf(", %d 个错误", len(errors))
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: gin.H{
"imported": imported,
"skipped": skipped,
"errors": errors,
},
})
}
// ImportTemplate 下载用户导入模板
// GET /api/v1/admin/users/import-template
func (h *UserHandler) ImportTemplate(c *gin.Context) {
// UTF-8 BOM for Excel compatibility
bom := "\xEF\xBB\xBF"
csv := bom + "用户名,昵称,邮箱,手机号,角色,密码\n"
csv += "zhangsan,张三,zhangsan@example.com,13800138000,developer,123456\n"
csv += "lisi,李四,lisi@example.com,13800138001,developer,123456\n"
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=users_import_template.csv")
c.String(http.StatusOK, csv)
}
// ensure strings import is used
var _ = strings.TrimSpace
+350
View File
@@ -0,0 +1,350 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
// ListUsers 获取用户列表
// @Summary 获取用户列表
// @Description 获取所有用户列表(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
var query models.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
db := h.db.Model(&models.User{})
if query.Keyword != "" {
db = db.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var users []models.User
db.Preload("Roles").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&users)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: users,
Total: total,
},
})
}
// GetUser 获取用户详情
// @Summary 获取用户详情
// @Description 获取指定用户的详细信息
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var user models.User
if err := h.db.Preload("Roles").First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: user,
})
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建新用户(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateUserRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 检查用户名是否已存在
var existingUser models.User
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "用户名已存在",
})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "密码加密失败",
})
return
}
user := models.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
Status: 1,
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建用户失败",
})
return
}
// 关联角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Append(roles)
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: user,
})
}
// UpdateUser 更新用户
// @Summary 更新用户
// @Description 更新指定用户信息(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Param request body models.UpdateUserRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
updates := map[string]interface{}{}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := h.db.Model(&user).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
// 更新角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Replace(roles)
}
h.db.Preload("Roles").First(&user, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: user,
})
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Description 删除指定用户(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response
// @Router /users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// BatchImportUsers 批量导入用户
// POST /api/v1/admin/users/batch-import
func (h *UserHandler) BatchImportUsers(c *gin.Context) {
var req struct {
Users []struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Phone string `json:"phone"`
Password string `json:"password"`
Role string `json:"role"`
} `json:"users"`
SkipExisting bool `json:"skip_existing"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
imported := 0
skipped := 0
var errors []string
for i, u := range req.Users {
if u.Username == "" {
errors = append(errors, fmt.Sprintf("第%d行: 用户名为空", i+1))
continue
}
// Check if exists
var existing models.User
if err := h.db.Where("username = ?", u.Username).First(&existing).Error; err == nil {
if req.SkipExisting {
skipped++
continue
}
errors = append(errors, fmt.Sprintf("第%d行: 用户 %s 已存在", i+1, u.Username))
skipped++
continue
}
// Default password
pwd := u.Password
if pwd == "" {
pwd = "admin123"
}
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 密码加密失败", i+1))
continue
}
user := models.User{
Username: u.Username,
Password: string(hashed),
Email: u.Email,
Phone: u.Phone,
RealName: u.Nickname,
Status: 1,
AuthSource: "local",
}
if err := h.db.Create(&user).Error; err != nil {
errors = append(errors, fmt.Sprintf("第%d行: 创建用户 %s 失败", i+1, u.Username))
continue
}
imported++
}
msg := fmt.Sprintf("导入完成: %d 成功, %d 跳过", imported, skipped)
if len(errors) > 0 {
msg += fmt.Sprintf(", %d 个错误", len(errors))
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: msg,
Data: gin.H{
"imported": imported,
"skipped": skipped,
"errors": errors,
},
})
}
// ImportTemplate 下载用户导入模板
// GET /api/v1/admin/users/import-template
func (h *UserHandler) ImportTemplate(c *gin.Context) {
// UTF-8 BOM for Excel compatibility
bom := "\xEF\xBB\xBF"
csv := bom + "用户名,昵称,邮箱,手机号,角色,密码\n"
csv += "zhangsan,张三,zhangsan@example.com,13800138000,developer,123456\n"
csv += "lisi,李四,lisi@example.com,13800138001,developer,123456\n"
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=users_import_template.csv")
c.String(http.StatusOK, csv)
}