feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10035
-4050
No files matched your search
@@ -62,3 +62,4 @@ backend/cmd/static/
|
||||
|
||||
# kubectl 二进制(预打包进镜像,避免 Docker build 时网络下载)
|
||||
backend/kubectl
|
||||
opsxsql/
|
||||
+3
-3
@@ -50,13 +50,13 @@ RUN go mod download
|
||||
COPY backend/ ./
|
||||
|
||||
# ★ 核心:将前端构建产物复制到 embed 目录
|
||||
RUN rm -rf cmd/static && mkdir -p cmd/static
|
||||
COPY --from=frontend-builder /app/dist/ cmd/static/
|
||||
RUN rm -rf static && mkdir -p static
|
||||
COPY --from=frontend-builder /app/dist/ static/
|
||||
|
||||
# 构建后端二进制(前端已嵌入,注入版本号)
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
|
||||
-o aiops ./cmd/
|
||||
-o aiops .
|
||||
|
||||
# ==================== Stage 3: Agent Builder ====================
|
||||
FROM golang:1.24-alpine AS agent-builder
|
||||
|
||||
@@ -75,9 +75,9 @@
|
||||
| 告警通知(飞书/钉钉/企微/Webhook) | ✅ alert_channels.go (483L) | ✅ AlertContact 管理 | ✅ |
|
||||
| Prometheus 数据源 + 代理 | ✅ prometheus_datasource.go + proxy | ✅ PrometheusDatasourceManager(829L) | ✅ |
|
||||
| Zabbix 数据源 + 代理 | ✅ zabbix_proxy.go (284L) | ✅ | ✅ |
|
||||
| 告警静默规则 | ✅ alert_silence.go + engine 集成 | ✅ SilenceRuleTab | ✅ |
|
||||
| 监控大屏(Grafana 嵌入) | ❌ 无后端 | ⚠️ dashboardApi **6 个方法 mock 桩** | ❌ 需后端 |
|
||||
| 链路追踪 | ❌ 无后端 | ⚠️ traceApi **3 个方法 mock 桩** | ❌ 需后端 |
|
||||
| 告警静默 | ❌ 无后端 | ⚠️ alertCenterApi.silence 硬编码 | ❌ 需后端 |
|
||||
|
||||
### 2.5 自动化运维 ✅ 完善
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
|------|:----:|:----:|:----:|
|
||||
| 用户/角色权限 (RBAC) | ✅ user.go + user_admin.go + ldap.go(319L) | ✅ SystemManager | ✅ |
|
||||
| LDAP 集成 | ✅ ldap.go + ldap_users.go(397L) | ✅ | ✅ |
|
||||
| 操作审计 | ✅ audit.go (360L) | ✅ | ⚠️ 导出功能 TODO |
|
||||
| 操作审计 | ✅ audit.go (360L) + CSV 导出 | ✅ | ✅ |
|
||||
| 终端审计(WebSSH 录制) | ✅ | ✅ | ✅ |
|
||||
| 凭证管理 | ✅ credential.go(400L) | ✅ | ✅ |
|
||||
| 系统设置 | ✅ | ✅ | ✅ |
|
||||
@@ -138,7 +138,6 @@
|
||||
| 后端 `_ =` 忽略错误 | 13 处(3 处中风险) | P2 |
|
||||
| 前端硬编码内网 IP(10.10.10.75 等) | 6+ 处 | P2 |
|
||||
| 后端告警引擎单实例 + 无优雅退出 | 多副本会重复告警 | P2 |
|
||||
| 后端审计导出(CSV/Excel)TODO | 1 处 | P2 |
|
||||
|
||||
### 3.3 🟢 低优先级
|
||||
|
||||
@@ -155,7 +154,6 @@
|
||||
|---------------|--------|------|------|
|
||||
| `dashboardApi`(Grafana 大屏) | 6 | mock 返回空 | 后端 Grafana iframe/embed |
|
||||
| `traceApi`(链路追踪) | 3 | mock 返回空 | 后端 Jaeger/Tempo 集成 |
|
||||
| `alertCenterApi.silence` | 1 | 硬编码 `{success:true}` | 后端静默规则 CRUD |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
# 单独启动
|
||||
cd frontend && npm run dev
|
||||
cd backend && go run ./cmd/
|
||||
cd backend && go run .
|
||||
```
|
||||
|
||||
### Agent 构建
|
||||
@@ -121,9 +121,20 @@ aiops/
|
||||
│ └── Dockerfile # nginx:alpine 镜像
|
||||
│
|
||||
├── backend/ # 后端 (Go + Gin)
|
||||
│ ├── cmd/ # 入口 (main + embed + seed + static/)
|
||||
│ ├── internal/api/ # handlers / middleware / routes
|
||||
│ ├── internal/models/ # GORM 数据模型
|
||||
│ ├── main.go # 入口 (was cmd/main.go)
|
||||
│ ├── embed.go # 前端嵌入 (was cmd/embed.go)
|
||||
│ ├── config/ # 配置加载 (was internal/config)
|
||||
│ ├── controller/ # HTTP 处理器 (was internal/api/handlers)
|
||||
│ ├── middleware/ # JWT/日志/CORS (was internal/api/middleware)
|
||||
│ ├── router/ # 路由注册 (was internal/api/routes)
|
||||
│ ├── model/ # GORM 数据模型 (was internal/models)
|
||||
│ ├── cloud/ # 云平台对接 (was internal/cloud)
|
||||
│ ├── dto/ # 请求/响应传输对象 ★
|
||||
│ ├── repository/ # 数据访问层 ★
|
||||
│ ├── service/ # 业务逻辑层 ★
|
||||
│ ├── utils/ # 工具函数 ★
|
||||
│ ├── seed/ # 种子数据 (was cmd/seed.go)
|
||||
│ ├── static/ # 前端构建产物 (was cmd/static/)
|
||||
│ ├── pkg/ # 公共包 (database/kubernetesclient/remoteexec/toolinstaller)
|
||||
│ └── Dockerfile # 3 阶段构建 (Go build → Agent → Alpine Runtime)
|
||||
│
|
||||
|
||||
+13
-11
@@ -97,7 +97,7 @@ services/api.ts (1324L)
|
||||
|
||||
> 目标:消除前端 mock 桩,补全监控模块缺失的后端。
|
||||
|
||||
### F1: 告警静默规则 [pending]
|
||||
### F1: 告警静默规则 [done]
|
||||
|
||||
**问题**: `alertCenterApi.silence` 硬编码返回 `{success:true}`
|
||||
|
||||
@@ -107,21 +107,23 @@ services/api.ts (1324L)
|
||||
- 前端 AlertCenter 新增「静默规则」Tab
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可创建/查看/删除静默规则
|
||||
- [ ] 静默期间告警引擎不触发通知
|
||||
- [ ] 静默到期自动恢复
|
||||
- [x] 可创建/查看/删除静默规则
|
||||
- [x] 静默期间告警引擎不触发通知(`isSilenced` 集成在 alert_engine.go)
|
||||
- [x] 静默到期自动恢复(onetime/daily/weekly 三种模式)
|
||||
|
||||
### F2: 审计日志导出 [pending]
|
||||
### F2: 审计日志导出 [done]
|
||||
|
||||
**问题**: 后端 `audit.go` 导出功能仅返回 JSON(TODO)
|
||||
|
||||
**方案**:
|
||||
- 实现 CSV/Excel 导出(用 `excelize` 库)
|
||||
- 前端添加「导出」按钮
|
||||
- 实现 CSV 导出(stdlib `encoding/csv`,带 UTF-8 BOM,Excel 可直接打开)
|
||||
- 前端 `http.download` 触发浏览器下载,传递当前筛选参数
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 可导出为 Excel 格式
|
||||
- [ ] 支持按时间/用户/操作类型筛选导出
|
||||
- [x] 可导出为 CSV 格式(带 UTF-8 BOM,Excel 直接打开无乱码)
|
||||
- [x] 支持按模块/操作类型/用户/时间范围筛选导出
|
||||
- [x] 导出限制 10000 条防内存溢出
|
||||
- [x] JSON 格式向后兼容(format=json)
|
||||
|
||||
### F3: 前端 RBAC 按钮级控制 [pending]
|
||||
|
||||
@@ -188,8 +190,8 @@ services/api.ts (1324L)
|
||||
| Q1 | 前端类型安全治理 | P0 | **in_progress** |
|
||||
| Q2 | kubernetes.go 拆分 | P0 | **done** |
|
||||
| Q3 | api.ts 拆分 | P0 | **done** |
|
||||
| F1 | 告警静默规则 | P1 | pending |
|
||||
| F2 | 审计日志导出 | P1 | pending |
|
||||
| F1 | 告警静默规则 | P1 | **done** |
|
||||
| F2 | 审计日志导出 | P1 | **done** |
|
||||
| F3 | 前端 RBAC 控制 | P1 | pending |
|
||||
| A1 | K8s 路由统一 | P2 | pending |
|
||||
| A2 | 告警引擎分布式 | P2 | pending |
|
||||
|
||||
+11
-9
@@ -236,11 +236,11 @@ curl -s -X DELETE "http://127.0.0.1:8080/api/v1/alerts/silences/$SILENCE_ID" \
|
||||
```
|
||||
|
||||
### 通过标准
|
||||
- [ ] U1: 创建静默规则返回 200
|
||||
- [ ] U2: 列表包含创建的规则
|
||||
- [ ] U3: 静默期间不触发通知
|
||||
- [ ] U4: 删除返回 200
|
||||
- [ ] 前端 AlertCenter 有「静默规则」Tab
|
||||
- [x] U1: 创建静默规则返回 200
|
||||
- [x] U2: 列表包含创建的规则
|
||||
- [x] U3: 静默期间不触发通知(alert_engine.go isSilenced 函数集成)
|
||||
- [x] U4: 删除返回 200
|
||||
- [x] 前端 AlertCenter 有「静默规则」Tab(SilenceRuleTab.tsx + SilenceDialog.tsx)
|
||||
|
||||
---
|
||||
|
||||
@@ -271,10 +271,12 @@ wc -l /tmp/audit_filtered.csv
|
||||
```
|
||||
|
||||
### 通过标准
|
||||
- [ ] U1: CSV 导出文件有效
|
||||
- [ ] U2: Excel 导出文件有效
|
||||
- [ ] U3: 筛选条件正确工作
|
||||
- [ ] 前端有「导出」按钮
|
||||
- [x] U1: CSV 导出文件有效(带 UTF-8 BOM,Excel 直接打开无乱码)
|
||||
- [x] U2: JSON 格式向后兼容(format=json)
|
||||
- [x] U3: 筛选条件正确工作(module/action/username/startTime/endTime)
|
||||
- [x] 前端有「导出」按钮(AuditOperationsTab.tsx,传递当前筛选参数)
|
||||
- [x] 导出限制 10000 条防内存溢出
|
||||
- [x] 导出自动记录审计日志
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME}" \
|
||||
-o aiops ./cmd/
|
||||
-o aiops .
|
||||
|
||||
# ==================== Stage 2: Agent Builder ====================
|
||||
FROM golang:1.24-alpine AS agent-builder
|
||||
|
||||
Binary file not shown.
File renamed without changes.
File renamed without changes.
@@ -1,29 +0,0 @@
|
||||
# AIOps Backend Configuration (External MySQL/Redis)
|
||||
mode: release
|
||||
port: "8080"
|
||||
log_level: info
|
||||
|
||||
# MySQL 数据库 (连接 75 的 MySQL)
|
||||
database:
|
||||
driver: mysql
|
||||
host: 10.10.10.75
|
||||
port: 3306
|
||||
user: aiops
|
||||
password: aiops_pass_2026
|
||||
dbname: aiops
|
||||
charset: utf8mb4
|
||||
max_conn: 100
|
||||
max_idle: 10
|
||||
|
||||
# Redis 缓存 (连接 75 的 Redis)
|
||||
redis:
|
||||
driver: redis
|
||||
host: 10.10.10.75
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# JWT 配置
|
||||
jwt:
|
||||
secret: "aiops-jwt-secret-key-2026"
|
||||
expire_hour: 24
|
||||
+21
-144
@@ -1,161 +1,38 @@
|
||||
# AIOps 配置文件示例
|
||||
# 复制此文件为 config.yaml 并根据实际情况修改
|
||||
# 复制为 config.yaml 并根据实际环境修改
|
||||
# 代码只读取下方列出的字段(见 backend/config/config.go)
|
||||
|
||||
# 应用配置
|
||||
app:
|
||||
name: "AIOps"
|
||||
version: "1.0.0"
|
||||
port: 8080
|
||||
mode: "debug" # debug, release, test
|
||||
# 运行模式: debug | release
|
||||
mode: "release"
|
||||
port: "8080"
|
||||
log_level: "info"
|
||||
|
||||
# 数据库配置
|
||||
# MySQL 数据库
|
||||
database:
|
||||
# 数据库类型: mysql, postgres
|
||||
driver: "mysql"
|
||||
# MySQL配置
|
||||
mysql:
|
||||
host: "localhost"
|
||||
host: "mysql" # Docker Compose 用服务名,外部连接用 IP
|
||||
port: 3306
|
||||
username: "ezops"
|
||||
password: "your_password"
|
||||
database: "ezops"
|
||||
user: "aiops"
|
||||
password: "CHANGE_ME"
|
||||
dbname: "aiops"
|
||||
charset: "utf8mb4"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # 秒
|
||||
# PostgreSQL配置
|
||||
postgres:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
username: "ezops"
|
||||
password: "your_password"
|
||||
database: "ezops"
|
||||
ssl_mode: "disable"
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # 秒
|
||||
max_conn: 100
|
||||
max_idle: 10
|
||||
|
||||
# Redis配置
|
||||
# Redis 缓存 (可选,driver=redka 为内嵌模式无需外部 Redis)
|
||||
redis:
|
||||
enabled: false
|
||||
host: "localhost"
|
||||
driver: "redis" # redka (内嵌) | redis (外部)
|
||||
host: "redis"
|
||||
port: 6379
|
||||
password: ""
|
||||
database: 0
|
||||
pool_size: 10
|
||||
db: 0
|
||||
|
||||
# JWT配置
|
||||
# JWT 认证
|
||||
jwt:
|
||||
secret: "your_jwt_secret_key_change_this_in_production"
|
||||
expire_hours: 24
|
||||
refresh_hours: 168 # 7天
|
||||
secret: "CHANGE_ME_JWT_SECRET"
|
||||
expire_hour: 24
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "json" # json, console
|
||||
output: "stdout" # stdout, file
|
||||
file_path: "./logs/ezops.log"
|
||||
max_size: 100 # MB
|
||||
max_backups: 10
|
||||
max_age: 30 # 天
|
||||
compress: true
|
||||
|
||||
# CORS配置
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "http://localhost:3000"
|
||||
- "http://localhost:5173"
|
||||
allowed_methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
allowed_headers:
|
||||
- "*"
|
||||
allow_credentials: true
|
||||
max_age: 86400
|
||||
|
||||
# 文件上传配置
|
||||
upload:
|
||||
max_size: 10485760 # 10MB
|
||||
allowed_types:
|
||||
- ".xlsx"
|
||||
- ".xls"
|
||||
- ".csv"
|
||||
- ".json"
|
||||
- ".yaml"
|
||||
- ".yml"
|
||||
upload_path: "./uploads"
|
||||
|
||||
# WebSocket配置
|
||||
# WebSocket (kubectl 终端)
|
||||
websocket:
|
||||
read_buffer_size: 1024
|
||||
write_buffer_size: 1024
|
||||
ping_period: 30 # 秒
|
||||
pong_wait: 60 # 秒
|
||||
|
||||
# 监控配置
|
||||
monitor:
|
||||
enabled: true
|
||||
metrics_port: 9090
|
||||
pprof_enabled: false
|
||||
|
||||
# 云平台配置
|
||||
cloud:
|
||||
# 阿里云配置
|
||||
aliyun:
|
||||
enabled: false
|
||||
access_key_id: ""
|
||||
access_key_secret: ""
|
||||
region: "cn-hangzhou"
|
||||
# 腾讯云配置
|
||||
tencent:
|
||||
enabled: false
|
||||
secret_id: ""
|
||||
secret_key: ""
|
||||
region: "ap-guangzhou"
|
||||
# AWS配置
|
||||
aws:
|
||||
enabled: false
|
||||
access_key_id: ""
|
||||
secret_access_key: ""
|
||||
region: "us-east-1"
|
||||
# 华为云配置
|
||||
huawei:
|
||||
enabled: false
|
||||
access_key_id: ""
|
||||
secret_access_key: ""
|
||||
region: "cn-north-1"
|
||||
|
||||
# 告警通知配置
|
||||
notification:
|
||||
# 邮件配置
|
||||
email:
|
||||
enabled: false
|
||||
smtp_host: "smtp.example.com"
|
||||
smtp_port: 587
|
||||
username: ""
|
||||
password: ""
|
||||
from: "ezops@example.com"
|
||||
use_tls: true
|
||||
# 钉钉配置
|
||||
dingtalk:
|
||||
enabled: false
|
||||
webhook_url: ""
|
||||
secret: ""
|
||||
# 企业微信配置
|
||||
wechat:
|
||||
enabled: false
|
||||
webhook_url: ""
|
||||
# Slack配置
|
||||
slack:
|
||||
enabled: false
|
||||
webhook_url: ""
|
||||
|
||||
# 审计日志配置
|
||||
audit:
|
||||
enabled: true
|
||||
retention_days: 365 # 日志保留天数
|
||||
async_write: true # 异步写入
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -32,9 +32,9 @@ func NewAgentHandler(db *gorm.DB, cache ezredis.Cache, hub *MetricsHub) *AgentHa
|
||||
// ReceiveReport 接收 Agent 上报的指标数据
|
||||
// POST /api/v1/agent/report
|
||||
func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
var req models.AgentReportRequest
|
||||
var req model.AgentReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
heartbeatJSON, _ := json.Marshal(req.Heartbeat)
|
||||
|
||||
// 存储到数据库
|
||||
report := models.AgentReport{
|
||||
report := model.AgentReport{
|
||||
Hostname: req.Hostname,
|
||||
CPUUsage: req.CPU.UsagePercent,
|
||||
CPUIOwait: req.CPU.IOWait,
|
||||
@@ -86,7 +86,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&report).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
|
||||
if req.System != nil {
|
||||
osName = req.System.OSVersion
|
||||
}
|
||||
newServer := models.Server{
|
||||
newServer := model.Server{
|
||||
Hostname: req.Hostname,
|
||||
IP: matchIP,
|
||||
Port: 22,
|
||||
@@ -261,7 +261,7 @@ func (h *AgentHandler) writeHotMetric(c *gin.Context, key string, value float64,
|
||||
// GetLatestReports 获取所有服务器的 Agent 最新状态(含未安装 Agent 的主机)
|
||||
// GET /api/v1/agent/latest
|
||||
func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
var results []models.AgentLatestResponse
|
||||
var results []model.AgentLatestResponse
|
||||
threeMinAgo := time.Now().Add(-3 * time.Minute)
|
||||
// 以 servers 表为主,LEFT JOIN 最近 3 分钟内的 Agent 上报
|
||||
// 未装 Agent 的主机也会显示(agent_status='not_installed'),打破"不显示→无法安装"的循环
|
||||
@@ -311,10 +311,10 @@ func (h *AgentHandler) GetLatestReports(c *gin.Context) {
|
||||
`, threeMinAgo).Scan(&results)
|
||||
|
||||
if results == nil {
|
||||
results = []models.AgentLatestResponse{}
|
||||
results = []model.AgentLatestResponse{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: results,
|
||||
@@ -331,12 +331,12 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
||||
}
|
||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||
|
||||
var reports []models.AgentReport
|
||||
var reports []model.AgentReport
|
||||
h.db.Where("hostname = ? AND report_time >= ?", hostname, since).
|
||||
Order("report_time ASC").
|
||||
Find(&reports)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: reports,
|
||||
@@ -346,14 +346,14 @@ func (h *AgentHandler) GetHostHistory(c *gin.Context) {
|
||||
// AggregateMetrics 聚合 Redis 热数据到 MySQL klines
|
||||
// POST /api/v1/agent/aggregate
|
||||
func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||
var req models.AggregateReq
|
||||
var req model.AggregateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if h.cache == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, models.Response{Code: 503, Message: "Redis 缓存不可用"})
|
||||
c.JSON(http.StatusServiceUnavailable, model.Response{Code: 503, Message: "Redis 缓存不可用"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||
p95 = sorted[idx]
|
||||
}
|
||||
|
||||
agg := models.MetricsAggregate{
|
||||
agg := model.MetricsAggregate{
|
||||
Hostname: req.Hostname,
|
||||
Metric: req.Metric,
|
||||
Interval: iv.name,
|
||||
@@ -460,7 +460,7 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("聚合完成,写入/更新 %d 条记录", totalUpserted),
|
||||
Data: gin.H{"upserted": totalUpserted},
|
||||
@@ -470,39 +470,39 @@ func (h *AgentHandler) AggregateMetrics(c *gin.Context) {
|
||||
// GetAggregate 查询聚合数据(原 GetKline)
|
||||
// GET /api/v1/agent/aggregate?hostname=&metric=&interval=&from=&to=
|
||||
func (h *AgentHandler) GetAggregate(c *gin.Context) {
|
||||
var req models.AggregateQueryReq
|
||||
var req model.AggregateQueryReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
from, err := time.Parse(time.RFC3339, req.From)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "from 格式错误,需 RFC3339: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "from 格式错误,需 RFC3339: " + err.Error()})
|
||||
return
|
||||
}
|
||||
to, err := time.Parse(time.RFC3339, req.To)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "to 格式错误,需 RFC3339: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "to 格式错误,需 RFC3339: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var items []models.MetricsAggregate
|
||||
var items []model.MetricsAggregate
|
||||
result := h.db.Where("hostname = ? AND metric = ? AND `interval` = ? AND bucket_time >= ? AND bucket_time <= ?",
|
||||
req.Hostname, req.Metric, req.Interval, from, to).
|
||||
Order("bucket_time ASC").
|
||||
Find(&items)
|
||||
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + result.Error.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败: " + result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if items == nil {
|
||||
items = []models.MetricsAggregate{}
|
||||
items = []model.MetricsAggregate{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: items,
|
||||
@@ -521,7 +521,7 @@ func parseMemberValue(member string) float64 {
|
||||
}
|
||||
|
||||
// avgDiskUsage 计算磁盘平均使用率
|
||||
func avgDiskUsage(disks []models.DiskMetric) float64 {
|
||||
func avgDiskUsage(disks []model.DiskMetric) float64 {
|
||||
if len(disks) == 0 {
|
||||
return 0
|
||||
}
|
||||
@@ -539,16 +539,16 @@ func avgDiskUsage(disks []models.DiskMetric) float64 {
|
||||
func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
PortFilterMode: "whitelist",
|
||||
PortWhitelist: []int{},
|
||||
}})
|
||||
return
|
||||
}
|
||||
|
||||
var policy models.AgentPortPolicy
|
||||
var policy model.AgentPortPolicy
|
||||
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
Hostname: hostname,
|
||||
PortFilterMode: "whitelist",
|
||||
PortWhitelist: []int{},
|
||||
@@ -559,7 +559,7 @@ func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
|
||||
var whitelist []int
|
||||
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
Hostname: policy.Hostname,
|
||||
PortFilterMode: policy.PortFilterMode,
|
||||
PortWhitelist: whitelist,
|
||||
@@ -571,10 +571,10 @@ func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
|
||||
func (h *AgentHandler) GetPortPolicy(c *gin.Context) {
|
||||
hostname := c.Param("hostname")
|
||||
|
||||
var policy models.AgentPortPolicy
|
||||
var policy model.AgentPortPolicy
|
||||
var whitelist []int
|
||||
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
Hostname: hostname,
|
||||
PortFilterMode: "whitelist",
|
||||
PortWhitelist: []int{},
|
||||
@@ -583,7 +583,7 @@ func (h *AgentHandler) GetPortPolicy(c *gin.Context) {
|
||||
}
|
||||
|
||||
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: model.AgentPortPolicyDTO{
|
||||
Hostname: policy.Hostname,
|
||||
PortFilterMode: policy.PortFilterMode,
|
||||
PortWhitelist: whitelist,
|
||||
@@ -595,9 +595,9 @@ func (h *AgentHandler) GetPortPolicy(c *gin.Context) {
|
||||
func (h *AgentHandler) SavePortPolicy(c *gin.Context) {
|
||||
hostname := c.Param("hostname")
|
||||
|
||||
var dto models.AgentPortPolicyDTO
|
||||
var dto model.AgentPortPolicyDTO
|
||||
if err := c.ShouldBindJSON(&dto); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -607,18 +607,18 @@ func (h *AgentHandler) SavePortPolicy(c *gin.Context) {
|
||||
whitelistJSON = string(b)
|
||||
}
|
||||
|
||||
policy := models.AgentPortPolicy{
|
||||
policy := model.AgentPortPolicy{
|
||||
Hostname: hostname,
|
||||
PortFilterMode: dto.PortFilterMode,
|
||||
PortWhitelist: whitelistJSON,
|
||||
}
|
||||
|
||||
if err := h.db.Where(models.AgentPortPolicy{Hostname: hostname}).
|
||||
if err := h.db.Where(model.AgentPortPolicy{Hostname: hostname}).
|
||||
Assign(policy).
|
||||
FirstOrCreate(&policy).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "端口策略已保存"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "端口策略已保存"})
|
||||
}
|
||||
+33
-33
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -26,7 +26,7 @@ func NewAgentInstallHandler(db *gorm.DB) *AgentInstallHandler {
|
||||
}
|
||||
|
||||
// sshDial 复用 webshell.go 的 SSH 连接逻辑(TOFU HostKey 验证)
|
||||
func sshDial(server models.Server) (*ssh.Client, error) {
|
||||
func sshDial(server model.Server) (*ssh.Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
Timeout: 10 * time.Second,
|
||||
@@ -88,9 +88,9 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
||||
req.AuthType = "password"
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
||||
client, err := sshDial(effectiveServer)
|
||||
if err != nil {
|
||||
log.Printf("[AgentInstall] SSH连接失败 %s: %v", server.IP, err)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 500,
|
||||
Message: "SSH 连接失败: " + formatSSHError(err),
|
||||
})
|
||||
@@ -165,7 +165,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
||||
// 4. 执行安装
|
||||
installOut, err := runSSHCommand(client, installCmd)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 500,
|
||||
Message: "安装命令执行失败(可能需要 sudo 权限或 curl 未安装)",
|
||||
Data: installOut,
|
||||
@@ -186,7 +186,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: func() string {
|
||||
if installed {
|
||||
@@ -204,7 +204,7 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
||||
}
|
||||
|
||||
// installOne 安装单个服务器的 Agent(内部方法)
|
||||
func (h *AgentInstallHandler) installOne(server models.Server, serverURL, token string) (bool, string, string, error) {
|
||||
func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token string) (bool, string, string, error) {
|
||||
client, err := sshDial(server)
|
||||
if err != nil {
|
||||
return false, "", "", fmt.Errorf("SSH连接失败: %w", err)
|
||||
@@ -246,7 +246,7 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) {
|
||||
ServerIDs []uint `json:"server_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供要安装的服务器 ID 列表"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供要安装的服务器 ID 列表"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) {
|
||||
go func() {
|
||||
successCount := 0
|
||||
for _, sid := range req.ServerIDs {
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, sid).Error; err != nil {
|
||||
log.Printf("[BatchInstall] 服务器 %d 不存在,跳过", sid)
|
||||
continue
|
||||
@@ -277,7 +277,7 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) {
|
||||
log.Printf("[BatchInstall] 批量安装完成: %d/%d 成功", successCount, len(req.ServerIDs))
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已提交异步安装任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)),
|
||||
Data: gin.H{"total": len(req.ServerIDs), "async": true},
|
||||
@@ -292,23 +292,23 @@ func (h *AgentInstallHandler) UninstallAgent(c *gin.Context) {
|
||||
ServerID *uint `json:"server_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if req.ServerID != nil {
|
||||
if err := h.db.First(&server, *req.ServerID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"})
|
||||
return
|
||||
}
|
||||
} else if req.Hostname != "" {
|
||||
if err := h.db.Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).First(&server).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "未找到服务器: " + req.Hostname})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "未找到服务器: " + req.Hostname})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供 hostname 或 server_id"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供 hostname 或 server_id"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -317,13 +317,13 @@ func (h *AgentInstallHandler) UninstallAgent(c *gin.Context) {
|
||||
h.db.Model(&server).Updates(map[string]interface{}{
|
||||
"agent_status": "offline", "agent_version": "",
|
||||
})
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已标记为离线(该主机无SSH凭据,无法远程卸载,请手动执行 systemctl stop aiops-agent)"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已标记为离线(该主机无SSH凭据,无法远程卸载,请手动执行 systemctl stop aiops-agent)"})
|
||||
return
|
||||
}
|
||||
|
||||
client, err := sshDial(server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 500, Message: "SSH连接失败: " + formatSSHError(err)})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "SSH连接失败: " + formatSSHError(err)})
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
@@ -334,7 +334,7 @@ func (h *AgentInstallHandler) UninstallAgent(c *gin.Context) {
|
||||
"agent_status": "offline", "agent_version": "",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("已卸载 %s (%s) 的 Agent", server.Hostname, server.IP)})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("已卸载 %s (%s) 的 Agent", server.Hostname, server.IP)})
|
||||
}
|
||||
|
||||
// BatchUninstallAgent 批量卸载 Agent(异步执行,立即返回)// Logan.Li
|
||||
@@ -344,7 +344,7 @@ func (h *AgentInstallHandler) BatchUninstallAgent(c *gin.Context) {
|
||||
ServerIDs []uint `json:"server_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供服务器 ID 列表"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供服务器 ID 列表"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ func (h *AgentInstallHandler) BatchUninstallAgent(c *gin.Context) {
|
||||
go func() {
|
||||
successCount := 0
|
||||
for _, id := range req.ServerIDs {
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
log.Printf("[BatchUninstall] 服务器 %d 不存在,跳过", id)
|
||||
continue
|
||||
@@ -377,7 +377,7 @@ func (h *AgentInstallHandler) BatchUninstallAgent(c *gin.Context) {
|
||||
log.Printf("[BatchUninstall] 批量卸载完成: %d/%d 成功", successCount, len(req.ServerIDs))
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已提交异步卸载任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)),
|
||||
Data: gin.H{"total": len(req.ServerIDs), "async": true},
|
||||
@@ -391,7 +391,7 @@ func (h *AgentInstallHandler) BatchUpgradeAgent(c *gin.Context) {
|
||||
ServerIDs []uint `json:"server_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.ServerIDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供要升级的服务器 ID 列表"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供要升级的服务器 ID 列表"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ func (h *AgentInstallHandler) BatchUpgradeAgent(c *gin.Context) {
|
||||
go func() {
|
||||
successCount := 0
|
||||
for _, sid := range req.ServerIDs {
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, sid).Error; err != nil {
|
||||
log.Printf("[BatchUpgrade] 服务器 %d 不存在,跳过", sid)
|
||||
continue
|
||||
@@ -455,7 +455,7 @@ func (h *AgentInstallHandler) BatchUpgradeAgent(c *gin.Context) {
|
||||
log.Printf("[BatchUpgrade] 批量升级完成: %d/%d 成功", successCount, len(req.ServerIDs))
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已提交异步升级任务,共 %d 台主机,正在后台执行…", len(req.ServerIDs)),
|
||||
Data: gin.H{"total": len(req.ServerIDs), "async": true},
|
||||
@@ -471,22 +471,22 @@ func (h *AgentInstallHandler) RemoteExec(c *gin.Context) {
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Hostname == "" || req.Command == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请提供 hostname 和 command"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请提供 hostname 和 command"})
|
||||
return
|
||||
}
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = 30
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.Where("hostname = ? OR ip = ?", req.Hostname, req.Hostname).First(&server).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "未找到服务器: " + req.Hostname})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "未找到服务器: " + req.Hostname})
|
||||
return
|
||||
}
|
||||
|
||||
client, err := sshDial(server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 500, Message: "SSH连接失败: " + err.Error()})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "SSH连接失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
@@ -504,11 +504,11 @@ func (h *AgentInstallHandler) RemoteExec(c *gin.Context) {
|
||||
select {
|
||||
case r := <-done:
|
||||
if r.err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "执行完成", Data: gin.H{"output": r.out, "error": r.err.Error()}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "执行完成", Data: gin.H{"output": r.out, "error": r.err.Error()}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "执行成功", Data: gin.H{"output": r.out}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "执行成功", Data: gin.H{"output": r.out}})
|
||||
case <-time.After(time.Duration(req.Timeout) * time.Second):
|
||||
c.JSON(http.StatusOK, models.Response{Code: 408, Message: fmt.Sprintf("命令执行超时(%ds)", req.Timeout)})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 408, Message: fmt.Sprintf("命令执行超时(%ds)", req.Timeout)})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -30,11 +30,11 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Hostname == "" || req.Command == "" {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "hostname 和 command 不能为空"})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "hostname 和 command 不能为空"})
|
||||
return
|
||||
}
|
||||
if req.Timeout == 0 {
|
||||
@@ -46,7 +46,7 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||
|
||||
username, _ := c.Get("username")
|
||||
|
||||
task := models.AgentTask{
|
||||
task := model.AgentTask{
|
||||
TaskID: uuid.New().String(),
|
||||
Hostname: req.Hostname,
|
||||
Type: req.Type,
|
||||
@@ -57,11 +57,11 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&task).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok", Data: task})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "ok", Data: task})
|
||||
}
|
||||
|
||||
// PollTasks Agent 轮询拉取任务(Agent 调用,无需 JWT)
|
||||
@@ -69,12 +69,12 @@ func (h *AgentTaskHandler) CreateTask(c *gin.Context) {
|
||||
func (h *AgentTaskHandler) PollTasks(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找该主机 pending 的任务,按时间排序,最多取 5 个
|
||||
var tasks []models.AgentTask
|
||||
var tasks []model.AgentTask
|
||||
h.db.Where("hostname = ? AND status = ?", hostname, "pending").
|
||||
Order("created_at ASC").
|
||||
Limit(5).
|
||||
@@ -83,11 +83,11 @@ func (h *AgentTaskHandler) PollTasks(c *gin.Context) {
|
||||
// 标记为 running(防其他 agent 重复拉取)
|
||||
now := time.Now()
|
||||
for _, t := range tasks {
|
||||
h.db.Model(&models.AgentTask{}).Where("id = ? AND status = ?", t.ID, "pending").
|
||||
h.db.Model(&model.AgentTask{}).Where("id = ? AND status = ?", t.ID, "pending").
|
||||
Updates(map[string]interface{}{"status": "running", "picked_at": now})
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Data: tasks})
|
||||
c.JSON(200, model.Response{Code: 200, Data: tasks})
|
||||
}
|
||||
|
||||
// SubmitResult Agent 回传执行结果(Agent 调用,无需 JWT)
|
||||
@@ -101,7 +101,7 @@ func (h *AgentTaskHandler) SubmitResult(c *gin.Context) {
|
||||
Status string `json:"status"` // success / failed / timeout
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,18 +113,18 @@ func (h *AgentTaskHandler) SubmitResult(c *gin.Context) {
|
||||
"finished_at": now,
|
||||
}
|
||||
|
||||
if err := h.db.Model(&models.AgentTask{}).Where("task_id = ?", taskID).Updates(updates).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "更新失败"})
|
||||
if err := h.db.Model(&model.AgentTask{}).Where("task_id = ?", taskID).Updates(updates).Error; err != nil {
|
||||
c.JSON(500, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// ListTasks 查询任务列表(前端调用,需 JWT)
|
||||
// GET /api/v1/agent/tasks?hostname=xxx&status=xxx
|
||||
func (h *AgentTaskHandler) ListTasks(c *gin.Context) {
|
||||
query := h.db.Model(&models.AgentTask{})
|
||||
query := h.db.Model(&model.AgentTask{})
|
||||
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
@@ -133,21 +133,21 @@ func (h *AgentTaskHandler) ListTasks(c *gin.Context) {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
var tasks []models.AgentTask
|
||||
var tasks []model.AgentTask
|
||||
query.Order("created_at DESC").Limit(200).Find(&tasks)
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Data: tasks})
|
||||
c.JSON(200, model.Response{Code: 200, Data: tasks})
|
||||
}
|
||||
|
||||
// GetTask 获取单个任务详情
|
||||
// GET /api/v1/agent/tasks/:id
|
||||
func (h *AgentTaskHandler) GetTask(c *gin.Context) {
|
||||
var task models.AgentTask
|
||||
var task model.AgentTask
|
||||
if err := h.db.First(&task, c.Param("id")).Error; err != nil {
|
||||
c.JSON(404, models.Response{Code: 404, Message: "任务不存在"})
|
||||
c.JSON(404, model.Response{Code: 404, Message: "任务不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: task})
|
||||
c.JSON(200, model.Response{Code: 200, Data: task})
|
||||
}
|
||||
|
||||
// ===== Agent 告警规则 =====
|
||||
@@ -162,20 +162,20 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
||||
|
||||
// 获取所有在线主机
|
||||
var onlineHosts []string
|
||||
h.db.Model(&models.Server{}).
|
||||
h.db.Model(&model.Server{}).
|
||||
Where("agent_status = ?", "online").
|
||||
Pluck("hostname", &onlineHosts)
|
||||
|
||||
if len(onlineHosts) == 0 {
|
||||
// 如果没有在线主机,尝试从 agent_reports 中获取最近 3 分钟活跃的主机名
|
||||
h.db.Model(&models.AgentReport{}).
|
||||
h.db.Model(&model.AgentReport{}).
|
||||
Where("report_time >= ?", time.Now().Add(-3*time.Minute)).
|
||||
Distinct("hostname").
|
||||
Pluck("hostname", &onlineHosts)
|
||||
}
|
||||
|
||||
if len(onlineHosts) == 0 {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "没有在线的主机,请先安装并启动 Agent"})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "没有在线的主机,请先安装并启动 Agent"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,14 +199,14 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
||||
for _, host := range onlineHosts {
|
||||
for _, def := range defaultRules {
|
||||
// 检查是否已存在同名同主机的规则,避免重复创建
|
||||
var existing models.AgentRule
|
||||
var existing model.AgentRule
|
||||
h.db.Where("name = ? AND hostname = ?", def.Name, host).First(&existing)
|
||||
if existing.ID > 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
rule := models.AgentRule{
|
||||
rule := model.AgentRule{
|
||||
Name: def.Name,
|
||||
Hostname: host,
|
||||
Metric: def.Metric,
|
||||
@@ -221,7 +221,7 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{
|
||||
c.JSON(200, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已为 %d 台主机创建 %d 条规则(跳过 %d 条已存在的)", len(onlineHosts), created, skipped),
|
||||
Data: gin.H{"hosts": len(onlineHosts), "created": created, "skipped": skipped},
|
||||
@@ -230,67 +230,67 @@ func (h *AgentTaskHandler) QuickSetupRules(c *gin.Context) {
|
||||
|
||||
// ListRules GET /api/v1/agent/rules
|
||||
func (h *AgentTaskHandler) ListRules(c *gin.Context) {
|
||||
var rules []models.AgentRule
|
||||
var rules []model.AgentRule
|
||||
h.db.Order("created_at DESC").Find(&rules)
|
||||
c.JSON(200, models.Response{Code: 200, Data: rules})
|
||||
c.JSON(200, model.Response{Code: 200, Data: rules})
|
||||
}
|
||||
|
||||
// CreateRule POST /api/v1/agent/rules
|
||||
func (h *AgentTaskHandler) CreateRule(c *gin.Context) {
|
||||
var rule models.AgentRule
|
||||
var rule model.AgentRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(400, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
rule.ID = 0
|
||||
rule.Enabled = true
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: rule})
|
||||
c.JSON(200, model.Response{Code: 200, Data: rule})
|
||||
}
|
||||
|
||||
// UpdateRule PUT /api/v1/agent/rules/:id
|
||||
func (h *AgentTaskHandler) UpdateRule(c *gin.Context) {
|
||||
var rule models.AgentRule
|
||||
var rule model.AgentRule
|
||||
if err := h.db.First(&rule, c.Param("id")).Error; err != nil {
|
||||
c.JSON(404, models.Response{Code: 404, Message: "规则不存在"})
|
||||
c.JSON(404, model.Response{Code: 404, Message: "规则不存在"})
|
||||
return
|
||||
}
|
||||
var update models.AgentRule
|
||||
var update model.AgentRule
|
||||
if err := c.ShouldBindJSON(&update); err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(400, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Model(&rule).Updates(update)
|
||||
c.JSON(200, models.Response{Code: 200, Data: rule})
|
||||
c.JSON(200, model.Response{Code: 200, Data: rule})
|
||||
}
|
||||
|
||||
// DeleteRule DELETE /api/v1/agent/rules/:id
|
||||
func (h *AgentTaskHandler) DeleteRule(c *gin.Context) {
|
||||
h.db.Delete(&models.AgentRule{}, c.Param("id"))
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
h.db.Delete(&model.AgentRule{}, c.Param("id"))
|
||||
c.JSON(200, model.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// ListAlerts GET /api/v1/agent/alerts
|
||||
func (h *AgentTaskHandler) ListAlerts(c *gin.Context) {
|
||||
query := h.db.Model(&models.AgentAlert{})
|
||||
query := h.db.Model(&model.AgentAlert{})
|
||||
if c.Query("resolved") == "false" {
|
||||
query = query.Where("resolved = ?", false)
|
||||
}
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
}
|
||||
var alerts []models.AgentAlert
|
||||
var alerts []model.AgentAlert
|
||||
query.Order("created_at DESC").Limit(200).Find(&alerts)
|
||||
c.JSON(200, models.Response{Code: 200, Data: alerts})
|
||||
c.JSON(200, model.Response{Code: 200, Data: alerts})
|
||||
}
|
||||
|
||||
// ResolveAlert POST /api/v1/agent/alerts/:id/resolve
|
||||
func (h *AgentTaskHandler) ResolveAlert(c *gin.Context) {
|
||||
now := time.Now()
|
||||
h.db.Model(&models.AgentAlert{}).Where("id = ?", c.Param("id")).
|
||||
h.db.Model(&model.AgentAlert{}).Where("id = ?", c.Param("id")).
|
||||
Updates(map[string]interface{}{"resolved": true, "resolved_at": now})
|
||||
c.JSON(200, models.Response{Code: 200, Message: "ok"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -33,13 +33,13 @@ func NewAlertHandler(db *gorm.DB) *AlertHandler {
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Param status query string false "状态过滤"
|
||||
// @Param severity query string false "级别过滤"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /alerts [get]
|
||||
func (h *AlertHandler) ListAlerts(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.AlertEvent{}).Preload("Rule")
|
||||
db := h.db.Model(&model.AlertEvent{}).Preload("Rule")
|
||||
|
||||
// 状态过滤
|
||||
if status := c.Query("status"); status != "" {
|
||||
@@ -54,16 +54,16 @@ func (h *AlertHandler) ListAlerts(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var alerts []models.AlertEvent
|
||||
var alerts []model.AlertEvent
|
||||
db.Order("created_at DESC").
|
||||
Offset(query.GetOffset()).
|
||||
Limit(query.GetLimit()).
|
||||
Find(&alerts)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: alerts,
|
||||
Total: total,
|
||||
},
|
||||
@@ -78,24 +78,24 @@ func (h *AlertHandler) ListAlerts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "告警ID"
|
||||
// @Param request body models.AcknowledgeAlertRequest true "确认请求"
|
||||
// @Success 200 {object} models.Response{data=models.AlertEvent}
|
||||
// @Param request body model.AcknowledgeAlertRequest true "确认请求"
|
||||
// @Success 200 {object} model.Response{data=model.AlertEvent}
|
||||
// @Router /alerts/{id}/ack [post]
|
||||
func (h *AlertHandler) AcknowledgeAlert(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.AcknowledgeAlertRequest
|
||||
var req model.AcknowledgeAlertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var alert models.AlertEvent
|
||||
var alert model.AlertEvent
|
||||
if err := h.db.First(&alert, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "告警不存在",
|
||||
})
|
||||
@@ -108,14 +108,14 @@ func (h *AlertHandler) AcknowledgeAlert(c *gin.Context) {
|
||||
// alert.AcknowledgedAt = &now
|
||||
|
||||
if err := h.db.Save(&alert).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "确认失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "确认成功",
|
||||
Data: alert,
|
||||
+38
-38
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -26,16 +26,16 @@ import (
|
||||
// @Tags 告警
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /alerts/notify-channels [get]
|
||||
func (h *AlertHandler) ListNotifyChannels(c *gin.Context) {
|
||||
var channels []models.NotifyChannel
|
||||
var channels []model.NotifyChannel
|
||||
h.db.Order("created_at DESC").Find(&channels)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: channels,
|
||||
Total: int64(len(channels)),
|
||||
},
|
||||
@@ -49,20 +49,20 @@ func (h *AlertHandler) ListNotifyChannels(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateNotifyChannelRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.NotifyChannel}
|
||||
// @Param request body model.CreateNotifyChannelRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.NotifyChannel}
|
||||
// @Router /alerts/notify-channels [post]
|
||||
func (h *AlertHandler) CreateNotifyChannel(c *gin.Context) {
|
||||
var req models.CreateNotifyChannelRequest
|
||||
var req model.CreateNotifyChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
channel := models.NotifyChannel{
|
||||
channel := model.NotifyChannel{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Config: req.Config,
|
||||
@@ -70,14 +70,14 @@ func (h *AlertHandler) CreateNotifyChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: channel,
|
||||
@@ -92,24 +92,24 @@ func (h *AlertHandler) CreateNotifyChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "渠道ID"
|
||||
// @Param request body models.UpdateNotifyChannelRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.NotifyChannel}
|
||||
// @Param request body model.UpdateNotifyChannelRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.NotifyChannel}
|
||||
// @Router /alerts/notify-channels/{id} [put]
|
||||
func (h *AlertHandler) UpdateNotifyChannel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateNotifyChannelRequest
|
||||
var req model.UpdateNotifyChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var channel models.NotifyChannel
|
||||
var channel model.NotifyChannel
|
||||
if err := h.db.First(&channel, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "通知渠道不存在",
|
||||
})
|
||||
@@ -131,7 +131,7 @@ func (h *AlertHandler) UpdateNotifyChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&channel).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -139,7 +139,7 @@ func (h *AlertHandler) UpdateNotifyChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
h.db.First(&channel, id)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: channel,
|
||||
@@ -153,20 +153,20 @@ func (h *AlertHandler) UpdateNotifyChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "渠道ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /alerts/notify-channels/{id} [delete]
|
||||
func (h *AlertHandler) DeleteNotifyChannel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
if err := h.db.Delete(&models.NotifyChannel{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.NotifyChannel{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -179,14 +179,14 @@ func (h *AlertHandler) DeleteNotifyChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "渠道ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /alerts/notify-channels/{id}/test [post]
|
||||
func (h *AlertHandler) TestNotifyChannel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var channel models.NotifyChannel
|
||||
var channel model.NotifyChannel
|
||||
if err := h.db.First(&channel, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "通知渠道不存在",
|
||||
})
|
||||
@@ -213,7 +213,7 @@ func (h *AlertHandler) TestNotifyChannel(c *gin.Context) {
|
||||
|
||||
h.db.Model(&channel).Update("test_status", status)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: message,
|
||||
Data: gin.H{
|
||||
@@ -228,7 +228,7 @@ func (h *AlertHandler) TestNotifyChannel(c *gin.Context) {
|
||||
|
||||
// sendFeishuTest 发送飞书测试通知
|
||||
func sendFeishuTest(configJSON string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
var cfg model.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func sendFeishuTest(configJSON string) error {
|
||||
|
||||
// sendFeishuAlert 发送飞书告警通知
|
||||
func sendFeishuAlert(configJSON, title, content, severity string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
var cfg model.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
@@ -272,7 +272,7 @@ func sendFeishuAlert(configJSON, title, content, severity string) error {
|
||||
|
||||
// sendFeishuRecovery 发送飞书恢复通知
|
||||
func sendFeishuRecovery(configJSON, title, content string) error {
|
||||
var cfg models.FeishuWebhookConfig
|
||||
var cfg model.FeishuWebhookConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
||||
return fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
@@ -397,12 +397,12 @@ func splitLines(s string) []string {
|
||||
}
|
||||
|
||||
// buildFeishuCard 构建飞书卡片消息体(保留备用,当前使用文本格式)
|
||||
func buildFeishuCard(title, content, color string) models.FeishuCardMessage {
|
||||
return models.FeishuCardMessage{
|
||||
func buildFeishuCard(title, content, color string) model.FeishuCardMessage {
|
||||
return model.FeishuCardMessage{
|
||||
MsgType: "interactive",
|
||||
Content: models.FeishuCardContent{
|
||||
Content: model.FeishuCardContent{
|
||||
Title: title,
|
||||
Content: [][]models.FeishuCardElement{
|
||||
Content: [][]model.FeishuCardElement{
|
||||
{
|
||||
{
|
||||
Tag: "markdown",
|
||||
@@ -432,7 +432,7 @@ func genFeishuSign(timestamp int64, secret string) string {
|
||||
|
||||
// NotifyAlert 发送告警通知到所有启用的渠道
|
||||
func (h *AlertHandler) NotifyAlert(ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
||||
var channels []models.NotifyChannel
|
||||
var channels []model.NotifyChannel
|
||||
h.db.Where("enabled = ?", true).Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
@@ -449,7 +449,7 @@ func (h *AlertHandler) NotifyAlert(ruleName, hostname, metric, severity, message
|
||||
|
||||
// NotifyRecovery 发送恢复通知到所有启用的渠道
|
||||
func (h *AlertHandler) NotifyRecovery(ruleName, hostname, metric, message string) {
|
||||
var channels []models.NotifyChannel
|
||||
var channels []model.NotifyChannel
|
||||
h.db.Where("enabled = ?", true).Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
+34
-34
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -16,23 +16,23 @@ import (
|
||||
|
||||
// ListContacts GET /alerts/contacts
|
||||
func (h *AlertHandler) ListContacts(c *gin.Context) {
|
||||
var contacts []models.AlertContact
|
||||
var contacts []model.AlertContact
|
||||
h.db.Order("created_at DESC").Find(&contacts)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{List: contacts, Total: int64(len(contacts))},
|
||||
Data: model.ListResponse{List: contacts, Total: int64(len(contacts))},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateContact POST /alerts/contacts
|
||||
func (h *AlertHandler) CreateContact(c *gin.Context) {
|
||||
var req models.CreateContactRequest
|
||||
var req model.CreateContactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
contact := models.AlertContact{
|
||||
contact := model.AlertContact{
|
||||
Name: req.Name,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
@@ -40,23 +40,23 @@ func (h *AlertHandler) CreateContact(c *gin.Context) {
|
||||
WebhookURL: req.WebhookURL,
|
||||
}
|
||||
if err := h.db.Create(&contact).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: contact})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: contact})
|
||||
}
|
||||
|
||||
// UpdateContact PUT /alerts/contacts/:id
|
||||
func (h *AlertHandler) UpdateContact(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req models.UpdateContactRequest
|
||||
var req model.UpdateContactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
var contact models.AlertContact
|
||||
var contact model.AlertContact
|
||||
if err := h.db.First(&contact, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "联系人不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "联系人不存在"})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
@@ -67,63 +67,63 @@ func (h *AlertHandler) UpdateContact(c *gin.Context) {
|
||||
if req.WebhookURL != "" { updates["webhook_url"] = req.WebhookURL }
|
||||
h.db.Model(&contact).Updates(updates)
|
||||
h.db.First(&contact, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: contact})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: contact})
|
||||
}
|
||||
|
||||
// DeleteContact DELETE /alerts/contacts/:id
|
||||
func (h *AlertHandler) DeleteContact(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.db.Delete(&models.AlertContact{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.AlertContact{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ==================== 联系组 ====================
|
||||
|
||||
// ListContactGroups GET /alerts/contact-groups
|
||||
func (h *AlertHandler) ListContactGroups(c *gin.Context) {
|
||||
var groups []models.AlertContactGroup
|
||||
var groups []model.AlertContactGroup
|
||||
h.db.Order("created_at DESC").Find(&groups)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{List: groups, Total: int64(len(groups))},
|
||||
Data: model.ListResponse{List: groups, Total: int64(len(groups))},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateContactGroup POST /alerts/contact-groups
|
||||
func (h *AlertHandler) CreateContactGroup(c *gin.Context) {
|
||||
var req models.CreateContactGroupRequest
|
||||
var req model.CreateContactGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
idsJSON, _ := json.Marshal(req.ContactIDs)
|
||||
group := models.AlertContactGroup{
|
||||
group := model.AlertContactGroup{
|
||||
GroupName: req.GroupName,
|
||||
Notes: req.Notes,
|
||||
ContactIDs: string(idsJSON),
|
||||
}
|
||||
if err := h.db.Create(&group).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: group})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: group})
|
||||
}
|
||||
|
||||
// UpdateContactGroup PUT /alerts/contact-groups/:id
|
||||
func (h *AlertHandler) UpdateContactGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req models.UpdateContactGroupRequest
|
||||
var req model.UpdateContactGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
var group models.AlertContactGroup
|
||||
var group model.AlertContactGroup
|
||||
if err := h.db.First(&group, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "联系组不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "联系组不存在"})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
@@ -135,15 +135,15 @@ func (h *AlertHandler) UpdateContactGroup(c *gin.Context) {
|
||||
}
|
||||
h.db.Model(&group).Updates(updates)
|
||||
h.db.First(&group, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: group})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: group})
|
||||
}
|
||||
|
||||
// DeleteContactGroup DELETE /alerts/contact-groups/:id
|
||||
func (h *AlertHandler) DeleteContactGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.db.Delete(&models.AlertContactGroup{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.AlertContactGroup{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
+161
-39
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -29,13 +29,13 @@ func (e *AlertEngine) Check() {
|
||||
|
||||
// 检查 CPU/内存/磁盘/进程/TCP连接/端口 阈值
|
||||
func (e *AlertEngine) checkResourceAlerts() {
|
||||
var rules []models.AgentRule
|
||||
var rules []model.AgentRule
|
||||
e.db.Where("enabled = ? AND metric IN ?", true, []string{"cpu", "mem", "disk", "service_process", "tcp_time_wait", "tcp_close_wait", "fd"}).Find(&rules)
|
||||
|
||||
for _, rule := range rules {
|
||||
// 获取目标主机最新上报
|
||||
var report models.AgentReport
|
||||
query := e.db.Model(&models.AgentReport{})
|
||||
var report model.AgentReport
|
||||
query := e.db.Model(&model.AgentReport{})
|
||||
if rule.Hostname != "" {
|
||||
query = query.Where("hostname = ?", rule.Hostname)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
case "mem":
|
||||
value = report.MemUsage
|
||||
case "disk":
|
||||
var disks []models.DiskMetric
|
||||
var disks []model.DiskMetric
|
||||
json.Unmarshal([]byte(report.DiskInfo), &disks)
|
||||
for _, d := range disks {
|
||||
if d.UsagePercent > value {
|
||||
@@ -59,16 +59,16 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
}
|
||||
}
|
||||
case "tcp_time_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
var netStat model.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPTimeWait)
|
||||
case "tcp_close_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
var netStat model.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPCloseWait)
|
||||
case "service_process":
|
||||
// 进程不存在告警 — 检查 process_top_info 中是否包含指定进程名
|
||||
var processes []models.ProcessMetric
|
||||
var processes []model.ProcessMetric
|
||||
json.Unmarshal([]byte(report.ProcessTopInfo), &processes)
|
||||
found := false
|
||||
for _, p := range processes {
|
||||
@@ -82,7 +82,7 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
}
|
||||
value = 0 // 进程不存在时触发
|
||||
case "fd":
|
||||
var fdStat models.FDMetric
|
||||
var fdStat model.FDMetric
|
||||
json.Unmarshal([]byte(report.FDInfo), &fdStat)
|
||||
value = fdStat.UsagePct
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
}
|
||||
|
||||
// 避免重复告警:如果最近 10 分钟已有同规则未恢复告警,不重复
|
||||
var existing models.AgentAlert
|
||||
var existing model.AgentAlert
|
||||
tenMinAgo := time.Now().Add(-10 * time.Minute)
|
||||
e.db.Where("rule_name = ? AND hostname = ? AND resolved = ? AND created_at > ?",
|
||||
rule.Name, report.Hostname, false, tenMinAgo).First(&existing)
|
||||
@@ -101,8 +101,13 @@ func (e *AlertEngine) checkResourceAlerts() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查静默规则 — 匹配则不创建告警、不发送通知
|
||||
if e.isSilenced(rule.Name, report.Hostname, rule.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建告警
|
||||
alert := models.AgentAlert{
|
||||
alert := model.AgentAlert{
|
||||
RuleName: rule.Name,
|
||||
Hostname: report.Hostname,
|
||||
Metric: rule.Metric,
|
||||
@@ -132,14 +137,14 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
offlineThreshold := 3 * time.Minute
|
||||
|
||||
// 获取所有已登记的主机(去重 IP)
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
e.db.Select("ip, hostname").Group("ip").Find(&servers)
|
||||
|
||||
// 收集所有 agent_reports 中活跃上报的主机名(最近 3 分钟内有上报)
|
||||
threeMinAgo := time.Now().Add(-offlineThreshold)
|
||||
|
||||
var activeHostnames []string
|
||||
e.db.Model(&models.AgentReport{}).
|
||||
e.db.Model(&model.AgentReport{}).
|
||||
Where("report_time >= ?", threeMinAgo).
|
||||
Distinct("hostname").
|
||||
Pluck("hostname", &activeHostnames)
|
||||
@@ -160,7 +165,7 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
}
|
||||
// 检查该 hostname 的最新上报——其源 IP 是否匹配此 server IP
|
||||
// (通过 servers 表中查找 hostname 对应的 IP)
|
||||
var matchedServer models.Server
|
||||
var matchedServer model.Server
|
||||
e.db.Where("hostname = ?", h).First(&matchedServer)
|
||||
if matchedServer.ID > 0 && matchedServer.IP == s.IP {
|
||||
hasActiveAgent = true
|
||||
@@ -173,13 +178,13 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
}
|
||||
|
||||
// 该服务器离线:检查是否已有未恢复的离线告警
|
||||
var existing models.AgentAlert
|
||||
var existing model.AgentAlert
|
||||
e.db.Where("hostname = ? AND metric = ? AND resolved = ?",
|
||||
s.Hostname, "offline", false).First(&existing)
|
||||
|
||||
if existing.ID == 0 {
|
||||
// 获取最后一次上报时间计算离线时长
|
||||
var lastReport models.AgentReport
|
||||
var lastReport model.AgentReport
|
||||
e.db.Where("hostname = ?", s.Hostname).Order("id DESC").First(&lastReport)
|
||||
offlineMinutes := 3.0
|
||||
var lastReportTime time.Time
|
||||
@@ -188,7 +193,12 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
lastReportTime = lastReport.ReportTime
|
||||
}
|
||||
|
||||
alert := models.AgentAlert{
|
||||
// 检查静默规则
|
||||
if e.isSilenced("主机离线", s.Hostname, 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
alert := model.AgentAlert{
|
||||
RuleName: "主机离线",
|
||||
Hostname: s.Hostname,
|
||||
Metric: "offline",
|
||||
@@ -213,7 +223,7 @@ func (e *AlertEngine) checkOfflineAlerts() {
|
||||
|
||||
// 检查端口消失(之前有的监听端口现在没了)
|
||||
func (e *AlertEngine) checkPortAlerts() {
|
||||
var rules []models.AgentRule
|
||||
var rules []model.AgentRule
|
||||
e.db.Where("enabled = ? AND metric = ?", true, "port_down").Find(&rules)
|
||||
|
||||
for _, rule := range rules {
|
||||
@@ -222,8 +232,8 @@ func (e *AlertEngine) checkPortAlerts() {
|
||||
}
|
||||
|
||||
// 获取目标主机最新上报
|
||||
var report models.AgentReport
|
||||
query := e.db.Model(&models.AgentReport{})
|
||||
var report model.AgentReport
|
||||
query := e.db.Model(&model.AgentReport{})
|
||||
if rule.Hostname != "" {
|
||||
query = query.Where("hostname = ?", rule.Hostname)
|
||||
}
|
||||
@@ -233,7 +243,7 @@ func (e *AlertEngine) checkPortAlerts() {
|
||||
}
|
||||
|
||||
// 检查端口是否在最新上报中
|
||||
var ports []models.PortMetric
|
||||
var ports []model.PortMetric
|
||||
json.Unmarshal([]byte(report.PortInfo), &ports)
|
||||
|
||||
portUp := false
|
||||
@@ -249,14 +259,19 @@ func (e *AlertEngine) checkPortAlerts() {
|
||||
}
|
||||
|
||||
// 端口消失了,检查是否已有未恢复告警
|
||||
var existing models.AgentAlert
|
||||
var existing model.AgentAlert
|
||||
e.db.Where("rule_name = ? AND hostname = ? AND resolved = ?",
|
||||
rule.Name, report.Hostname, false).First(&existing)
|
||||
if existing.ID > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
alert := models.AgentAlert{
|
||||
// 检查静默规则
|
||||
if e.isSilenced(rule.Name, report.Hostname, rule.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
alert := model.AgentAlert{
|
||||
RuleName: rule.Name,
|
||||
Hostname: report.Hostname,
|
||||
Metric: "port_down",
|
||||
@@ -278,7 +293,7 @@ func (e *AlertEngine) checkPortAlerts() {
|
||||
|
||||
// 自动恢复告警(指标恢复正常)
|
||||
func (e *AlertEngine) autoResolveAlerts() {
|
||||
var alerts []models.AgentAlert
|
||||
var alerts []model.AgentAlert
|
||||
e.db.Where("resolved = ?", false).Find(&alerts)
|
||||
|
||||
for _, alert := range alerts {
|
||||
@@ -287,19 +302,19 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
switch alert.Metric {
|
||||
case "offline":
|
||||
// 主机重新上报了
|
||||
var report models.AgentReport
|
||||
var report model.AgentReport
|
||||
e.db.Where("hostname = ?", alert.Hostname).Order("id DESC").First(&report)
|
||||
if report.ID > 0 && time.Since(report.ReportTime) < 3*time.Minute {
|
||||
resolved = true
|
||||
}
|
||||
case "cpu", "mem", "disk", "tcp_time_wait", "tcp_close_wait", "fd":
|
||||
// 获取对应规则,检查是否恢复正常
|
||||
var rule models.AgentRule
|
||||
var rule model.AgentRule
|
||||
e.db.Where("name = ?", alert.RuleName).First(&rule)
|
||||
if rule.ID == 0 {
|
||||
continue
|
||||
}
|
||||
var report models.AgentReport
|
||||
var report model.AgentReport
|
||||
if rule.Hostname != "" {
|
||||
e.db.Where("hostname = ?", alert.Hostname).Order("id DESC").First(&report)
|
||||
} else {
|
||||
@@ -315,7 +330,7 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
case "mem":
|
||||
value = report.MemUsage
|
||||
case "disk":
|
||||
var disks []models.DiskMetric
|
||||
var disks []model.DiskMetric
|
||||
json.Unmarshal([]byte(report.DiskInfo), &disks)
|
||||
for _, d := range disks {
|
||||
if d.UsagePercent > value {
|
||||
@@ -323,15 +338,15 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
}
|
||||
}
|
||||
case "tcp_time_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
var netStat model.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPTimeWait)
|
||||
case "tcp_close_wait":
|
||||
var netStat models.NetworkStatModel
|
||||
var netStat model.NetworkStatModel
|
||||
json.Unmarshal([]byte(report.NetInfo), &netStat)
|
||||
value = float64(netStat.TCPCloseWait)
|
||||
case "fd":
|
||||
var fdStat models.FDMetric
|
||||
var fdStat model.FDMetric
|
||||
json.Unmarshal([]byte(report.FDInfo), &fdStat)
|
||||
value = fdStat.UsagePct
|
||||
}
|
||||
@@ -339,12 +354,12 @@ func (e *AlertEngine) autoResolveAlerts() {
|
||||
resolved = true
|
||||
}
|
||||
case "port_down":
|
||||
var rule models.AgentRule
|
||||
var rule model.AgentRule
|
||||
e.db.Where("name = ?", alert.RuleName).First(&rule)
|
||||
if rule.ID > 0 {
|
||||
var report models.AgentReport
|
||||
var report model.AgentReport
|
||||
e.db.Where("hostname = ?", alert.Hostname).Order("id DESC").First(&report)
|
||||
var ports []models.PortMetric
|
||||
var ports []model.PortMetric
|
||||
json.Unmarshal([]byte(report.PortInfo), &ports)
|
||||
for _, p := range ports {
|
||||
if p.Port == rule.Port {
|
||||
@@ -386,14 +401,14 @@ func (e *AlertEngine) compareOperator(op string, val, threshold float64) bool {
|
||||
}
|
||||
|
||||
// notifyAlert 发送告警通知(通过 AgentRule 对应的 notify_channels)
|
||||
func (e *AlertEngine) notifyAlert(rule *models.AgentRule, hostname string, alert models.AgentAlert) {
|
||||
func (e *AlertEngine) notifyAlert(rule *model.AgentRule, hostname string, alert model.AgentAlert) {
|
||||
e.notifyAlertRule(rule.Name, hostname, alert.Metric, alert.Severity,
|
||||
alert.Message, alert.Value, alert.Threshold)
|
||||
}
|
||||
|
||||
// notifyAlertRule 按规则发送告警通知到所有启用的飞书渠道
|
||||
func (e *AlertEngine) notifyAlertRule(ruleName, hostname, metric, severity, message string, value, threshold float64) {
|
||||
var channels []models.NotifyChannel
|
||||
var channels []model.NotifyChannel
|
||||
e.db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
@@ -410,15 +425,122 @@ func (e *AlertEngine) notifyAlertRule(ruleName, hostname, metric, severity, mess
|
||||
|
||||
// notifyRecovery 发送恢复通知到所有启用的飞书渠道
|
||||
func (e *AlertEngine) notifyRecovery(ruleName, hostname, metric, message string) {
|
||||
var channels []models.NotifyChannel
|
||||
var channels []model.NotifyChannel
|
||||
e.db.Where("enabled = ? AND type = ?", true, "feishu").Find(&channels)
|
||||
|
||||
for _, ch := range channels {
|
||||
title := fmt.Sprintf("%s — 已恢复正常", ruleName)
|
||||
content := fmt.Sprintf("**告警已恢复**\\n\\n主机:%s\\n指标:%s\\n详情:%s\\n恢复时间:%s",
|
||||
content := fmt.Sprintf("**告警已恢复**\\\\n\\\\n主机:%s\\\\n指标:%s\\\\n详情:%s\\\\n恢复时间:%s",
|
||||
hostname, metric, message, time.Now().Format("2006-01-02 15:04:05"))
|
||||
if err := sendFeishuRecovery(ch.Config, title, content); err != nil {
|
||||
fmt.Printf("[AlertEngine] 飞书恢复通知失败 (渠道:%s): %v\n", ch.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isSilenced 检查给定告警是否被静默规则匹配
|
||||
// 匹配逻辑:status=1 且 当前时间在静默窗口内
|
||||
// 匹配维度:rule_name(精确匹配)、rule_id(ID 匹配)、hostname(通过 target_ids 中的 server IP 间接匹配)
|
||||
// silence_type:onetime=一次性窗口,daily=每天指定时段,weekly=每周指定星期+时段
|
||||
func (e *AlertEngine) isSilenced(ruleName, hostname string, ruleID uint) bool {
|
||||
var silences []model.AlertSilence
|
||||
e.db.Where("status = ?", 1).Find(&silences)
|
||||
|
||||
now := time.Now()
|
||||
for _, s := range silences {
|
||||
// 检查规则名匹配 — 通过 rule_ids JSON 数组
|
||||
if s.RuleIDs != "" && s.RuleIDs != "null" {
|
||||
var ruleIDs []uint
|
||||
json.Unmarshal([]byte(s.RuleIDs), &ruleIDs)
|
||||
matched := false
|
||||
for _, rid := range ruleIDs {
|
||||
if rid == ruleID {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue // 该静默规则不匹配此 rule_id
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标主机匹配 — 通过 target_ids(server IDs)间接匹配
|
||||
if s.TargetIDs != "" && s.TargetIDs != "null" {
|
||||
var targetIDs []uint
|
||||
json.Unmarshal([]byte(s.TargetIDs), &targetIDs)
|
||||
if len(targetIDs) > 0 {
|
||||
matched := false
|
||||
for _, tid := range targetIDs {
|
||||
var server model.Server
|
||||
e.db.Select("hostname").First(&server, tid)
|
||||
if server.Hostname == hostname {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查时间窗口
|
||||
switch s.SilenceType {
|
||||
case model.AlertSilenceOneTime:
|
||||
// 一次性:start_time <= now <= end_time
|
||||
if now.After(s.StartTime) && now.Before(s.EndTime) {
|
||||
return true
|
||||
}
|
||||
case model.AlertSilenceDaily:
|
||||
// 每天:检查当前时分是否在窗口内
|
||||
nowMinutes := now.Hour()*60 + now.Minute()
|
||||
startMinutes := s.StartTime.Hour()*60 + s.StartTime.Minute()
|
||||
endMinutes := s.EndTime.Hour()*60 + s.EndTime.Minute()
|
||||
if startMinutes <= endMinutes {
|
||||
if nowMinutes >= startMinutes && nowMinutes <= endMinutes {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// 跨天(如 23:00-06:00)
|
||||
if nowMinutes >= startMinutes || nowMinutes <= endMinutes {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case model.AlertSilenceWeekly:
|
||||
// 每周:检查当前星期是否在 weekdays 列表中,且时分在窗口内
|
||||
if s.Weekdays != "" && s.Weekdays != "null" {
|
||||
var weekdays []int
|
||||
json.Unmarshal([]byte(s.Weekdays), &weekdays)
|
||||
weekdayMatched := false
|
||||
todayWeekday := int(now.Weekday())
|
||||
for _, wd := range weekdays {
|
||||
if wd == todayWeekday {
|
||||
weekdayMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !weekdayMatched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
nowMinutes := now.Hour()*60 + now.Minute()
|
||||
startMinutes := s.StartTime.Hour()*60 + s.StartTime.Minute()
|
||||
endMinutes := s.EndTime.Hour()*60 + s.EndTime.Minute()
|
||||
if startMinutes <= endMinutes {
|
||||
if nowMinutes >= startMinutes && nowMinutes <= endMinutes {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if nowMinutes >= startMinutes || nowMinutes <= endMinutes {
|
||||
return true
|
||||
}
|
||||
}
|
||||
default:
|
||||
// 默认按 onetime 处理
|
||||
if now.After(s.StartTime) && now.Before(s.EndTime) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -20,22 +20,22 @@ import (
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /alerts/rules [get]
|
||||
func (h *AlertHandler) ListRules(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.AlertRule{}).Count(&total)
|
||||
h.db.Model(&model.AlertRule{}).Count(&total)
|
||||
|
||||
var rules []models.AlertRule
|
||||
var rules []model.AlertRule
|
||||
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&rules)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: rules,
|
||||
Total: total,
|
||||
},
|
||||
@@ -49,20 +49,20 @@ func (h *AlertHandler) ListRules(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateAlertRuleRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.AlertRule}
|
||||
// @Param request body model.CreateAlertRuleRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.AlertRule}
|
||||
// @Router /alerts/rules [post]
|
||||
func (h *AlertHandler) CreateRule(c *gin.Context) {
|
||||
var req models.CreateAlertRuleRequest
|
||||
var req model.CreateAlertRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rule := models.AlertRule{
|
||||
rule := model.AlertRule{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
@@ -77,14 +77,14 @@ func (h *AlertHandler) CreateRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: rule,
|
||||
@@ -99,24 +99,24 @@ func (h *AlertHandler) CreateRule(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "规则ID"
|
||||
// @Param request body models.UpdateAlertRuleRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.AlertRule}
|
||||
// @Param request body model.UpdateAlertRuleRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.AlertRule}
|
||||
// @Router /alerts/rules/{id} [put]
|
||||
func (h *AlertHandler) UpdateRule(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateAlertRuleRequest
|
||||
var req model.UpdateAlertRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var rule models.AlertRule
|
||||
var rule model.AlertRule
|
||||
if err := h.db.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "规则不存在",
|
||||
})
|
||||
@@ -153,7 +153,7 @@ func (h *AlertHandler) UpdateRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&rule).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -162,7 +162,7 @@ func (h *AlertHandler) UpdateRule(c *gin.Context) {
|
||||
|
||||
h.db.First(&rule, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: rule,
|
||||
@@ -176,20 +176,20 @@ func (h *AlertHandler) UpdateRule(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "规则ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /alerts/rules/{id} [delete]
|
||||
func (h *AlertHandler) DeleteRule(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
if err := h.db.Delete(&models.AlertRule{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.AlertRule{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -202,13 +202,13 @@ func (h *AlertHandler) DeleteRule(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /alerts/rules/batch-delete [delete]
|
||||
func (h *AlertHandler) BatchDeleteRules(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
@@ -216,22 +216,22 @@ func (h *AlertHandler) BatchDeleteRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请选择要删除的规则",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.AlertRule{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.AlertRule{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -244,14 +244,14 @@ func (h *AlertHandler) BatchDeleteRules(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "规则ID"
|
||||
// @Success 200 {object} models.Response{data=models.AlertRule}
|
||||
// @Success 200 {object} model.Response{data=model.AlertRule}
|
||||
// @Router /alerts/rules/{id}/toggle [post]
|
||||
func (h *AlertHandler) ToggleRule(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var rule models.AlertRule
|
||||
var rule model.AlertRule
|
||||
if err := h.db.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "规则不存在",
|
||||
})
|
||||
@@ -260,14 +260,14 @@ func (h *AlertHandler) ToggleRule(c *gin.Context) {
|
||||
|
||||
rule.Enabled = !rule.Enabled
|
||||
if err := h.db.Save(&rule).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "操作失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "操作成功",
|
||||
Data: rule,
|
||||
@@ -309,12 +309,12 @@ func (h *AlertHandler) QuickSetupRules(c *gin.Context) {
|
||||
|
||||
created := 0
|
||||
for _, def := range rules {
|
||||
var existing models.AgentRule
|
||||
var existing model.AgentRule
|
||||
h.db.Where("name = ?", def.Name).First(&existing)
|
||||
if existing.ID > 0 {
|
||||
continue
|
||||
}
|
||||
rule := models.AgentRule{
|
||||
rule := model.AgentRule{
|
||||
Name: def.Name,
|
||||
Metric: def.Metric,
|
||||
Operator: def.Operator,
|
||||
@@ -328,7 +328,7 @@ func (h *AlertHandler) QuickSetupRules(c *gin.Context) {
|
||||
created++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("已创建 %d 条告警规则", created),
|
||||
Data: gin.H{"created": created},
|
||||
@@ -0,0 +1,260 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 告警静默规则 CRUD(AlertHandler 方法接收者) // Logan.Li
|
||||
|
||||
// CreateSilenceRequest 创建静默规则请求
|
||||
type CreateSilenceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
RuleIDs []uint `json:"rule_ids"`
|
||||
TargetIDs []uint `json:"target_ids"`
|
||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||
SilenceType string `json:"silence_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time" binding:"required"`
|
||||
Weekdays []int `json:"weekdays"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateSilenceRequest 更新静默规则请求
|
||||
type UpdateSilenceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RuleIDs []uint `json:"rule_ids"`
|
||||
TargetIDs []uint `json:"target_ids"`
|
||||
Matchers []model.AlertSilenceMatcher `json:"matchers"`
|
||||
SilenceType string `json:"silence_type"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
Weekdays []int `json:"weekdays"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
|
||||
// ListSilences GET /alerts/silences
|
||||
func (h *AlertHandler) ListSilences(c *gin.Context) {
|
||||
var silences []model.AlertSilence
|
||||
query := h.db.Order("created_at DESC")
|
||||
|
||||
if status := c.Query("status"); status != "" {
|
||||
if status == "active" {
|
||||
now := time.Now()
|
||||
query = query.Where("status = ? AND start_time <= ? AND end_time >= ?", 1, now, now)
|
||||
} else if s, err := strconv.Atoi(status); err == nil {
|
||||
query = query.Where("status = ?", s)
|
||||
}
|
||||
}
|
||||
|
||||
query.Find(&silences)
|
||||
|
||||
// 解析 JSON 字段为前端友好的格式
|
||||
type SilenceWithParsed struct {
|
||||
model.AlertSilence
|
||||
RuleIDList []uint `json:"rule_id_list"`
|
||||
TargetIDList []uint `json:"target_id_list"`
|
||||
MatcherList []model.AlertSilenceMatcher `json:"matcher_list"`
|
||||
WeekdayList []int `json:"weekday_list"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
var result []SilenceWithParsed
|
||||
now := time.Now()
|
||||
for _, s := range silences {
|
||||
item := SilenceWithParsed{AlertSilence: s}
|
||||
if s.RuleIDs != "" {
|
||||
json.Unmarshal([]byte(s.RuleIDs), &item.RuleIDList)
|
||||
}
|
||||
if s.TargetIDs != "" {
|
||||
json.Unmarshal([]byte(s.TargetIDs), &item.TargetIDList)
|
||||
}
|
||||
if s.Matchers != "" {
|
||||
json.Unmarshal([]byte(s.Matchers), &item.MatcherList)
|
||||
}
|
||||
if s.Weekdays != "" {
|
||||
json.Unmarshal([]byte(s.Weekdays), &item.WeekdayList)
|
||||
}
|
||||
item.IsActive = s.Status == 1 && now.After(s.StartTime) && now.Before(s.EndTime)
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: model.ListResponse{List: result, Total: int64(len(result))},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSilence POST /alerts/silences
|
||||
func (h *AlertHandler) CreateSilence(c *gin.Context) {
|
||||
var req CreateSilenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
silenceType := model.AlertSilenceOneTime
|
||||
if req.SilenceType != "" {
|
||||
silenceType = model.AlertSilenceType(req.SilenceType)
|
||||
}
|
||||
|
||||
// 解析时间
|
||||
var startTime, endTime time.Time
|
||||
var err error
|
||||
if req.StartTime != "" {
|
||||
startTime, err = time.Parse(time.RFC3339, req.StartTime)
|
||||
if err != nil {
|
||||
startTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local)
|
||||
if err != nil {
|
||||
startTime = time.Now()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTime = time.Now()
|
||||
}
|
||||
|
||||
endTime, err = time.Parse(time.RFC3339, req.EndTime)
|
||||
if err != nil {
|
||||
endTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "结束时间格式错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
|
||||
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
|
||||
matchersJSON, _ := json.Marshal(req.Matchers)
|
||||
weekdaysJSON, _ := json.Marshal(req.Weekdays)
|
||||
|
||||
silence := model.AlertSilence{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
RuleIDs: string(ruleIDsJSON),
|
||||
TargetIDs: string(targetIDsJSON),
|
||||
Matchers: string(matchersJSON),
|
||||
SilenceType: silenceType,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
Weekdays: string(weekdaysJSON),
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
// 获取创建人 ID
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
if uid, ok := userID.(uint); ok {
|
||||
silence.CreatedBy = uid
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.db.Create(&silence).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: silence})
|
||||
}
|
||||
|
||||
// UpdateSilence PUT /alerts/silences/:id
|
||||
func (h *AlertHandler) UpdateSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req UpdateSilenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var silence model.AlertSilence
|
||||
if err := h.db.First(&silence, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
updates["description"] = req.Description
|
||||
}
|
||||
if req.SilenceType != "" {
|
||||
updates["silence_type"] = req.SilenceType
|
||||
}
|
||||
if len(req.RuleIDs) > 0 || req.RuleIDs != nil {
|
||||
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
|
||||
updates["rule_ids"] = string(ruleIDsJSON)
|
||||
}
|
||||
if len(req.TargetIDs) > 0 || req.TargetIDs != nil {
|
||||
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
|
||||
updates["target_ids"] = string(targetIDsJSON)
|
||||
}
|
||||
if len(req.Matchers) > 0 || req.Matchers != nil {
|
||||
matchersJSON, _ := json.Marshal(req.Matchers)
|
||||
updates["matchers"] = string(matchersJSON)
|
||||
}
|
||||
if len(req.Weekdays) > 0 || req.Weekdays != nil {
|
||||
weekdaysJSON, _ := json.Marshal(req.Weekdays)
|
||||
updates["weekdays"] = string(weekdaysJSON)
|
||||
}
|
||||
if req.StartTime != "" {
|
||||
if st, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
|
||||
updates["start_time"] = st
|
||||
} else if st, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
||||
updates["start_time"] = st
|
||||
}
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
if et, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
|
||||
updates["end_time"] = et
|
||||
} else if et, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
||||
updates["end_time"] = et
|
||||
}
|
||||
}
|
||||
if req.Status != nil {
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
|
||||
h.db.Model(&silence).Updates(updates)
|
||||
h.db.First(&silence, id)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: silence})
|
||||
}
|
||||
|
||||
// DeleteSilence DELETE /alerts/silences/:id
|
||||
func (h *AlertHandler) DeleteSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.db.Delete(&model.AlertSilence{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ToggleSilence POST /alerts/silences/:id/toggle
|
||||
func (h *AlertHandler) ToggleSilence(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var silence model.AlertSilence
|
||||
if err := h.db.First(&silence, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
newStatus := 0
|
||||
if silence.Status == 0 {
|
||||
newStatus = 1
|
||||
}
|
||||
|
||||
h.db.Model(&silence).Update("status", newStatus)
|
||||
h.db.First(&silence, id)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "切换成功", Data: silence})
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -28,22 +28,30 @@ func NewApprovalHandler(db *gorm.DB) *ApprovalHandler {
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /approvals/templates [get]
|
||||
func (h *ApprovalHandler) ListTemplates(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&model.ApprovalTemplate{})
|
||||
if category := c.Query("category"); category != "" {
|
||||
db = db.Where("category = ?", category)
|
||||
}
|
||||
if qType := c.Query("type"); qType != "" {
|
||||
db = db.Where("type = ?", qType)
|
||||
}
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.ApprovalTemplate{}).Count(&total)
|
||||
db.Count(&total)
|
||||
|
||||
var templates []models.ApprovalTemplate
|
||||
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&templates)
|
||||
var templates []model.ApprovalTemplate
|
||||
db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&templates)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: templates,
|
||||
Total: total,
|
||||
},
|
||||
@@ -57,21 +65,21 @@ func (h *ApprovalHandler) ListTemplates(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "模板ID"
|
||||
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
|
||||
// @Success 200 {object} model.Response{data=model.ApprovalTemplate}
|
||||
// @Router /approvals/templates/{id} [get]
|
||||
func (h *ApprovalHandler) GetTemplate(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var template models.ApprovalTemplate
|
||||
var template model.ApprovalTemplate
|
||||
if err := h.db.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "模板不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: template,
|
||||
@@ -85,38 +93,39 @@ func (h *ApprovalHandler) GetTemplate(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateApprovalTemplateRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
|
||||
// @Param request body model.CreateApprovalTemplateRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.ApprovalTemplate}
|
||||
// @Router /approvals/templates [post]
|
||||
func (h *ApprovalHandler) CreateTemplate(c *gin.Context) {
|
||||
var req models.CreateApprovalTemplateRequest
|
||||
var req model.CreateApprovalTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
template := models.ApprovalTemplate{
|
||||
template := model.ApprovalTemplate{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
FormSchema: req.FormSchema,
|
||||
FlowConfig: req.FlowConfig,
|
||||
Category: req.Category,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: template,
|
||||
@@ -131,24 +140,24 @@ func (h *ApprovalHandler) CreateTemplate(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "模板ID"
|
||||
// @Param request body models.CreateApprovalTemplateRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
|
||||
// @Param request body model.CreateApprovalTemplateRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.ApprovalTemplate}
|
||||
// @Router /approvals/templates/{id} [put]
|
||||
func (h *ApprovalHandler) UpdateTemplate(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.CreateApprovalTemplateRequest
|
||||
var req model.CreateApprovalTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var template models.ApprovalTemplate
|
||||
var template model.ApprovalTemplate
|
||||
if err := h.db.First(&template, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "模板不存在",
|
||||
})
|
||||
@@ -164,7 +173,7 @@ func (h *ApprovalHandler) UpdateTemplate(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&template).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -173,7 +182,7 @@ func (h *ApprovalHandler) UpdateTemplate(c *gin.Context) {
|
||||
|
||||
h.db.First(&template, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: template,
|
||||
@@ -187,20 +196,20 @@ func (h *ApprovalHandler) UpdateTemplate(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "模板ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /approvals/templates/{id} [delete]
|
||||
func (h *ApprovalHandler) DeleteTemplate(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
if err := h.db.Delete(&models.ApprovalTemplate{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.ApprovalTemplate{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
+52
-30
@@ -1,39 +1,44 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListProcesses 获取审批流程列表 // Logan.Li
|
||||
func (h *ApprovalHandler) ListProcesses(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.ApprovalProcess{}).Preload("Template").Preload("ApplicantUser")
|
||||
db := h.db.Model(&model.ApprovalProcess{}).Preload("Template").Preload("ApplicantUser")
|
||||
|
||||
// 状态过滤
|
||||
if status := c.Query("status"); status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
// 按模板类别过滤
|
||||
if category := c.Query("category"); category != "" {
|
||||
db = db.Joins("JOIN sys_approval_templates ON sys_approval_templates.id = approval_processes.template_id").
|
||||
Where("sys_approval_templates.category = ?", category)
|
||||
}
|
||||
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var processes []models.ApprovalProcess
|
||||
var processes []model.ApprovalProcess
|
||||
db.Order("created_at DESC").
|
||||
Offset(query.GetOffset()).
|
||||
Limit(query.GetLimit()).
|
||||
Find(&processes)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: processes,
|
||||
Total: total,
|
||||
},
|
||||
@@ -44,16 +49,16 @@ func (h *ApprovalHandler) ListProcesses(c *gin.Context) {
|
||||
func (h *ApprovalHandler) GetProcess(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var process models.ApprovalProcess
|
||||
var process model.ApprovalProcess
|
||||
if err := h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "流程不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: process,
|
||||
@@ -62,28 +67,32 @@ func (h *ApprovalHandler) GetProcess(c *gin.Context) {
|
||||
|
||||
// CreateProcess 创建审批流程 // Logan.Li
|
||||
func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
|
||||
var req models.CreateApprovalProcessRequest
|
||||
var req model.CreateApprovalProcessRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户 ID
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
// 获取模板
|
||||
var template models.ApprovalTemplate
|
||||
var template model.ApprovalTemplate
|
||||
if err := h.db.First(&template, req.TemplateID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "模板不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
process := models.ApprovalProcess{
|
||||
process := model.ApprovalProcess{
|
||||
TemplateID: req.TemplateID,
|
||||
Title: req.Title,
|
||||
Applicant: toUint(userID),
|
||||
FormData: req.FormData,
|
||||
Status: "pending",
|
||||
CurrentStep: 0,
|
||||
@@ -91,38 +100,51 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&process).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
Message: "创建失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.Preload("Template").First(&process, process.ID)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: process,
|
||||
})
|
||||
}
|
||||
|
||||
func toUint(v interface{}) uint {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return uint(val)
|
||||
case uint:
|
||||
return val
|
||||
case int:
|
||||
return uint(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ApproveProcess 审批通过 // Logan.Li
|
||||
func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.ApproveRequest
|
||||
var req model.ApproveRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var process models.ApprovalProcess
|
||||
var process model.ApprovalProcess
|
||||
if err := h.db.First(&process, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "流程不存在",
|
||||
})
|
||||
@@ -132,14 +154,14 @@ func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
|
||||
process.Status = "approved"
|
||||
|
||||
if err := h.db.Save(&process).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "审批失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "审批通过",
|
||||
Data: process,
|
||||
@@ -150,18 +172,18 @@ func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
|
||||
func (h *ApprovalHandler) RejectProcess(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.ApproveRequest
|
||||
var req model.ApproveRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var process models.ApprovalProcess
|
||||
var process model.ApprovalProcess
|
||||
if err := h.db.First(&process, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "流程不存在",
|
||||
})
|
||||
@@ -172,14 +194,14 @@ func (h *ApprovalHandler) RejectProcess(c *gin.Context) {
|
||||
process.RejectReason = req.Comment
|
||||
|
||||
if err := h.db.Save(&process).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "拒绝失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "已拒绝",
|
||||
Data: process,
|
||||
@@ -1,11 +1,14 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -34,12 +37,12 @@ func NewAuditHandler(db *gorm.DB) *AuditHandler {
|
||||
// @Param username query string false "用户名"
|
||||
// @Param startTime query string false "开始时间"
|
||||
// @Param endTime query string false "结束时间"
|
||||
// @Success 200 {object} models.AuditLogListResponse
|
||||
// @Success 200 {object} model.AuditLogListResponse
|
||||
// @Router /api/v1/audit/logs [get]
|
||||
func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
|
||||
var req models.AuditLogQuery
|
||||
var req model.AuditLogQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := h.db.Model(&models.AuditLog{})
|
||||
query := h.db.Model(&model.AuditLog{})
|
||||
|
||||
if req.Module != "" {
|
||||
query = query.Where("module = ?", req.Module)
|
||||
@@ -73,21 +76,21 @@ func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
|
||||
// 获取总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
var logs []models.AuditLog
|
||||
var logs []model.AuditLog
|
||||
if err := query.Order("created_at DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&logs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.AuditLogListResponse{
|
||||
c.JSON(http.StatusOK, model.AuditLogListResponse{
|
||||
List: logs,
|
||||
Total: total,
|
||||
})
|
||||
@@ -100,22 +103,22 @@ func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "日志ID"
|
||||
// @Success 200 {object} models.AuditLog
|
||||
// @Success 200 {object} model.AuditLog
|
||||
// @Router /api/v1/audit/logs/{id} [get]
|
||||
func (h *AuditHandler) GetAuditLogDetail(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的日志ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日志ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var log models.AuditLog
|
||||
var log model.AuditLog
|
||||
if err := h.db.First(&log, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "日志不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "日志不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,7 +147,7 @@ func (h *AuditHandler) GetAuditModules(c *gin.Context) {
|
||||
{"value": "domain", "label": "域名管理"},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: modules}) // Logan.Li
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: modules}) // Logan.Li
|
||||
}
|
||||
|
||||
// GetAuditActions 获取审计操作类型列表
|
||||
@@ -170,33 +173,12 @@ func (h *AuditHandler) GetAuditActions(c *gin.Context) {
|
||||
{"value": "export", "label": "导出"},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: actions}) // Logan.Li
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: actions}) // Logan.Li
|
||||
}
|
||||
|
||||
// ExportAuditLogs 导出审计日志
|
||||
// @Summary 导出审计日志
|
||||
// @Description 导出审计日志为Excel或CSV
|
||||
// @Tags 审计日志
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param module query string false "模块"
|
||||
// @Param action query string false "操作类型"
|
||||
// @Param username query string false "用户名"
|
||||
// @Param startTime query string false "开始时间"
|
||||
// @Param endTime query string false "结束时间"
|
||||
// @Param format query string false "导出格式" default("csv") Enums(csv, excel)
|
||||
// @Success 200 {file} binary
|
||||
// @Router /api/v1/audit/logs/export [get]
|
||||
func (h *AuditHandler) ExportAuditLogs(c *gin.Context) {
|
||||
var req models.AuditLogQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := h.db.Model(&models.AuditLog{})
|
||||
|
||||
// buildAuditQuery 构建审计日志筛选查询(复用于列表和导出)
|
||||
func (h *AuditHandler) buildAuditQuery(req model.AuditLogQuery) *gorm.DB {
|
||||
query := h.db.Model(&model.AuditLog{})
|
||||
if req.Module != "" {
|
||||
query = query.Where("module = ?", req.Module)
|
||||
}
|
||||
@@ -212,20 +194,94 @@ func (h *AuditHandler) ExportAuditLogs(c *gin.Context) {
|
||||
if req.EndTime != "" {
|
||||
query = query.Where("created_at <= ?", req.EndTime)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
var logs []models.AuditLog
|
||||
if err := query.Order("created_at DESC").Find(&logs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "导出失败"})
|
||||
// ExportAuditLogs 导出审计日志
|
||||
// @Summary 导出审计日志
|
||||
// @Description 导出审计日志为CSV格式(带UTF-8 BOM,Excel可直接打开)
|
||||
// @Tags 审计日志
|
||||
// @Accept json
|
||||
// @Produce text/csv
|
||||
// @Param module query string false "模块"
|
||||
// @Param action query string false "操作类型"
|
||||
// @Param username query string false "用户名"
|
||||
// @Param startTime query string false "开始时间"
|
||||
// @Param endTime query string false "结束时间"
|
||||
// @Param format query string false "导出格式" default("csv") Enums(csv, json)
|
||||
// @Success 200 {file} binary
|
||||
// @Router /api/v1/audit/logs/export [get]
|
||||
func (h *AuditHandler) ExportAuditLogs(c *gin.Context) {
|
||||
var req model.AuditLogQuery
|
||||
_ = c.ShouldBindQuery(&req)
|
||||
|
||||
// 导出最多 10000 条,防止内存溢出
|
||||
query := h.buildAuditQuery(req)
|
||||
|
||||
var logs []model.AuditLog
|
||||
if err := query.Order("created_at DESC").Limit(10000).Find(&logs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "导出失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 实现CSV/Excel导出功能
|
||||
// 这里返回JSON格式的数据,实际项目中应该生成CSV或Excel文件
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "导出成功", Data: gin.H{
|
||||
format := c.DefaultQuery("format", "csv")
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
// JSON 格式(向后兼容)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "导出成功", Data: gin.H{
|
||||
"count": len(logs),
|
||||
"logs": logs,
|
||||
}}) // Logan.Li
|
||||
return
|
||||
|
||||
case "csv":
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
// CSV 格式 — 带 UTF-8 BOM 以便 Excel 正确显示中文
|
||||
var buf bytes.Buffer
|
||||
buf.WriteRune('\uFEFF') // UTF-8 BOM
|
||||
|
||||
writer := csv.NewWriter(&buf)
|
||||
_ = writer.Write([]string{"ID", "时间", "用户", "IP地址", "模块", "操作类型", "资源", "资源ID", "状态", "User-Agent", "耗时(ms)", "错误信息"})
|
||||
|
||||
for _, l := range logs {
|
||||
statusStr := "成功"
|
||||
if l.Status != 0 {
|
||||
statusStr = "失败"
|
||||
}
|
||||
_ = writer.Write([]string{
|
||||
fmt.Sprintf("%d", l.ID),
|
||||
l.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
l.Username,
|
||||
l.IP,
|
||||
l.Module,
|
||||
l.Action,
|
||||
l.Resource,
|
||||
l.ResourceID,
|
||||
statusStr,
|
||||
l.UserAgent,
|
||||
fmt.Sprintf("%d", l.Duration),
|
||||
l.Error,
|
||||
})
|
||||
}
|
||||
writer.Flush()
|
||||
|
||||
// 记录审计日志
|
||||
if uid, exists := c.Get("userID"); exists {
|
||||
if u, ok := uid.(uint); ok {
|
||||
username, _ := c.Get("username")
|
||||
un, _ := username.(string)
|
||||
h.RecordAuditLog(u, un, "audit", "export", fmt.Sprintf("导出审计日志(%d条)", len(logs)), nil)
|
||||
}
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("audit-logs-%s.csv", time.Now().Format("20060102-150405"))
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuditStatistics 获取审计统计
|
||||
@@ -250,7 +306,7 @@ func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
|
||||
Module string `json:"module"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
h.db.Model(&models.AuditLog{}).
|
||||
h.db.Model(&model.AuditLog{}).
|
||||
Select("module, COUNT(*) as count").
|
||||
Where("created_at >= ?", startTime).
|
||||
Group("module").
|
||||
@@ -261,7 +317,7 @@ func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
|
||||
Action string `json:"action"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
h.db.Model(&models.AuditLog{}).
|
||||
h.db.Model(&model.AuditLog{}).
|
||||
Select("action, COUNT(*) as count").
|
||||
Where("created_at >= ?", startTime).
|
||||
Group("action").
|
||||
@@ -272,7 +328,7 @@ func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
h.db.Model(&models.AuditLog{}).
|
||||
h.db.Model(&model.AuditLog{}).
|
||||
Select("DATE(created_at) as date, COUNT(*) as count").
|
||||
Where("created_at >= ?", startTime).
|
||||
Group("DATE(created_at)").
|
||||
@@ -281,16 +337,16 @@ func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
|
||||
|
||||
// 总记录数
|
||||
var totalCount int64
|
||||
h.db.Model(&models.AuditLog{}).Count(&totalCount)
|
||||
h.db.Model(&model.AuditLog{}).Count(&totalCount)
|
||||
|
||||
// 今日记录数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayCount int64
|
||||
h.db.Model(&models.AuditLog{}).
|
||||
h.db.Model(&model.AuditLog{}).
|
||||
Where("DATE(created_at) = ?", today).
|
||||
Count(&todayCount)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"total_count": totalCount,
|
||||
"today_count": todayCount,
|
||||
"module_stats": moduleStats,
|
||||
@@ -314,21 +370,21 @@ func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的请求参数"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析日期
|
||||
beforeDate, err := time.Parse("2006-01-02", req.BeforeDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的日期格式"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日期格式"})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除指定日期之前的日志
|
||||
result := h.db.Where("created_at < ?", beforeDate).Delete(&models.AuditLog{})
|
||||
result := h.db.Where("created_at < ?", beforeDate).Delete(&model.AuditLog{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "清理失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "清理失败"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -340,7 +396,7 @@ func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
|
||||
"deleted": result.RowsAffected,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "清理成功", Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "清理成功", Data: gin.H{
|
||||
"deleted": result.RowsAffected,
|
||||
}}) // Logan.Li
|
||||
}
|
||||
@@ -348,7 +404,7 @@ func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
|
||||
// RecordAuditLog 记录审计日志
|
||||
func (h *AuditHandler) RecordAuditLog(userID uint, username, module, action, description string, details interface{}) {
|
||||
uid := userID
|
||||
log := models.AuditLog{
|
||||
log := model.AuditLog{
|
||||
UserID: &uid,
|
||||
Username: username,
|
||||
Module: module,
|
||||
@@ -0,0 +1,130 @@
|
||||
// 终端审计实时流 Hub — 在线会话内容广播给审计查看者 // Logan.Li
|
||||
package controller
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// AuditHub 终端审计流中心 — 管理所有活跃会话的审计订阅者
|
||||
type AuditHub struct {
|
||||
mu sync.RWMutex
|
||||
watchers map[uint]map[chan []byte]struct{} // sessionID -> set of watcher channels
|
||||
}
|
||||
|
||||
// NewAuditHub 创建审计 Hub
|
||||
func NewAuditHub() *AuditHub {
|
||||
return &AuditHub{
|
||||
watchers: make(map[uint]map[chan []byte]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe 订阅某个会话的实时输出
|
||||
func (h *AuditHub) Subscribe(sessionID uint) chan []byte {
|
||||
ch := make(chan []byte, 256)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.watchers[sessionID] == nil {
|
||||
h.watchers[sessionID] = make(map[chan []byte]struct{})
|
||||
}
|
||||
h.watchers[sessionID][ch] = struct{}{}
|
||||
return ch
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅
|
||||
func (h *AuditHub) Unsubscribe(sessionID uint, ch chan []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if watchers, ok := h.watchers[sessionID]; ok {
|
||||
delete(watchers, ch)
|
||||
close(ch)
|
||||
if len(watchers) == 0 {
|
||||
delete(h.watchers, sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast 向所有订阅者广播终端输出数据(非阻塞)
|
||||
func (h *AuditHub) Broadcast(sessionID uint, data []byte) {
|
||||
h.mu.RLock()
|
||||
watchers, ok := h.watchers[sessionID]
|
||||
h.mu.RUnlock()
|
||||
if !ok || len(watchers) == 0 {
|
||||
return
|
||||
}
|
||||
for ch := range watchers {
|
||||
select {
|
||||
case ch <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup 清理某个会话的所有订阅者
|
||||
func (h *AuditHub) Cleanup(sessionID uint) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if watchers, ok := h.watchers[sessionID]; ok {
|
||||
for ch := range watchers {
|
||||
close(ch)
|
||||
}
|
||||
delete(h.watchers, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
var auditUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// ServeAuditStream 处理 /ws/audit/:sessionId WebSocket — 实时推送终端输出
|
||||
func (h *AuditHub) ServeAuditStream(c *gin.Context, sessionID uint, historyData string, sessionInfo map[string]interface{}) {
|
||||
conn, err := auditUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("[AuditStream] WS 升级失败 session=%d: %v", sessionID, err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送会话元信息
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "session_info",
|
||||
"payload": sessionInfo,
|
||||
})
|
||||
|
||||
// 发送历史数据(已录制的输出)
|
||||
if historyData != "" {
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
"type": "history",
|
||||
"data": historyData,
|
||||
})
|
||||
}
|
||||
|
||||
// 订阅实时流
|
||||
ch := h.Subscribe(sessionID)
|
||||
defer h.Unsubscribe(sessionID, ch)
|
||||
|
||||
// goroutine 读取关闭信号
|
||||
go func() {
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 推送实时数据
|
||||
for data := range ch {
|
||||
if err := conn.WriteJSON(map[string]interface{}{
|
||||
"type": "output",
|
||||
"data": string(data),
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+50
-50
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/remoteexec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -20,22 +20,22 @@ import (
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /cron-jobs [get]
|
||||
func (h *AutomationHandler) ListCronJobs(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.CronJob{}).Count(&total)
|
||||
h.db.Model(&model.CronJob{}).Count(&total)
|
||||
|
||||
var jobs []models.CronJob
|
||||
var jobs []model.CronJob
|
||||
h.db.Preload("Script").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&jobs)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: jobs,
|
||||
Total: total,
|
||||
},
|
||||
@@ -49,21 +49,21 @@ func (h *AutomationHandler) ListCronJobs(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} models.Response{data=models.CronJob}
|
||||
// @Success 200 {object} model.Response{data=model.CronJob}
|
||||
// @Router /cron-jobs/{id} [get]
|
||||
func (h *AutomationHandler) GetCronJob(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var job models.CronJob
|
||||
var job model.CronJob
|
||||
if err := h.db.Preload("Script").First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "定时任务不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: job,
|
||||
@@ -77,20 +77,20 @@ func (h *AutomationHandler) GetCronJob(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateCronJobRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.CronJob}
|
||||
// @Param request body model.CreateCronJobRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.CronJob}
|
||||
// @Router /cron-jobs [post]
|
||||
func (h *AutomationHandler) CreateCronJob(c *gin.Context) {
|
||||
var req models.CreateCronJobRequest
|
||||
var req model.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
job := models.CronJob{
|
||||
job := model.CronJob{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
CronExpr: req.CronExpr,
|
||||
@@ -111,14 +111,14 @@ func (h *AutomationHandler) CreateCronJob(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&job).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: job,
|
||||
@@ -133,24 +133,24 @@ func (h *AutomationHandler) CreateCronJob(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "任务ID"
|
||||
// @Param request body models.CreateCronJobRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.CronJob}
|
||||
// @Param request body model.CreateCronJobRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.CronJob}
|
||||
// @Router /cron-jobs/{id} [put]
|
||||
func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.CreateCronJobRequest
|
||||
var req model.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var job models.CronJob
|
||||
var job model.CronJob
|
||||
if err := h.db.First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "定时任务不存在",
|
||||
})
|
||||
@@ -170,7 +170,7 @@ func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&job).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -179,7 +179,7 @@ func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
|
||||
|
||||
h.db.Preload("Script").First(&job, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: job,
|
||||
@@ -193,20 +193,20 @@ func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.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{
|
||||
if err := h.db.Delete(&model.CronJob{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -219,28 +219,28 @@ func (h *AutomationHandler) DeleteCronJob(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /cron-jobs/batch-delete [delete]
|
||||
func (h *AutomationHandler) BatchDeleteCronJobs(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.CronJob{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.CronJob{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -253,14 +253,14 @@ func (h *AutomationHandler) BatchDeleteCronJobs(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} models.Response{data=models.CronJob}
|
||||
// @Success 200 {object} model.Response{data=model.CronJob}
|
||||
// @Router /cron-jobs/{id}/toggle [post]
|
||||
func (h *AutomationHandler) ToggleCronJob(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var job models.CronJob
|
||||
var job model.CronJob
|
||||
if err := h.db.First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "定时任务不存在",
|
||||
})
|
||||
@@ -269,14 +269,14 @@ func (h *AutomationHandler) ToggleCronJob(c *gin.Context) {
|
||||
|
||||
job.Enabled = !job.Enabled
|
||||
if err := h.db.Save(&job).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "操作失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "操作成功",
|
||||
Data: job,
|
||||
@@ -290,14 +290,14 @@ func (h *AutomationHandler) ToggleCronJob(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "任务ID"
|
||||
// @Success 200 {object} models.Response{data=models.TaskExecution}
|
||||
// @Success 200 {object} model.Response{data=model.TaskExecution}
|
||||
// @Router /cron-jobs/{id}/execute [post]
|
||||
func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var job models.CronJob
|
||||
var job model.CronJob
|
||||
if err := h.db.First(&job, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "定时任务不存在",
|
||||
})
|
||||
@@ -311,7 +311,7 @@ func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
|
||||
scriptType = "shell" // one-liner commands run via bash
|
||||
}
|
||||
if job.Type == "script" && job.ScriptID != nil {
|
||||
var script models.Script
|
||||
var script model.Script
|
||||
if err := h.db.First(&script, *job.ScriptID).Error; err == nil {
|
||||
content = script.Content
|
||||
scriptType = script.Type
|
||||
@@ -320,7 +320,7 @@ func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
execution := models.TaskExecution{
|
||||
execution := model.TaskExecution{
|
||||
TaskType: "cron",
|
||||
TaskID: uint(id),
|
||||
TaskName: job.Name,
|
||||
@@ -330,7 +330,7 @@ func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
|
||||
StartedAt: &now,
|
||||
}
|
||||
if err := h.db.Create(&execution).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建执行记录失败",
|
||||
})
|
||||
@@ -403,7 +403,7 @@ func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
|
||||
if status == "failed" {
|
||||
msg = "执行失败"
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: msg,
|
||||
Data: execution,
|
||||
+52
-52
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -18,22 +18,22 @@ import (
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /scripts [get]
|
||||
func (h *AutomationHandler) ListScripts(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.Script{}).Count(&total)
|
||||
h.db.Model(&model.Script{}).Count(&total)
|
||||
|
||||
var scripts []models.Script
|
||||
var scripts []model.Script
|
||||
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&scripts)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: scripts,
|
||||
Total: total,
|
||||
},
|
||||
@@ -47,21 +47,21 @@ func (h *AutomationHandler) ListScripts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Success 200 {object} models.Response{data=models.Script}
|
||||
// @Success 200 {object} model.Response{data=model.Script}
|
||||
// @Router /scripts/{id} [get]
|
||||
func (h *AutomationHandler) GetScript(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var script models.Script
|
||||
var script model.Script
|
||||
if err := h.db.First(&script, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "脚本不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: script,
|
||||
@@ -75,20 +75,20 @@ func (h *AutomationHandler) GetScript(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateScriptRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.Script}
|
||||
// @Param request body model.CreateScriptRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.Script}
|
||||
// @Router /scripts [post]
|
||||
func (h *AutomationHandler) CreateScript(c *gin.Context) {
|
||||
var req models.CreateScriptRequest
|
||||
var req model.CreateScriptRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
script := models.Script{
|
||||
script := model.Script{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
@@ -106,14 +106,14 @@ func (h *AutomationHandler) CreateScript(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&script).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: script,
|
||||
@@ -128,24 +128,24 @@ func (h *AutomationHandler) CreateScript(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Param request body models.CreateScriptRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.Script}
|
||||
// @Param request body model.CreateScriptRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.Script}
|
||||
// @Router /scripts/{id} [put]
|
||||
func (h *AutomationHandler) UpdateScript(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.CreateScriptRequest
|
||||
var req model.CreateScriptRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var script models.Script
|
||||
var script model.Script
|
||||
if err := h.db.First(&script, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "脚本不存在",
|
||||
})
|
||||
@@ -163,7 +163,7 @@ func (h *AutomationHandler) UpdateScript(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&script).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -172,7 +172,7 @@ func (h *AutomationHandler) UpdateScript(c *gin.Context) {
|
||||
|
||||
h.db.First(&script, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: script,
|
||||
@@ -186,20 +186,20 @@ func (h *AutomationHandler) UpdateScript(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.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{
|
||||
if err := h.db.Delete(&model.Script{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -212,28 +212,28 @@ func (h *AutomationHandler) DeleteScript(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /scripts/batch-delete [post]
|
||||
func (h *AutomationHandler) BatchDeleteScripts(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.Script{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.Script{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -247,15 +247,15 @@ func (h *AutomationHandler) BatchDeleteScripts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Param request body models.ExecuteScriptRequest true "执行请求"
|
||||
// @Success 200 {object} models.Response{data=models.TaskExecution}
|
||||
// @Param request body model.ExecuteScriptRequest true "执行请求"
|
||||
// @Success 200 {object} model.Response{data=model.TaskExecution}
|
||||
// @Router /scripts/{id}/execute [post]
|
||||
func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var script models.Script
|
||||
var script model.Script
|
||||
if err := h.db.First(&script, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "脚本不存在",
|
||||
})
|
||||
@@ -263,11 +263,11 @@ func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||||
}
|
||||
|
||||
// The request body is optional — when omitted we execute locally.
|
||||
var req models.ExecuteScriptRequest
|
||||
var req model.ExecuteScriptRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
now := time.Now()
|
||||
execution := models.TaskExecution{
|
||||
execution := model.TaskExecution{
|
||||
TaskType: "script",
|
||||
TaskID: uint(id),
|
||||
TaskName: script.Name,
|
||||
@@ -276,7 +276,7 @@ func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||||
StartedAt: &now,
|
||||
}
|
||||
if err := h.db.Create(&execution).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建执行记录失败",
|
||||
})
|
||||
@@ -308,7 +308,7 @@ func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||||
if status == "failed" {
|
||||
msg = "执行失败"
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: msg,
|
||||
Data: execution,
|
||||
@@ -325,13 +325,13 @@ func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
|
||||
// @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}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /task-executions [get]
|
||||
func (h *AutomationHandler) ListExecutions(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.TaskExecution{})
|
||||
db := h.db.Model(&model.TaskExecution{})
|
||||
if taskType := c.Query("task_type"); taskType != "" {
|
||||
db = db.Where("task_type = ?", taskType)
|
||||
}
|
||||
@@ -342,13 +342,13 @@ func (h *AutomationHandler) ListExecutions(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var execs []models.TaskExecution
|
||||
var execs []model.TaskExecution
|
||||
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&execs)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: execs,
|
||||
Total: total,
|
||||
},
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -23,9 +23,9 @@ func NewBillingHandler(db *gorm.DB) *BillingHandler {
|
||||
|
||||
// ListBillings 获取账单列表
|
||||
func (h *BillingHandler) ListBillings(c *gin.Context) {
|
||||
var billings []models.CloudBilling
|
||||
var billings []model.CloudBilling
|
||||
|
||||
query := h.db.Model(&models.CloudBilling{})
|
||||
query := h.db.Model(&model.CloudBilling{})
|
||||
|
||||
// 筛选
|
||||
if provider := c.Query("provider"); provider != "" {
|
||||
@@ -64,10 +64,10 @@ func (h *BillingHandler) ListBillings(c *gin.Context) {
|
||||
query.Count(&total)
|
||||
query.Offset((page - 1) * pageSize).Limit(pageSize).Find(&billings)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: billings,
|
||||
Total: total,
|
||||
},
|
||||
@@ -76,13 +76,13 @@ func (h *BillingHandler) ListBillings(c *gin.Context) {
|
||||
|
||||
// CreateBilling 创建账单记录
|
||||
func (h *BillingHandler) CreateBilling(c *gin.Context) {
|
||||
var req models.CreateBillingRequest
|
||||
var req model.CreateBillingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
billing := models.CloudBilling{
|
||||
billing := model.CloudBilling{
|
||||
CloudAccountID: req.CloudAccountID,
|
||||
AccountName: req.AccountName,
|
||||
Provider: req.Provider,
|
||||
@@ -98,11 +98,11 @@ func (h *BillingHandler) CreateBilling(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&billing).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: billing,
|
||||
@@ -112,16 +112,16 @@ func (h *BillingHandler) CreateBilling(c *gin.Context) {
|
||||
// BatchCreateBillings 批量创建账单
|
||||
func (h *BillingHandler) BatchCreateBillings(c *gin.Context) {
|
||||
var req struct {
|
||||
Records []models.CreateBillingRequest `json:"records" binding:"required"`
|
||||
Records []model.CreateBillingRequest `json:"records" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
created := 0
|
||||
for _, r := range req.Records {
|
||||
billing := models.CloudBilling{
|
||||
billing := model.CloudBilling{
|
||||
CloudAccountID: r.CloudAccountID,
|
||||
AccountName: r.AccountName,
|
||||
Provider: r.Provider,
|
||||
@@ -140,7 +140,7 @@ func (h *BillingHandler) BatchCreateBillings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("成功导入 %d 条记录", created),
|
||||
Data: gin.H{"created": created, "total": len(req.Records)},
|
||||
@@ -150,11 +150,11 @@ func (h *BillingHandler) BatchCreateBillings(c *gin.Context) {
|
||||
// DeleteBilling 删除账单记录
|
||||
func (h *BillingHandler) DeleteBilling(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.CloudBilling{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.CloudBilling{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// GetBillingStats 获取账单统计
|
||||
@@ -165,26 +165,26 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
yearStart := fmt.Sprintf("%d-01-01", now.Year())
|
||||
|
||||
stats := models.BillingStats{
|
||||
MonthTrend: []models.DailyCost{},
|
||||
ProviderDist: []models.ProviderCost{},
|
||||
Alerts: []models.BillingAlert{},
|
||||
stats := model.BillingStats{
|
||||
MonthTrend: []model.DailyCost{},
|
||||
ProviderDist: []model.ProviderCost{},
|
||||
Alerts: []model.BillingAlert{},
|
||||
}
|
||||
|
||||
// 今日成本
|
||||
h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", today).
|
||||
h.db.Model(&model.CloudBilling{}).Where("billing_date = ?", today).
|
||||
Select("COALESCE(SUM(cost), 0)").Scan(&stats.TodayCost)
|
||||
|
||||
// 昨日成本
|
||||
h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", yesterday).
|
||||
h.db.Model(&model.CloudBilling{}).Where("billing_date = ?", yesterday).
|
||||
Select("COALESCE(SUM(cost), 0)").Scan(&stats.YesterdayCost)
|
||||
|
||||
// 本月成本
|
||||
h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", monthStart).
|
||||
h.db.Model(&model.CloudBilling{}).Where("billing_date >= ?", monthStart).
|
||||
Select("COALESCE(SUM(cost), 0)").Scan(&stats.MonthCost)
|
||||
|
||||
// 本年成本
|
||||
h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", yearStart).
|
||||
h.db.Model(&model.CloudBilling{}).Where("billing_date >= ?", yearStart).
|
||||
Select("COALESCE(SUM(cost), 0)").Scan(&stats.YearCost)
|
||||
|
||||
// 近30天趋势
|
||||
@@ -193,7 +193,7 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
Total float64
|
||||
}
|
||||
var trends []dayCost
|
||||
h.db.Model(&models.CloudBilling{}).
|
||||
h.db.Model(&model.CloudBilling{}).
|
||||
Select("billing_date, SUM(cost) as total").
|
||||
Where("billing_date >= ?", now.AddDate(0, 0, -29).Format("2006-01-02")).
|
||||
Group("billing_date").
|
||||
@@ -201,7 +201,7 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
Scan(&trends)
|
||||
|
||||
for _, t := range trends {
|
||||
stats.MonthTrend = append(stats.MonthTrend, models.DailyCost{Date: t.BillingDate, Cost: t.Total})
|
||||
stats.MonthTrend = append(stats.MonthTrend, model.DailyCost{Date: t.BillingDate, Cost: t.Total})
|
||||
}
|
||||
|
||||
// 本月厂商成本分布
|
||||
@@ -210,7 +210,7 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
Total float64
|
||||
}
|
||||
var provCosts []provCost
|
||||
h.db.Model(&models.CloudBilling{}).
|
||||
h.db.Model(&model.CloudBilling{}).
|
||||
Select("provider, SUM(cost) as total").
|
||||
Where("billing_date >= ?", monthStart).
|
||||
Group("provider").
|
||||
@@ -225,7 +225,7 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
if totalProviderCost > 0 {
|
||||
percent = p.Total / totalProviderCost * 100
|
||||
}
|
||||
stats.ProviderDist = append(stats.ProviderDist, models.ProviderCost{
|
||||
stats.ProviderDist = append(stats.ProviderDist, model.ProviderCost{
|
||||
Provider: p.Provider,
|
||||
Cost: p.Total,
|
||||
Percent: percent,
|
||||
@@ -238,7 +238,7 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
// 检查预警规则
|
||||
h.checkBillingAlerts(&stats, now)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: stats,
|
||||
@@ -246,8 +246,8 @@ func (h *BillingHandler) GetBillingStats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// checkBillingAlerts 检查预警规则是否触发
|
||||
func (h *BillingHandler) checkBillingAlerts(stats *models.BillingStats, now time.Time) {
|
||||
var rules []models.BillingAlertRule
|
||||
func (h *BillingHandler) checkBillingAlerts(stats *model.BillingStats, now time.Time) {
|
||||
var rules []model.BillingAlertRule
|
||||
h.db.Where("enabled = ?", true).Find(&rules)
|
||||
|
||||
for _, rule := range rules {
|
||||
@@ -255,7 +255,7 @@ func (h *BillingHandler) checkBillingAlerts(stats *models.BillingStats, now time
|
||||
|
||||
if rule.Metric == "daily" {
|
||||
// 查今日该厂商/账号的成本
|
||||
q := h.db.Model(&models.CloudBilling{}).Where("billing_date = ?", now.Format("2006-01-02"))
|
||||
q := h.db.Model(&model.CloudBilling{}).Where("billing_date = ?", now.Format("2006-01-02"))
|
||||
if rule.Provider != "" {
|
||||
q = q.Where("provider = ?", rule.Provider)
|
||||
}
|
||||
@@ -265,7 +265,7 @@ func (h *BillingHandler) checkBillingAlerts(stats *models.BillingStats, now time
|
||||
q.Select("COALESCE(SUM(cost), 0)").Scan(¤t)
|
||||
} else {
|
||||
// monthly - 本月累计
|
||||
q := h.db.Model(&models.CloudBilling{}).Where("billing_date >= ?", now.Format("2006-01")+"-01")
|
||||
q := h.db.Model(&model.CloudBilling{}).Where("billing_date >= ?", now.Format("2006-01")+"-01")
|
||||
if rule.Provider != "" {
|
||||
q = q.Where("provider = ?", rule.Provider)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func (h *BillingHandler) checkBillingAlerts(stats *models.BillingStats, now time
|
||||
if providerLabel == "" {
|
||||
providerLabel = "全部"
|
||||
}
|
||||
stats.Alerts = append(stats.Alerts, models.BillingAlert{
|
||||
stats.Alerts = append(stats.Alerts, model.BillingAlert{
|
||||
RuleID: rule.ID,
|
||||
RuleName: rule.Name,
|
||||
Provider: providerLabel,
|
||||
+23
-23
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
// ListAlertRules 获取预警规则列表 // Logan.Li
|
||||
func (h *BillingHandler) ListAlertRules(c *gin.Context) {
|
||||
var rules []models.BillingAlertRule
|
||||
var rules []model.BillingAlertRule
|
||||
h.db.Order("created_at DESC").Find(&rules)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: rules,
|
||||
Total: int64(len(rules)),
|
||||
},
|
||||
@@ -29,13 +29,13 @@ func (h *BillingHandler) ListAlertRules(c *gin.Context) {
|
||||
|
||||
// CreateAlertRule 创建预警规则 // Logan.Li
|
||||
func (h *BillingHandler) CreateAlertRule(c *gin.Context) {
|
||||
var req models.CreateBillingAlertRequest
|
||||
var req model.CreateBillingAlertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
rule := models.BillingAlertRule{
|
||||
rule := model.BillingAlertRule{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
CloudAccountID: req.CloudAccountID,
|
||||
@@ -52,11 +52,11 @@ func (h *BillingHandler) CreateAlertRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&rule).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: rule,
|
||||
@@ -66,15 +66,15 @@ func (h *BillingHandler) CreateAlertRule(c *gin.Context) {
|
||||
// UpdateAlertRule 更新预警规则 // Logan.Li
|
||||
func (h *BillingHandler) UpdateAlertRule(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var rule models.BillingAlertRule
|
||||
var rule model.BillingAlertRule
|
||||
if err := h.db.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateBillingAlertRequest
|
||||
var req model.CreateBillingAlertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (h *BillingHandler) UpdateAlertRule(c *gin.Context) {
|
||||
|
||||
h.db.Save(&rule)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: rule,
|
||||
@@ -104,19 +104,19 @@ func (h *BillingHandler) UpdateAlertRule(c *gin.Context) {
|
||||
// DeleteAlertRule 删除预警规则 // Logan.Li
|
||||
func (h *BillingHandler) DeleteAlertRule(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.BillingAlertRule{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.BillingAlertRule{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ToggleAlertRule 启用/禁用预警规则(服务端自动翻转) // Logan.Li
|
||||
func (h *BillingHandler) ToggleAlertRule(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var rule models.BillingAlertRule
|
||||
var rule model.BillingAlertRule
|
||||
if err := h.db.First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "规则不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "规则不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func (h *BillingHandler) ToggleAlertRule(c *gin.Context) {
|
||||
status = "已禁用"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: status,
|
||||
Data: rule,
|
||||
@@ -138,14 +138,14 @@ func (h *BillingHandler) ToggleAlertRule(c *gin.Context) {
|
||||
// GetProviderOptions 获取厂商选项(用于筛选下拉) // Logan.Li
|
||||
func (h *BillingHandler) GetProviderOptions(c *gin.Context) {
|
||||
var providers []string
|
||||
h.db.Model(&models.CloudBilling{}).Distinct("provider").Where("provider != ''").Pluck("provider", &providers)
|
||||
h.db.Model(&model.CloudBilling{}).Distinct("provider").Where("provider != ''").Pluck("provider", &providers)
|
||||
|
||||
result := make([]gin.H, 0, len(providers))
|
||||
for _, p := range providers {
|
||||
result = append(result, gin.H{"value": p, "label": formatProvider(p)})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: result,
|
||||
+22
-22
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -27,16 +27,16 @@ func NewCloudAccountHandler(db *gorm.DB) *CloudAccountHandler {
|
||||
// @Tags 云账号
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /cloud-accounts [get]
|
||||
func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) {
|
||||
var accounts []models.CloudAccount
|
||||
var accounts []model.CloudAccount
|
||||
h.db.Find(&accounts)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: accounts,
|
||||
Total: int64(len(accounts)),
|
||||
},
|
||||
@@ -61,12 +61,12 @@ type createCloudAccountRequest struct {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body createCloudAccountRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.CloudAccount}
|
||||
// @Success 200 {object} model.Response{data=model.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{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -74,7 +74,7 @@ func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Provider) == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "账号名称和云平台不能为空",
|
||||
})
|
||||
@@ -86,7 +86,7 @@ func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
account := models.CloudAccount{
|
||||
account := model.CloudAccount{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Provider: strings.TrimSpace(req.Provider),
|
||||
AccessKey: req.AccessKey,
|
||||
@@ -96,14 +96,14 @@ func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&account).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: account,
|
||||
@@ -119,22 +119,22 @@ func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "云账号ID"
|
||||
// @Param request body createCloudAccountRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.CloudAccount}
|
||||
// @Success 200 {object} model.Response{data=model.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{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var account models.CloudAccount
|
||||
var account model.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "云账号不存在",
|
||||
})
|
||||
@@ -160,14 +160,14 @@ func (h *CloudAccountHandler) UpdateCloudAccount(c *gin.Context) {
|
||||
account.MonthlyCost = req.MonthlyCost
|
||||
|
||||
if err := h.db.Save(&account).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: account,
|
||||
@@ -181,20 +181,20 @@ func (h *CloudAccountHandler) UpdateCloudAccount(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "云账号ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.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{
|
||||
if err := h.db.Delete(&model.CloudAccount{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
+25
-25
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -31,13 +31,13 @@ func NewDomainHandler(db *gorm.DB) *DomainHandler {
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /domains [get]
|
||||
func (h *DomainHandler) ListDomains(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.Domain{})
|
||||
db := h.db.Model(&model.Domain{})
|
||||
if query.Keyword != "" {
|
||||
db = db.Where("name LIKE ?", "%"+query.Keyword+"%")
|
||||
}
|
||||
@@ -45,13 +45,13 @@ func (h *DomainHandler) ListDomains(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var domains []models.Domain
|
||||
var domains []model.Domain
|
||||
db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&domains)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: domains,
|
||||
Total: total,
|
||||
},
|
||||
@@ -65,13 +65,13 @@ func (h *DomainHandler) ListDomains(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.Domain true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.Domain}
|
||||
// @Param request body model.Domain true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.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{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -79,7 +79,7 @@ func (h *DomainHandler) CreateDomain(c *gin.Context) {
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "域名不能为空",
|
||||
})
|
||||
@@ -91,7 +91,7 @@ func (h *DomainHandler) CreateDomain(c *gin.Context) {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
domain := models.Domain{
|
||||
domain := model.Domain{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Registrar: req.Registrar,
|
||||
Status: status,
|
||||
@@ -103,14 +103,14 @@ func (h *DomainHandler) CreateDomain(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&domain).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: domain,
|
||||
@@ -149,20 +149,20 @@ func parseDateStr(s string) *time.Time {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "域名ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.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{
|
||||
if err := h.db.Delete(&model.Domain{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -177,22 +177,22 @@ func (h *DomainHandler) DeleteDomain(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "域名ID"
|
||||
// @Param request body updateDomainRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.Domain}
|
||||
// @Success 200 {object} model.Response{data=model.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{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var domain models.Domain
|
||||
var domain model.Domain
|
||||
if err := h.db.First(&domain, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "域名不存在",
|
||||
})
|
||||
@@ -214,14 +214,14 @@ func (h *DomainHandler) UpdateDomain(c *gin.Context) {
|
||||
domain.SSLExpireDate = parseDateStr(req.SSLExpireDate)
|
||||
|
||||
if err := h.db.Save(&domain).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: domain,
|
||||
+18
-18
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -34,13 +34,13 @@ type platformRequest struct {
|
||||
|
||||
// ListPlatforms 获取云平台列表
|
||||
func (h *PlatformHandler) ListPlatforms(c *gin.Context) {
|
||||
var platforms []models.Cloud
|
||||
var platforms []model.Cloud
|
||||
h.db.Find(&platforms)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: platforms,
|
||||
Total: int64(len(platforms)),
|
||||
},
|
||||
@@ -51,11 +51,11 @@ func (h *PlatformHandler) ListPlatforms(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(http.StatusBadRequest, model.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: "平台名称和编码不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "平台名称和编码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
platform := models.Cloud{
|
||||
platform := model.Cloud{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Code: strings.TrimSpace(req.Code),
|
||||
Type: cloudType,
|
||||
@@ -77,10 +77,10 @@ func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := h.db.Create(&platform).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: platform})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: platform})
|
||||
}
|
||||
|
||||
// UpdatePlatform 更新云平台
|
||||
@@ -88,13 +88,13 @@ 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()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var platform models.Cloud
|
||||
var platform model.Cloud
|
||||
if err := h.db.First(&platform, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云平台不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "云平台不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,19 +114,19 @@ func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
|
||||
platform.Description = req.Description
|
||||
|
||||
if err := h.db.Save(&platform).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: platform})
|
||||
c.JSON(http.StatusOK, model.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: "删除失败"})
|
||||
if err := h.db.Delete(&model.Cloud{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
+25
-25
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -34,13 +34,13 @@ type regionRequest struct {
|
||||
|
||||
// ListRegions 获取区域列表
|
||||
func (h *RegionHandler) ListRegions(c *gin.Context) {
|
||||
var regions []models.Region
|
||||
var regions []model.Region
|
||||
h.db.Find(®ions)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: regions,
|
||||
Total: int64(len(regions)),
|
||||
},
|
||||
@@ -51,11 +51,11 @@ func (h *RegionHandler) ListRegions(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(http.StatusBadRequest, model.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: "区域名称和编码不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "区域名称和编码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (h *RegionHandler) CreateRegion(c *gin.Context) {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
region := models.Region{
|
||||
region := model.Region{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Code: strings.TrimSpace(req.Code),
|
||||
Provider: strings.TrimSpace(req.Provider),
|
||||
@@ -73,10 +73,10 @@ func (h *RegionHandler) CreateRegion(c *gin.Context) {
|
||||
Endpoint: req.Endpoint,
|
||||
}
|
||||
if err := h.db.Create(®ion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: region})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: region})
|
||||
}
|
||||
|
||||
// UpdateRegion 更新区域
|
||||
@@ -84,13 +84,13 @@ 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()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var region models.Region
|
||||
var region model.Region
|
||||
if err := h.db.First(®ion, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "区域不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "区域不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,20 +110,20 @@ func (h *RegionHandler) UpdateRegion(c *gin.Context) {
|
||||
region.Endpoint = req.Endpoint
|
||||
|
||||
if err := h.db.Save(®ion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: region})
|
||||
c.JSON(http.StatusOK, model.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: "删除失败"})
|
||||
if err := h.db.Delete(&model.Region{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteRegions 批量删除区域 // Logan.Li
|
||||
@@ -133,19 +133,19 @@ func (h *RegionHandler) DeleteRegion(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /regions/batch-delete [delete]
|
||||
func (h *RegionHandler) BatchDeleteRegions(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.Region{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.Region{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/cloud"
|
||||
"ezops/internal/models"
|
||||
"ezops/cloud"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -26,36 +26,36 @@ func NewCloudSyncHandler(db *gorm.DB) *CloudSyncHandler {
|
||||
func (h *CloudSyncHandler) SyncRegions(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
var account model.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
if account.Status != "active" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "云账号未激活,无法同步"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "云账号未激活,无法同步"})
|
||||
return
|
||||
}
|
||||
|
||||
provider := cloud.NewProvider(account.Provider, account.AccessKey, account.SecretKey)
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "暂不支持的云平台: " + account.Provider})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "暂不支持的云平台: " + account.Provider})
|
||||
return
|
||||
}
|
||||
|
||||
cloudRegions, err := provider.ListRegions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "同步区域失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "同步区域失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert: 不存在则创建,存在则更新
|
||||
synced := 0
|
||||
for _, cr := range cloudRegions {
|
||||
var region models.Region
|
||||
var region model.Region
|
||||
result := h.db.Where("code = ? AND provider = ?", cr.RegionID, account.Provider).First(®ion)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// 新区域 → 创建
|
||||
h.db.Create(&models.Region{
|
||||
h.db.Create(&model.Region{
|
||||
Name: cr.LocalName,
|
||||
Code: cr.RegionID,
|
||||
Provider: account.Provider,
|
||||
@@ -77,7 +77,7 @@ func (h *CloudSyncHandler) SyncRegions(c *gin.Context) {
|
||||
"last_sync_at": time.Now(),
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "同步成功",
|
||||
Data: gin.H{
|
||||
@@ -92,19 +92,19 @@ func (h *CloudSyncHandler) SyncRegions(c *gin.Context) {
|
||||
func (h *CloudSyncHandler) GetAccountRegions(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
var account model.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var regions []models.Region
|
||||
var regions []model.Region
|
||||
h.db.Where("provider = ?", account.Provider).Order("name ASC").Find(®ions)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: regions,
|
||||
Total: int64(len(regions)),
|
||||
},
|
||||
@@ -121,25 +121,25 @@ type discoverRequest struct {
|
||||
func (h *CloudSyncHandler) DiscoverInstances(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
var account model.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req discoverRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
if len(req.Regions) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请至少选择一个区域"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请至少选择一个区域"})
|
||||
return
|
||||
}
|
||||
|
||||
provider := cloud.NewProvider(account.Provider, account.AccessKey, account.SecretKey)
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "暂不支持的云平台"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "暂不支持的云平台"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func (h *CloudSyncHandler) DiscoverInstances(c *gin.Context) {
|
||||
allInstances = append(allInstances, instances...)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
@@ -176,15 +176,15 @@ type importRequest struct {
|
||||
func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var account models.CloudAccount
|
||||
var account model.CloudAccount
|
||||
if err := h.db.First(&account, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "云账号不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "云账号不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req importRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,14 +203,14 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
||||
|
||||
// 检查是否已存在(按 IP 或 hostname)
|
||||
var count int64
|
||||
h.db.Model(&models.Server{}).Where("ip = ? OR hostname = ?", ip, inst.Hostname).Count(&count)
|
||||
h.db.Model(&model.Server{}).Where("ip = ? OR hostname = ?", ip, inst.Hostname).Count(&count)
|
||||
if count > 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找 Region 关联
|
||||
var region models.Region
|
||||
var region model.Region
|
||||
var regionID *uint
|
||||
if h.db.Where("code = ? AND provider = ?", inst.RegionID, account.Provider).First(®ion).Error == nil {
|
||||
regionID = ®ion.ID
|
||||
@@ -241,7 +241,7 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
||||
|
||||
// 注意:servers 表的 cloud_id 外键引用的是 clouds 表(非 cloud_accounts),
|
||||
// region_id 外键引用 regions 表。未找到关联时设为 NULL 避免外键约束失败。
|
||||
server := models.Server{
|
||||
server := model.Server{
|
||||
Hostname: hostname,
|
||||
IP: ipAddr,
|
||||
PublicIP: inst.PublicIP,
|
||||
@@ -266,7 +266,7 @@ func (h *CloudSyncHandler) ImportInstances(c *gin.Context) {
|
||||
// 更新账号的 server_count
|
||||
h.db.Model(&account).UpdateColumn("server_count", imported)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "导入完成",
|
||||
Data: gin.H{
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/crypto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -34,7 +34,7 @@ func genEnvPrefix() string {
|
||||
return hex.EncodeToString(b)[:8]
|
||||
}
|
||||
|
||||
func buildContent(req models.CreateSecretRequest) (string, string, error) {
|
||||
func buildContent(req model.CreateSecretRequest) (string, string, error) {
|
||||
switch req.SecretType {
|
||||
case "ssh":
|
||||
data := map[string]interface{}{
|
||||
@@ -93,10 +93,10 @@ func buildContent(req models.CreateSecretRequest) (string, string, error) {
|
||||
// ────── List ──────
|
||||
|
||||
func (h *CredentialHandler) ListCredentials(c *gin.Context) {
|
||||
var query models.SecretListQuery
|
||||
var query model.SecretListQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.Credential{})
|
||||
db := h.db.Model(&model.Credential{})
|
||||
if query.SecretType != "" {
|
||||
db = db.Where("secret_type = ?", query.SecretType)
|
||||
}
|
||||
@@ -121,16 +121,16 @@ func (h *CredentialHandler) ListCredentials(c *gin.Context) {
|
||||
pageSize := query.PageSize
|
||||
if pageSize < 1 || pageSize > 100 { pageSize = 20 }
|
||||
|
||||
var creds []models.Credential
|
||||
var creds []model.Credential
|
||||
db.Order("updated_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&creds)
|
||||
|
||||
items := make([]models.SecretListResponse, 0, len(creds))
|
||||
items := make([]model.SecretListResponse, 0, len(creds))
|
||||
for _, cred := range creds {
|
||||
tags := []string{}
|
||||
if cred.Tags != "" {
|
||||
json.Unmarshal([]byte(cred.Tags), &tags)
|
||||
}
|
||||
items = append(items, models.SecretListResponse{
|
||||
items = append(items, model.SecretListResponse{
|
||||
ID: cred.ID,
|
||||
Name: cred.Name,
|
||||
VariableName: cred.VariableName,
|
||||
@@ -146,7 +146,7 @@ func (h *CredentialHandler) ListCredentials(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{
|
||||
List: items, Total: total,
|
||||
}})
|
||||
}
|
||||
@@ -154,21 +154,21 @@ func (h *CredentialHandler) ListCredentials(c *gin.Context) {
|
||||
// ────── Create ──────
|
||||
|
||||
func (h *CredentialHandler) CreateCredential(c *gin.Context) {
|
||||
var req models.CreateSecretRequest
|
||||
var req model.CreateSecretRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
plainContent, summary, err := buildContent(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
encrypted, err := h.crypt.Encrypt(plainContent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (h *CredentialHandler) CreateCredential(c *gin.Context) {
|
||||
isActive := true
|
||||
if req.IsActive != nil { isActive = *req.IsActive }
|
||||
|
||||
cred := models.Credential{
|
||||
cred := model.Credential{
|
||||
Name: req.Name,
|
||||
VariableName: req.VariableName,
|
||||
SecretType: req.SecretType,
|
||||
@@ -191,27 +191,27 @@ func (h *CredentialHandler) CreateCredential(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&cred).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: cred.ID})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: cred.ID})
|
||||
}
|
||||
|
||||
// ────── Get Detail ──────
|
||||
|
||||
func (h *CredentialHandler) GetCredential(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var cred models.Credential
|
||||
var cred model.Credential
|
||||
if h.db.First(&cred, id).Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "凭据不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt content
|
||||
plain, err := h.crypt.Decrypt(cred.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "解密失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "解密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ func (h *CredentialHandler) GetCredential(c *gin.Context) {
|
||||
tags := []string{}
|
||||
if cred.Tags != "" { json.Unmarshal([]byte(cred.Tags), &tags) }
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"id": cred.ID,
|
||||
"name": cred.Name,
|
||||
"variable_name": cred.VariableName,
|
||||
@@ -241,15 +241,15 @@ func (h *CredentialHandler) GetCredential(c *gin.Context) {
|
||||
|
||||
func (h *CredentialHandler) UpdateCredential(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
var cred models.Credential
|
||||
var cred model.Credential
|
||||
if h.db.First(&cred, id).Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "凭据不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateSecretRequest
|
||||
var req model.UpdateSecretRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ func (h *CredentialHandler) UpdateCredential(c *gin.Context) {
|
||||
newPlain, _ := json.Marshal(old)
|
||||
enc, err := h.crypt.Encrypt(string(newPlain))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "加密失败"})
|
||||
return
|
||||
}
|
||||
updates["content"] = enc
|
||||
@@ -292,10 +292,10 @@ func (h *CredentialHandler) UpdateCredential(c *gin.Context) {
|
||||
h.db.Model(&cred).Updates(updates)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功"})
|
||||
}
|
||||
|
||||
func hasTypeFieldUpdate(req models.UpdateSecretRequest) bool {
|
||||
func hasTypeFieldUpdate(req model.UpdateSecretRequest) bool {
|
||||
return req.SSHPrivateKey != nil || req.SSHPublicKey != nil || req.SSHUser != nil || req.SSHPort != nil || req.SSHPassword != nil ||
|
||||
req.DockerServer != nil || req.DockerUsername != nil || req.DockerPassword != nil ||
|
||||
req.TLSCert != nil || req.TLSKey != nil || req.TLSCA != nil ||
|
||||
@@ -305,7 +305,7 @@ func hasTypeFieldUpdate(req models.UpdateSecretRequest) bool {
|
||||
req.GenericKey != nil || req.GenericValue != nil
|
||||
}
|
||||
|
||||
func mergeUpdate(req models.UpdateSecretRequest, old map[string]interface{}) {
|
||||
func mergeUpdate(req model.UpdateSecretRequest, old map[string]interface{}) {
|
||||
if req.SSHPrivateKey != nil { old["private_key"] = *req.SSHPrivateKey }
|
||||
if req.SSHPublicKey != nil { old["public_key"] = *req.SSHPublicKey }
|
||||
if req.SSHUser != nil { old["ssh_user"] = *req.SSHUser }
|
||||
@@ -365,27 +365,27 @@ func (h *CredentialHandler) PatchCredential(c *gin.Context) {
|
||||
updates["is_active"] = v
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
h.db.Model(&models.Credential{}).Where("id = ?", id).Updates(updates)
|
||||
h.db.Model(&model.Credential{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "ok"})
|
||||
}
|
||||
|
||||
// ────── Delete ──────
|
||||
|
||||
func (h *CredentialHandler) DeleteCredential(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if h.db.Delete(&models.Credential{}, id).Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if h.db.Delete(&model.Credential{}, id).Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ────── Tags ──────
|
||||
|
||||
func (h *CredentialHandler) GetTags(c *gin.Context) {
|
||||
var rows []string
|
||||
h.db.Model(&models.Credential{}).Where("tags != ''").Pluck("tags", &rows)
|
||||
h.db.Model(&model.Credential{}).Where("tags != ''").Pluck("tags", &rows)
|
||||
|
||||
tagSet := map[string]bool{}
|
||||
for _, r := range rows {
|
||||
@@ -396,5 +396,5 @@ func (h *CredentialHandler) GetTags(c *gin.Context) {
|
||||
}
|
||||
tags := make([]string, 0, len(tagSet))
|
||||
for t := range tagSet { tags = append(tags, t) }
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: tags})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: tags})
|
||||
}
|
||||
+11
-11
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func (h *CredentialHandler) AssignCredential(c *gin.Context) {
|
||||
serverID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的服务器ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的服务器ID"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,25 +21,25 @@ func (h *CredentialHandler) AssignCredential(c *gin.Context) {
|
||||
CredentialID uint `json:"credential_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.CredentialID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请选择凭据"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请选择凭据"})
|
||||
return
|
||||
}
|
||||
|
||||
var cred models.Credential
|
||||
var cred model.Credential
|
||||
if err := h.db.First(&cred, req.CredentialID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "凭据不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
plain, err := h.crypt.Decrypt(cred.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "凭据解密失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, serverID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ func (h *CredentialHandler) AssignCredential(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Save(&server).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "分配失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "分配失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "凭据已关联"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "凭据已关联"})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -33,7 +33,7 @@ func NewDashboardHandler(db *gorm.DB, cache ezredis.Cache) *DashboardHandler {
|
||||
// @Tags 仪表盘
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=models.DashboardStats}
|
||||
// @Success 200 {object} model.Response{data=model.DashboardStats}
|
||||
// @Router /dashboard/stats [get]
|
||||
func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
const cacheKey = "dashboard:stats"
|
||||
@@ -41,9 +41,9 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
|
||||
// 尝试从 Redis 缓存读取
|
||||
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
|
||||
var stats models.DashboardStats
|
||||
var stats model.DashboardStats
|
||||
if json.Unmarshal([]byte(cached), &stats) == nil {
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: stats,
|
||||
@@ -52,20 +52,20 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var stats models.DashboardStats
|
||||
var stats model.DashboardStats
|
||||
|
||||
// 服务器统计
|
||||
var serverStats models.ServerStats
|
||||
h.db.Model(&models.Server{}).Count(&serverStats.Total)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "running").Count(&serverStats.Running)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "error").Count(&serverStats.Error)
|
||||
var serverStats model.ServerStats
|
||||
h.db.Model(&model.Server{}).Count(&serverStats.Total)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&serverStats.Running)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&serverStats.Stopped)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&serverStats.Maintenance)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&serverStats.Error)
|
||||
|
||||
var avgCPU, avgMemory, avgDisk float64
|
||||
h.db.Model(&models.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
|
||||
h.db.Model(&models.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
|
||||
h.db.Model(&models.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
|
||||
h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
|
||||
h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
|
||||
h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
|
||||
serverStats.AvgCPU = math.Round(avgCPU*100) / 100
|
||||
serverStats.AvgMemory = math.Round(avgMemory*100) / 100
|
||||
serverStats.AvgDisk = math.Round(avgDisk*100) / 100
|
||||
@@ -73,16 +73,16 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
stats.ServerStats = serverStats
|
||||
|
||||
// K8s集群统计
|
||||
var k8sStats models.KubernetesClusterStats
|
||||
h.db.Model(&models.KubernetesCluster{}).Count(&k8sStats.Total)
|
||||
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
|
||||
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
|
||||
h.db.Model(&models.KubernetesCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
|
||||
var k8sStats model.KubernetesClusterStats
|
||||
h.db.Model(&model.KubernetesCluster{}).Count(&k8sStats.Total)
|
||||
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
|
||||
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
|
||||
h.db.Model(&model.KubernetesCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
|
||||
|
||||
var totalNodes, totalPods, totalServices int64
|
||||
h.db.Model(&models.KubernetesNode{}).Count(&totalNodes)
|
||||
h.db.Model(&models.KubernetesPod{}).Count(&totalPods)
|
||||
h.db.Model(&models.KubernetesService{}).Count(&totalServices)
|
||||
h.db.Model(&model.KubernetesNode{}).Count(&totalNodes)
|
||||
h.db.Model(&model.KubernetesPod{}).Count(&totalPods)
|
||||
h.db.Model(&model.KubernetesService{}).Count(&totalServices)
|
||||
k8sStats.TotalNodes = totalNodes
|
||||
k8sStats.TotalPods = totalPods
|
||||
k8sStats.TotalServices = totalServices
|
||||
@@ -90,31 +90,31 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
stats.K8sStats = k8sStats
|
||||
|
||||
// 告警统计
|
||||
var alertStats models.AlertStats
|
||||
h.db.Model(&models.AlertEvent{}).Count(&alertStats.Total)
|
||||
h.db.Model(&models.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing)
|
||||
h.db.Model(&models.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved)
|
||||
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical)
|
||||
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning)
|
||||
h.db.Model(&models.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info)
|
||||
var alertStats model.AlertStats
|
||||
h.db.Model(&model.AlertEvent{}).Count(&alertStats.Total)
|
||||
h.db.Model(&model.AlertEvent{}).Where("status = ?", "firing").Count(&alertStats.Firing)
|
||||
h.db.Model(&model.AlertEvent{}).Where("status = ?", "resolved").Count(&alertStats.Resolved)
|
||||
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "critical").Count(&alertStats.Critical)
|
||||
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "warning").Count(&alertStats.Warning)
|
||||
h.db.Model(&model.AlertEvent{}).Where("severity = ?", "info").Count(&alertStats.Info)
|
||||
|
||||
// 今日告警
|
||||
today := time.Now().Format("2006-01-02")
|
||||
h.db.Model(&models.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount)
|
||||
h.db.Model(&model.AlertEvent{}).Where("DATE(created_at) = ?", today).Count(&alertStats.TodayCount)
|
||||
|
||||
// 本周告警
|
||||
weekAgo := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
|
||||
h.db.Model(&models.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount)
|
||||
h.db.Model(&model.AlertEvent{}).Where("created_at >= ?", weekAgo).Count(&alertStats.WeekCount)
|
||||
|
||||
stats.AlertStats = alertStats
|
||||
|
||||
// 流水线统计
|
||||
var pipelineStats models.PipelineStats
|
||||
h.db.Model(&models.PipelineRun{}).Count(&pipelineStats.Total)
|
||||
h.db.Model(&models.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success)
|
||||
h.db.Model(&models.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed)
|
||||
h.db.Model(&models.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running)
|
||||
h.db.Model(&models.PipelineRun{}).Where("DATE(created_at) = ?", today).Count(&pipelineStats.TodayCount)
|
||||
var pipelineStats model.PipelineStats
|
||||
h.db.Model(&model.PipelineRun{}).Count(&pipelineStats.Total)
|
||||
h.db.Model(&model.PipelineRun{}).Where("status = ?", "success").Count(&pipelineStats.Success)
|
||||
h.db.Model(&model.PipelineRun{}).Where("status = ?", "failed").Count(&pipelineStats.Failed)
|
||||
h.db.Model(&model.PipelineRun{}).Where("status = ?", "running").Count(&pipelineStats.Running)
|
||||
h.db.Model(&model.PipelineRun{}).Where("DATE(created_at) = ?", today).Count(&pipelineStats.TodayCount)
|
||||
|
||||
if pipelineStats.Total > 0 {
|
||||
pipelineStats.SuccessRate = math.Round(float64(pipelineStats.Success)/float64(pipelineStats.Total)*10000) / 100
|
||||
@@ -123,7 +123,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
stats.PipelineStats = pipelineStats
|
||||
|
||||
// 成本统计 — 基于云账号月费用(monthly_cost)计算,不再硬编码
|
||||
var cloudAccounts []models.CloudAccount
|
||||
var cloudAccounts []model.CloudAccount
|
||||
h.db.Find(&cloudAccounts)
|
||||
|
||||
providerCost := make(map[string]float64)
|
||||
@@ -145,21 +145,21 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
"huawei": "华为云",
|
||||
"azure": "Azure",
|
||||
}
|
||||
var cloudCosts []models.CloudCost
|
||||
var cloudCosts []model.CloudCost
|
||||
for _, p := range providerOrder {
|
||||
name := providerName[p]
|
||||
if name == "" {
|
||||
name = p
|
||||
}
|
||||
cloudCosts = append(cloudCosts, models.CloudCost{Cloud: name, Cost: math.Round(providerCost[p]*100) / 100})
|
||||
cloudCosts = append(cloudCosts, model.CloudCost{Cloud: name, Cost: math.Round(providerCost[p]*100) / 100})
|
||||
}
|
||||
|
||||
// 月度趋势:以本月实际费用为锚,向前回推 5 个月(轻微增长趋势),合计即为 total_cost
|
||||
var monthTrend []models.MonthTrend
|
||||
var monthTrend []model.MonthTrend
|
||||
trendScales := []float64{0.85, 0.88, 0.91, 0.94, 0.97, 1.0}
|
||||
for i := 0; i < 6; i++ {
|
||||
t := time.Now().AddDate(0, -(5 - i), 0)
|
||||
monthTrend = append(monthTrend, models.MonthTrend{
|
||||
monthTrend = append(monthTrend, model.MonthTrend{
|
||||
Month: fmt.Sprintf("%d月", int(t.Month())),
|
||||
Cost: math.Round(monthCostTotal*trendScales[i]*100) / 100,
|
||||
})
|
||||
@@ -170,7 +170,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
totalCost += m.Cost
|
||||
}
|
||||
|
||||
stats.CostStats = models.CostStats{
|
||||
stats.CostStats = model.CostStats{
|
||||
TotalCost: math.Round(totalCost*100) / 100,
|
||||
MonthCost: math.Round(monthCostTotal*100) / 100,
|
||||
CloudCosts: cloudCosts,
|
||||
@@ -178,14 +178,14 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Top资源使用
|
||||
var topCPU, topMemory, topDisk []models.Server
|
||||
var topCPU, topMemory, topDisk []model.Server
|
||||
h.db.Order("cpu_usage DESC").Limit(5).Find(&topCPU)
|
||||
h.db.Order("memory_usage DESC").Limit(5).Find(&topMemory)
|
||||
h.db.Order("disk_usage DESC").Limit(5).Find(&topDisk)
|
||||
|
||||
var topResourceUsage models.TopResourceUsage
|
||||
var topResourceUsage model.TopResourceUsage
|
||||
for _, s := range topCPU {
|
||||
topResourceUsage.CPU = append(topResourceUsage.CPU, models.ServerResource{
|
||||
topResourceUsage.CPU = append(topResourceUsage.CPU, model.ServerResource{
|
||||
ID: s.ID,
|
||||
Hostname: s.Hostname,
|
||||
IP: s.IP,
|
||||
@@ -193,7 +193,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
for _, s := range topMemory {
|
||||
topResourceUsage.Memory = append(topResourceUsage.Memory, models.ServerResource{
|
||||
topResourceUsage.Memory = append(topResourceUsage.Memory, model.ServerResource{
|
||||
ID: s.ID,
|
||||
Hostname: s.Hostname,
|
||||
IP: s.IP,
|
||||
@@ -201,7 +201,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
for _, s := range topDisk {
|
||||
topResourceUsage.Disk = append(topResourceUsage.Disk, models.ServerResource{
|
||||
topResourceUsage.Disk = append(topResourceUsage.Disk, model.ServerResource{
|
||||
ID: s.ID,
|
||||
Hostname: s.Hostname,
|
||||
IP: s.IP,
|
||||
@@ -211,16 +211,16 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
stats.TopResourceUsage = topResourceUsage
|
||||
|
||||
// 即将过期的证书
|
||||
var domains []models.Domain
|
||||
var domains []model.Domain
|
||||
h.db.Where("ssl_enabled = ?", true).Find(&domains)
|
||||
|
||||
var expiringCerts []models.DomainCert
|
||||
var expiringCerts []model.DomainCert
|
||||
now := time.Now()
|
||||
for _, d := range domains {
|
||||
if d.SSLExpireDate != nil {
|
||||
days := int(d.SSLExpireDate.Sub(now).Hours() / 24)
|
||||
if days < 90 {
|
||||
expiringCerts = append(expiringCerts, models.DomainCert{
|
||||
expiringCerts = append(expiringCerts, model.DomainCert{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
ExpireDate: d.SSLExpireDate.Format("2006-01-02"),
|
||||
@@ -236,7 +236,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
_ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: stats,
|
||||
@@ -250,7 +250,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param limit query int false "数量限制"
|
||||
// @Success 200 {object} models.Response{data=[]models.AlertEvent}
|
||||
// @Success 200 {object} model.Response{data=[]model.AlertEvent}
|
||||
// @Router /dashboard/recent-alerts [get]
|
||||
func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) {
|
||||
limit := 10
|
||||
@@ -260,13 +260,13 @@ func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var alerts []models.AlertEvent
|
||||
var alerts []model.AlertEvent
|
||||
h.db.Preload("Rule").Where("status = ?", "firing").
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&alerts)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: alerts,
|
||||
@@ -280,7 +280,7 @@ func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param limit query int false "数量限制"
|
||||
// @Success 200 {object} models.Response{data=[]models.AppRelease}
|
||||
// @Success 200 {object} model.Response{data=[]model.AppRelease}
|
||||
// @Router /dashboard/recent-releases [get]
|
||||
func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
|
||||
limit := 5
|
||||
@@ -290,12 +290,12 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var releases []models.AppRelease
|
||||
var releases []model.AppRelease
|
||||
h.db.Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&releases)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: releases,
|
||||
@@ -309,7 +309,7 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param hours query int false "小时数"
|
||||
// @Success 200 {object} models.Response{data=[]models.MetricData}
|
||||
// @Success 200 {object} model.Response{data=[]model.MetricData}
|
||||
// @Router /dashboard/metrics [get]
|
||||
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
||||
hours := 6 // 默认6小时,减少扫描行数
|
||||
@@ -327,28 +327,28 @@ func (h *DashboardHandler) GetMetrics(c *gin.Context) {
|
||||
AvgMem float64
|
||||
}
|
||||
var rows []avgPoint
|
||||
h.db.Model(&models.AgentReport{}).
|
||||
h.db.Model(&model.AgentReport{}).
|
||||
Select("UNIX_TIMESTAMP(report_time) DIV 3600 as bucket_ts, ROUND(AVG(cpu_usage),2) as avg_cpu, ROUND(AVG(mem_usage),2) as avg_mem").
|
||||
Where("report_time >= ?", since).
|
||||
Group("bucket_ts").
|
||||
Order("bucket_ts ASC").
|
||||
Scan(&rows)
|
||||
|
||||
var metrics []models.MetricData
|
||||
var metrics []model.MetricData
|
||||
for _, r := range rows {
|
||||
ts := time.Unix(r.BucketTS*3600, 0)
|
||||
metrics = append(metrics,
|
||||
models.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts},
|
||||
models.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts},
|
||||
model.MetricData{Name: "cpu", Value: r.AvgCPU, Timestamp: ts},
|
||||
model.MetricData{Name: "memory", Value: r.AvgMem, Timestamp: ts},
|
||||
)
|
||||
}
|
||||
|
||||
// 如果没有真实数据,返回空数组(不造假数据)
|
||||
if metrics == nil {
|
||||
metrics = []models.MetricData{}
|
||||
metrics = []model.MetricData{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: metrics,
|
||||
@@ -1,9 +1,9 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -21,18 +21,18 @@ func NewDeployHandler(db *gorm.DB) *DeployHandler {
|
||||
// ==================== Git 仓库认证 ====================
|
||||
|
||||
func (h *DeployHandler) ListRepos(c *gin.Context) {
|
||||
var items []models.PipelineRepo
|
||||
var items []model.PipelineRepo
|
||||
h.db.Order("updated_at DESC").Find(&items)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: int64(len(items))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) GetRepo(c *gin.Context) {
|
||||
var item models.PipelineRepo
|
||||
var item model.PipelineRepo
|
||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) CreateRepo(c *gin.Context) {
|
||||
@@ -40,34 +40,34 @@ func (h *DeployHandler) CreateRepo(c *gin.Context) {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
AuthType models.RepoAuthType `json:"auth_type"`
|
||||
AuthType model.RepoAuthType `json:"auth_type"`
|
||||
CredentialID *uint `json:"credential_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.AuthType == "" { req.AuthType = models.RepoAuthToken }
|
||||
item := models.PipelineRepo{
|
||||
if req.AuthType == "" { req.AuthType = model.RepoAuthToken }
|
||||
item := model.PipelineRepo{
|
||||
Name: req.Name, Description: req.Description, URL: req.URL,
|
||||
AuthType: req.AuthType, CredentialID: req.CredentialID,
|
||||
CreatedBy: c.GetString("username"),
|
||||
}
|
||||
h.db.Create(&item)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) UpdateRepo(c *gin.Context) {
|
||||
var item models.PipelineRepo
|
||||
var item model.PipelineRepo
|
||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
URL *string `json:"url"`
|
||||
AuthType *models.RepoAuthType `json:"auth_type"`
|
||||
AuthType *model.RepoAuthType `json:"auth_type"`
|
||||
CredentialID *uint `json:"credential_id"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
@@ -81,29 +81,29 @@ func (h *DeployHandler) UpdateRepo(c *gin.Context) {
|
||||
if req.CredentialID != nil { updates["credential_id"] = req.CredentialID }
|
||||
h.db.Model(&item).Updates(updates)
|
||||
h.db.First(&item, item.ID)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) DeleteRepo(c *gin.Context) {
|
||||
h.db.Delete(&models.PipelineRepo{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
h.db.Delete(&model.PipelineRepo{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 镜像仓库认证 ====================
|
||||
|
||||
func (h *DeployHandler) ListRegistries(c *gin.Context) {
|
||||
var items []models.PipelineRegistry
|
||||
var items []model.PipelineRegistry
|
||||
h.db.Order("updated_at DESC").Find(&items)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: int64(len(items))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) GetRegistry(c *gin.Context) {
|
||||
var item models.PipelineRegistry
|
||||
var item model.PipelineRegistry
|
||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) CreateRegistry(c *gin.Context) {
|
||||
@@ -112,27 +112,27 @@ func (h *DeployHandler) CreateRegistry(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
RegistryURL string `json:"registry_url" binding:"required"`
|
||||
Namespace string `json:"namespace"`
|
||||
AuthType models.RegistryAuthType `json:"auth_type"`
|
||||
AuthType model.RegistryAuthType `json:"auth_type"`
|
||||
CredentialID *uint `json:"credential_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.AuthType == "" { req.AuthType = models.RegistryAuthPassword }
|
||||
item := models.PipelineRegistry{
|
||||
if req.AuthType == "" { req.AuthType = model.RegistryAuthPassword }
|
||||
item := model.PipelineRegistry{
|
||||
Name: req.Name, Description: req.Description, RegistryURL: req.RegistryURL,
|
||||
Namespace: req.Namespace, AuthType: req.AuthType, CredentialID: req.CredentialID,
|
||||
CreatedBy: c.GetString("username"),
|
||||
}
|
||||
h.db.Create(&item)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) UpdateRegistry(c *gin.Context) {
|
||||
var item models.PipelineRegistry
|
||||
var item model.PipelineRegistry
|
||||
if err := h.db.First(&item, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
@@ -140,7 +140,7 @@ func (h *DeployHandler) UpdateRegistry(c *gin.Context) {
|
||||
Description *string `json:"description"`
|
||||
RegistryURL *string `json:"registry_url"`
|
||||
Namespace *string `json:"namespace"`
|
||||
AuthType *models.RegistryAuthType `json:"auth_type"`
|
||||
AuthType *model.RegistryAuthType `json:"auth_type"`
|
||||
CredentialID *uint `json:"credential_id"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
@@ -155,10 +155,10 @@ func (h *DeployHandler) UpdateRegistry(c *gin.Context) {
|
||||
if req.CredentialID != nil { updates["credential_id"] = req.CredentialID }
|
||||
h.db.Model(&item).Updates(updates)
|
||||
h.db.First(&item, item.ID)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
func (h *DeployHandler) DeleteRegistry(c *gin.Context) {
|
||||
h.db.Delete(&models.PipelineRegistry{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
h.db.Delete(&model.PipelineRegistry{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
+36
-36
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
|
||||
// ListEnvironments 环境列表
|
||||
func (h *PipelineHandler) ListEnvironments(c *gin.Context) {
|
||||
var items []models.DeployEnvironment
|
||||
var items []model.DeployEnvironment
|
||||
h.db.Order("sort_order ASC, id ASC").Find(&items)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: int64(len(items))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: int64(len(items))}})
|
||||
}
|
||||
|
||||
// CreateEnvironment 创建环境
|
||||
@@ -32,28 +32,28 @@ func (h *PipelineHandler) CreateEnvironment(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Color == "" { req.Color = typeDefaultColor(req.Type) }
|
||||
item := models.DeployEnvironment{
|
||||
item := model.DeployEnvironment{
|
||||
Name: req.Name, DisplayName: req.DisplayName,
|
||||
Type: models.EnvironmentType(req.Type), Color: req.Color,
|
||||
Type: model.EnvironmentType(req.Type), Color: req.Color,
|
||||
SortOrder: req.SortOrder, Description: req.Description,
|
||||
}
|
||||
if err := h.db.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
// UpdateEnvironment 更新环境
|
||||
func (h *PipelineHandler) UpdateEnvironment(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var item models.DeployEnvironment
|
||||
var item model.DeployEnvironment
|
||||
if err := h.db.First(&item, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
@@ -76,13 +76,13 @@ func (h *PipelineHandler) UpdateEnvironment(c *gin.Context) {
|
||||
if req.Enabled != nil { updates["enabled"] = *req.Enabled }
|
||||
h.db.Model(&item).Updates(updates)
|
||||
h.db.First(&item, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
// DeleteEnvironment 删除环境
|
||||
func (h *PipelineHandler) DeleteEnvironment(c *gin.Context) {
|
||||
h.db.Delete(&models.DeployEnvironment{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
h.db.Delete(&model.DeployEnvironment{}, c.Param("id"))
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
func typeDefaultColor(t string) string {
|
||||
@@ -99,9 +99,9 @@ func typeDefaultColor(t string) string {
|
||||
// ListPipelineMembers 成员列表
|
||||
func (h *PipelineHandler) ListPipelineMembers(c *gin.Context) {
|
||||
pipelineID := getPipelineID(c)
|
||||
var members []models.DeployPipelineMember
|
||||
var members []model.DeployPipelineMember
|
||||
h.db.Where("pipeline_id = ?", pipelineID).Order("created_at ASC").Find(&members)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: members})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: members})
|
||||
}
|
||||
|
||||
// AddPipelineMember 添加成员
|
||||
@@ -112,38 +112,38 @@ func (h *PipelineHandler) AddPipelineMember(c *gin.Context) {
|
||||
Role string `json:"role" binding:"required,oneof=owner editor viewer"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.First(&pipeline, pipelineID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "流水线不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流水线不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
member := models.DeployPipelineMember{
|
||||
member := model.DeployPipelineMember{
|
||||
PipelineID: uint(pipelineID),
|
||||
Username: &req.Username,
|
||||
Role: models.MemberRole(req.Role),
|
||||
Role: model.MemberRole(req.Role),
|
||||
}
|
||||
if err := h.db.Create(&member).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, models.Response{Code: 409, Message: "该用户已是成员"})
|
||||
c.JSON(http.StatusConflict, model.Response{Code: 409, Message: "该用户已是成员"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: member})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: member})
|
||||
}
|
||||
|
||||
// RemovePipelineMember 移除成员
|
||||
func (h *PipelineHandler) RemovePipelineMember(c *gin.Context) {
|
||||
pipelineID := getPipelineID(c)
|
||||
memberID, _ := strconv.Atoi(c.Param("memberId"))
|
||||
result := h.db.Where("id = ? AND pipeline_id = ?", memberID, pipelineID).Delete(&models.DeployPipelineMember{})
|
||||
result := h.db.Where("id = ? AND pipeline_id = ?", memberID, pipelineID).Delete(&model.DeployPipelineMember{})
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "成员不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "成员不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已移除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已移除"})
|
||||
}
|
||||
|
||||
// ==================== 流水线运行控制 ====================
|
||||
@@ -153,13 +153,13 @@ func (h *PipelineHandler) StopPipelineRun(c *gin.Context) {
|
||||
pipelineID := getPipelineID(c)
|
||||
runID, _ := strconv.Atoi(c.Param("runId"))
|
||||
|
||||
var run models.PipelineRun
|
||||
var run model.PipelineRun
|
||||
if err := h.db.Where("id = ? AND pipeline_id = ?", runID, pipelineID).First(&run).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "运行记录不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "运行记录不存在"})
|
||||
return
|
||||
}
|
||||
if run.Status != "running" && run.Status != "pending" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "仅运行中或等待中的任务可停止, 当前状态: " + run.Status})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "仅运行中或等待中的任务可停止, 当前状态: " + run.Status})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (h *PipelineHandler) StopPipelineRun(c *gin.Context) {
|
||||
h.db.Model(&run).Update("logs", string(newLogs))
|
||||
|
||||
h.db.First(&run, runID)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: run, Message: "已停止"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: run, Message: "已停止"})
|
||||
}
|
||||
|
||||
// GetRunLog 获取运行日志
|
||||
@@ -200,9 +200,9 @@ func (h *PipelineHandler) GetRunLog(c *gin.Context) {
|
||||
pipelineID := getPipelineID(c)
|
||||
runID, _ := strconv.Atoi(c.Param("runId"))
|
||||
|
||||
var run models.PipelineRun
|
||||
var run model.PipelineRun
|
||||
if err := h.db.Where("id = ? AND pipeline_id = ?", runID, pipelineID).First(&run).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "运行记录不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "运行记录不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ func (h *PipelineHandler) GetRunLog(c *gin.Context) {
|
||||
json.Unmarshal([]byte(run.Logs), &logs)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"run": run,
|
||||
"logs": logs,
|
||||
}})
|
||||
@@ -225,10 +225,10 @@ func (h *PipelineHandler) BatchDeleteRuns(c *gin.Context) {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请选择要删除的记录"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请选择要删除的记录"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("pipeline_id = ? AND id IN ?", pipelineID, req.IDs).Delete(&models.PipelineRun{})
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("已删除 %d 条记录", result.RowsAffected)})
|
||||
result := h.db.Where("pipeline_id = ? AND id IN ?", pipelineID, req.IDs).Delete(&model.PipelineRun{})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("已删除 %d 条记录", result.RowsAffected)})
|
||||
}
|
||||
+33
-33
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
|
||||
// ListGroups 用户组列表
|
||||
func (h *PipelineHandler) ListGroups(c *gin.Context) {
|
||||
var groups []models.DeployUserGroup
|
||||
var groups []model.DeployUserGroup
|
||||
h.db.Preload("Members").Order("created_at DESC").Find(&groups)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: groups, Total: int64(len(groups))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: groups, Total: int64(len(groups))}})
|
||||
}
|
||||
|
||||
// CreateGroup 创建用户组
|
||||
@@ -25,26 +25,26 @@ func (h *PipelineHandler) CreateGroup(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
group := models.DeployUserGroup{
|
||||
group := model.DeployUserGroup{
|
||||
Name: req.Name, Description: req.Description,
|
||||
CreatedBy: c.GetString("username"),
|
||||
}
|
||||
if err := h.db.Create(&group).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, models.Response{Code: 409, Message: "组名已存在: " + err.Error()})
|
||||
c.JSON(http.StatusConflict, model.Response{Code: 409, Message: "组名已存在: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: group})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: group})
|
||||
}
|
||||
|
||||
// UpdateGroup 更新用户组
|
||||
func (h *PipelineHandler) UpdateGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var group models.DeployUserGroup
|
||||
var group model.DeployUserGroup
|
||||
if err := h.db.First(&group, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
@@ -57,17 +57,17 @@ func (h *PipelineHandler) UpdateGroup(c *gin.Context) {
|
||||
if req.Description != nil { updates["description"] = *req.Description }
|
||||
h.db.Model(&group).Updates(updates)
|
||||
h.db.First(&group, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: group})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: group})
|
||||
}
|
||||
|
||||
// DeleteGroup 删除用户组
|
||||
func (h *PipelineHandler) DeleteGroup(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// 先删组成员和流水线授权
|
||||
h.db.Where("group_id = ?", id).Delete(&models.DeployUserGroupMember{})
|
||||
h.db.Where("group_id = ?", id).Delete(&models.DeployPipelineMember{})
|
||||
h.db.Delete(&models.DeployUserGroup{}, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
h.db.Where("group_id = ?", id).Delete(&model.DeployUserGroupMember{})
|
||||
h.db.Where("group_id = ?", id).Delete(&model.DeployPipelineMember{})
|
||||
h.db.Delete(&model.DeployUserGroup{}, id)
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// ==================== 用户组成员管理 ====================
|
||||
@@ -75,9 +75,9 @@ func (h *PipelineHandler) DeleteGroup(c *gin.Context) {
|
||||
// ListGroupMembers 组成员列表
|
||||
func (h *PipelineHandler) ListGroupMembers(c *gin.Context) {
|
||||
groupID, _ := strconv.Atoi(c.Param("id"))
|
||||
var members []models.DeployUserGroupMember
|
||||
var members []model.DeployUserGroupMember
|
||||
h.db.Where("group_id = ?", groupID).Find(&members)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: members})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: members})
|
||||
}
|
||||
|
||||
// AddGroupMember 添加组成员
|
||||
@@ -87,27 +87,27 @@ func (h *PipelineHandler) AddGroupMember(c *gin.Context) {
|
||||
Username string `json:"username" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
member := models.DeployUserGroupMember{GroupID: uint(groupID), Username: req.Username}
|
||||
member := model.DeployUserGroupMember{GroupID: uint(groupID), Username: req.Username}
|
||||
if err := h.db.Create(&member).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, models.Response{Code: 409, Message: "该用户已在组内"})
|
||||
c.JSON(http.StatusConflict, model.Response{Code: 409, Message: "该用户已在组内"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: member})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: member})
|
||||
}
|
||||
|
||||
// RemoveGroupMember 移除组成员
|
||||
func (h *PipelineHandler) RemoveGroupMember(c *gin.Context) {
|
||||
groupID, _ := strconv.Atoi(c.Param("id"))
|
||||
memberID, _ := strconv.Atoi(c.Param("memberId"))
|
||||
result := h.db.Where("id = ? AND group_id = ?", memberID, groupID).Delete(&models.DeployUserGroupMember{})
|
||||
result := h.db.Where("id = ? AND group_id = ?", memberID, groupID).Delete(&model.DeployUserGroupMember{})
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "成员不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "成员不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已移除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已移除"})
|
||||
}
|
||||
|
||||
// ==================== 组授权给流水线 ====================
|
||||
@@ -120,32 +120,32 @@ func (h *PipelineHandler) AssignGroupToPipeline(c *gin.Context) {
|
||||
Role string `json:"role" binding:"required,oneof=owner editor viewer"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.First(&pipeline, pipelineID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "流水线不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流水线不存在"})
|
||||
return
|
||||
}
|
||||
var group models.DeployUserGroup
|
||||
var group model.DeployUserGroup
|
||||
if err := h.db.First(&group, req.GroupID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "用户组不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "用户组不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
member := models.DeployPipelineMember{
|
||||
member := model.DeployPipelineMember{
|
||||
PipelineID: uint(pipelineID),
|
||||
GroupID: &req.GroupID,
|
||||
Role: models.MemberRole(req.Role),
|
||||
Role: model.MemberRole(req.Role),
|
||||
}
|
||||
if err := h.db.Create(&member).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, models.Response{Code: 409, Message: "该组已被授权到此流水线"})
|
||||
c.JSON(http.StatusConflict, model.Response{Code: 409, Message: "该组已被授权到此流水线"})
|
||||
return
|
||||
}
|
||||
|
||||
// 预加载关联
|
||||
member.Group = &group
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: member, Message: "已授权"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: member, Message: "已授权"})
|
||||
}
|
||||
+48
-48
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// ListHealthProbes GET /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) ListHealthProbes(c *gin.Context) {
|
||||
query := h.db.Model(&models.HealthProbe{})
|
||||
query := h.db.Model(&model.HealthProbe{})
|
||||
|
||||
if hostname := c.Query("hostname"); hostname != "" {
|
||||
query = query.Where("hostname = ?", hostname)
|
||||
@@ -30,14 +30,14 @@ func (h *AgentHandler) ListHealthProbes(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var probes []models.HealthProbe
|
||||
var probes []model.HealthProbe
|
||||
query.Order("created_at DESC").Limit(500).Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
@@ -46,42 +46,42 @@ func (h *AgentHandler) ListHealthProbes(c *gin.Context) {
|
||||
|
||||
// CreateHealthProbe POST /api/v1/agent/health-probes
|
||||
func (h *AgentHandler) CreateHealthProbe(c *gin.Context) {
|
||||
var probe models.HealthProbe
|
||||
var probe model.HealthProbe
|
||||
if err := c.ShouldBindJSON(&probe); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if probe.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "探针名称不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针名称不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.Hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "目标主机不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "目标主机不能为空"})
|
||||
return
|
||||
}
|
||||
if probe.ProbeType == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "探针类型不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "探针类型不能为空"})
|
||||
return
|
||||
}
|
||||
// 验证探针类型配置
|
||||
switch probe.ProbeType {
|
||||
case models.ProbeTypeHTTP:
|
||||
case model.ProbeTypeHTTP:
|
||||
if probe.HTTPURL == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "HTTP 探针必须填写 URL"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "HTTP 探针必须填写 URL"})
|
||||
return
|
||||
}
|
||||
case models.ProbeTypeTCP:
|
||||
case model.ProbeTypeTCP:
|
||||
if probe.TCPHost == "" || probe.TCPPort == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "TCP 探针必须填写 Host 和 Port"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "TCP 探针必须填写 Host 和 Port"})
|
||||
return
|
||||
}
|
||||
case models.ProbeTypeExec:
|
||||
case model.ProbeTypeExec:
|
||||
if probe.ExecCommand == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "Exec 探针必须填写执行命令"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "Exec 探针必须填写执行命令"})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "不支持的探针类型: " + string(probe.ProbeType)})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "不支持的探针类型: " + string(probe.ProbeType)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -95,25 +95,25 @@ func (h *AgentHandler) CreateHealthProbe(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&probe).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: probe})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: probe})
|
||||
}
|
||||
|
||||
// UpdateHealthProbe PUT /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) UpdateHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var probe models.HealthProbe
|
||||
var probe model.HealthProbe
|
||||
if err := h.db.First(&probe, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "探针不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "探针不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var input models.HealthProbe
|
||||
var input model.HealthProbe
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,22 +164,22 @@ func (h *AgentHandler) UpdateHealthProbe(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&probe).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.db.First(&probe, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: probe})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: probe})
|
||||
}
|
||||
|
||||
// DeleteHealthProbe DELETE /api/v1/agent/health-probes/:id
|
||||
func (h *AgentHandler) DeleteHealthProbe(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.HealthProbe{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.HealthProbe{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// GetHealthProbesForAgent Agent 拉取本机探针配置(Agent 调用,无需 JWT)
|
||||
@@ -187,20 +187,20 @@ func (h *AgentHandler) DeleteHealthProbe(c *gin.Context) {
|
||||
func (h *AgentHandler) GetHealthProbesForAgent(c *gin.Context) {
|
||||
hostname := c.Query("hostname")
|
||||
if hostname == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "hostname 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var probes []models.HealthProbe
|
||||
var probes []model.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: probes,
|
||||
@@ -212,7 +212,7 @@ func (h *AgentHandler) GetHealthProbesForAgent(c *gin.Context) {
|
||||
// ────────────────────────────────────────
|
||||
|
||||
// processProbeResults 处理 Agent 上报的探针结果,更新探针状态
|
||||
func (h *AgentHandler) processProbeResults(hostname string, results []models.HealthProbeResult) {
|
||||
func (h *AgentHandler) processProbeResults(hostname string, results []model.HealthProbeResult) {
|
||||
now := time.Now()
|
||||
for _, r := range results {
|
||||
updates := map[string]interface{}{
|
||||
@@ -221,9 +221,9 @@ func (h *AgentHandler) processProbeResults(hostname string, results []models.Hea
|
||||
"last_latency_ms": r.LatencyMs,
|
||||
}
|
||||
|
||||
if r.Status == models.ProbeStatusDown {
|
||||
if r.Status == model.ProbeStatusDown {
|
||||
// 失败时增加连续失败计数
|
||||
h.db.Model(&models.HealthProbe{}).
|
||||
h.db.Model(&model.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
UpdateColumn("fail_count", gorm.Expr("fail_count + 1"))
|
||||
} else {
|
||||
@@ -231,27 +231,27 @@ func (h *AgentHandler) processProbeResults(hostname string, results []models.Hea
|
||||
updates["fail_count"] = 0
|
||||
}
|
||||
|
||||
h.db.Model(&models.HealthProbe{}).
|
||||
h.db.Model(&model.HealthProbe{}).
|
||||
Where("id = ? AND hostname = ?", r.ProbeID, hostname).
|
||||
Updates(updates)
|
||||
|
||||
// ═══ 探针告警:Agent 本地探测 down → 平台创建告警事件 ═══
|
||||
if r.Status == models.ProbeStatusDown {
|
||||
if r.Status == model.ProbeStatusDown {
|
||||
// 检查是否存在未恢复的同探针告警
|
||||
var existingAlert models.AgentAlert
|
||||
var existingAlert model.AgentAlert
|
||||
h.db.Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
First(&existingAlert)
|
||||
|
||||
if existingAlert.ID == 0 {
|
||||
// 获取 fail_count 判断严重级别
|
||||
var probe models.HealthProbe
|
||||
var probe model.HealthProbe
|
||||
if err := h.db.First(&probe, r.ProbeID).Error; err == nil {
|
||||
severity := "warning"
|
||||
if probe.FailCount >= 3 {
|
||||
severity = "critical"
|
||||
}
|
||||
alert := models.AgentAlert{
|
||||
alert := model.AgentAlert{
|
||||
RuleName: r.Name,
|
||||
Hostname: hostname,
|
||||
Metric: "health_probe",
|
||||
@@ -265,7 +265,7 @@ func (h *AgentHandler) processProbeResults(hostname string, results []models.Hea
|
||||
}
|
||||
} else {
|
||||
// 探针恢复正常:自动恢复告警
|
||||
h.db.Model(&models.AgentAlert{}).
|
||||
h.db.Model(&model.AgentAlert{}).
|
||||
Where("rule_name = ? AND hostname = ? AND metric = ? AND resolved = ?",
|
||||
r.Name, hostname, "health_probe", false).
|
||||
Updates(map[string]interface{}{
|
||||
@@ -278,28 +278,28 @@ func (h *AgentHandler) processProbeResults(hostname string, results []models.Hea
|
||||
|
||||
// probeConfigResponse ReceiveReport 的响应结构(含探针配置)
|
||||
type probeConfigResponse struct {
|
||||
models.Response
|
||||
ProbeConfigs []models.HealthProbe `json:"probe_configs,omitempty"`
|
||||
model.Response
|
||||
ProbeConfigs []model.HealthProbe `json:"probe_configs,omitempty"`
|
||||
}
|
||||
|
||||
// makeProbeConfigResponse 构建带探针配置的响应
|
||||
func (h *AgentHandler) makeProbeConfigResponse(hostname string, respCode int, respMsg string, probeResults []models.HealthProbeResult) probeConfigResponse {
|
||||
func (h *AgentHandler) makeProbeConfigResponse(hostname string, respCode int, respMsg string, probeResults []model.HealthProbeResult) probeConfigResponse {
|
||||
// 处理上报的探针结果
|
||||
if len(probeResults) > 0 {
|
||||
h.processProbeResults(hostname, probeResults)
|
||||
}
|
||||
|
||||
// 拉取该主机的新探针配置(供 Agent 下次执行)
|
||||
var probes []models.HealthProbe
|
||||
var probes []model.HealthProbe
|
||||
h.db.Where("hostname = ? AND enabled = ?", hostname, true).
|
||||
Order("id ASC").
|
||||
Find(&probes)
|
||||
if probes == nil {
|
||||
probes = []models.HealthProbe{}
|
||||
probes = []model.HealthProbe{}
|
||||
}
|
||||
|
||||
return probeConfigResponse{
|
||||
Response: models.Response{
|
||||
Response: model.Response{
|
||||
Code: respCode,
|
||||
Message: respMsg,
|
||||
},
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -22,30 +22,30 @@ func NewItemRoleHandler(db *gorm.DB) *ItemRoleHandler {
|
||||
|
||||
// ListItemRoles GET /admin/item-roles
|
||||
func (h *ItemRoleHandler) ListItemRoles(c *gin.Context) {
|
||||
var roles []models.ItemRole
|
||||
var roles []model.ItemRole
|
||||
h.db.Order("created_at DESC").Find(&roles)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: models.ListResponse{List: roles, Total: int64(len(roles))},
|
||||
Data: model.ListResponse{List: roles, Total: int64(len(roles))},
|
||||
})
|
||||
}
|
||||
|
||||
// CreateItemRole POST /admin/item-roles
|
||||
func (h *ItemRoleHandler) CreateItemRole(c *gin.Context) {
|
||||
var req models.CreateItemRoleRequest
|
||||
var req model.CreateItemRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 唯一性校验
|
||||
var existing models.ItemRole
|
||||
var existing model.ItemRole
|
||||
if err := h.db.Where("name = ? OR code = ?", req.Name, req.Code).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "Item Role 名称或编码已存在"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "Item Role 名称或编码已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
role := models.ItemRole{
|
||||
role := model.ItemRole{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Pattern: req.Pattern,
|
||||
@@ -54,30 +54,30 @@ func (h *ItemRoleHandler) CreateItemRole(c *gin.Context) {
|
||||
Status: 1,
|
||||
}
|
||||
if err := h.db.Create(&role).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: role})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: role})
|
||||
}
|
||||
|
||||
// UpdateItemRole PUT /admin/item-roles/:id
|
||||
func (h *ItemRoleHandler) UpdateItemRole(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateItemRoleRequest
|
||||
var req model.UpdateItemRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var role models.ItemRole
|
||||
var role model.ItemRole
|
||||
if err := h.db.First(&role, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Item Role 不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "Item Role 不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,46 +103,46 @@ func (h *ItemRoleHandler) UpdateItemRole(c *gin.Context) {
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := h.db.Model(&role).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.db.First(&role, id)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: role})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: role})
|
||||
}
|
||||
|
||||
// DeleteItemRole DELETE /admin/item-roles/:id
|
||||
func (h *ItemRoleHandler) DeleteItemRole(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up user associations first
|
||||
h.db.Where("item_role_id = ?", id).Delete(&models.UserItemRole{})
|
||||
if err := h.db.Delete(&models.ItemRole{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
h.db.Where("item_role_id = ?", id).Delete(&model.UserItemRole{})
|
||||
if err := h.db.Delete(&model.ItemRole{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ListUserItemRoles GET /admin/users/:id/item-roles
|
||||
func (h *ItemRoleHandler) ListUserItemRoles(c *gin.Context) {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var assignments []models.UserItemRole
|
||||
var assignments []model.UserItemRole
|
||||
h.db.Preload("ItemRole").Where("user_id = ?", userID).Find(&assignments)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: models.ListResponse{List: assignments, Total: int64(len(assignments))},
|
||||
Data: model.ListResponse{List: assignments, Total: int64(len(assignments))},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,52 +150,52 @@ func (h *ItemRoleHandler) ListUserItemRoles(c *gin.Context) {
|
||||
func (h *ItemRoleHandler) AssignItemRole(c *gin.Context) {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.AssignItemRoleRequest
|
||||
var req model.AssignItemRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already assigned
|
||||
var existing models.UserItemRole
|
||||
var existing model.UserItemRole
|
||||
if err := h.db.Where("user_id = ? AND item_role_id = ?", userID, req.ItemRoleID).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "该用户已拥有此 Item Role"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "该用户已拥有此 Item Role"})
|
||||
return
|
||||
}
|
||||
|
||||
assignment := models.UserItemRole{
|
||||
assignment := model.UserItemRole{
|
||||
UserID: uint(userID),
|
||||
ItemRoleID: req.ItemRoleID,
|
||||
}
|
||||
if err := h.db.Create(&assignment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "分配失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "分配失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "分配成功", Data: assignment})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "分配成功", Data: assignment})
|
||||
}
|
||||
|
||||
// RemoveItemRole DELETE /admin/users/:id/item-roles/:roleId
|
||||
func (h *ItemRoleHandler) RemoveItemRole(c *gin.Context) {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的用户 ID"})
|
||||
return
|
||||
}
|
||||
roleID, err := strconv.Atoi(c.Param("roleId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的 Role ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的 Role ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Where("user_id = ? AND item_role_id = ?", uint(userID), roleID).Delete(&models.UserItemRole{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "移除失败"})
|
||||
if err := h.db.Where("user_id = ? AND item_role_id = ?", uint(userID), roleID).Delete(&model.UserItemRole{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "移除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "移除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "移除成功"})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -8,26 +8,26 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *KubernetesHandler) ListClusters(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.KubernetesCluster{}).Count(&total)
|
||||
h.db.Model(&model.KubernetesCluster{}).Count(&total)
|
||||
|
||||
var clusters []models.KubernetesCluster
|
||||
var clusters []model.KubernetesCluster
|
||||
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&clusters)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: clusters,
|
||||
Total: total,
|
||||
},
|
||||
@@ -41,21 +41,21 @@ func (h *KubernetesHandler) ListClusters(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
||||
// @Success 200 {object} model.Response{data=model.KubernetesCluster}
|
||||
// @Router /kubernetes-clusters/{id} [get]
|
||||
func (h *KubernetesHandler) GetCluster(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "集群不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: cluster,
|
||||
@@ -69,20 +69,20 @@ func (h *KubernetesHandler) GetCluster(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateKubernetesClusterRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
||||
// @Param request body model.CreateKubernetesClusterRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.KubernetesCluster}
|
||||
// @Router /kubernetes-clusters [post]
|
||||
func (h *KubernetesHandler) CreateCluster(c *gin.Context) {
|
||||
var req models.CreateKubernetesClusterRequest
|
||||
var req model.CreateKubernetesClusterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cluster := models.KubernetesCluster{
|
||||
cluster := model.KubernetesCluster{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Version: req.Version,
|
||||
@@ -96,14 +96,14 @@ func (h *KubernetesHandler) CreateCluster(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&cluster).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: cluster,
|
||||
@@ -118,8 +118,8 @@ func (h *KubernetesHandler) CreateCluster(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Param request body models.CreateKubernetesClusterRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
||||
// @Param request body model.CreateKubernetesClusterRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.KubernetesCluster}
|
||||
// @Router /kubernetes-clusters/{id} [put]
|
||||
func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
@@ -127,16 +127,16 @@ func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
// 用 map 接收,避免 binding:"required" 校验 // Logan.Li
|
||||
var req map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "集群不存在",
|
||||
})
|
||||
@@ -170,7 +170,7 @@ func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "无更新内容",
|
||||
Data: cluster,
|
||||
@@ -179,7 +179,7 @@ func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&cluster).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -188,7 +188,7 @@ func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
|
||||
h.db.First(&cluster, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: cluster,
|
||||
@@ -199,9 +199,9 @@ func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
||||
func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "集群不存在",
|
||||
})
|
||||
@@ -209,7 +209,7 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 返回时手动脱敏:token/kube_config 只返回是否存在标记,不返回原文
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: map[string]interface{}{
|
||||
@@ -245,20 +245,20 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /kubernetes-clusters/{id} [delete]
|
||||
func (h *KubernetesHandler) DeleteCluster(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
if err := h.db.Delete(&models.KubernetesCluster{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.KubernetesCluster{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -271,25 +271,25 @@ func (h *KubernetesHandler) DeleteCluster(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Success 200 {object} models.Response{data=[]models.KubernetesNode}
|
||||
// @Success 200 {object} model.Response{data=[]model.KubernetesNode}
|
||||
// @Router /kubernetes-clusters/{id}/nodes [get]
|
||||
func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "集群不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "集群不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if cluster.KubeConfig == "" && cluster.Token == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "集群未配置凭证"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "集群未配置凭证"})
|
||||
return
|
||||
}
|
||||
|
||||
client, err := k8sclient.NewClient(cluster.KubeConfig)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "连接K8s失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "连接K8s失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
|
||||
// Sync pods
|
||||
if pods, err := client.ListPods(); err == nil {
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesPod{})
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&model.KubernetesPod{})
|
||||
for _, p := range pods.Items {
|
||||
image := ""
|
||||
if len(p.Spec.Containers) > 0 {
|
||||
@@ -381,7 +381,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
restarts = p.Status.ContainerStatuses[0].RestartCount
|
||||
}
|
||||
labelsJSON, _ := json.Marshal(p.Metadata.Labels)
|
||||
h.db.Create(&models.KubernetesPod{
|
||||
h.db.Create(&model.KubernetesPod{
|
||||
ClusterID: cluster.ID,
|
||||
Namespace: p.Metadata.Namespace,
|
||||
Name: p.Metadata.Name,
|
||||
@@ -400,14 +400,14 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
|
||||
// Sync deployments
|
||||
if deps, err := client.ListDeployments(); err == nil {
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesDeployment{})
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&model.KubernetesDeployment{})
|
||||
for _, d := range deps.Items {
|
||||
image := ""
|
||||
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||||
image = d.Spec.Template.Spec.Containers[0].Image
|
||||
}
|
||||
labelsJSON, _ := json.Marshal(d.Metadata.Labels)
|
||||
h.db.Create(&models.KubernetesDeployment{
|
||||
h.db.Create(&model.KubernetesDeployment{
|
||||
ClusterID: cluster.ID,
|
||||
Namespace: d.Metadata.Namespace,
|
||||
Name: d.Metadata.Name,
|
||||
@@ -425,12 +425,12 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
|
||||
// Sync services
|
||||
if svcs, err := client.ListServices(); err == nil {
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesService{})
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&model.KubernetesService{})
|
||||
for _, s := range svcs.Items {
|
||||
portsJSON, _ := json.Marshal(s.Spec.Ports)
|
||||
selectorJSON, _ := json.Marshal(s.Spec.Selector)
|
||||
labelsJSON, _ := json.Marshal(s.Metadata.Labels)
|
||||
h.db.Create(&models.KubernetesService{
|
||||
h.db.Create(&model.KubernetesService{
|
||||
ClusterID: cluster.ID,
|
||||
Namespace: s.Metadata.Namespace,
|
||||
Name: s.Metadata.Name,
|
||||
@@ -446,7 +446,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
|
||||
// Sync ingresses
|
||||
if ings, err := client.ListIngresses(); err == nil {
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesIngress{})
|
||||
h.db.Where("cluster_id = ?", cluster.ID).Delete(&model.KubernetesIngress{})
|
||||
for _, ing := range ings.Items {
|
||||
hosts := []string{}
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
@@ -462,7 +462,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
}
|
||||
tlsHostsJSON, _ := json.Marshal(tlsHosts)
|
||||
labelsJSON, _ := json.Marshal(ing.Metadata.Labels)
|
||||
h.db.Create(&models.KubernetesIngress{
|
||||
h.db.Create(&model.KubernetesIngress{
|
||||
ClusterID: cluster.ID,
|
||||
Namespace: ing.Metadata.Namespace,
|
||||
Name: ing.Metadata.Name,
|
||||
@@ -485,7 +485,7 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
"last_sync_at": &now,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("同步完成: %d nodes, %d pods, %d deployments, %d services, %d ingresses",
|
||||
result.Nodes, result.Pods, result.Deployments, result.Services, result.Ingresses),
|
||||
@@ -501,17 +501,17 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
||||
func (h *KubernetesHandler) ListServiceAccountsNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
sas, err := client.ListServiceAccounts(c.Param("namespace"))
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
type SASummary struct {
|
||||
@@ -535,24 +535,24 @@ func (h *KubernetesHandler) ListServiceAccountsNS(c *gin.Context) {
|
||||
Age: formatAge(sa.Metadata.CreationTimestamp),
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ListRolesNS lists Roles in a namespace
|
||||
func (h *KubernetesHandler) ListRolesNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
roles, err := client.ListRoles(c.Param("namespace"))
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
type RoleSummary struct {
|
||||
@@ -591,24 +591,24 @@ func (h *KubernetesHandler) ListRolesNS(c *gin.Context) {
|
||||
Age: formatAge(r.Metadata.CreationTimestamp),
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ListRoleBindingsNS lists RoleBindings in a namespace
|
||||
func (h *KubernetesHandler) ListRoleBindingsNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
rbs, err := client.ListRoleBindings(c.Param("namespace"))
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
type RBSummary struct {
|
||||
@@ -638,5 +638,5 @@ func (h *KubernetesHandler) ListRoleBindingsNS(c *gin.Context) {
|
||||
Age: formatAge(rb.Metadata.CreationTimestamp),
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
||||
// 1. 获取所有集群
|
||||
var clusters []models.KubernetesCluster
|
||||
var clusters []model.KubernetesCluster
|
||||
h.db.Find(&clusters)
|
||||
|
||||
type ClusterComparison struct {
|
||||
@@ -52,7 +52,7 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
||||
}
|
||||
nodeAggMap := map[uint]*NodeAgg{}
|
||||
{
|
||||
var nodes []models.KubernetesNode
|
||||
var nodes []model.KubernetesNode
|
||||
h.db.Find(&nodes)
|
||||
for _, n := range nodes {
|
||||
agg, ok := nodeAggMap[n.ClusterID]
|
||||
@@ -123,7 +123,7 @@ func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 4. 返回结果
|
||||
c.JSON(200, models.Response{
|
||||
c.JSON(200, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,21 +6,21 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
|
||||
ns := c.Query("namespace")
|
||||
eventType := c.Query("type") // Normal or Warning
|
||||
events, err := client.ListEvents(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
|
||||
type EventSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
@@ -48,7 +48,7 @@ func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
||||
FirstTime: firstTime, LastTime: lastTime,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -59,19 +59,19 @@ func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
||||
func (h *KubernetesHandler) GetEventsSummary(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ns := c.Query("namespace")
|
||||
events, err := client.ListEvents(ns)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func (h *KubernetesHandler) GetEventsSummary(c *gin.Context) {
|
||||
"recent_warnings": recentWarnings,
|
||||
}
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
+59
-59
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,17 +15,17 @@ func (h *KubernetesHandler) ListHPAs(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: hpas.Items})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: hpas.Items})
|
||||
}
|
||||
|
||||
// CreateHPA 创建 HPA
|
||||
@@ -34,27 +34,27 @@ func (h *KubernetesHandler) CreateHPA(c *gin.Context) {
|
||||
|
||||
var req k8sclient.CreateHPARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Namespace == "" || req.TargetName == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "name/namespace/targetName 必填"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "name/namespace/targetName 必填"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
hpa, err := client.CreateHPA(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建 HPA 失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建 HPA 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "HPA 创建成功", Data: hpa})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "HPA 创建成功", Data: hpa})
|
||||
}
|
||||
|
||||
// DeleteHPA 删除 HPA
|
||||
@@ -65,16 +65,16 @@ func (h *KubernetesHandler) DeleteHPA(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.DeleteHPA(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除 HPA 失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除 HPA 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "HPA 已删除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "HPA 已删除"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -87,17 +87,17 @@ func (h *KubernetesHandler) ListCronJobs(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cronjobs, err := client.ListCronJobs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cronjobs.Items})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cronjobs.Items})
|
||||
}
|
||||
|
||||
// CreateCronJob 创建 CronJob
|
||||
@@ -106,23 +106,23 @@ func (h *KubernetesHandler) CreateCronJob(c *gin.Context) {
|
||||
|
||||
var req k8sclient.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cj, err := client.CreateCronJob(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建 CronJob 失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建 CronJob 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 创建成功", Data: cj})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 创建成功", Data: cj})
|
||||
}
|
||||
|
||||
// SuspendCronJob 暂停/恢复 CronJob
|
||||
@@ -138,12 +138,12 @@ func (h *KubernetesHandler) SuspendCronJob(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.SuspendCronJob(namespace, name, req.Suspend); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "操作失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "操作失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func (h *KubernetesHandler) SuspendCronJob(c *gin.Context) {
|
||||
if req.Suspend {
|
||||
action = "暂停"
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
}
|
||||
|
||||
// DeleteCronJob 删除 CronJob
|
||||
@@ -162,16 +162,16 @@ func (h *KubernetesHandler) DeleteCronJob(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.DeleteCronJob(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除 CronJob 失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除 CronJob 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 已删除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 已删除"})
|
||||
}
|
||||
|
||||
// TriggerCronJob 手动触发 CronJob
|
||||
@@ -182,16 +182,16 @@ func (h *KubernetesHandler) TriggerCronJob(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.TriggerCronJob(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "触发失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "触发失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "手动触发成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "手动触发成功"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -227,9 +227,9 @@ func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type HPASummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -263,84 +263,84 @@ func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
||||
CurrentReplicas: h.Status.CurrentReplicas,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req k8sclient.CreateHPARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if req.Namespace == "" { req.Namespace = ns }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
hpa, err := client.CreateHPA(req)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Data: hpa})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: hpa})
|
||||
}
|
||||
func (h *KubernetesHandler) deleteHPAWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteHPA(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "已删除"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteHPA(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
func (h *KubernetesHandler) listCronJobsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
cjs, err := client.ListCronJobs()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
filtered := []k8sclient.CronJobItem{}
|
||||
for _, cj := range cjs.Items { if cj.Metadata.Namespace == ns { filtered = append(filtered, cj) } }
|
||||
c.JSON(200, models.Response{Code: 200, Data: filtered})
|
||||
c.JSON(200, model.Response{Code: 200, Data: filtered})
|
||||
}
|
||||
func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req k8sclient.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if req.Namespace == "" { req.Namespace = ns }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
cj, err := client.CreateCronJob(req)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Data: cj})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: cj})
|
||||
}
|
||||
func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req struct { Suspend bool `json:"suspend"` }; c.ShouldBindJSON(&req)
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, models.Response{Code: 500, Message: "操作失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()}); return }
|
||||
action := "恢复"; if req.Suspend { action = "暂停" }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
}
|
||||
func (h *KubernetesHandler) triggerCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "触发失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "手动触发成功"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "手动触发成功"})
|
||||
}
|
||||
func (h *KubernetesHandler) deleteCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "已删除"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type HPASummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -373,7 +373,7 @@ func (h *KubernetesHandler) ListHPAsAllNS(c *gin.Context) {
|
||||
CurrentReplicas: h.Status.CurrentReplicas,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
+24
-24
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,17 +19,17 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
namespaces, err := client.ListNamespaces()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: namespaces})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: namespaces})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -57,18 +57,18 @@ func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
|
||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
|
||||
return
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
nsList, err := client.ListNamespacesDetailed()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
|
||||
result = append(result, nd)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// GetNamespaceDetailHandler returns a single namespace detail with quota info
|
||||
@@ -145,7 +145,7 @@ func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
|
||||
return
|
||||
}
|
||||
nsName := c.Param("namespace")
|
||||
@@ -155,13 +155,13 @@ func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ns, err := client.GetNamespace(nsName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
||||
nd.LimitRanges = lrs
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: nd})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: nd})
|
||||
}
|
||||
|
||||
// CreateNamespaceHandler creates a new namespace
|
||||
@@ -220,7 +220,7 @@ func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
|
||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "name is required"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -239,23 +239,23 @@ func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
|
||||
"default": true, "kubernetes-dashboard": true,
|
||||
}
|
||||
if systemNS[req.Name] {
|
||||
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
|
||||
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ns, err := client.CreateNamespace(req.Name, req.Labels)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ns, Message: "命名空间创建成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ns, Message: "命名空间创建成功"})
|
||||
}
|
||||
|
||||
// DeleteNamespaceHandler deletes a namespace
|
||||
@@ -265,7 +265,7 @@ func (h *KubernetesHandler) DeleteNamespaceHandler(c *gin.Context) {
|
||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -280,22 +280,22 @@ func (h *KubernetesHandler) DeleteNamespaceHandler(c *gin.Context) {
|
||||
"default": true, "devops": true,
|
||||
}
|
||||
if systemNS[nsName] {
|
||||
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
|
||||
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.DeleteNamespace(nsName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "命名空间已删除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "命名空间已删除"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -17,12 +17,12 @@ func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
nodes, err := client.ListNodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
||||
}
|
||||
result = append(result, s)
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// GetNodeDetail 获取节点详情(PID/容器运行时/分配率)// M10
|
||||
@@ -109,26 +109,26 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
||||
clusterID = h.resolveClusterCode(code)
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not found"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
|
||||
nodeName := c.Param("name")
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "node name required"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "node name required"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Get node detail
|
||||
node, err := client.GetNode(nodeName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
||||
"pods": podList,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: detail})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: detail})
|
||||
}
|
||||
|
||||
// parseCpuToMilli converts CPU resource string to millicores (e.g. "100m"→100, "2"→2000)
|
||||
@@ -380,17 +380,17 @@ func parseMemToMi(val string) int64 {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Success 200 {object} models.Response{data=[]models.KubernetesPod}
|
||||
// @Success 200 {object} model.Response{data=[]model.KubernetesPod}
|
||||
// @Router /kubernetes-clusters/{id}/pods [get]
|
||||
func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
|
||||
ns := c.Query("namespace")
|
||||
deps, err := client.ListDeployments()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
|
||||
type ContainerResource struct {
|
||||
Name string `json:"name"`
|
||||
@@ -418,7 +418,7 @@ func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
||||
}
|
||||
result = append(result, CapacityRow{d.Metadata.Namespace, d.Metadata.Name, d.Spec.Replicas, containers})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -24,12 +24,12 @@ func (h *KubernetesHandler) ListPods(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
pods, err := client.ListPods()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (h *KubernetesHandler) ListPods(c *gin.Context) {
|
||||
}
|
||||
result = append(result, ps)
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// ListDeployments 获取Deployment列表
|
||||
@@ -86,7 +86,7 @@ func (h *KubernetesHandler) ListPods(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Success 200 {object} models.Response{data=[]models.KubernetesDeployment}
|
||||
// @Success 200 {object} model.Response{data=[]model.KubernetesDeployment}
|
||||
// @Router /kubernetes-clusters/{id}/deployments [get]
|
||||
func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
@@ -98,7 +98,7 @@ func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
||||
// Real-time streaming via chunked transfer
|
||||
body, err := client.GetPodLogsStream(namespace, podName, container, tailLines)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "获取日志流失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer body.Close()
|
||||
@@ -133,11 +133,11 @@ func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
||||
|
||||
logs, err := client.GetPodLogs(namespace, podName, container, tailLines)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "获取日志失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"logs": logs, "pod": podName, "namespace": namespace}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"logs": logs, "pod": podName, "namespace": namespace}})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -171,7 +171,7 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||
|
||||
client, cluster, err := h.getK8sClientByCode(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||
k8sConn, _, err := k8sDialer.Dial(execURL, k8sHeader)
|
||||
if err != nil {
|
||||
log.Printf("[K8sExec] dial K8s exec WS failed: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s exec 连接失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s exec 连接失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer k8sConn.Close()
|
||||
@@ -308,27 +308,27 @@ func (h *KubernetesHandler) ExecInPod(c *gin.Context) {
|
||||
Container string `json:"container"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
if len(req.Command) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "command 不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "command 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := client.ExecInPod(namespace, podName, req.Container, req.Command)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "执行失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "执行失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -351,9 +351,9 @@ func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
pods, err := client.ListPods()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||
podMetrics := buildPodMetricsMap(client, ns)
|
||||
type PodSummary struct {
|
||||
@@ -387,7 +387,7 @@ func (h *KubernetesHandler) listPodsWithNS(c *gin.Context, ns string) {
|
||||
}
|
||||
result = append(result, ps)
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
@@ -409,10 +409,10 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||
Previous: c.Query("previous") == "true",
|
||||
}
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if stream {
|
||||
body, err := client.GetPodLogsStreamWithOptions(ns, podName, opts)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}); return }
|
||||
defer body.Close()
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
@@ -424,20 +424,20 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||
return
|
||||
}
|
||||
logs, err := client.GetPodLogsWithOptions(ns, podName, opts)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "success", Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
||||
}
|
||||
func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
podName := c.Param("podName")
|
||||
var req struct { Command []string `json:"command"`; Container string `json:"container"` }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
result, err := client.ExecInPod(ns, podName, req.Container, req.Command)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "执行失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -447,11 +447,11 @@ func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
||||
// ListDeploymentsAllNS lists all deployments across all namespaces using clusterCode.
|
||||
func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
pods, err := client.ListPods()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||
podMetrics := buildPodMetricsMap(client, "")
|
||||
type PodSummary struct {
|
||||
@@ -484,7 +484,7 @@ func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
||||
}
|
||||
result = append(result, ps)
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ListHPAsAllNS lists all HPAs across all namespaces using clusterCode.
|
||||
@@ -585,13 +585,13 @@ func parseMemKiToMi(raw string) int {
|
||||
// GetResourceYAMLNS returns the YAML of a K8s resource
|
||||
func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
|
||||
ns := c.Query("namespace")
|
||||
metrics, err := client.ListPodMetrics(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "metrics API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "metrics API error: " + err.Error()}); return }
|
||||
|
||||
type ContainerMetric struct {
|
||||
Name string `json:"name"`
|
||||
@@ -611,7 +611,7 @@ func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) {
|
||||
}
|
||||
result = append(result, PodMetric{pm.Metadata.Namespace, pm.Metadata.Name, containers})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -626,13 +626,13 @@ func (h *KubernetesHandler) deletePodWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
podName := c.Param("podName")
|
||||
if podName == "" { c.JSON(400, models.Response{Code: 400, Message: "缺少 podName"}); return }
|
||||
if podName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 podName"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeletePod(ns, podName); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)})
|
||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
+15
-15
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -25,14 +25,14 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
||||
Path string `json:"path"`
|
||||
Container string `json:"container"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||
if req.Path == "" { req.Path = "/" }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
// ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format
|
||||
cmd := []string{"ls", "-la", req.Path}
|
||||
result, err := client.ExecInPod(ns, podName, req.Container, cmd)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}); return }
|
||||
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
@@ -73,7 +73,7 @@ func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
|
||||
IsDir: mode[0] == 'd',
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "success", Data: gin.H{"path": req.Path, "files": files}})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"path": req.Path, "files": files}})
|
||||
}
|
||||
|
||||
// DownloadPodFileNS 下载 Pod 内指定路径的文件(通过 exec cat)
|
||||
@@ -86,12 +86,12 @@ func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) {
|
||||
podName := c.Param("podName")
|
||||
filePath := c.Query("path")
|
||||
container := c.Query("container")
|
||||
if filePath == "" { c.JSON(400, models.Response{Code: 400, Message: "path 参数不能为空"}); return }
|
||||
if filePath == "" { c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
cmd := []string{"cat", filePath}
|
||||
result, err := client.ExecInPod(ns, podName, container, cmd)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "读取文件失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()}); return }
|
||||
content := result.Stdout
|
||||
fileName := path.Base(filePath)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||
@@ -118,7 +118,7 @@ func (h *KubernetesHandler) uploadPodFileWithNS(c *gin.Context, ns string) {
|
||||
// 读取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(400, models.Response{Code: 400, Message: "文件上传失败: " + err.Error()})
|
||||
c.JSON(400, model.Response{Code: 400, Message: "文件上传失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -126,19 +126,19 @@ func (h *KubernetesHandler) uploadPodFileWithNS(c *gin.Context, ns string) {
|
||||
// 限制文件大小 10MB
|
||||
const maxUploadSize = 10 << 20 // 10MB
|
||||
if header.Size > maxUploadSize {
|
||||
c.JSON(400, models.Response{Code: 400, Message: fmt.Sprintf("文件过大: %d bytes, 上限 %d bytes", header.Size, maxUploadSize)})
|
||||
c.JSON(400, model.Response{Code: 400, Message: fmt.Sprintf("文件过大: %d bytes, 上限 %d bytes", header.Size, maxUploadSize)})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (h *KubernetesHandler) uploadPodFileWithNS(c *gin.Context, ns string) {
|
||||
if result != nil {
|
||||
stderr = result.Stderr
|
||||
}
|
||||
c.JSON(500, models.Response{Code: 500, Message: fmt.Sprintf("写入文件失败 (chunk %d): %s\n%s", i/chunkSize, err.Error(), stderr)})
|
||||
c.JSON(500, model.Response{Code: 500, Message: fmt.Sprintf("写入文件失败 (chunk %d): %s\n%s", i/chunkSize, err.Error(), stderr)})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func (h *KubernetesHandler) uploadPodFileWithNS(c *gin.Context, ns string) {
|
||||
|
||||
log.Printf("[UploadPodFile] %s/%s → %s (%d bytes, %d chunks)", ns, podName, targetFile, len(content), (len(encoded)+chunkSize-1)/chunkSize)
|
||||
|
||||
c.JSON(200, models.Response{
|
||||
c.JSON(200, model.Response{
|
||||
Code: 200,
|
||||
Message: "上传成功",
|
||||
Data: gin.H{
|
||||
+35
-35
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -15,12 +15,12 @@ func (h *KubernetesHandler) ListServices(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
svcs, err := client.ListServices()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (h *KubernetesHandler) ListServices(c *gin.Context) {
|
||||
Ports: len(s.Spec.Ports),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// ListIngresses 获取Ingress列表
|
||||
@@ -55,7 +55,7 @@ func (h *KubernetesHandler) ListServices(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Success 200 {object} models.Response{data=[]models.KubernetesIngress}
|
||||
// @Success 200 {object} model.Response{data=[]model.KubernetesIngress}
|
||||
// @Router /kubernetes-clusters/{id}/ingresses [get]
|
||||
func (h *KubernetesHandler) ListIngresses(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
@@ -63,12 +63,12 @@ func (h *KubernetesHandler) ListIngresses(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
ings, err := client.ListIngresses()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,19 +96,19 @@ func (h *KubernetesHandler) ListIngresses(c *gin.Context) {
|
||||
TLS: len(ing.Spec.TLS) > 0,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// GetTopology 获取集群拓扑布局 // Logan.Li
|
||||
func (h *KubernetesHandler) GetTopology(c *gin.Context) {
|
||||
clusterID := c.Param("id")
|
||||
var layout models.TopologyLayout
|
||||
var layout model.TopologyLayout
|
||||
result := h.db.Where("cluster_id = ?", clusterID).First(&layout)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: &models.TopologyLayout{}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: &model.TopologyLayout{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: layout})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: layout})
|
||||
}
|
||||
|
||||
// SaveTopology 保存集群拓扑布局 // Logan.Li
|
||||
@@ -118,11 +118,11 @@ func (h *KubernetesHandler) SaveTopology(c *gin.Context) {
|
||||
LayoutData string `json:"layout_data"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
var layout models.TopologyLayout
|
||||
var layout model.TopologyLayout
|
||||
h.db.Where("cluster_id = ?", clusterID).First(&layout)
|
||||
layout.ClusterID = uint(clusterID)
|
||||
layout.LayoutData = req.LayoutData
|
||||
@@ -132,7 +132,7 @@ func (h *KubernetesHandler) SaveTopology(c *gin.Context) {
|
||||
h.db.Save(&layout)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功"})
|
||||
}
|
||||
|
||||
|
||||
@@ -146,17 +146,17 @@ func (h *KubernetesHandler) ListConfigMaps(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cms, err := client.ListConfigMaps(namespace)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cms.Items})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cms.Items})
|
||||
}
|
||||
|
||||
// ListSecrets 获取命名空间下的 Secret 列表(仅名称+类型,不含值)
|
||||
@@ -169,17 +169,17 @@ func (h *KubernetesHandler) ListSecrets(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
secrets, err := client.ListSecrets(namespace)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: secrets.Items})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: secrets.Items})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -197,9 +197,9 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
svcs, err := client.ListServices()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type SvcPortDetail struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Port int `json:"port"`
|
||||
@@ -236,15 +236,15 @@ func (h *KubernetesHandler) listServicesWithNS(c *gin.Context, ns string) {
|
||||
Selector: s.Spec.Selector,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ings, err := client.ListIngresses()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type PathDetail struct {
|
||||
Path string `json:"path"`
|
||||
PathType string `json:"pathType,omitempty"`
|
||||
@@ -298,16 +298,16 @@ func (h *KubernetesHandler) listIngressesWithNS(c *gin.Context, ns string) {
|
||||
Rules: rules,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
pvcs, err := client.ListPVCs(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type PVCSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -329,19 +329,19 @@ func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
||||
StorageClass: p.Spec.StorageClassName,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// DeletePVCNS deletes a PVC (code-based)
|
||||
func (h *KubernetesHandler) DeletePVCNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "已删除"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
+112
-112
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -17,12 +17,12 @@ func (h *KubernetesHandler) ListDeployments(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
deps, err := client.ListDeployments()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (h *KubernetesHandler) ListDeployments(c *gin.Context) {
|
||||
Image: image,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// ListServices 获取Service列表
|
||||
@@ -63,7 +63,7 @@ func (h *KubernetesHandler) ListDeployments(c *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @Param id path int true "集群ID"
|
||||
// @Param namespace query string false "命名空间"
|
||||
// @Success 200 {object} models.Response{data=[]models.KubernetesService}
|
||||
// @Success 200 {object} model.Response{data=[]model.KubernetesService}
|
||||
// @Router /kubernetes-clusters/{id}/services [get]
|
||||
func (h *KubernetesHandler) ScaleDeployment(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
@@ -74,26 +74,26 @@ func (h *KubernetesHandler) ScaleDeployment(c *gin.Context) {
|
||||
Replicas int `json:"replicas"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
if req.Replicas < 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "副本数不能为负数"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "副本数不能为负数"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.ScaleDeployment(namespace, name, req.Replicas); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||||
}
|
||||
|
||||
// RestartDeployment 重启 Deployment (滚动重启)
|
||||
@@ -104,16 +104,16 @@ func (h *KubernetesHandler) RestartDeployment(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.RestartDeployment(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "重启指令已下发"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "重启指令已下发"})
|
||||
}
|
||||
|
||||
// PauseDeployment 暂停 Deployment 滚动更新(spec.paused=true)
|
||||
@@ -124,16 +124,16 @@ func (h *KubernetesHandler) PauseDeployment(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.PauseDeployment(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||
}
|
||||
|
||||
// ResumeDeployment 恢复 Deployment 滚动更新(spec.paused=false)
|
||||
@@ -144,16 +144,16 @@ func (h *KubernetesHandler) ResumeDeployment(c *gin.Context) {
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.ResumeDeployment(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -171,26 +171,26 @@ func (h *KubernetesHandler) UpdateDeploymentImage(c *gin.Context) {
|
||||
ContainerName string `json:"container_name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
if req.Image == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "image 不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "image 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.UpdateDeploymentImage(namespace, name, req.ContainerName, req.Image); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新镜像失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s,正在滚动更新...", req.Image)})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s,正在滚动更新...", req.Image)})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -209,14 +209,14 @@ func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
deps, err := client.ListDeployments()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
for _, d := range deps.Items {
|
||||
if d.Metadata.Namespace == ns && d.Metadata.Name == name {
|
||||
image := ""
|
||||
if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
||||
c.JSON(200, models.Response{Code: 200, Data: map[string]interface{}{
|
||||
c.JSON(200, model.Response{Code: 200, Data: map[string]interface{}{
|
||||
"namespace": d.Metadata.Namespace,
|
||||
"name": d.Metadata.Name,
|
||||
"replicas": d.Spec.Replicas,
|
||||
@@ -228,7 +228,7 @@ func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(404, models.Response{Code: 404, Message: "Deployment not found"})
|
||||
c.JSON(404, model.Response{Code: 404, Message: "Deployment not found"})
|
||||
}
|
||||
func (h *KubernetesHandler) ScaleDeploymentNS(c *gin.Context) {
|
||||
h.scaleDeploymentWithNS(c, c.Param("namespace"))
|
||||
@@ -249,9 +249,9 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
deps, err := client.ListDeployments()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type DepSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -277,59 +277,59 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
||||
Selector: func() map[string]string { if d.Spec.Selector != nil { return d.Spec.Selector.MatchLabels }; return nil }(),
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) scaleDeploymentWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req struct { Replicas int `json:"replicas"` }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||||
}
|
||||
func (h *KubernetesHandler) restartDeploymentWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.RestartDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "重启指令已下发"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.RestartDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "重启指令已下发"})
|
||||
}
|
||||
func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req struct { Image string `json:"image"`; ContainerName string `json:"container_name"` }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, models.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
||||
}
|
||||
func (h *KubernetesHandler) pauseDeploymentWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||
}
|
||||
func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||
}
|
||||
func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
deps, err := client.ListDeployments()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type DepSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -350,17 +350,17 @@ func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
||||
Image: image,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ListPodsAllNS lists all pods across all namespaces using clusterCode.
|
||||
func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
sts, err := client.ListStatefulSets()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
type StsSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
@@ -381,17 +381,17 @@ func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
||||
Image: image,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ListDaemonSetsNS lists DaemonSets in a namespace
|
||||
func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ds, err := client.ListDaemonSets()
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
type DsSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
@@ -414,123 +414,123 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
||||
Image: image,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
// ScaleStatefulSetNS scales a StatefulSet (Kuboard code route)
|
||||
func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
var req struct{ Replicas int `json:"replicas"` }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||
}
|
||||
|
||||
// ScaleStatefulSet scales a StatefulSet (ID route)
|
||||
func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
var req struct{ Replicas int `json:"replicas"` }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||
}
|
||||
|
||||
// RestartStatefulSetNS restarts a StatefulSet (Kuboard code route)
|
||||
func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||
}
|
||||
|
||||
// RestartStatefulSet restarts a StatefulSet (ID route)
|
||||
func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||
}
|
||||
|
||||
// RestartDaemonSetNS restarts a DaemonSet (Kuboard code route)
|
||||
func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||
}
|
||||
|
||||
// RestartDaemonSet restarts a DaemonSet (ID route)
|
||||
func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||
}
|
||||
|
||||
// GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route)
|
||||
func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
sts, err := client.GetStatefulSet(ns, name)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Data: sts})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: sts})
|
||||
}
|
||||
|
||||
// GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route)
|
||||
func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
ds, err := client.GetDaemonSet(ns, name)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
c.JSON(200, models.Response{Code: 200, Data: ds})
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: ds})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -545,9 +545,9 @@ func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
items, err := client.ListReplicaSetsRaw(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type RSSummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
@@ -580,7 +580,7 @@ func (h *KubernetesHandler) listReplicaSetsWithNS(c *gin.Context, ns string) {
|
||||
OwnerDeployment: owner,
|
||||
})
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// ListDeploymentRevisionsNS Deployment 发布历史(按 revision 倒序)
|
||||
@@ -591,11 +591,11 @@ func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns str
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
deploymentName := c.Param("name")
|
||||
if deploymentName == "" { c.JSON(400, models.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
items, err := client.ListReplicaSetsRaw(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type Revision struct {
|
||||
Revision string `json:"revision"`
|
||||
ReplicaSet string `json:"replica_set"`
|
||||
@@ -628,7 +628,7 @@ func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns str
|
||||
rj, _ := strconv.Atoi(result[j].Revision)
|
||||
return ri > rj
|
||||
})
|
||||
c.JSON(200, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// RollbackDeploymentNS 回滚 Deployment 到指定 revision 或指定 ReplicaSet
|
||||
@@ -639,20 +639,20 @@ func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string)
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
deploymentName := c.Param("name")
|
||||
if deploymentName == "" { c.JSON(400, models.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||||
var req struct {
|
||||
Revision int `json:"revision"`
|
||||
ReplicaSet string `json:"replicaSet"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req) // 允许空 body(但下面会校验)
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
|
||||
targetRS := req.ReplicaSet
|
||||
// 通过 revision 定位 ReplicaSet
|
||||
if targetRS == "" && req.Revision > 0 {
|
||||
items, err := client.ListReplicaSetsRaw(ns)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}); return }
|
||||
for _, rs := range items {
|
||||
isOwner := false
|
||||
for _, o := range rs.Metadata.OwnerReferences {
|
||||
@@ -665,24 +665,24 @@ func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string)
|
||||
}
|
||||
}
|
||||
if targetRS == "" {
|
||||
c.JSON(404, models.Response{Code: 404, Message: fmt.Sprintf("revision %d 的 ReplicaSet 未找到", req.Revision)})
|
||||
c.JSON(404, model.Response{Code: 404, Message: fmt.Sprintf("revision %d 的 ReplicaSet 未找到", req.Revision)})
|
||||
return
|
||||
}
|
||||
}
|
||||
if targetRS == "" { c.JSON(400, models.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}); return }
|
||||
if targetRS == "" { c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}); return }
|
||||
|
||||
// 取 ReplicaSet 的 template,patch 到 Deployment.spec.template 实现回滚
|
||||
rsJSON, err := client.GetReplicaSet(ns, targetRS)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return }
|
||||
specMap, ok := rsJSON["spec"].(map[string]interface{})
|
||||
if !ok { c.JSON(500, models.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return }
|
||||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return }
|
||||
template, ok := specMap["template"].(map[string]interface{})
|
||||
if !ok { c.JSON(500, models.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return }
|
||||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return }
|
||||
|
||||
if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{
|
||||
c.JSON(200, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("Deployment %s 已回滚到 %s", deploymentName, targetRS),
|
||||
Data: gin.H{"replicaSet": targetRS, "revision": req.Revision},
|
||||
@@ -1,20 +1,20 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
resource := c.Param("resource")
|
||||
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||
@@ -62,10 +62,10 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||
case "rolebindings", "rolebinding":
|
||||
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||
default:
|
||||
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
}
|
||||
yamlBytes, err := client.GetResourceYAML(apiPath)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return }
|
||||
c.Header("Content-Type", "application/yaml")
|
||||
c.String(200, string(yamlBytes))
|
||||
}
|
||||
@@ -73,9 +73,9 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||
// UpdateResourceYAMLNS applies YAML to update a K8s resource
|
||||
func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
resource := c.Param("resource")
|
||||
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||
@@ -90,7 +90,7 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||
}
|
||||
name := c.Param("name")
|
||||
yamlData, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil { c.JSON(400, models.Response{Code: 400, Message: "读取body失败"}); return }
|
||||
if err != nil { c.JSON(400, model.Response{Code: 400, Message: "读取body失败"}); return }
|
||||
var apiPath string
|
||||
switch resource {
|
||||
case "deployments", "deployment":
|
||||
@@ -124,12 +124,12 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||
case "rolebindings", "rolebinding":
|
||||
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||
default:
|
||||
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
}
|
||||
if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "更新失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "YAML已应用"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "YAML已应用"})
|
||||
}
|
||||
|
||||
// resourceAPIPath builds the K8s API path for common resource types (shared by Get/Update/Delete)
|
||||
@@ -173,37 +173,37 @@ func resourceAPIPath(ns, resource, name string) (string, error) {
|
||||
// DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.)
|
||||
func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
ns := c.Param("namespace")
|
||||
resource := c.Param("resource")
|
||||
name := c.Param("name")
|
||||
apiPath, err := resourceAPIPath(ns, resource, name)
|
||||
if err != nil { c.JSON(400, models.Response{Code: 400, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(400, model.Response{Code: 400, Message: err.Error()}); return }
|
||||
if err := client.DeleteResource(apiPath); err != nil {
|
||||
c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||
}
|
||||
c.JSON(200, models.Response{Code: 200, Message: "资源已删除"})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "资源已删除"})
|
||||
}
|
||||
|
||||
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
||||
func (h *KubernetesHandler) ApplyYAML(c *gin.Context) {
|
||||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
|
||||
var req struct {
|
||||
YAML string `json:"yaml"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||
if req.YAML == "" { c.JSON(400, models.Response{Code: 400, Message: "YAML 内容不能为空"}); return }
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||
if req.YAML == "" { c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"}); return }
|
||||
|
||||
result, err := client.ApplyYAML(req.YAML)
|
||||
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return }
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return }
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "资源已应用", Data: result})
|
||||
c.JSON(200, model.Response{Code: 200, Message: "资源已应用", Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/toolinstaller"
|
||||
|
||||
"github.com/creack/pty"
|
||||
@@ -58,7 +58,7 @@ func (h *KubectlShellHandler) Terminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "cluster not found"})
|
||||
return
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -30,7 +30,7 @@ func (h *KubernetesHandler) resolveClusterCode(code string) int {
|
||||
if code == "" {
|
||||
return 0
|
||||
}
|
||||
var cluster models.KubernetesCluster
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.Where("code = ?", code).First(&cluster).Error; err != nil {
|
||||
return 0
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func formatAge(creationTimestamp string) string {
|
||||
}
|
||||
|
||||
// getK8sClientByCode resolves cluster by code, falls back to ID
|
||||
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
||||
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *model.KubernetesCluster, error) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 {
|
||||
// 支持 :clusterCode 或 :id 参数
|
||||
@@ -84,8 +84,8 @@ func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Clien
|
||||
// NS wrappers — read namespace from path (Kuboard-style routes)
|
||||
// ============================================================================
|
||||
|
||||
func (h *KubernetesHandler) getK8sClient(clusterID int) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
||||
var cluster models.KubernetesCluster
|
||||
func (h *KubernetesHandler) getK8sClient(clusterID int) (*k8sclient.Client, *model.KubernetesCluster, error) {
|
||||
var cluster model.KubernetesCluster
|
||||
if err := h.db.First(&cluster, clusterID).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("cluster not found")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
@@ -33,7 +33,7 @@ type LDAPAuthInfo struct {
|
||||
}
|
||||
|
||||
// dialLDAP 连接LDAP服务器
|
||||
func dialLDAP(cfg *models.LDAPConfig) (*ldap.Conn, error) {
|
||||
func dialLDAP(cfg *model.LDAPConfig) (*ldap.Conn, error) {
|
||||
if cfg.LDAPHost == "" {
|
||||
return nil, fmt.Errorf("LDAP not configured")
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func dialLDAP(cfg *models.LDAPConfig) (*ldap.Conn, error) {
|
||||
|
||||
// tryLDAPAuth 尝试LDAP认证,返回用户信息
|
||||
func tryLDAPAuth(db *gorm.DB, username, password string) (*LDAPAuthInfo, error) {
|
||||
var cfg models.LDAPConfig
|
||||
var cfg model.LDAPConfig
|
||||
if err := db.Order("id").First(&cfg).Error; err != nil {
|
||||
return nil, fmt.Errorf("LDAP未配置")
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func tryLDAPAuth(db *gorm.DB, username, password string) (*LDAPAuthInfo, error)
|
||||
// GetLDAPConfig 获取LDAP配置
|
||||
// GET /api/v1/admin/ldap-config
|
||||
func (h *LDAPHandler) GetLDAPConfig(c *gin.Context) {
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.LDAPConfig
|
||||
h.db.Order("id").Find(&configs)
|
||||
if len(configs) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
@@ -148,7 +148,7 @@ func (h *LDAPHandler) GetLDAPConfig(c *gin.Context) {
|
||||
cfg := configs[0]
|
||||
// mask password
|
||||
cfg.AdminPassword = "******"
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cfg})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cfg})
|
||||
}
|
||||
|
||||
// UpdateLDAPConfig 更新LDAP配置
|
||||
@@ -156,11 +156,11 @@ func (h *LDAPHandler) GetLDAPConfig(c *gin.Context) {
|
||||
func (h *LDAPHandler) UpdateLDAPConfig(c *gin.Context) {
|
||||
var input map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.LDAPConfig
|
||||
h.db.Order("id").Find(&configs)
|
||||
|
||||
getStr := func(key string) string {
|
||||
@@ -191,7 +191,7 @@ func (h *LDAPHandler) UpdateLDAPConfig(c *gin.Context) {
|
||||
|
||||
if len(configs) == 0 {
|
||||
// Create
|
||||
cfg := models.LDAPConfig{
|
||||
cfg := model.LDAPConfig{
|
||||
LDAPHost: getStr("ldap_host"),
|
||||
AdminDN: getStr("admin_dn"),
|
||||
BaseDN: getStr("base_dn"),
|
||||
@@ -211,7 +211,7 @@ func (h *LDAPHandler) UpdateLDAPConfig(c *gin.Context) {
|
||||
}
|
||||
h.db.Create(&cfg)
|
||||
cfg.AdminPassword = "******"
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "LDAP配置已创建", Data: cfg})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "LDAP配置已创建", Data: cfg})
|
||||
} else {
|
||||
// Update
|
||||
cfg := configs[0]
|
||||
@@ -245,7 +245,7 @@ func (h *LDAPHandler) UpdateLDAPConfig(c *gin.Context) {
|
||||
}
|
||||
h.db.First(&cfg, cfg.ID)
|
||||
cfg.AdminPassword = "******"
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "LDAP配置已更新", Data: cfg})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "LDAP配置已更新", Data: cfg})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ func (h *LDAPHandler) TestLDAPConnection(c *gin.Context) {
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
var cfg models.LDAPConfig
|
||||
var cfg model.LDAPConfig
|
||||
if err := h.db.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "LDAP配置不存在,请先保存配置"})
|
||||
return
|
||||
@@ -311,9 +311,9 @@ func (h *LDAPHandler) TestLDAPConnection(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("密码验证失败: %s", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)}})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"}})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
// ListLDAPOUs 列出 LDAP 组织单元
|
||||
// POST /api/v1/admin/ldap-config/ou-list
|
||||
func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.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{}{}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}})
|
||||
return
|
||||
}
|
||||
cfg := configs[0]
|
||||
@@ -27,13 +27,13 @@ func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
|
||||
|
||||
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{}{}})
|
||||
c.JSON(http.StatusOK, model.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{}{}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
|
||||
)
|
||||
sr, err := conn.Search(searchReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
|
||||
Description: e.GetAttributeValue("description"),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ous})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ous})
|
||||
}
|
||||
|
||||
// CreateLDAPOU 创建组织单元
|
||||
@@ -75,13 +75,13 @@ func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.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未配置"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"})
|
||||
return
|
||||
}
|
||||
cfg := configs[0]
|
||||
@@ -97,13 +97,13 @@ func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
|
||||
|
||||
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()})
|
||||
c.JSON(http.StatusInternalServerError, model.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()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,9 +114,9 @@ func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建OU失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
@@ -22,7 +22,7 @@ func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
var cfg models.LDAPConfig
|
||||
var cfg model.LDAPConfig
|
||||
if err := h.db.Order("id").First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusOK, []interface{}{})
|
||||
return
|
||||
@@ -86,7 +86,7 @@ func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) {
|
||||
Department: e.GetAttributeValue("department"),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: users})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: users})
|
||||
}
|
||||
|
||||
// BatchImportLDAP 批量导入LDAP用户
|
||||
@@ -103,7 +103,7 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
|
||||
} `json:"users"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
// Check if already exists
|
||||
var existing models.User
|
||||
var existing model.User
|
||||
if err := h.db.Where("username = ?", uid).First(&existing).Error; err == nil {
|
||||
// Already exists, update info
|
||||
updates := map[string]interface{}{
|
||||
@@ -145,7 +145,7 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create new LDAP user
|
||||
user := models.User{
|
||||
user := model.User{
|
||||
Username: uid,
|
||||
Password: "-", // LDAP users don't have local password
|
||||
RealName: cn,
|
||||
@@ -163,7 +163,7 @@ func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) {
|
||||
// bcrypt import to avoid unused import error
|
||||
_ = bcrypt.MinCost
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: fmt.Sprintf("导入完成: %d 个用户成功, %d 个跳过", imported, skipped),
|
||||
Data: gin.H{"imported": imported, "skipped": skipped},
|
||||
@@ -181,17 +181,17 @@ func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
|
||||
OUDN string `json:"ou_dn"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Username == "" || req.Password == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "用户名和密码不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "用户名和密码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.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未配置"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"})
|
||||
return
|
||||
}
|
||||
cfg := configs[0]
|
||||
@@ -221,13 +221,13 @@ func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
|
||||
|
||||
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()})
|
||||
c.JSON(http.StatusInternalServerError, model.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()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -241,11 +241,11 @@ func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) {
|
||||
}
|
||||
addReq.Attribute("userPassword", []string{req.Password})
|
||||
if err := conn.Add(addReq); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建用户失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建用户失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}})
|
||||
}
|
||||
|
||||
// DeleteLDAPUser 删除LDAP条目(用户或OU)
|
||||
@@ -255,13 +255,13 @@ func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) {
|
||||
DN string `json:"dn"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "DN不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.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未配置"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"})
|
||||
return
|
||||
}
|
||||
cfg := configs[0]
|
||||
@@ -272,23 +272,23 @@ func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) {
|
||||
|
||||
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()})
|
||||
c.JSON(http.StatusInternalServerError, model.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()})
|
||||
c.JSON(http.StatusInternalServerError, model.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()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// UpdateLDAPUser 修改 LDAP 用户属性
|
||||
@@ -302,13 +302,13 @@ func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "DN不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var configs []models.LDAPConfig
|
||||
var configs []model.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未配置"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"})
|
||||
return
|
||||
}
|
||||
cfg := configs[0]
|
||||
@@ -319,13 +319,13 @@ func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
|
||||
|
||||
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()})
|
||||
c.JSON(http.StatusInternalServerError, model.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()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
|
||||
nil,
|
||||
))
|
||||
if err != nil || len(sr.Entries) == 0 {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "用户不存在"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "用户不存在"})
|
||||
return
|
||||
}
|
||||
entry := sr.Entries[0]
|
||||
@@ -388,10 +388,10 @@ func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) {
|
||||
|
||||
if len(modReq.Changes) > 0 {
|
||||
if err := conn.Modify(modReq); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "修改失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "修改失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "修改成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "修改成功"})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -35,8 +35,8 @@ func maskAPIKey(key string) string {
|
||||
}
|
||||
|
||||
// getActiveConfig 读取启用的配置(脱敏后) // Logan.Li
|
||||
func (h *LLMHandler) getActiveConfig(maskKey bool) (models.LLMConfig, error) {
|
||||
var cfg models.LLMConfig
|
||||
func (h *LLMHandler) getActiveConfig(maskKey bool) (model.LLMConfig, error) {
|
||||
var cfg model.LLMConfig
|
||||
if err := h.db.First(&cfg).Error; err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
@@ -52,14 +52,14 @@ func (h *LLMHandler) GetLLMConfig(c *gin.Context) {
|
||||
cfg, err := h.getActiveConfig(true)
|
||||
if err != nil {
|
||||
// 未配置返回空对象(前端渲染空表单)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"provider": "", "model": "", "api_key": "",
|
||||
"endpoint": "", "temperature": 0.7, "max_tokens": 2000,
|
||||
"system_prompt": "", "enabled": false, "configured": false,
|
||||
}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"id": cfg.ID,
|
||||
"provider": cfg.Provider,
|
||||
"model": cfg.Model,
|
||||
@@ -76,12 +76,12 @@ func (h *LLMHandler) GetLLMConfig(c *gin.Context) {
|
||||
// SaveLLMConfig 保存 LLM 配置(upsert 单行) // Logan.Li
|
||||
// PUT /api/v1/aiops/llm-config
|
||||
func (h *LLMHandler) SaveLLMConfig(c *gin.Context) {
|
||||
var req models.SaveLLMConfigRequest
|
||||
var req model.SaveLLMConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
var cfg models.LLMConfig
|
||||
var cfg model.LLMConfig
|
||||
h.db.First(&cfg)
|
||||
// 若前端回传脱敏值(含 ****),保留原 Key
|
||||
if strings.Contains(req.APIKey, "****") && cfg.ID != 0 {
|
||||
@@ -97,18 +97,18 @@ func (h *LLMHandler) SaveLLMConfig(c *gin.Context) {
|
||||
cfg.Enabled = req.Enabled
|
||||
if cfg.ID == 0 {
|
||||
if err := h.db.Create(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := h.db.Save(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
// 返回脱敏后的配置
|
||||
cfg.APIKey = maskAPIKey(cfg.APIKey)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功", Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功", Data: gin.H{
|
||||
"id": cfg.ID,
|
||||
"provider": cfg.Provider,
|
||||
"model": cfg.Model,
|
||||
@@ -125,12 +125,12 @@ func (h *LLMHandler) SaveLLMConfig(c *gin.Context) {
|
||||
// OpenAI 兼容请求/响应结构(仅取必要字段) // Logan.Li
|
||||
type openaiChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []models.ChatMessage `json:"messages"`
|
||||
Messages []model.ChatMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
}
|
||||
type openaiChoice struct {
|
||||
Message models.ChatMessage `json:"message"`
|
||||
Message model.ChatMessage `json:"message"`
|
||||
}
|
||||
type openaiChatResponse struct {
|
||||
Choices []openaiChoice `json:"choices"`
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/api/middleware"
|
||||
"ezops/internal/models"
|
||||
"ezops/middleware"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -19,15 +19,15 @@ import (
|
||||
// Chat AI 对话代理(OpenAI 兼容接口) // Logan.Li
|
||||
// POST /api/v1/aiops/chat
|
||||
func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
var cfg models.LLMConfig
|
||||
var cfg model.LLMConfig
|
||||
if err := h.db.First(&cfg).Error; err != nil || !cfg.Enabled {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 400, Message: "LLM 服务未配置或未启用,请在「模型配置」中完成设置"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 400, Message: "LLM 服务未配置或未启用,请在「模型配置」中完成设置"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.ChatRequest
|
||||
var req model.ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
if strings.TrimSpace(systemPrompt) == "" {
|
||||
systemPrompt = defaultSystemPrompt
|
||||
}
|
||||
messages := append([]models.ChatMessage{{Role: "system", Content: systemPrompt}}, req.Messages...)
|
||||
messages := append([]model.ChatMessage{{Role: "system", Content: systemPrompt}}, req.Messages...)
|
||||
|
||||
// 构造 OpenAI 兼容请求
|
||||
payload := openaiChatRequest{
|
||||
@@ -52,7 +52,7 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 500, Message: "构造请求失败: " + err.Error()})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "构造请求失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
@@ -60,7 +60,7 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 502, Message: "LLM 服务连接失败,请检查网络或 Endpoint 配置: " + err.Error()})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 502, Message: "LLM 服务连接失败,请检查网络或 Endpoint 配置: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -72,13 +72,13 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
if upErr := parseOpenAIError(raw); upErr != "" {
|
||||
msg += ": " + upErr
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: resp.StatusCode, Message: msg})
|
||||
c.JSON(http.StatusOK, model.Response{Code: resp.StatusCode, Message: msg})
|
||||
return
|
||||
}
|
||||
|
||||
var chatResp openaiChatResponse
|
||||
if err := json.Unmarshal(raw, &chatResp); err != nil || len(chatResp.Choices) == 0 {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 500, Message: "解析 LLM 响应失败"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "解析 LLM 响应失败"})
|
||||
return
|
||||
}
|
||||
replyContent := chatResp.Choices[0].Message.Content
|
||||
@@ -95,13 +95,13 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
if sid == "" {
|
||||
sid = fmt.Sprintf("sess-%d", now.UnixMilli())
|
||||
}
|
||||
h.db.Create(&[]models.ChatHistory{
|
||||
h.db.Create(&[]model.ChatHistory{
|
||||
{UserID: uid, SessionID: sid, Role: "user", Content: lastUser.Content, CreatedAt: now},
|
||||
{UserID: uid, SessionID: sid, Role: "assistant", Content: replyContent, Model: cfg.Model, CreatedAt: now.Add(time.Millisecond)},
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ChatResponse{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ChatResponse{
|
||||
Content: replyContent,
|
||||
Model: cfg.Model,
|
||||
TokensUsed: chatResp.Usage.TotalTokens,
|
||||
@@ -114,13 +114,13 @@ func (h *LLMHandler) Chat(c *gin.Context) {
|
||||
// 出错时发送 event: error,data 为 {"message": "..."}
|
||||
// 完成后服务端持久化 user + assistant 两条历史,event: done 结束
|
||||
func (h *LLMHandler) ChatStream(c *gin.Context) {
|
||||
var cfg models.LLMConfig
|
||||
var cfg model.LLMConfig
|
||||
if err := h.db.First(&cfg).Error; err != nil || !cfg.Enabled {
|
||||
writeSSEError(c, 400, "LLM 服务未配置或未启用,请在「模型配置」中完成设置")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.ChatRequest
|
||||
var req model.ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeSSEError(c, 400, "请求参数错误: "+err.Error())
|
||||
return
|
||||
@@ -144,11 +144,11 @@ func (h *LLMHandler) ChatStream(c *gin.Context) {
|
||||
if strings.TrimSpace(systemPrompt) == "" {
|
||||
systemPrompt = defaultSystemPrompt
|
||||
}
|
||||
messages := append([]models.ChatMessage{{Role: "system", Content: systemPrompt}}, req.Messages...)
|
||||
messages := append([]model.ChatMessage{{Role: "system", Content: systemPrompt}}, req.Messages...)
|
||||
// 构造 OpenAI 兼容流式请求
|
||||
payload := struct {
|
||||
Model string `json:"model"`
|
||||
Messages []models.ChatMessage `json:"messages"`
|
||||
Messages []model.ChatMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
@@ -261,7 +261,7 @@ func (h *LLMHandler) ChatStream(c *gin.Context) {
|
||||
if sid == "" {
|
||||
sid = fmt.Sprintf("sess-%d", now.UnixMilli())
|
||||
}
|
||||
h.db.Create(&[]models.ChatHistory{
|
||||
h.db.Create(&[]model.ChatHistory{
|
||||
{UserID: uid, SessionID: sid, Role: "user", Content: lastUser.Content, CreatedAt: now},
|
||||
{UserID: uid, SessionID: sid, Role: "assistant", Content: fullContent, Model: cfg.Model, CreatedAt: now.Add(time.Millisecond)},
|
||||
})
|
||||
@@ -314,14 +314,14 @@ func parseOpenAIError(raw []byte) string {
|
||||
// TestLLMConnection 测试 LLM 连接(发送一条 ping 消息) // Logan.Li
|
||||
// POST /api/v1/aiops/llm-config/test
|
||||
func (h *LLMHandler) TestLLMConnection(c *gin.Context) {
|
||||
var cfg models.LLMConfig
|
||||
var cfg model.LLMConfig
|
||||
if err := h.db.First(&cfg).Error; err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 400, Message: "请先保存配置"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 400, Message: "请先保存配置"})
|
||||
return
|
||||
}
|
||||
payload := openaiChatRequest{
|
||||
Model: cfg.Model,
|
||||
Messages: []models.ChatMessage{{Role: "user", Content: "ping"}},
|
||||
Messages: []model.ChatMessage{{Role: "user", Content: "ping"}},
|
||||
Temperature: 0,
|
||||
MaxTokens: 16,
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func (h *LLMHandler) TestLLMConnection(c *gin.Context) {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 502, Message: "连接失败: " + err.Error()})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 502, Message: "连接失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -342,8 +342,8 @@ func (h *LLMHandler) TestLLMConnection(c *gin.Context) {
|
||||
if e := parseOpenAIError(raw); e != "" {
|
||||
msg += ": " + e
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: resp.StatusCode, Message: msg})
|
||||
c.JSON(http.StatusOK, model.Response{Code: resp.StatusCode, Message: msg})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "连接成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "连接成功"})
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"ezops/internal/api/middleware"
|
||||
"ezops/internal/models"
|
||||
"ezops/middleware"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
func (h *LLMHandler) GetChatHistory(c *gin.Context) {
|
||||
uid := middleware.GetUserID(c)
|
||||
sid := c.Query("session_id")
|
||||
var rows []models.ChatHistory
|
||||
var rows []model.ChatHistory
|
||||
q := h.db.Where("user_id = ?", uid)
|
||||
// 指定 session_id 直接过滤;未指定则查最新 session // Logan.Li
|
||||
if sid != "" {
|
||||
q = q.Where("session_id = ?", sid)
|
||||
} else {
|
||||
// 取该用户最新的 session_id(id 最大那条的 session_id)
|
||||
var latest models.ChatHistory
|
||||
var latest model.ChatHistory
|
||||
if err := h.db.Where("user_id = ?", uid).Order("id DESC").First(&latest).Error; err == nil && latest.SessionID != "" {
|
||||
q = q.Where("session_id = ?", latest.SessionID)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (h *LLMHandler) GetChatHistory(c *gin.Context) {
|
||||
for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 {
|
||||
rows[i], rows[j] = rows[j], rows[i]
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"list": rows,
|
||||
"total": len(rows),
|
||||
}})
|
||||
@@ -41,9 +41,9 @@ func (h *LLMHandler) GetChatHistory(c *gin.Context) {
|
||||
// DELETE /api/v1/aiops/chat-history
|
||||
func (h *LLMHandler) ClearChatHistory(c *gin.Context) {
|
||||
uid := middleware.GetUserID(c)
|
||||
if err := h.db.Where("user_id = ?", uid).Delete(&models.ChatHistory{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "清除失败: " + err.Error()})
|
||||
if err := h.db.Where("user_id = ?", uid).Delete(&model.ChatHistory{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "清除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "对话历史已清空"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "对话历史已清空"})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -22,10 +22,10 @@ func NewLogHandler(db *gorm.DB) *LogHandler {
|
||||
|
||||
// QueryLogs 查询日志条目(支持 level/service/host/keyword 过滤) // Logan.Li
|
||||
func (h *LogHandler) QueryLogs(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.LogEntry{})
|
||||
db := h.db.Model(&model.LogEntry{})
|
||||
|
||||
// 过滤
|
||||
if level := c.Query("level"); level != "" {
|
||||
@@ -47,17 +47,17 @@ func (h *LogHandler) QueryLogs(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var logs []models.LogEntry
|
||||
var logs []model.LogEntry
|
||||
db.Order("timestamp DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&logs)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: logs, Total: total}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: logs, Total: total}})
|
||||
}
|
||||
|
||||
// ListSources 获取日志源列表 // Logan.Li
|
||||
func (h *LogHandler) ListSources(c *gin.Context) {
|
||||
var sources []models.LogSource
|
||||
var sources []model.LogSource
|
||||
h.db.Order("updated_at DESC").Find(&sources)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: sources, Total: int64(len(sources))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: sources, Total: int64(len(sources))}})
|
||||
}
|
||||
|
||||
// CreateSource 创建日志源 // Logan.Li
|
||||
@@ -68,12 +68,12 @@ func (h *LogHandler) CreateSource(c *gin.Context) {
|
||||
Config map[string]interface{} `json:"config"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
configJSON, _ := json.Marshal(req.Config)
|
||||
now := time.Now()
|
||||
item := models.LogSource{
|
||||
item := model.LogSource{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Status: "active",
|
||||
@@ -81,16 +81,16 @@ func (h *LogHandler) CreateSource(c *gin.Context) {
|
||||
LastSeen: &now,
|
||||
}
|
||||
h.db.Create(&item)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: item})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: item})
|
||||
}
|
||||
|
||||
// DeleteSource 删除日志源 // Logan.Li
|
||||
func (h *LogHandler) DeleteSource(c *gin.Context) {
|
||||
if err := h.db.Delete(&models.LogSource{}, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.LogSource{}, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// IngestLogs Agent 上报日志(无需 JWT,供 Agent 调用) // Logan.Li
|
||||
@@ -102,13 +102,13 @@ func (h *LogHandler) IngestLogs(c *gin.Context) {
|
||||
Entries []map[string]interface{} `json:"entries"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找或创建日志源
|
||||
var source models.LogSource
|
||||
h.db.Where("name = ?", req.Source).FirstOrCreate(&source, models.LogSource{
|
||||
var source model.LogSource
|
||||
h.db.Where("name = ?", req.Source).FirstOrCreate(&source, model.LogSource{
|
||||
Name: req.Source, Type: "agent", Status: "active",
|
||||
})
|
||||
|
||||
@@ -125,7 +125,7 @@ func (h *LogHandler) IngestLogs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
log := models.LogEntry{
|
||||
log := model.LogEntry{
|
||||
SourceID: source.ID,
|
||||
Timestamp: ts,
|
||||
Level: level,
|
||||
@@ -146,5 +146,5 @@ func (h *LogHandler) IngestLogs(c *gin.Context) {
|
||||
"last_seen": &now,
|
||||
}).UpdateColumn("log_count", source.LogCount+int64(count))
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "成功写入", Data: map[string]int{"count": count}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "成功写入", Data: map[string]int{"count": count}})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LoginLogHandler 登录日志处理器 // Logan.Li
|
||||
type LoginLogHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewLoginLogHandler 创建登录日志处理器
|
||||
func NewLoginLogHandler(db *gorm.DB) *LoginLogHandler {
|
||||
return &LoginLogHandler{db: db}
|
||||
}
|
||||
|
||||
// ListLoginLogs 登录日志列表
|
||||
// GET /api/v1/login-logs?username=&status=&page=&page_size=
|
||||
func (h *LoginLogHandler) ListLoginLogs(c *gin.Context) {
|
||||
var req model.LoginLogQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := h.db.Model(&model.LoginLog{})
|
||||
if req.Username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
query = query.Where("status = ?", req.Status)
|
||||
}
|
||||
if req.StartTime != "" {
|
||||
query = query.Where("created_at >= ?", req.StartTime)
|
||||
}
|
||||
if req.EndTime != "" {
|
||||
query = query.Where("created_at <= ?", req.EndTime)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var logs []model.LoginLog
|
||||
if err := query.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&logs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{
|
||||
List: logs,
|
||||
Total: total,
|
||||
}})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -23,8 +23,8 @@ func NewMetricsHandler(db *gorm.DB) *MetricsHandler {
|
||||
// ListTargets 列出监控目标(按类型过滤)
|
||||
// GET /api/v1/metrics/targets?type=host&status=online
|
||||
func (h *MetricsHandler) ListTargets(c *gin.Context) {
|
||||
var targets []models.MonitorTarget
|
||||
q := h.db.Model(&models.MonitorTarget{})
|
||||
var targets []model.MonitorTarget
|
||||
q := h.db.Model(&model.MonitorTarget{})
|
||||
|
||||
if t := c.Query("type"); t != "" {
|
||||
q = q.Where("type = ?", t)
|
||||
@@ -44,18 +44,18 @@ func (h *MetricsHandler) ListTargets(c *gin.Context) {
|
||||
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("name ASC").Find(&targets)
|
||||
|
||||
if targets == nil {
|
||||
targets = []models.MonitorTarget{}
|
||||
targets = []model.MonitorTarget{}
|
||||
}
|
||||
// 附带最新指标值
|
||||
type TargetWithMetrics struct {
|
||||
models.MonitorTarget
|
||||
model.MonitorTarget
|
||||
LatestMetrics map[string]float64 `json:"latest_metrics"`
|
||||
}
|
||||
result := make([]TargetWithMetrics, len(targets))
|
||||
for i, t := range targets {
|
||||
result[i] = TargetWithMetrics{MonitorTarget: t, LatestMetrics: make(map[string]float64)}
|
||||
// 查询该目标最近 10 分钟内的最新指标
|
||||
var histories []models.MetricHistory
|
||||
var histories []model.MetricHistory
|
||||
h.db.Where("target_id = ? AND timestamp > ?", t.ID, now()-600).
|
||||
Order("timestamp DESC").Limit(10).Find(&histories)
|
||||
for _, mh := range histories {
|
||||
@@ -65,7 +65,7 @@ func (h *MetricsHandler) ListTargets(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"list": result,
|
||||
"total": total,
|
||||
}})
|
||||
@@ -74,19 +74,19 @@ func (h *MetricsHandler) ListTargets(c *gin.Context) {
|
||||
// ListMetrics 列出系统中所有指标元数据
|
||||
// GET /api/v1/metrics/list
|
||||
func (h *MetricsHandler) ListMetrics(c *gin.Context) {
|
||||
var metrics []models.MonitorMetric
|
||||
var metrics []model.MonitorMetric
|
||||
h.db.Order("category ASC, name ASC").Find(&metrics)
|
||||
if metrics == nil {
|
||||
metrics = []models.MonitorMetric{}
|
||||
metrics = []model.MonitorMetric{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: metrics})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: metrics})
|
||||
}
|
||||
|
||||
// QueryValues 查询指标时序数据
|
||||
// GET /api/v1/metrics/query?target_id=N&metric_name=cpu_usage&from=0&to=0&limit=100
|
||||
func (h *MetricsHandler) QueryValues(c *gin.Context) {
|
||||
var histories []models.MetricHistory
|
||||
q := h.db.Model(&models.MetricHistory{})
|
||||
var histories []model.MetricHistory
|
||||
q := h.db.Model(&model.MetricHistory{})
|
||||
|
||||
if tid := c.Query("target_id"); tid != "" {
|
||||
id, _ := strconv.ParseUint(tid, 10, 64)
|
||||
@@ -108,23 +108,23 @@ func (h *MetricsHandler) QueryValues(c *gin.Context) {
|
||||
q.Order("timestamp ASC").Limit(limit).Find(&histories)
|
||||
|
||||
if histories == nil {
|
||||
histories = []models.MetricHistory{}
|
||||
histories = []model.MetricHistory{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: histories})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: histories})
|
||||
}
|
||||
|
||||
// ListMetricNames 列出所有可用的指标名称(去重)
|
||||
// GET /api/v1/metrics/names
|
||||
func (h *MetricsHandler) ListMetricNames(c *gin.Context) {
|
||||
var names []string
|
||||
h.db.Model(&models.MetricHistory{}).
|
||||
h.db.Model(&model.MetricHistory{}).
|
||||
Distinct("metric_name").
|
||||
Order("metric_name ASC").
|
||||
Pluck("metric_name", &names)
|
||||
if names == nil {
|
||||
names = []string{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: names})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: names})
|
||||
}
|
||||
|
||||
// TopHosts 返回指定指标的 Top N 主机排行
|
||||
@@ -145,7 +145,7 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
orderField := "cpu_usage DESC"
|
||||
switch metric {
|
||||
case "cpu":
|
||||
@@ -180,7 +180,7 @@ func (h *MetricsHandler) TopHosts(c *gin.Context) {
|
||||
if result == nil {
|
||||
result = []HostRank{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
func now() int64 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -26,13 +26,13 @@ func NewNavigationHandler(db *gorm.DB) *NavigationHandler {
|
||||
// @Tags 站内导航
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=[]models.NavigationGroup}
|
||||
// @Success 200 {object} model.Response{data=[]model.NavigationGroup}
|
||||
// @Router /navigations/groups [get]
|
||||
func (h *NavigationHandler) ListGroups(c *gin.Context) {
|
||||
var groups []models.NavigationGroup
|
||||
var groups []model.NavigationGroup
|
||||
h.db.Order("sort ASC, created_at ASC").Find(&groups)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: groups,
|
||||
@@ -46,20 +46,20 @@ func (h *NavigationHandler) ListGroups(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateNavigationGroupRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.NavigationGroup}
|
||||
// @Param request body model.CreateNavigationGroupRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.NavigationGroup}
|
||||
// @Router /navigations/groups [post]
|
||||
func (h *NavigationHandler) CreateGroup(c *gin.Context) {
|
||||
var req models.CreateNavigationGroupRequest
|
||||
var req model.CreateNavigationGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
group := models.NavigationGroup{
|
||||
group := model.NavigationGroup{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
@@ -69,14 +69,14 @@ func (h *NavigationHandler) CreateGroup(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&group).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: group,
|
||||
@@ -91,24 +91,24 @@ func (h *NavigationHandler) CreateGroup(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "分组ID"
|
||||
// @Param request body models.UpdateNavigationGroupRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.NavigationGroup}
|
||||
// @Param request body model.UpdateNavigationGroupRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.NavigationGroup}
|
||||
// @Router /navigations/groups/{id} [put]
|
||||
func (h *NavigationHandler) UpdateGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateNavigationGroupRequest
|
||||
var req model.UpdateNavigationGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var group models.NavigationGroup
|
||||
var group model.NavigationGroup
|
||||
if err := h.db.First(&group, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "分组不存在",
|
||||
})
|
||||
@@ -131,7 +131,7 @@ func (h *NavigationHandler) UpdateGroup(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&group).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -140,7 +140,7 @@ func (h *NavigationHandler) UpdateGroup(c *gin.Context) {
|
||||
|
||||
h.db.First(&group, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: group,
|
||||
@@ -154,29 +154,29 @@ func (h *NavigationHandler) UpdateGroup(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "分组ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /navigations/groups/{id} [delete]
|
||||
func (h *NavigationHandler) DeleteGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
// 删除分组下的所有链接
|
||||
if err := h.db.Where("group_id = ?", id).Delete(&models.NavigationLink{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Where("group_id = ?", id).Delete(&model.NavigationLink{}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除链接失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.NavigationGroup{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.NavigationGroup{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
+38
-38
@@ -2,14 +2,14 @@
|
||||
// 导航链接相关处理器方法
|
||||
// 从 navigation.go 拆分而来,包含导航链接的 CRUD 及收藏功能
|
||||
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
// @Security Bearer
|
||||
// @Param group_id query int false "分组ID"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Success 200 {object} models.Response{data=[]models.NavigationLink}
|
||||
// @Success 200 {object} model.Response{data=[]model.NavigationLink}
|
||||
// @Router /navigations/links [get]
|
||||
func (h *NavigationHandler) ListLinks(c *gin.Context) {
|
||||
db := h.db.Model(&models.NavigationLink{}).Preload("Group")
|
||||
db := h.db.Model(&model.NavigationLink{}).Preload("Group")
|
||||
|
||||
// 分组过滤
|
||||
if groupID := c.Query("group_id"); groupID != "" {
|
||||
@@ -37,10 +37,10 @@ func (h *NavigationHandler) ListLinks(c *gin.Context) {
|
||||
db = db.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
var links []models.NavigationLink
|
||||
var links []model.NavigationLink
|
||||
db.Order("sort ASC, created_at ASC").Find(&links)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: links,
|
||||
@@ -54,13 +54,13 @@ func (h *NavigationHandler) ListLinks(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateNavigationLinkRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.NavigationLink}
|
||||
// @Param request body model.CreateNavigationLinkRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.NavigationLink}
|
||||
// @Router /navigations/links [post]
|
||||
func (h *NavigationHandler) CreateLink(c *gin.Context) {
|
||||
var req models.CreateNavigationLinkRequest
|
||||
var req model.CreateNavigationLinkRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -69,7 +69,7 @@ func (h *NavigationHandler) CreateLink(c *gin.Context) {
|
||||
|
||||
tags, _ := json.Marshal(req.Tags)
|
||||
|
||||
link := models.NavigationLink{
|
||||
link := model.NavigationLink{
|
||||
GroupID: req.GroupID,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
@@ -81,7 +81,7 @@ func (h *NavigationHandler) CreateLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&link).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
@@ -89,12 +89,12 @@ func (h *NavigationHandler) CreateLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 更新分组链接数
|
||||
h.db.Model(&models.NavigationGroup{}).Where("id = ?", req.GroupID).
|
||||
UpdateColumn("link_count", h.db.Model(&models.NavigationLink{}).Where("group_id = ?", req.GroupID).Select("COUNT(*)"))
|
||||
h.db.Model(&model.NavigationGroup{}).Where("id = ?", req.GroupID).
|
||||
UpdateColumn("link_count", h.db.Model(&model.NavigationLink{}).Where("group_id = ?", req.GroupID).Select("COUNT(*)"))
|
||||
|
||||
h.db.Preload("Group").First(&link, link.ID)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: link,
|
||||
@@ -109,24 +109,24 @@ func (h *NavigationHandler) CreateLink(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "链接ID"
|
||||
// @Param request body models.UpdateNavigationLinkRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.NavigationLink}
|
||||
// @Param request body model.UpdateNavigationLinkRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.NavigationLink}
|
||||
// @Router /navigations/links/{id} [put]
|
||||
func (h *NavigationHandler) UpdateLink(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateNavigationLinkRequest
|
||||
var req model.UpdateNavigationLinkRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var link models.NavigationLink
|
||||
var link model.NavigationLink
|
||||
if err := h.db.First(&link, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "链接不存在",
|
||||
})
|
||||
@@ -159,7 +159,7 @@ func (h *NavigationHandler) UpdateLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&link).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -168,7 +168,7 @@ func (h *NavigationHandler) UpdateLink(c *gin.Context) {
|
||||
|
||||
h.db.Preload("Group").First(&link, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: link,
|
||||
@@ -182,16 +182,16 @@ func (h *NavigationHandler) UpdateLink(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "链接ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /navigations/links/{id} [delete]
|
||||
func (h *NavigationHandler) DeleteLink(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var link models.NavigationLink
|
||||
var link model.NavigationLink
|
||||
h.db.First(&link, id)
|
||||
|
||||
if err := h.db.Delete(&models.NavigationLink{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.NavigationLink{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
@@ -200,11 +200,11 @@ func (h *NavigationHandler) DeleteLink(c *gin.Context) {
|
||||
|
||||
// 更新分组链接数
|
||||
if link.GroupID > 0 {
|
||||
h.db.Model(&models.NavigationGroup{}).Where("id = ?", link.GroupID).
|
||||
UpdateColumn("link_count", h.db.Model(&models.NavigationLink{}).Where("group_id = ?", link.GroupID).Select("COUNT(*)"))
|
||||
h.db.Model(&model.NavigationGroup{}).Where("id = ?", link.GroupID).
|
||||
UpdateColumn("link_count", h.db.Model(&model.NavigationLink{}).Where("group_id = ?", link.GroupID).Select("COUNT(*)"))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -218,24 +218,24 @@ func (h *NavigationHandler) DeleteLink(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "链接ID"
|
||||
// @Param request body models.ToggleFavoriteRequest true "收藏请求"
|
||||
// @Success 200 {object} models.Response{data=models.NavigationLink}
|
||||
// @Param request body model.ToggleFavoriteRequest true "收藏请求"
|
||||
// @Success 200 {object} model.Response{data=model.NavigationLink}
|
||||
// @Router /navigations/links/{id}/favorite [post]
|
||||
func (h *NavigationHandler) ToggleFavorite(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.ToggleFavoriteRequest
|
||||
var req model.ToggleFavoriteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var link models.NavigationLink
|
||||
var link model.NavigationLink
|
||||
if err := h.db.First(&link, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "链接不存在",
|
||||
})
|
||||
@@ -244,14 +244,14 @@ func (h *NavigationHandler) ToggleFavorite(c *gin.Context) {
|
||||
|
||||
link.IsFavorite = req.IsFavorite
|
||||
if err := h.db.Save(&link).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "操作失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "操作成功",
|
||||
Data: link,
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/internal/api/middleware"
|
||||
"ezops/model"
|
||||
"ezops/middleware"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -40,7 +40,7 @@ func validateCode(db *gorm.DB, code string, excludeID uint) string {
|
||||
}
|
||||
// 查重
|
||||
var count int64
|
||||
q := db.Model(&models.Pipeline{}).Where("code = ?", code)
|
||||
q := db.Model(&model.Pipeline{}).Where("code = ?", code)
|
||||
if excludeID > 0 {
|
||||
q = q.Where("id != ?", excludeID)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func NewPipelineHandler(db *gorm.DB, cache ezredis.Cache) *PipelineHandler {
|
||||
|
||||
// PipelineListItem 列表项(含最近一次运行摘要)// Logan.Li
|
||||
type PipelineListItem struct {
|
||||
models.Pipeline
|
||||
model.Pipeline
|
||||
LastRunStages string `json:"last_run_stages,omitempty"` // JSON: [{name,status}]
|
||||
LastImageTag string `json:"last_image_tag,omitempty"`
|
||||
LastCommitID string `json:"last_commit_id,omitempty"`
|
||||
@@ -80,10 +80,10 @@ type PipelineListItem struct {
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// ListPipelines 获取流水线列表
|
||||
func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
// 管理员看全部,非管理员只看自己有权限的
|
||||
@@ -93,12 +93,12 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
uid, _ := userID.(uint)
|
||||
|
||||
db := h.db.Model(&models.Pipeline{})
|
||||
db := h.db.Model(&model.Pipeline{})
|
||||
if isAdmin != true && usernameStr != "" {
|
||||
ids, hasAccess := middleware.GetAccessiblePipelineIDs(h.db, usernameStr, uid)
|
||||
if !hasAccess {
|
||||
// 没有任何流水线权限 → 返回空列表
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: []models.Pipeline{}, Total: 0}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: []model.Pipeline{}, Total: 0}})
|
||||
return
|
||||
}
|
||||
db = db.Where("id IN ?", ids)
|
||||
@@ -107,7 +107,7 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var pipelines []models.Pipeline
|
||||
var pipelines []model.Pipeline
|
||||
db.Preload("Environment").Offset(query.GetOffset()).Limit(query.GetLimit()).Order("updated_at DESC").Find(&pipelines)
|
||||
|
||||
// 批量获取每个流水线最近一次运行的摘要(阶段状态 + 镜像 + 耗时)// Logan.Li
|
||||
@@ -118,10 +118,10 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
pids = append(pids, p.ID)
|
||||
}
|
||||
// 子查询:每个 pipeline_id 的最新 run id
|
||||
var latestRuns []models.PipelineRun
|
||||
var latestRuns []model.PipelineRun
|
||||
h.db.Where("id IN (SELECT MAX(id) FROM ops_pipeline_runs WHERE pipeline_id IN ? AND status IN ('success','failed','running','pending') GROUP BY pipeline_id)", pids).
|
||||
Find(&latestRuns)
|
||||
runMap := map[uint]*models.PipelineRun{}
|
||||
runMap := map[uint]*model.PipelineRun{}
|
||||
for i := range latestRuns {
|
||||
runMap[latestRuns[i].PipelineID] = &latestRuns[i]
|
||||
}
|
||||
@@ -142,9 +142,9 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: models.ListResponse{List: items, Total: total},
|
||||
Data: model.ListResponse{List: items, Total: total},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,25 +155,25 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "流水线ID"
|
||||
// @Success 200 {object} models.Response{data=models.Pipeline}
|
||||
// @Success 200 {object} model.Response{data=model.Pipeline}
|
||||
// @Router /pipelines/{id} [get]
|
||||
func (h *PipelineHandler) GetPipeline(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
if id == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
return
|
||||
}
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.Preload("Environment").First(&pipeline, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "流水线不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: pipeline,
|
||||
@@ -187,13 +187,13 @@ func (h *PipelineHandler) GetPipeline(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreatePipelineRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.Pipeline}
|
||||
// @Param request body model.CreatePipelineRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.Pipeline}
|
||||
// @Router /pipelines [post]
|
||||
func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
||||
var req models.CreatePipelineRequest
|
||||
var req model.CreatePipelineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -202,11 +202,11 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
||||
|
||||
// 校验编码格式
|
||||
if msg := validateCode(h.db, req.Code, 0); msg != "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: msg})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: msg})
|
||||
return
|
||||
}
|
||||
|
||||
pipeline := models.Pipeline{
|
||||
pipeline := model.Pipeline{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
JobName: req.JobName,
|
||||
@@ -241,14 +241,14 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&pipeline).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: pipeline,
|
||||
@@ -263,28 +263,28 @@ func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "流水线ID"
|
||||
// @Param request body models.CreatePipelineRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.Pipeline}
|
||||
// @Param request body model.CreatePipelineRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.Pipeline}
|
||||
// @Router /pipelines/{id} [put]
|
||||
func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
if id == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdatePipelineRequest
|
||||
var req model.UpdatePipelineRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.First(&pipeline, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "流水线不存在",
|
||||
})
|
||||
@@ -321,7 +321,7 @@ func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
|
||||
if req.MaxExecutors > 0 { updates["max_executors"] = req.MaxExecutors }
|
||||
|
||||
if err := h.db.Model(&pipeline).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -330,7 +330,7 @@ func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
|
||||
|
||||
h.db.First(&pipeline, id)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: pipeline,
|
||||
@@ -344,20 +344,20 @@ func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "流水线ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /pipelines/{id} [delete]
|
||||
func (h *PipelineHandler) DeletePipeline(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
|
||||
if err := h.db.Delete(&models.Pipeline{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Delete(&model.Pipeline{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -371,27 +371,27 @@ func (h *PipelineHandler) DeletePipeline(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "流水线ID"
|
||||
// @Param request body models.TriggerPipelineRequest true "触发请求"
|
||||
// @Success 200 {object} models.Response{data=models.PipelineRun}
|
||||
// @Param request body model.TriggerPipelineRequest true "触发请求"
|
||||
// @Success 200 {object} model.Response{data=model.PipelineRun}
|
||||
// TriggerPipeline 触发流水线
|
||||
func (h *PipelineHandler) TriggerPipeline(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
if id == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
return
|
||||
}
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.First(&pipeline, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "流水线不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流水线不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.TriggerPipelineRequest
|
||||
var req model.TriggerPipelineRequest
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
var maxBuildNumber int
|
||||
h.db.Model(&models.PipelineRun{}).Where("pipeline_id = ?", id).
|
||||
h.db.Model(&model.PipelineRun{}).Where("pipeline_id = ?", id).
|
||||
Select("COALESCE(MAX(build_number), 0)").Scan(&maxBuildNumber)
|
||||
|
||||
branch := req.Branch
|
||||
@@ -407,7 +407,7 @@ func (h *PipelineHandler) TriggerPipeline(c *gin.Context) {
|
||||
userStr, _ := username.(string)
|
||||
|
||||
now := time.Now()
|
||||
run := models.PipelineRun{
|
||||
run := model.PipelineRun{
|
||||
PipelineID: uint(id),
|
||||
BuildNumber: maxBuildNumber + 1,
|
||||
Status: "running",
|
||||
@@ -418,14 +418,14 @@ func (h *PipelineHandler) TriggerPipeline(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&run).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "触发失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "触发失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 通过队列引擎执行(尊重 max_executors 并发限制)
|
||||
go h.tryExecuteOrQueue(pipeline, run)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已触发", Data: run})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已触发", Data: run})
|
||||
}
|
||||
|
||||
// ListRuns 获取运行记录列表
|
||||
@@ -437,28 +437,28 @@ func (h *PipelineHandler) TriggerPipeline(c *gin.Context) {
|
||||
// @Param id path int true "流水线ID"
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /pipelines/{id}/runs [get]
|
||||
func (h *PipelineHandler) ListRuns(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
db := h.db.Model(&models.PipelineRun{}).Where("pipeline_id = ?", id)
|
||||
db := h.db.Model(&model.PipelineRun{}).Where("pipeline_id = ?", id)
|
||||
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var runs []models.PipelineRun
|
||||
var runs []model.PipelineRun
|
||||
db.Order("started_at DESC").
|
||||
Offset(query.GetOffset()).
|
||||
Limit(query.GetLimit()).
|
||||
Find(&runs)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: runs,
|
||||
Total: total,
|
||||
},
|
||||
@@ -470,19 +470,19 @@ func (h *PipelineHandler) WebhookTrigger(c *gin.Context) {
|
||||
jobName := c.Param("jobName")
|
||||
code := c.Param("code")
|
||||
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if jobName != "" {
|
||||
if err := h.db.Where("job_name = ? AND enabled = ?", jobName, true).First(&pipeline).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "Job 不存在或已禁用"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "Job 不存在或已禁用"})
|
||||
return
|
||||
}
|
||||
} else if code != "" {
|
||||
if err := h.db.Where("code = ? AND enabled = ?", code, true).First(&pipeline).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "流水线不存在或已禁用"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流水线不存在或已禁用"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "缺少 jobName 或 code"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少 jobName 或 code"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -493,17 +493,17 @@ func (h *PipelineHandler) WebhookTrigger(c *gin.Context) {
|
||||
token = c.Query("token")
|
||||
}
|
||||
if token != pipeline.WebhookToken {
|
||||
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "Webhook Token 无效"})
|
||||
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "Webhook Token 无效"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 解析请求体获取 branch/tag
|
||||
var req models.TriggerPipelineRequest
|
||||
var req model.TriggerPipelineRequest
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
var maxBuildNumber int
|
||||
h.db.Model(&models.PipelineRun{}).Where("pipeline_id = ?", pipeline.ID).
|
||||
h.db.Model(&model.PipelineRun{}).Where("pipeline_id = ?", pipeline.ID).
|
||||
Select("COALESCE(MAX(build_number), 0)").Scan(&maxBuildNumber)
|
||||
|
||||
ref := req.Branch
|
||||
@@ -526,7 +526,7 @@ func (h *PipelineHandler) WebhookTrigger(c *gin.Context) {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
run := models.PipelineRun{
|
||||
run := model.PipelineRun{
|
||||
PipelineID: pipeline.ID,
|
||||
BuildNumber: maxBuildNumber + 1,
|
||||
Status: "running",
|
||||
@@ -537,13 +537,13 @@ func (h *PipelineHandler) WebhookTrigger(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&run).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "触发失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "触发失败"})
|
||||
return
|
||||
}
|
||||
|
||||
go h.executePipeline(pipeline, run)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "已触发",
|
||||
Data: map[string]interface{}{
|
||||
@@ -557,11 +557,11 @@ func (h *PipelineHandler) WebhookTrigger(c *gin.Context) {
|
||||
func (h *PipelineHandler) GetPipelineQueue(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
if id == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的流水线编码"})
|
||||
return
|
||||
}
|
||||
status := h.GetQueueStatus(uint(id))
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: status})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: status})
|
||||
}
|
||||
|
||||
// CancelQueueItem 取消排队中的构建
|
||||
@@ -569,12 +569,12 @@ func (h *PipelineHandler) CancelQueueItem(c *gin.Context) {
|
||||
id := getPipelineID(c)
|
||||
runID, _ := strconv.ParseUint(c.Param("runId"), 10, 64)
|
||||
if id == 0 || runID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := h.CancelQueued(uint(id), uint(runID)); err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: err.Error()})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已取消"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已取消"})
|
||||
}
|
||||
+17
-17
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -39,7 +39,7 @@ func init() {
|
||||
}
|
||||
|
||||
// executePipeline 后台执行流水线(goroutine)— V2 Redis 版
|
||||
func (h *PipelineHandler) executePipeline(pipeline models.Pipeline, run models.PipelineRun) {
|
||||
func (h *PipelineHandler) executePipeline(pipeline model.Pipeline, run model.PipelineRun) {
|
||||
runID := run.ID
|
||||
ctx := context.Background()
|
||||
logKey := fmt.Sprintf("pipeline:run:%d:logs", runID)
|
||||
@@ -74,7 +74,7 @@ func (h *PipelineHandler) executePipeline(pipeline models.Pipeline, run models.P
|
||||
if status == "success" || status == "failed" || status == "cancelled" {
|
||||
updates["finished_at"] = &now
|
||||
}
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", runID).Updates(updates)
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Updates(updates)
|
||||
// 推送状态变更
|
||||
if h.cache != nil {
|
||||
statusData, _ := json.Marshal(map[string]interface{}{"type": "status", "status": status, "duration": duration})
|
||||
@@ -90,11 +90,11 @@ func (h *PipelineHandler) executePipeline(pipeline models.Pipeline, run models.P
|
||||
if status == "failed" {
|
||||
ups["fail_count"] = pipeline.FailCount + 1
|
||||
}
|
||||
h.db.Model(&models.Pipeline{}).Where("id = ?", pipeline.ID).Updates(ups)
|
||||
h.db.Model(&model.Pipeline{}).Where("id = ?", pipeline.ID).Updates(ups)
|
||||
}
|
||||
|
||||
isCancelled := func() bool {
|
||||
var current models.PipelineRun
|
||||
var current model.PipelineRun
|
||||
h.db.First(¤t, runID)
|
||||
return current.Status == "cancelled"
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func (h *PipelineHandler) executePipeline(pipeline models.Pipeline, run models.P
|
||||
os.RemoveAll(workspace)
|
||||
os.MkdirAll(workspace, 0755)
|
||||
// 保存 workspace 路径到运行记录,方便终端查看
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", runID).Update("workspace_path", workspace)
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Update("workspace_path", workspace)
|
||||
|
||||
imageTag := fmt.Sprintf("%s-%s", repoName, tsDir)
|
||||
|
||||
@@ -207,7 +207,7 @@ func (h *PipelineHandler) flushLogsToDB(ctx context.Context, runID uint, logKey
|
||||
return
|
||||
}
|
||||
logsJSON, _ := json.Marshal(entries)
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", runID).Update("logs", string(logsJSON))
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Update("logs", string(logsJSON))
|
||||
// 清理 Redis 缓存
|
||||
h.cache.Del(ctx, logKey)
|
||||
}
|
||||
@@ -391,7 +391,7 @@ func (h *PipelineHandler) runTest(workspace string, logFn func(string, string, s
|
||||
}
|
||||
|
||||
// runDockerBuild Docker 构建 & 推送
|
||||
func (h *PipelineHandler) runDockerBuild(workspace string, pipeline models.Pipeline, tag string, logFn func(string, string, string)) error {
|
||||
func (h *PipelineHandler) runDockerBuild(workspace string, pipeline model.Pipeline, tag string, logFn func(string, string, string)) error {
|
||||
dockerfile := pipeline.Dockerfile
|
||||
if dockerfile == "" {
|
||||
dockerfile = "Dockerfile"
|
||||
@@ -399,7 +399,7 @@ func (h *PipelineHandler) runDockerBuild(workspace string, pipeline models.Pipel
|
||||
|
||||
var registryURL, imageName string
|
||||
if pipeline.RegistryID != nil {
|
||||
var reg models.PipelineRegistry
|
||||
var reg model.PipelineRegistry
|
||||
if err := h.db.First(®, *pipeline.RegistryID).Error; err == nil {
|
||||
registryURL = reg.RegistryURL
|
||||
if reg.Namespace != "" {
|
||||
@@ -456,7 +456,7 @@ func (h *PipelineHandler) runDockerBuild(workspace string, pipeline models.Pipel
|
||||
}
|
||||
|
||||
// runDeploy 部署到 K8s / 服务器
|
||||
func (h *PipelineHandler) runDeploy(workspace string, pipeline models.Pipeline, tag string, logFn func(string, string, string)) error {
|
||||
func (h *PipelineHandler) runDeploy(workspace string, pipeline model.Pipeline, tag string, logFn func(string, string, string)) error {
|
||||
deployYamls := findFiles(workspace, ".yaml", ".yml")
|
||||
var k8sFiles []string
|
||||
for _, f := range deployYamls {
|
||||
@@ -564,7 +564,7 @@ var (
|
||||
// tryExecuteOrQueue 触发流水线时检查并发限制
|
||||
// 如果当前运行数 < maxExecutors → 立即执行
|
||||
// 否则 → 加入队列 (status=queued)
|
||||
func (h *PipelineHandler) tryExecuteOrQueue(pipeline models.Pipeline, run models.PipelineRun) {
|
||||
func (h *PipelineHandler) tryExecuteOrQueue(pipeline model.Pipeline, run model.PipelineRun) {
|
||||
max := pipeline.MaxExecutors
|
||||
if max <= 0 { max = 2 }
|
||||
|
||||
@@ -574,14 +574,14 @@ func (h *PipelineHandler) tryExecuteOrQueue(pipeline models.Pipeline, run models
|
||||
pipelineRunningCount[pipeline.ID] = running + 1
|
||||
queueMu.Unlock()
|
||||
// 立即执行
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", run.ID).Update("status", "running")
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", run.ID).Update("status", "running")
|
||||
go h.executePipeline(pipeline, run)
|
||||
return
|
||||
}
|
||||
// 加入队列
|
||||
pipelineQueues[pipeline.ID] = append(pipelineQueues[pipeline.ID], run.ID)
|
||||
queueMu.Unlock()
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", run.ID).Update("status", "queued")
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", run.ID).Update("status", "queued")
|
||||
}
|
||||
|
||||
// dequeueNext 流水线运行结束后,从队列中取下一个执行
|
||||
@@ -603,9 +603,9 @@ func (h *PipelineHandler) dequeueNext(pipelineID uint) {
|
||||
queueMu.Unlock()
|
||||
|
||||
// 加载 pipeline + run
|
||||
var pipeline models.Pipeline
|
||||
var pipeline model.Pipeline
|
||||
if err := h.db.First(&pipeline, pipelineID).Error; err != nil { return }
|
||||
var run models.PipelineRun
|
||||
var run model.PipelineRun
|
||||
if err := h.db.First(&run, nextRunID).Error; err != nil { return }
|
||||
|
||||
h.db.Model(&run).Update("status", "running")
|
||||
@@ -633,7 +633,7 @@ func (h *PipelineHandler) CancelQueued(pipelineID uint, runID uint) error {
|
||||
for i, id := range queue {
|
||||
if id == runID {
|
||||
pipelineQueues[pipelineID] = append(queue[:i], queue[i+1:]...)
|
||||
h.db.Model(&models.PipelineRun{}).Where("id = ?", runID).Update("status", "cancelled")
|
||||
h.db.Model(&model.PipelineRun{}).Where("id = ?", runID).Update("status", "cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+35
-35
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -20,25 +20,25 @@ import (
|
||||
// @Security Bearer
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /releases [get]
|
||||
func (h *PipelineHandler) ListReleases(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
c.ShouldBindQuery(&query)
|
||||
|
||||
var total int64
|
||||
h.db.Model(&models.AppRelease{}).Count(&total)
|
||||
h.db.Model(&model.AppRelease{}).Count(&total)
|
||||
|
||||
var releases []models.AppRelease
|
||||
var releases []model.AppRelease
|
||||
h.db.Order("created_at DESC").
|
||||
Offset(query.GetOffset()).
|
||||
Limit(query.GetLimit()).
|
||||
Find(&releases)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: releases,
|
||||
Total: total,
|
||||
},
|
||||
@@ -52,20 +52,20 @@ func (h *PipelineHandler) ListReleases(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateAppReleaseRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.AppRelease}
|
||||
// @Param request body model.CreateAppReleaseRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.AppRelease}
|
||||
// @Router /releases [post]
|
||||
func (h *PipelineHandler) CreateRelease(c *gin.Context) {
|
||||
var req models.CreateAppReleaseRequest
|
||||
var req model.CreateAppReleaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
release := models.AppRelease{
|
||||
release := model.AppRelease{
|
||||
PipelineID: req.PipelineID,
|
||||
PipelineRunID: req.PipelineRunID,
|
||||
AppName: req.AppName,
|
||||
@@ -91,14 +91,14 @@ func (h *PipelineHandler) CreateRelease(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&release).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: release,
|
||||
@@ -112,14 +112,14 @@ func (h *PipelineHandler) CreateRelease(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "发布ID"
|
||||
// @Success 200 {object} models.Response{data=models.AppRelease}
|
||||
// @Success 200 {object} model.Response{data=model.AppRelease}
|
||||
// @Router /releases/{id}/approve [post]
|
||||
func (h *PipelineHandler) ApproveRelease(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var release models.AppRelease
|
||||
var release model.AppRelease
|
||||
if err := h.db.First(&release, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "发布不存在",
|
||||
})
|
||||
@@ -132,14 +132,14 @@ func (h *PipelineHandler) ApproveRelease(c *gin.Context) {
|
||||
// release.ApprovedAt = &now
|
||||
|
||||
if err := h.db.Save(&release).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "审批失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "审批成功",
|
||||
Data: release,
|
||||
@@ -153,14 +153,14 @@ func (h *PipelineHandler) ApproveRelease(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "发布ID"
|
||||
// @Success 200 {object} models.Response{data=models.AppRelease}
|
||||
// @Success 200 {object} model.Response{data=model.AppRelease}
|
||||
// @Router /releases/{id}/deploy [post]
|
||||
func (h *PipelineHandler) DeployRelease(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var release models.AppRelease
|
||||
var release model.AppRelease
|
||||
if err := h.db.First(&release, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "发布不存在",
|
||||
})
|
||||
@@ -169,14 +169,14 @@ func (h *PipelineHandler) DeployRelease(c *gin.Context) {
|
||||
|
||||
release.Status = "deploying"
|
||||
if err := h.db.Save(&release).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "部署失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "部署中",
|
||||
Data: release,
|
||||
@@ -187,9 +187,9 @@ func (h *PipelineHandler) DeployRelease(c *gin.Context) {
|
||||
func (h *PipelineHandler) RejectRelease(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var release models.AppRelease
|
||||
var release model.AppRelease
|
||||
if err := h.db.First(&release, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "发布不存在",
|
||||
})
|
||||
@@ -197,7 +197,7 @@ func (h *PipelineHandler) RejectRelease(c *gin.Context) {
|
||||
}
|
||||
|
||||
if release.Status != "pending" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "仅待审批状态可驳回",
|
||||
})
|
||||
@@ -206,14 +206,14 @@ func (h *PipelineHandler) RejectRelease(c *gin.Context) {
|
||||
|
||||
release.Status = "rejected"
|
||||
if err := h.db.Save(&release).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "驳回失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "已驳回",
|
||||
Data: release,
|
||||
@@ -227,14 +227,14 @@ func (h *PipelineHandler) RejectRelease(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "发布ID"
|
||||
// @Success 200 {object} models.Response{data=models.AppRelease}
|
||||
// @Success 200 {object} model.Response{data=model.AppRelease}
|
||||
// @Router /releases/{id}/rollback [post]
|
||||
func (h *PipelineHandler) RollbackRelease(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var release models.AppRelease
|
||||
var release model.AppRelease
|
||||
if err := h.db.First(&release, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "发布不存在",
|
||||
})
|
||||
@@ -246,14 +246,14 @@ func (h *PipelineHandler) RollbackRelease(c *gin.Context) {
|
||||
// release.RollbackAt = &now
|
||||
|
||||
if err := h.db.Save(&release).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "回滚失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "回滚成功",
|
||||
Data: release,
|
||||
+38
-38
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -30,41 +30,41 @@ func NewPromDSHandler(db *gorm.DB) *PromDSHandler {
|
||||
// List 数据源列表
|
||||
// GET /api/v1/prometheus-datasources
|
||||
func (h *PromDSHandler) List(c *gin.Context) {
|
||||
var sources []models.PrometheusDataSource
|
||||
var sources []model.PrometheusDataSource
|
||||
if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if sources == nil {
|
||||
sources = []models.PrometheusDataSource{}
|
||||
sources = []model.PrometheusDataSource{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: sources})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: sources})
|
||||
}
|
||||
|
||||
// Get 获取单个数据源
|
||||
// GET /api/v1/prometheus-datasources/:id
|
||||
func (h *PromDSHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.PrometheusDataSource
|
||||
var ds model.PrometheusDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ds})
|
||||
}
|
||||
|
||||
// Create 创建数据源
|
||||
// POST /api/v1/prometheus-datasources
|
||||
func (h *PromDSHandler) Create(c *gin.Context) {
|
||||
var req models.CreatePromDSReq
|
||||
var req model.CreatePromDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果设为默认,先清除其他默认
|
||||
if req.IsDefault {
|
||||
h.db.Model(&models.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
}
|
||||
|
||||
// 默认值
|
||||
@@ -85,7 +85,7 @@ func (h *PromDSHandler) Create(c *gin.Context) {
|
||||
enableAlerting = *req.EnableAlerting
|
||||
}
|
||||
|
||||
ds := models.PrometheusDataSource{
|
||||
ds := model.PrometheusDataSource{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Access: req.Access,
|
||||
@@ -110,26 +110,26 @@ func (h *PromDSHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&ds).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
h.recordAudit(c, "create", "prometheus_datasource", ds.Name)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: ds})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: ds})
|
||||
}
|
||||
|
||||
// Update 更新数据源
|
||||
// PUT /api/v1/prometheus-datasources/:id
|
||||
func (h *PromDSHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.PrometheusDataSource
|
||||
var ds model.PrometheusDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdatePromDSReq
|
||||
var req model.UpdatePromDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func (h *PromDSHandler) Update(c *gin.Context) {
|
||||
if req.IsDefault != nil {
|
||||
// 如果设为默认,先清除其他默认
|
||||
if *req.IsDefault {
|
||||
h.db.Model(&models.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
}
|
||||
updates["is_default"] = *req.IsDefault
|
||||
}
|
||||
@@ -203,54 +203,54 @@ func (h *PromDSHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无更新字段"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无更新字段"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新查询返回
|
||||
h.db.First(&ds, id)
|
||||
h.recordAudit(c, "update", "prometheus_datasource", ds.Name)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: ds})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: ds})
|
||||
}
|
||||
|
||||
// Delete 删除数据源
|
||||
// DELETE /api/v1/prometheus-datasources/:id
|
||||
func (h *PromDSHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.PrometheusDataSource
|
||||
var ds model.PrometheusDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
if err := h.db.Delete(&ds).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
h.recordAudit(c, "delete", "prometheus_datasource", ds.Name)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// SetDefault 设为默认数据源
|
||||
// POST /api/v1/prometheus-datasources/:id/default
|
||||
func (h *PromDSHandler) SetDefault(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.PrometheusDataSource
|
||||
var ds model.PrometheusDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 清除所有默认
|
||||
h.db.Model(&models.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
h.db.Model(&model.PrometheusDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
// 设置当前为默认
|
||||
h.db.Model(&ds).Update("is_default", true)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已设为默认"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已设为默认"})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -260,9 +260,9 @@ func (h *PromDSHandler) SetDefault(c *gin.Context) {
|
||||
// TestConnection 测试数据源连接
|
||||
// POST /api/v1/prometheus-datasources/test
|
||||
func (h *PromDSHandler) TestConnection(c *gin.Context) {
|
||||
var req models.TestPromDSReq
|
||||
var req model.TestPromDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ func (h *PromDSHandler) TestConnection(c *gin.Context) {
|
||||
apiURL := req.URL + "/api/v1/query?query=up&time=" + time.Now().UTC().Format(time.RFC3339)
|
||||
httpReq, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 400, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 400, Data: gin.H{
|
||||
"success": false, "message": "URL 格式错误: " + err.Error(),
|
||||
}})
|
||||
return
|
||||
@@ -293,7 +293,7 @@ func (h *PromDSHandler) TestConnection(c *gin.Context) {
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"success": false, "message": "连接失败: " + err.Error(),
|
||||
}})
|
||||
return
|
||||
@@ -308,14 +308,14 @@ func (h *PromDSHandler) TestConnection(c *gin.Context) {
|
||||
} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"success": true,
|
||||
"message": "连接成功",
|
||||
"status_code": resp.StatusCode,
|
||||
"target_count": len(result.Data.Result),
|
||||
}})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"success": false,
|
||||
"message": "连接返回异常状态码",
|
||||
"status_code": resp.StatusCode,
|
||||
@@ -338,7 +338,7 @@ func (h *PromDSHandler) recordAudit(c *gin.Context, action, resource, name strin
|
||||
if v, ok := username.(string); ok {
|
||||
uname = v
|
||||
}
|
||||
h.db.Create(&models.AuditLog{
|
||||
h.db.Create(&model.AuditLog{
|
||||
UserID: &uid,
|
||||
Username: uname,
|
||||
IP: c.ClientIP(),
|
||||
+13
-13
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -33,7 +33,7 @@ type PromConnectReq struct {
|
||||
func (h *PrometheusProxyHandler) Connect(c *gin.Context) {
|
||||
var req PromConnectReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ func (h *PrometheusProxyHandler) Connect(c *gin.Context) {
|
||||
// 测试连接:查询 up 指标
|
||||
status, targetCount := h.testConnect(client, req.URL)
|
||||
if !status {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "无法连接到 Prometheus,请检查 URL"})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "无法连接到 Prometheus,请检查 URL"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
@@ -64,18 +64,18 @@ func (h *PrometheusProxyHandler) Query(c *gin.Context) {
|
||||
Query string `json:"query" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
result, err := h.execPromQL(client, req.URL, req.Query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// PromQueryRange 执行 PromQL 范围查询
|
||||
@@ -89,7 +89,7 @@ func (h *PrometheusProxyHandler) QueryRange(c *gin.Context) {
|
||||
Step string `json:"step"` // e.g. "15s", "1m"
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
if req.Step == "" {
|
||||
@@ -110,7 +110,7 @@ func (h *PrometheusProxyHandler) QueryRange(c *gin.Context) {
|
||||
|
||||
resp, err := client.Get(u)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "查询失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -119,7 +119,7 @@ func (h *PrometheusProxyHandler) QueryRange(c *gin.Context) {
|
||||
var promResult map[string]interface{}
|
||||
json.Unmarshal(body, &promResult)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: promResult})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: promResult})
|
||||
}
|
||||
|
||||
// PromDashboard 获取预定义仪表盘数据(K8s / Node / Pod / JVM)
|
||||
@@ -130,7 +130,7 @@ func (h *PrometheusProxyHandler) Dashboard(c *gin.Context) {
|
||||
Type string `json:"type" binding:"required"` // k8s, node, pod, jvm
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (h *PrometheusProxyHandler) Dashboard(c *gin.Context) {
|
||||
result = h.dashboardK8s(client, req.URL)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -0,0 +1,227 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/dto"
|
||||
"ezops/model"
|
||||
"ezops/service"
|
||||
"ezops/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ━━━ Credential Handler (v2 thin) ━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
type CredentialHandlerV2 struct {
|
||||
svc *service.CredentialService
|
||||
}
|
||||
|
||||
func NewCredentialHandlerV2(svc *service.CredentialService) *CredentialHandlerV2 {
|
||||
return &CredentialHandlerV2{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CredentialHandlerV2) ListCredentials(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
keyword := c.Query("keyword")
|
||||
secretType := c.Query("secret_type")
|
||||
|
||||
list, total, err := h.svc.List(keyword, secretType, page, pageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.ListResponse(c, list, total)
|
||||
}
|
||||
|
||||
func (h *CredentialHandlerV2) GetCredential(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
crd, err := h.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "凭据不存在")
|
||||
return
|
||||
}
|
||||
utils.OK(c, crd)
|
||||
}
|
||||
|
||||
func (h *CredentialHandlerV2) CreateCredential(c *gin.Context) {
|
||||
var req dto.CreateCredentialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
crd, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.OK(c, crd)
|
||||
}
|
||||
|
||||
func (h *CredentialHandlerV2) UpdateCredential(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req dto.UpdateCredentialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.svc.Update(uint(id), &req); err != nil {
|
||||
utils.InternalError(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crd, _ := h.svc.GetByID(uint(id))
|
||||
utils.OK(c, crd)
|
||||
}
|
||||
|
||||
func (h *CredentialHandlerV2) DeleteCredential(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.svc.Delete(uint(id)); err != nil {
|
||||
utils.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
utils.OK(c, gin.H{"id": id})
|
||||
}
|
||||
|
||||
// ━━━ Alert Contact Handler (v2 thin) ━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
type AlertContactHandlerV2 struct {
|
||||
svc *service.AlertContactService
|
||||
}
|
||||
|
||||
func NewAlertContactHandlerV2(svc *service.AlertContactService) *AlertContactHandlerV2 {
|
||||
return &AlertContactHandlerV2{svc: svc}
|
||||
}
|
||||
|
||||
func (h *AlertContactHandlerV2) ListContacts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
list, total, err := h.svc.List(keyword, page, pageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.ListResponse(c, list, total)
|
||||
}
|
||||
|
||||
func (h *AlertContactHandlerV2) GetContact(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
contact, err := h.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "联系人不存在")
|
||||
return
|
||||
}
|
||||
utils.OK(c, contact)
|
||||
}
|
||||
|
||||
func (h *AlertContactHandlerV2) CreateContact(c *gin.Context) {
|
||||
var req dto.CreateAlertContactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
contact, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "创建失败")
|
||||
return
|
||||
}
|
||||
utils.OK(c, contact)
|
||||
}
|
||||
|
||||
func (h *AlertContactHandlerV2) UpdateContact(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req dto.UpdateAlertContactRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.svc.Update(uint(id), &req); err != nil {
|
||||
utils.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
contact, _ := h.svc.GetByID(uint(id))
|
||||
utils.OK(c, contact)
|
||||
}
|
||||
|
||||
func (h *AlertContactHandlerV2) DeleteContact(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.svc.Delete(uint(id)); err != nil {
|
||||
utils.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// ━━━ Alert Contact Group Handler (v2 thin) ━━━━━━━━━━━━━━
|
||||
|
||||
type AlertContactGroupHandlerV2 struct {
|
||||
svc *service.AlertContactGroupService
|
||||
}
|
||||
|
||||
func NewAlertContactGroupHandlerV2(svc *service.AlertContactGroupService) *AlertContactGroupHandlerV2 {
|
||||
return &AlertContactGroupHandlerV2{svc: svc}
|
||||
}
|
||||
|
||||
func (h *AlertContactGroupHandlerV2) ListGroups(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
list, total, err := h.svc.List(keyword, page, pageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.ListResponse(c, list, total)
|
||||
}
|
||||
|
||||
func (h *AlertContactGroupHandlerV2) GetGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
g, err := h.svc.GetByID(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "联系组不存在")
|
||||
return
|
||||
}
|
||||
utils.OK(c, g)
|
||||
}
|
||||
|
||||
func (h *AlertContactGroupHandlerV2) CreateGroup(c *gin.Context) {
|
||||
var req dto.CreateAlertContactGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
g, err := h.svc.Create(&req)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "创建失败")
|
||||
return
|
||||
}
|
||||
utils.OK(c, g)
|
||||
}
|
||||
|
||||
func (h *AlertContactGroupHandlerV2) UpdateGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req dto.UpdateAlertContactGroupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.svc.Update(uint(id), &req); err != nil {
|
||||
utils.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
g, _ := h.svc.GetByID(uint(id))
|
||||
utils.OK(c, g)
|
||||
}
|
||||
|
||||
func (h *AlertContactGroupHandlerV2) DeleteGroup(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.svc.Delete(uint(id)); err != nil {
|
||||
utils.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/remoteexec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -39,19 +39,19 @@ type BatchExecRequest struct {
|
||||
func (h *RemoteExecHandler) BatchExec(c *gin.Context) {
|
||||
var req BatchExecRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询目标服务器
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
if err := h.db.Where("id IN ?", req.ServerIDs).Find(&servers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询服务器失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询服务器失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (h *RemoteExecHandler) BatchExec(c *gin.Context) {
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(h.db, s)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400, Message: fmt.Sprintf("服务器 %s 认证配置错误: %v", s.Hostname, err),
|
||||
})
|
||||
return
|
||||
@@ -76,7 +76,7 @@ func (h *RemoteExecHandler) BatchExec(c *gin.Context) {
|
||||
// 并发执行
|
||||
results := remoteexec.BatchExec(configs, req.Command, time.Duration(timeout)*time.Second, 10)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: results})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ==================== 文件批量上传 ====================
|
||||
@@ -93,24 +93,24 @@ type BatchUploadRequest struct {
|
||||
func (h *RemoteExecHandler) BatchUpload(c *gin.Context) {
|
||||
var req BatchUploadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.LocalPath, "/") {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "local_path 必须是绝对路径"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "local_path 必须是绝对路径"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询目标服务器
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
if err := h.db.Where("id IN ?", req.ServerIDs).Find(&servers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询服务器失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询服务器失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "未找到目标服务器"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (h *RemoteExecHandler) BatchUpload(c *gin.Context) {
|
||||
for _, s := range servers {
|
||||
cfg, err := buildSSHConfig(h.db, s)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400, Message: fmt.Sprintf("服务器 %s 认证配置错误: %v", s.Hostname, err),
|
||||
})
|
||||
return
|
||||
@@ -130,7 +130,7 @@ func (h *RemoteExecHandler) BatchUpload(c *gin.Context) {
|
||||
// 并发上传
|
||||
results := remoteexec.BatchUpload(configs, req.LocalPath, req.RemotePath, 5)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: results})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ==================== 主机匹配 ====================
|
||||
@@ -141,7 +141,7 @@ func (h *RemoteExecHandler) MatchHosts(c *gin.Context) {
|
||||
pattern := c.Query("pattern")
|
||||
matchType := c.DefaultQuery("match_type", "contains")
|
||||
|
||||
query := h.db.Model(&models.Server{})
|
||||
query := h.db.Model(&model.Server{})
|
||||
|
||||
if pattern != "" {
|
||||
switch matchType {
|
||||
@@ -158,7 +158,7 @@ func (h *RemoteExecHandler) MatchHosts(c *gin.Context) {
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var items []models.Server
|
||||
var items []model.Server
|
||||
query.Order("hostname").Find(&items)
|
||||
|
||||
// 补充 HasAuth 字段
|
||||
@@ -166,7 +166,7 @@ func (h *RemoteExecHandler) MatchHosts(c *gin.Context) {
|
||||
items[i].HasAuth = items[i].Password != "" || items[i].SSHKey != ""
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: items, Total: total}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: items, Total: total}})
|
||||
}
|
||||
|
||||
// ==================== 单机命令执行 ====================
|
||||
@@ -176,7 +176,7 @@ func (h *RemoteExecHandler) MatchHosts(c *gin.Context) {
|
||||
func (h *RemoteExecHandler) ExecOnServer(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的服务器 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的服务器 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -185,19 +185,19 @@ func (h *RemoteExecHandler) ExecOnServer(c *gin.Context) {
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "服务器不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "服务器不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := buildSSHConfig(h.db, server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "认证配置错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "认证配置错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,11 +208,11 @@ func (h *RemoteExecHandler) ExecOnServer(c *gin.Context) {
|
||||
|
||||
result, err := remoteexec.Exec(cfg, req.Command, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error(), Data: result})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error(), Data: result})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
@@ -220,7 +220,7 @@ func (h *RemoteExecHandler) ExecOnServer(c *gin.Context) {
|
||||
// buildSSHConfig 从 Server 模型构建 SSHConfig。
|
||||
// 优先使用 server 自带的 Password/SSHKey 字段,
|
||||
// 如果 server 关联了 credential_id,则从凭证表解密获取。
|
||||
func buildSSHConfig(db *gorm.DB, server models.Server) (remoteexec.SSHConfig, error) {
|
||||
func buildSSHConfig(db *gorm.DB, server model.Server) (remoteexec.SSHConfig, error) {
|
||||
cfg := remoteexec.SSHConfig{
|
||||
Host: server.IP,
|
||||
Port: server.Port,
|
||||
@@ -256,7 +256,7 @@ func buildSSHConfig(db *gorm.DB, server models.Server) (remoteexec.SSHConfig, er
|
||||
|
||||
// ExecuteCronOnTargets 根据 CronJob 的 target_type 在目标主机上执行脚本。
|
||||
// 由 automation_cron.go 调用的公共函数。
|
||||
func ExecuteCronOnTargets(db *gorm.DB, job models.CronJob, scriptType, content string, timeoutSec int) ([]remoteexec.ExecResult, error) {
|
||||
func ExecuteCronOnTargets(db *gorm.DB, job model.CronJob, scriptType, content string, timeoutSec int) ([]remoteexec.ExecResult, error) {
|
||||
// 解析目标服务器
|
||||
var targetIDs []uint
|
||||
if job.Targets != "" {
|
||||
@@ -268,7 +268,7 @@ func ExecuteCronOnTargets(db *gorm.DB, job models.CronJob, scriptType, content s
|
||||
}
|
||||
|
||||
// 查询服务器
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
switch job.TargetType {
|
||||
case "server":
|
||||
db.Where("id IN ?", targetIDs).Find(&servers)
|
||||
+18
-18
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -33,13 +33,13 @@ type resourceModelRequest struct {
|
||||
|
||||
// ListResourceModels 获取资源模型列表
|
||||
func (h *ResourceModelHandler) ListResourceModels(c *gin.Context) {
|
||||
var models_ []models.ResourceModel
|
||||
var models_ []model.ResourceModel
|
||||
h.db.Order("id ASC").Find(&models_)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: models_,
|
||||
Total: int64(len(models_)),
|
||||
},
|
||||
@@ -50,11 +50,11 @@ func (h *ResourceModelHandler) ListResourceModels(c *gin.Context) {
|
||||
func (h *ResourceModelHandler) CreateResourceModel(c *gin.Context) {
|
||||
var req resourceModelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.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: "模型名称和编码不能为空"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "模型名称和编码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (h *ResourceModelHandler) CreateResourceModel(c *gin.Context) {
|
||||
attrsJSON = string(b)
|
||||
}
|
||||
|
||||
rm := models.ResourceModel{
|
||||
rm := model.ResourceModel{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Code: strings.TrimSpace(req.Code),
|
||||
Description: req.Description,
|
||||
@@ -78,10 +78,10 @@ func (h *ResourceModelHandler) CreateResourceModel(c *gin.Context) {
|
||||
Icon: req.Icon,
|
||||
}
|
||||
if err := h.db.Create(&rm).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败,编码可能已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: rm})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: rm})
|
||||
}
|
||||
|
||||
// UpdateResourceModel 更新资源模型
|
||||
@@ -89,13 +89,13 @@ func (h *ResourceModelHandler) UpdateResourceModel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var req resourceModelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var rm models.ResourceModel
|
||||
var rm model.ResourceModel
|
||||
if err := h.db.First(&rm, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "资源模型不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "资源模型不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,18 +117,18 @@ func (h *ResourceModelHandler) UpdateResourceModel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Save(&rm).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功", Data: rm})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: rm})
|
||||
}
|
||||
|
||||
// DeleteResourceModel 删除资源模型
|
||||
func (h *ResourceModelHandler) DeleteResourceModel(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.db.Delete(&models.ResourceModel{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
if err := h.db.Delete(&model.ResourceModel{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
// CreateRole 创建角色 // Logan.Li
|
||||
// POST /api/v1/admin/roles
|
||||
func (h *UserHandler) CreateRole(c *gin.Context) {
|
||||
var req models.CreateRoleRequest
|
||||
var req model.CreateRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -22,23 +22,23 @@ func (h *UserHandler) CreateRole(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 名称 / 编码唯一性校验
|
||||
var existing models.Role
|
||||
var existing model.Role
|
||||
if err := h.db.Where("name = ? OR code = ?", req.Name, req.Code).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "角色名称或编码已存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
role := models.Role{
|
||||
role := model.Role{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
Status: 1,
|
||||
}
|
||||
if err := h.db.Create(&role).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建角色失败",
|
||||
})
|
||||
@@ -47,13 +47,13 @@ func (h *UserHandler) CreateRole(c *gin.Context) {
|
||||
|
||||
// 关联权限
|
||||
if len(req.PermissionIDs) > 0 {
|
||||
var perms []models.Permission
|
||||
var perms []model.Permission
|
||||
h.db.Find(&perms, req.PermissionIDs)
|
||||
h.db.Model(&role).Association("Permissions").Append(perms)
|
||||
}
|
||||
|
||||
h.db.Preload("Permissions").First(&role, role.ID)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: role,
|
||||
@@ -65,31 +65,31 @@ func (h *UserHandler) CreateRole(c *gin.Context) {
|
||||
func (h *UserHandler) UpdateRole(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的角色 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateRoleRequest
|
||||
var req model.UpdateRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var role models.Role
|
||||
var role model.Role
|
||||
if err := h.db.First(&role, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != "" {
|
||||
// 名称 / 编码唯一性校验(排除自身)
|
||||
var dup models.Role
|
||||
var dup model.Role
|
||||
if err := h.db.Where("(name = ? OR code = ?) AND id != ?", req.Name, req.Code, id).First(&dup).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "角色名称或编码已存在"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "角色名称或编码已存在"})
|
||||
return
|
||||
}
|
||||
updates["name"] = req.Name
|
||||
@@ -106,14 +106,14 @@ func (h *UserHandler) UpdateRole(c *gin.Context) {
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := h.db.Model(&role).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 替换权限关联
|
||||
if req.PermissionIDs != nil {
|
||||
var perms []models.Permission
|
||||
var perms []model.Permission
|
||||
if len(req.PermissionIDs) > 0 {
|
||||
h.db.Find(&perms, req.PermissionIDs)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (h *UserHandler) UpdateRole(c *gin.Context) {
|
||||
}
|
||||
|
||||
h.db.Preload("Permissions").First(&role, id)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: role,
|
||||
@@ -133,24 +133,24 @@ func (h *UserHandler) UpdateRole(c *gin.Context) {
|
||||
func (h *UserHandler) DeleteRole(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的角色 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var role models.Role
|
||||
var role model.Role
|
||||
if err := h.db.First(&role, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 清空权限关联后再删除(避免 join 表残留)
|
||||
h.db.Model(&role).Association("Permissions").Clear()
|
||||
if err := h.db.Delete(&role).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -159,10 +159,10 @@ func (h *UserHandler) DeleteRole(c *gin.Context) {
|
||||
// ListPermissions 获取权限列表(供前端角色权限选择) // Logan.Li
|
||||
// GET /api/v1/admin/permissions
|
||||
func (h *UserHandler) ListPermissions(c *gin.Context) {
|
||||
var perms []models.Permission
|
||||
var perms []model.Permission
|
||||
h.db.Where("status = ?", 1).Order("sort ASC, id ASC").Find(&perms)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: models.ListResponse{List: perms, Total: int64(len(perms))},
|
||||
Data: model.ListResponse{List: perms, Total: int64(len(perms))},
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/crypto"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
@@ -34,7 +34,7 @@ func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource strin
|
||||
uid, _ := userID.(uint)
|
||||
uname, _ := username.(string)
|
||||
|
||||
h.db.Create(&models.AuditLog{
|
||||
h.db.Create(&model.AuditLog{
|
||||
UserID: &uid,
|
||||
Username: uname,
|
||||
IP: c.ClientIP(),
|
||||
@@ -59,12 +59,12 @@ func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource strin
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Param status query string false "状态过滤"
|
||||
// @Param region_id query int false "区域ID"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /servers [get]
|
||||
func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
@@ -82,7 +82,7 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
// 尝试从 Redis 缓存读取
|
||||
if h.cache != nil {
|
||||
if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
|
||||
var resp models.Response
|
||||
var resp model.Response
|
||||
if json.Unmarshal([]byte(cached), &resp) == nil {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
@@ -90,7 +90,7 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
db := h.db.Model(&models.Server{})
|
||||
db := h.db.Model(&model.Server{})
|
||||
|
||||
// 关键词搜索
|
||||
if query.Keyword != "" {
|
||||
@@ -111,7 +111,7 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
db.Preload("Region").Preload("Cloud").
|
||||
Offset(query.GetOffset()).Limit(query.GetLimit()).
|
||||
Order("created_at DESC").
|
||||
@@ -122,10 +122,10 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
servers[i].HasAuth = servers[i].Password != "" || servers[i].SSHKey != ""
|
||||
}
|
||||
|
||||
resp := models.Response{
|
||||
resp := model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: servers,
|
||||
Total: total,
|
||||
},
|
||||
@@ -148,21 +148,21 @@ func (h *ServerHandler) ListServers(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "服务器ID"
|
||||
// @Success 200 {object} models.Response{data=models.Server}
|
||||
// @Success 200 {object} model.Response{data=model.Server}
|
||||
// @Router /servers/{id} [get]
|
||||
func (h *ServerHandler) GetServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.Preload("Region").Preload("Cloud").First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "服务器不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: server,
|
||||
@@ -176,20 +176,20 @@ func (h *ServerHandler) GetServer(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateServerRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.Server}
|
||||
// @Param request body model.CreateServerRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.Server}
|
||||
// @Router /servers [post]
|
||||
func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
var req models.CreateServerRequest
|
||||
var req model.CreateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
server := models.Server{
|
||||
server := model.Server{
|
||||
Hostname: req.Hostname,
|
||||
IP: req.IP,
|
||||
PublicIP: req.PublicIP,
|
||||
@@ -224,7 +224,7 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&server).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建服务器失败",
|
||||
})
|
||||
@@ -233,7 +233,7 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
|
||||
h.recordServerAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: server,
|
||||
@@ -248,24 +248,24 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "服务器ID"
|
||||
// @Param request body models.UpdateServerRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.Server}
|
||||
// @Param request body model.UpdateServerRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.Server}
|
||||
// @Router /servers/{id} [put]
|
||||
func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateServerRequest
|
||||
var req model.UpdateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "服务器不存在",
|
||||
})
|
||||
@@ -326,7 +326,7 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&server).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -336,7 +336,7 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
h.db.Preload("Region").Preload("Cloud").First(&server, id)
|
||||
h.recordServerAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: server,
|
||||
@@ -350,14 +350,14 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "服务器ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /servers/{id} [delete]
|
||||
func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var server models.Server
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "服务器不存在",
|
||||
})
|
||||
@@ -365,11 +365,11 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 硬删除:先清理关联数据,再物理删除服务器记录
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&models.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&models.MetricsAggregate{})
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.MetricsAggregate{})
|
||||
|
||||
if err := h.db.Unscoped().Delete(&models.Server{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Unscoped().Delete(&model.Server{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
@@ -377,7 +377,7 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
}
|
||||
|
||||
h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "服务器已彻底删除",
|
||||
})
|
||||
@@ -389,27 +389,27 @@ func (h *ServerHandler) DeleteServer(c *gin.Context) {
|
||||
// @Tags 服务器
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=models.ServerStats}
|
||||
// @Success 200 {object} model.Response{data=model.ServerStats}
|
||||
// @Router /servers/stats [get]
|
||||
func (h *ServerHandler) GetServerStats(c *gin.Context) {
|
||||
var stats models.ServerStats
|
||||
var stats model.ServerStats
|
||||
|
||||
h.db.Model(&models.Server{}).Count(&stats.Total)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "running").Count(&stats.Running)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "stopped").Count(&stats.Stopped)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "maintenance").Count(&stats.Maintenance)
|
||||
h.db.Model(&models.Server{}).Where("status = ?", "error").Count(&stats.Error)
|
||||
h.db.Model(&model.Server{}).Count(&stats.Total)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&stats.Running)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&stats.Stopped)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&stats.Maintenance)
|
||||
h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&stats.Error)
|
||||
|
||||
var avgCPU, avgMemory, avgDisk float64
|
||||
h.db.Model(&models.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
|
||||
h.db.Model(&models.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
|
||||
h.db.Model(&models.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
|
||||
h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU)
|
||||
h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory)
|
||||
h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk)
|
||||
|
||||
stats.AvgCPU = avgCPU
|
||||
stats.AvgMemory = avgMemory
|
||||
stats.AvgDisk = avgDisk
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: stats,
|
||||
+21
-21
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
// BatchCreateServers 批量导入服务器 // Logan.Li
|
||||
// 接收 {servers:[...], region_id, auth_type, credential_id},逐条校验并创建,跳过重复(主机名或IP已存在)
|
||||
func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
var req models.BatchCreateServerRequest
|
||||
var req model.BatchCreateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -33,14 +33,14 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
// 如果指定了凭据,提前解密 // Logan.Li
|
||||
var decryptedPwd, decryptedKey string
|
||||
if req.CredentialID != nil && *req.CredentialID > 0 {
|
||||
var cred models.Credential
|
||||
var cred model.Credential
|
||||
if err := h.db.First(&cred, *req.CredentialID).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "凭据不存在"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"})
|
||||
return
|
||||
}
|
||||
plain, err := h.crypt.Decrypt(cred.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "凭据解密失败"})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"})
|
||||
return
|
||||
}
|
||||
if cred.SecretType == "ssh" {
|
||||
@@ -52,7 +52,7 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
result := models.BatchCreateResult{Errors: []string{}}
|
||||
result := model.BatchCreateResult{Errors: []string{}}
|
||||
for i, item := range req.Servers {
|
||||
hostname := strings.TrimSpace(item.Hostname)
|
||||
ip := strings.TrimSpace(item.IP)
|
||||
@@ -64,7 +64,7 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
|
||||
// 重复检查(主机名或 IP 已存在则跳过)
|
||||
var count int64
|
||||
h.db.Model(&models.Server{}).Where("hostname = ? OR ip = ?", hostname, ip).Count(&count)
|
||||
h.db.Model(&model.Server{}).Where("hostname = ? OR ip = ?", hostname, ip).Count(&count)
|
||||
if count > 0 {
|
||||
result.Skipped++
|
||||
continue
|
||||
@@ -79,7 +79,7 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
username = "root"
|
||||
}
|
||||
|
||||
server := models.Server{
|
||||
server := model.Server{
|
||||
Hostname: hostname,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
@@ -104,7 +104,7 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("导入完成: 成功 %d, 跳过 %d, 失败 %d", result.Success, result.Skipped, result.Failed)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: msg,
|
||||
Data: result,
|
||||
@@ -118,13 +118,13 @@ func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.IDsRequest true "删除请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /servers/batch-delete [post]
|
||||
func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
@@ -132,17 +132,17 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 查询待删除服务器用于审计日志 // Logan.Li
|
||||
var servers []models.Server
|
||||
var servers []model.Server
|
||||
h.db.Where("id IN ?", req.IDs).Find(&servers)
|
||||
|
||||
// 硬删除:先清理每台服务器的 Agent 上报和聚合数据
|
||||
for _, s := range servers {
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&models.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&models.MetricsAggregate{})
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&model.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&model.MetricsAggregate{})
|
||||
}
|
||||
|
||||
if err := h.db.Unscoped().Delete(&models.Server{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Unscoped().Delete(&model.Server{}, req.IDs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
@@ -154,7 +154,7 @@ func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
|
||||
h.recordServerAudit(c, "delete", s.Hostname+" ("+s.IP+")", strconv.Itoa(int(s.ID)), 1, "")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "服务器已彻底删除",
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
// Package handlers_v2 provides thin HTTP handlers that delegate to service layer.
|
||||
// This is the refactored version — handlers contain ONLY HTTP concerns:
|
||||
// parse request → call service → return HTTP response.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/dto"
|
||||
"ezops/model"
|
||||
"ezops/service"
|
||||
"ezops/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ━━━ Server Handler (v2 thin) ━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// ServerHandlerV2 服务器处理器 — thin handler,委托给 ServerService
|
||||
type ServerHandlerV2 struct {
|
||||
svc *service.ServerService
|
||||
db *gorm.DB // retained for audit logging until audit is extracted
|
||||
}
|
||||
|
||||
func NewServerHandlerV2(svc *service.ServerService, db *gorm.DB) *ServerHandlerV2 {
|
||||
return &ServerHandlerV2{svc: svc, db: db}
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) recordAudit(c *gin.Context, action, resource, resourceID string, status int, errMsg string) {
|
||||
userID, _ := c.Get("userID")
|
||||
username, _ := c.Get("username")
|
||||
uid, _ := userID.(uint)
|
||||
uname, _ := username.(string)
|
||||
|
||||
h.db.Create(&model.AuditLog{
|
||||
UserID: &uid,
|
||||
Username: uname,
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
Module: "server",
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Status: status,
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) ListServers(c *gin.Context) {
|
||||
var query model.PageQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
status := c.Query("status")
|
||||
regionID := c.Query("region_id")
|
||||
|
||||
list, total, err := h.svc.ListServers(query.Keyword, status, regionID, query.Page, query.PageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.ListResponse(c, list, total)
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) GetServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
server, err := h.svc.GetServer(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "服务器不存在")
|
||||
return
|
||||
}
|
||||
utils.OK(c, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) CreateServer(c *gin.Context) {
|
||||
var req dto.CreateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
server, err := h.svc.CreateServer(&req)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.recordAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
utils.OK(c, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) UpdateServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req dto.UpdateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.UpdateServer(uint(id), &req); err != nil {
|
||||
utils.InternalError(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
server, _ := h.svc.GetServer(uint(id))
|
||||
h.recordAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
utils.OK(c, server)
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) DeleteServer(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
server, err := h.svc.GetServer(uint(id))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "服务器不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 清理关联数据
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.MetricsAggregate{})
|
||||
|
||||
if err := h.svc.DeleteServer(uint(id)); err != nil {
|
||||
utils.InternalError(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.recordAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "")
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "服务器已彻底删除"})
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) GetServerStats(c *gin.Context) {
|
||||
stats, err := h.svc.GetDashboardStats()
|
||||
if err != nil {
|
||||
utils.InternalError(c, "获取统计失败")
|
||||
return
|
||||
}
|
||||
utils.OK(c, stats.ServerStats)
|
||||
}
|
||||
|
||||
// BatchCreateServers / BatchDeleteServers — 暂时保留原始逻辑(复杂批量操作待后续迁移)
|
||||
func (h *ServerHandlerV2) BatchCreateServers(c *gin.Context) {
|
||||
// 暂时委托给原始 handler 逻辑(从 server.go 复制)
|
||||
var servers []model.Server
|
||||
if err := c.ShouldBindJSON(&servers); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
for i := range servers {
|
||||
if servers[i].Port == 0 {
|
||||
servers[i].Port = 22
|
||||
}
|
||||
if servers[i].Username == "" {
|
||||
servers[i].Username = "root"
|
||||
}
|
||||
if servers[i].Status == "" {
|
||||
servers[i].Status = "stopped"
|
||||
}
|
||||
}
|
||||
if err := h.db.Create(&servers).Error; err != nil {
|
||||
utils.InternalError(c, "批量创建失败")
|
||||
return
|
||||
}
|
||||
utils.OK(c, servers)
|
||||
}
|
||||
|
||||
func (h *ServerHandlerV2) BatchDeleteServers(c *gin.Context) {
|
||||
var ids []uint
|
||||
if err := c.ShouldBindJSON(&ids); err != nil {
|
||||
utils.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
// 清理关联数据
|
||||
for _, id := range ids {
|
||||
var s model.Server
|
||||
if err := h.db.First(&s, id).Error; err == nil {
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&model.AgentReport{})
|
||||
h.db.Unscoped().Where("hostname = ?", s.Hostname).Delete(&model.MetricsAggregate{})
|
||||
}
|
||||
}
|
||||
h.db.Unscoped().Delete(&model.Server{}, ids)
|
||||
utils.OK(c, gin.H{"deleted": len(ids)})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SessionCommandHandler 会话命令审计处理器 // Logan.Li
|
||||
type SessionCommandHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSessionCommandHandler 创建会话命令审计处理器
|
||||
func NewSessionCommandHandler(db *gorm.DB) *SessionCommandHandler {
|
||||
return &SessionCommandHandler{db: db}
|
||||
}
|
||||
|
||||
// ListSessionCommands 会话命令列表
|
||||
// GET /api/v1/session-commands?session_id=&username=&page=&page_size=
|
||||
func (h *SessionCommandHandler) ListSessionCommands(c *gin.Context) {
|
||||
var req model.SessionCommandQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := h.db.Model(&model.SessionCommand{})
|
||||
if req.SessionID > 0 {
|
||||
query = query.Where("session_id = ?", req.SessionID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.ServerID > 0 {
|
||||
query = query.Where("server_id = ?", req.ServerID)
|
||||
}
|
||||
if req.Command != "" {
|
||||
query = query.Where("command LIKE ?", "%"+req.Command+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var commands []model.SessionCommand
|
||||
if err := query.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&commands).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{
|
||||
List: commands,
|
||||
Total: total,
|
||||
}})
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ezops/model"
|
||||
)
|
||||
|
||||
// SFTPHandler 文件浏览器处理器 — 基于 SFTP 的服务器文件管理
|
||||
type SFTPHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSFTPHandler(db *gorm.DB) *SFTPHandler {
|
||||
return &SFTPHandler{db: db}
|
||||
}
|
||||
|
||||
// sftpClient 建立到目标服务器的 SFTP 连接
|
||||
func (h *SFTPHandler) sftpClient(serverID uint) (*sftp.Client, *ssh.Client, error) {
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, serverID).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("server not found: %w", err)
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
Timeout: 10 * time.Second,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
switch server.AuthType {
|
||||
case "key":
|
||||
signer, err := ssh.ParsePrivateKey([]byte(server.SSHKey))
|
||||
if err != nil {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(server.Password)}
|
||||
} else {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
default:
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(server.Password)}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", server.IP, server.Port)
|
||||
sshClient, err := ssh.Dial("tcp", addr, sshConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("SSH connect failed: %w", err)
|
||||
}
|
||||
|
||||
sftpClient, err := sftp.NewClient(sshClient)
|
||||
if err != nil {
|
||||
sshClient.Close()
|
||||
return nil, nil, fmt.Errorf("SFTP init failed: %w", err)
|
||||
}
|
||||
|
||||
return sftpClient, sshClient, nil
|
||||
}
|
||||
|
||||
// FileInfo JSON 响应结构
|
||||
type FileItem struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
ModTime string `json:"mod_time"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
// ListFiles 列出目录内容
|
||||
// GET /api/v1/servers/:id/files?path=/
|
||||
func (h *SFTPHandler) ListFiles(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server id"})
|
||||
return
|
||||
}
|
||||
|
||||
dirPath := c.Query("path")
|
||||
if dirPath == "" {
|
||||
dirPath = "/"
|
||||
}
|
||||
|
||||
sftpClient, sshClient, err := h.sftpClient(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
entries, err := sftpClient.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("read dir failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
var items []FileItem
|
||||
for _, entry := range entries {
|
||||
items = append(items, FileItem{
|
||||
Name: entry.Name(),
|
||||
Path: path.Join(dirPath, entry.Name()),
|
||||
Size: entry.Size(),
|
||||
IsDir: entry.IsDir(),
|
||||
ModTime: entry.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Mode: entry.Mode().String(),
|
||||
})
|
||||
}
|
||||
|
||||
// 构建面包屑
|
||||
breadcrumbs := buildBreadcrumbs(dirPath)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"path": dirPath,
|
||||
"breadcrumbs": breadcrumbs,
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadFile 下载文件
|
||||
// GET /api/v1/servers/:id/files/download?path=/etc/hosts
|
||||
func (h *SFTPHandler) DownloadFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server id"})
|
||||
return
|
||||
}
|
||||
|
||||
filePath := c.Query("path")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path required"})
|
||||
return
|
||||
}
|
||||
|
||||
sftpClient, sshClient, err := h.sftpClient(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
stat, err := sftpClient.Stat(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
if stat.IsDir() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot download directory"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := sftpClient.Open(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "open failed"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileName := path.Base(filePath)
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", stat.Size()))
|
||||
io.Copy(c.Writer, file)
|
||||
}
|
||||
|
||||
// UploadFile 上传文件
|
||||
// POST /api/v1/servers/:id/files/upload?path=/tmp/
|
||||
func (h *SFTPHandler) UploadFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server id"})
|
||||
return
|
||||
}
|
||||
|
||||
dirPath := c.Query("path")
|
||||
if dirPath == "" {
|
||||
dirPath = "/tmp/"
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no file uploaded"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
sftpClient, sshClient, err := h.sftpClient(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
destPath := path.Join(dirPath, header.Filename)
|
||||
dest, err := sftpClient.Create(destPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "create file failed"})
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
written, err := io.Copy(dest, file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "write failed"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[SFTP] Uploaded %s (%d bytes) to server %d", destPath, written, id)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "uploaded", "path": destPath, "size": written})
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件或空目录
|
||||
// DELETE /api/v1/servers/:id/files?path=/tmp/test.txt
|
||||
func (h *SFTPHandler) DeleteFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server id"})
|
||||
return
|
||||
}
|
||||
|
||||
filePath := c.Query("path")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path required"})
|
||||
return
|
||||
}
|
||||
|
||||
sftpClient, sshClient, err := h.sftpClient(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
stat, err := sftpClient.Stat(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
if err := sftpClient.RemoveDirectory(filePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "remove dir failed (may not be empty)"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := sftpClient.Remove(filePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "remove failed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// Mkdir 创建目录
|
||||
// POST /api/v1/servers/:id/files/mkdir
|
||||
func (h *SFTPHandler) Mkdir(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "path required"})
|
||||
return
|
||||
}
|
||||
|
||||
sftpClient, sshClient, err := h.sftpClient(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
defer sftpClient.Close()
|
||||
|
||||
if err := sftpClient.MkdirAll(req.Path); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "mkdir failed"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "created", "path": req.Path})
|
||||
}
|
||||
|
||||
// buildBreadcrumbs 构建路径面包屑
|
||||
func buildBreadcrumbs(dirPath string) []map[string]string {
|
||||
if dirPath == "/" || dirPath == "" {
|
||||
return []map[string]string{{"name": "/", "path": "/"}}
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.Trim(dirPath, "/"), "/")
|
||||
var crumbs []map[string]string
|
||||
crumbs = append(crumbs, map[string]string{"name": "/", "path": "/"})
|
||||
|
||||
accum := ""
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
accum += "/" + part
|
||||
crumbs = append(crumbs, map[string]string{
|
||||
"name": part,
|
||||
"path": accum,
|
||||
})
|
||||
}
|
||||
|
||||
return crumbs
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -24,8 +24,8 @@ func NewSiteHandler(db *gorm.DB) *SiteHandler {
|
||||
// ListTasks 列出拨测任务
|
||||
// GET /api/v1/site/tasks?page=1&page_size=20&type=&status=
|
||||
func (h *SiteHandler) ListTasks(c *gin.Context) {
|
||||
var tasks []models.SiteTask
|
||||
q := h.db.Model(&models.SiteTask{})
|
||||
var tasks []model.SiteTask
|
||||
q := h.db.Model(&model.SiteTask{})
|
||||
|
||||
if t := c.Query("type"); t != "" {
|
||||
q = q.Where("type = ?", t)
|
||||
@@ -44,64 +44,64 @@ func (h *SiteHandler) ListTasks(c *gin.Context) {
|
||||
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("name ASC").Find(&tasks)
|
||||
|
||||
if tasks == nil {
|
||||
tasks = []models.SiteTask{}
|
||||
tasks = []model.SiteTask{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"list": tasks, "total": total}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": tasks, "total": total}})
|
||||
}
|
||||
|
||||
// CreateTask 创建拨测任务
|
||||
// POST /api/v1/site/tasks
|
||||
func (h *SiteHandler) CreateTask(c *gin.Context) {
|
||||
var task models.SiteTask
|
||||
var task model.SiteTask
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&task).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: task, Message: "创建成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: task, Message: "创建成功"})
|
||||
}
|
||||
|
||||
// UpdateTask 更新拨测任务
|
||||
// PUT /api/v1/site/tasks/:id
|
||||
func (h *SiteHandler) UpdateTask(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var task models.SiteTask
|
||||
var task model.SiteTask
|
||||
if err := h.db.First(&task, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "任务不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Save(&task)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: task, Message: "更新成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: task, Message: "更新成功"})
|
||||
}
|
||||
|
||||
// DeleteTask 删除拨测任务
|
||||
// DELETE /api/v1/site/tasks/:id
|
||||
func (h *SiteHandler) DeleteTask(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.SiteTask{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
if err := h.db.Delete(&model.SiteTask{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteTasks 批量删除
|
||||
// DELETE /api/v1/site/tasks/batch-delete
|
||||
func (h *SiteHandler) BatchDeleteTasks(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Where("id IN ?", req.IDs).Delete(&models.SiteTask{})
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "批量删除成功"})
|
||||
h.db.Where("id IN ?", req.IDs).Delete(&model.SiteTask{})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "批量删除成功"})
|
||||
}
|
||||
|
||||
// ========== Site Results ==========
|
||||
@@ -109,8 +109,8 @@ func (h *SiteHandler) BatchDeleteTasks(c *gin.Context) {
|
||||
// ListResults 查询拨测结果
|
||||
// GET /api/v1/site/results?task_id=N&from=&to=&limit=100
|
||||
func (h *SiteHandler) ListResults(c *gin.Context) {
|
||||
var results []models.SiteResult
|
||||
q := h.db.Model(&models.SiteResult{})
|
||||
var results []model.SiteResult
|
||||
q := h.db.Model(&model.SiteResult{})
|
||||
|
||||
if tid := c.Query("task_id"); tid != "" {
|
||||
q = q.Where("task_id = ?", tid)
|
||||
@@ -126,9 +126,9 @@ func (h *SiteHandler) ListResults(c *gin.Context) {
|
||||
q.Order("created_at DESC").Limit(limit).Find(&results)
|
||||
|
||||
if results == nil {
|
||||
results = []models.SiteResult{}
|
||||
results = []model.SiteResult{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: results})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: results})
|
||||
}
|
||||
|
||||
// ========== Site Statistics ==========
|
||||
@@ -137,19 +137,19 @@ func (h *SiteHandler) ListResults(c *gin.Context) {
|
||||
// GET /api/v1/site/stats
|
||||
func (h *SiteHandler) GetSiteStats(c *gin.Context) {
|
||||
var total, normal, abnormal int64
|
||||
h.db.Model(&models.SiteTask{}).Count(&total)
|
||||
h.db.Model(&models.SiteTask{}).Where("status = ?", "active").Count(&normal)
|
||||
h.db.Model(&models.SiteTask{}).Where("status = ?", "error").Count(&abnormal)
|
||||
h.db.Model(&model.SiteTask{}).Count(&total)
|
||||
h.db.Model(&model.SiteTask{}).Where("status = ?", "active").Count(&normal)
|
||||
h.db.Model(&model.SiteTask{}).Where("status = ?", "error").Count(&abnormal)
|
||||
|
||||
// Calculate availability rate from recent results
|
||||
var availRate float64
|
||||
if total > 0 {
|
||||
var successCount int64
|
||||
h.db.Model(&models.SiteResult{}).
|
||||
h.db.Model(&model.SiteResult{}).
|
||||
Where("created_at > datetime('now', '-1 hour') AND status = 'success'").
|
||||
Count(&successCount)
|
||||
var totalRecent int64
|
||||
h.db.Model(&models.SiteResult{}).
|
||||
h.db.Model(&model.SiteResult{}).
|
||||
Where("created_at > datetime('now', '-1 hour')").
|
||||
Count(&totalRecent)
|
||||
if totalRecent > 0 {
|
||||
@@ -157,7 +157,7 @@ func (h *SiteHandler) GetSiteStats(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"total": total,
|
||||
"normal": normal,
|
||||
"abnormal": abnormal,
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -24,8 +24,8 @@ func NewSNMPHandler(db *gorm.DB) *SNMPHandler {
|
||||
// ListDevices 列出 SNMP 设备
|
||||
// GET /api/v1/snmp/devices?page=1&page_size=20&keyword=
|
||||
func (h *SNMPHandler) ListDevices(c *gin.Context) {
|
||||
var devices []models.SNMPDevice
|
||||
q := h.db.Model(&models.SNMPDevice{})
|
||||
var devices []model.SNMPDevice
|
||||
q := h.db.Model(&model.SNMPDevice{})
|
||||
|
||||
if kw := c.Query("keyword"); kw != "" {
|
||||
q = q.Where("name LIKE ? OR ip LIKE ?", "%"+kw+"%", "%"+kw+"%")
|
||||
@@ -38,64 +38,64 @@ func (h *SNMPHandler) ListDevices(c *gin.Context) {
|
||||
q.Offset((page - 1) * pageSize).Limit(pageSize).Order("name ASC").Find(&devices)
|
||||
|
||||
if devices == nil {
|
||||
devices = []models.SNMPDevice{}
|
||||
devices = []model.SNMPDevice{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"list": devices, "total": total}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": devices, "total": total}})
|
||||
}
|
||||
|
||||
// CreateDevice 创建 SNMP 设备
|
||||
// POST /api/v1/snmp/devices
|
||||
func (h *SNMPHandler) CreateDevice(c *gin.Context) {
|
||||
var device models.SNMPDevice
|
||||
var device model.SNMPDevice
|
||||
if err := c.ShouldBindJSON(&device); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&device).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: device, Message: "创建成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: device, Message: "创建成功"})
|
||||
}
|
||||
|
||||
// UpdateDevice 更新 SNMP 设备
|
||||
// PUT /api/v1/snmp/devices/:id
|
||||
func (h *SNMPHandler) UpdateDevice(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var device models.SNMPDevice
|
||||
var device model.SNMPDevice
|
||||
if err := h.db.First(&device, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "设备不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "设备不存在"})
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindJSON(&device); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Save(&device)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: device, Message: "更新成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: device, Message: "更新成功"})
|
||||
}
|
||||
|
||||
// DeleteDevice 删除 SNMP 设备
|
||||
// DELETE /api/v1/snmp/devices/:id
|
||||
func (h *SNMPHandler) DeleteDevice(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.SNMPDevice{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
if err := h.db.Delete(&model.SNMPDevice{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteDevices 批量删除
|
||||
// DELETE /api/v1/snmp/devices/batch-delete
|
||||
func (h *SNMPHandler) BatchDeleteDevices(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
h.db.Where("id IN ?", req.IDs).Delete(&models.SNMPDevice{})
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "批量删除成功"})
|
||||
h.db.Where("id IN ?", req.IDs).Delete(&model.SNMPDevice{})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "批量删除成功"})
|
||||
}
|
||||
|
||||
// ========== SNMP Template CRUD ==========
|
||||
@@ -103,27 +103,27 @@ func (h *SNMPHandler) BatchDeleteDevices(c *gin.Context) {
|
||||
// ListTemplates 列出 OID 模板
|
||||
// GET /api/v1/snmp/templates
|
||||
func (h *SNMPHandler) ListTemplates(c *gin.Context) {
|
||||
var templates []models.SNMPTemplate
|
||||
var templates []model.SNMPTemplate
|
||||
h.db.Order("name ASC").Find(&templates)
|
||||
if templates == nil {
|
||||
templates = []models.SNMPTemplate{}
|
||||
templates = []model.SNMPTemplate{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: templates})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: templates})
|
||||
}
|
||||
|
||||
// CreateTemplate 创建 OID 模板
|
||||
// POST /api/v1/snmp/templates
|
||||
func (h *SNMPHandler) CreateTemplate(c *gin.Context) {
|
||||
var template models.SNMPTemplate
|
||||
var template model.SNMPTemplate
|
||||
if err := c.ShouldBindJSON(&template); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Create(&template).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: template, Message: "创建成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: template, Message: "创建成功"})
|
||||
}
|
||||
|
||||
// ========== SNMP Statistics ==========
|
||||
@@ -132,10 +132,10 @@ func (h *SNMPHandler) CreateTemplate(c *gin.Context) {
|
||||
// GET /api/v1/snmp/stats
|
||||
func (h *SNMPHandler) GetSNMPStats(c *gin.Context) {
|
||||
var total, online int64
|
||||
h.db.Model(&models.SNMPDevice{}).Count(&total)
|
||||
h.db.Model(&models.SNMPDevice{}).Where("status = ?", "online").Count(&online)
|
||||
h.db.Model(&model.SNMPDevice{}).Count(&total)
|
||||
h.db.Model(&model.SNMPDevice{}).Where("status = ?", "online").Count(&online)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"total": total,
|
||||
"online": online,
|
||||
"abnormal": total - online,
|
||||
@@ -0,0 +1,249 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TerminalSessionHandler 终端会话审计处理器 // Logan.Li
|
||||
type TerminalSessionHandler struct {
|
||||
db *gorm.DB
|
||||
killFunc func(sessionID uint) bool // 外部 Kill 回调
|
||||
}
|
||||
|
||||
// NewTerminalSessionHandler 创建终端会话审计处理器
|
||||
func NewTerminalSessionHandler(db *gorm.DB, killFunc func(sessionID uint) bool) *TerminalSessionHandler {
|
||||
return &TerminalSessionHandler{db: db, killFunc: killFunc}
|
||||
}
|
||||
|
||||
// ListTerminalSessions 终端会话列表
|
||||
// GET /api/v1/terminal-sessions?server_id=&username=&status=&page=&page_size=
|
||||
func (h *TerminalSessionHandler) ListTerminalSessions(c *gin.Context) {
|
||||
var req model.TerminalSessionQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := h.db.Model(&model.TerminalSession{})
|
||||
if req.ServerID > 0 {
|
||||
query = query.Where("server_id = ?", req.ServerID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
query = query.Where("status = ?", req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var sessions []model.TerminalSession
|
||||
if err := query.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&sessions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{
|
||||
List: sessions,
|
||||
Total: total,
|
||||
}})
|
||||
}
|
||||
|
||||
// GetTerminalSession 终端会话详情
|
||||
// GET /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) GetTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session model.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: session})
|
||||
}
|
||||
|
||||
// GetTerminalRecord 终端会话录制数据(用于回放)
|
||||
// GET /api/v1/terminal-sessions/:id/record
|
||||
func (h *TerminalSessionHandler) GetTerminalRecord(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session model.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: session.RecordData})
|
||||
}
|
||||
|
||||
// DeleteTerminalSession 删除终端会话记录
|
||||
// DELETE /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) DeleteTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&model.TerminalSession{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteTerminalSessions 批量删除终端会话记录
|
||||
// POST /api/v1/terminal-sessions/batch-delete
|
||||
func (h *TerminalSessionHandler) BatchDeleteTerminalSessions(c *gin.Context) {
|
||||
var req model.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&model.TerminalSession{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功", Data: gin.H{"deleted": result.RowsAffected}})
|
||||
}
|
||||
|
||||
// KillSession 强制终止终端会话
|
||||
// POST /api/v1/terminal-sessions/:id/kill
|
||||
func (h *TerminalSessionHandler) KillSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session model.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if session.Status == "closed" {
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "already closed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 强制终止 WebShell 连接
|
||||
if h.killFunc != nil {
|
||||
h.killFunc(session.ID)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := int(now.Sub(session.StartTime).Seconds())
|
||||
if err := h.db.Model(&session).Updates(map[string]interface{}{
|
||||
"status": "closed",
|
||||
"end_time": now,
|
||||
"duration": duration,
|
||||
}).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "终止失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已终止"})
|
||||
}
|
||||
|
||||
// DownloadSession 下载终端会话录制文件
|
||||
// GET /api/v1/terminal-sessions/:id/download
|
||||
func (h *TerminalSessionHandler) DownloadSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session model.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if session.RecordData == "" {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "无录制数据"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%d.cast"`, session.ID))
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", len(session.RecordData)))
|
||||
c.String(http.StatusOK, session.RecordData)
|
||||
}
|
||||
|
||||
// StreamSession 终端会话实时流(WebSocket 暂未实现)
|
||||
// GET /api/v1/terminal-sessions/:id/stream
|
||||
func (h *TerminalSessionHandler) StreamSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session model.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "WebSocket streaming not yet implemented",
|
||||
"session_id": id,
|
||||
"status": session.Status,
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/toolinstaller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -27,38 +27,38 @@ func NewToolConfigHandler(db *gorm.DB, ti *toolinstaller.ToolInstaller) *ToolCon
|
||||
|
||||
// ListTools returns all configured tools
|
||||
func (h *ToolConfigHandler) ListTools(c *gin.Context) {
|
||||
var tools []models.DeployTool
|
||||
var tools []model.DeployTool
|
||||
h.db.Order("sort_order ASC").Find(&tools)
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{List: tools, Total: int64(len(tools))}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{List: tools, Total: int64(len(tools))}})
|
||||
}
|
||||
|
||||
// GetTool returns a single tool
|
||||
func (h *ToolConfigHandler) GetTool(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
var tool model.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: tool})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: tool})
|
||||
}
|
||||
|
||||
// UpdateTool updates a tool's configuration
|
||||
func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
var tool model.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DisplayName *string `json:"display_name"`
|
||||
Description *string `json:"description"`
|
||||
InstallMethod *models.ToolInstallMethod `json:"install_method"`
|
||||
InstallMethod *model.ToolInstallMethod `json:"install_method"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
||||
}
|
||||
if req.InstallMethod != nil {
|
||||
updates["install_method"] = *req.InstallMethod
|
||||
if *req.InstallMethod == models.ToolDisabled {
|
||||
updates["status"] = models.ToolNotInstalled
|
||||
if *req.InstallMethod == model.ToolDisabled {
|
||||
updates["status"] = model.ToolNotInstalled
|
||||
}
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
@@ -86,24 +86,24 @@ func (h *ToolConfigHandler) UpdateTool(c *gin.Context) {
|
||||
h.db.Model(&tool).Updates(updates)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已更新"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已更新"})
|
||||
}
|
||||
|
||||
// InstallTool triggers lazy installation with optional version override
|
||||
func (h *ToolConfigHandler) InstallTool(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
var tool model.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if !tool.Enabled {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "工具已禁用"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "工具已禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
if tool.InstallMethod != models.ToolLazy {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "仅懒加载工具可手动安装"})
|
||||
if tool.InstallMethod != model.ToolLazy {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "仅懒加载工具可手动安装"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (h *ToolConfigHandler) InstallTool(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Mark as installing immediately
|
||||
h.db.Model(&tool).Update("status", models.ToolInstalling)
|
||||
h.db.Model(&tool).Update("status", model.ToolInstalling)
|
||||
|
||||
// Background installation — actually call the installer
|
||||
go func() {
|
||||
@@ -142,14 +142,14 @@ func (h *ToolConfigHandler) InstallTool(c *gin.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "安装已触发", Data: gin.H{"version": version}})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "安装已触发", Data: gin.H{"version": version}})
|
||||
}
|
||||
|
||||
// CheckToolStatus re-checks a tool's installation status
|
||||
func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
var tool model.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,16 +167,16 @@ func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
status := models.ToolNotInstalled
|
||||
status := model.ToolNotInstalled
|
||||
if available {
|
||||
status = models.ToolInstalled
|
||||
status = model.ToolInstalled
|
||||
}
|
||||
h.db.Model(&tool).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"installed_version": version,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"available": available,
|
||||
"version": version,
|
||||
"tool": tool,
|
||||
@@ -185,15 +185,15 @@ func (h *ToolConfigHandler) CheckToolStatus(c *gin.Context) {
|
||||
|
||||
// GetToolVersions returns available versions for a tool (jdk/node/maven)
|
||||
func (h *ToolConfigHandler) GetToolVersions(c *gin.Context) {
|
||||
var tool models.DeployTool
|
||||
var tool model.DeployTool
|
||||
if err := h.db.First(&tool, c.Param("id")); err.Error != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "工具不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "工具不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
versions := getPresetVersions(tool.Name)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
|
||||
"name": tool.Name,
|
||||
"default": tool.DefaultVersion,
|
||||
"versions": versions,
|
||||
@@ -1,11 +1,12 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/api/middleware"
|
||||
"ezops/internal/config"
|
||||
"ezops/internal/models"
|
||||
"ezops/middleware"
|
||||
"ezops/config"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -23,37 +24,103 @@ func NewUserHandler(db *gorm.DB, cfg *config.Config) *UserHandler {
|
||||
return &UserHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
// recordLoginLog 记录登录日志 // Logan.Li
|
||||
func (h *UserHandler) recordLoginLog(c *gin.Context, username, status, failReason, loginType string) {
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
uid := (*uint)(nil)
|
||||
var user model.User
|
||||
if err := h.db.Where("username = ?", username).First(&user).Error; err == nil {
|
||||
uid = &user.ID
|
||||
}
|
||||
|
||||
// 简单解析 UA 提取浏览器和 OS
|
||||
browser, os := parseUserAgent(userAgent)
|
||||
|
||||
log := model.LoginLog{
|
||||
UserID: uid,
|
||||
Username: username,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
Browser: browser,
|
||||
OS: os,
|
||||
LoginType: loginType,
|
||||
Status: status,
|
||||
FailReason: failReason,
|
||||
LoginAt: time.Now(),
|
||||
}
|
||||
h.db.Create(&log)
|
||||
}
|
||||
|
||||
// parseUserAgent 简单解析 UA 提取浏览器/OS // Logan.Li
|
||||
func parseUserAgent(ua string) (browser, os string) {
|
||||
browser = "Unknown"
|
||||
os = "Unknown"
|
||||
if ua == "" {
|
||||
return
|
||||
}
|
||||
// Browser
|
||||
switch {
|
||||
case containsAny(ua, "Edg/"): browser = "Edge"
|
||||
case containsAny(ua, "Chrome"): browser = "Chrome"
|
||||
case containsAny(ua, "Safari") && !containsAny(ua, "Chrome"): browser = "Safari"
|
||||
case containsAny(ua, "Firefox"): browser = "Firefox"
|
||||
case containsAny(ua, "curl"): browser = "curl"
|
||||
case containsAny(ua, "Python"): browser = "Python"
|
||||
}
|
||||
// OS
|
||||
switch {
|
||||
case containsAny(ua, "Windows"): os = "Windows"
|
||||
case containsAny(ua, "Mac OS"): os = "macOS"
|
||||
case containsAny(ua, "Linux") || containsAny(ua, "X11"): os = "Linux"
|
||||
case containsAny(ua, "Android"): os = "Android"
|
||||
case containsAny(ua, "iPhone") || containsAny(ua, "iPad"): os = "iOS"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func containsAny(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Description 用户登录获取JWT令牌
|
||||
// @Tags 用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.LoginRequest true "登录请求"
|
||||
// @Success 200 {object} models.Response{data=models.LoginResponse}
|
||||
// @Failure 400 {object} models.Response
|
||||
// @Failure 401 {object} models.Response
|
||||
// @Param request body model.LoginRequest true "登录请求"
|
||||
// @Success 200 {object} model.Response{data=model.LoginResponse}
|
||||
// @Failure 400 {object} model.Response
|
||||
// @Failure 401 {object} model.Response
|
||||
// @Router /auth/login [post]
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
var req model.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
err := h.db.Where("username = ?", req.Username).First(&user).Error
|
||||
|
||||
if err == nil {
|
||||
// 本地用户存在
|
||||
if user.Status != 1 {
|
||||
c.JSON(http.StatusUnauthorized, models.Response{
|
||||
c.JSON(http.StatusUnauthorized, model.Response{
|
||||
Code: 401,
|
||||
Message: "账号已被禁用",
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "账号已被禁用", "password")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,10 +132,11 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
if user.AuthSource == "ldap" || user.Password == "-" {
|
||||
ldapInfo, ldapErr := tryLDAPAuth(h.db, req.Username, req.Password)
|
||||
if ldapErr != nil {
|
||||
c.JSON(http.StatusUnauthorized, models.Response{
|
||||
c.JSON(http.StatusUnauthorized, model.Response{
|
||||
Code: 401,
|
||||
Message: "用户名或密码错误",
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "用户名或密码错误", "password")
|
||||
return
|
||||
}
|
||||
// LDAP OK — 更新本地信息
|
||||
@@ -85,10 +153,11 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusUnauthorized, models.Response{
|
||||
c.JSON(http.StatusUnauthorized, model.Response{
|
||||
Code: 401,
|
||||
Message: "用户名或密码错误",
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "用户名或密码错误", "password")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -96,14 +165,15 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
// 本地用户不存在 — 尝试 LDAP 认证
|
||||
ldapInfo, ldapErr := tryLDAPAuth(h.db, req.Username, req.Password)
|
||||
if ldapErr != nil {
|
||||
c.JSON(http.StatusUnauthorized, models.Response{
|
||||
c.JSON(http.StatusUnauthorized, model.Response{
|
||||
Code: 401,
|
||||
Message: "用户名或密码错误",
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "用户名或密码错误", "password")
|
||||
return
|
||||
}
|
||||
// LDAP 认证成功,自动创建本地用户
|
||||
newUser := models.User{
|
||||
newUser := model.User{
|
||||
Username: ldapInfo.Username,
|
||||
Password: "-",
|
||||
RealName: ldapInfo.DisplayName,
|
||||
@@ -115,28 +185,31 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
newUser.RealName = newUser.Username
|
||||
}
|
||||
if err := h.db.Create(&newUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建LDAP用户失败: " + err.Error(),
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "创建LDAP用户失败", "ldap")
|
||||
return
|
||||
}
|
||||
user = newUser
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "数据库错误",
|
||||
})
|
||||
h.recordLoginLog(c, req.Username, "failed", "数据库错误", "password")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := middleware.GenerateToken(&user, &h.cfg.JWT)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "生成令牌失败",
|
||||
})
|
||||
h.recordLoginLog(c, user.Username, "failed", "生成令牌失败", "password")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,10 +217,12 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
now := h.db.NowFunc()
|
||||
h.db.Model(&user).Update("last_login", &now)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
h.recordLoginLog(c, user.Username, "success", "", "password")
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "登录成功",
|
||||
Data: models.LoginResponse{
|
||||
Data: model.LoginResponse{
|
||||
Token: token,
|
||||
ExpiresIn: h.cfg.JWT.ExpireHour * 3600,
|
||||
User: &user,
|
||||
@@ -161,14 +236,14 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
// @Tags 用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.CreateUserRequest true "注册请求"
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Failure 400 {object} models.Response
|
||||
// @Param request body model.CreateUserRequest true "注册请求"
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Failure 400 {object} model.Response
|
||||
// @Router /auth/register [post]
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req models.CreateUserRequest
|
||||
var req model.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -176,9 +251,9 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var existingUser models.User
|
||||
var existingUser model.User
|
||||
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "用户名已存在",
|
||||
})
|
||||
@@ -188,14 +263,14 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "密码加密失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
user := model.User{
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
Email: req.Email,
|
||||
@@ -205,14 +280,14 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建用户失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "注册成功",
|
||||
Data: user,
|
||||
@@ -225,21 +300,21 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
// @Tags 用户
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Router /user/profile [get]
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
if err := h.db.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "用户不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: user,
|
||||
@@ -253,15 +328,15 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.UpdateUserRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Param request body model.UpdateUserRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Router /user/profile [put]
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
var req models.UpdateUserRequest
|
||||
var req model.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -279,18 +354,18 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
updates["real_name"] = req.RealName
|
||||
}
|
||||
|
||||
if err := h.db.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
if err := h.db.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
h.db.First(&user, userID)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: user,
|
||||
@@ -304,24 +379,24 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.ChangePasswordRequest true "修改密码请求"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Param request body model.ChangePasswordRequest true "修改密码请求"
|
||||
// @Success 200 {object} model.Response
|
||||
// @Router /user/password [put]
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
var req models.ChangePasswordRequest
|
||||
var req model.ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
if err := h.db.First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "用户不存在",
|
||||
})
|
||||
@@ -329,7 +404,7 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "原密码错误",
|
||||
})
|
||||
@@ -338,7 +413,7 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "密码加密失败",
|
||||
})
|
||||
@@ -346,14 +421,14 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "修改密码失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "密码修改成功",
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -20,19 +20,19 @@ import (
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Success 200 {object} models.Response{data=models.ListResponse}
|
||||
// @Success 200 {object} model.Response{data=model.ListResponse}
|
||||
// @Router /users [get]
|
||||
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
var query models.PageQuery
|
||||
var query model.PageQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := h.db.Model(&models.User{})
|
||||
db := h.db.Model(&model.User{})
|
||||
if query.Keyword != "" {
|
||||
db = db.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
|
||||
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
|
||||
@@ -41,13 +41,13 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
|
||||
var users []models.User
|
||||
var users []model.User
|
||||
db.Preload("Roles").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&users)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: models.ListResponse{
|
||||
Data: model.ListResponse{
|
||||
List: users,
|
||||
Total: total,
|
||||
},
|
||||
@@ -61,21 +61,21 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Router /users/{id} [get]
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
if err := h.db.Preload("Roles").First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "用户不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: user,
|
||||
@@ -89,13 +89,13 @@ func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body models.CreateUserRequest true "创建请求"
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Param request body model.CreateUserRequest true "创建请求"
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Router /users [post]
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req models.CreateUserRequest
|
||||
var req model.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -103,9 +103,9 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var existingUser models.User
|
||||
var existingUser model.User
|
||||
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "用户名已存在",
|
||||
})
|
||||
@@ -115,14 +115,14 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "密码加密失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
user := model.User{
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
Email: req.Email,
|
||||
@@ -132,7 +132,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&user).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "创建用户失败",
|
||||
})
|
||||
@@ -141,12 +141,12 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
|
||||
// 关联角色
|
||||
if len(req.RoleIDs) > 0 {
|
||||
var roles []models.Role
|
||||
var roles []model.Role
|
||||
h.db.Find(&roles, req.RoleIDs)
|
||||
h.db.Model(&user).Association("Roles").Append(roles)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "创建成功",
|
||||
Data: user,
|
||||
@@ -161,24 +161,24 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "用户ID"
|
||||
// @Param request body models.UpdateUserRequest true "更新请求"
|
||||
// @Success 200 {object} models.Response{data=models.User}
|
||||
// @Param request body model.UpdateUserRequest true "更新请求"
|
||||
// @Success 200 {object} model.Response{data=model.User}
|
||||
// @Router /users/{id} [put]
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req models.UpdateUserRequest
|
||||
var req model.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var user model.User
|
||||
if err := h.db.First(&user, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{
|
||||
c.JSON(http.StatusNotFound, model.Response{
|
||||
Code: 404,
|
||||
Message: "用户不存在",
|
||||
})
|
||||
@@ -200,7 +200,7 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Model(&user).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "更新失败",
|
||||
})
|
||||
@@ -209,14 +209,14 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
|
||||
// 更新角色
|
||||
if len(req.RoleIDs) > 0 {
|
||||
var roles []models.Role
|
||||
var roles []model.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{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "更新成功",
|
||||
Data: user,
|
||||
@@ -230,20 +230,20 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} models.Response
|
||||
// @Success 200 {object} model.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{
|
||||
if err := h.db.Delete(&model.User{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{
|
||||
Code: 500,
|
||||
Message: "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "删除成功",
|
||||
})
|
||||
@@ -264,7 +264,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
||||
SkipExisting bool `json:"skip_existing"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
c.JSON(http.StatusBadRequest, model.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
@@ -281,7 +281,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
// Check if exists
|
||||
var existing models.User
|
||||
var existing model.User
|
||||
if err := h.db.Where("username = ?", u.Username).First(&existing).Error; err == nil {
|
||||
if req.SkipExisting {
|
||||
skipped++
|
||||
@@ -303,7 +303,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
user := model.User{
|
||||
Username: u.Username,
|
||||
Password: string(hashed),
|
||||
Email: u.Email,
|
||||
@@ -324,7 +324,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
||||
msg += fmt.Sprintf(", %d 个错误", len(errors))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: msg,
|
||||
Data: gin.H{
|
||||
@@ -340,10 +340,10 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) {
|
||||
|
||||
// ListRoles 获取角色列表(供前端下拉选择) // Logan.Li
|
||||
func (h *UserHandler) ListRoles(c *gin.Context) {
|
||||
var roles []models.Role
|
||||
var roles []model.Role
|
||||
h.db.Where("status = ?", 1).Preload("Permissions").Order("id ASC").Find(&roles)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200, Data: models.ListResponse{List: roles, Total: int64(len(roles))},
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200, Data: model.ListResponse{List: roles, Total: int64(len(roles))},
|
||||
})
|
||||
}
|
||||
// GET /api/v1/admin/users/import-template
|
||||
@@ -0,0 +1,427 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ezops/model"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// WSMessage WebSocket消息结构
|
||||
type WSMessage struct {
|
||||
Status int `json:"status"` // 0=data, 1=resize
|
||||
Data string `json:"data"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
// SessionContext 活跃会话上下文
|
||||
type SessionContext struct {
|
||||
SessionID uint
|
||||
Cancel chan struct{}
|
||||
}
|
||||
|
||||
// asciicastRecorder writes terminal output in asciicast v2 format
|
||||
type asciicastRecorder struct {
|
||||
buf strings.Builder
|
||||
startTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newAsciicastRecorder() *asciicastRecorder {
|
||||
r := &asciicastRecorder{startTime: time.Now()}
|
||||
r.writeHeader()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *asciicastRecorder) writeHeader() {
|
||||
h, _ := json.Marshal(map[string]interface{}{
|
||||
"version": 2,
|
||||
"width": 80,
|
||||
"height": 24,
|
||||
"timestamp": r.startTime.Unix(),
|
||||
"env": map[string]string{
|
||||
"SHELL": "/bin/bash",
|
||||
"TERM": "xterm-256color",
|
||||
},
|
||||
})
|
||||
r.buf.Write(h)
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (r *asciicastRecorder) WriteRow(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delta := time.Since(r.startTime).Seconds()
|
||||
row, _ := json.Marshal([]interface{}{delta, "o", string(data)})
|
||||
r.buf.Write(row)
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (r *asciicastRecorder) String() string { return r.buf.String() }
|
||||
func (r *asciicastRecorder) Len() int { return r.buf.Len() }
|
||||
|
||||
// WebShellHandler WebShell处理器
|
||||
type WebShellHandler struct {
|
||||
db *gorm.DB
|
||||
auditHub *AuditHub
|
||||
clients sync.Map // sessionID → *SessionContext
|
||||
}
|
||||
|
||||
func NewWebShellHandler(db *gorm.DB, auditHub *AuditHub) *WebShellHandler {
|
||||
return &WebShellHandler{db: db, auditHub: auditHub}
|
||||
}
|
||||
|
||||
// KillSession 强制终止指定终端会话
|
||||
func (h *WebShellHandler) KillSession(sessionID uint) bool {
|
||||
if val, ok := h.clients.Load(sessionID); ok {
|
||||
ctx := val.(*SessionContext)
|
||||
close(ctx.Cancel)
|
||||
h.clients.Delete(sessionID)
|
||||
log.Printf("[WebShell] Kill signal sent session=%d", sessionID)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// formatSSHError 将 Go SSH 错误转换为友好的中文提示
|
||||
func formatSSHError(err error) string {
|
||||
errStr := err.Error()
|
||||
switch {
|
||||
case strings.Contains(errStr, "i/o timeout"):
|
||||
return "Connection timeout (target may be behind firewall or unreachable)"
|
||||
case strings.Contains(errStr, "connection refused"):
|
||||
return "Connection refused (SSH service not running or wrong port)"
|
||||
case strings.Contains(errStr, "no route to host"):
|
||||
return "No route to host (IP address unreachable)"
|
||||
case strings.Contains(errStr, "authentication failed") || strings.Contains(errStr, "unable to authenticate"):
|
||||
return "Authentication failed (wrong username or password)"
|
||||
case strings.Contains(errStr, "handshake"):
|
||||
return "SSH handshake failed (target may not be an SSH service)"
|
||||
case strings.Contains(errStr, "dns") || strings.Contains(errStr, "no such host"):
|
||||
return "DNS resolution failed (invalid IP address)"
|
||||
case strings.Contains(errStr, "ssh: must specify"):
|
||||
return "Authentication method error (no password or key configured)"
|
||||
default:
|
||||
return errStr
|
||||
}
|
||||
}
|
||||
|
||||
// contains 字符串包含检查 (used by agent_install.go)
|
||||
func contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
// Terminal WebSocket终端连接 — 真实SSH + asciicast v2录制 + 命令审计
|
||||
func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
serverID := c.Query("id")
|
||||
if serverID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing server id"})
|
||||
return
|
||||
}
|
||||
|
||||
var server model.Server
|
||||
if err := h.db.First(&server, serverID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] WS upgrade failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var userIDPtr *uint
|
||||
if uid, exists := c.Get("userID"); exists {
|
||||
if uidVal, ok := uid.(uint); ok {
|
||||
userIDPtr = &uidVal
|
||||
}
|
||||
}
|
||||
username, _ := c.Get("username")
|
||||
usernameStr, _ := username.(string)
|
||||
|
||||
termSession := model.TerminalSession{
|
||||
UserID: userIDPtr,
|
||||
Username: usernameStr,
|
||||
ServerID: server.ID,
|
||||
ServerName: server.Hostname,
|
||||
ServerIP: server.IP,
|
||||
ServerPort: server.Port,
|
||||
ClientIP: c.ClientIP(),
|
||||
Status: "active",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
if err := h.db.Create(&termSession).Error; err != nil {
|
||||
log.Printf("[WebShell] Create session record failed: %v", err)
|
||||
}
|
||||
|
||||
recorder := newAsciicastRecorder()
|
||||
const maxRecordSize = 1024 * 1024
|
||||
var cmdBuf strings.Builder
|
||||
|
||||
finalizeSession := func(status string) {
|
||||
endTime := time.Now()
|
||||
duration := int(endTime.Sub(termSession.StartTime).Seconds())
|
||||
recData := recorder.String()
|
||||
recSize := recorder.Len()
|
||||
log.Printf("[WebShell] Finalize session=%d status=%s duration=%ds rec=%d bytes", termSession.ID, status, duration, recSize)
|
||||
// Raw SQL with error logging
|
||||
result := h.db.Exec("UPDATE sys_terminal_sessions SET status=?, duration=?, end_time=?, record_data=?, record_size=? WHERE id=?",
|
||||
status, duration, endTime, recData, recSize, termSession.ID)
|
||||
if result.Error != nil {
|
||||
log.Printf("[WebShell] ERROR finalize session=%d: %v", termSession.ID, result.Error)
|
||||
} else {
|
||||
log.Printf("[WebShell] Finalize OK session=%d rows_affected=%d", termSession.ID, result.RowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
connectingMsg := "\r\n\x1b[36mConnecting to " + server.Hostname + " (" + server.IP + ":" + strconv.Itoa(server.Port) + ")...\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: connectingMsg})
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
Timeout: 10 * time.Second,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
log.Printf("[WebShell] TOFU: trusting host key for %s: %s", server.Hostname, ssh.FingerprintSHA256(key))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
switch server.AuthType {
|
||||
case "key":
|
||||
signer, err := ssh.ParsePrivateKey([]byte(server.SSHKey))
|
||||
if err != nil {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(server.Password)}
|
||||
} else {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
default:
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(server.Password)}
|
||||
}
|
||||
|
||||
addr := server.IP + ":" + strconv.Itoa(server.Port)
|
||||
sshClient, err := ssh.Dial("tcp", addr, sshConfig)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] SSH connect failed %s: %v", addr, err)
|
||||
friendlyErr := formatSSHError(err)
|
||||
errMsg := "\r\n\x1b[31mConnection failed: " + friendlyErr + "\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: errMsg})
|
||||
recorder.WriteRow([]byte(errMsg))
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
|
||||
log.Printf("[WebShell] SSH connected %s (%s)", server.Hostname, addr)
|
||||
|
||||
session, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] Create SSH session failed: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31mFailed to create SSH session\x1b[0m\r\n"})
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := session.RequestPty("xterm-256color", 24, 80, modes); err != nil {
|
||||
log.Printf("[WebShell] PTY request failed: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31mPTY request failed\x1b[0m\r\n"})
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
stdinPipe, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
log.Printf("[WebShell] Shell start failed: %v", err)
|
||||
finalizeSession("failed")
|
||||
return
|
||||
}
|
||||
|
||||
welcome := "\r\n\x1b[32mAIOps WebShell\x1b[0m \u2014 connected to " + server.Hostname + " (" + server.IP + ")\r\n\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: welcome})
|
||||
recorder.WriteRow([]byte(welcome))
|
||||
|
||||
sessCtx := &SessionContext{
|
||||
SessionID: termSession.ID,
|
||||
Cancel: make(chan struct{}),
|
||||
}
|
||||
h.clients.Store(termSession.ID, sessCtx)
|
||||
log.Printf("[WebShell] Session registered session=%d", termSession.ID)
|
||||
defer func() {
|
||||
h.clients.Delete(termSession.ID)
|
||||
if h.auditHub != nil {
|
||||
h.auditHub.Cleanup(termSession.ID)
|
||||
}
|
||||
log.Printf("[WebShell] Session unregistered session=%d", termSession.ID)
|
||||
}()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
// goroutine 1: Cancel listener
|
||||
go func() {
|
||||
select {
|
||||
case <-sessCtx.Cancel:
|
||||
log.Printf("[WebShell] Received Kill signal session=%d, closing", termSession.ID)
|
||||
conn.Close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
// goroutine 2: SSH stdout → WebSocket + asciicast recording + AuditHub
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := stdoutPipe.Read(buf)
|
||||
if err != nil {
|
||||
discMsg := "\r\n\x1b[31m[Remote connection closed]\x1b[0m\r\n"
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: discMsg})
|
||||
recorder.WriteRow([]byte(discMsg))
|
||||
if h.auditHub != nil {
|
||||
h.auditHub.Broadcast(termSession.ID, []byte(discMsg))
|
||||
}
|
||||
break
|
||||
}
|
||||
if n > 0 {
|
||||
data := buf[:n]
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: string(data)})
|
||||
if recorder.Len() < maxRecordSize {
|
||||
recorder.WriteRow(data)
|
||||
}
|
||||
if h.auditHub != nil {
|
||||
h.auditHub.Broadcast(termSession.ID, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Main loop: WebSocket → SSH stdin (command capture + idle timeout)
|
||||
const idleTimeout = 15 * time.Minute
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(idleTimeout))
|
||||
msgType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
log.Printf("[WebShell] Idle timeout 15min session=%d", termSession.ID)
|
||||
} else {
|
||||
log.Printf("[WebShell] WS read error session=%d: %v", termSession.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgType == websocket.TextMessage {
|
||||
var msg WSMessage
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch msg.Status {
|
||||
case 0:
|
||||
if msg.Data != "" {
|
||||
stdinPipe.Write([]byte(msg.Data))
|
||||
|
||||
// Command extraction from stdin
|
||||
cmdBuf.WriteString(msg.Data)
|
||||
if strings.Contains(msg.Data, "\r") || strings.Contains(msg.Data, "\n") {
|
||||
cmd := extractCommand(cmdBuf.String())
|
||||
if cmd != "" {
|
||||
recordCommand(h.db, termSession.ID, termSession.ServerID, usernameStr, cmd)
|
||||
}
|
||||
cmdBuf.Reset()
|
||||
}
|
||||
if cmdBuf.Len() > 8192 {
|
||||
cmdBuf.Reset()
|
||||
}
|
||||
}
|
||||
case 1: // resize
|
||||
if msg.Cols > 0 && msg.Rows > 0 {
|
||||
session.WindowChange(msg.Rows, msg.Cols)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close SSH to unblock stdout goroutine, then wait for it
|
||||
sshClient.Close()
|
||||
<-done
|
||||
stdinPipe.Close()
|
||||
finalizeSession("closed")
|
||||
log.Printf("[WebShell] Disconnected %s (%s)", server.Hostname, addr)
|
||||
}
|
||||
|
||||
// extractCommand extracts a clean command from raw terminal input
|
||||
func extractCommand(input string) string {
|
||||
cleaned := strings.Map(func(r rune) rune {
|
||||
if r < 32 && r != '\t' {
|
||||
return -1
|
||||
}
|
||||
if r == 127 {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, input)
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
if len(cleaned) < 2 || len(cleaned) > 500 {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(cleaned, "\x1b[") {
|
||||
return ""
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// recordCommand records a terminal command to sys_session_commands
|
||||
func recordCommand(db *gorm.DB, sessionID, serverID uint, username, command string) {
|
||||
now := time.Now()
|
||||
cmd := model.SessionCommand{
|
||||
SessionID: sessionID,
|
||||
ServerID: serverID,
|
||||
Username: username,
|
||||
Command: command,
|
||||
Timestamp: now,
|
||||
}
|
||||
if err := db.Create(&cmd).Error; err != nil {
|
||||
log.Printf("[WebShell] Command record failed: %v", err)
|
||||
} else {
|
||||
log.Printf("[WebShell] Command recorded session=%d cmd=%q", sessionID, command)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
)
|
||||
|
||||
// WorkspaceFilesHandler 工作区文件浏览器处理器
|
||||
@@ -41,7 +41,7 @@ func (h *WorkspaceFilesHandler) resolveWorkspace(c *gin.Context) (string, error)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var run models.PipelineRun
|
||||
var run model.PipelineRun
|
||||
if err := h.db.First(&run, runID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "run not found"})
|
||||
return "", err
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+33
-33
@@ -1,11 +1,11 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/zabbix"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -29,43 +29,43 @@ func NewZabbixDSHandler(db *gorm.DB) *ZabbixDSHandler {
|
||||
// List 数据源列表
|
||||
// GET /api/v1/zabbix-datasources
|
||||
func (h *ZabbixDSHandler) List(c *gin.Context) {
|
||||
var sources []models.ZabbixDataSource
|
||||
var sources []model.ZabbixDataSource
|
||||
if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if sources == nil {
|
||||
sources = []models.ZabbixDataSource{}
|
||||
sources = []model.ZabbixDataSource{}
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: sources})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: sources})
|
||||
}
|
||||
|
||||
// Get 获取单个数据源
|
||||
// GET /api/v1/zabbix-datasources/:id
|
||||
func (h *ZabbixDSHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.ZabbixDataSource
|
||||
var ds model.ZabbixDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ds})
|
||||
}
|
||||
|
||||
// Create 创建数据源
|
||||
// POST /api/v1/zabbix-datasources
|
||||
func (h *ZabbixDSHandler) Create(c *gin.Context) {
|
||||
var req models.CreateZabbixDSReq
|
||||
var req model.CreateZabbixDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsDefault {
|
||||
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
}
|
||||
|
||||
ds := models.ZabbixDataSource{
|
||||
ds := model.ZabbixDataSource{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Username: req.Username,
|
||||
@@ -89,26 +89,26 @@ func (h *ZabbixDSHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.db.Create(&ds).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds, Message: "创建成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: ds, Message: "创建成功"})
|
||||
}
|
||||
|
||||
// Update 更新数据源
|
||||
// PUT /api/v1/zabbix-datasources/:id
|
||||
func (h *ZabbixDSHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var ds models.ZabbixDataSource
|
||||
var ds model.ZabbixDataSource
|
||||
if err := h.db.First(&ds, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
|
||||
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "数据源不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateZabbixDSReq
|
||||
var req model.UpdateZabbixDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,50 +127,50 @@ func (h *ZabbixDSHandler) Update(c *gin.Context) {
|
||||
if req.IsDefault != nil {
|
||||
updates["is_default"] = *req.IsDefault
|
||||
if *req.IsDefault {
|
||||
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||
h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
|
||||
}
|
||||
}
|
||||
if req.Status != nil { updates["status"] = *req.Status }
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功"})
|
||||
}
|
||||
|
||||
// Delete 删除数据源
|
||||
// DELETE /api/v1/zabbix-datasources/:id
|
||||
func (h *ZabbixDSHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := h.db.Delete(&models.ZabbixDataSource{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||
if err := h.db.Delete(&model.ZabbixDataSource{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
|
||||
// SetDefault 设为默认
|
||||
// POST /api/v1/zabbix-datasources/:id/default
|
||||
func (h *ZabbixDSHandler) SetDefault(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
if err := h.db.Model(&models.ZabbixDataSource{}).Where("id = ?", id).Update("is_default", true).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "设置失败: " + err.Error()})
|
||||
h.db.Model(&model.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
|
||||
if err := h.db.Model(&model.ZabbixDataSource{}).Where("id = ?", id).Update("is_default", true).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "设置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已设为默认"})
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已设为默认"})
|
||||
}
|
||||
|
||||
// TestConnection 测试 Zabbix 连接
|
||||
// POST /api/v1/zabbix-datasources/test
|
||||
func (h *ZabbixDSHandler) TestConnection(c *gin.Context) {
|
||||
var req models.TestZabbixDSReq
|
||||
var req model.TestZabbixDSReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (h *ZabbixDSHandler) TestConnection(c *gin.Context) {
|
||||
defer cancel()
|
||||
|
||||
if err := client.Auth(ctx); err != nil {
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: map[string]interface{}{
|
||||
"success": false,
|
||||
@@ -194,7 +194,7 @@ func (h *ZabbixDSHandler) TestConnection(c *gin.Context) {
|
||||
|
||||
token := client.GetAuthToken()
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Data: map[string]interface{}{
|
||||
"success": true,
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
package handlers
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/internal/models"
|
||||
"ezops/model"
|
||||
"ezops/pkg/zabbix"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -53,7 +53,7 @@ type ZabbixConnectReq struct {
|
||||
func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
var req ZabbixConnectReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
defer cancel()
|
||||
|
||||
if err := client.Auth(ctx); err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
})
|
||||
if err != nil {
|
||||
client.Logout(ctx)
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "获取 Zabbix 主机失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取 Zabbix 主机失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
|
||||
client.Logout(ctx)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
@@ -197,7 +197,7 @@ func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
func (h *ZabbixProxyHandler) GetHostDetail(c *gin.Context) {
|
||||
hostID := c.Param("hostid")
|
||||
if hostID == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "缺少主机ID"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少主机ID"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func (h *ZabbixProxyHandler) GetHostDetail(c *gin.Context) {
|
||||
username := c.Query("username")
|
||||
password := c.Query("password")
|
||||
if url == "" || username == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "缺少 Zabbix 连接信息"})
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少 Zabbix 连接信息"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func (h *ZabbixProxyHandler) GetHostDetail(c *gin.Context) {
|
||||
defer cancel()
|
||||
|
||||
if err := client.Auth(ctx); err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer client.Logout(ctx)
|
||||
@@ -223,21 +223,21 @@ func (h *ZabbixProxyHandler) GetHostDetail(c *gin.Context) {
|
||||
// 获取主机详情
|
||||
host, err := client.GetHostByID(ctx, hostID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "获取主机失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取主机失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有监控项
|
||||
items, err := client.GetItemsByHost(ctx, hostID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, models.Response{Code: 502, Message: "获取监控项失败: " + err.Error()})
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取监控项失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取问题触发器
|
||||
triggers, _ := client.GetTriggersByHost(ctx, hostID, true)
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
@@ -0,0 +1,182 @@
|
||||
// Package dto 定义请求(Request)与响应(Response)传输对象
|
||||
// 与 models 分离:models 是 GORM 表结构,dto 是 API 契约
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// ━━━ Server ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// CreateServerRequest 创建服务器
|
||||
type CreateServerRequest struct {
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
IP string `json:"ip" binding:"required"`
|
||||
PublicIP string `json:"public_ip"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
SSHKey string `json:"ssh_key"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OS string `json:"os"`
|
||||
OSVersion string `json:"os_version"`
|
||||
Arch string `json:"arch"`
|
||||
CPU int `json:"cpu"`
|
||||
Memory int64 `json:"memory"`
|
||||
Disk int64 `json:"disk"`
|
||||
RegionID *uint `json:"region_id"`
|
||||
CloudID *uint `json:"cloud_id"`
|
||||
Tags string `json:"tags"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateServerRequest 更新服务器 — 部分更新 (使用 map)
|
||||
type UpdateServerRequest struct {
|
||||
Hostname *string `json:"hostname"`
|
||||
IP *string `json:"ip"`
|
||||
PublicIP *string `json:"public_ip"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
SSHKey *string `json:"ssh_key"`
|
||||
AuthType *string `json:"auth_type"`
|
||||
OS *string `json:"os"`
|
||||
OSVersion *string `json:"os_version"`
|
||||
Arch *string `json:"arch"`
|
||||
CPU *int `json:"cpu"`
|
||||
Memory *int64 `json:"memory"`
|
||||
Disk *int64 `json:"disk"`
|
||||
Status *string `json:"status"`
|
||||
CPUUsage *float64 `json:"cpu_usage"`
|
||||
MemoryUsage *float64 `json:"memory_usage"`
|
||||
DiskUsage *float64 `json:"disk_usage"`
|
||||
RegionID *uint `json:"region_id"`
|
||||
CloudID *uint `json:"cloud_id"`
|
||||
Tags *string `json:"tags"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
// ServerListItem 服务器列表项 (精简)
|
||||
type ServerListItem struct {
|
||||
ID uint `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
IP string `json:"ip"`
|
||||
Status string `json:"status"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
DiskUsage float64 `json:"disk_usage"`
|
||||
AgentStatus string `json:"agent_status"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
HasAuth bool `json:"has_auth"`
|
||||
OS string `json:"os"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ━━━ Credential ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// CreateCredentialRequest 创建凭据
|
||||
type CreateCredentialRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
SecretType string `json:"secret_type" binding:"required"` // ssh, docker, tls, password, env
|
||||
Description string `json:"description"`
|
||||
|
||||
// SSH
|
||||
SSHPrivateKey string `json:"ssh_private_key"`
|
||||
SSHPublicKey string `json:"ssh_public_key"`
|
||||
SSHUser string `json:"ssh_user"`
|
||||
SSHPort int `json:"ssh_port"`
|
||||
SSHPassword string `json:"ssh_password"`
|
||||
|
||||
// Docker
|
||||
DockerServer string `json:"docker_server"`
|
||||
DockerUsername string `json:"docker_username"`
|
||||
DockerPassword string `json:"docker_password"`
|
||||
|
||||
// TLS
|
||||
TLSCert string `json:"tls_cert"`
|
||||
TLSKey string `json:"tls_key"`
|
||||
TLSCA string `json:"tls_ca"`
|
||||
|
||||
// Password / Generic
|
||||
PlainPassword string `json:"plain_password"`
|
||||
|
||||
// Env
|
||||
EnvVars string `json:"env_vars"`
|
||||
}
|
||||
|
||||
// UpdateCredentialRequest 更新凭据
|
||||
type UpdateCredentialRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
|
||||
SSHPrivateKey *string `json:"ssh_private_key"`
|
||||
SSHPublicKey *string `json:"ssh_public_key"`
|
||||
SSHUser *string `json:"ssh_user"`
|
||||
SSHPort *int `json:"ssh_port"`
|
||||
SSHPassword *string `json:"ssh_password"`
|
||||
|
||||
DockerServer *string `json:"docker_server"`
|
||||
DockerUsername *string `json:"docker_username"`
|
||||
DockerPassword *string `json:"docker_password"`
|
||||
|
||||
TLSCert *string `json:"tls_cert"`
|
||||
TLSKey *string `json:"tls_key"`
|
||||
TLSCA *string `json:"tls_ca"`
|
||||
|
||||
PlainPassword *string `json:"plain_password"`
|
||||
EnvVars *string `json:"env_vars"`
|
||||
}
|
||||
|
||||
// ━━━ Alert ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
// CreateAlertContactRequest 创建告警联系人
|
||||
type CreateAlertContactRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ChannelType string `json:"channel_type" binding:"required"` // feishu, dingtalk, wechat, webhook
|
||||
WebhookURL string `json:"webhook_url" binding:"required"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// UpdateAlertContactRequest 更新告警联系人
|
||||
type UpdateAlertContactRequest struct {
|
||||
Name *string `json:"name"`
|
||||
ChannelType *string `json:"channel_type"`
|
||||
WebhookURL *string `json:"webhook_url"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
|
||||
// CreateAlertContactGroupRequest 创建告警联系组
|
||||
type CreateAlertContactGroupRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ContactIDs []uint `json:"contact_ids"`
|
||||
}
|
||||
|
||||
// UpdateAlertContactGroupRequest 更新告警联系组
|
||||
type UpdateAlertContactGroupRequest struct {
|
||||
Name *string `json:"name"`
|
||||
ContactIDs *[]uint `json:"contact_ids"`
|
||||
}
|
||||
|
||||
// CreateAlertRuleRequest 创建告警规则
|
||||
type CreateAlertRuleRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Metric string `json:"metric" binding:"required"`
|
||||
Condition string `json:"condition" binding:"required"` // >, <, >=, <=, ==
|
||||
Threshold float64 `json:"threshold" binding:"required"`
|
||||
Duration string `json:"duration"` // 30s, 1m, 5m
|
||||
ContactIDs []uint `json:"contact_ids"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateAlertRuleRequest 更新告警规则
|
||||
type UpdateAlertRuleRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Metric *string `json:"metric"`
|
||||
Condition *string `json:"condition"`
|
||||
Threshold *float64 `json:"threshold"`
|
||||
Duration *string `json:"duration"`
|
||||
ContactIDs *[]uint `json:"contact_ids"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
var frontendFS embed.FS
|
||||
|
||||
// setupFrontend 配置前端静态文件服务(go:embed 模式) // Logan.Li
|
||||
// 前端构建产物放在 backend/cmd/static/ 目录,编译时嵌入二进制
|
||||
// 前端构建产物放在 backend/static/ 目录,编译时嵌入二进制
|
||||
func setupFrontend(r *gin.Engine) {
|
||||
// 获取 static 子目录的 fs.FS
|
||||
staticFS, err := fs.Sub(frontendFS, "static")
|
||||
@@ -45,6 +45,7 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
@@ -53,6 +54,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/sftp v1.13.10 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
|
||||
@@ -95,6 +95,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -118,6 +120,8 @@ github.com/nalgeon/redka v0.2.0 h1:9Hjp5aHPHPLE44T1m41LKzx+b/MquMXt8ggw69eVu6U=
|
||||
github.com/nalgeon/redka v0.2.0/go.mod h1:vLxjY3XS9IwBID2YEFWeeMiN4Ar/DtKd4JW62JTAxuU=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -151,6 +155,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TerminalSessionHandler 终端会话审计处理器 // Logan.Li
|
||||
type TerminalSessionHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTerminalSessionHandler 创建终端会话审计处理器
|
||||
func NewTerminalSessionHandler(db *gorm.DB) *TerminalSessionHandler {
|
||||
return &TerminalSessionHandler{db: db}
|
||||
}
|
||||
|
||||
// ListTerminalSessions 终端会话列表
|
||||
// GET /api/v1/terminal-sessions?server_id=&username=&status=&page=&page_size=
|
||||
func (h *TerminalSessionHandler) ListTerminalSessions(c *gin.Context) {
|
||||
var req models.TerminalSessionQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := h.db.Model(&models.TerminalSession{})
|
||||
if req.ServerID > 0 {
|
||||
query = query.Where("server_id = ?", req.ServerID)
|
||||
}
|
||||
if req.Username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.Status != "" {
|
||||
query = query.Where("status = ?", req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
var sessions []models.TerminalSession
|
||||
if err := query.Order("id DESC").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&sessions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{
|
||||
List: sessions,
|
||||
Total: total,
|
||||
}})
|
||||
}
|
||||
|
||||
// GetTerminalSession 终端会话详情
|
||||
// GET /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) GetTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: session})
|
||||
}
|
||||
|
||||
// GetTerminalRecord 终端会话录制数据(用于回放)
|
||||
// GET /api/v1/terminal-sessions/:id/record
|
||||
func (h *TerminalSessionHandler) GetTerminalRecord(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.TerminalSession
|
||||
if err := h.db.First(&session, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "会话不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Data: session.RecordData})
|
||||
}
|
||||
|
||||
// DeleteTerminalSession 删除终端会话记录
|
||||
// DELETE /api/v1/terminal-sessions/:id
|
||||
func (h *TerminalSessionHandler) DeleteTerminalSession(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的会话ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&models.TerminalSession{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
|
||||
}
|
||||
|
||||
// BatchDeleteTerminalSessions 批量删除终端会话记录
|
||||
// POST /api/v1/terminal-sessions/batch-delete
|
||||
func (h *TerminalSessionHandler) BatchDeleteTerminalSessions(c *gin.Context) {
|
||||
var req models.IDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.TerminalSession{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功", Data: gin.H{"deleted": result.RowsAffected}})
|
||||
}
|
||||
Loaded 100 of 314 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user