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:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10035 -4050

No files matched your search

+1
View File
@@ -62,3 +62,4 @@ backend/cmd/static/
# kubectl 二进制(预打包进镜像,避免 Docker build 时网络下载)
backend/kubectl
opsxsql/
+3 -3
View File
@@ -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
+2 -4
View File
@@ -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 |
---
+15 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-29
View File
@@ -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
View File
@@ -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: "端口策略已保存"})
}
@@ -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,
@@ -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 {
@@ -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: "删除成功"})
}
@@ -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},
+260
View File
@@ -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: "删除成功",
})
@@ -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,
+130
View File
@@ -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"
@@ -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,
@@ -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(&current)
} 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,
@@ -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,
@@ -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: "删除成功",
})
@@ -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,
@@ -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: "删除成功"})
}
@@ -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(&regions)
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(&region).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(&region, 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(&region).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(&region)
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(&regions)
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(&region).Error == nil {
regionID = &region.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})
}
@@ -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: "已删除"})
}
@@ -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)})
}
@@ -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: "已授权"})
}
@@ -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"
@@ -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})
}
// ============================================================================
@@ -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)})
}
// ============================================================================
@@ -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{
@@ -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: "已删除"})
}
// ============================================================================
@@ -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})
}
// ============================================================================
@@ -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}})
}
+70
View File
@@ -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: "删除成功",
})
@@ -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: "已取消"})
}
@@ -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(&current, 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(&reg, *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
}
}
@@ -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,
@@ -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(),
@@ -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})
}
// ============================================
+227
View File
@@ -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)
@@ -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,
@@ -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: "服务器已彻底删除",
})
+187
View File
@@ -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)})
}
+70
View File
@@ -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,
}})
}
+324
View File
@@ -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,
+249
View File
@@ -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
+427
View File
@@ -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)
}
}
@@ -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,4 +1,4 @@
package handlers
package controller
import (
"encoding/json"
@@ -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,
@@ -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{
+182
View File
@@ -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"`
}
+1 -1
View File
@@ -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")
+2
View File
@@ -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
+5
View File
@@ -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