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:
commit
7d6674bddd
156 files changed
+45704
No files matched your search
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 # 异步写入
|
||||
@@ -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
|
||||
@@ -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
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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: "删除成功",
|
||||
})
|
||||
}
|
||||
@@ -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: "删除成功",
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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{},
|
||||
)
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user