init: EZOps AIOps 智能运维平台

- 前端: React 18 + TypeScript + Vite + Tailwind CSS + shadcn/ui
- 后端: Go + Gin + GORM + JWT (golang-jwt/jwt/v5)
- 功能: 服务器管理、K8s集群、监控告警、CI/CD、自动化运维、审批流程、审计日志
- 文档: 设计文档(ROADMAP/PDD/架构/模块)
This commit is contained in:
attacker committed 2026-06-13 23:09:47 +08:00
commit 7d6674bddd
156 files changed
+45704

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Build output
/build/
/dist/
/bin/
/ezops
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Environment variables
.env
.env.local
# Config files (may contain sensitive data)
config.yaml
config.yml
config.local.yaml
config.local.yml
# Log files
/logs/
*.log
# Upload files
/uploads/
# Database files
*.db
*.sqlite
*.sqlite3
# Coverage
coverage.out
coverage.html
# Temporary files
/tmp/
/temp/
# Air live reload
/tmp/
.air.toml
# Swagger docs (auto-generated)
/docs/swagger.json
/docs/swagger.yaml
# SSL certificates
/ssl/
*.pem
*.key
*.crt
+66
View File
@@ -0,0 +1,66 @@
# 构建阶段
FROM golang:1.21-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装构建依赖
RUN apk add --no-cache git ca-certificates tzdata
# 设置时区
ENV TZ=Asia/Shanghai
# 复制 go.mod 和 go.sum
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s \
-X main.Version=$(git describe --tags --always --dirty 2>/dev/null || echo 'dev') \
-X main.BuildTime=$(date +%Y-%m-%d_%H:%M:%S) \
-X main.GitCommit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" \
-o ezops cmd/main.go
# 运行阶段
FROM alpine:latest
# 安装运行时依赖
RUN apk --no-cache add ca-certificates tzdata curl
# 设置时区
ENV TZ=Asia/Shanghai
# 创建非 root 用户
RUN addgroup -g 1000 ezops && \
adduser -u 1000 -G ezops -s /bin/sh -D ezops
# 设置工作目录
WORKDIR /app
# 复制构建产物
COPY --from=builder /app/ezops .
# 复制配置文件(可选,可以通过挂载覆盖)
COPY --from=builder /app/config.yaml.example ./config.yaml
# 创建日志目录
RUN mkdir -p /app/logs && chown -R ezops:ezops /app
# 切换到非 root 用户
USER ezops
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动应用
CMD ["./ezops"]
+136
View File
@@ -0,0 +1,136 @@
.PHONY: all build run test clean docker swagger lint fmt vet
# 应用名称
APP_NAME=ezops
MAIN_FILE=cmd/main.go
BUILD_DIR=build
# Go 参数
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
# 版本信息
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date +%Y-%m-%d_%H:%M:%S)
GIT_COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
all: test build
# 构建应用
build:
@echo "Building $(APP_NAME)..."
@mkdir -p $(BUILD_DIR)
$(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE)
@echo "Build complete: $(BUILD_DIR)/$(APP_NAME)"
# 运行应用
run:
$(GOCMD) run $(MAIN_FILE)
# 开发模式运行(带热重载)
dev:
@which air > /dev/null || go install github.com/cosmtrek/air@latest
air
# 运行测试
test:
$(GOTEST) -v ./...
# 运行测试并生成覆盖率报告
test-coverage:
$(GOTEST) -v -coverprofile=coverage.out ./...
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
# 清理构建产物
clean:
$(GOCLEAN)
@rm -rf $(BUILD_DIR)
@rm -f coverage.out coverage.html
@echo "Clean complete"
# 下载依赖
deps:
$(GOMOD) download
$(GOMOD) tidy
# 更新依赖
update-deps:
$(GOGET) -u ./...
$(GOMOD) tidy
# 生成 Swagger 文档
swagger:
@which swag > /dev/null || go install github.com/swaggo/swag/cmd/swag@latest
swag init -g $(MAIN_FILE)
@echo "Swagger docs generated"
# 代码格式化
fmt:
$(GOCMD) fmt ./...
# 代码检查
vet:
$(GOCMD) vet ./...
# 运行 linter
lint:
@which golangci-lint > /dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run
# 安全检查
security:
@which gosec > /dev/null || go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
# 构建 Docker 镜像
docker-build:
docker build -t $(APP_NAME):$(VERSION) -t $(APP_NAME):latest .
# 运行 Docker 容器
docker-run:
docker run -p 8080:8080 -v $(PWD)/config.yaml:/root/config.yaml $(APP_NAME):latest
# 停止 Docker 容器
docker-stop:
docker stop $(shell docker ps -q --filter ancestor=$(APP_NAME))
# 数据库迁移
migrate:
$(GOCMD) run $(MAIN_FILE) migrate
# 初始化数据库
init-db:
@echo "Initializing database..."
@mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS ezops CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null || true
@echo "Database initialized"
# 生成 Mock 数据
mock:
$(GOCMD) run scripts/mock.go
# 查看帮助
help:
@echo "Available targets:"
@echo " make build - Build the application"
@echo " make run - Run the application"
@echo " make dev - Run with hot reload"
@echo " make test - Run tests"
@echo " make test-coverage - Run tests with coverage"
@echo " make clean - Clean build artifacts"
@echo " make deps - Download dependencies"
@echo " make update-deps - Update dependencies"
@echo " make swagger - Generate Swagger docs"
@echo " make fmt - Format code"
@echo " make vet - Run go vet"
@echo " make lint - Run linter"
@echo " make security - Run security check"
@echo " make docker-build - Build Docker image"
@echo " make docker-run - Run Docker container"
@echo " make init-db - Initialize database"
@echo " make help - Show this help"
+304
View File
@@ -0,0 +1,304 @@
# EZOps 后端服务
EZOps 是一个专业的 AIOps 智能运维平台后端服务,基于 Go + Gin 框架开发。
## 功能特性
### 核心模块
- **用户管理**: 用户认证、角色权限管理
- **资产管理**: 服务器资产、云账号、域名管理
- **Kubernetes 管理**: 集群、节点、Pod、Deployment、Service、Ingress 管理
- **监控告警**: 告警规则、告警事件、通知渠道
- **应用发布**: CI/CD 流水线、发布审批
- **自动化运维**: 定时任务、脚本管理、任务执行
- **审批流程**: 审批模板、审批流程管理
- **站内导航**: 导航分组、链接管理
- **审计日志**: 操作日志记录、查询、统计
## 技术栈
- **框架**: Gin Web Framework
- **ORM**: GORM
- **数据库**: MySQL / PostgreSQL
- **缓存**: Redis (可选)
- **认证**: JWT
- **日志**: Zap
- **配置**: Viper
- **文档**: Swagger
## 快速开始
### 环境要求
- Go 1.21+
- MySQL 8.0+ 或 PostgreSQL 14+
- Redis 6.0+ (可选)
### 安装依赖
```bash
cd backend
go mod download
```
### 配置
1. 复制配置文件示例:
```bash
cp config.yaml.example config.yaml
```
2. 编辑 `config.yaml`,根据实际情况修改数据库等配置
### 数据库初始化
```bash
# 创建数据库
mysql -u root -p -e "CREATE DATABASE ezops CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 或者使用 PostgreSQL
# createdb -U postgres ezops
```
### 运行
```bash
# 开发模式
go run cmd/main.go
# 或者编译后运行
go build -o ezops cmd/main.go
./ezops
```
服务启动后,访问 http://localhost:8080 查看 API 文档。
## API 文档
启动服务后,可以通过以下地址访问 Swagger API 文档:
- Swagger UI: http://localhost:8080/swagger/index.html
- API 规范: http://localhost:8080/swagger/doc.json
## 项目结构
```
backend/
├── cmd/
│ └── main.go # 应用入口
├── internal/
│ ├── api/
│ │ ├── handlers/ # HTTP 处理器
│ │ ├── middleware/ # 中间件
│ │ └── routes/ # 路由注册
│ ├── config/ # 配置管理
│ └── models/ # 数据模型
├── pkg/
│ ├── database/ # 数据库连接
│ └── logger/ # 日志工具
├── config.yaml.example # 配置文件示例
├── go.mod # Go 模块定义
├── go.sum # Go 模块校验
└── README.md # 项目说明
```
## API 路由
### 认证相关
- `POST /api/v1/auth/login` - 用户登录
- `POST /api/v1/auth/register` - 用户注册
- `GET /api/v1/user/profile` - 获取用户信息
- `PUT /api/v1/user/profile` - 更新用户信息
- `PUT /api/v1/user/password` - 修改密码
### 用户管理 (管理员)
- `GET /api/v1/admin/users` - 用户列表
- `GET /api/v1/admin/users/:id` - 用户详情
- `POST /api/v1/admin/users` - 创建用户
- `PUT /api/v1/admin/users/:id` - 更新用户
- `DELETE /api/v1/admin/users/:id` - 删除用户
### 仪表盘
- `GET /api/v1/dashboard/stats` - 统计数据
- `GET /api/v1/dashboard/recent-alerts` - 最近告警
- `GET /api/v1/dashboard/recent-releases` - 最近发布
- `GET /api/v1/dashboard/metrics` - 指标数据
### 服务器管理
- `GET /api/v1/servers` - 服务器列表
- `GET /api/v1/servers/stats` - 服务器统计
- `GET /api/v1/servers/:id` - 服务器详情
- `POST /api/v1/servers` - 创建服务器
- `PUT /api/v1/servers/:id` - 更新服务器
- `DELETE /api/v1/servers/:id` - 删除服务器
- `POST /api/v1/servers/batch-delete` - 批量删除
### 云账号管理
- `GET /api/v1/cloud-accounts` - 云账号列表
- `POST /api/v1/cloud-accounts` - 创建云账号
- `DELETE /api/v1/cloud-accounts/:id` - 删除云账号
### 域名管理
- `GET /api/v1/domains` - 域名列表
- `POST /api/v1/domains` - 创建域名
- `DELETE /api/v1/domains/:id` - 删除域名
### Kubernetes 管理
- `GET /api/v1/k8s-clusters` - 集群列表
- `GET /api/v1/k8s-clusters/:id` - 集群详情
- `POST /api/v1/k8s-clusters` - 创建集群
- `PUT /api/v1/k8s-clusters/:id` - 更新集群
- `DELETE /api/v1/k8s-clusters/:id` - 删除集群
- `GET /api/v1/k8s-clusters/:id/nodes` - 节点列表
- `GET /api/v1/k8s-clusters/:id/pods` - Pod 列表
- `GET /api/v1/k8s-clusters/:id/deployments` - Deployment 列表
- `GET /api/v1/k8s-clusters/:id/services` - Service 列表
- `GET /api/v1/k8s-clusters/:id/ingresses` - Ingress 列表
### 监控告警
- `GET /api/v1/alerts` - 告警事件列表
- `GET /api/v1/alerts/rules` - 告警规则列表
- `POST /api/v1/alerts/rules` - 创建告警规则
- `PUT /api/v1/alerts/rules/:id` - 更新告警规则
- `DELETE /api/v1/alerts/rules/:id` - 删除告警规则
- `POST /api/v1/alerts/rules/:id/toggle` - 启用/禁用规则
- `POST /api/v1/alerts/:id/ack` - 确认告警
### 应用发布
- `GET /api/v1/pipelines` - 流水线列表
- `POST /api/v1/pipelines` - 创建流水线
- `GET /api/v1/pipelines/:id` - 流水线详情
- `PUT /api/v1/pipelines/:id` - 更新流水线
- `DELETE /api/v1/pipelines/:id` - 删除流水线
- `POST /api/v1/pipelines/:id/trigger` - 触发流水线
- `GET /api/v1/pipelines/:id/runs` - 运行记录
- `GET /api/v1/releases` - 发布列表
- `POST /api/v1/releases` - 创建发布
- `POST /api/v1/releases/:id/approve` - 审批发布
- `POST /api/v1/releases/:id/deploy` - 执行发布
- `POST /api/v1/releases/:id/rollback` - 回滚发布
### 自动化运维
- `GET /api/v1/cron-jobs` - 定时任务列表
- `POST /api/v1/cron-jobs` - 创建定时任务
- `GET /api/v1/cron-jobs/:id` - 定时任务详情
- `PUT /api/v1/cron-jobs/:id` - 更新定时任务
- `DELETE /api/v1/cron-jobs/:id` - 删除定时任务
- `POST /api/v1/cron-jobs/:id/toggle` - 启用/禁用任务
- `POST /api/v1/cron-jobs/:id/execute` - 立即执行
- `GET /api/v1/scripts` - 脚本列表
- `POST /api/v1/scripts` - 创建脚本
- `GET /api/v1/scripts/:id` - 脚本详情
- `PUT /api/v1/scripts/:id` - 更新脚本
- `DELETE /api/v1/scripts/:id` - 删除脚本
- `POST /api/v1/scripts/:id/execute` - 执行脚本
### 审批流程
- `GET /api/v1/approvals/templates` - 审批模板列表
- `POST /api/v1/approvals/templates` - 创建审批模板
- `GET /api/v1/approvals/templates/:id` - 模板详情
- `PUT /api/v1/approvals/templates/:id` - 更新模板
- `DELETE /api/v1/approvals/templates/:id` - 删除模板
- `GET /api/v1/approvals/processes` - 审批流程列表
- `POST /api/v1/approvals/processes` - 创建审批流程
- `GET /api/v1/approvals/processes/:id` - 流程详情
- `POST /api/v1/approvals/processes/:id/approve` - 审批通过
- `POST /api/v1/approvals/processes/:id/reject` - 审批驳回
### 站内导航
- `GET /api/v1/navigations/groups` - 导航分组列表
- `POST /api/v1/navigations/groups` - 创建分组
- `PUT /api/v1/navigations/groups/:id` - 更新分组
- `DELETE /api/v1/navigations/groups/:id` - 删除分组
- `GET /api/v1/navigations/links` - 导航链接列表
- `POST /api/v1/navigations/links` - 创建链接
- `PUT /api/v1/navigations/links/:id` - 更新链接
- `DELETE /api/v1/navigations/links/:id` - 删除链接
- `POST /api/v1/navigations/links/:id/favorite` - 收藏/取消收藏
### 审计日志
- `GET /api/v1/audit/logs` - 审计日志列表
- `GET /api/v1/audit/logs/:id` - 日志详情
- `GET /api/v1/audit/modules` - 模块列表
- `GET /api/v1/audit/actions` - 操作类型列表
- `GET /api/v1/audit/logs/export` - 导出日志
- `GET /api/v1/audit/statistics` - 统计信息
- `POST /api/v1/audit/logs/clear` - 清理日志
## 开发指南
### 添加新的 API
1. 在 `internal/models/` 下定义数据模型
2. 在 `internal/api/handlers/` 下创建处理器
3. 在 `internal/api/routes/routes.go` 中注册路由
### 生成 Swagger 文档
```bash
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/main.go
```
## 部署
### Docker 部署
```dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o ezops cmd/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/ezops .
COPY config.yaml .
EXPOSE 8080
CMD ["./ezops"]
```
构建并运行:
```bash
docker build -t ezops-backend .
docker run -p 8080:8080 -v $(pwd)/config.yaml:/root/config.yaml ezops-backend
```
### 生产环境建议
1. 使用反向代理(Nginx/Traefik)
2. 配置 HTTPS
3. 使用环境变量覆盖配置文件
4. 配置日志轮转
5. 启用数据库连接池
6. 配置监控告警
## 贡献指南
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/xxx`)
3. 提交更改 (`git commit -am 'Add xxx'`)
4. 推送到分支 (`git push origin feature/xxx`)
5. 创建 Pull Request
## 许可证
MIT License
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"ezops/internal/api/routes"
"ezops/internal/config"
"ezops/pkg/database"
"ezops/pkg/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// 版本信息(通过编译时 ldflags 注入)
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
// @title AIOps运维平台 API
// @version 1.0
// @description AIOps智能运维平台后端API文档
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
// 加载配置
cfg := config.Load()
// 初始化日志
log := logger.New(cfg.LogLevel)
defer log.Sync()
// 设置Gin模式
if cfg.Mode == "production" {
gin.SetMode(gin.ReleaseMode)
}
// 初始化数据库
db, err := database.New(cfg.Database)
if err != nil {
log.Fatal("failed to connect database", zap.Error(err))
}
// 自动迁移
if err := database.AutoMigrate(db); err != nil {
log.Fatal("failed to migrate database", zap.Error(err))
}
// 创建Gin实例
r := gin.New()
// 中间件
r.Use(gin.Recovery())
r.Use(logger.GinLogger(log))
r.Use(CORS())
// 版本信息 API
r.GET("/version", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"version": Version,
"build_time": BuildTime,
"git_commit": GitCommit,
})
})
// 注册路由
routes.Register(r, db, log, cfg)
// 创建HTTP服务器
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
// 优雅关闭
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("failed to start server", zap.Error(err))
}
}()
log.Info("server started", zap.String("port", cfg.Port))
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("server forced to shutdown", zap.Error(err))
}
log.Info("server exited")
}
// CORS 跨域中间件
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
+161
View File
@@ -0,0 +1,161 @@
# EZOps 配置文件示例
# 复制此文件为 config.yaml 并根据实际情况修改
# 应用配置
app:
name: "EZOps"
version: "1.0.0"
port: 8080
mode: "debug" # debug, release, test
# 数据库配置
database:
# 数据库类型: mysql, postgres
driver: "mysql"
# MySQL配置
mysql:
host: "localhost"
port: 3306
username: "ezops"
password: "your_password"
database: "ezops"
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 # 秒
# Redis配置
redis:
enabled: false
host: "localhost"
port: 6379
password: ""
database: 0
pool_size: 10
# JWT配置
jwt:
secret: "your_jwt_secret_key_change_this_in_production"
expire_hours: 24
refresh_hours: 168 # 7天
# 日志配置
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:
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 # 异步写入
+98
View File
@@ -0,0 +1,98 @@
version: '3.8'
services:
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: ezops-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ezops
MYSQL_USER: ezops
MYSQL_PASSWORD: ezops123
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存(可选)
redis:
image: redis:7-alpine
container_name: ezops-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# EZOps 后端服务
backend:
build:
context: .
dockerfile: Dockerfile
container_name: ezops-backend
restart: unless-stopped
ports:
- "8080:8080"
environment:
- APP_MODE=release
- DATABASE_DRIVER=mysql
- DATABASE_MYSQL_HOST=mysql
- DATABASE_MYSQL_PORT=3306
- DATABASE_MYSQL_USERNAME=ezops
- DATABASE_MYSQL_PASSWORD=ezops123
- DATABASE_MYSQL_DATABASE=ezops
- REDIS_ENABLED=true
- REDIS_HOST=redis
- REDIS_PORT=6379
volumes:
- ./config.yaml:/app/config.yaml
- ./logs:/app/logs
- ./uploads:/app/uploads
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Nginx 反向代理(可选)
nginx:
image: nginx:alpine
container_name: ezops-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- backend
volumes:
mysql_data:
redis_data:
networks:
default:
name: ezops-network
+60
View File
@@ -0,0 +1,60 @@
module ezops
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.18.0
gorm.io/driver/mysql v1.5.2
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
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/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
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/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+152
View File
@@ -0,0 +1,152 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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/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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/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=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/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=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+350
View File
@@ -0,0 +1,350 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AlertHandler 告警处理器
type AlertHandler struct {
db *gorm.DB
}
// NewAlertHandler 创建告警处理器
func NewAlertHandler(db *gorm.DB) *AlertHandler {
return &AlertHandler{db: db}
}
// ListAlerts 获取告警事件列表
// @Summary 获取告警事件列表
// @Description 获取所有告警事件列表
// @Tags 告警
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param status query string false "状态过滤"
// @Param severity query string false "级别过滤"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /alerts [get]
func (h *AlertHandler) ListAlerts(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.AlertEvent{}).Preload("Rule")
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
// 级别过滤
if severity := c.Query("severity"); severity != "" {
db = db.Where("severity = ?", severity)
}
var total int64
db.Count(&total)
var alerts []models.AlertEvent
db.Order("created_at DESC").
Offset(query.GetOffset()).
Limit(query.GetLimit()).
Find(&alerts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: alerts,
Total: total,
},
})
}
// ListRules 获取告警规则列表
// @Summary 获取告警规则列表
// @Description 获取所有告警规则列表
// @Tags 告警
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /alerts/rules [get]
func (h *AlertHandler) ListRules(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.AlertRule{}).Count(&total)
var rules []models.AlertRule
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&rules)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: rules,
Total: total,
},
})
}
// CreateRule 创建告警规则
// @Summary 创建告警规则
// @Description 创建新告警规则
// @Tags 告警
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateAlertRuleRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.AlertRule}
// @Router /alerts/rules [post]
func (h *AlertHandler) CreateRule(c *gin.Context) {
var req models.CreateAlertRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
rule := models.AlertRule{
Name: req.Name,
Description: req.Description,
Type: req.Type,
Source: req.Source,
Expression: req.Expression,
Severity: req.Severity,
Duration: req.Duration,
Labels: req.Labels,
Annotations: req.Annotations,
NotifyChannels: toJSON(req.NotifyChannels),
Enabled: true,
}
if err := h.db.Create(&rule).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: rule,
})
}
// UpdateRule 更新告警规则
// @Summary 更新告警规则
// @Description 更新指定告警规则
// @Tags 告警
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "规则ID"
// @Param request body models.UpdateAlertRuleRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.AlertRule}
// @Router /alerts/rules/{id} [put]
func (h *AlertHandler) UpdateRule(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateAlertRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var rule models.AlertRule
if err := h.db.First(&rule, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.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.Expression != "" {
updates["expression"] = req.Expression
}
if req.Severity != "" {
updates["severity"] = req.Severity
}
if req.Duration > 0 {
updates["duration"] = req.Duration
}
if req.Labels != "" {
updates["labels"] = req.Labels
}
if req.Annotations != "" {
updates["annotations"] = req.Annotations
}
if len(req.NotifyChannels) > 0 {
updates["notify_channels"] = req.NotifyChannels
}
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if err := h.db.Model(&rule).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&rule, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: rule,
})
}
// DeleteRule 删除告警规则
// @Summary 删除告警规则
// @Description 删除指定告警规则
// @Tags 告警
// @Produce json
// @Security Bearer
// @Param id path int true "规则ID"
// @Success 200 {object} models.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{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ToggleRule 切换告警规则状态
// @Summary 切换告警规则状态
// @Description 启用或禁用告警规则
// @Tags 告警
// @Produce json
// @Security Bearer
// @Param id path int true "规则ID"
// @Success 200 {object} models.Response{data=models.AlertRule}
// @Router /alerts/rules/{id}/toggle [post]
func (h *AlertHandler) ToggleRule(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var rule models.AlertRule
if err := h.db.First(&rule, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "规则不存在",
})
return
}
rule.Enabled = !rule.Enabled
if err := h.db.Save(&rule).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "操作失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "操作成功",
Data: rule,
})
}
// AcknowledgeAlert 确认告警
// @Summary 确认告警
// @Description 确认告警事件
// @Tags 告警
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "告警ID"
// @Param request body models.AcknowledgeAlertRequest true "确认请求"
// @Success 200 {object} models.Response{data=models.AlertEvent}
// @Router /alerts/{id}/ack [post]
func (h *AlertHandler) AcknowledgeAlert(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.AcknowledgeAlertRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
var alert models.AlertEvent
if err := h.db.First(&alert, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "告警不存在",
})
return
}
alert.Acknowledged = true
// alert.AcknowledgedBy = middleware.GetUserID(c)
// now := time.Now()
// alert.AcknowledgedAt = &now
if err := h.db.Save(&alert).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "确认失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "确认成功",
Data: alert,
})
}
// toJSON serializes a value to JSON string. Returns empty string on error.
func toJSON(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
return ""
}
return string(b)
}
+435
View File
@@ -0,0 +1,435 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ApprovalHandler 审批处理器
type ApprovalHandler struct {
db *gorm.DB
}
// NewApprovalHandler 创建审批处理器
func NewApprovalHandler(db *gorm.DB) *ApprovalHandler {
return &ApprovalHandler{db: db}
}
// ListTemplates 获取审批模板列表
// @Summary 获取审批模板列表
// @Description 获取所有审批模板列表
// @Tags 审批流程
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /approvals/templates [get]
func (h *ApprovalHandler) ListTemplates(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.ApprovalTemplate{}).Count(&total)
var templates []models.ApprovalTemplate
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&templates)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: templates,
Total: total,
},
})
}
// GetTemplate 获取审批模板详情
// @Summary 获取审批模板详情
// @Description 获取指定审批模板的详细信息
// @Tags 审批流程
// @Produce json
// @Security Bearer
// @Param id path int true "模板ID"
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
// @Router /approvals/templates/{id} [get]
func (h *ApprovalHandler) GetTemplate(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var template models.ApprovalTemplate
if err := h.db.First(&template, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "模板不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: template,
})
}
// CreateTemplate 创建审批模板
// @Summary 创建审批模板
// @Description 创建新审批模板
// @Tags 审批流程
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateApprovalTemplateRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
// @Router /approvals/templates [post]
func (h *ApprovalHandler) CreateTemplate(c *gin.Context) {
var req models.CreateApprovalTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
template := models.ApprovalTemplate{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Type: req.Type,
FormSchema: req.FormSchema,
FlowConfig: req.FlowConfig,
Enabled: true,
}
if err := h.db.Create(&template).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: template,
})
}
// UpdateTemplate 更新审批模板
// @Summary 更新审批模板
// @Description 更新指定审批模板
// @Tags 审批流程
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "模板ID"
// @Param request body models.CreateApprovalTemplateRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.ApprovalTemplate}
// @Router /approvals/templates/{id} [put]
func (h *ApprovalHandler) UpdateTemplate(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateApprovalTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var template models.ApprovalTemplate
if err := h.db.First(&template, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "模板不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"type": req.Type,
"form_schema": req.FormSchema,
"flow_config": req.FlowConfig,
}
if err := h.db.Model(&template).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&template, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: template,
})
}
// DeleteTemplate 删除审批模板
// @Summary 删除审批模板
// @Description 删除指定审批模板
// @Tags 审批流程
// @Produce json
// @Security Bearer
// @Param id path int true "模板ID"
// @Success 200 {object} models.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{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ListProcesses 获取审批流程列表
// @Summary 获取审批流程列表
// @Description 获取所有审批流程列表
// @Tags 审批流程
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param status query string false "状态过滤"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /approvals/processes [get]
func (h *ApprovalHandler) ListProcesses(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.ApprovalProcess{}).Preload("Template").Preload("ApplicantUser")
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
var total int64
db.Count(&total)
var processes []models.ApprovalProcess
db.Order("created_at DESC").
Offset(query.GetOffset()).
Limit(query.GetLimit()).
Find(&processes)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: processes,
Total: total,
},
})
}
// GetProcess 获取审批流程详情
// @Summary 获取审批流程详情
// @Description 获取指定审批流程的详细信息
// @Tags 审批流程
// @Produce json
// @Security Bearer
// @Param id path int true "流程ID"
// @Success 200 {object} models.Response{data=models.ApprovalProcess}
// @Router /approvals/processes/{id} [get]
func (h *ApprovalHandler) GetProcess(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var process models.ApprovalProcess
if err := h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流程不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: process,
})
}
// CreateProcess 创建审批流程
// @Summary 创建审批流程
// @Description 创建新审批流程
// @Tags 审批流程
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateApprovalProcessRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.ApprovalProcess}
// @Router /approvals/processes [post]
func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
var req models.CreateApprovalProcessRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 获取模板
var template models.ApprovalTemplate
if err := h.db.First(&template, req.TemplateID).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "模板不存在",
})
return
}
process := models.ApprovalProcess{
TemplateID: req.TemplateID,
Title: req.Title,
FormData: req.FormData,
Status: "pending",
CurrentStep: 0,
TotalSteps: 1, // 简化处理
}
if err := h.db.Create(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
h.db.Preload("Template").First(&process, process.ID)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: process,
})
}
// ApproveProcess 审批通过
// @Summary 审批通过
// @Description 审批通过指定流程
// @Tags 审批流程
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "流程ID"
// @Param request body models.ApproveRequest true "审批请求"
// @Success 200 {object} models.Response{data=models.ApprovalProcess}
// @Router /approvals/processes/{id}/approve [post]
func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.ApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
var process models.ApprovalProcess
if err := h.db.First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流程不存在",
})
return
}
process.Status = "approved"
// now := time.Now()
// process.ApprovedAt = &now
if err := h.db.Save(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "审批失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "审批通过",
Data: process,
})
}
// RejectProcess 审批拒绝
// @Summary 审批拒绝
// @Description 审批拒绝指定流程
// @Tags 审批流程
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "流程ID"
// @Param request body models.ApproveRequest true "审批请求"
// @Success 200 {object} models.Response{data=models.ApprovalProcess}
// @Router /approvals/processes/{id}/reject [post]
func (h *ApprovalHandler) RejectProcess(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.ApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
var process models.ApprovalProcess
if err := h.db.First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流程不存在",
})
return
}
process.Status = "rejected"
process.RejectReason = req.Comment
// now := time.Now()
// process.RejectedAt = &now
if err := h.db.Save(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "拒绝失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "已拒绝",
Data: process,
})
}
+362
View File
@@ -0,0 +1,362 @@
package handlers
import (
"net/http"
"strconv"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AuditHandler 审计日志处理器
type AuditHandler struct {
db *gorm.DB
}
// NewAuditHandler 创建审计日志处理器
func NewAuditHandler(db *gorm.DB) *AuditHandler {
return &AuditHandler{db: db}
}
// ListAuditLogs 获取审计日志列表
// @Summary 获取审计日志列表
// @Description 获取审计日志列表,支持分页和筛选
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页数量" default(20)
// @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 "结束时间"
// @Success 200 {object} models.AuditLogListResponse
// @Router /api/v1/audit/logs [get]
func (h *AuditHandler) ListAuditLogs(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
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
// 构建查询
query := h.db.Model(&models.AuditLog{})
if req.Module != "" {
query = query.Where("module = ?", req.Module)
}
if req.Action != "" {
query = query.Where("action = ?", req.Action)
}
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
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, models.Response{Code: 500, Message: "查询失败"})
return
}
// 获取列表
var logs []models.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: "查询失败"})
return
}
c.JSON(http.StatusOK, models.AuditLogListResponse{
List: logs,
Total: total,
})
}
// GetAuditLogDetail 获取审计日志详情
// @Summary 获取审计日志详情
// @Description 获取指定ID的审计日志详情
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param id path int true "日志ID"
// @Success 200 {object} models.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"})
return
}
var log models.AuditLog
if err := h.db.First(&log, 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, log)
}
// GetAuditModules 获取审计模块列表
// @Summary 获取审计模块列表
// @Description 获取所有审计模块类型
// @Tags 审计日志
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/modules [get]
func (h *AuditHandler) GetAuditModules(c *gin.Context) {
modules := []map[string]string{
{"value": "user", "label": "用户管理"},
{"value": "server", "label": "服务器管理"},
{"value": "k8s", "label": "Kubernetes"},
{"value": "alert", "label": "监控告警"},
{"value": "pipeline", "label": "应用发布"},
{"value": "automation", "label": "自动化运维"},
{"value": "approval", "label": "审批流程"},
{"value": "navigation", "label": "站内导航"},
{"value": "cloud_account", "label": "云账号管理"},
{"value": "domain", "label": "域名管理"},
}
c.JSON(http.StatusOK, gin.H{"data": modules})
}
// GetAuditActions 获取审计操作类型列表
// @Summary 获取审计操作类型列表
// @Description 获取所有审计操作类型
// @Tags 审计日志
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/actions [get]
func (h *AuditHandler) GetAuditActions(c *gin.Context) {
actions := []map[string]string{
{"value": "create", "label": "创建"},
{"value": "update", "label": "更新"},
{"value": "delete", "label": "删除"},
{"value": "view", "label": "查看"},
{"value": "login", "label": "登录"},
{"value": "logout", "label": "登出"},
{"value": "execute", "label": "执行"},
{"value": "approve", "label": "审批"},
{"value": "reject", "label": "驳回"},
{"value": "import", "label": "导入"},
{"value": "export", "label": "导出"},
}
c.JSON(http.StatusOK, gin.H{"data": actions})
}
// 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{})
if req.Module != "" {
query = query.Where("module = ?", req.Module)
}
if req.Action != "" {
query = query.Where("action = ?", req.Action)
}
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
// 获取数据
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: "导出失败"})
return
}
// TODO: 实现CSV/Excel导出功能
// 这里返回JSON格式的数据,实际项目中应该生成CSV或Excel文件
c.JSON(http.StatusOK, gin.H{
"message": "导出成功",
"count": len(logs),
"data": logs,
})
}
// GetAuditStatistics 获取审计统计
// @Summary 获取审计统计
// @Description 获取审计日志统计信息
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param days query int false "统计天数" default(7)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/statistics [get]
func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "7"))
if days <= 0 {
days = 7
}
startTime := time.Now().AddDate(0, 0, -days)
// 统计各模块的操作数量
var moduleStats []struct {
Module string `json:"module"`
Count int64 `json:"count"`
}
h.db.Model(&models.AuditLog{}).
Select("module, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("module").
Scan(&moduleStats)
// 统计各操作类型的数量
var actionStats []struct {
Action string `json:"action"`
Count int64 `json:"count"`
}
h.db.Model(&models.AuditLog{}).
Select("action, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("action").
Scan(&actionStats)
// 统计每日操作数量
var dailyStats []struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
h.db.Model(&models.AuditLog{}).
Select("DATE(created_at) as date, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("DATE(created_at)").
Order("date").
Scan(&dailyStats)
// 总记录数
var totalCount int64
h.db.Model(&models.AuditLog{}).Count(&totalCount)
// 今日记录数
today := time.Now().Format("2006-01-02")
var todayCount int64
h.db.Model(&models.AuditLog{}).
Where("DATE(created_at) = ?", today).
Count(&todayCount)
c.JSON(http.StatusOK, gin.H{
"total_count": totalCount,
"today_count": todayCount,
"module_stats": moduleStats,
"action_stats": actionStats,
"daily_stats": dailyStats,
})
}
// ClearAuditLogs 清理审计日志
// @Summary 清理审计日志
// @Description 清理指定日期之前的审计日志
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param request body map[string]string true "清理请求"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/logs/clear [post]
func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
var req struct {
BeforeDate string `json:"before_date" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.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: "无效的日期格式"})
return
}
// 删除指定日期之前的日志
result := h.db.Where("created_at < ?", beforeDate).Delete(&models.AuditLog{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "清理失败"})
return
}
// 记录审计日志
userID, _ := c.Get("userID")
username, _ := c.Get("username")
h.RecordAuditLog(userID.(uint), username.(string), "audit", "clear", "清理审计日志", gin.H{
"before_date": req.BeforeDate,
"deleted": result.RowsAffected,
})
c.JSON(http.StatusOK, gin.H{
"message": "清理成功",
"deleted": result.RowsAffected,
})
}
// RecordAuditLog 记录审计日志
func (h *AuditHandler) RecordAuditLog(userID uint, username, module, action, description string, details interface{}) {
uid := userID
log := models.AuditLog{
UserID: &uid,
Username: username,
Module: module,
Action: action,
Resource: description,
}
h.db.Create(&log)
}
+553
View File
@@ -0,0 +1,553 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AutomationHandler 自动化运维处理器
type AutomationHandler struct {
db *gorm.DB
}
// NewAutomationHandler 创建自动化运维处理器
func NewAutomationHandler(db *gorm.DB) *AutomationHandler {
return &AutomationHandler{db: db}
}
// ListCronJobs 获取定时任务列表
// @Summary 获取定时任务列表
// @Description 获取所有定时任务列表
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /cron-jobs [get]
func (h *AutomationHandler) ListCronJobs(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.CronJob{}).Count(&total)
var jobs []models.CronJob
h.db.Preload("Script").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&jobs)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: jobs,
Total: total,
},
})
}
// GetCronJob 获取定时任务详情
// @Summary 获取定时任务详情
// @Description 获取指定定时任务的详细信息
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id} [get]
func (h *AutomationHandler) GetCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.Preload("Script").First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: job,
})
}
// CreateCronJob 创建定时任务
// @Summary 创建定时任务
// @Description 创建新定时任务
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateCronJobRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs [post]
func (h *AutomationHandler) CreateCronJob(c *gin.Context) {
var req models.CreateCronJobRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
job := models.CronJob{
Name: req.Name,
Description: req.Description,
CronExpr: req.CronExpr,
Type: req.Type,
Content: req.Content,
ScriptID: req.ScriptID,
TargetType: req.TargetType,
Timeout: req.Timeout,
NotifyOn: req.NotifyOn,
Enabled: true,
}
if job.Timeout == 0 {
job.Timeout = 300
}
if job.NotifyOn == "" {
job.NotifyOn = "failure"
}
if err := h.db.Create(&job).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: job,
})
}
// UpdateCronJob 更新定时任务
// @Summary 更新定时任务
// @Description 更新指定定时任务
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Param request body models.CreateCronJobRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id} [put]
func (h *AutomationHandler) UpdateCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateCronJobRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"cron_expr": req.CronExpr,
"type": req.Type,
"content": req.Content,
"script_id": req.ScriptID,
"target_type": req.TargetType,
"timeout": req.Timeout,
"notify_on": req.NotifyOn,
}
if err := h.db.Model(&job).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Script").First(&job, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: job,
})
}
// DeleteCronJob 删除定时任务
// @Summary 删除定时任务
// @Description 删除指定定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response
// @Router /cron-jobs/{id} [delete]
func (h *AutomationHandler) DeleteCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.CronJob{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ToggleCronJob 切换定时任务状态
// @Summary 切换定时任务状态
// @Description 启用或禁用定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.CronJob}
// @Router /cron-jobs/{id}/toggle [post]
func (h *AutomationHandler) ToggleCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
job.Enabled = !job.Enabled
if err := h.db.Save(&job).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "操作失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "操作成功",
Data: job,
})
}
// ExecuteCronJob 执行定时任务
// @Summary 执行定时任务
// @Description 手动执行定时任务
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "任务ID"
// @Success 200 {object} models.Response{data=models.TaskExecution}
// @Router /cron-jobs/{id}/execute [post]
func (h *AutomationHandler) ExecuteCronJob(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var job models.CronJob
if err := h.db.First(&job, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "定时任务不存在",
})
return
}
execution := models.TaskExecution{
TaskType: "cron",
TaskID: uint(id),
TaskName: job.Name,
Status: "pending",
TargetType: job.TargetType,
Targets: job.Targets,
}
if err := h.db.Create(&execution).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建执行记录失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "执行中",
Data: execution,
})
}
// ListScripts 获取脚本列表
// @Summary 获取脚本列表
// @Description 获取所有脚本列表
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /scripts [get]
func (h *AutomationHandler) ListScripts(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.Script{}).Count(&total)
var scripts []models.Script
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&scripts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: scripts,
Total: total,
},
})
}
// GetScript 获取脚本详情
// @Summary 获取脚本详情
// @Description 获取指定脚本的详细信息
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts/{id} [get]
func (h *AutomationHandler) GetScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: script,
})
}
// CreateScript 创建脚本
// @Summary 创建脚本
// @Description 创建新脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateScriptRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts [post]
func (h *AutomationHandler) CreateScript(c *gin.Context) {
var req models.CreateScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
script := models.Script{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Type: req.Type,
Content: req.Content,
Params: req.Params,
Timeout: req.Timeout,
}
if script.Type == "" {
script.Type = "shell"
}
if script.Timeout == 0 {
script.Timeout = 300
}
if err := h.db.Create(&script).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: script,
})
}
// UpdateScript 更新脚本
// @Summary 更新脚本
// @Description 更新指定脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Param request body models.CreateScriptRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Script}
// @Router /scripts/{id} [put]
func (h *AutomationHandler) UpdateScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"type": req.Type,
"content": req.Content,
"params": req.Params,
"timeout": req.Timeout,
"version": script.Version + 1,
}
if err := h.db.Model(&script).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&script, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: script,
})
}
// DeleteScript 删除脚本
// @Summary 删除脚本
// @Description 删除指定脚本
// @Tags 自动化运维
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Success 200 {object} models.Response
// @Router /scripts/{id} [delete]
func (h *AutomationHandler) DeleteScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Script{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ExecuteScript 执行脚本
// @Summary 执行脚本
// @Description 在目标服务器上执行脚本
// @Tags 自动化运维
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "脚本ID"
// @Param request body models.ExecuteScriptRequest true "执行请求"
// @Success 200 {object} models.Response{data=models.TaskExecution}
// @Router /scripts/{id}/execute [post]
func (h *AutomationHandler) ExecuteScript(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var script models.Script
if err := h.db.First(&script, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "脚本不存在",
})
return
}
var req models.ExecuteScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
execution := models.TaskExecution{
TaskType: "script",
TaskID: uint(id),
TaskName: script.Name,
Status: "pending",
TargetType: req.TargetType,
}
if err := h.db.Create(&execution).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建执行记录失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "执行中",
Data: execution,
})
}
+279
View File
@@ -0,0 +1,279 @@
package handlers
import (
"math"
"net/http"
"time"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DashboardHandler 仪表盘处理器
type DashboardHandler struct {
db *gorm.DB
}
// NewDashboardHandler 创建仪表盘处理器
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
return &DashboardHandler{db: db}
}
// GetStats 获取仪表盘统计
// @Summary 获取仪表盘统计
// @Description 获取仪表盘所有统计数据
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.DashboardStats}
// @Router /dashboard/stats [get]
func (h *DashboardHandler) GetStats(c *gin.Context) {
var stats models.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 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)
serverStats.AvgCPU = math.Round(avgCPU*100) / 100
serverStats.AvgMemory = math.Round(avgMemory*100) / 100
serverStats.AvgDisk = math.Round(avgDisk*100) / 100
stats.ServerStats = serverStats
// K8s集群统计
var k8sStats models.K8sClusterStats
h.db.Model(&models.K8sCluster{}).Count(&k8sStats.Total)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "healthy").Count(&k8sStats.Healthy)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "warning").Count(&k8sStats.Warning)
h.db.Model(&models.K8sCluster{}).Where("status = ?", "error").Count(&k8sStats.Error)
var totalNodes, totalPods, totalServices int64
h.db.Model(&models.K8sNode{}).Count(&totalNodes)
h.db.Model(&models.K8sPod{}).Count(&totalPods)
h.db.Model(&models.K8sService{}).Count(&totalServices)
k8sStats.TotalNodes = totalNodes
k8sStats.TotalPods = totalPods
k8sStats.TotalServices = totalServices
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)
// 今日告警
today := time.Now().Format("2006-01-02")
h.db.Model(&models.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)
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)
if pipelineStats.Total > 0 {
pipelineStats.SuccessRate = math.Round(float64(pipelineStats.Success)/float64(pipelineStats.Total)*10000) / 100
}
stats.PipelineStats = pipelineStats
// 成本统计
costStats := models.CostStats{
TotalCost: 450000,
MonthCost: 107700,
CloudCosts: []models.CloudCost{
{Cloud: "阿里云", Cost: 28500},
{Cloud: "AWS", Cost: 42000},
{Cloud: "腾讯云", Cost: 15800},
{Cloud: "华为云", Cost: 8900},
{Cloud: "Azure", Cost: 12500},
},
MonthTrend: []models.MonthTrend{
{Month: "1月", Cost: 85000},
{Month: "2月", Cost: 92000},
{Month: "3月", Cost: 88000},
{Month: "4月", Cost: 95000},
{Month: "5月", Cost: 107700},
},
}
stats.CostStats = costStats
// Top资源使用
var topCPU, topMemory, topDisk []models.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
for _, s := range topCPU {
topResourceUsage.CPU = append(topResourceUsage.CPU, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.CPUUsage,
})
}
for _, s := range topMemory {
topResourceUsage.Memory = append(topResourceUsage.Memory, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.MemoryUsage,
})
}
for _, s := range topDisk {
topResourceUsage.Disk = append(topResourceUsage.Disk, models.ServerResource{
ID: s.ID,
Hostname: s.Hostname,
IP: s.IP,
Usage: s.DiskUsage,
})
}
stats.TopResourceUsage = topResourceUsage
// 即将过期的证书
var domains []models.Domain
h.db.Where("ssl_enabled = ?", true).Find(&domains)
var expiringCerts []models.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{
ID: d.ID,
Name: d.Name,
ExpireDate: d.SSLExpireDate.Format("2006-01-02"),
DaysUntilExpiry: days,
})
}
}
}
stats.ExpiringCerts = expiringCerts
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: stats,
})
}
// GetRecentAlerts 获取最近告警
// @Summary 获取最近告警
// @Description 获取最近触发的告警事件
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} models.Response{data=[]models.AlertEvent}
// @Router /dashboard/recent-alerts [get]
func (h *DashboardHandler) GetRecentAlerts(c *gin.Context) {
limit := 10
if l := c.Query("limit"); l != "" {
if parsed, err := time.Parse("2006-01-02", l); err == nil {
_ = parsed
}
}
var alerts []models.AlertEvent
h.db.Preload("Rule").Where("status = ?", "firing").
Order("created_at DESC").
Limit(limit).
Find(&alerts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: alerts,
})
}
// GetRecentReleases 获取最近发布
// @Summary 获取最近发布
// @Description 获取最近的应用发布记录
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param limit query int false "数量限制"
// @Success 200 {object} models.Response{data=[]models.AppRelease}
// @Router /dashboard/recent-releases [get]
func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
limit := 5
var releases []models.AppRelease
h.db.Order("created_at DESC").
Limit(limit).
Find(&releases)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: releases,
})
}
// GetMetrics 获取监控指标
// @Summary 获取监控指标
// @Description 获取系统监控指标数据
// @Tags 仪表盘
// @Produce json
// @Security Bearer
// @Param hours query int false "小时数"
// @Success 200 {object} models.Response{data=[]models.MetricData}
// @Router /dashboard/metrics [get]
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
hours := 24
// 生成模拟数据
var metrics []models.MetricData
now := time.Now()
for i := 0; i < hours; i++ {
timestamp := now.Add(-time.Duration(hours-i) * time.Hour)
metrics = append(metrics, models.MetricData{
Name: "cpu",
Value: 40 + float64(i%30),
Timestamp: timestamp,
})
metrics = append(metrics, models.MetricData{
Name: "memory",
Value: 50 + float64(i%20),
Timestamp: timestamp,
})
metrics = append(metrics, models.MetricData{
Name: "disk",
Value: 45 + float64(i%10),
Timestamp: timestamp,
})
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: metrics,
})
}
+345
View File
@@ -0,0 +1,345 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// K8sHandler K8s处理器
type K8sHandler struct {
db *gorm.DB
}
// NewK8sHandler 创建K8s处理器
func NewK8sHandler(db *gorm.DB) *K8sHandler {
return &K8sHandler{db: db}
}
// ListClusters 获取集群列表
// @Summary 获取集群列表
// @Description 获取所有K8s集群列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /k8s-clusters [get]
func (h *K8sHandler) ListClusters(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.K8sCluster{}).Count(&total)
var clusters []models.K8sCluster
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&clusters)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: clusters,
Total: total,
},
})
}
// GetCluster 获取集群详情
// @Summary 获取集群详情
// @Description 获取指定K8s集群的详细信息
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Success 200 {object} models.Response{data=models.K8sCluster}
// @Router /k8s-clusters/{id} [get]
func (h *K8sHandler) GetCluster(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var cluster models.K8sCluster
if err := h.db.First(&cluster, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "集群不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: cluster,
})
}
// CreateCluster 创建集群
// @Summary 创建集群
// @Description 创建新K8s集群
// @Tags K8s集群
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateK8sClusterRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.K8sCluster}
// @Router /k8s-clusters [post]
func (h *K8sHandler) CreateCluster(c *gin.Context) {
var req models.CreateK8sClusterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
cluster := models.K8sCluster{
Name: req.Name,
Code: req.Code,
Version: req.Version,
APIEndpoint: req.APIEndpoint,
KubeConfig: req.KubeConfig,
Description: req.Description,
Status: "healthy",
}
if err := h.db.Create(&cluster).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: cluster,
})
}
// UpdateCluster 更新集群
// @Summary 更新集群
// @Description 更新指定K8s集群信息
// @Tags K8s集群
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param request body models.CreateK8sClusterRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.K8sCluster}
// @Router /k8s-clusters/{id} [put]
func (h *K8sHandler) UpdateCluster(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreateK8sClusterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var cluster models.K8sCluster
if err := h.db.First(&cluster, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "集群不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"version": req.Version,
"api_endpoint": req.APIEndpoint,
"kube_config": req.KubeConfig,
"description": req.Description,
}
if err := h.db.Model(&cluster).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&cluster, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: cluster,
})
}
// DeleteCluster 删除集群
// @Summary 删除集群
// @Description 删除指定K8s集群
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Success 200 {object} models.Response
// @Router /k8s-clusters/{id} [delete]
func (h *K8sHandler) DeleteCluster(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.K8sCluster{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ListNodes 获取节点列表
// @Summary 获取节点列表
// @Description 获取指定集群的节点列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Success 200 {object} models.Response{data=[]models.K8sNode}
// @Router /k8s-clusters/{id}/nodes [get]
func (h *K8sHandler) ListNodes(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
var nodes []models.K8sNode
h.db.Where("cluster_id = ?", clusterID).Find(&nodes)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: nodes,
})
}
// ListPods 获取Pod列表
// @Summary 获取Pod列表
// @Description 获取指定集群的Pod列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param namespace query string false "命名空间"
// @Success 200 {object} models.Response{data=[]models.K8sPod}
// @Router /k8s-clusters/{id}/pods [get]
func (h *K8sHandler) ListPods(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
namespace := c.Query("namespace")
db := h.db.Where("cluster_id = ?", clusterID)
if namespace != "" {
db = db.Where("namespace = ?", namespace)
}
var pods []models.K8sPod
db.Find(&pods)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: pods,
})
}
// ListDeployments 获取Deployment列表
// @Summary 获取Deployment列表
// @Description 获取指定集群的Deployment列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param namespace query string false "命名空间"
// @Success 200 {object} models.Response{data=[]models.K8sDeployment}
// @Router /k8s-clusters/{id}/deployments [get]
func (h *K8sHandler) ListDeployments(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
namespace := c.Query("namespace")
db := h.db.Where("cluster_id = ?", clusterID)
if namespace != "" {
db = db.Where("namespace = ?", namespace)
}
var deployments []models.K8sDeployment
db.Find(&deployments)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: deployments,
})
}
// ListServices 获取Service列表
// @Summary 获取Service列表
// @Description 获取指定集群的Service列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param namespace query string false "命名空间"
// @Success 200 {object} models.Response{data=[]models.K8sService}
// @Router /k8s-clusters/{id}/services [get]
func (h *K8sHandler) ListServices(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
namespace := c.Query("namespace")
db := h.db.Where("cluster_id = ?", clusterID)
if namespace != "" {
db = db.Where("namespace = ?", namespace)
}
var services []models.K8sService
db.Find(&services)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: services,
})
}
// ListIngresses 获取Ingress列表
// @Summary 获取Ingress列表
// @Description 获取指定集群的Ingress列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param namespace query string false "命名空间"
// @Success 200 {object} models.Response{data=[]models.K8sIngress}
// @Router /k8s-clusters/{id}/ingresses [get]
func (h *K8sHandler) ListIngresses(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
namespace := c.Query("namespace")
db := h.db.Where("cluster_id = ?", clusterID)
if namespace != "" {
db = db.Where("namespace = ?", namespace)
}
var ingresses []models.K8sIngress
db.Find(&ingresses)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: ingresses,
})
}
+428
View File
@@ -0,0 +1,428 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// NavigationHandler 导航处理器
type NavigationHandler struct {
db *gorm.DB
}
// NewNavigationHandler 创建导航处理器
func NewNavigationHandler(db *gorm.DB) *NavigationHandler {
return &NavigationHandler{db: db}
}
// ListGroups 获取导航分组列表
// @Summary 获取导航分组列表
// @Description 获取所有导航分组列表
// @Tags 站内导航
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=[]models.NavigationGroup}
// @Router /navigations/groups [get]
func (h *NavigationHandler) ListGroups(c *gin.Context) {
var groups []models.NavigationGroup
h.db.Order("sort ASC, created_at ASC").Find(&groups)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: groups,
})
}
// CreateGroup 创建导航分组
// @Summary 创建导航分组
// @Description 创建新导航分组
// @Tags 站内导航
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateNavigationGroupRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.NavigationGroup}
// @Router /navigations/groups [post]
func (h *NavigationHandler) CreateGroup(c *gin.Context) {
var req models.CreateNavigationGroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
group := models.NavigationGroup{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Icon: req.Icon,
Sort: req.Sort,
Enabled: true,
}
if err := h.db.Create(&group).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: group,
})
}
// UpdateGroup 更新导航分组
// @Summary 更新导航分组
// @Description 更新指定导航分组
// @Tags 站内导航
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "分组ID"
// @Param request body models.UpdateNavigationGroupRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.NavigationGroup}
// @Router /navigations/groups/{id} [put]
func (h *NavigationHandler) UpdateGroup(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateNavigationGroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var group models.NavigationGroup
if err := h.db.First(&group, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.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.Icon != "" {
updates["icon"] = req.Icon
}
updates["sort"] = req.Sort
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if err := h.db.Model(&group).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&group, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: group,
})
}
// DeleteGroup 删除导航分组
// @Summary 删除导航分组
// @Description 删除指定导航分组
// @Tags 站内导航
// @Produce json
// @Security Bearer
// @Param id path int true "分组ID"
// @Success 200 {object} models.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{
Code: 500,
Message: "删除链接失败",
})
return
}
if err := h.db.Delete(&models.NavigationGroup{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ListLinks 获取导航链接列表
// @Summary 获取导航链接列表
// @Description 获取所有导航链接列表
// @Tags 站内导航
// @Produce json
// @Security Bearer
// @Param group_id query int false "分组ID"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=[]models.NavigationLink}
// @Router /navigations/links [get]
func (h *NavigationHandler) ListLinks(c *gin.Context) {
db := h.db.Model(&models.NavigationLink{}).Preload("Group")
// 分组过滤
if groupID := c.Query("group_id"); groupID != "" {
db = db.Where("group_id = ?", groupID)
}
// 关键词搜索
if keyword := c.Query("keyword"); keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
var links []models.NavigationLink
db.Order("sort ASC, created_at ASC").Find(&links)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: links,
})
}
// CreateLink 创建导航链接
// @Summary 创建导航链接
// @Description 创建新导航链接
// @Tags 站内导航
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateNavigationLinkRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.NavigationLink}
// @Router /navigations/links [post]
func (h *NavigationHandler) CreateLink(c *gin.Context) {
var req models.CreateNavigationLinkRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
tags, _ := json.Marshal(req.Tags)
link := models.NavigationLink{
GroupID: req.GroupID,
Name: req.Name,
URL: req.URL,
Description: req.Description,
Icon: req.Icon,
Tags: string(tags),
Sort: req.Sort,
Enabled: true,
}
if err := h.db.Create(&link).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
// 更新分组链接数
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.Preload("Group").First(&link, link.ID)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: link,
})
}
// UpdateLink 更新导航链接
// @Summary 更新导航链接
// @Description 更新指定导航链接
// @Tags 站内导航
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "链接ID"
// @Param request body models.UpdateNavigationLinkRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.NavigationLink}
// @Router /navigations/links/{id} [put]
func (h *NavigationHandler) UpdateLink(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateNavigationLinkRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var link models.NavigationLink
if err := h.db.First(&link, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "链接不存在",
})
return
}
updates := map[string]interface{}{}
if req.GroupID > 0 {
updates["group_id"] = req.GroupID
}
if req.Name != "" {
updates["name"] = req.Name
}
if req.URL != "" {
updates["url"] = req.URL
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Icon != "" {
updates["icon"] = req.Icon
}
if len(req.Tags) > 0 {
tags, _ := json.Marshal(req.Tags)
updates["tags"] = string(tags)
}
updates["sort"] = req.Sort
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if err := h.db.Model(&link).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Group").First(&link, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: link,
})
}
// DeleteLink 删除导航链接
// @Summary 删除导航链接
// @Description 删除指定导航链接
// @Tags 站内导航
// @Produce json
// @Security Bearer
// @Param id path int true "链接ID"
// @Success 200 {object} models.Response
// @Router /navigations/links/{id} [delete]
func (h *NavigationHandler) DeleteLink(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var link models.NavigationLink
h.db.First(&link, id)
if err := h.db.Delete(&models.NavigationLink{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
// 更新分组链接数
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(*)"))
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// ToggleFavorite 切换收藏状态
// @Summary 切换收藏状态
// @Description 切换导航链接的收藏状态
// @Tags 站内导航
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "链接ID"
// @Param request body models.ToggleFavoriteRequest true "收藏请求"
// @Success 200 {object} models.Response{data=models.NavigationLink}
// @Router /navigations/links/{id}/favorite [post]
func (h *NavigationHandler) ToggleFavorite(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.ToggleFavoriteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
var link models.NavigationLink
if err := h.db.First(&link, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "链接不存在",
})
return
}
link.IsFavorite = req.IsFavorite
if err := h.db.Save(&link).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "操作失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "操作成功",
Data: link,
})
}
+504
View File
@@ -0,0 +1,504 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PipelineHandler 流水线处理器
type PipelineHandler struct {
db *gorm.DB
}
// NewPipelineHandler 创建流水线处理器
func NewPipelineHandler(db *gorm.DB) *PipelineHandler {
return &PipelineHandler{db: db}
}
// ListPipelines 获取流水线列表
// @Summary 获取流水线列表
// @Description 获取所有流水线列表
// @Tags 流水线
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /pipelines [get]
func (h *PipelineHandler) ListPipelines(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.Pipeline{}).Count(&total)
var pipelines []models.Pipeline
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&pipelines)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: pipelines,
Total: total,
},
})
}
// GetPipeline 获取流水线详情
// @Summary 获取流水线详情
// @Description 获取指定流水线的详细信息
// @Tags 流水线
// @Produce json
// @Security Bearer
// @Param id path int true "流水线ID"
// @Success 200 {object} models.Response{data=models.Pipeline}
// @Router /pipelines/{id} [get]
func (h *PipelineHandler) GetPipeline(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var pipeline models.Pipeline
if err := h.db.First(&pipeline, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流水线不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: pipeline,
})
}
// CreatePipeline 创建流水线
// @Summary 创建流水线
// @Description 创建新流水线
// @Tags 流水线
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreatePipelineRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.Pipeline}
// @Router /pipelines [post]
func (h *PipelineHandler) CreatePipeline(c *gin.Context) {
var req models.CreatePipelineRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
pipeline := models.Pipeline{
Name: req.Name,
Code: req.Code,
Description: req.Description,
Type: req.Type,
Source: req.Source,
Branch: req.Branch,
Dockerfile: req.Dockerfile,
Stages: req.Stages,
Variables: req.Variables,
Enabled: true,
}
if err := h.db.Create(&pipeline).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: pipeline,
})
}
// UpdatePipeline 更新流水线
// @Summary 更新流水线
// @Description 更新指定流水线
// @Tags 流水线
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "流水线ID"
// @Param request body models.CreatePipelineRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Pipeline}
// @Router /pipelines/{id} [put]
func (h *PipelineHandler) UpdatePipeline(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.CreatePipelineRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var pipeline models.Pipeline
if err := h.db.First(&pipeline, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流水线不存在",
})
return
}
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"type": req.Type,
"source": req.Source,
"branch": req.Branch,
"dockerfile": req.Dockerfile,
"stages": req.Stages,
"variables": req.Variables,
}
if err := h.db.Model(&pipeline).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.First(&pipeline, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: pipeline,
})
}
// DeletePipeline 删除流水线
// @Summary 删除流水线
// @Description 删除指定流水线
// @Tags 流水线
// @Produce json
// @Security Bearer
// @Param id path int true "流水线ID"
// @Success 200 {object} models.Response
// @Router /pipelines/{id} [delete]
func (h *PipelineHandler) DeletePipeline(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Pipeline{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// TriggerPipeline 触发流水线
// @Summary 触发流水线
// @Description 手动触发流水线执行
// @Tags 流水线
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "流水线ID"
// @Param request body models.TriggerPipelineRequest true "触发请求"
// @Success 200 {object} models.Response{data=models.PipelineRun}
// @Router /pipelines/{id}/trigger [post]
func (h *PipelineHandler) TriggerPipeline(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var pipeline models.Pipeline
if err := h.db.First(&pipeline, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "流水线不存在",
})
return
}
var req models.TriggerPipelineRequest
c.ShouldBindJSON(&req)
// 获取最新构建号
var maxBuildNumber int
h.db.Model(&models.PipelineRun{}).Where("pipeline_id = ?", id).Select("COALESCE(MAX(build_number), 0)").Scan(&maxBuildNumber)
run := models.PipelineRun{
PipelineID: uint(id),
BuildNumber: maxBuildNumber + 1,
Status: "pending",
Branch: req.Branch,
TriggeredBy: "manual",
}
if req.Branch == "" {
run.Branch = pipeline.Branch
}
if err := h.db.Create(&run).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "触发失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "触发成功",
Data: run,
})
}
// ListRuns 获取运行记录列表
// @Summary 获取运行记录列表
// @Description 获取指定流水线的运行记录
// @Tags 流水线
// @Produce json
// @Security Bearer
// @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}
// @Router /pipelines/{id}/runs [get]
func (h *PipelineHandler) ListRuns(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.PipelineRun{}).Where("pipeline_id = ?", id)
var total int64
db.Count(&total)
var runs []models.PipelineRun
db.Order("created_at DESC").
Offset(query.GetOffset()).
Limit(query.GetLimit()).
Find(&runs)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: runs,
Total: total,
},
})
}
// ListReleases 获取发布列表
// @Summary 获取发布列表
// @Description 获取所有应用发布列表
// @Tags 应用发布
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /releases [get]
func (h *PipelineHandler) ListReleases(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
var total int64
h.db.Model(&models.AppRelease{}).Count(&total)
var releases []models.AppRelease
h.db.Order("created_at DESC").
Offset(query.GetOffset()).
Limit(query.GetLimit()).
Find(&releases)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: releases,
Total: total,
},
})
}
// CreateRelease 创建发布
// @Summary 创建发布
// @Description 创建新应用发布
// @Tags 应用发布
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateAppReleaseRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.AppRelease}
// @Router /releases [post]
func (h *PipelineHandler) CreateRelease(c *gin.Context) {
var req models.CreateAppReleaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
release := models.AppRelease{
AppName: req.AppName,
Version: req.Version,
Environment: req.Environment,
Status: "pending",
CommitID: req.CommitID,
CommitMsg: req.CommitMsg,
Servers: "[]",
Description: req.Description,
}
if err := h.db.Create(&release).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: release,
})
}
// ApproveRelease 审批发布
// @Summary 审批发布
// @Description 审批应用发布
// @Tags 应用发布
// @Produce json
// @Security Bearer
// @Param id path int true "发布ID"
// @Success 200 {object} models.Response{data=models.AppRelease}
// @Router /releases/{id}/approve [post]
func (h *PipelineHandler) ApproveRelease(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var release models.AppRelease
if err := h.db.First(&release, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "发布不存在",
})
return
}
release.Status = "approved"
// release.Approver = middleware.GetUsername(c)
// now := time.Now()
// release.ApprovedAt = &now
if err := h.db.Save(&release).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "审批失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "审批成功",
Data: release,
})
}
// DeployRelease 部署发布
// @Summary 部署发布
// @Description 部署应用发布
// @Tags 应用发布
// @Produce json
// @Security Bearer
// @Param id path int true "发布ID"
// @Success 200 {object} models.Response{data=models.AppRelease}
// @Router /releases/{id}/deploy [post]
func (h *PipelineHandler) DeployRelease(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var release models.AppRelease
if err := h.db.First(&release, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "发布不存在",
})
return
}
release.Status = "deploying"
if err := h.db.Save(&release).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "部署失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "部署中",
Data: release,
})
}
// RollbackRelease 回滚发布
// @Summary 回滚发布
// @Description 回滚应用发布
// @Tags 应用发布
// @Produce json
// @Security Bearer
// @Param id path int true "发布ID"
// @Success 200 {object} models.Response{data=models.AppRelease}
// @Router /releases/{id}/rollback [post]
func (h *PipelineHandler) RollbackRelease(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var release models.AppRelease
if err := h.db.First(&release, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "发布不存在",
})
return
}
release.Status = "rolled_back"
// now := time.Now()
// release.RollbackAt = &now
if err := h.db.Save(&release).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "回滚失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "回滚成功",
Data: release,
})
}
+566
View File
@@ -0,0 +1,566 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ServerHandler 服务器处理器
type ServerHandler struct {
db *gorm.DB
}
// NewServerHandler 创建服务器处理器
func NewServerHandler(db *gorm.DB) *ServerHandler {
return &ServerHandler{db: db}
}
// ListServers 获取服务器列表
// @Summary 获取服务器列表
// @Description 获取所有服务器列表
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @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}
// @Router /servers [get]
func (h *ServerHandler) ListServers(c *gin.Context) {
var query models.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
db := h.db.Model(&models.Server{})
// 关键词搜索
if query.Keyword != "" {
db = db.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
// 区域过滤
if regionID := c.Query("region_id"); regionID != "" {
db = db.Where("region_id = ?", regionID)
}
var total int64
db.Count(&total)
var servers []models.Server
db.Preload("Region").Preload("Cloud").
Offset(query.GetOffset()).Limit(query.GetLimit()).
Order("created_at DESC").
Find(&servers)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: servers,
Total: total,
},
})
}
// GetServer 获取服务器详情
// @Summary 获取服务器详情
// @Description 获取指定服务器的详细信息
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} models.Response{data=models.Server}
// @Router /servers/{id} [get]
func (h *ServerHandler) GetServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var server models.Server
if err := h.db.Preload("Region").Preload("Cloud").First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: server,
})
}
// CreateServer 创建服务器
// @Summary 创建服务器
// @Description 创建新服务器
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateServerRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.Server}
// @Router /servers [post]
func (h *ServerHandler) CreateServer(c *gin.Context) {
var req models.CreateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
server := models.Server{
Hostname: req.Hostname,
IP: req.IP,
PublicIP: req.PublicIP,
Port: req.Port,
Username: req.Username,
Password: req.Password,
SSHKey: req.SSHKey,
AuthType: req.AuthType,
OS: req.OS,
CPU: req.CPU,
Memory: req.Memory,
Disk: req.Disk,
RegionID: req.RegionID,
CloudID: req.CloudID,
Tags: req.Tags,
Description: req.Description,
Status: "stopped",
}
if server.Port == 0 {
server.Port = 22
}
if server.Username == "" {
server.Username = "root"
}
if server.AuthType == "" {
server.AuthType = "password"
}
if err := h.db.Create(&server).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建服务器失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: server,
})
}
// UpdateServer 更新服务器
// @Summary 更新服务器
// @Description 更新指定服务器信息
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Param request body models.UpdateServerRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.Server}
// @Router /servers/{id} [put]
func (h *ServerHandler) UpdateServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateServerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var server models.Server
if err := h.db.First(&server, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "服务器不存在",
})
return
}
updates := map[string]interface{}{}
if req.Hostname != "" {
updates["hostname"] = req.Hostname
}
if req.PublicIP != "" {
updates["public_ip"] = req.PublicIP
}
if req.Port > 0 {
updates["port"] = req.Port
}
if req.Username != "" {
updates["username"] = req.Username
}
if req.Password != "" {
updates["password"] = req.Password
}
if req.SSHKey != "" {
updates["ssh_key"] = req.SSHKey
}
if req.AuthType != "" {
updates["auth_type"] = req.AuthType
}
if req.OS != "" {
updates["os"] = req.OS
}
if req.CPU > 0 {
updates["cpu"] = req.CPU
}
if req.Memory > 0 {
updates["memory"] = req.Memory
}
if req.Disk > 0 {
updates["disk"] = req.Disk
}
if req.Status != "" {
updates["status"] = req.Status
}
if req.RegionID != nil {
updates["region_id"] = *req.RegionID
}
if req.CloudID != nil {
updates["cloud_id"] = *req.CloudID
}
if req.Tags != "" {
updates["tags"] = req.Tags
}
if req.Description != "" {
updates["description"] = req.Description
}
if err := h.db.Model(&server).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
h.db.Preload("Region").Preload("Cloud").First(&server, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: server,
})
}
// DeleteServer 删除服务器
// @Summary 删除服务器
// @Description 删除指定服务器
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Param id path int true "服务器ID"
// @Success 200 {object} models.Response
// @Router /servers/{id} [delete]
func (h *ServerHandler) DeleteServer(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Server{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// BatchDeleteServers 批量删除服务器
// @Summary 批量删除服务器
// @Description 批量删除服务器
// @Tags 服务器
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.IDsRequest true "删除请求"
// @Success 200 {object} models.Response
// @Router /servers/batch-delete [post]
func (h *ServerHandler) BatchDeleteServers(c *gin.Context) {
var req models.IDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
if err := h.db.Delete(&models.Server{}, req.IDs).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// GetServerStats 获取服务器统计
// @Summary 获取服务器统计
// @Description 获取服务器统计数据
// @Tags 服务器
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.ServerStats}
// @Router /servers/stats [get]
func (h *ServerHandler) GetServerStats(c *gin.Context) {
var stats models.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)
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)
stats.AvgCPU = avgCPU
stats.AvgMemory = avgMemory
stats.AvgDisk = avgDisk
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: stats,
})
}
// CloudAccountHandler 云账号处理器
type CloudAccountHandler struct {
db *gorm.DB
}
// NewCloudAccountHandler 创建云账号处理器
func NewCloudAccountHandler(db *gorm.DB) *CloudAccountHandler {
return &CloudAccountHandler{db: db}
}
// ListCloudAccounts 获取云账号列表
// @Summary 获取云账号列表
// @Description 获取所有云账号列表
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /cloud-accounts [get]
func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) {
var accounts []models.CloudAccount
h.db.Find(&accounts)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: accounts,
Total: int64(len(accounts)),
},
})
}
// CreateCloudAccount 创建云账号
// @Summary 创建云账号
// @Description 创建新云账号
// @Tags 云账号
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CloudAccount true "创建请求"
// @Success 200 {object} models.Response{data=models.CloudAccount}
// @Router /cloud-accounts [post]
func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) {
var account models.CloudAccount
if err := c.ShouldBindJSON(&account); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if err := h.db.Create(&account).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: account,
})
}
// DeleteCloudAccount 删除云账号
// @Summary 删除云账号
// @Description 删除指定云账号
// @Tags 云账号
// @Produce json
// @Security Bearer
// @Param id path int true "云账号ID"
// @Success 200 {object} models.Response
// @Router /cloud-accounts/{id} [delete]
func (h *CloudAccountHandler) DeleteCloudAccount(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.CloudAccount{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
// DomainHandler 域名处理器
type DomainHandler struct {
db *gorm.DB
}
// NewDomainHandler 创建域名处理器
func NewDomainHandler(db *gorm.DB) *DomainHandler {
return &DomainHandler{db: db}
}
// ListDomains 获取域名列表
// @Summary 获取域名列表
// @Description 获取所有域名列表
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /domains [get]
func (h *DomainHandler) ListDomains(c *gin.Context) {
var query models.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.Domain{})
if query.Keyword != "" {
db = db.Where("name LIKE ?", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var domains []models.Domain
db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&domains)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: domains,
Total: total,
},
})
}
// CreateDomain 创建域名
// @Summary 创建域名
// @Description 创建新域名
// @Tags 域名
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.Domain true "创建请求"
// @Success 200 {object} models.Response{data=models.Domain}
// @Router /domains [post]
func (h *DomainHandler) CreateDomain(c *gin.Context) {
var domain models.Domain
if err := c.ShouldBindJSON(&domain); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
if err := h.db.Create(&domain).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: domain,
})
}
// DeleteDomain 删除域名
// @Summary 删除域名
// @Description 删除指定域名
// @Tags 域名
// @Produce json
// @Security Bearer
// @Param id path int true "域名ID"
// @Success 200 {object} models.Response
// @Router /domains/{id} [delete]
func (h *DomainHandler) DeleteDomain(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.Domain{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
+538
View File
@@ -0,0 +1,538 @@
package handlers
import (
"net/http"
"strconv"
"ezops/internal/api/middleware"
"ezops/internal/config"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// UserHandler 用户处理器
type UserHandler struct {
db *gorm.DB
cfg *config.Config
}
// NewUserHandler 创建用户处理器
func NewUserHandler(db *gorm.DB, cfg *config.Config) *UserHandler {
return &UserHandler{db: db, cfg: cfg}
}
// 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
// @Router /auth/login [post]
func (h *UserHandler) Login(c *gin.Context) {
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var user models.User
if err := h.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "用户名或密码错误",
})
return
}
if user.Status != 1 {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "账号已被禁用",
})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "用户名或密码错误",
})
return
}
// 生成JWT令牌
token, err := middleware.GenerateToken(&user, &h.cfg.JWT)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "生成令牌失败",
})
return
}
// 更新最后登录时间
now := h.db.NowFunc()
h.db.Model(&user).Update("last_login", &now)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "登录成功",
Data: models.LoginResponse{
Token: token,
ExpiresIn: h.cfg.JWT.ExpireHour * 3600,
User: &user,
},
})
}
// Register 用户注册
// @Summary 用户注册
// @Description 注册新用户
// @Tags 用户
// @Accept json
// @Produce json
// @Param request body models.CreateUserRequest true "注册请求"
// @Success 200 {object} models.Response{data=models.User}
// @Failure 400 {object} models.Response
// @Router /auth/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 检查用户名是否已存在
var existingUser models.User
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "用户名已存在",
})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "密码加密失败",
})
return
}
user := models.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
Status: 1,
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建用户失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "注册成功",
Data: user,
})
}
// GetProfile 获取当前用户信息
// @Summary 获取当前用户信息
// @Description 获取当前登录用户的信息
// @Tags 用户
// @Produce json
// @Security Bearer
// @Success 200 {object} models.Response{data=models.User}
// @Router /user/profile [get]
func (h *UserHandler) GetProfile(c *gin.Context) {
userID := middleware.GetUserID(c)
var user models.User
if err := h.db.Preload("Roles").First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: user,
})
}
// UpdateProfile 更新当前用户信息
// @Summary 更新当前用户信息
// @Description 更新当前登录用户的信息
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.UpdateUserRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /user/profile [put]
func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID := middleware.GetUserID(c)
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
updates := map[string]interface{}{}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if err := h.db.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
var user models.User
h.db.First(&user, userID)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: user,
})
}
// ChangePassword 修改密码
// @Summary 修改密码
// @Description 修改当前用户密码
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.ChangePasswordRequest true "修改密码请求"
// @Success 200 {object} models.Response
// @Router /user/password [put]
func (h *UserHandler) ChangePassword(c *gin.Context) {
userID := middleware.GetUserID(c)
var req models.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var user models.User
if err := h.db.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "原密码错误",
})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "密码加密失败",
})
return
}
if err := h.db.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "修改密码失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "密码修改成功",
})
}
// ListUsers 获取用户列表
// @Summary 获取用户列表
// @Description 获取所有用户列表(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param keyword query string false "搜索关键词"
// @Success 200 {object} models.Response{data=models.ListResponse}
// @Router /users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
var query models.PageQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误",
})
return
}
db := h.db.Model(&models.User{})
if query.Keyword != "" {
db = db.Where("username LIKE ? OR email LIKE ? OR real_name LIKE ?",
"%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
var total int64
db.Count(&total)
var users []models.User
db.Preload("Roles").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&users)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: models.ListResponse{
List: users,
Total: total,
},
})
}
// GetUser 获取用户详情
// @Summary 获取用户详情
// @Description 获取指定用户的详细信息
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var user models.User
if err := h.db.Preload("Roles").First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "success",
Data: user,
})
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建新用户(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body models.CreateUserRequest true "创建请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
// 检查用户名是否已存在
var existingUser models.User
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "用户名已存在",
})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "密码加密失败",
})
return
}
user := models.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
Phone: req.Phone,
RealName: req.RealName,
Status: 1,
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "创建用户失败",
})
return
}
// 关联角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Append(roles)
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "创建成功",
Data: user,
})
}
// UpdateUser 更新用户
// @Summary 更新用户
// @Description 更新指定用户信息(管理员)
// @Tags 用户
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Param request body models.UpdateUserRequest true "更新请求"
// @Success 200 {object} models.Response{data=models.User}
// @Router /users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{
Code: 400,
Message: "请求参数错误: " + err.Error(),
})
return
}
var user models.User
if err := h.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, models.Response{
Code: 404,
Message: "用户不存在",
})
return
}
updates := map[string]interface{}{}
if req.Email != "" {
updates["email"] = req.Email
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.RealName != "" {
updates["real_name"] = req.RealName
}
if req.Status != nil {
updates["status"] = *req.Status
}
if err := h.db.Model(&user).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "更新失败",
})
return
}
// 更新角色
if len(req.RoleIDs) > 0 {
var roles []models.Role
h.db.Find(&roles, req.RoleIDs)
h.db.Model(&user).Association("Roles").Replace(roles)
}
h.db.Preload("Roles").First(&user, id)
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "更新成功",
Data: user,
})
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Description 删除指定用户(管理员)
// @Tags 用户
// @Produce json
// @Security Bearer
// @Param id path int true "用户ID"
// @Success 200 {object} models.Response
// @Router /users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&models.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{
Code: 500,
Message: "删除失败",
})
return
}
c.JSON(http.StatusOK, models.Response{
Code: 200,
Message: "删除成功",
})
}
+133
View File
@@ -0,0 +1,133 @@
package middleware
import (
"net/http"
"strings"
"time"
"ezops/internal/config"
"ezops/internal/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// JWTClaims JWT声明
type JWTClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
IsAdmin bool `json:"is_admin"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT令牌
func GenerateToken(user *models.User, cfg *config.JWTConfig) (string, error) {
claims := JWTClaims{
UserID: user.ID,
Username: user.Username,
IsAdmin: user.IsAdmin,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(cfg.ExpireHour) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "ezops",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.Secret))
}
// JWTAuth JWT认证中间件
func JWTAuth(cfg *config.JWTConfig) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "未提供认证令牌",
})
c.Abort()
return
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "认证令牌格式错误",
})
c.Abort()
return
}
tokenString := parts[1]
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(cfg.Secret), nil
})
if err != nil {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "无效的认证令牌: " + err.Error(),
})
c.Abort()
return
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Set("isAdmin", claims.IsAdmin)
c.Next()
} else {
c.JSON(http.StatusUnauthorized, models.Response{
Code: 401,
Message: "无效的认证令牌",
})
c.Abort()
}
}
}
// AdminAuth 管理员认证中间件
func AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
isAdmin, exists := c.Get("isAdmin")
if !exists || !isAdmin.(bool) {
c.JSON(http.StatusForbidden, models.Response{
Code: 403,
Message: "需要管理员权限",
})
c.Abort()
return
}
c.Next()
}
}
// GetUserID 获取当前用户ID
func GetUserID(c *gin.Context) uint {
userID, exists := c.Get("userID")
if !exists {
return 0
}
return userID.(uint)
}
// GetUsername 获取当前用户名
func GetUsername(c *gin.Context) string {
username, exists := c.Get("username")
if !exists {
return ""
}
return username.(string)
}
// GetIsAdmin 获取是否为管理员
func GetIsAdmin(c *gin.Context) bool {
isAdmin, exists := c.Get("isAdmin")
if !exists {
return false
}
return isAdmin.(bool)
}
+212
View File
@@ -0,0 +1,212 @@
package routes
import (
"ezops/internal/api/handlers"
"ezops/internal/api/middleware"
"ezops/internal/config"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Register 注册所有路由
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
// API版本前缀
api := r.Group("/api/v1")
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 公开路由
authHandler := handlers.NewUserHandler(db, cfg)
api.POST("/auth/login", authHandler.Login)
api.POST("/auth/register", authHandler.Register)
// 需要认证的路由
authorized := api.Group("")
authorized.Use(middleware.JWTAuth(&cfg.JWT))
// 用户路由
user := authorized.Group("/user")
{
user.GET("/profile", authHandler.GetProfile)
user.PUT("/profile", authHandler.UpdateProfile)
user.PUT("/password", authHandler.ChangePassword)
}
// 管理员路由
admin := authorized.Group("/admin")
admin.Use(middleware.AdminAuth())
{
admin.GET("/users", authHandler.ListUsers)
admin.GET("/users/:id", authHandler.GetUser)
admin.POST("/users", authHandler.CreateUser)
admin.PUT("/users/:id", authHandler.UpdateUser)
admin.DELETE("/users/:id", authHandler.DeleteUser)
}
// 仪表盘路由
dashboardHandler := handlers.NewDashboardHandler(db)
dashboard := authorized.Group("/dashboard")
{
dashboard.GET("/stats", dashboardHandler.GetStats)
dashboard.GET("/recent-alerts", dashboardHandler.GetRecentAlerts)
dashboard.GET("/recent-releases", dashboardHandler.GetRecentReleases)
dashboard.GET("/metrics", dashboardHandler.GetMetrics)
}
// 服务器路由
serverHandler := handlers.NewServerHandler(db)
servers := authorized.Group("/servers")
{
servers.GET("", serverHandler.ListServers)
servers.GET("/stats", serverHandler.GetServerStats)
servers.GET("/:id", serverHandler.GetServer)
servers.POST("", serverHandler.CreateServer)
servers.PUT("/:id", serverHandler.UpdateServer)
servers.DELETE("/:id", serverHandler.DeleteServer)
servers.POST("/batch-delete", serverHandler.BatchDeleteServers)
}
// 云账号路由
cloudAccountHandler := handlers.NewCloudAccountHandler(db)
cloudAccounts := authorized.Group("/cloud-accounts")
{
cloudAccounts.GET("", cloudAccountHandler.ListCloudAccounts)
cloudAccounts.POST("", cloudAccountHandler.CreateCloudAccount)
cloudAccounts.DELETE("/:id", cloudAccountHandler.DeleteCloudAccount)
}
// 域名路由
domainHandler := handlers.NewDomainHandler(db)
domains := authorized.Group("/domains")
{
domains.GET("", domainHandler.ListDomains)
domains.POST("", domainHandler.CreateDomain)
domains.DELETE("/:id", domainHandler.DeleteDomain)
}
// K8s集群路由
k8sHandler := handlers.NewK8sHandler(db)
clusters := authorized.Group("/k8s-clusters")
{
clusters.GET("", k8sHandler.ListClusters)
clusters.GET("/:id", k8sHandler.GetCluster)
clusters.POST("", k8sHandler.CreateCluster)
clusters.PUT("/:id", k8sHandler.UpdateCluster)
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
clusters.GET("/:id/nodes", k8sHandler.ListNodes)
clusters.GET("/:id/pods", k8sHandler.ListPods)
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
clusters.GET("/:id/services", k8sHandler.ListServices)
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses)
}
// 告警路由
alertHandler := handlers.NewAlertHandler(db)
alerts := authorized.Group("/alerts")
{
alerts.GET("", alertHandler.ListAlerts)
alerts.GET("/rules", alertHandler.ListRules)
alerts.POST("/rules", alertHandler.CreateRule)
alerts.PUT("/rules/:id", alertHandler.UpdateRule)
alerts.DELETE("/rules/:id", alertHandler.DeleteRule)
alerts.POST("/rules/:id/toggle", alertHandler.ToggleRule)
alerts.POST("/:id/ack", alertHandler.AcknowledgeAlert)
}
// 流水线路由
pipelineHandler := handlers.NewPipelineHandler(db)
pipelines := authorized.Group("/pipelines")
{
pipelines.GET("", pipelineHandler.ListPipelines)
pipelines.POST("", pipelineHandler.CreatePipeline)
pipelines.GET("/:id", pipelineHandler.GetPipeline)
pipelines.PUT("/:id", pipelineHandler.UpdatePipeline)
pipelines.DELETE("/:id", pipelineHandler.DeletePipeline)
pipelines.POST("/:id/trigger", pipelineHandler.TriggerPipeline)
pipelines.GET("/:id/runs", pipelineHandler.ListRuns)
}
// 应用发布路由
releases := authorized.Group("/releases")
{
releases.GET("", pipelineHandler.ListReleases)
releases.POST("", pipelineHandler.CreateRelease)
releases.POST("/:id/approve", pipelineHandler.ApproveRelease)
releases.POST("/:id/deploy", pipelineHandler.DeployRelease)
releases.POST("/:id/rollback", pipelineHandler.RollbackRelease)
}
// 自动化运维路由
automationHandler := handlers.NewAutomationHandler(db)
cronJobs := authorized.Group("/cron-jobs")
{
cronJobs.GET("", automationHandler.ListCronJobs)
cronJobs.POST("", automationHandler.CreateCronJob)
cronJobs.GET("/:id", automationHandler.GetCronJob)
cronJobs.PUT("/:id", automationHandler.UpdateCronJob)
cronJobs.DELETE("/:id", automationHandler.DeleteCronJob)
cronJobs.POST("/:id/toggle", automationHandler.ToggleCronJob)
cronJobs.POST("/:id/execute", automationHandler.ExecuteCronJob)
}
scripts := authorized.Group("/scripts")
{
scripts.GET("", automationHandler.ListScripts)
scripts.POST("", automationHandler.CreateScript)
scripts.GET("/:id", automationHandler.GetScript)
scripts.PUT("/:id", automationHandler.UpdateScript)
scripts.DELETE("/:id", automationHandler.DeleteScript)
scripts.POST("/:id/execute", automationHandler.ExecuteScript)
}
// 审批流程路由
approvalHandler := handlers.NewApprovalHandler(db)
approvals := authorized.Group("/approvals")
{
approvals.GET("/templates", approvalHandler.ListTemplates)
approvals.POST("/templates", approvalHandler.CreateTemplate)
approvals.GET("/templates/:id", approvalHandler.GetTemplate)
approvals.PUT("/templates/:id", approvalHandler.UpdateTemplate)
approvals.DELETE("/templates/:id", approvalHandler.DeleteTemplate)
approvals.GET("/processes", approvalHandler.ListProcesses)
approvals.POST("/processes", approvalHandler.CreateProcess)
approvals.GET("/processes/:id", approvalHandler.GetProcess)
approvals.POST("/processes/:id/approve", approvalHandler.ApproveProcess)
approvals.POST("/processes/:id/reject", approvalHandler.RejectProcess)
}
// 导航路由
navHandler := handlers.NewNavigationHandler(db)
navigations := authorized.Group("/navigations")
{
navigations.GET("/groups", navHandler.ListGroups)
navigations.POST("/groups", navHandler.CreateGroup)
navigations.PUT("/groups/:id", navHandler.UpdateGroup)
navigations.DELETE("/groups/:id", navHandler.DeleteGroup)
navigations.GET("/links", navHandler.ListLinks)
navigations.POST("/links", navHandler.CreateLink)
navigations.PUT("/links/:id", navHandler.UpdateLink)
navigations.DELETE("/links/:id", navHandler.DeleteLink)
navigations.POST("/links/:id/favorite", navHandler.ToggleFavorite)
}
// 审计日志路由
auditHandler := handlers.NewAuditHandler(db)
audit := authorized.Group("/audit")
{
audit.GET("/logs", auditHandler.ListAuditLogs)
audit.GET("/logs/:id", auditHandler.GetAuditLogDetail)
audit.GET("/modules", auditHandler.GetAuditModules)
audit.GET("/actions", auditHandler.GetAuditActions)
audit.GET("/logs/export", auditHandler.ExportAuditLogs)
audit.GET("/statistics", auditHandler.GetAuditStatistics)
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
}
}
+96
View File
@@ -0,0 +1,96 @@
package config
import (
"github.com/spf13/viper"
"go.uber.org/zap"
)
// Config 应用配置
type Config struct {
Mode string `mapstructure:"mode"`
Port string `mapstructure:"port"`
LogLevel string `mapstructure:"log_level"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
Redis RedisConfig `mapstructure:"redis"`
Websocket WebsocketConfig `mapstructure:"websocket"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
Charset string `mapstructure:"charset"`
MaxConn int `mapstructure:"max_conn"`
MaxIdle int `mapstructure:"max_idle"`
}
// JWTConfig JWT配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireHour int `mapstructure:"expire_hour"`
}
// RedisConfig Redis配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// WebsocketConfig WebSocket配置
type WebsocketConfig struct {
ReadBufferSize int `mapstructure:"read_buffer_size"`
WriteBufferSize int `mapstructure:"write_buffer_size"`
}
// Load 加载配置
func Load() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.AddConfigPath(".")
// 设置默认值
viper.SetDefault("mode", "development")
viper.SetDefault("port", "8080")
viper.SetDefault("log_level", "info")
viper.SetDefault("database.driver", "mysql")
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 3306)
viper.SetDefault("database.charset", "utf8mb4")
viper.SetDefault("database.max_conn", 100)
viper.SetDefault("database.max_idle", 10)
viper.SetDefault("jwt.expire_hour", 24)
viper.SetDefault("redis.host", "localhost")
viper.SetDefault("redis.port", 6379)
viper.SetDefault("websocket.read_buffer_size", 1024)
viper.SetDefault("websocket.write_buffer_size", 1024)
// 读取环境变量
viper.AutomaticEnv()
var cfg Config
if err := viper.ReadInConfig(); err != nil {
zap.L().Warn("config file not found, using defaults", zap.Error(err))
}
if err := viper.Unmarshal(&cfg); err != nil {
zap.L().Fatal("failed to unmarshal config", zap.Error(err))
}
return &cfg
}
// DSN 返回数据库连接字符串
func (d *DatabaseConfig) DSN() string {
if d.Driver == "mysql" {
return d.User + ":" + d.Password + "@tcp(" + d.Host + ":" + string(rune(d.Port)) + ")/" + d.DBName + "?charset=" + d.Charset + "&parseTime=True&loc=Local"
}
return ""
}
+137
View File
@@ -0,0 +1,137 @@
package models
import (
"time"
"gorm.io/gorm"
)
// AlertRule 告警规则模型
type AlertRule struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
Type string `gorm:"size:32;not null" json:"type"` // metric, log, event
Source string `gorm:"size:64;not null" json:"source"` // prometheus, zabbix, custom
Expression string `gorm:"type:text;not null" json:"expression"`
Severity string `gorm:"size:32;default:warning" json:"severity"` // info, warning, critical
Duration int `json:"duration"` // 持续时间(秒)
Enabled bool `gorm:"default:true" json:"enabled"`
Labels string `gorm:"type:text" json:"labels"` // JSON
Annotations string `gorm:"type:text" json:"annotations"` // JSON
NotifyChannels string `gorm:"type:text" json:"notify_channels"` // JSON
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (AlertRule) TableName() string {
return "alert_rules"
}
// AlertEvent 告警事件模型
type AlertEvent struct {
ID uint `gorm:"primarykey" json:"id"`
RuleID uint `json:"rule_id"`
RuleName string `gorm:"size:128" json:"rule_name"`
Severity string `gorm:"size:32" json:"severity"`
Status string `gorm:"size:32;default:firing" json:"status"` // firing, resolved
Target string `gorm:"size:255" json:"target"`
Message string `gorm:"type:text" json:"message"`
Value float64 `json:"value"`
Labels string `gorm:"type:text" json:"labels"`
StartedAt time.Time `json:"started_at"`
ResolvedAt *time.Time `json:"resolved_at"`
Acknowledged bool `json:"acknowledged"`
AcknowledgedBy *uint `json:"acknowledged_by"`
AcknowledgedAt *time.Time `json:"acknowledged_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
Rule *AlertRule `json:"rule,omitempty"`
}
// TableName 表名
func (AlertEvent) TableName() string {
return "alert_events"
}
// NotifyChannel 通知渠道模型
type NotifyChannel struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Type string `gorm:"size:32;not null" json:"type"` // email, sms, webhook, dingtalk, wechat
Config string `gorm:"type:text" json:"config"` // JSON
Enabled bool `gorm:"default:true" json:"enabled"`
TestStatus string `gorm:"size:32" json:"test_status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (NotifyChannel) TableName() string {
return "notify_channels"
}
// MetricData 监控指标数据
type MetricData struct {
ID uint `gorm:"primarykey" json:"id"`
ServerID *uint `json:"server_id"`
ClusterID *uint `json:"cluster_id"`
Name string `gorm:"size:64" json:"name"`
Value float64 `json:"value"`
Labels string `gorm:"type:text" json:"labels"`
Timestamp time.Time `json:"timestamp"`
}
// TableName 表名
func (MetricData) TableName() string {
return "metric_data"
}
// CreateAlertRuleRequest 创建告警规则请求
type CreateAlertRuleRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Type string `json:"type" binding:"required,oneof=metric log event"`
Source string `json:"source" binding:"required"`
Expression string `json:"expression" binding:"required"`
Severity string `json:"severity" binding:"omitempty,oneof=info warning critical"`
Duration int `json:"duration"`
Labels string `json:"labels"`
Annotations string `json:"annotations"`
NotifyChannels []string `json:"notify_channels"`
}
// UpdateAlertRuleRequest 更新告警规则请求
type UpdateAlertRuleRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Expression string `json:"expression"`
Severity string `json:"severity" binding:"omitempty,oneof=info warning critical"`
Duration int `json:"duration"`
Enabled *bool `json:"enabled"`
Labels string `json:"labels"`
Annotations string `json:"annotations"`
NotifyChannels []string `json:"notify_channels"`
}
// AlertStats 告警统计
type AlertStats struct {
Total int64 `json:"total"`
Firing int64 `json:"firing"`
Resolved int64 `json:"resolved"`
Critical int64 `json:"critical"`
Warning int64 `json:"warning"`
Info int64 `json:"info"`
TodayCount int64 `json:"today_count"`
WeekCount int64 `json:"week_count"`
}
// AcknowledgeAlertRequest 确认告警请求
type AcknowledgeAlertRequest struct {
Message string `json:"message"`
}
+108
View File
@@ -0,0 +1,108 @@
package models
import (
"time"
"gorm.io/gorm"
)
// ApprovalTemplate 审批模板模型
type ApprovalTemplate struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:500" json:"description"`
Type string `gorm:"size:32;not null" json:"type"` // release, server, permission
FormSchema string `gorm:"type:text" json:"form_schema"` // JSON
FlowConfig string `gorm:"type:text" json:"flow_config"` // JSON
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (ApprovalTemplate) TableName() string {
return "approval_templates"
}
// ApprovalProcess 审批流程模型
type ApprovalProcess struct {
ID uint `gorm:"primarykey" json:"id"`
TemplateID uint `json:"template_id"`
Title string `gorm:"size:255;not null" json:"title"`
Applicant uint `json:"applicant"`
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
FormData string `gorm:"type:text" json:"form_data"` // JSON
CurrentStep int `gorm:"default:0" json:"current_step"`
TotalSteps int `json:"total_steps"`
ApprovedAt *time.Time `json:"approved_at"`
RejectedAt *time.Time `json:"rejected_at"`
RejectReason string `gorm:"size:500" json:"reject_reason"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Template *ApprovalTemplate `json:"template,omitempty"`
ApplicantUser *User `json:"applicant_user,omitempty"`
Steps []ApprovalStep `json:"steps,omitempty"`
}
// TableName 表名
func (ApprovalProcess) TableName() string {
return "approval_processes"
}
// ApprovalStep 审批步骤模型
type ApprovalStep struct {
ID uint `gorm:"primarykey" json:"id"`
ProcessID uint `json:"process_id"`
Step int `json:"step"`
Approver uint `json:"approver"`
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
Comment string `gorm:"size:500" json:"comment"`
ApprovedAt *time.Time `json:"approved_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
ApproverUser *User `json:"approver_user,omitempty"`
}
// TableName 表名
func (ApprovalStep) TableName() string {
return "approval_steps"
}
// CreateApprovalTemplateRequest 创建审批模板请求
type CreateApprovalTemplateRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Type string `json:"type" binding:"required,oneof=release server permission"`
FormSchema string `json:"form_schema"`
FlowConfig string `json:"flow_config"`
}
// CreateApprovalProcessRequest 创建审批流程请求
type CreateApprovalProcessRequest struct {
TemplateID uint `json:"template_id" binding:"required"`
Title string `json:"title" binding:"required"`
FormData string `json:"form_data"`
}
// ApproveRequest 审批请求
type ApproveRequest struct {
Status string `json:"status" binding:"required,oneof=approved rejected"`
Comment string `json:"comment"`
}
// ApprovalStats 审批统计
type ApprovalStats struct {
Pending int64 `json:"pending"`
Approved int64 `json:"approved"`
Rejected int64 `json:"rejected"`
Total int64 `json:"total"`
MyPending int64 `json:"my_pending"`
}
+68
View File
@@ -0,0 +1,68 @@
package models
import (
"time"
)
// AuditLog 审计日志模型
type AuditLog struct {
ID uint `gorm:"primarykey" json:"id"`
UserID *uint `json:"user_id"`
Username string `gorm:"size:64" json:"username"`
IP string `gorm:"size:64" json:"ip"`
UserAgent string `gorm:"size:500" json:"user_agent"`
Module string `gorm:"size:64" json:"module"`
Action string `gorm:"size:64" json:"action"`
Resource string `gorm:"size:128" json:"resource"`
ResourceID string `gorm:"size:64" json:"resource_id"`
Request string `gorm:"type:text" json:"request"`
Response string `gorm:"type:text" json:"response"`
Status int `json:"status"`
Duration int `json:"duration"` // ms
Error string `gorm:"type:text" json:"error"`
CreatedAt time.Time `json:"created_at"`
}
// TableName 表名
func (AuditLog) TableName() string {
return "audit_logs"
}
// AuditLogQuery 审计日志查询
type AuditLogQuery struct {
UserID *uint `form:"user_id"`
Username string `form:"username"`
Module string `form:"module"`
Action string `form:"action"`
StartTime string `form:"start_time"`
EndTime string `form:"end_time"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
}
// AuditLogListResponse 审计日志列表响应
type AuditLogListResponse struct {
List []AuditLog `json:"list"`
Total int64 `json:"total"`
}
// AuditStats 审计统计
type AuditStats struct {
TodayCount int64 `json:"today_count"`
WeekCount int64 `json:"week_count"`
MonthCount int64 `json:"month_count"`
TopModules []ModuleStat `json:"top_modules"`
TopUsers []UserStat `json:"top_users"`
}
// ModuleStat 模块统计
type ModuleStat struct {
Module string `json:"module"`
Count int64 `json:"count"`
}
// UserStat 用户统计
type UserStat struct {
Username string `json:"username"`
Count int64 `json:"count"`
}
+151
View File
@@ -0,0 +1,151 @@
package models
import (
"time"
"gorm.io/gorm"
)
// CronJob 定时任务模型
type CronJob struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
CronExpr string `gorm:"size:64;not null" json:"cron_expr"`
Type string `gorm:"size:32;not null" json:"type"` // script, command, playbook
Content string `gorm:"type:text" json:"content"`
ScriptID *uint `json:"script_id"`
TargetType string `gorm:"size:32;default:server" json:"target_type"` // server, group, tag
Targets string `gorm:"type:text" json:"targets"` // JSON
Timeout int `gorm:"default:300" json:"timeout"`
Enabled bool `gorm:"default:true" json:"enabled"`
LastRunAt *time.Time `json:"last_run_at"`
LastStatus string `gorm:"size:32" json:"last_status"`
RunCount int `gorm:"default:0" json:"run_count"`
SuccessCount int `gorm:"default:0" json:"success_count"`
FailCount int `gorm:"default:0" json:"fail_count"`
NotifyOn string `gorm:"size:32;default:failure" json:"notify_on"` // always, failure, success
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Script *Script `json:"script,omitempty"`
}
// TableName 表名
func (CronJob) TableName() string {
return "cron_jobs"
}
// Script 脚本库模型
type Script struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:500" json:"description"`
Type string `gorm:"size:32;default:shell" json:"type"` // shell, python, powershell, yaml
Content string `gorm:"type:text;not null" json:"content"`
Params string `gorm:"type:text" json:"params"` // JSON
Timeout int `gorm:"default:300" json:"timeout"`
Version int `gorm:"default:1" json:"version"`
RunCount int `gorm:"default:0" json:"run_count"`
CreatedBy uint `json:"created_by"`
UpdatedBy uint `json:"updated_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Script) TableName() string {
return "scripts"
}
// TaskExecution 任务执行记录
type TaskExecution struct {
ID uint `gorm:"primarykey" json:"id"`
TaskType string `gorm:"size:32;not null" json:"task_type"` // cron, script, playbook
TaskID uint `json:"task_id"`
TaskName string `gorm:"size:128" json:"task_name"`
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
TargetType string `gorm:"size:32" json:"target_type"`
Targets string `gorm:"type:text" json:"targets"`
Output string `gorm:"type:longtext" json:"output"`
Error string `gorm:"type:text" json:"error"`
Duration int `json:"duration"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
CreatedBy uint `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 表名
func (TaskExecution) TableName() string {
return "task_executions"
}
// Playbook Ansible Playbook模型
type Playbook struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:500" json:"description"`
Content string `gorm:"type:text;not null" json:"content"`
Variables string `gorm:"type:text" json:"variables"`
Inventory string `gorm:"type:text" json:"inventory"`
Timeout int `gorm:"default:600" json:"timeout"`
RunCount int `gorm:"default:0" json:"run_count"`
CreatedBy uint `json:"created_by"`
UpdatedBy uint `json:"updated_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Playbook) TableName() string {
return "playbooks"
}
// CreateCronJobRequest 创建定时任务请求
type CreateCronJobRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
CronExpr string `json:"cron_expr" binding:"required"`
Type string `json:"type" binding:"required,oneof=script command playbook"`
Content string `json:"content"`
ScriptID *uint `json:"script_id"`
TargetType string `json:"target_type" binding:"omitempty,oneof=server group tag"`
Targets []uint `json:"targets"`
Timeout int `json:"timeout"`
NotifyOn string `json:"notify_on" binding:"omitempty,oneof=always failure success"`
}
// CreateScriptRequest 创建脚本请求
type CreateScriptRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Type string `json:"type" binding:"omitempty,oneof=shell python powershell yaml"`
Content string `json:"content" binding:"required"`
Params string `json:"params"`
Timeout int `json:"timeout"`
}
// ExecuteScriptRequest 执行脚本请求
type ExecuteScriptRequest struct {
TargetType string `json:"target_type" binding:"required,oneof=server group tag"`
Targets []uint `json:"targets" binding:"required"`
Params map[string]string `json:"params"`
}
// AutomationStats 自动化统计
type AutomationStats struct {
CronJobCount int64 `json:"cron_job_count"`
ScriptCount int64 `json:"script_count"`
PlaybookCount int64 `json:"playbook_count"`
TodayExecutions int64 `json:"today_executions"`
SuccessRate float64 `json:"success_rate"`
}
+130
View File
@@ -0,0 +1,130 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Pipeline CI/CD流水线模型
type Pipeline struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:500" json:"description"`
Type string `gorm:"size:32;default:deploy" json:"type"` // build, deploy, test
Source string `gorm:"size:255" json:"source"` // Git仓库地址
Branch string `gorm:"size:64;default:main" json:"branch"`
Dockerfile string `gorm:"size:255" json:"dockerfile"`
Stages string `gorm:"type:text" json:"stages"` // JSON
Variables string `gorm:"type:text" json:"variables"` // JSON
Enabled bool `gorm:"default:true" json:"enabled"`
LastRunAt *time.Time `json:"last_run_at"`
LastStatus string `gorm:"size:32" json:"last_status"`
RunCount int `gorm:"default:0" json:"run_count"`
SuccessCount int `gorm:"default:0" json:"success_count"`
FailCount int `gorm:"default:0" json:"fail_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Pipeline) TableName() string {
return "pipelines"
}
// PipelineRun 流水线运行记录
type PipelineRun struct {
ID uint `gorm:"primarykey" json:"id"`
PipelineID uint `gorm:"not null" json:"pipeline_id"`
BuildNumber int `json:"build_number"`
Status string `gorm:"size:32;default:pending" json:"status"` // pending, running, success, failed, cancelled
Branch string `gorm:"size:64" json:"branch"`
CommitID string `gorm:"size:64" json:"commit_id"`
CommitMsg string `gorm:"size:500" json:"commit_msg"`
Author string `gorm:"size:64" json:"author"`
Duration int `json:"duration"` // 秒
Stages string `gorm:"type:text" json:"stages"` // JSON
Logs string `gorm:"type:longtext" json:"logs"`
TriggeredBy string `gorm:"size:64" json:"triggered_by"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
Pipeline *Pipeline `json:"pipeline,omitempty"`
}
// TableName 表名
func (PipelineRun) TableName() string {
return "pipeline_runs"
}
// AppRelease 应用发布模型
type AppRelease struct {
ID uint `gorm:"primarykey" json:"id"`
AppName string `gorm:"size:128;not null" json:"app_name"`
Version string `gorm:"size:64;not null" json:"version"`
Environment string `gorm:"size:32;not null" json:"environment"` // dev, test, staging, prod
Status string `gorm:"size:32;default:pending" json:"status"` // pending, building, deploying, success, failed, rolled_back
CommitID string `gorm:"size:64" json:"commit_id"`
CommitMsg string `gorm:"size:500" json:"commit_msg"`
Author string `gorm:"size:64" json:"author"`
Approver string `gorm:"size:64" json:"approver"`
ApprovedAt *time.Time `json:"approved_at"`
DeployedAt *time.Time `json:"deployed_at"`
RollbackAt *time.Time `json:"rollback_at"`
Duration int `json:"duration"`
Servers string `gorm:"type:text" json:"servers"` // JSON
Description string `gorm:"size:500" json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (AppRelease) TableName() string {
return "app_releases"
}
// CreatePipelineRequest 创建流水线请求
type CreatePipelineRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Type string `json:"type" binding:"omitempty,oneof=build deploy test"`
Source string `json:"source"`
Branch string `json:"branch"`
Dockerfile string `json:"dockerfile"`
Stages string `json:"stages"`
Variables string `json:"variables"`
}
// TriggerPipelineRequest 触发流水线请求
type TriggerPipelineRequest struct {
Branch string `json:"branch"`
Variables map[string]string `json:"variables"`
}
// CreateAppReleaseRequest 创建应用发布请求
type CreateAppReleaseRequest struct {
AppName string `json:"app_name" binding:"required"`
Version string `json:"version" binding:"required"`
Environment string `json:"environment" binding:"required,oneof=dev test staging prod"`
CommitID string `json:"commit_id"`
CommitMsg string `json:"commit_msg"`
Servers []uint `json:"servers"`
Description string `json:"description"`
}
// PipelineStats 流水线统计
type PipelineStats struct {
Total int64 `json:"total"`
Success int64 `json:"success"`
Failed int64 `json:"failed"`
Running int64 `json:"running"`
TodayCount int64 `json:"today_count"`
SuccessRate float64 `json:"success_rate"`
}
+111
View File
@@ -0,0 +1,111 @@
package models
// Response 通用响应
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// ListResponse 列表响应
type ListResponse struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
}
// PageQuery 分页查询
type PageQuery struct {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
Keyword string `form:"keyword"`
Sort string `form:"sort"`
Order string `form:"order,default=desc"`
}
// GetOffset 获取偏移量
func (p *PageQuery) GetOffset() int {
if p.Page < 1 {
p.Page = 1
}
return (p.Page - 1) * p.GetLimit()
}
// GetLimit 获取每页数量
func (p *PageQuery) GetLimit() int {
if p.PageSize < 1 {
p.PageSize = 20
}
if p.PageSize > 100 {
p.PageSize = 100
}
return p.PageSize
}
// IDRequest ID请求
type IDRequest struct {
ID uint `uri:"id" binding:"required"`
}
// IDsRequest IDs请求
type IDsRequest struct {
IDs []uint `json:"ids" binding:"required"`
}
// StatusRequest 状态请求
type StatusRequest struct {
Status int `json:"status" binding:"required,oneof=0 1"`
}
// DashboardStats 仪表盘统计
type DashboardStats struct {
ServerStats ServerStats `json:"server_stats"`
K8sStats K8sClusterStats `json:"k8s_stats"`
AlertStats AlertStats `json:"alert_stats"`
PipelineStats PipelineStats `json:"pipeline_stats"`
CostStats CostStats `json:"cost_stats"`
TopResourceUsage TopResourceUsage `json:"top_resource_usage"`
ExpiringCerts []DomainCert `json:"expiring_certs"`
}
// CostStats 成本统计
type CostStats struct {
TotalCost float64 `json:"total_cost"`
MonthCost float64 `json:"month_cost"`
CloudCosts []CloudCost `json:"cloud_costs"`
MonthTrend []MonthTrend `json:"month_trend"`
}
// CloudCost 云平台成本
type CloudCost struct {
Cloud string `json:"cloud"`
Cost float64 `json:"cost"`
}
// MonthTrend 月度趋势
type MonthTrend struct {
Month string `json:"month"`
Cost float64 `json:"cost"`
}
// TopResourceUsage Top资源使用
type TopResourceUsage struct {
CPU []ServerResource `json:"cpu"`
Memory []ServerResource `json:"memory"`
Disk []ServerResource `json:"disk"`
}
// ServerResource 服务器资源
type ServerResource struct {
ID uint `json:"id"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
Usage float64 `json:"usage"`
}
// DomainCert 域名证书
type DomainCert struct {
ID uint `json:"id"`
Name string `json:"name"`
ExpireDate string `json:"expire_date"`
DaysUntilExpiry int `json:"days_until_expiry"`
}
+188
View File
@@ -0,0 +1,188 @@
package models
import (
"time"
"gorm.io/gorm"
)
// K8sCluster K8s集群模型
type K8sCluster struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Version string `gorm:"size:32" json:"version"`
Status string `gorm:"size:32;default:healthy" json:"status"` // healthy, warning, error
APIEndpoint string `gorm:"size:255" json:"api_endpoint"`
KubeConfig string `gorm:"type:text" json:"-"`
NodeCount int `gorm:"default:0" json:"node_count"`
PodCount int `gorm:"default:0" json:"pod_count"`
ServiceCount int `gorm:"default:0" json:"service_count"`
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
Description string `gorm:"size:500" json:"description"`
Labels string `gorm:"type:text" json:"labels"` // JSON
LastSyncAt *time.Time `json:"last_sync_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (K8sCluster) TableName() string {
return "k8s_clusters"
}
// K8sNode K8s节点模型
type K8sNode struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"not null" json:"cluster_id"`
Name string `gorm:"size:128;not null" json:"name"`
IP string `gorm:"size:64" json:"ip"`
Role string `gorm:"size:32" json:"role"` // master, worker
Status string `gorm:"size:32" json:"status"`
OS string `gorm:"size:64" json:"os"`
KubeletVer string `gorm:"size:32" json:"kubelet_version"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
Disk int64 `json:"disk"`
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
DiskUsage float64 `json:"disk_usage"`
PodCount int `json:"pod_count"`
MaxPods int `json:"max_pods"`
Labels string `gorm:"type:text" json:"labels"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Cluster *K8sCluster `json:"cluster,omitempty"`
}
// TableName 表名
func (K8sNode) TableName() string {
return "k8s_nodes"
}
// K8sPod K8s Pod模型
type K8sPod struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"not null" json:"cluster_id"`
Namespace string `gorm:"size:64;not null" json:"namespace"`
Name string `gorm:"size:128;not null" json:"name"`
Status string `gorm:"size:32" json:"status"` // Running, Pending, Succeeded, Failed
NodeName string `gorm:"size:128" json:"node_name"`
IP string `gorm:"size:64" json:"ip"`
RestartCount int `json:"restart_count"`
Image string `gorm:"size:255" json:"image"`
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
Labels string `gorm:"type:text" json:"labels"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Cluster *K8sCluster `json:"cluster,omitempty"`
}
// TableName 表名
func (K8sPod) TableName() string {
return "k8s_pods"
}
// K8sDeployment K8s Deployment模型
type K8sDeployment struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"not null" json:"cluster_id"`
Namespace string `gorm:"size:64;not null" json:"namespace"`
Name string `gorm:"size:128;not null" json:"name"`
Replicas int `json:"replicas"`
AvailableReplicas int `json:"available_replicas"`
ReadyReplicas int `json:"ready_replicas"`
Image string `gorm:"size:255" json:"image"`
Strategy string `gorm:"size:32" json:"strategy"`
Labels string `gorm:"type:text" json:"labels"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Cluster *K8sCluster `json:"cluster,omitempty"`
}
// TableName 表名
func (K8sDeployment) TableName() string {
return "k8s_deployments"
}
// K8sService K8s Service模型
type K8sService struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"not null" json:"cluster_id"`
Namespace string `gorm:"size:64;not null" json:"namespace"`
Name string `gorm:"size:128;not null" json:"name"`
Type string `gorm:"size:32" json:"type"` // ClusterIP, NodePort, LoadBalancer
ClusterIP string `gorm:"size:64" json:"cluster_ip"`
ExternalIP string `gorm:"size:255" json:"external_ip"`
Ports string `gorm:"type:text" json:"ports"` // JSON
Selector string `gorm:"type:text" json:"selector"`
Labels string `gorm:"type:text" json:"labels"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Cluster *K8sCluster `json:"cluster,omitempty"`
}
// TableName 表名
func (K8sService) TableName() string {
return "k8s_services"
}
// K8sIngress K8s Ingress模型
type K8sIngress struct {
ID uint `gorm:"primarykey" json:"id"`
ClusterID uint `gorm:"not null" json:"cluster_id"`
Namespace string `gorm:"size:64;not null" json:"namespace"`
Name string `gorm:"size:128;not null" json:"name"`
Hosts string `gorm:"type:text" json:"hosts"` // JSON
Backend string `gorm:"size:255" json:"backend"`
TLS bool `json:"tls"`
TLSHosts string `gorm:"type:text" json:"tls_hosts"`
Labels string `gorm:"type:text" json:"labels"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Cluster *K8sCluster `json:"cluster,omitempty"`
}
// TableName 表名
func (K8sIngress) TableName() string {
return "k8s_ingresses"
}
// CreateK8sClusterRequest 创建K8s集群请求
type CreateK8sClusterRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Version string `json:"version"`
APIEndpoint string `json:"api_endpoint"`
KubeConfig string `json:"kube_config"`
Description string `json:"description"`
}
// K8sClusterStats K8s集群统计
type K8sClusterStats struct {
Total int64 `json:"total"`
Healthy int64 `json:"healthy"`
Warning int64 `json:"warning"`
Error int64 `json:"error"`
TotalNodes int64 `json:"total_nodes"`
TotalPods int64 `json:"total_pods"`
TotalServices int64 `json:"total_services"`
}
+98
View File
@@ -0,0 +1,98 @@
package models
import (
"time"
"gorm.io/gorm"
)
// NavigationGroup 导航分组模型
type NavigationGroup struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:255" json:"description"`
Icon string `gorm:"size:64" json:"icon"`
Sort int `gorm:"default:0" json:"sort"`
Enabled bool `gorm:"default:true" json:"enabled"`
LinkCount int `gorm:"default:0" json:"link_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (NavigationGroup) TableName() string {
return "navigation_groups"
}
// NavigationLink 导航链接模型
type NavigationLink struct {
ID uint `gorm:"primarykey" json:"id"`
GroupID uint `json:"group_id"`
Name string `gorm:"size:128;not null" json:"name"`
URL string `gorm:"size:500;not null" json:"url"`
Description string `gorm:"size:255" json:"description"`
Icon string `gorm:"size:64" json:"icon"`
Tags string `gorm:"type:text" json:"tags"` // JSON
IsFavorite bool `gorm:"default:false" json:"is_favorite"`
Sort int `gorm:"default:0" json:"sort"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Group *NavigationGroup `json:"group,omitempty"`
}
// TableName 表名
func (NavigationLink) TableName() string {
return "navigation_links"
}
// CreateNavigationGroupRequest 创建导航分组请求
type CreateNavigationGroupRequest struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required"`
Description string `json:"description"`
Icon string `json:"icon"`
Sort int `json:"sort"`
}
// UpdateNavigationGroupRequest 更新导航分组请求
type UpdateNavigationGroupRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Sort int `json:"sort"`
Enabled *bool `json:"enabled"`
}
// CreateNavigationLinkRequest 创建导航链接请求
type CreateNavigationLinkRequest struct {
GroupID uint `json:"group_id" binding:"required"`
Name string `json:"name" binding:"required"`
URL string `json:"url" binding:"required,url"`
Description string `json:"description"`
Icon string `json:"icon"`
Tags []string `json:"tags"`
Sort int `json:"sort"`
}
// UpdateNavigationLinkRequest 更新导航链接请求
type UpdateNavigationLinkRequest struct {
GroupID uint `json:"group_id"`
Name string `json:"name"`
URL string `json:"url" binding:"omitempty,url"`
Description string `json:"description"`
Icon string `json:"icon"`
Tags []string `json:"tags"`
Sort int `json:"sort"`
Enabled *bool `json:"enabled"`
}
// ToggleFavoriteRequest 切换收藏请求
type ToggleFavoriteRequest struct {
IsFavorite bool `json:"is_favorite"`
}
+189
View File
@@ -0,0 +1,189 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Server 服务器模型
type Server struct {
ID uint `gorm:"primarykey" json:"id"`
Hostname string `gorm:"size:128;not null" json:"hostname"`
IP string `gorm:"size:64;not null" json:"ip"`
PublicIP string `gorm:"size:64" json:"public_ip"`
Port int `gorm:"default:22" json:"port"`
Username string `gorm:"size:64;default:root" json:"username"`
Password string `gorm:"size:255" json:"-"`
SSHKey string `gorm:"type:text" json:"-"`
AuthType string `gorm:"size:32;default:password" json:"auth_type"` // password, key
OS string `gorm:"size:64" json:"os"`
OSVersion string `gorm:"size:64" json:"os_version"`
Arch string `gorm:"size:32" json:"arch"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"` // MB
Disk int64 `json:"disk"` // GB
Status string `gorm:"size:32;default:stopped" json:"status"` // running, stopped, maintenance, error
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
DiskUsage float64 `json:"disk_usage"`
Uptime int64 `json:"uptime"` // seconds
AgentVersion string `gorm:"size:32" json:"agent_version"`
AgentStatus string `gorm:"size:32;default:offline" json:"agent_status"` // online, offline
RegionID *uint `json:"region_id"`
CloudID *uint `json:"cloud_id"`
Tags string `gorm:"type:text" json:"tags"` // JSON
Description string `gorm:"size:500" json:"description"`
LastCheckAt *time.Time `json:"last_check_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Region *Region `json:"region,omitempty"`
Cloud *Cloud `json:"cloud,omitempty"`
}
// TableName 表名
func (Server) TableName() string {
return "servers"
}
// Cloud 云平台模型
type Cloud struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Type string `gorm:"size:32;default:public" json:"type"` // public, private, hybrid
Status string `gorm:"size:32;default:active" json:"status"`
Website string `gorm:"size:255" json:"website"`
Description string `gorm:"size:500" json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Cloud) TableName() string {
return "clouds"
}
// CloudAccount 云账号模型
type CloudAccount struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Provider string `gorm:"size:64;not null" json:"provider"` // aws, aliyun, tencent, huawei, azure
AccessKey string `gorm:"size:255;not null" json:"-"`
SecretKey string `gorm:"size:255;not null" json:"-"`
Status string `gorm:"size:32;default:active" json:"status"` // active, inactive, error
RegionCount int `gorm:"default:0" json:"region_count"`
ServerCount int `gorm:"default:0" json:"server_count"`
LastSyncAt *time.Time `json:"last_sync_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (CloudAccount) TableName() string {
return "cloud_accounts"
}
// Region 区域模型
type Region struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Provider string `gorm:"size:64;not null" json:"provider"`
Location string `gorm:"size:128" json:"location"`
Status string `gorm:"size:32;default:active" json:"status"`
Endpoint string `gorm:"size:255" json:"endpoint"`
VPCCount int `gorm:"default:0" json:"vpc_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Region) TableName() string {
return "regions"
}
// Domain 域名模型
type Domain struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:255;uniqueIndex;not null" json:"name"`
Registrar string `gorm:"size:128" json:"registrar"`
Status string `gorm:"size:32;default:active" json:"status"` // active, expired, pending
ExpireDate *time.Time `json:"expire_date"`
DNSProvider string `gorm:"size:128" json:"dns_provider"`
RecordCount int `gorm:"default:0" json:"record_count"`
SSLEnabled bool `gorm:"default:false" json:"ssl_enabled"`
SSLExpireDate *time.Time `json:"ssl_expire_date"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Domain) TableName() string {
return "domains"
}
// CreateServerRequest 创建服务器请求
type CreateServerRequest struct {
Hostname string `json:"hostname" binding:"required"`
IP string `json:"ip" binding:"required,ip"`
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
Username string `json:"username"`
Password string `json:"password"`
SSHKey string `json:"ssh_key"`
AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"`
OS string `json:"os"`
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 更新服务器请求
type UpdateServerRequest struct {
Hostname string `json:"hostname"`
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
Username string `json:"username"`
Password string `json:"password"`
SSHKey string `json:"ssh_key"`
AuthType string `json:"auth_type" binding:"omitempty,oneof=password key"`
OS string `json:"os"`
CPU int `json:"cpu"`
Memory int64 `json:"memory"`
Disk int64 `json:"disk"`
Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"`
RegionID *uint `json:"region_id"`
CloudID *uint `json:"cloud_id"`
Tags string `json:"tags"`
Description string `json:"description"`
}
// ServerListResponse 服务器列表响应
type ServerListResponse struct {
List []Server `json:"list"`
Total int64 `json:"total"`
}
// ServerStats 服务器统计
type ServerStats struct {
Total int64 `json:"total"`
Running int64 `json:"running"`
Stopped int64 `json:"stopped"`
Maintenance int64 `json:"maintenance"`
Error int64 `json:"error"`
AvgCPU float64 `json:"avg_cpu"`
AvgMemory float64 `json:"avg_memory"`
AvgDisk float64 `json:"avg_disk"`
}
+111
View File
@@ -0,0 +1,111 @@
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primarykey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
Password string `gorm:"size:255;not null" json:"-"`
Email string `gorm:"size:128;uniqueIndex" json:"email"`
Phone string `gorm:"size:32" json:"phone"`
RealName string `gorm:"size:64" json:"real_name"`
Avatar string `gorm:"size:255" json:"avatar"`
Status int `gorm:"default:1" json:"status"` // 0:禁用 1:启用
IsAdmin bool `gorm:"default:false" json:"is_admin"`
LastLogin *time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Roles []Role `gorm:"many2many:user_roles;" json:"roles,omitempty"`
}
// TableName 表名
func (User) TableName() string {
return "users"
}
// Role 角色模型
type Role struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;uniqueIndex;not null" json:"name"`
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:255" json:"description"`
Status int `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"`
}
// TableName 表名
func (Role) TableName() string {
return "roles"
}
// Permission 权限模型
type Permission struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:64;not null" json:"name"`
Code string `gorm:"size:128;uniqueIndex;not null" json:"code"`
Type string `gorm:"size:32" json:"type"` // menu, button, api
Path string `gorm:"size:255" json:"path"`
Method string `gorm:"size:32" json:"method"`
ParentID *uint `json:"parent_id"`
Sort int `gorm:"default:0" json:"sort"`
Status int `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 表名
func (Permission) TableName() string {
return "permissions"
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"`
User *User `json:"user"`
}
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password" binding:"required,min=6,max=32"`
Email string `json:"email" binding:"required,email"`
Phone string `json:"phone"`
RealName string `json:"real_name"`
RoleIDs []uint `json:"role_ids"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Email string `json:"email" binding:"omitempty,email"`
Phone string `json:"phone"`
RealName string `json:"real_name"`
Status *int `json:"status"`
RoleIDs []uint `json:"role_ids"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6,max=32"`
}
+108
View File
@@ -0,0 +1,108 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Upstream backend
upstream backend {
server backend:8080;
}
# HTTP server - redirect to HTTPS
server {
listen 80;
server_name _;
# Allow ACME challenge for Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
server_name _;
# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Proxy to backend
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# WebSocket support
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# Static files (if needed)
location /static {
alias /var/www/static;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
+88
View File
@@ -0,0 +1,88 @@
package database
import (
"fmt"
"time"
"ezops/internal/config"
"ezops/internal/models"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// New 创建数据库连接
func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName, cfg.Charset)
dialector = mysql.Open(dsn)
case "postgres":
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName)
dialector = postgres.Open(dsn)
default:
return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return nil, err
}
// 设置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(cfg.MaxConn)
sqlDB.SetMaxIdleConns(cfg.MaxIdle)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}
// AutoMigrate 自动迁移数据库表
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&models.User{},
&models.Role{},
&models.Permission{},
&models.Server{},
&models.Cloud{},
&models.CloudAccount{},
&models.Region{},
&models.Domain{},
&models.K8sCluster{},
&models.K8sNode{},
&models.K8sPod{},
&models.K8sDeployment{},
&models.K8sService{},
&models.K8sIngress{},
&models.AlertRule{},
&models.AlertEvent{},
&models.NotifyChannel{},
&models.MetricData{},
&models.Pipeline{},
&models.PipelineRun{},
&models.AppRelease{},
&models.CronJob{},
&models.Script{},
&models.TaskExecution{},
&models.Playbook{},
&models.ApprovalTemplate{},
&models.ApprovalProcess{},
&models.ApprovalStep{},
&models.NavigationGroup{},
&models.NavigationLink{},
&models.AuditLog{},
)
}
+85
View File
@@ -0,0 +1,85 @@
package logger
import (
"os"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// New 创建日志实例
func New(level string) *zap.Logger {
var lvl zapcore.Level
switch level {
case "debug":
lvl = zapcore.DebugLevel
case "info":
lvl = zapcore.InfoLevel
case "warn":
lvl = zapcore.WarnLevel
case "error":
lvl = zapcore.ErrorLevel
default:
lvl = zapcore.InfoLevel
}
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)),
lvl,
)
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
}
// GinLogger Gin日志中间件
func GinLogger(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
status := c.Writer.Status()
fields := []zap.Field{
zap.Int("status", status),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.Duration("cost", cost),
}
if len(c.Errors) > 0 {
fields = append(fields, zap.Strings("errors", c.Errors.Errors()))
}
if status >= 500 {
log.Error("Server Error", fields...)
} else if status >= 400 {
log.Warn("Client Error", fields...)
} else {
log.Info("Request", fields...)
}
}
}
+31
View File
@@ -0,0 +1,31 @@
-- EZOps 数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS ezops CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ezops;
-- 创建初始管理员用户
-- 密码: admin123 (bcrypt hash)
INSERT INTO users (username, password, email, real_name, phone, status, is_admin, created_at, updated_at) VALUES
('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMy.MqrqQzBZN0UfGNEsKYGsGqNQJx3wDxG', 'admin@ezops.local', '系统管理员', '13800138000', 'active', true, NOW(), NOW())
ON DUPLICATE KEY UPDATE updated_at = NOW();
-- 创建默认角色
INSERT INTO roles (name, description, created_at, updated_at) VALUES
('超级管理员', '拥有所有权限', NOW(), NOW()),
('运维工程师', '运维操作权限', NOW(), NOW()),
('开发人员', '开发相关权限', NOW(), NOW()),
('只读用户', '只读查看权限', NOW(), NOW())
ON DUPLICATE KEY UPDATE updated_at = NOW();
-- 创建默认权限
INSERT INTO permissions (name, resource, action, created_at, updated_at) VALUES
('用户管理', 'user', 'manage', NOW(), NOW()),
('服务器管理', 'server', 'manage', NOW(), NOW()),
('K8s管理', 'k8s', 'manage', NOW(), NOW()),
('监控告警', 'alert', 'manage', NOW(), NOW()),
('应用发布', 'pipeline', 'manage', NOW(), NOW()),
('自动化运维', 'automation', 'manage', NOW(), NOW()),
('审批流程', 'approval', 'manage', NOW(), NOW()),
('系统设置', 'system', 'manage', NOW(), NOW())
ON DUPLICATE KEY UPDATE updated_at = NOW();
+354
View File
@@ -0,0 +1,354 @@
package main
import (
"fmt"
"math/rand"
"time"
"ezops/internal/config"
"ezops/internal/models"
"ezops/pkg/database"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func main() {
cfg := config.Load()
db, err := database.New(cfg.Database)
if err != nil {
fmt.Printf("Failed to connect to database: %v\n", err)
return
}
if err := generateMockData(db); err != nil {
fmt.Printf("Failed to generate mock data: %v\n", err)
return
}
fmt.Println("Mock data generated successfully!")
}
func generateMockData(db *gorm.DB) error {
if err := generateUsers(db); err != nil {
return err
}
if err := generateServers(db); err != nil {
return err
}
if err := generateCloudAccounts(db); err != nil {
return err
}
if err := generateDomains(db); err != nil {
return err
}
if err := generateK8sClusters(db); err != nil {
return err
}
if err := generateAlertRules(db); err != nil {
return err
}
if err := generatePipelines(db); err != nil {
return err
}
if err := generateCronJobs(db); err != nil {
return err
}
if err := generateScripts(db); err != nil {
return err
}
if err := generateApprovalTemplates(db); err != nil {
return err
}
if err := generateNavigations(db); err != nil {
return err
}
return nil
}
func generateUsers(db *gorm.DB) error {
users := []models.User{
{Username: "admin", Email: "admin@ezops.local", RealName: "系统管理员", Phone: "13800138000", Status: 1, IsAdmin: true},
{Username: "ops01", Email: "ops01@ezops.local", RealName: "运维工程师1", Phone: "13800138001", Status: 1, IsAdmin: false},
{Username: "ops02", Email: "ops02@ezops.local", RealName: "运维工程师2", Phone: "13800138002", Status: 1, IsAdmin: false},
{Username: "dev01", Email: "dev01@ezops.local", RealName: "开发工程师1", Phone: "13800138003", Status: 1, IsAdmin: false},
{Username: "dev02", Email: "dev02@ezops.local", RealName: "开发工程师2", Phone: "13800138004", Status: 0, IsAdmin: false},
}
password, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
for i := range users {
users[i].Password = string(password)
if err := db.Create(&users[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated users:", len(users))
return nil
}
func generateServers(db *gorm.DB) error {
osTypes := []string{"CentOS 7.9", "CentOS 8.5", "Ubuntu 20.04", "Ubuntu 22.04", "Debian 11", "Rocky Linux 8.6"}
statuses := []string{"running", "stopped", "maintenance"}
servers := make([]models.Server, 0, 50)
for i := 0; i < 50; i++ {
server := models.Server{
Hostname: fmt.Sprintf("server-%03d", i+1),
IP: fmt.Sprintf("10.0.%d.%d", rand.Intn(256), rand.Intn(256)),
PublicIP: fmt.Sprintf("47.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256)),
OS: osTypes[rand.Intn(len(osTypes))],
CPU: []int{2, 4, 8, 16, 32}[rand.Intn(5)],
Memory: []int64{4096, 8192, 16384, 32768, 65536, 131072}[rand.Intn(6)],
Disk: []int64{100, 200, 500, 1000}[rand.Intn(4)],
Status: statuses[rand.Intn(len(statuses))],
CPUUsage: rand.Float64() * 80,
MemoryUsage: rand.Float64() * 80,
DiskUsage: rand.Float64() * 70,
Tags: fmt.Sprintf("{\"project\":\"project%d\",\"team\":\"team%d\"}", rand.Intn(10), rand.Intn(5)),
Description: fmt.Sprintf("服务器 %d 描述", i+1),
}
servers = append(servers, server)
}
if err := db.Create(&servers).Error; err != nil {
return err
}
fmt.Println("Generated servers:", len(servers))
return nil
}
func generateCloudAccounts(db *gorm.DB) error {
accounts := []models.CloudAccount{
{Name: "阿里云主账号", Provider: "aliyun", Status: "active"},
{Name: "腾讯云主账号", Provider: "tencent", Status: "active"},
{Name: "AWS 账号", Provider: "aws", Status: "active"},
{Name: "华为云账号", Provider: "huawei", Status: "inactive"},
}
for i := range accounts {
accounts[i].AccessKey = "LTAI" + randomString(16)
accounts[i].SecretKey = randomString(32)
if err := db.Create(&accounts[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated cloud accounts:", len(accounts))
return nil
}
func generateDomains(db *gorm.DB) error {
domains := []models.Domain{
{Name: "ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns"},
{Name: "api.ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns"},
{Name: "app.ezops.local", Registrar: "tencent", Status: "active", DNSProvider: "dnspod"},
{Name: "monitor.ezops.local", Registrar: "aliyun", Status: "active", DNSProvider: "alidns", SSLEnabled: true},
}
for i := range domains {
expireDays := rand.Intn(365) + 30
expireDate := time.Now().AddDate(0, 0, expireDays)
domains[i].ExpireDate = &expireDate
domains[i].SSLExpireDate = &expireDate
if err := db.Create(&domains[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated domains:", len(domains))
return nil
}
func generateK8sClusters(db *gorm.DB) error {
clusters := []models.K8sCluster{
{Name: "prod-cluster", Code: "prod", Version: "v1.28.0", Status: "healthy", NodeCount: 10, PodCount: 150, ServiceCount: 30, APIEndpoint: "https://prod-api.ezops.local:6443", CPUUsage: 65.5, MemoryUsage: 72.3, Description: "生产环境 Kubernetes 集群"},
{Name: "staging-cluster", Code: "staging", Version: "v1.28.0", Status: "healthy", NodeCount: 5, PodCount: 80, ServiceCount: 15, APIEndpoint: "https://staging-api.ezops.local:6443", CPUUsage: 45.2, MemoryUsage: 55.0, Description: "预发环境 Kubernetes 集群"},
{Name: "dev-cluster", Code: "dev", Version: "v1.27.0", Status: "warning", NodeCount: 3, PodCount: 45, ServiceCount: 8, APIEndpoint: "https://dev-api.ezops.local:6443", CPUUsage: 80.1, MemoryUsage: 85.0, Description: "开发环境 Kubernetes 集群"},
{Name: "test-cluster", Code: "test", Version: "v1.27.0", Status: "error", NodeCount: 2, PodCount: 20, ServiceCount: 5, APIEndpoint: "https://test-api.ezops.local:6443", CPUUsage: 30.0, MemoryUsage: 40.0, Description: "测试环境 Kubernetes 集群"},
}
for i := range clusters {
if err := db.Create(&clusters[i]).Error; err != nil {
return err
}
}
for _, cluster := range clusters {
for j := 0; j < cluster.NodeCount; j++ {
node := models.K8sNode{
ClusterID: cluster.ID,
Name: fmt.Sprintf("%s-node-%d", cluster.Code, j+1),
IP: fmt.Sprintf("192.168.%d.%d", rand.Intn(256), rand.Intn(256)),
Status: []string{"Ready", "Ready", "Ready", "NotReady"}[rand.Intn(4)],
Role: []string{"master", "worker", "worker"}[rand.Intn(3)],
CPU: []int{4, 8, 16}[rand.Intn(3)],
Memory: []int64{16384, 32768, 65536}[rand.Intn(3)],
Disk: []int64{100, 200, 500}[rand.Intn(3)],
CPUUsage: rand.Float64() * 80,
MemoryUsage: rand.Float64() * 80,
DiskUsage: rand.Float64() * 70,
PodCount: rand.Intn(30) + 5,
MaxPods: 110,
OS: "Ubuntu 22.04.3 LTS",
KubeletVer: cluster.Version,
}
if err := db.Create(&node).Error; err != nil {
return err
}
}
}
fmt.Println("Generated K8s clusters:", len(clusters))
return nil
}
func generateAlertRules(db *gorm.DB) error {
rules := []models.AlertRule{
{Name: "CPU 使用率告警", Type: "metric", Source: "prometheus", Expression: "cpu_usage > 80", Severity: "warning", Duration: 300, Enabled: true, Description: "CPU 使用率超过 80%"},
{Name: "CPU 使用率严重告警", Type: "metric", Source: "prometheus", Expression: "cpu_usage > 95", Severity: "critical", Duration: 120, Enabled: true, Description: "CPU 使用率超过 95%"},
{Name: "内存使用率告警", Type: "metric", Source: "prometheus", Expression: "memory_usage > 80", Severity: "warning", Duration: 300, Enabled: true, Description: "内存使用率超过 80%"},
{Name: "磁盘使用率告警", Type: "metric", Source: "prometheus", Expression: "disk_usage > 80", Severity: "warning", Duration: 600, Enabled: true, Description: "磁盘使用率超过 80%"},
{Name: "服务不可用告警", Type: "event", Source: "custom", Expression: "service_up == 0", Severity: "critical", Duration: 60, Enabled: true, Description: "服务不可用"},
{Name: "Pod 重启告警", Type: "event", Source: "kubernetes", Expression: "pod_restart > 5", Severity: "warning", Duration: 600, Enabled: true, Description: "Pod 重启次数超过 5 次"},
}
for i := range rules {
if err := db.Create(&rules[i]).Error; err != nil {
return err
}
}
for i := 0; i < 20; i++ {
event := models.AlertEvent{
RuleID: rules[rand.Intn(len(rules))].ID,
RuleName: rules[rand.Intn(len(rules))].Name,
Severity: []string{"info", "warning", "critical"}[rand.Intn(3)],
Status: []string{"firing", "resolved"}[rand.Intn(2)],
Target: fmt.Sprintf("server-%03d", rand.Intn(50)+1),
Message: fmt.Sprintf("告警事件 %d", i+1),
Value: rand.Float64() * 100,
StartedAt: time.Now().Add(-time.Duration(rand.Intn(72)) * time.Hour),
}
if err := db.Create(&event).Error; err != nil {
return err
}
}
fmt.Println("Generated alert rules:", len(rules))
return nil
}
func generatePipelines(db *gorm.DB) error {
pipelines := []models.Pipeline{
{Name: "前端构建", Code: "frontend-build", Type: "build", Source: "https://gitlab.com/ezops/frontend", Branch: "main", Enabled: true, Description: "前端项目构建流水线"},
{Name: "后端构建", Code: "backend-build", Type: "build", Source: "https://gitlab.com/ezops/backend", Branch: "main", Enabled: true, Description: "后端项目构建流水线"},
{Name: "部署预发", Code: "deploy-staging", Type: "deploy", Source: "https://gitlab.com/ezops/deploy", Branch: "staging", Enabled: true, Description: "部署到预发环境"},
{Name: "部署生产", Code: "deploy-production", Type: "deploy", Source: "https://gitlab.com/ezops/deploy", Branch: "main", Enabled: true, Description: "部署到生产环境"},
{Name: "集成测试", Code: "integration-test", Type: "test", Source: "https://gitlab.com/ezops/tests", Branch: "main", Enabled: true, Description: "集成测试流水线"},
}
for i := range pipelines {
if err := db.Create(&pipelines[i]).Error; err != nil {
return err
}
}
for _, pipeline := range pipelines {
for j := 0; j < 5; j++ {
run := models.PipelineRun{
PipelineID: pipeline.ID,
BuildNumber: j + 1,
Status: []string{"success", "success", "success", "failed", "running"}[rand.Intn(5)],
Branch: pipeline.Branch,
CommitID: randomString(8),
CommitMsg: "Update " + randomString(6),
Author: []string{"admin", "ops01", "dev01"}[rand.Intn(3)],
TriggeredBy: []string{"admin", "ops01", "dev01"}[rand.Intn(3)],
}
if err := db.Create(&run).Error; err != nil {
return err
}
}
}
fmt.Println("Generated pipelines:", len(pipelines))
return nil
}
func generateCronJobs(db *gorm.DB) error {
cronJobs := []models.CronJob{
{Name: "数据库备份", CronExpr: "0 2 * * *", Type: "script", Content: "/scripts/backup-db.sh", Enabled: true, Description: "每日凌晨 2 点备份数据库"},
{Name: "清理日志", CronExpr: "0 3 * * 0", Type: "command", Content: "find /var/log -mtime +30 -delete", Enabled: true, Description: "每周日凌晨 3 点清理日志"},
{Name: "数据同步", CronExpr: "*/30 * * * *", Type: "script", Content: "/scripts/sync-data.sh", Enabled: true, Description: "每 30 分钟同步数据"},
{Name: "健康检查", CronExpr: "*/5 * * * *", Type: "command", Content: "curl -sf http://localhost:8080/health", Enabled: true, Description: "每 5 分钟健康检查"},
{Name: "生成报表", CronExpr: "0 9 * * 1", Type: "script", Content: "/scripts/generate-report.sh", Enabled: false, Description: "每周一上午 9 点生成报表"},
}
for i := range cronJobs {
if err := db.Create(&cronJobs[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated cron jobs:", len(cronJobs))
return nil
}
func generateScripts(db *gorm.DB) error {
scripts := []models.Script{
{Name: "重启服务", Code: "restart-service", Type: "shell", Content: "#!/bin/bash\nsystemctl restart $1", Description: "重启服务脚本"},
{Name: "检查磁盘", Code: "check-disk", Type: "shell", Content: "#!/bin/bash\ndf -h", Description: "检查磁盘空间"},
{Name: "检查内存", Code: "check-memory", Type: "shell", Content: "#!/bin/bash\nfree -h", Description: "检查内存使用"},
{Name: "部署应用", Code: "deploy-app", Type: "python", Content: "#!/usr/bin/env python3\nimport sys\nprint(f'Deploying {sys.argv[1]}')", Description: "部署应用脚本"},
{Name: "备份配置", Code: "backup-config", Type: "shell", Content: "#!/bin/bash\ntar -czf /backup/config-$(date +%Y%m%d).tar.gz /etc/ezops", Description: "备份配置文件"},
}
for i := range scripts {
if err := db.Create(&scripts[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated scripts:", len(scripts))
return nil
}
func generateApprovalTemplates(db *gorm.DB) error {
templates := []models.ApprovalTemplate{
{Name: "服务器申请审批", Code: "server-request", Type: "server", Enabled: true, Description: "服务器资源申请审批流程"},
{Name: "发布审批", Code: "release-approval", Type: "release", Enabled: true, Description: "应用发布审批流程"},
{Name: "权限申请审批", Code: "permission-request", Type: "permission", Enabled: true, Description: "系统权限申请审批流程"},
}
for i := range templates {
if err := db.Create(&templates[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated approval templates:", len(templates))
return nil
}
func generateNavigations(db *gorm.DB) error {
groups := []models.NavigationGroup{
{Name: "常用工具", Code: "tools", Icon: "Tool", Sort: 1, Enabled: true},
{Name: "监控平台", Code: "monitor", Icon: "Activity", Sort: 2, Enabled: true},
{Name: "开发工具", Code: "devtools", Icon: "Code", Sort: 3, Enabled: true},
{Name: "文档中心", Code: "docs", Icon: "FileText", Sort: 4, Enabled: true},
}
for i := range groups {
if err := db.Create(&groups[i]).Error; err != nil {
return err
}
}
links := []models.NavigationLink{
{GroupID: groups[0].ID, Name: "GitLab", URL: "https://gitlab.com", Icon: "GitBranch", Description: "代码仓库", Sort: 1},
{GroupID: groups[0].ID, Name: "Jenkins", URL: "https://jenkins.io", Icon: "Settings", Description: "CI/CD 工具", Sort: 2},
{GroupID: groups[0].ID, Name: "Nexus", URL: "https://www.sonatype.com/nexus-repository-oss", Icon: "Package", Description: "制品库", Sort: 3},
{GroupID: groups[1].ID, Name: "Prometheus", URL: "https://prometheus.io", Icon: "Activity", Description: "监控系统", Sort: 1},
{GroupID: groups[1].ID, Name: "Grafana", URL: "https://grafana.com", Icon: "BarChart", Description: "可视化平台", Sort: 2},
{GroupID: groups[1].ID, Name: "ELK", URL: "https://www.elastic.co/what-is/elk-stack", Icon: "Search", Description: "日志分析", Sort: 3},
{GroupID: groups[2].ID, Name: "Swagger", URL: "https://swagger.io", Icon: "FileCode", Description: "API 文档", Sort: 1},
{GroupID: groups[2].ID, Name: "Postman", URL: "https://www.postman.com", Icon: "Send", Description: "API 测试", Sort: 2},
{GroupID: groups[3].ID, Name: "Confluence", URL: "https://www.atlassian.com/software/confluence", Icon: "BookOpen", Description: "知识库", Sort: 1},
}
for i := range links {
if err := db.Create(&links[i]).Error; err != nil {
return err
}
}
fmt.Println("Generated navigation groups:", len(groups))
fmt.Println("Generated navigation links:", len(links))
return nil
}
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}