first commit

This commit is contained in:
zhangtao 2025-08-01 16:38:08 +08:00
commit d9ece60bff
73 changed files with 17483 additions and 0 deletions

34
.editorconfig Normal file
View File

@ -0,0 +1,34 @@
# EditorConfig is awesome: https://EditorConfig.org
# 顶层配置文件
root = true
# 所有文件通用配置
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# Go 文件配置
[*.go]
indent_size = 4
indent_style = tab
# Markdown 文件配置
[*.md]
trim_trailing_whitespace = false
# YAML 文件配置
[*.{yml,yaml}]
indent_size = 2
# JSON 文件配置
[*.json]
indent_size = 2
# Shell 脚本配置
[*.sh]
end_of_line = lf

12
.env Normal file
View File

@ -0,0 +1,12 @@
PORT=3000
MYSQL_DSN="root:200967tao@tcp(www.suyun.store:3306)/ego?charset=utf8mb4&parseTime=True&loc=Local"
REDIS_ADDR="www.suyun.store:6379"
REDIS_PW="tao200967"
REDIS_DB="0"
REDIS_POOL_SIZE=20
REDIS_READ_TIMEOUT=3s
JWT_SECRET="YouOnlyLiveOnce"
GIN_MODE="test"
LOG_LEVEL="info"
LOG_PATH="./logs/"
CORS_ALLOWED_ORIGINS="http://www.unbug.cn:3000,http://127.0.0.1:3000"

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
PORT=3000
MYSQL_DSN="root:200967tao@tcp(192.168.1.12:3306)/ego?charset=utf8mb4&parseTime=True&loc=Local"
REDIS_ADDR="192.168.1.12:6379"
REDIS_PW="200967tao"
REDIS_DB="0"
REDIS_POOL_SIZE=20
REDIS_READ_TIMEOUT=3s
JWT_SECRET="YouOnlyLiveOnce"
GIN_MODE="test"
LOG_LEVEL="info"
LOG_PATH="./logs/"
CORS_ALLOWED_ORIGINS="http://localhost:3000,http://127.0.0.1:8080"

0
.gitignore vendored Normal file
View File

0
.idea/.gitignore vendored Normal file
View File

9
.idea/DeployHelper.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

207
.idea/misc.xml Normal file
View File

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CodeInsightWorkspaceSettings">
<option name="optimizeImportsOnTheFly" value="true" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="mavenHome" value="D:/SoftWare/apache-maven-3.5.0/apache-maven-3.5.0" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State />
<State>
<id>Android</id>
</State>
<State>
<id>Bitwise operation issuesJava</id>
</State>
<State>
<id>Bitwise operation issuesJavaScript and TypeScript</id>
</State>
<State>
<id>Class structureJava</id>
</State>
<State>
<id>Code maturityJava</id>
</State>
<State>
<id>Code style issuesJava</id>
</State>
<State>
<id>CodeSpring CoreSpring</id>
</State>
<State>
<id>Compiler issuesJava</id>
</State>
<State>
<id>Control flow issuesJava</id>
</State>
<State>
<id>CorrectnessLintAndroid</id>
</State>
<State>
<id>Cucumber</id>
</State>
<State>
<id>FinalizationJava</id>
</State>
<State>
<id>GeneralJavaScript and TypeScript</id>
</State>
<State>
<id>Groovy</id>
</State>
<State>
<id>HTTP Client</id>
</State>
<State>
<id>IconsUsabilityLintAndroid</id>
</State>
<State>
<id>ImportsJava</id>
</State>
<State>
<id>Inheritance issuesJava</id>
</State>
<State>
<id>InternationalizationLintAndroid</id>
</State>
<State>
<id>InteroperabilityLintAndroid</id>
</State>
<State>
<id>JUnitJava</id>
</State>
<State>
<id>JVM languages</id>
</State>
<State>
<id>Java</id>
</State>
<State>
<id>Java 14Java language level migration aidsJava</id>
</State>
<State>
<id>Java 15Java language level migration aidsJava</id>
</State>
<State>
<id>Java 5Java language level migration aidsJava</id>
</State>
<State>
<id>Java 7Java language level migration aidsJava</id>
</State>
<State>
<id>Java 8Java language level migration aidsJava</id>
</State>
<State>
<id>Java 9Java language level migration aidsJava</id>
</State>
<State>
<id>Java language level migration aidsJava</id>
</State>
<State>
<id>JavaScript and TypeScript</id>
</State>
<State>
<id>JavadocJava</id>
</State>
<State>
<id>Kotlin</id>
</State>
<State>
<id>LintAndroid</id>
</State>
<State>
<id>MigrationKotlin</id>
</State>
<State>
<id>Numeric issuesJava</id>
</State>
<State>
<id>Other problemsKotlin</id>
</State>
<State>
<id>PerformanceJava</id>
</State>
<State>
<id>PerformanceLintAndroid</id>
</State>
<State>
<id>Potentially confusing code constructsGroovy</id>
</State>
<State>
<id>Potentially confusing code constructsJavaScript and TypeScript</id>
</State>
<State>
<id>Probable bugsJava</id>
</State>
<State>
<id>Probable bugsKotlin</id>
</State>
<State>
<id>Reactive Streams</id>
</State>
<State>
<id>ReactorReactive Streams</id>
</State>
<State>
<id>Redundant constructsKotlin</id>
</State>
<State>
<id>Resource managementJava</id>
</State>
<State>
<id>SQL</id>
</State>
<State>
<id>SecurityLintAndroid</id>
</State>
<State>
<id>Spring</id>
</State>
<State>
<id>Spring AOPSpring</id>
</State>
<State>
<id>Spring BootSpring</id>
</State>
<State>
<id>Spring CoreSpring</id>
</State>
<State>
<id>Style issuesKotlin</id>
</State>
<State>
<id>Test frameworksJVM languages</id>
</State>
<State>
<id>TestNGJava</id>
</State>
<State>
<id>Threading issuesJava</id>
</State>
<State>
<id>UI form</id>
</State>
<State>
<id>UsabilityLintAndroid</id>
</State>
<State>
<id>XMLSpring CoreSpring</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>Android</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DeployHelper.iml" filepath="$PROJECT_DIR$/.idea/DeployHelper.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

84
.idea/workspace.xml Normal file
View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="48503f58-7b9d-42e5-b962-e07f79b5cec9" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="GOROOT" url="file://$USER_HOME$/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.5.windows-amd64" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 5
}</component>
<component name="ProjectId" id="30g0BlhEax2aR8AzLfMyUWJcy8p" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Go Build.go build ego/cmd/ego.executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"git-widget-placeholder": "master",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "E:/home/DeployHelper",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "shared-indexes",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration name="go build ego/cmd/ego" type="GoApplicationRunConfiguration" factoryName="Go Application" temporary="true" nameIsGenerated="true">
<module name="DeployHelper" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="ego/cmd/ego" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/cmd/ego/main.go" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Go Build.go build ego/cmd/ego" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-gosdk-3b128438d3f6-07d2d2d66b1e-org.jetbrains.plugins.go.sharedIndexes.bundled-GO-251.27812.54" />
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-GO-251.27812.54" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="48503f58-7b9d-42e5-b962-e07f79b5cec9" name="Changes" comment="" />
<created>1754033774462</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1754033774462</updated>
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>

183
Makefile Normal file
View File

@ -0,0 +1,183 @@
.PHONY: build run test clean docker-build docker-run docker-compose-up docker-compose-down wire docs fmt lint deps dev-setup build-all install-tools check-env mod-tidy
# 构建变量
BINARY_NAME=ego
MAIN_FILE=cmd/ego/main.go
DOCKER_IMAGE=ego:latest
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
# Go版本检查
GO_VERSION := $(shell go version | cut -d ' ' -f 3 | sed 's/go//')
REQUIRED_GO_VERSION := 1.21
# 默认目标
.DEFAULT_GOAL := help
# 帮助信息
help: ## 显示帮助信息
@echo "可用的命令:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
# 环境检查
check-env: ## 检查开发环境
@echo "检查Go版本..."
@if [ "$(shell printf '%s\n' $(REQUIRED_GO_VERSION) $(GO_VERSION) | sort -V | head -n1)" != "$(REQUIRED_GO_VERSION)" ]; then \
echo "错误: 需要Go $(REQUIRED_GO_VERSION)或更高版本,当前版本: $(GO_VERSION)"; \
exit 1; \
fi
@echo "✓ Go版本检查通过: $(GO_VERSION)"
@echo "检查必要工具..."
@command -v git >/dev/null 2>&1 || { echo "错误: 需要安装git"; exit 1; }
@echo "✓ 环境检查完成"
# 构建
build: check-env ## 构建应用程序
@echo "构建 $(BINARY_NAME)..."
@go build -ldflags="-w -s" -o $(BINARY_NAME) ./$(MAIN_FILE)
@echo "✓ 构建完成: $(BINARY_NAME)"
# 运行
run: check-env ## 运行应用程序
@echo "运行应用程序..."
@go run ./$(MAIN_FILE)
# 开发模式运行
dev: check-env ## 开发模式运行(调试模式)
@echo "以开发模式运行..."
@GIN_MODE=debug LOG_LEVEL=debug go run ./$(MAIN_FILE)
# 测试
test: ## 运行测试
@echo "运行测试..."
@go test -v -race -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
@echo "✓ 测试完成,覆盖率报告: coverage.html"
# 基准测试
bench: ## 运行基准测试
@echo "运行基准测试..."
@go test -bench=. -benchmem ./...
# 清理
clean: ## 清理构建文件
@echo "清理构建文件..."
@go clean
@rm -f $(BINARY_NAME)
@rm -f cmd/ego/$(BINARY_NAME)
@rm -f coverage.out coverage.html
@rm -rf dist/
@echo "✓ 清理完成"
# 整理依赖
mod-tidy: ## 整理Go模块依赖
@echo "整理依赖..."
@go mod tidy
@go mod verify
@echo "✓ 依赖整理完成"
# Docker 构建
docker-build: ## 构建Docker镜像
@echo "构建Docker镜像..."
@docker build -f deployments/Dockerfile -t $(DOCKER_IMAGE) .
@echo "✓ Docker镜像构建完成: $(DOCKER_IMAGE)"
# Docker 运行
docker-run: ## 运行Docker容器
@echo "运行Docker容器..."
@docker run -p 3000:3000 --env-file .env $(DOCKER_IMAGE)
# Docker Compose 启动
docker-compose-up: ## 使用Docker Compose启动服务
@echo "启动Docker Compose服务..."
@docker-compose -f deployments/docker-compose.yml up -d
@echo "✓ 服务已启动"
# Docker Compose 停止
docker-compose-down: ## 停止Docker Compose服务
@echo "停止Docker Compose服务..."
@docker-compose -f deployments/docker-compose.yml down
@echo "✓ 服务已停止"
# 生成 wire 依赖注入代码
wire: ## 生成依赖注入代码
@echo "生成Wire依赖注入代码..."
@cd internal/wire && wire
@echo "✓ Wire代码生成完成"
# 生成 API 文档
docs: ## 生成API文档
@echo "生成API文档..."
@swag init -g $(MAIN_FILE) -o ./api --parseDependency --parseInternal
@echo "✓ API文档生成完成"
# 安装依赖
deps: check-env ## 下载依赖包
@echo "下载依赖包..."
@go mod download
@go mod verify
@echo "✓ 依赖下载完成"
# 代码格式化
fmt: ## 格式化代码
@echo "格式化代码..."
@go fmt ./...
@gofmt -w .
@echo "✓ 代码格式化完成"
# 代码检查
lint: ## 代码静态检查
@echo "运行代码检查..."
@golangci-lint run --timeout=5m
@echo "✓ 代码检查完成"
# 安全检查
security: ## 运行安全检查
@echo "运行安全检查..."
@govulncheck ./...
@echo "✓ 安全检查完成"
# 开发环境设置
dev-setup: check-env ## 设置开发环境
@echo "设置开发环境..."
@if [ ! -f "configs/config.yaml" ]; then \
cp "configs/config.yaml.example" "configs/config.yaml"; \
echo "✓ 配置文件已创建: configs/config.yaml"; \
fi
@if [ ! -f ".env" ]; then \
cp ".env.example" ".env" 2>/dev/null || echo "# 请根据需要设置环境变量" > .env; \
echo "✓ 环境变量文件已创建: .env"; \
fi
@mkdir -p logs
@echo "✓ 日志目录已创建: logs/"
@$(MAKE) deps
@$(MAKE) install-tools
@$(MAKE) wire
@echo "✓ 开发环境设置完成!"
# 构建所有平台
build-all: check-env ## 构建所有平台的二进制文件
@echo "构建所有平台..."
@mkdir -p dist
@GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o dist/$(BINARY_NAME)-linux-amd64 ./$(MAIN_FILE)
@GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o dist/$(BINARY_NAME)-windows-amd64.exe ./$(MAIN_FILE)
@GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o dist/$(BINARY_NAME)-darwin-amd64 ./$(MAIN_FILE)
@GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o dist/$(BINARY_NAME)-darwin-arm64 ./$(MAIN_FILE)
@echo "✓ 所有平台构建完成,文件位于 dist/ 目录"
# 安装开发工具
install-tools: ## 安装开发工具
@echo "安装开发工具..."
@go install github.com/swaggo/swag/cmd/swag@latest
@go install github.com/google/wire/cmd/wire@latest
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@go install golang.org/x/vuln/cmd/govulncheck@latest
@echo "✓ 开发工具安装完成"
# 运行所有检查
check-all: fmt lint test security ## 运行所有代码质量检查
@echo "✓ 所有检查完成"
# 发布准备
release-prep: check-all build-all docs ## 准备发布版本
@echo "✓ 发布准备完成"

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

4345
api/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2742
api/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
bin/ego Normal file

Binary file not shown.

128
cmd/ego/main.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"context"
"ego/internal/conf"
"ego/internal/router"
"ego/pkg/logger"
"errors"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// @title EGO API
// @version 1.0
// @description EGO 系统 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 127.0.0.1:3000
// @BasePath /
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
// 从配置文件读取配置
if err := conf.Init(); err != nil {
logger.Error(nil, "配置初始化失败", zap.Error(err))
os.Exit(1)
}
// 设置 gin 模式
ginMode := getGinMode()
gin.SetMode(ginMode)
logger.Info(nil, "Gin模式设置完成", zap.String("mode", ginMode))
// 创建路由
engine := router.NewRouter()
// 获取服务端口
port := getServerPort()
// 创建HTTP服务器
server := &http.Server{
Addr: port,
Handler: engine,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 在goroutine中启动服务器
go func() {
logger.Info(nil, "服务器启动中...", zap.String("addr", port))
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error(nil, "服务器启动失败", zap.Error(err))
os.Exit(1)
}
}()
logger.Info(nil, "服务器启动成功", zap.String("addr", port))
// 等待中断信号来优雅地关闭服务器
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info(nil, "收到关闭信号,开始优雅关闭服务器...")
// 创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 优雅关闭HTTP服务器
if err := server.Shutdown(ctx); err != nil {
logger.Error(nil, "服务器强制关闭", zap.Error(err))
} else {
logger.Info(nil, "HTTP服务器已优雅关闭")
}
// 关闭其他资源
conf.Close()
logger.Info(nil, "服务器关闭完成")
}
// getGinMode 获取Gin运行模式
func getGinMode() string {
mode := os.Getenv("GIN_MODE")
if mode == "" {
mode = gin.ReleaseMode // 默认为生产模式
}
// 验证模式是否有效
switch mode {
case gin.DebugMode, gin.ReleaseMode, gin.TestMode:
return mode
default:
logger.Warn(nil, "无效的GIN_MODE使用默认值",
zap.String("invalid_mode", mode),
zap.String("default_mode", gin.ReleaseMode))
return gin.ReleaseMode
}
}
// getServerPort 获取服务器端口
func getServerPort() string {
port := os.Getenv("SERVER_PORT")
if port == "" {
port = ":3000" // 默认端口
}
// 确保端口格式正确
if port[0] != ':' {
port = ":" + port
}
return port
}

View File

@ -0,0 +1,33 @@
# EGO 应用配置示例文件
# 复制此文件为 config.yaml 并根据实际情况修改配置
server:
port: ":3000"
mode: "release" # debug, release, test
database:
host: "localhost"
port: 3306
user: "your_username"
password: "your_password"
dbname: "ego_db"
charset: "utf8mb4"
parse_time: true
loc: "Local"
redis:
host: "localhost"
port: 6379
password: ""
db: 0
jwt:
secret: "your_jwt_secret_key"
expire_time: 24h
log:
level: "info" # debug, info, warn, error
file_path: "logs/app.log"
max_size: 100 # MB
max_age: 30 # days
max_backups: 5

41
deployments/Dockerfile Normal file
View File

@ -0,0 +1,41 @@
# 构建阶段
FROM golang:1.23-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装必要的包
RUN apk add --no-cache git ca-certificates tzdata
# 复制go mod文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用程序
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ego ./cmd/ego
# 运行阶段
FROM alpine:latest
# 安装ca证书和时区数据
RUN apk --no-cache add ca-certificates tzdata
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/ego .
# 创建必要的目录
RUN mkdir -p logs configs
# 暴露端口
EXPOSE 3000
# 运行应用程序
CMD ["./ego"]

View File

@ -0,0 +1,53 @@
services:
ego-app:
build:
context: ..
dockerfile: deployments/Dockerfile
ports:
- "3000:3000"
environment:
- GIN_MODE=release
depends_on:
- mysql
- redis
volumes:
- ../configs:/app/configs
- ../logs:/app/logs
networks:
- ego-network
mysql:
image: mysql:8.0
container_name: ego-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ego_db
MYSQL_USER: ego_user
MYSQL_PASSWORD: ego_password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ../sql:/docker-entrypoint-initdb.d
networks:
- ego-network
redis:
image: redis:7-alpine
container_name: ego-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- ego-network
volumes:
mysql_data:
redis_data:
networks:
ego-network:
driver: bridge

4369
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

76
go.mod Normal file
View File

@ -0,0 +1,76 @@
module ego
go 1.23.0
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-playground/validator/v10 v10.27.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/uuid v1.6.0
github.com/google/wire v0.6.0
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/redis/go-redis/v9 v9.11.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.40.0
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.30.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/strftime v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // 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.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.19.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

265
go.sum Normal file
View File

@ -0,0 +1,265 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg=
github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

194
internal/cache/cache.go vendored Normal file
View File

@ -0,0 +1,194 @@
package cache
import (
"context"
"ego/pkg/logger"
"fmt"
"os"
"strconv"
"sync"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
var (
RedisClient *redis.Client
once sync.Once
initErr error
)
// RedisConfig Redis连接配置
type RedisConfig struct {
Addr string `env:"REDIS_ADDR"`
Password string `env:"REDIS_PW"`
DB int `env:"REDIS_DB"`
MaxRetries int `env:"REDIS_RETRIES"`
PoolSize int `env:"REDIS_POOL_SIZE"`
ReadTimeout time.Duration `env:"REDIS_READ_TIMEOUT"`
}
// Redis 初始化Redis连接
func Redis() error {
once.Do(func() {
initErr = initRedis()
})
return initErr
}
// initRedis 实际的Redis初始化逻辑
func initRedis() error {
config, err := loadRedisConfig()
if err != nil {
logger.Error(nil, "Redis配置加载失败", zap.Error(err))
return fmt.Errorf("redis配置加载失败: %w", err)
}
// 创建客户端
client := redis.NewClient(&redis.Options{
Addr: config.Addr,
Password: config.Password,
DB: config.DB,
MaxRetries: config.MaxRetries,
PoolSize: config.PoolSize,
ReadTimeout: config.ReadTimeout,
WriteTimeout: config.ReadTimeout * 2,
// 添加更多配置选项
DialTimeout: 5 * time.Second,
PoolTimeout: 4 * time.Second,
MinIdleConns: 2,
})
// 验证连接
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
logger.Error(nil, "Redis连接失败",
zap.Error(err),
zap.String("addr", config.Addr),
zap.Int("db", config.DB))
return fmt.Errorf("Redis连接失败: %w", err)
}
RedisClient = client
logger.Info(nil, "Redis连接成功",
zap.String("address", config.Addr),
zap.Int("db", config.DB),
zap.Int("pool_size", config.PoolSize),
zap.Duration("read_timeout", config.ReadTimeout))
return nil
}
// Close 关闭Redis连接
func Close() error {
if RedisClient != nil {
err := RedisClient.Close()
if err != nil {
logger.Error(nil, "关闭Redis失败", zap.Error(err))
return fmt.Errorf("关闭Redis失败: %w", err)
}
logger.Info(nil, "Redis已关闭")
}
return nil
}
// GetClient 获取Redis客户端用于外部调用
func GetClient() (*redis.Client, error) {
if RedisClient == nil {
return nil, fmt.Errorf("Redis连接未初始化")
}
return RedisClient, nil
}
// IsConnected 检查Redis连接状态
func IsConnected() bool {
if RedisClient == nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return RedisClient.Ping(ctx).Err() == nil
}
// loadRedisConfig 从环境变量加载配置
func loadRedisConfig() (*RedisConfig, error) {
// 必要参数校验
addr := getEnvWithDefault("REDIS_ADDR", "localhost:6379")
dbStr := getEnvWithDefault("REDIS_DB", "0")
db, err := strconv.Atoi(dbStr)
if err != nil {
return nil, fmt.Errorf("无效的 REDIS_DB 值 '%s': %w", dbStr, err)
}
// 基础配置
config := &RedisConfig{
Addr: addr,
Password: os.Getenv("REDIS_PW"), // 密码可以为空
DB: db,
MaxRetries: 5,
PoolSize: 10,
ReadTimeout: 5 * time.Second,
}
// 读取扩展配置
if err := loadExtendedConfig(config); err != nil {
return nil, fmt.Errorf("加载扩展配置失败: %w", err)
}
return config, nil
}
// loadExtendedConfig 加载扩展配置
func loadExtendedConfig(config *RedisConfig) error {
// 连接池大小
if val := os.Getenv("REDIS_POOL_SIZE"); val != "" {
poolSize, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("无效的 REDIS_POOL_SIZE 值 '%s': %w", val, err)
}
if poolSize <= 0 {
return fmt.Errorf("REDIS_POOL_SIZE 必须大于 0")
}
config.PoolSize = poolSize
}
// 重试次数
if val := os.Getenv("REDIS_RETRIES"); val != "" {
retries, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("无效的 REDIS_RETRIES 值 '%s': %w", val, err)
}
if retries < 0 {
return fmt.Errorf("REDIS_RETRIES 不能小于 0")
}
config.MaxRetries = retries
}
// 读取超时
if val := os.Getenv("REDIS_READ_TIMEOUT"); val != "" {
timeout, err := time.ParseDuration(val)
if err != nil {
return fmt.Errorf("无效的 REDIS_READ_TIMEOUT 值 '%s': %w", val, err)
}
if timeout <= 0 {
return fmt.Errorf("REDIS_READ_TIMEOUT 必须大于 0")
}
config.ReadTimeout = timeout
}
return nil
}
// getEnvWithDefault 获取环境变量,如果不存在则返回默认值
func getEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

46
internal/conf/common.go Normal file
View File

@ -0,0 +1,46 @@
package conf
import (
conf "ego/internal/conf/locales"
"ego/internal/serializer"
"ego/pkg/logger"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Ping 状态检查页面
// @Summary Get a message
// @Description Get a message from the server
// @Produce json
// @Success 200 {object} map[string]string
// @Router /ping [get]
func Ping(c *gin.Context) {
logger.Info(c, "Ping", zap.Field{Key: "msg", Type: zapcore.StringType, String: "Ping"})
c.JSON(200, serializer.Succ("tong", nil))
}
// ErrorResponse 返回错误消息
func ErrorResponse(err error) serializer.Response {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
for _, e := range ve {
field := conf.T(fmt.Sprintf("Field.%s", e.Field()))
tag := conf.T(fmt.Sprintf("Tag.Valid.%s", e.Tag()))
return serializer.ParamErr(
fmt.Sprintf("%s%s", field, tag),
err,
)
}
}
var unmarshalTypeError *json.UnmarshalTypeError
if errors.As(err, &unmarshalTypeError) {
return serializer.ParamErr("JSON类型不匹配", err)
}
return serializer.ParamErr("参数错误", err)
}

116
internal/conf/conf.go Normal file
View File

@ -0,0 +1,116 @@
package conf
import (
"ego/internal/cache"
"ego/internal/conf/locales"
"ego/internal/util"
"ego/pkg/logger"
"fmt"
"os"
"github.com/joho/godotenv"
"go.uber.org/zap"
)
// Init 初始化所有配置
func Init() error {
// 1. 加载环境变量
if err := loadEnvironmentVariables(); err != nil {
// 这里不能使用logger因为还没初始化
return fmt.Errorf("环境变量加载失败: %w", err)
}
// 2. 初始化日志系统在验证环境变量之前因为后续步骤需要使用logger
if err := logger.BuildLogger(); err != nil {
// 这里不能使用logger因为初始化失败了
return fmt.Errorf("日志系统初始化失败: %w", err)
}
// 3. 验证必要环境变量
if err := validateRequiredEnvVars(); err != nil {
logger.Error(nil, "环境变量验证失败", zap.Error(err))
return fmt.Errorf("环境变量验证失败: %w", err)
}
// 4. 加载翻译文件
if err := locales.LoadLocales(); err != nil {
logger.Error(nil, "翻译文件加载失败", zap.Error(err))
return fmt.Errorf("翻译文件加载失败: %w", err)
}
// 5. 连接数据库
mysqlDSN := os.Getenv("MYSQL_DSN")
if err := Database(mysqlDSN); err != nil {
logger.Error(nil, "数据库初始化失败", zap.Error(err))
return fmt.Errorf("数据库初始化失败: %w", err)
}
// 6. 连接 Redis
if err := cache.Redis(); err != nil {
logger.Error(nil, "Redis初始化失败", zap.Error(err))
return fmt.Errorf("Redis初始化失败: %w", err)
}
// 7. 输出JWT密钥状态日志在logger初始化完成后
util.LogJwtKeyStatus()
logger.Info(nil, "所有配置初始化完成")
return nil
}
// Close 关闭所有资源
func Close() {
logger.Info(nil, "开始关闭应用程序资源...")
// 关闭数据库
if err := closeDatabase(); err != nil {
logger.Error(nil, "数据库关闭失败", zap.Error(err))
} else {
logger.Info(nil, "数据库已关闭")
}
// 关闭 Redis
if err := cache.Close(); err != nil {
logger.Error(nil, "Redis关闭失败", zap.Error(err))
} else {
logger.Info(nil, "Redis已关闭")
}
logger.Info(nil, "应用程序资源关闭完成")
}
// loadEnvironmentVariables 加载环境变量并处理错误
func loadEnvironmentVariables() error {
// 尝试加载.env文件如果文件不存在也不报错
if err := godotenv.Load(); err != nil {
// 只有在文件存在但读取失败时才报错
if !os.IsNotExist(err) {
return fmt.Errorf("加载 .env 文件失败: %w", err)
}
// .env文件不存在使用系统环境变量
logger.Info(nil, ".env 文件不存在,使用系统环境变量")
} else {
logger.Info(nil, "成功加载 .env 文件")
}
return nil
}
// validateRequiredEnvVars 验证必要的环境变量
func validateRequiredEnvVars() error {
requiredVars := map[string]string{
"MYSQL_DSN": "数据库连接字符串",
}
var missingVars []string
for envVar, description := range requiredVars {
if os.Getenv(envVar) == "" {
missingVars = append(missingVars, fmt.Sprintf("%s (%s)", envVar, description))
}
}
if len(missingVars) > 0 {
return fmt.Errorf("缺少必要环境变量: %v", missingVars)
}
return nil
}

181
internal/conf/db.go Normal file
View File

@ -0,0 +1,181 @@
package conf
import (
"database/sql"
"ego/pkg/logger"
"fmt"
"log"
"os"
"sync"
"time"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
glogger "gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var (
// Db 数据库链接单例
Db *gorm.DB
once sync.Once
dbInitErr error
)
// NewDb 初始化数据库连接
func NewDb() *gorm.DB {
return Db
}
// Database 初始化mysql连接
func Database(connString string) error {
once.Do(func() {
dbInitErr = initDatabase(connString)
})
return dbInitErr
}
// initDatabase 实际的数据库初始化逻辑
func initDatabase(connString string) error {
if connString == "" {
err := fmt.Errorf("数据库连接字符串不能为空")
logger.Error(nil, "数据库初始化失败", zap.Error(err))
return err
}
// 初始化GORM日志配置
logLevel := getGormLogLevel()
newLogger := glogger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
glogger.Config{
SlowThreshold: time.Second,
LogLevel: logLevel,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
)
// 配置 gorm
db, err := gorm.Open(mysql.Open(connString), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: newLogger,
NamingStrategy: schema.NamingStrategy{TablePrefix: ""},
})
if err != nil {
logger.Error(nil, "数据库连接失败", zap.Error(err), zap.String("connString", maskConnectionString(connString)))
return fmt.Errorf("数据库连接失败: %w", err)
}
// 获取底层的sql.DB对象
sqlDB, err := db.DB()
if err != nil {
logger.Error(nil, "获取数据库实例失败", zap.Error(err))
return fmt.Errorf("获取数据库实例失败: %w", err)
}
// 配置连接池
if err := configureConnectionPool(sqlDB); err != nil {
logger.Error(nil, "配置数据库连接池失败", zap.Error(err))
return fmt.Errorf("配置数据库连接池失败: %w", err)
}
// 测试数据库连接
if err := sqlDB.Ping(); err != nil {
logger.Error(nil, "数据库连接测试失败", zap.Error(err))
return fmt.Errorf("数据库连接测试失败: %w", err)
}
Db = db
logger.Info(nil, "数据库连接成功",
zap.String("connString", maskConnectionString(connString)),
zap.Int("maxOpenConns", 20),
zap.Int("maxIdleConns", 10))
return nil
}
// configureConnectionPool 配置数据库连接池
func configureConnectionPool(sqlDB *sql.DB) error {
// 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
// 设置打开数据库连接的最大数量
sqlDB.SetMaxOpenConns(20)
// 设置连接可复用的最大时间
sqlDB.SetConnMaxLifetime(time.Hour)
// 设置连接空闲的最大时间
sqlDB.SetConnMaxIdleTime(time.Minute * 30)
return nil
}
// getGormLogLevel 根据环境变量获取GORM日志级别
func getGormLogLevel() glogger.LogLevel {
switch os.Getenv("GORM_LOG_LEVEL") {
case "silent":
return glogger.Silent
case "error":
return glogger.Error
case "warn":
return glogger.Warn
case "info":
return glogger.Info
default:
// 根据应用环境决定默认级别
if os.Getenv("GIN_MODE") == "release" {
return glogger.Error
}
return glogger.Info
}
}
// maskConnectionString 遮盖连接字符串中的敏感信息
func maskConnectionString(connString string) string {
// 简单遮盖密码部分,实际项目中可以使用更复杂的正则表达式
return "***已遮盖敏感信息***"
}
// Migration 执行数据迁移
func Migration() error {
if Db == nil {
return fmt.Errorf("数据库连接未初始化")
}
// 自动迁移模式
models := []any{}
for _, model := range models {
if err := Db.AutoMigrate(model); err != nil {
logger.Error(nil, "数据库迁移失败",
zap.Error(err),
zap.String("model", fmt.Sprintf("%T", model)))
return fmt.Errorf("迁移模型 %T 失败: %w", model, err)
}
}
logger.Info(nil, "数据库迁移完成", zap.Int("models", len(models)))
return nil
}
// GetDB 获取数据库连接(用于外部调用)
func GetDB() (*gorm.DB, error) {
if Db == nil {
return nil, fmt.Errorf("数据库连接未初始化")
}
return Db, nil
}
// closeDatabase 安全关闭数据库连接
func closeDatabase() error {
if Db == nil {
return nil
}
sqlDB, err := Db.DB()
if err != nil {
return fmt.Errorf("获取数据库连接失败: %w", err)
}
return sqlDB.Close()
}

View File

@ -0,0 +1,60 @@
package locales
import (
_ "embed"
"strings"
"gopkg.in/yaml.v2"
)
//go:embed zh-cn.yaml
var Zhcn string
// Dictionary 字典
var Dictionary *map[any]any
// LoadLocales 读取国际化文件
func LoadLocales() error {
m := make(map[any]any)
err := yaml.Unmarshal([]byte(Zhcn), &m)
if err != nil {
return err
}
Dictionary = &m
return nil
}
// T 翻译
func T(key string) string {
dic := *Dictionary
keys := strings.Split(key, ".")
for index, path := range keys {
// 如果到达了最后一层,寻找目标翻译
if len(keys) == (index + 1) {
for k, v := range dic {
if k, ok := k.(string); ok {
if k == path {
if value, ok := v.(string); ok {
return value
}
}
}
}
return path
}
// 如果还有下一层,继续寻找
for k, v := range dic {
if ks, ok := k.(string); !ok {
return ""
} else if ks == path {
if dic, ok = v.(map[any]any); !ok {
return path
}
}
}
}
return ""
}

View File

@ -0,0 +1,11 @@
Tag:
required: "必须存在,而且不能为空"
min: "不够长"
max: "太长"
Field:
Name: "名称"
Nickname: "用户昵称"
UserName: "用户名"
PassWord: "密码"
PassWordConfirm: "密码校验"
Checked: "已选中"

17
internal/example_test.go Normal file
View File

@ -0,0 +1,17 @@
package test
import (
"testing"
)
// TestSetup 是测试设置函数
func TestSetup(t *testing.T) {
// 这里可以添加测试前的设置代码
t.Log("测试环境设置")
}
// TestTeardown 是测试清理函数
func TestTeardown(t *testing.T) {
// 这里可以添加测试后的清理代码
t.Log("测试环境清理")
}

View File

@ -0,0 +1,20 @@
package handler
import "github.com/gin-gonic/gin"
type BaseHandler interface {
// Create 创建
Create(c *gin.Context)
// DeleteByID 删除
DeleteByID(c *gin.Context)
// UpdateByID 更新
UpdateByID(c *gin.Context)
// GetByID 获取单个
GetByID(c *gin.Context)
// DeleteByIDs 批量删除
DeleteByIDs(c *gin.Context)
// GetByCondition 条件查询
GetByCondition(c *gin.Context)
// ListByIDs 批量查询
ListByIDs(c *gin.Context)
}

View File

@ -0,0 +1,92 @@
package handler
import (
"ego/internal/service"
"ego/pkg/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// SysDeployFileHandler 部署文件处理器
type SysDeployFileHandler struct {
deployFileService *service.SysDeployFileService
}
// NewSysDeployFileHandler 构建部署文件处理器
func NewSysDeployFileHandler(deployFileService *service.SysDeployFileService) *SysDeployFileHandler {
return &SysDeployFileHandler{
deployFileService: deployFileService,
}
}
// Create 创建部署文件记录
// @Summary 创建部署文件记录
// @Description 创建新的部署文件记录
// @Tags 部署文件管理
// @Accept json
// @Produce json
// @Param deployFile body model.SysDeployFile true "部署文件信息"
// @Success 200 {object} serializer.Response
// @Router /deploy-files [post]
func (h *SysDeployFileHandler) Create(c *gin.Context) {
logger.Info(c, "创建部署文件记录")
c.JSON(200, h.deployFileService.Create(c))
}
// GetByID 根据ID获取部署文件记录
// @Summary 获取部署文件记录
// @Description 根据ID获取部署文件记录详情
// @Tags 部署文件管理
// @Accept json
// @Produce json
// @Param id path string true "部署ID"
// @Success 200 {object} serializer.Response
// @Router /deploy-files/{id} [get]
func (h *SysDeployFileHandler) GetByID(c *gin.Context) {
logger.Info(c, "获取部署文件记录", zap.String("id", c.Param("id")))
c.JSON(200, h.deployFileService.GetByID(c))
}
// UpdateByID 根据ID更新部署文件记录
// @Summary 更新部署文件记录
// @Description 根据ID更新部署文件记录
// @Tags 部署文件管理
// @Accept json
// @Produce json
// @Param id path string true "部署ID"
// @Param deployFile body model.SysDeployFile true "部署文件信息"
// @Success 200 {object} serializer.Response
// @Router /deploy-files/{id} [put]
func (h *SysDeployFileHandler) UpdateByID(c *gin.Context) {
logger.Info(c, "更新部署文件记录", zap.String("id", c.Param("id")))
c.JSON(200, h.deployFileService.UpdateByID(c))
}
// DeleteByID 根据ID删除部署文件记录
// @Summary 删除部署文件记录
// @Description 根据ID删除部署文件记录
// @Tags 部署文件管理
// @Accept json
// @Produce json
// @Param id path string true "部署ID"
// @Success 200 {object} serializer.Response
// @Router /deploy-files/{id} [delete]
func (h *SysDeployFileHandler) DeleteByID(c *gin.Context) {
logger.Info(c, "删除部署文件记录", zap.String("id", c.Param("id")))
c.JSON(200, h.deployFileService.DeleteByID(c))
}
// GetByCondition 条件查询部署文件记录
// @Summary 条件查询部署文件记录
// @Description 根据条件分页查询部署文件记录
// @Tags 部署文件管理
// @Accept json
// @Produce json
// @Param params query types.Params true "查询参数"
// @Success 200 {object} serializer.Response
// @Router /deploy-files [get]
func (h *SysDeployFileHandler) GetByCondition(c *gin.Context) {
logger.Info(c, "条件查询部署文件记录")
c.JSON(200, h.deployFileService.GetByCondition(c))
}

View File

@ -0,0 +1,55 @@
package handler
import (
"ego/internal/service"
"net/http"
"github.com/gin-gonic/gin"
)
// SysLoginLogHandler 登录日志处理器
type SysLoginLogHandler struct {
sysLoginLogService *service.SysLoginLogService
}
// NewSysLoginLogHandler 构建登录日志处理器
func NewSysLoginLogHandler(sysLoginLogService *service.SysLoginLogService) *SysLoginLogHandler {
return &SysLoginLogHandler{
sysLoginLogService: sysLoginLogService,
}
}
// Create 创建登录日志
func (h *SysLoginLogHandler) Create(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.Create(c))
}
// DeleteByID 根据ID删除登录日志
func (h *SysLoginLogHandler) DeleteByID(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.DeleteByID(c))
}
// UpdateByID 根据ID更新登录日志
func (h *SysLoginLogHandler) UpdateByID(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.UpdateByID(c))
}
// GetByID 根据ID获取登录日志
func (h *SysLoginLogHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.GetByID(c))
}
// GetByCondition 根据条件获取登录日志
func (h *SysLoginLogHandler) GetByCondition(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.GetByCondition(c))
}
// ListByIDs 根据ID列表获取登录日志
func (h *SysLoginLogHandler) ListByIDs(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.ListByIDs(c))
}
// DeleteByIDs 根据ID列表删除登录日志
func (h *SysLoginLogHandler) DeleteByIDs(c *gin.Context) {
c.JSON(http.StatusOK, h.sysLoginLogService.DeleteByIDs(c))
}

View File

@ -0,0 +1,34 @@
package handler
import (
"ego/internal/service"
"net/http"
"github.com/gin-gonic/gin"
)
// SysUploadHandler 文件上传处理器
type SysUploadHandler struct {
UploadSvc *service.SysUploadService
}
// NewSysUploadHandler 构建文件上传处理器
func NewSysUploadHandler(uploadSvc *service.SysUploadService) *SysUploadHandler {
return &SysUploadHandler{
UploadSvc: uploadSvc,
}
}
// UploadZip 上传压缩包
// @Summary 上传压缩包
// @Description 上传包含index.html的压缩包并解压到指定目录
// @Tags 文件上传
// @Accept multipart/form-data
// @Produce json
// @Security ApiKeyAuth
// @Param file formData file true "压缩包文件"
// @Success 200 {object} serializer.Response
// @Router /api/v1/upload/zip [post]
func (h *SysUploadHandler) UploadZip(c *gin.Context) {
c.JSON(http.StatusOK, h.UploadSvc.UploadZip(c))
}

View File

@ -0,0 +1,176 @@
package handler
import (
"ego/internal/serializer"
"ego/internal/service"
"ego/internal/types"
"net/http"
"github.com/gin-gonic/gin"
)
type SysUserHandler struct {
UserSvc *service.SysUserService
}
// NewSysUserHandler 提供 SysUserHandler 的实例
func NewSysUserHandler(userSvc *service.SysUserService) *SysUserHandler {
return &SysUserHandler{
UserSvc: userSvc,
}
}
// Create 创建用户
// @Summary 创建用户
// @Description 创建新用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body model.SysUser true "用户信息"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser [post]
func (h *SysUserHandler) Create(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.Create(c))
}
// DeleteByID 删除用户
// @Summary 删除用户
// @Description 根据ID删除用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "用户ID"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/{id} [delete]
func (h *SysUserHandler) DeleteByID(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.DeleteByID(c))
}
// UpdateByID 更新用户
// @Summary 更新用户
// @Description 根据ID更新用户信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "用户ID"
// @Param user body model.SysUser true "用户信息"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/{id} [put]
func (h *SysUserHandler) UpdateByID(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.UpdateByID(c))
}
// GetByID 根据ID查询用户
// @Summary 获取用户
// @Description 根据ID获取用户信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "用户ID"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/{id} [get]
func (h *SysUserHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.GetByID(c))
}
// DeleteByIDs 批量删除用户
// @Summary 批量删除用户
// @Description 根据ID列表批量删除用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body types.Payload true "ID列表"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/batch [delete]
func (h *SysUserHandler) DeleteByIDs(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.DeleteByIDs(c))
}
// GetByCondition 根据条件查询用户
// @Summary 条件查询用户
// @Description 根据条件查询用户列表
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param query body types.Params true "查询条件"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/condition [post]
func (h *SysUserHandler) GetByCondition(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.GetByCondition(c))
}
// ListByIDs 根据ID列表查询用户
// @Summary 批量查询用户
// @Description 根据ID列表批量查询用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param ids body []string true "用户ID列表"
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/list/ids [post]
func (h *SysUserHandler) ListByIDs(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.ListByIDs(c))
}
// UserRegister 用户注册接口
// @Summary 用户注册
// @Description 新用户注册接口
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param user body service.UserRegisterRequest true "用户注册信息"
// @Success 200 {object} serializer.Response
// @Router /api/v1/user/register [post]
func (h *SysUserHandler) UserRegister(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.Register(c))
}
// UserLogin 用户登录接口
// @Summary 用户登录
// @Description 用户登录接口
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param user body service.UserLoginRequest true "用户登录信息"
// @Success 200 {object} serializer.Response
// @Router /api/v1/user/login [post]
func (h *SysUserHandler) UserLogin(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.Login(c))
}
// UserMe 用户详情
// @Summary 获取当前用户信息
// @Description 获取当前登录用户的详细信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/me [get]
func (h *SysUserHandler) UserMe(c *gin.Context) {
if user, err := h.UserSvc.CurrentUser(c); err == nil {
c.JSON(http.StatusOK, types.BuildUserResponse(*user))
} else {
c.JSON(http.StatusOK, serializer.Err(http.StatusInternalServerError, "获取用户信息失败!", err))
}
}
// UserLogout 用户登出
// @Summary 用户登出
// @Description 用户退出登录
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} serializer.Response
// @Router /api/v1/sysUser/logout [post]
func (h *SysUserHandler) UserLogout(c *gin.Context) {
c.JSON(http.StatusOK, h.UserSvc.UserLogout(c))
}

View File

@ -0,0 +1,73 @@
package middleware
import (
_ "ego/docs"
"ego/pkg/logger"
"os"
"regexp"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// Cors 跨域配置(支持环境变量动态配置)
func Cors() gin.HandlerFunc {
config := cors.DefaultConfig()
// 设置基础配置
config.AllowMethods = []string{
"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS",
}
config.AllowHeaders = []string{
"Origin", "Content-Length", "Content-Type", "Cookie", "Authorization",
"X-Requested-With", "X-CSRF-Token", // 添加常用自定义头
}
config.AllowCredentials = true
config.MaxAge = 12 * time.Hour // 预检请求缓存12小时
// 根据环境配置允许的来源
if gin.Mode() == gin.ReleaseMode {
// 生产环境:从环境变量读取允许的域名(逗号分隔)
origins := os.Getenv("CORS_ALLOWED_ORIGINS")
if origins == "" {
// 如果环境变量未设置,使用默认的安全配置
logger.Warn(nil, "CORS_ALLOWED_ORIGINS 环境变量未设置,使用默认安全配置")
config.AllowOrigins = []string{
"https://yourdomain.com", // 请替换为实际域名
}
} else {
// 清理空白字符并分割
originList := make([]string, 0)
for _, origin := range strings.Split(origins, ",") {
trimmed := strings.TrimSpace(origin)
if trimmed != "" {
originList = append(originList, trimmed)
}
}
config.AllowOrigins = originList
}
} else {
// 开发环境:匹配本地开发域名
config.AllowOriginFunc = func(origin string) bool {
// 匹配 http://localhost:端口 或 http://127.0.0.1:端口
re := regexp.MustCompile(`^http://(localhost|127\.0\.0\.1):\d+$`)
return re.MatchString(origin)
}
}
// 输出当前生效的配置(方便调试)
if logger.Logger != nil {
logger.Info(nil, "跨域配置初始化完成",
zap.Any("允许方法", config.AllowMethods),
zap.Any("允许头", config.AllowHeaders),
zap.Any("允许来源", config.AllowOrigins),
zap.Duration("MaxAge", config.MaxAge),
zap.String("模式", gin.Mode()),
)
}
return cors.New(config)
}

View File

@ -0,0 +1,91 @@
package middleware
import (
"ego/internal/serializer"
"ego/internal/util"
"ego/pkg/logger"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// AuthRequired 验证 JWT 的中间件
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
sendUnauthorized(c, "缺少认证头")
return
}
// 检查Bearer前缀
if !strings.HasPrefix(authHeader, "Bearer ") {
sendUnauthorized(c, "无效的认证格式请使用Bearer Token")
return
}
// 提取token
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == "" {
sendUnauthorized(c, "Token不能为空")
return
}
// 验证token
claims, err := util.ValidateToken(tokenString)
if err != nil {
logger.Error(c, "Token验证失败", zap.Error(err))
sendUnauthorized(c, "Token验证失败: "+err.Error())
return
}
// 检查Redis中的token状态
redisKey := util.TokenGroup + claims.Username
val, err := util.Get(c, redisKey)
if err != nil {
logger.Error(c, "Redis验证失败",
zap.Error(err),
zap.String("redisKey", redisKey))
sendUnauthorized(c, "登录状态验证失败")
return
}
if val == "" {
logger.Warn(c, "Token已过期或已注销",
zap.String("username", claims.Username))
sendUnauthorized(c, "登录状态已过期,请重新登录")
return
}
// 验证Redis中的token是否与当前token一致
if val != tokenString {
logger.Warn(c, "Token不匹配可能存在重复登录",
zap.String("username", claims.Username))
sendUnauthorized(c, "登录状态异常,请重新登录")
return
}
// 将用户信息存储到context中
c.Set("userID", claims.ID)
c.Set("username", claims.Username)
c.Set("id", claims.ID) // 保持向后兼容
logger.Debug(c, "JWT认证成功",
zap.String("userID", claims.ID),
zap.String("username", claims.Username))
c.Next()
}
}
// sendUnauthorized 发送401未授权响应
func sendUnauthorized(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, serializer.Response{
Code: http.StatusUnauthorized,
Msg: message,
Data: nil,
})
}

View File

@ -0,0 +1,70 @@
package middleware
import (
"ego/internal/model"
"ego/internal/service"
"ego/internal/util"
"ego/pkg/logger"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// LoginLogMiddleware 登录日志中间件
func LoginLogMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 只处理登录请求
if c.Request.URL.Path != "/api/v1/user/login" {
c.Next()
return
}
// 执行登录逻辑
c.Next()
// 登录成功
recordLoginLog(c, db)
}
}
// recordLoginLog 记录登录日志
func recordLoginLog(c *gin.Context, db *gorm.DB) {
// 获取User-Agent
userAgent := c.GetHeader("User-Agent")
// 获取当前时间
now := time.Now()
var status, msg string
if c.GetString("status") == "1" {
status = "1"
msg = "登录成功"
} else {
status = "0"
msg = c.GetString("msg")
}
// 创建登录日志
loginLog := model.SysLoginLog{
UserId: c.GetString("account"),
IpAddr: c.ClientIP(),
LoginLocation: "", // TODO: 需要集成IP地理位置查询服务
Browser: userAgent,
Os: util.ParseUserAgent(userAgent),
Status: status,
Msg: msg,
LoginTime: &now,
UpdateTime: &now,
CreateTime: &now,
}
sequenceService := service.SysSequenceServiceBuilder(loginLog.TableName())
id, err := sequenceService.GenerateId()
if err != nil {
logger.Error(c, "生成日志ID失败", zap.Error(err))
return
}
loginLog.Id = id
if err := db.Create(&loginLog).Error; err != nil {
logger.Error(c, "记录登录日志失败!", zap.Error(err))
}
}

View File

@ -0,0 +1,20 @@
package middleware
import (
"ego/internal/serializer"
"ego/pkg/logger"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// UnifiedErrorResponse 统一错误处理中间件
func UnifiedErrorResponse() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, err any) {
// 1. 记录完整错误日志
logger.Error(c, fmt.Sprintf("系统错误: %v", err))
// 2. 返回统一的Json风格
c.AbortWithStatusJSON(http.StatusOK, serializer.Err(http.StatusInternalServerError, "系统繁忙稍后重试!", err.(error)))
})
}

View File

@ -0,0 +1,27 @@
package middleware
import (
"ego/pkg/logger"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// GinZapLogger Gin 日志中间件
func GinZapLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
trackID := logger.GetTrackID(c)
c.Next()
// 记录请求日志
logger.Info(c, "Handled request",
zap.String("trackID", trackID),
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.Int("status", c.Writer.Status()),
zap.Duration("duration", time.Since(start)),
)
}
}

View File

@ -0,0 +1,49 @@
package model
import (
"time"
)
// SysDeployFile 部署文件记录表
type SysDeployFile struct {
DeployId string `gorm:"column:deploy_id;type:varchar(64);primary_key;comment:部署ID" json:"deployId"`
FileName string `gorm:"column:file_name;type:varchar(255);not null;comment:原始文件名" json:"fileName"`
ProjectName string `gorm:"column:project_name;type:varchar(100);not null;comment:项目名称" json:"projectName"`
Domain string `gorm:"column:domain;type:varchar(255);not null;comment:访问域名" json:"domain"`
DeployPath string `gorm:"column:deploy_path;type:varchar(500);not null;comment:部署路径" json:"deployPath"`
FileSize int64 `gorm:"column:file_size;type:bigint;comment:文件大小(字节)" json:"fileSize"`
FileHash string `gorm:"column:file_hash;type:varchar(64);comment:文件哈希值" json:"fileHash"`
Status string `gorm:"column:status;type:char(1);default:1;comment:状态0停用 1正常 2部署中 3部署失败" json:"status"`
DeployStatus string `gorm:"column:deploy_status;type:char(1);default:0;comment:部署状态0未部署 1部署成功 2部署失败" json:"deployStatus"`
ErrorMsg string `gorm:"column:error_msg;type:text;comment:错误信息" json:"errorMsg"`
Version string `gorm:"column:version;type:varchar(50);comment:版本号" json:"version"`
Description string `gorm:"column:description;type:varchar(500);comment:描述" json:"description"`
DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志0代表存在 1代表删除" json:"delFlag"`
CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"`
CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"`
UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"`
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"`
DeployTime *time.Time `gorm:"column:deploy_time;type:datetime;comment:部署时间" json:"deployTime"`
LastAccessTime *time.Time `gorm:"column:last_access_time;type:datetime;comment:最后访问时间" json:"lastAccessTime"`
AccessCount int64 `gorm:"column:access_count;type:bigint;default:0;comment:访问次数" json:"accessCount"`
}
// TableName 表名
func (m *SysDeployFile) TableName() string {
return "sys_deploy_file"
}
// DeployFileStatus 部署文件状态常量
const (
DeployFileStatusDisabled = "0" // 停用
DeployFileStatusNormal = "1" // 正常
DeployFileStatusDeploying = "2" // 部署中
DeployFileStatusFailed = "3" // 部署失败
)
// DeployStatus 部署状态常量
const (
DeployStatusNotDeployed = "0" // 未部署
DeployStatusSuccess = "1" // 部署成功
DeployStatusFailed = "2" // 部署失败
)

View File

@ -0,0 +1,25 @@
package model
import "time"
// SysLoginLog 系统登录日志
type SysLoginLog struct {
Id string `gorm:"column:id;type:varchar(64);primary_key;comment:主键" json:"id"`
UserId string `gorm:"column:user_id;type:varchar(64);comment:用户ID" json:"userId"`
IpAddr string `gorm:"column:ip_addr;type:varchar(128);comment:登录IP地址" json:"ipAddr"`
LoginLocation string `gorm:"column:login_location;type:varchar(255);comment:登录地点" json:"loginLocation"`
Browser string `gorm:"column:browser;type:varchar(128);comment:浏览器类型" json:"browser"`
Os string `gorm:"column:os;type:varchar(50);comment:操作系统" json:"os"`
Status string `gorm:"column:status;type:varchar(10);comment:登录状态0成功 1失败" json:"status"`
Msg string `gorm:"column:msg;type:varchar(255);comment:提示消息" json:"msg"`
LoginTime *time.Time `gorm:"column:login_time;type:varchar(20);comment:登录时间" json:"loginTime"`
CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"`
CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"`
UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"`
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"`
}
// TableName 设置表名
func (SysLoginLog) TableName() string {
return "sys_login_log"
}

View File

@ -0,0 +1,13 @@
package model
// SysSequence 系统序列表
type SysSequence struct {
Name string `gorm:"column:table_name;type:varchar(32);not null;comment:表名" json:"name"`
Seq int `gorm:"column:seq;type:int(11);not null;comment:序列" json:"seq"`
Prefix string `gorm:"column:Prefix;type:varchar(32);not null;comment:前缀" json:"prefix"`
}
// TableName table name
func (m *SysSequence) TableName() string {
return "sys_sequence"
}

View File

@ -0,0 +1,36 @@
package model
import (
"time"
)
// SysUser 用户信息表
type SysUser struct {
UserId string `gorm:"column:user_id;type:varchar(64);primary_key;comment:用户ID" json:"userId"`
DeptId string `gorm:"column:dept_id;type:varchar(64);comment:部门ID" json:"deptId"`
UserName string `gorm:"column:user_name;type:varchar(30);not null;comment:用户账号" json:"userName"`
NickName string `gorm:"column:nick_name;type:varchar(30);not null;comment:用户昵称" json:"nickName"`
UserType string `gorm:"column:user_type;type:varchar(2);default:00;comment:用户类型00系统用户" json:"userType"`
Email string `gorm:"column:email;type:varchar(50);comment:用户邮箱" json:"email"`
PhoneNumber string `gorm:"column:phone_number;type:varchar(11);comment:手机号码" json:"phoneNumber"`
Solt int `gorm:"column:solt;type:int(11);comment:排序" json:"solt"`
Gender string `gorm:"column:gender;type:char(1);default:0;comment:用户性别0男 1女 2未知" json:"gender"`
Avatar string `gorm:"column:avatar;type:varchar(100);comment:头像地址" json:"avatar"`
PassWord string `gorm:"column:pass_word;type:varchar(100);comment:密码" json:"passWord"`
Status string `gorm:"column:status;type:char(1);default:0;comment:帐号状态0正常 1停用" json:"status"`
DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志0代表存在 2代表删除" json:"delFlag"`
LoginIP string `gorm:"column:login_ip;type:varchar(128);comment:最后登录IP" json:"loginIP"`
LoginDate *time.Time `gorm:"column:login_date;type:datetime;comment:最后登录时间" json:"loginDate"`
ResourceInvoke string `gorm:"column:resource_invoke;type:varchar(255);comment:资源来源映射,多个用,分割" json:"resourceInvoke"`
CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"`
CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"`
UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"`
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"`
Remark string `gorm:"column:remark;type:varchar(500);comment:备注" json:"remark"`
SelectKey string `gorm:"column:select_key;type:varchar(64);comment:动态验证" json:"selectKey"`
}
// TableName table name
func (m *SysUser) TableName() string {
return "sys_user"
}

View File

@ -0,0 +1,44 @@
package router
import (
"ego/internal/conf"
"ego/internal/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
var (
apiRouterFns []func(r *gin.RouterGroup)
)
// NewRouter 路由配置
func NewRouter() *gin.Engine {
// gin 实例
engine := gin.Default()
// 跨域处理
engine.Use(middleware.Cors())
// zap 日志
engine.Use(middleware.GinZapLogger())
// 统一错误处理
engine.Use(middleware.UnifiedErrorResponse())
// 登录日志中间件
engine.Use(middleware.LoginLogMiddleware(conf.Db))
// swagger
if gin.Mode() != gin.ReleaseMode {
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// register routers, middleware support
registerRouters(engine, "/api/v1", apiRouterFns)
return engine
}
// registerRouters 注册路由
func registerRouters(r *gin.Engine, groupPath string, routerFns []func(*gin.RouterGroup), handlers ...gin.HandlerFunc) {
rg := r.Group(groupPath, handlers...)
for _, fn := range routerFns {
fn(rg)
}
}

View File

@ -0,0 +1,34 @@
package router
import (
"ego/internal/handler"
"ego/internal/middleware"
"ego/internal/wire"
"github.com/gin-gonic/gin"
)
func init() {
apiRouterFns = append(apiRouterFns, func(group *gin.RouterGroup) {
SysDeployFileHandlerRouter(group, wire.InjectSysDeployFileHandler())
})
}
// SysDeployFileHandlerRouter 部署文件相关路由
// @Summary 部署文件管理路由
// @Description 包含部署文件的增删改查等接口
// @Tags 部署文件管理
// @Accept json
// @Produce json
func SysDeployFileHandlerRouter(group *gin.RouterGroup, h *handler.SysDeployFileHandler) {
// 部署文件管理路由组
g := group.Group("/deploy-files")
// 鉴权
g.Use(middleware.AuthRequired())
g.POST("/", h.Create)
g.GET("/:id", h.GetByID)
g.PUT("/", h.UpdateByID)
g.DELETE("/:id", h.DeleteByID)
g.GET("", h.GetByCondition)
}

View File

@ -0,0 +1,34 @@
package router
import (
"ego/internal/handler"
"ego/internal/middleware"
"ego/internal/wire"
"github.com/gin-gonic/gin"
)
// @tag.name 登录记录
// @tag.description 登录记录相关的所有接口,包括创建、删除、更新、查询等
func init() {
apiRouterFns = append(apiRouterFns, func(group *gin.RouterGroup) {
SysLoginLogRouter(group, wire.InjectSysLoginLogHandler())
})
}
// SysLoginLogRouter 登录日志路由
func SysLoginLogRouter(r *gin.RouterGroup, h *handler.SysLoginLogHandler) {
g := r.Group("/sysLoginLog")
// 登录日志需要认证
g.Use(middleware.AuthRequired())
{
g.POST("/create", h.Create)
g.POST("/delete", h.DeleteByID)
g.POST("/update", h.UpdateByID)
g.POST("/get", h.GetByID)
g.POST("/list", h.GetByCondition)
g.POST("/list/ids", h.ListByIDs)
g.POST("/delete/ids", h.DeleteByIDs)
}
}

View File

@ -0,0 +1,22 @@
package router
import (
"ego/internal/handler"
"ego/internal/wire"
"github.com/gin-gonic/gin"
)
func init() {
apiRouterFns = append(apiRouterFns, func(group *gin.RouterGroup) {
SysUploadHandlerRouter(group, wire.InjectSysUploadHandler())
})
}
// SysUploadHandlerRouter 文件上传路由
func SysUploadHandlerRouter(r *gin.RouterGroup, h *handler.SysUploadHandler) {
upload := r.Group("/upload")
//upload.Use(middleware.AuthRequired())
{
upload.POST("/zip", h.UploadZip)
}
}

View File

@ -0,0 +1,53 @@
package router
import (
"ego/internal/handler"
"ego/internal/middleware"
"ego/internal/wire"
"github.com/gin-gonic/gin"
)
// @title EGO API
// @version 1.0
// @description EGO 系统 API 文档
// @termsOfService http://swagger.io/terms/
// @tag.name 用户管理
// @tag.description 用户相关的所有接口,包括注册、登录、信息管理等
func init() {
apiRouterFns = append(apiRouterFns, func(group *gin.RouterGroup) {
SysUserHandlerRouter(group, wire.InjectSysUserHandler())
})
}
// SysUserHandlerRouter 用户相关路由
// @Summary 用户管理路由
// @Description 包含用户注册、登录、信息管理等接口
// @Tags 用户管理
// @Accept json
// @Produce json
func SysUserHandlerRouter(group *gin.RouterGroup, h *handler.SysUserHandler) {
// 不需要认证
rg := group.Group("/user")
rg.POST("/register", h.UserRegister)
rg.POST("/login", h.UserLogin)
g := group.Group("/sysUser")
// 鉴权
g.Use(middleware.AuthRequired())
g.POST("/", h.Create)
g.DELETE("/:id", h.DeleteByID)
g.PUT("/", h.UpdateByID)
g.GET("/:id", h.GetByID)
g.DELETE("/batch", h.DeleteByIDs)
g.POST("/condition", h.GetByCondition)
g.POST("/list/ids", h.ListByIDs)
g.GET("/me", h.UserMe)
g.POST("/logout", h.UserLogout)
}

View File

@ -0,0 +1,78 @@
package serializer
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Response 基础序列化器
type Response struct {
Code int `json:"code"`
Data any `json:"data,omitempty"`
Msg string `json:"msg"`
Error string `json:"error,omitempty"`
}
// TrackedErrorResponse 有追踪信息的错误响应
type TrackedErrorResponse struct {
Response
TrackID string `json:"track_id"`
}
const (
CodeCheckLogin = 401
CodeNoRightErr = 403
CodeDBError = 50001
CodeEncryptError = 50002
CodeParamErr = 40001
)
// CheckLogin 检查登录
func CheckLogin() Response {
return Err(CodeCheckLogin, "未登录", nil)
}
// Err 通用错误处理
func Err(errCode int, msg string, err error) Response {
if msg == "" {
msg = "未知错误"
}
res := Response{
Code: errCode,
Msg: msg,
}
if err != nil && gin.Mode() != gin.ReleaseMode {
res.Error = fmt.Sprintf("%+v", err)
}
return res
}
// DBErr 数据库操作失败
func DBErr(msg string, err error) Response {
if msg == "" {
msg = "数据库操作失败"
}
return Err(CodeDBError, msg, err)
}
// ParamErr 各种参数错误
func ParamErr(msg string, err error) Response {
if msg == "" {
msg = "参数错误"
}
return Err(CodeParamErr, msg, err)
}
// Succ 返回结果
func Succ(msg string, data any) Response {
if msg == "" {
msg = "操作成功!"
}
return Response{
Code: http.StatusOK,
Msg: msg,
Data: data,
}
}

View File

@ -0,0 +1,227 @@
package service
import (
"crypto/md5"
"ego/internal/model"
"ego/internal/serializer"
"ego/internal/types"
"ego/pkg/logger"
"fmt"
"io"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// SysDeployFileService 部署文件服务
type SysDeployFileService struct {
Db *gorm.DB
}
// NewSysDeployFileService 构建部署文件服务
func NewSysDeployFileService(db *gorm.DB) *SysDeployFileService {
return &SysDeployFileService{
Db: db,
}
}
// Create 创建部署文件记录
func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
var deployFile model.SysDeployFile
if err := c.ShouldBind(&deployFile); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
// 生成部署ID
if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil {
deployFile.DeployId = id
} else {
return serializer.DBErr("序列生成失败!", err)
}
// 设置默认值
now := time.Now()
deployFile.CreateTime = &now
deployFile.Status = model.DeployFileStatusNormal
deployFile.DeployStatus = model.DeployStatusNotDeployed
deployFile.DelFlag = "0"
// 获取当前用户
if createBy := c.GetString("id"); createBy != "" {
deployFile.CreateBy = createBy
}
if err := s.Db.Create(&deployFile).Error; err != nil {
logger.Error(c, "创建部署文件记录失败!")
return serializer.DBErr("创建部署文件记录失败!", err)
}
return serializer.Succ("创建部署文件记录成功!", deployFile)
}
// GetByID 根据ID获取部署文件记录
func (s *SysDeployFileService) GetByID(c *gin.Context) serializer.Response {
var deployFile model.SysDeployFile
if err := s.Db.Where("deploy_id = ? AND del_flag = ?", c.Param("id"), "0").First(&deployFile).Error; err != nil {
logger.Error(c, "获取部署文件记录失败!")
return serializer.DBErr("获取部署文件记录失败!", err)
}
return serializer.Succ("查询成功!", deployFile)
}
// UpdateByID 根据ID更新部署文件记录
func (s *SysDeployFileService) UpdateByID(c *gin.Context) serializer.Response {
var deployFile model.SysDeployFile
if err := c.ShouldBind(&deployFile); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
id := deployFile.DeployId
if id == "" {
logger.Error(c, "id 不可为空!")
return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空"))
}
// 设置更新时间
now := time.Now()
deployFile.UpdateTime = &now
// 获取当前用户
if updateBy := c.GetString("id"); updateBy != "" {
deployFile.UpdateBy = updateBy
}
if err := s.Db.Model(&deployFile).Where("deploy_id = ? AND del_flag = ?", id, "0").Updates(&deployFile).Error; err != nil {
logger.Error(c, "更新部署文件记录失败!")
return serializer.DBErr("更新部署文件记录失败!", err)
}
return serializer.Succ("更新部署文件记录成功!", deployFile)
}
// DeleteByID 根据ID删除部署文件记录
func (s *SysDeployFileService) DeleteByID(c *gin.Context) serializer.Response {
id := c.Param("id")
if id == "" {
logger.Error(c, "id 不可为空!")
return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空"))
}
// 软删除
data := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": c.GetString("id"),
}
if err := s.Db.Model(&model.SysDeployFile{}).Where("deploy_id = ?", id).Updates(data).Error; err != nil {
logger.Error(c, "删除部署文件记录失败!")
return serializer.DBErr("删除部署文件记录失败!", err)
}
return serializer.Succ("删除部署文件记录成功!", nil)
}
// GetByCondition 条件查询部署文件记录
func (s *SysDeployFileService) GetByCondition(c *gin.Context) serializer.Response {
var p types.Params
if err := c.ShouldBind(&p); err != nil {
return serializer.ParamErr("参数绑定失败!", err)
}
queryStr, args, err := p.ConvertToGormConditions()
if err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
var total int64
var deployFiles []model.SysDeployFile
offset := (p.Page - 1) * p.Limit
// 构建基础查询
db := s.Db.Model(&model.SysDeployFile{})
// 如果有查询条件,添加条件
if queryStr != "" {
db = db.Where(queryStr, args...)
}
// 排序
if p.Sort != "" {
db = db.Order(p.Sort)
} else {
db = db.Order("create_time DESC")
}
// 执行分页查询
if err := db.Where("del_flag = ?", "0").Offset(offset).Limit(p.Limit).Find(&deployFiles).Error; err != nil {
logger.Error(c, "获取部署文件记录失败!")
return serializer.DBErr("获取部署文件记录失败!", err)
}
// 执行总数查询
if err := db.Where("del_flag = ?", "0").Count(&total).Error; err != nil {
logger.Error(c, "获取部署文件记录总数失败!")
return serializer.DBErr("获取部署文件记录总数失败!", err)
}
return serializer.Succ("查询成功!", gin.H{
"total": total,
"items": deployFiles,
"page": p.Page,
"limit": p.Limit,
})
}
// UpdateDeployStatus 更新部署状态
func (s *SysDeployFileService) UpdateDeployStatus(deployId, status, errorMsg string) error {
data := map[string]any{
"deploy_status": status,
"update_time": time.Now(),
}
if status == model.DeployStatusSuccess {
now := time.Now()
data["deploy_time"] = &now
}
if errorMsg != "" {
data["error_msg"] = errorMsg
}
return s.Db.Model(&model.SysDeployFile{}).Where("deploy_id = ?", deployId).Updates(data).Error
}
// UpdateAccessInfo 更新访问信息
func (s *SysDeployFileService) UpdateAccessInfo(deployId string) error {
now := time.Now()
return s.Db.Model(&model.SysDeployFile{}).
Where("deploy_id = ?", deployId).
Updates(map[string]any{
"last_access_time": &now,
"access_count": gorm.Expr("access_count + 1"),
}).Error
}
// GetByDomain 根据域名获取部署文件记录
func (s *SysDeployFileService) GetByDomain(domain string) (*model.SysDeployFile, error) {
var deployFile model.SysDeployFile
err := s.Db.Where("domain = ? AND status = ? AND del_flag = ?",
domain, model.DeployFileStatusNormal, "0").First(&deployFile).Error
if err != nil {
return nil, err
}
return &deployFile, nil
}
// CalculateFileHash 计算文件哈希值
func (s *SysDeployFileService) CalculateFileHash(reader io.Reader) (string, error) {
hash := md5.New()
if _, err := io.Copy(hash, reader); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

View File

@ -0,0 +1,161 @@
package service
import (
"ego/internal/model"
"ego/internal/serializer"
"ego/internal/types"
"ego/pkg/logger"
"fmt"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// SysLoginLogService 登录日志服务
type SysLoginLogService struct {
Db *gorm.DB
}
// NewSysLoginLogService 构建登录日志服务
func NewSysLoginLogService(db *gorm.DB) *SysLoginLogService {
return &SysLoginLogService{
Db: db,
}
}
// Create 创建登录日志
func (s *SysLoginLogService) Create(c *gin.Context) serializer.Response {
var loginLog model.SysLoginLog
if err := c.ShouldBind(&loginLog); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
// 生成登录日志ID
id, err := SysSequenceServiceBuilder(loginLog.TableName()).GenerateId()
if err != nil {
return serializer.DBErr("创建登录日志失败!", err)
}
loginLog.Id = id
now := time.Now()
loginLog.LoginTime = &now
if err := s.Db.Create(&loginLog).Error; err != nil {
logger.Error(c, "创建登录日志失败!")
return serializer.DBErr("创建登录日志失败!", err)
}
return serializer.Succ("创建登录日志成功!", loginLog)
}
// DeleteByID 根据ID删除登录日志
func (s *SysLoginLogService) DeleteByID(c *gin.Context) serializer.Response {
var loginLog model.SysLoginLog
if err := c.ShouldBind(&loginLog); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
if err := s.Db.Delete(&loginLog).Error; err != nil {
logger.Error(c, "删除登录日志失败!")
return serializer.DBErr("删除登录日志失败!", err)
}
return serializer.Succ("删除登录日志成功!", nil)
}
// UpdateByID 根据ID更新登录日志
func (s *SysLoginLogService) UpdateByID(c *gin.Context) serializer.Response {
var loginLog model.SysLoginLog
if err := c.ShouldBind(&loginLog); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
if err := s.Db.Model(&loginLog).Updates(loginLog).Error; err != nil {
logger.Error(c, "更新登录日志失败!")
return serializer.DBErr("更新登录日志失败!", err)
}
return serializer.Succ("更新登录日志成功!", loginLog)
}
// GetByID 根据ID获取登录日志
func (s *SysLoginLogService) GetByID(c *gin.Context) serializer.Response {
var loginLog model.SysLoginLog
if err := c.ShouldBind(&loginLog); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
if err := s.Db.First(&loginLog).Error; err != nil {
logger.Error(c, "获取登录日志失败!")
return serializer.DBErr("获取登录日志失败!", err)
}
return serializer.Succ("获取登录日志成功!", loginLog)
}
// GetByCondition 根据条件获取登录日志
func (s *SysLoginLogService) GetByCondition(c *gin.Context) serializer.Response {
var loginLog model.SysLoginLog
if err := c.ShouldBind(&loginLog); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
var loginLogs []model.SysLoginLog
if err := s.Db.Where(&loginLog).Find(&loginLogs).Error; err != nil {
logger.Error(c, "获取登录日志失败!")
return serializer.DBErr("获取登录日志失败!", err)
}
return serializer.Succ("获取登录日志成功!", loginLogs)
}
// ListByIDs 根据ID列表获取登录日志
func (s *SysLoginLogService) ListByIDs(c *gin.Context) serializer.Response {
var ids types.Payload
if err := c.ShouldBind(&ids); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
var loginLogs []model.SysLoginLog
if err := s.Db.Where("id IN ?", ids.Ids).Find(&loginLogs).Error; err != nil {
logger.Error(c, "获取登录日志失败!")
return serializer.DBErr("获取登录日志失败!", err)
}
return serializer.Succ("获取登录日志成功!", loginLogs)
}
// DeleteByIDs 根据ID列表删除登录日志
func (s *SysLoginLogService) DeleteByIDs(c *gin.Context) serializer.Response {
var ids types.Payload
if err := c.ShouldBind(&ids); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
if err := s.Db.Where("id IN ?", ids.Ids).Delete(&model.SysLoginLog{}).Error; err != nil {
logger.Error(c, "删除登录日志失败!")
return serializer.DBErr("删除登录日志失败!", err)
}
return serializer.Succ("删除登录日志成功!", nil)
}
// CreateLogging 创建登录日志
func (s *SysLoginLogService) CreateLogging(sysLoginLog *model.SysLoginLog, c *gin.Context) error {
// 生成登录日志ID
id, err := SysSequenceServiceBuilder(sysLoginLog.TableName()).GenerateId()
if err != nil {
return fmt.Errorf("生成登录日志ID失败: %v", err)
}
sysLoginLog.Id = id
now := time.Now()
sysLoginLog.LoginTime = &now
if err := s.Db.Create(sysLoginLog).Error; err != nil {
logger.Error(c, "创建登录日志失败!")
return fmt.Errorf("创建登录日志失败: %v", err)
}
return nil
}

View File

@ -0,0 +1,31 @@
package service
import (
"ego/internal/conf"
"strings"
"sync"
)
type SysSequenceService struct {
mutex sync.Mutex
tableName string
}
// SysSequenceServiceBuilder 创建一个SysSequenceService
func SysSequenceServiceBuilder(tableName string) *SysSequenceService {
return &SysSequenceService{
tableName: tableName,
}
}
// GenerateId 根据表名生成ID使用序列
func (s *SysSequenceService) GenerateId() (string, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
var id string
err := conf.Db.Raw("SELECT NEXTVAL(?)", strings.ToLower(s.tableName)).Scan(&id).Error
if err != nil {
return "", err
}
return id, nil
}

View File

@ -0,0 +1,246 @@
package service
import (
"archive/zip"
"crypto/md5"
"ego/internal/model"
"ego/internal/serializer"
"ego/pkg/logger"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SysUploadService 文件上传服务
type SysUploadService struct {
Db *gorm.DB
}
// NewSysUploadService 构建文件上传服务
func NewSysUploadService(db *gorm.DB) *SysUploadService {
return &SysUploadService{
Db: db,
}
}
// UploadZip 上传压缩包
func (s *SysUploadService) UploadZip(c *gin.Context) serializer.Response {
// 获取上传的文件
file, err := c.FormFile("file")
if err != nil {
logger.Error(c, "获取上传文件失败!")
return serializer.ParamErr("获取上传文件失败!", err)
}
// 校验文件类型
if !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
logger.Error(c, "只支持zip格式文件!")
return serializer.ParamErr("只支持zip格式文件!", nil)
}
// 获取文件名(不包含扩展名)
filename := strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename))
// 检查目标文件夹是否存在
targetDir := filepath.Join("/home", filename)
if _, err := os.Stat(targetDir); !os.IsNotExist(err) {
logger.Error(c, "目标文件夹已存在!")
return serializer.ParamErr("目标文件夹已存在!", nil)
}
// 打开上传的文件
src, err := file.Open()
if err != nil {
logger.Error(c, "打开上传文件失败!")
return serializer.ParamErr("打开上传文件失败!", err)
}
defer src.Close()
// 创建临时文件
tempFile, err := os.CreateTemp("", "upload-*.zip")
if err != nil {
logger.Error(c, "创建临时文件失败!")
return serializer.ParamErr("创建临时文件失败!", err)
}
defer os.Remove(tempFile.Name())
defer tempFile.Close()
// 复制文件内容到临时文件
if _, err := io.Copy(tempFile, src); err != nil {
logger.Error(c, "复制文件内容失败!")
return serializer.ParamErr("复制文件内容失败!", err)
}
// 校验压缩包是否包含index.html
hasIndexHtml, err := s.validateZipFile(tempFile.Name())
if err != nil {
logger.Error(c, "校验压缩包失败!")
return serializer.ParamErr("校验压缩包失败!", err)
}
if !hasIndexHtml {
logger.Error(c, "压缩包必须包含index.html文件!")
return serializer.ParamErr("压缩包必须包含index.html文件!", nil)
}
// 创建目标目录
if err := os.MkdirAll(targetDir, 0755); err != nil {
logger.Error(c, "创建目标目录失败!")
return serializer.ParamErr("创建目标目录失败!", err)
}
// 解压文件
if err := s.extractZip(tempFile.Name(), targetDir); err != nil {
// 如果解压失败,删除已创建的目录
os.RemoveAll(targetDir)
logger.Error(c, "解压文件失败!")
return serializer.ParamErr("解压文件失败!", err)
}
// 返回域名格式的结果
domain := fmt.Sprintf("%s.unbug.cn", filename)
// 创建部署文件记录
deployFile := model.SysDeployFile{
FileName: file.Filename,
ProjectName: filename,
Domain: domain,
DeployPath: targetDir,
FileSize: file.Size,
Status: model.DeployFileStatusNormal,
DeployStatus: model.DeployStatusSuccess,
Description: fmt.Sprintf("自动部署项目: %s", filename),
}
// 生成部署ID
if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil {
deployFile.DeployId = id
} else {
logger.Error(c, "生成部署ID失败!", zap.Error(err))
return serializer.DBErr("生成部署ID失败!", err)
}
// 计算文件哈希值
if fileHash, err := s.calculateFileHash(tempFile.Name()); err == nil {
deployFile.FileHash = fileHash
}
// 设置时间
now := time.Now()
deployFile.CreateTime = &now
deployFile.DeployTime = &now
deployFile.DelFlag = "0"
// 获取当前用户
if createBy := c.GetString("id"); createBy != "" {
deployFile.CreateBy = createBy
}
// 保存到数据库
if err := s.Db.Create(&deployFile).Error; err != nil {
logger.Error(c, "保存部署文件记录失败!", zap.Error(err))
// 即使保存记录失败,也不影响文件部署
}
return serializer.Succ("上传成功!", map[string]interface{}{
"domain": domain,
"path": targetDir,
"deployId": deployFile.DeployId,
})
}
// validateZipFile 校验压缩包是否包含index.html
func (s *SysUploadService) validateZipFile(zipPath string) (bool, error) {
reader, err := zip.OpenReader(zipPath)
if err != nil {
return false, err
}
defer reader.Close()
for _, file := range reader.File {
if strings.ToLower(filepath.Base(file.Name)) == "index.html" {
return true, nil
}
}
return false, nil
}
// extractZip 解压zip文件到指定目录
func (s *SysUploadService) extractZip(zipPath, destDir string) error {
reader, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer reader.Close()
// 创建目标目录
if err := os.MkdirAll(destDir, 0755); err != nil {
return err
}
// 提取文件
for _, file := range reader.File {
rc, err := file.Open()
if err != nil {
return err
}
defer rc.Close()
// 构建文件路径
path := filepath.Join(destDir, file.Name)
// 检查路径是否安全(防止路径遍历攻击)
if !strings.HasPrefix(path, filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("invalid file path: %s", file.Name)
}
if file.FileInfo().IsDir() {
// 创建目录
if err := os.MkdirAll(path, file.FileInfo().Mode()); err != nil {
return err
}
} else {
// 创建文件的父目录
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
// 创建文件
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
if err != nil {
return err
}
defer outFile.Close()
// 复制文件内容
if _, err := io.Copy(outFile, rc); err != nil {
return err
}
}
}
return nil
}
// calculateFileHash 计算文件哈希值
func (s *SysUploadService) calculateFileHash(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

View File

@ -0,0 +1,427 @@
package service
import (
"ego/internal/model"
"ego/internal/serializer"
"ego/internal/types"
"ego/internal/util"
"ego/pkg/logger"
"fmt"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
const (
// PassWordCost 密码加密难度
PassWordCost = 12
// Suspend 被封禁用户
Suspend = "-1"
// Active 未激活用户
Active = "1"
// Inactive 激活用户
Inactive = "0"
)
// SysUserService 管理用户登录的服务
type SysUserService struct {
Db *gorm.DB
}
// NewSysUserService 构建用户登录服务
func NewSysUserService(db *gorm.DB) *SysUserService {
return &SysUserService{
Db: db,
}
}
type UserLoginRequest struct {
Account string `form:"account" json:"account" binding:"required,min=5,max=30"`
PassWord string `form:"password" json:"password" binding:"required,min=8,max=40"`
Checked *bool `form:"checked" json:"checked" binding:"required"`
Phone string `form:"phone" json:"phone"`
VerifyCode string `form:"verifyCode" json:"verifyCode"`
}
// UserRegisterRequest 用户注册表单验证
type UserRegisterRequest struct {
NickName string `form:"nickName" json:"nickName" binding:"required,min=2,max=30"`
UserName string `form:"userName" json:"userName" binding:"required,min=5,max=30"`
PassWord string `form:"passWord" json:"passWord" binding:"required,min=8,max=40"`
PasswordConfirm string `form:"passWordConfirm" json:"passWordConfirm" binding:"required,min=8,max=40"`
}
// Login 用户登录函数
func (s *SysUserService) Login(c *gin.Context) serializer.Response {
var user model.SysUser
u := UserLoginRequest{}
if err := c.ShouldBind(&u); err != nil {
logger.Error(c, "参数绑定错误!", zap.Error(err))
c.Set("msg", "参数绑定错误!")
return serializer.Err(serializer.CodeParamErr, "参数绑定错误!", err)
}
c.Set("account", u.Account)
if err := s.Db.Where("user_name = ?", u.Account).First(&user).Error; err != nil {
logger.Error(c, "账号或密码错误!")
c.Set("msg", "账号或密码错误!")
return serializer.ParamErr("账号或密码错误!", nil)
}
if user.Status == Suspend {
logger.Error(c, "账号已封禁!")
c.Set("msg", "账号已封禁!")
return serializer.ParamErr("账号已封禁!", nil)
}
if user.Status == Inactive {
logger.Error(c, "账号未激活!")
c.Set("msg", "账号未激活!")
return serializer.ParamErr("账号未激活!", nil)
}
if !CheckPassWord(u.PassWord, user.PassWord) {
logger.Error(c, "账号或密码错误!")
c.Set("msg", "账号或密码错误!")
return serializer.ParamErr("账号或密码错误!", nil)
}
if token, err := util.GenerateToken(user.UserId, user.UserName); err == nil {
logger.Info(c, "用户登录", zap.String("redisKey", util.TokenGroup+user.UserName))
// 删除key
del := util.Del(c, util.TokenGroup+user.UserName)
if del != nil {
logger.Error(c, "redis 删除失败!", zap.Error(del))
c.Set("msg", "redis 删除失败!")
return serializer.ParamErr("系统繁忙稍后重试!", nil)
}
// 结果放入redis
set := util.Set(c, util.TokenGroup+user.UserName, token, time.Hour*24)
if set != nil {
logger.Error(c, "redis 放入失败!", zap.Error(set))
c.Set("msg", "redis 放入失败!")
return serializer.ParamErr("系统繁忙稍后重试!", nil)
}
c.Set("status", Active)
return types.BuildUserResponseHasToken(user, token)
}
return serializer.ParamErr("系统繁忙稍后重试!", nil)
}
// UserLogout 退出登录
func (s *SysUserService) UserLogout(c *gin.Context) serializer.Response {
// 退出登录
go func() {
tokenString := c.GetHeader("Authorization")
token, err := util.ParseToken(tokenString[8:])
if err == nil {
username := token["username"].(string)
// 删除key
if err := util.Del(c, util.TokenGroup+username); err != nil {
logger.Error(c, "退出登录失败!", zap.Error(err))
} else {
logger.Info(c, "用户退出登录成功!", zap.String("redisKey", util.TokenGroup+username))
}
}
}()
return serializer.Succ("退出登录成功!", true)
}
// GetUser 用ID获取用户
func GetUser(id string, d *gorm.DB) (model.SysUser, error) {
var user model.SysUser
result := d.Where("user_id = ?", id).First(&user)
if result.Error == nil {
user.PassWord = "" // 清除密码
}
return user, result.Error
}
// SetPassWord 设置密码
func SetPassWord(passWord string, sysUser *model.SysUser) error {
bytes, err := bcrypt.GenerateFromPassword([]byte(passWord), PassWordCost)
if err != nil {
return err
}
sysUser.PassWord = string(bytes)
return nil
}
// CheckPassWord 校验密码
func CheckPassWord(password string, passwordDigest string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordDigest), []byte(password))
return err == nil
}
// valid 验证表单
func (s *SysUserService) valid(u *UserRegisterRequest) *serializer.Response {
if u.PasswordConfirm != u.PassWord {
return &serializer.Response{
Code: 40001,
Msg: "两次输入的密码不相同",
}
}
count := int64(0)
s.Db.Model(&model.SysUser{}).Where("nick_name = ?", u.NickName).Count(&count)
if count > 0 {
return &serializer.Response{
Code: 40001,
Msg: "昵称被占用",
}
}
count = 0
s.Db.Model(&model.SysUser{}).Where("user_name = ?", u.UserName).Count(&count)
if count > 0 {
return &serializer.Response{
Code: 40001,
Msg: "用户名已经注册",
}
}
return nil
}
// Register 用户注册
func (s *SysUserService) Register(c *gin.Context) serializer.Response {
u := UserRegisterRequest{}
if err := c.ShouldBind(&u); err != nil {
return serializer.Err(serializer.CodeParamErr, "参数绑定错误!", err)
}
now := time.Now()
user := model.SysUser{
NickName: u.NickName,
UserName: u.UserName,
Status: Active,
CreateTime: &now,
}
// 生成ID
if id, err := SysSequenceServiceBuilder(user.TableName()).GenerateId(); err == nil {
user.UserId = id
} else {
return serializer.DBErr("序列生成失败!", err)
}
// 表单验证
if err := s.valid(&u); err != nil {
logger.Error(c, err.Msg, zap.String("NickName", u.NickName), zap.String("UserName", u.UserName))
return *err
}
// 密码加密
if err := SetPassWord(u.PassWord, &user); err != nil {
logger.Error(c, "密码加密失败!")
return serializer.Err(serializer.CodeEncryptError, "密码加密失败!", err)
}
// 使用事务封装数据库操作
err := s.Db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
return nil
})
if err != nil {
logger.Error(c, "注册失败!")
return serializer.ParamErr("注册失败", err)
}
return types.BuildUserResponse(user)
}
// Create 创建用户
func (s *SysUserService) Create(c *gin.Context) serializer.Response {
var user model.SysUser
if err := c.ShouldBind(&user); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
if user.Status == "" {
user.Status = Active
}
now := time.Now()
user.CreateTime = &now
createBy := c.GetString("id")
user.CreateBy = createBy
id, err := SysSequenceServiceBuilder(user.TableName()).GenerateId()
if err != nil {
return serializer.DBErr("创建用户失败!", err)
}
user.UserId = id
if err := s.Db.Create(&user).Error; err != nil {
logger.Error(c, "创建用户失败!")
return serializer.DBErr("创建用户失败!", err)
}
return serializer.Succ("创建用户成功!", user)
}
// DeleteByID 根据ID删除用户
func (s *SysUserService) DeleteByID(c *gin.Context) serializer.Response {
id := c.Param("id")
if id == "" {
logger.Error(c, "id 不可为空!")
}
// 删除逻辑
data := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": c.GetString("id"),
}
var user model.SysUser
if err := s.Db.Model(&user).Where("user_id = ?", id).Updates(data).Error; err != nil {
logger.Error(c, "未查询用户信息!")
return serializer.DBErr("删除用户失败!", err)
}
return serializer.Succ("删除用户成功!", user)
}
// UpdateByID 根据ID更新用户
func (s *SysUserService) UpdateByID(c *gin.Context) serializer.Response {
var user model.SysUser
if err := c.ShouldBind(&user); err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
id := user.UserId
if id == "" {
logger.Error(c, "id 不可为空!")
return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空"))
}
if err := s.Db.Model(&user).Where("user_id = ?", id).Updates(&user).Error; err != nil {
logger.Error(c, "更新用户信息失败!")
return serializer.DBErr("更新用户信息失败!", err)
}
return serializer.Succ("更新用户信息成功!", user)
}
// GetByID 根据ID获取用户
func (s *SysUserService) GetByID(c *gin.Context) serializer.Response {
var user model.SysUser
if err := s.Db.Where("user_id = ?", c.Param("id")).First(&user).Error; err != nil {
logger.Error(c, "获取用户信息失败!")
return serializer.DBErr("获取用户信息失败!", err)
}
user.PassWord = "" // 清除密码
return serializer.Succ("查询成功!", user)
}
// DeleteByIDs 批量删除用户
func (s *SysUserService) DeleteByIDs(c *gin.Context) serializer.Response {
var ids types.Payload
if err := c.ShouldBind(&ids); err != nil {
return serializer.ParamErr("参数绑定失败!", err)
}
// 删除逻辑
data := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": c.GetString("id"),
}
if err := s.Db.Model(&model.SysUser{}).Where("user_id in (?)", ids.Ids).Updates(data).Error; err != nil {
logger.Error(c, "批量删除用户失败!")
return serializer.DBErr("批量删除用户失败!", err)
}
return serializer.Succ("批量删除用户成功!", nil)
}
// GetByCondition 条件查询用户
func (s *SysUserService) GetByCondition(c *gin.Context) serializer.Response {
var p types.Params
if err := c.ShouldBind(&p); err != nil {
return serializer.ParamErr("参数绑定失败!", err)
}
queryStr, args, err := p.ConvertToGormConditions()
if err != nil {
logger.Error(c, "参数绑定失败!")
return serializer.ParamErr("参数绑定失败!", err)
}
var total int64
var users []model.SysUser
offset := (p.Page - 1) * p.Limit
// 构建基础查询
db := s.Db.Model(&model.SysUser{})
// 如果有查询条件,添加条件
if queryStr != "" {
db = db.Where(queryStr, args...)
}
// 排序
if p.Sort != "" {
db = db.Order(p.Sort)
}
// 执行分页查询
if err := db.Where("del_flag = ?", "0").Offset(offset).Limit(p.Limit).Find(&users).Error; err != nil {
logger.Error(c, "获取用户信息失败!")
return serializer.DBErr("获取用户信息失败!", err)
}
// 清除所有用户的密码
for i := range users {
users[i].PassWord = ""
}
// 执行总数查询
if err := db.Where("del_flag = ?", "0").Count(&total).Error; err != nil {
logger.Error(c, "获取用户总数失败!")
return serializer.DBErr("获取用户总数失败!", err)
}
return serializer.Succ("查询成功!", gin.H{
"total": total,
"items": users,
"page": p.Page,
"limit": p.Limit,
})
}
// ListByIDs 根据ID列表获取用户
func (s *SysUserService) ListByIDs(c *gin.Context) serializer.Response {
var ids types.Payload
if err := c.ShouldBind(&ids); err != nil {
return serializer.ParamErr("参数绑定失败!", err)
}
var users []model.SysUser
if err := s.Db.Where("user_id in (?)", ids.Ids).Find(&users).Error; err != nil {
logger.Error(c, "获取用户信息失败!")
return serializer.DBErr("获取用户信息失败!", err)
}
// 清除所有用户的密码
for i := range users {
users[i].PassWord = ""
}
return serializer.Succ("查询成功!", users)
}
// CurrentUser 获取当前用户
func (s *SysUserService) CurrentUser(c *gin.Context) (*model.SysUser, error) {
tokenString := c.GetHeader("Authorization")
var user model.SysUser
token, err := util.ParseToken(tokenString[8:])
if err != nil {
return nil, fmt.Errorf("获取当前用户失败! %v", err)
}
id := token["id"].(string)
if err := s.Db.Model(&user).Where("user_id = ?", id).First(&user).Error; err != nil {
logger.Error(c, "获取当前用户失败!", zap.Error(err))
return nil, err
}
// 清除密码
user.PassWord = ""
return &user, nil
}

100
internal/types/page.go Normal file
View File

@ -0,0 +1,100 @@
package types
import "strings"
var defaultMaxSize = 1000
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(max int) {
if max < 10 {
max = 10
}
defaultMaxSize = max
}
// Page info
type Page struct {
page int // page number, starting from page 0
limit int // number per page
sort string // sort fields, default is id backwards, you can add - sign before the field to indicate reverse order, no - sign to indicate ascending order, multiple fields separated by comma
}
// Page get page value
func (p *Page) Page() int {
return p.page
}
// Limit number per page
func (p *Page) Limit() int {
return p.limit
}
// Size number per page
// Deprecated: use Limit instead
func (p *Page) Size() int {
return p.limit
}
// Sort get sort field
func (p *Page) Sort() string {
return p.sort
}
// Offset get offset value
func (p *Page) Offset() int {
return p.page * p.limit
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
page: page,
limit: 20,
sort: "id DESC",
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending,
// if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(page int, limit int, columnNames string) *Page {
if page < 0 {
page = 0
}
if limit > defaultMaxSize || limit < 1 {
limit = defaultMaxSize
}
return &Page{
page: page,
limit: limit,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) string {
columnNames = strings.Replace(columnNames, " ", "", -1)
if columnNames == "" {
return "id DESC"
}
names := strings.Split(columnNames, ",")
strs := make([]string, 0, len(names))
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
strs = append(strs, name[1:]+" DESC")
} else {
strs = append(strs, name+" ASC")
}
}
return strings.Join(strs, ", ")
}

View File

@ -0,0 +1,525 @@
package types
import (
"fmt"
"strings"
)
const (
// Eq 等于
Eq = "eq"
// Neq 不等于
Neq = "neq"
// Gt 大于
Gt = "gt"
// Gte 大于等于
Gte = "gte"
// Lt 小于
Lt = "lt"
// Lte 小于等于
Lte = "lte"
// Like 模糊查询
Like = "like"
// In 包含
In = "in"
// AND 逻辑与
AND string = "and"
// OR 逻辑或
OR string = "or"
)
// expMap 表达式映射表将查询条件转换为SQL表达式
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
}
// logicMap 逻辑运算符映射表将逻辑运算符转换为SQL逻辑运算符
var logicMap = map[string]string{
AND: " AND ",
OR: " OR ",
"&": " AND ",
"&&": " AND ",
"|": " OR ",
"||": " OR ",
"AND": " AND ",
"OR": " OR ",
}
// Params 查询参数结构体
type Params struct {
Page int `json:"page" form:"page" binding:"gte=0"` // 页码从0开始
Limit int `json:"limit" form:"limit" binding:"gte=10"` // 每页数量
Sort string `json:"sort,omitempty" form:"sort" binding:""` // 排序字段
Columns []Column `json:"columns,omitempty" form:"columns"` // 查询条件列表
}
// Column 查询条件结构体
type Column struct {
Name string `json:"name" form:"name"` // 字段名
Exp string `json:"exp" form:"exp"` // 表达式类型,默认为等于(=),支持 =, !=, >, >=, <, <=, like, in
Value any `json:"value" form:"value"` // 字段值
Logic string `json:"logic" form:"logic"` // 逻辑运算符默认为and支持 &(and), ||(or)
}
// checkValid 检查查询条件是否有效
func (c *Column) checkValid() error {
if c.Name == "" {
return fmt.Errorf("字段名不能为空")
}
return nil
}
// convert 将表达式类型和逻辑运算符转换为SQL语句
func (c *Column) convert() error {
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return fmt.Errorf("IN查询的值类型无效: '%s'", c.Value)
}
var iVal []any
ss := strings.Split(val, ",")
for _, s := range ss {
iVal = append(iVal, s)
}
c.Value = iVal
}
} else {
return fmt.Errorf("未知的表达式类型: '%s'", c.Exp)
}
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return fmt.Errorf("未知的逻辑运算符类型: '%s'", c.Logic)
}
return nil
}
// ConvertToPage 转换为分页参数
func (p *Params) ConvertToPage() (order string, limit int, offset int) { //nolint
page := NewPage(p.Page, p.Limit, p.Sort)
order = page.sort
limit = page.limit
offset = page.page * page.limit
return //nolint
}
// ConvertToGormConditions 将查询条件转换为GORM兼容的参数
// 忽略最后一个条件的逻辑运算符,无论是单条件还是多条件查询
func (p *Params) ConvertToGormConditions() (string, []any, error) {
str := ""
var args []any
var validColumns []Column
// 过滤掉空值参数
for _, column := range p.Columns {
if column.Value != nil && column.Value != "" {
validColumns = append(validColumns, column)
}
}
l := len(validColumns)
if l == 0 {
return "", nil, nil
}
isUseIN := true
if l == 1 {
isUseIN = false
}
field := validColumns[0].Name
for i, column := range validColumns {
if err := column.checkValid(); err != nil {
return "", nil, err
}
err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // 忽略最后一个条件的逻辑运算符
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
// 当多个条件字段相同时判断是否使用IN查询
if isUseIN {
if field != column.Name {
isUseIN = false
continue
}
if column.Exp != expMap[Eq] {
isUseIN = false
}
}
}
if isUseIN {
str = field + " IN (?)"
args = []any{args}
}
return str, args, nil
}
// Conditions 查询条件结构体
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // 查询条件列表
}
// CheckValid 检查查询条件是否有效
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return fmt.Errorf("查询条件不能为空")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return fmt.Errorf("未知的表达式类型: '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return fmt.Errorf("未知的逻辑运算符类型: '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToGorm 将查询条件转换为GORM兼容的参数
// 忽略最后一个条件的逻辑运算符,无论是单条件还是多条件查询
func (c *Conditions) ConvertToGorm() (string, []any, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToGormConditions()
}
// Payload 通用负载结构体
type Payload struct {
Ids []string `json:"ids"` // ID列表
}
// QueryBuilder 查询构造器,提供链式调用方式
type QueryBuilder struct {
params *Params
}
// NewQueryBuilder 创建查询构造器
func NewQueryBuilder() *QueryBuilder {
return &QueryBuilder{
params: &Params{
Page: 1,
Limit: 10,
Columns: make([]Column, 0),
},
}
}
// Page 设置页码
func (qb *QueryBuilder) Page(page int) *QueryBuilder {
qb.params.Page = page
return qb
}
// Limit 设置每页数量
func (qb *QueryBuilder) Limit(limit int) *QueryBuilder {
qb.params.Limit = limit
return qb
}
// Sort 设置排序
func (qb *QueryBuilder) Sort(sort string) *QueryBuilder {
qb.params.Sort = sort
return qb
}
// Eq 等于条件
func (qb *QueryBuilder) Eq(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Eq,
Value: value,
Logic: AND,
})
return qb
}
// Like 模糊查询条件
func (qb *QueryBuilder) Like(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Like,
Value: value,
Logic: AND,
})
return qb
}
// In 包含条件
func (qb *QueryBuilder) In(name string, values ...any) *QueryBuilder {
var valueStr []string
for _, v := range values {
valueStr = append(valueStr, fmt.Sprintf("%v", v))
}
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: In,
Value: strings.Join(valueStr, ","),
Logic: AND,
})
return qb
}
// Gt 大于条件
func (qb *QueryBuilder) Gt(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Gt,
Value: value,
Logic: AND,
})
return qb
}
// Gte 大于等于条件
func (qb *QueryBuilder) Gte(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Gte,
Value: value,
Logic: AND,
})
return qb
}
// Lt 小于条件
func (qb *QueryBuilder) Lt(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Lt,
Value: value,
Logic: AND,
})
return qb
}
// Lte 小于等于条件
func (qb *QueryBuilder) Lte(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Lte,
Value: value,
Logic: AND,
})
return qb
}
// Neq 不等于条件
func (qb *QueryBuilder) Neq(name string, value any) *QueryBuilder {
qb.params.Columns = append(qb.params.Columns, Column{
Name: name,
Exp: Neq,
Value: value,
Logic: AND,
})
return qb
}
// Or 设置最后一个条件为OR逻辑
func (qb *QueryBuilder) Or() *QueryBuilder {
if len(qb.params.Columns) > 0 {
qb.params.Columns[len(qb.params.Columns)-1].Logic = OR
}
return qb
}
// Build 构建查询参数
func (qb *QueryBuilder) Build() *Params {
return qb.params
}
// ToGormConditions 转换为GORM条件
func (qb *QueryBuilder) ToGormConditions() (string, []any, error) {
return qb.params.ConvertToGormConditions()
}
// URLQuery URL查询参数结构支持通用的URL参数解析
type URLQuery struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Limit int `json:"limit" form:"limit" binding:"gte=10"`
Sort string `json:"sort,omitempty" form:"sort"`
Query string `json:"query,omitempty" form:"query"` // 通用查询字符串格式field1=value1&field2__like=value2
}
// ParseQuery 解析查询字符串为Params
func (uq *URLQuery) ParseQuery() (*Params, error) {
params := &Params{
Page: uq.Page,
Limit: uq.Limit,
Sort: uq.Sort,
Columns: make([]Column, 0),
}
if uq.Query == "" {
return params, nil
}
// 简单的查询字符串解析,支持 field=value 和 field__exp=value 格式
pairs := strings.Split(uq.Query, "&")
for _, pair := range pairs {
kv := strings.Split(pair, "=")
if len(kv) != 2 {
continue
}
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if key == "" || value == "" {
continue
}
// 支持特殊语法字段名__操作符
parts := strings.Split(key, "__")
fieldName := parts[0]
exp := Eq // 默认等于
if len(parts) > 1 {
switch parts[1] {
case "like":
exp = Like
case "gt":
exp = Gt
case "gte":
exp = Gte
case "lt":
exp = Lt
case "lte":
exp = Lte
case "neq":
exp = Neq
case "in":
exp = In
}
}
params.Columns = append(params.Columns, Column{
Name: fieldName,
Exp: exp,
Value: value,
Logic: AND,
})
}
return params, nil
}
// ToGormConditions 转换为GORM条件
func (uq *URLQuery) ToGormConditions() (string, []any, error) {
params, err := uq.ParseQuery()
if err != nil {
return "", nil, err
}
return params.ConvertToGormConditions()
}
// KeyValueQuery 键值对查询结构,支持动态字段查询
type KeyValueQuery struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Limit int `json:"limit" form:"limit" binding:"gte=10"`
Sort string `json:"sort,omitempty" form:"sort"`
Conditions map[string]interface{} `json:"conditions,omitempty" form:"conditions"` // 动态查询条件
}
// ToParams 转换为标准Params结构
func (kv *KeyValueQuery) ToParams() *Params {
params := &Params{
Page: kv.Page,
Limit: kv.Limit,
Sort: kv.Sort,
Columns: make([]Column, 0),
}
for key, value := range kv.Conditions {
if value != nil && value != "" {
// 支持特殊语法字段名__操作符
parts := strings.Split(key, "__")
fieldName := parts[0]
exp := Eq // 默认等于
if len(parts) > 1 {
switch parts[1] {
case "like":
exp = Like
case "gt":
exp = Gt
case "gte":
exp = Gte
case "lt":
exp = Lt
case "lte":
exp = Lte
case "neq":
exp = Neq
case "in":
exp = In
}
}
params.Columns = append(params.Columns, Column{
Name: fieldName,
Exp: exp,
Value: value,
Logic: AND,
})
}
}
return params
}
// ToGormConditions 转换为GORM条件
func (kv *KeyValueQuery) ToGormConditions() (string, []any, error) {
return kv.ToParams().ConvertToGormConditions()
}

53
internal/types/user.go Normal file
View File

@ -0,0 +1,53 @@
package types
import (
"ego/internal/model"
"ego/internal/serializer"
"net/http"
)
// User 用户序列化器
type User struct {
ID string `json:"id"`
UserName string `json:"userName"`
Nickname string `json:"nickName"`
Token string `json:"token"`
Status string `json:"status"`
Avatar string `json:"avatar"`
CreatedAt int64 `json:"createdAt"`
}
// BuildUser 序列化用户
func BuildUser(user model.SysUser) User {
return User{
ID: user.UserId,
UserName: user.UserName,
Nickname: user.NickName,
Status: user.Status,
Avatar: user.Avatar,
CreatedAt: user.CreateTime.Unix(),
}
}
// BuildUserHasToken BuildUser 序列化用户
func BuildUserHasToken(user model.SysUser, token string) User {
return User{
ID: user.UserId,
UserName: user.UserName,
Nickname: user.NickName,
Status: user.Status,
Avatar: user.Avatar,
CreatedAt: user.CreateTime.Unix(),
Token: token,
}
}
// BuildUserResponse 序列化用户响应
func BuildUserResponse(user model.SysUser) serializer.Response {
return serializer.Response{Code: http.StatusOK, Data: BuildUser(user)}
}
// BuildUserResponseHasToken BuildUserResponse 序列化用户响应
func BuildUserResponseHasToken(user model.SysUser, token string) serializer.Response {
return serializer.Response{Code: http.StatusOK, Msg: "登录成功!", Data: BuildUserHasToken(user, token)}
}

23
internal/util/common.go Normal file
View File

@ -0,0 +1,23 @@
package util
import (
"math/rand"
"strings"
"time"
)
// RandStringRunes 返回随机字符串
func RandStringRunes(n int) string {
var letterRunes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
rand.NewSource(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
// GenerateID 生成32位唯一ID
func GenerateID() string {
return strings.ToLower(RandStringRunes(32))
}

157
internal/util/jwt_utils.go Normal file
View File

@ -0,0 +1,157 @@
package util
import (
"ego/pkg/logger"
"fmt"
"os"
"sync"
"time"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
)
const (
TokenExpireTime = 24 * time.Hour // Token 过期时间24小时
// TokenGroup token
TokenGroup = "user_token:"
)
var (
// JwtKey JWT签名密钥
JwtKey []byte
jwtKeyOnce sync.Once
)
// Claims JWT声明结构
type Claims struct {
ID string `json:"id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// initJwtKey 懒加载方式初始化JWT密钥
func initJwtKey() {
jwtKeyOnce.Do(func() {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = "default-jwt-secret-please-change-in-production"
// 注意这里不能使用logger因为可能还没初始化
// 如果需要日志,在配置初始化完成后再输出
}
JwtKey = []byte(secret)
})
}
// getJwtKey 获取JWT密钥确保已初始化
func getJwtKey() []byte {
initJwtKey()
return JwtKey
}
// GenerateToken 生成JWT Token
func GenerateToken(id, username string) (string, error) {
if id == "" || username == "" {
return "", fmt.Errorf("用户ID和用户名不能为空")
}
key := getJwtKey()
expireTime := time.Now().Add(TokenExpireTime)
claims := &Claims{
ID: id,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "ego-system",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(key)
if err != nil {
// 安全地记录错误如果logger已初始化
if logger.Logger != nil {
logger.Error(nil, "JWT Token生成失败", zap.Error(err))
}
return "", fmt.Errorf("token生成失败: %w", err)
}
return tokenString, nil
}
// ParseToken 解析JWT Token
func ParseToken(tokenString string) (jwt.MapClaims, error) {
if tokenString == "" {
return nil, fmt.Errorf("token不能为空")
}
key := getJwtKey()
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("无效的签名方法: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
// 安全地记录错误如果logger已初始化
if logger.Logger != nil {
logger.Error(nil, "JWT Token解析失败", zap.Error(err))
}
return nil, fmt.Errorf("token解析失败: %w", err)
}
if !token.Valid {
return nil, fmt.Errorf("token无效或已过期")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("token claims格式错误")
}
return claims, nil
}
// ValidateToken 验证Token有效性
func ValidateToken(tokenString string) (*Claims, error) {
if tokenString == "" {
return nil, fmt.Errorf("token不能为空")
}
key := getJwtKey()
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("无效的签名方法: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
// 安全地记录错误如果logger已初始化
if logger.Logger != nil {
logger.Error(nil, "JWT Token验证失败", zap.Error(err))
}
return nil, fmt.Errorf("token验证失败: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("token无效")
}
// LogJwtKeyStatus 在配置初始化完成后输出JWT密钥状态日志
func LogJwtKeyStatus() {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
logger.Warn(nil, "使用默认JWT密钥生产环境请设置JWT_SECRET环境变量")
} else {
logger.Info(nil, "JWT密钥已从环境变量加载")
}
}

View File

@ -0,0 +1,76 @@
package util
import (
"ego/pkg/logger"
"time"
"github.com/gin-gonic/gin"
"ego/internal/cache"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// Set 设置键值对到Redis
func Set(ctx *gin.Context, key string, value any, expiration time.Duration) error {
err := cache.RedisClient.Set(ctx, key, value, expiration).Err()
if err != nil {
logger.Error(ctx, "Redis Set 失败", zap.Error(err))
return err
}
return nil
}
// Get 从Redis获取键值对
func Get(ctx *gin.Context, key string) (string, error) {
val, err := cache.RedisClient.Get(ctx, key).Result()
if err == redis.Nil {
logger.Info(ctx, "Redis key 不存在", zap.String("key", key))
return "", nil
} else if err != nil {
logger.Error(ctx, "Redis Get 失败", zap.Error(err))
return "", err
}
return val, nil
}
// Del 从Redis删除键
func Del(ctx *gin.Context, key string) error {
err := cache.RedisClient.Del(ctx, key).Err()
if err != nil {
logger.Error(ctx, "Redis Del 失败", zap.Error(err))
return err
}
return nil
}
// Exists 检查Redis中是否存在键
func Exists(ctx *gin.Context, key string) (bool, error) {
val, err := cache.RedisClient.Exists(ctx, key).Result()
if err != nil {
logger.Error(ctx, "Redis Exists 失败", zap.Error(err))
return false, err
}
return val > 0, nil
}
// Incr 对Redis中的键进行自增操作
func Incr(ctx *gin.Context, key string) (int64, error) {
val, err := cache.RedisClient.Incr(ctx, key).Result()
if err != nil {
logger.Error(ctx, "Redis Incr 失败", zap.Error(err))
return 0, err
}
return val, nil
}
// Decr 对Redis中的键进行自减操作
func Decr(ctx *gin.Context, key string) (int64, error) {
val, err := cache.RedisClient.Decr(ctx, key).Result()
if err != nil {
logger.Error(ctx, "Redis Decr 失败", zap.Error(err))
return 0, err
}
return val, nil
}

View File

@ -0,0 +1,13 @@
package util
import "strings"
// TrimSpaces 去除字符串两端的空格
func TrimSpaces(s string) string {
return strings.ReplaceAll(s, " ", "")
}
// IsStringEmpty 判断字符串是否为空
func IsStringEmpty(s string) bool {
return TrimSpaces(s) == ""
}

View File

@ -0,0 +1,38 @@
package util
import (
"strings"
)
// ParseUserAgent 解析User-Agent字符串返回操作系统信息
func ParseUserAgent(userAgent string) string {
if userAgent == "" {
return "未知"
}
userAgent = strings.ToLower(userAgent)
// 操作系统判断
switch {
case strings.Contains(userAgent, "windows"):
return "Windows"
case strings.Contains(userAgent, "macintosh") || strings.Contains(userAgent, "mac os x"):
return "MacOS"
case strings.Contains(userAgent, "linux"):
return "Linux"
case strings.Contains(userAgent, "android"):
return "Android"
case strings.Contains(userAgent, "iphone") || strings.Contains(userAgent, "ipad") || strings.Contains(userAgent, "ipod"):
return "iOS"
case strings.Contains(userAgent, "freebsd"):
return "FreeBSD"
case strings.Contains(userAgent, "openbsd"):
return "OpenBSD"
case strings.Contains(userAgent, "netbsd"):
return "NetBSD"
case strings.Contains(userAgent, "sunos"):
return "Solaris"
default:
return "未知"
}
}

70
internal/wire/wire.go Normal file
View File

@ -0,0 +1,70 @@
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package wire
import (
"ego/internal/conf"
"ego/internal/handler"
"ego/internal/service"
"github.com/google/wire"
)
// DBSet 提供数据库连接
var DBSet = wire.NewSet(
conf.NewDb, // 依赖 conf 包的 NewDb
)
// HandlerSet 处理器集合
var HandlerSet = wire.NewSet(
handler.NewSysUserHandler,
handler.NewSysLoginLogHandler,
handler.NewSysUploadHandler,
handler.NewSysDeployFileHandler,
)
// UserServiceSet 定义 service 层依赖
var UserServiceSet = wire.NewSet(
service.NewSysUserService,
)
// LoginLogServiceSet 服务器信息服务层依赖
var LoginLogServiceSet = wire.NewSet(
service.NewSysLoginLogService,
)
// UploadServiceSet 文件上传服务层依赖
var UploadServiceSet = wire.NewSet(
service.NewSysUploadService,
)
// DeployFileServiceSet 部署文件服务层依赖
var DeployFileServiceSet = wire.NewSet(
service.NewSysDeployFileService,
)
// InjectSysUserHandler 注入 handler
func InjectSysUserHandler() *handler.SysUserHandler {
panic(wire.Build(
HandlerSet,
UserServiceSet,
DBSet,
))
}
// InjectSysLoginLogHandler 注入登录记录信息处理器
func InjectSysLoginLogHandler() *handler.SysLoginLogHandler {
panic(wire.Build(HandlerSet, LoginLogServiceSet, DBSet))
}
// InjectSysUploadHandler 注入文件上传处理器
func InjectSysUploadHandler() *handler.SysUploadHandler {
panic(wire.Build(HandlerSet, UploadServiceSet, DBSet))
}
// InjectSysDeployFileHandler 注入部署文件处理器
func InjectSysDeployFileHandler() *handler.SysDeployFileHandler {
panic(wire.Build(HandlerSet, DeployFileServiceSet, DBSet))
}

104
logs/ego.20250716.log Normal file
View File

@ -0,0 +1,104 @@
{"level":"info","ts":"2025-07-16 13:54:37.697","caller":"sync/once.go:76","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-07-16 13:54:37.792","caller":"conf/db.go:92","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-07-16 13:54:37.871","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-07-16 13:54:37.871","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-07-16 13:54:37.871","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-07-16 13:54:37.872","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-07-16 13:54:37.872","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-07-16 13:54:37.873","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-07-16 13:54:37.873","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-07-16 13:54:44.112","caller":"ego/main.go:93","msg":"服务器关闭完成"}
{"level":"info","ts":"2025-07-16 13:58:18.971","caller":"sync/once.go:76","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-07-16 13:58:19.104","caller":"conf/db.go:92","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-07-16 13:58:19.157","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 13:58:21.145","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-07-16 13:58:21.146","caller":"ego/main.go:93","msg":"服务器关闭完成"}
{"level":"info","ts":"2025-07-16 14:18:56.26","caller":"sync/once.go:76","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-07-16 14:18:56.401","caller":"conf/db.go:92","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-07-16 14:18:56.464","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-07-16 14:18:56.464","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-07-16 14:18:56.464","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-07-16 14:18:56.464","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-07-16 14:18:56.464","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-07-16 14:18:56.465","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:18:56.465","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:21:00.48","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"60125fcd-186e-402e-b8cc-e0a56984cf7a","method":"POST","path":"/:3000127.0.0.1/upload/zip","status":404,"duration":0,"trackID":"60125fcd-186e-402e-b8cc-e0a56984cf7a"}
{"level":"info","ts":"2025-07-16 14:21:00.559","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001178","trackID":"60125fcd-186e-402e-b8cc-e0a56984cf7a"}
{"level":"info","ts":"2025-07-16 14:22:03.926","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"ca81f555-f462-4133-a957-01ff3beff525","method":"POST","path":"/:3000127.0.0.1/api/v1/upload/zip","status":404,"duration":0,"trackID":"ca81f555-f462-4133-a957-01ff3beff525"}
{"level":"info","ts":"2025-07-16 14:22:03.957","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001179","trackID":"ca81f555-f462-4133-a957-01ff3beff525"}
{"level":"info","ts":"2025-07-16 14:23:27.959","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-07-16 14:23:27.96","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-07-16 14:23:27.961","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-07-16 14:23:27.961","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-07-16 14:23:27.962","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:23:27.962","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:23:27.962","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-07-16 14:23:27.962","caller":"ego/main.go:93","msg":"服务器关闭完成"}
{"level":"info","ts":"2025-07-16 14:23:32.987","caller":"sync/once.go:76","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-07-16 14:23:33.148","caller":"conf/db.go:92","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-07-16 14:23:33.204","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-07-16 14:23:33.204","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-07-16 14:23:33.204","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-07-16 14:23:33.204","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-07-16 14:23:33.204","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-07-16 14:23:33.205","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:23:33.205","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:23:36.633","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"5cf28fd2-49d3-4784-80d3-78fa745ab292","method":"POST","path":"/:3000127.0.0.1/api/v1/upload/zip","status":404,"duration":0,"trackID":"5cf28fd2-49d3-4784-80d3-78fa745ab292"}
{"level":"info","ts":"2025-07-16 14:23:36.741","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001180","trackID":"5cf28fd2-49d3-4784-80d3-78fa745ab292"}
{"level":"info","ts":"2025-07-16 14:23:48.474","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-07-16 14:23:48.474","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-07-16 14:23:48.474","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-07-16 14:23:48.474","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-07-16 14:23:48.475","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:23:48.475","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:23:48.475","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-07-16 14:23:48.475","caller":"ego/main.go:93","msg":"服务器关闭完成"}
{"level":"info","ts":"2025-07-16 14:24:31.605","caller":"sync/once.go:76","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-07-16 14:24:31.728","caller":"conf/db.go:92","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-07-16 14:24:31.774","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-07-16 14:24:31.774","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-07-16 14:24:31.774","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-07-16 14:24:31.774","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-07-16 14:24:31.775","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-07-16 14:24:31.775","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:24:31.775","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-07-16 14:25:05.982","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"76d2a5f2-dd16-40a4-8bdb-9acff8c5f873","method":"POST","path":"/:3000127.0.0.1/api/v1/upload/zip","status":404,"duration":15.5752447,"trackID":"76d2a5f2-dd16-40a4-8bdb-9acff8c5f873"}
{"level":"info","ts":"2025-07-16 14:25:06.018","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001181","trackID":"76d2a5f2-dd16-40a4-8bdb-9acff8c5f873"}
{"level":"error","ts":"2025-07-16 14:26:13.328","caller":"service/sys_user_service.go:62","msg":"参数绑定错误!","error":"EOF","trackID":"64311c08-a64d-4658-84df-fe8c9037c417"}
{"level":"info","ts":"2025-07-16 14:26:13.419","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"64311c08-a64d-4658-84df-fe8c9037c417","method":"POST","path":"/api/v1/user/login","status":200,"duration":2.4723363,"trackID":"64311c08-a64d-4658-84df-fe8c9037c417"}
{"level":"info","ts":"2025-07-16 14:26:13.452","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001182","trackID":"64311c08-a64d-4658-84df-fe8c9037c417"}
{"level":"info","ts":"2025-07-16 14:26:48.039","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"8dc8c313-3973-46f6-9b20-2428288b3a8e","method":"POST","path":"/api/v1/upload/zip","status":200,"duration":2.6992122,"trackID":"8dc8c313-3973-46f6-9b20-2428288b3a8e"}
{"level":"info","ts":"2025-07-16 14:26:48.071","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001183","trackID":"8dc8c313-3973-46f6-9b20-2428288b3a8e"}
{"level":"error","ts":"2025-07-16 14:27:57.545","caller":"service/sys_upload_service.go:46","msg":"目标文件夹已存在!","trackID":"92742f89-9afd-4f1e-8cee-50ab55c547df"}
{"level":"info","ts":"2025-07-16 14:27:57.545","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"92742f89-9afd-4f1e-8cee-50ab55c547df","method":"POST","path":"/api/v1/upload/zip","status":200,"duration":15.0455176,"trackID":"92742f89-9afd-4f1e-8cee-50ab55c547df"}
{"level":"info","ts":"2025-07-16 14:27:57.605","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001184","trackID":"92742f89-9afd-4f1e-8cee-50ab55c547df"}
{"level":"error","ts":"2025-07-16 14:28:09.967","caller":"service/sys_upload_service.go:46","msg":"目标文件夹已存在!","trackID":"3f6df601-8965-4f4a-b791-7ba07e076ada"}
{"level":"info","ts":"2025-07-16 14:28:09.967","caller":"middleware/zap_log.go:25","msg":"Handled request","trackID":"3f6df601-8965-4f4a-b791-7ba07e076ada","method":"POST","path":"/api/v1/upload/zip","status":200,"duration":1.9901575,"trackID":"3f6df601-8965-4f4a-b791-7ba07e076ada"}
{"level":"info","ts":"2025-07-16 14:28:10","caller":"service/sys_oper_log_service.go:167","msg":"保存操作日志成功","OperId":"SOL0000001185","trackID":"3f6df601-8965-4f4a-b791-7ba07e076ada"}
{"level":"info","ts":"2025-07-16 14:29:42.669","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-07-16 14:29:42.669","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-07-16 14:29:42.669","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-07-16 14:29:42.67","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-07-16 14:29:42.67","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:29:42.67","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-07-16 14:29:42.67","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-07-16 14:29:42.67","caller":"ego/main.go:93","msg":"服务器关闭完成"}

17
logs/ego.20250801.log Normal file
View File

@ -0,0 +1,17 @@
{"level":"info","ts":"2025-08-01 16:14:47.443","caller":"sync/once.go:78","msg":"日志系统初始化成功","log_path":"./logs/","log_level":"info"}
{"level":"info","ts":"2025-08-01 16:14:47.69","caller":"conf/db.go:91","msg":"数据库连接成功","connString":"***已遮盖敏感信息***","maxOpenConns":20,"maxIdleConns":10}
{"level":"info","ts":"2025-08-01 16:14:47.783","caller":"cache/cache.go:76","msg":"Redis连接成功","address":"www.suyun.store:6379","db":0,"pool_size":20,"read_timeout":3}
{"level":"info","ts":"2025-08-01 16:14:47.783","caller":"util/jwt_utils.go:155","msg":"JWT密钥已从环境变量加载"}
{"level":"info","ts":"2025-08-01 16:14:47.783","caller":"conf/conf.go:57","msg":"所有配置初始化完成"}
{"level":"info","ts":"2025-08-01 16:14:47.783","caller":"ego/main.go:44","msg":"Gin模式设置完成","mode":"test"}
{"level":"info","ts":"2025-08-01 16:14:47.784","caller":"middleware/cors.go:63","msg":"跨域配置初始化完成","允许方法":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"],"允许头":["Origin","Content-Length","Content-Type","Cookie","Authorization","X-Requested-With","X-CSRF-Token"],"允许来源":[],"MaxAge":43200,"模式":"test"}
{"level":"info","ts":"2025-08-01 16:14:47.784","caller":"ego/main.go:70","msg":"服务器启动成功","addr":":3000"}
{"level":"info","ts":"2025-08-01 16:14:47.784","caller":"ego/main.go:63","msg":"服务器启动中...","addr":":3000"}
{"level":"info","ts":"2025-08-01 16:14:52.21","caller":"ego/main.go:77","msg":"收到关闭信号,开始优雅关闭服务器..."}
{"level":"info","ts":"2025-08-01 16:14:52.21","caller":"ego/main.go:87","msg":"HTTP服务器已优雅关闭"}
{"level":"info","ts":"2025-08-01 16:14:52.21","caller":"conf/conf.go:63","msg":"开始关闭应用程序资源..."}
{"level":"info","ts":"2025-08-01 16:14:52.211","caller":"conf/conf.go:69","msg":"数据库已关闭"}
{"level":"info","ts":"2025-08-01 16:14:52.211","caller":"cache/cache.go:93","msg":"Redis已关闭"}
{"level":"info","ts":"2025-08-01 16:14:52.211","caller":"conf/conf.go:76","msg":"Redis已关闭"}
{"level":"info","ts":"2025-08-01 16:14:52.211","caller":"conf/conf.go:79","msg":"应用程序资源关闭完成"}
{"level":"info","ts":"2025-08-01 16:14:52.211","caller":"ego/main.go:93","msg":"服务器关闭完成"}

170
pkg/logger/logger.go Normal file
View File

@ -0,0 +1,170 @@
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
TRACKED = "trackID"
)
var (
Logger *zap.Logger
loggerMux sync.Once
CallerSkip = 2
initErr error
)
// BuildLogger 初始化日志系统
func BuildLogger() error {
loggerMux.Do(func() {
initErr = initLogger()
})
return initErr
}
// initLogger 实际的日志初始化逻辑
func initLogger() error {
logLevel := getLogLevel()
logPath := os.Getenv("LOG_PATH")
if logPath == "" {
logPath = "./logs" // 默认日志路径
}
// 尝试创建日志目录
if err := os.MkdirAll(logPath, 0755); err != nil {
// 使用标准库的log记录错误因为zap还没初始化
log.Printf("无法创建日志目录 %s: %v", logPath, err)
return fmt.Errorf("无法创建日志目录 %s: %w", logPath, err)
}
// 配置滚动日志按天滚动保留28天
logFileName := filepath.Join(logPath, "ego.%Y%m%d.log")
logs, err := rotatelogs.New(
logFileName,
rotatelogs.WithMaxAge(28*24*time.Hour),
rotatelogs.WithRotationTime(24*time.Hour),
)
if err != nil {
log.Printf("创建滚动日志失败: %v", err)
return fmt.Errorf("创建滚动日志失败: %w", err)
}
// 配置控制台输出
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
consoleSyncer := zapcore.AddSync(os.Stdout)
// 配置文件输出生产环境JSON格式
prodEncoderConfig := zap.NewProductionEncoderConfig()
prodEncoderConfig.EncodeTime = TimeEncoder
prodEncoderConfig.CallerKey = "caller"
prodEncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
prodEncoder := zapcore.NewJSONEncoder(prodEncoderConfig)
fileSyncer := zapcore.AddSync(logs)
// 创建日志核心
core := zapcore.NewTee(
zapcore.NewCore(consoleEncoder, consoleSyncer, logLevel),
zapcore.NewCore(prodEncoder, fileSyncer, logLevel),
)
// 初始化 Logger
Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(CallerSkip))
// 记录初始化成功
Logger.Info("日志系统初始化成功",
zap.String("log_path", logPath),
zap.String("log_level", logLevel.String()),
)
return nil
}
// Debug 记录调试日志
func Debug(c *gin.Context, msg string, fields ...zap.Field) {
if Logger != nil {
logWithTrack(c, zap.DebugLevel, msg, fields...)
}
}
// Info 记录信息日志
func Info(c *gin.Context, msg string, fields ...zap.Field) {
if Logger != nil {
logWithTrack(c, zap.InfoLevel, msg, fields...)
}
}
// Warn 记录警告日志
func Warn(c *gin.Context, msg string, fields ...zap.Field) {
if Logger != nil {
logWithTrack(c, zap.WarnLevel, msg, fields...)
}
}
// Error 记录错误日志
func Error(c *gin.Context, msg string, fields ...zap.Field) {
if Logger != nil {
logWithTrack(c, zap.ErrorLevel, msg, fields...)
}
}
// logWithTrack 封装带 trackID 的日志记录
func logWithTrack(c *gin.Context, level zapcore.Level, msg string, fields ...zap.Field) {
if Logger == nil {
return
}
if c == nil {
Logger.Check(level, msg).Write(fields...)
return
}
trackID := GetTrackID(c)
Logger.Check(level, msg).Write(append(fields, zap.String("trackID", trackID))...)
}
// GetTrackID 获取或生成 trackID
func GetTrackID(c *gin.Context) string {
if trackID, exists := c.Get(TRACKED); exists {
return trackID.(string)
}
if random, err := uuid.NewRandom(); err == nil {
// 生成新的 trackID 并存入上下文
newTrackID := strings.ToLower(random.String())
c.Set(TRACKED, newTrackID)
return newTrackID
}
return ""
}
// getLogLevel 根据环境变量获取日志级别
func getLogLevel() zapcore.Level {
switch strings.ToLower(os.Getenv("LOG_LEVEL")) {
case "debug":
return zap.DebugLevel
case "info":
return zap.InfoLevel
case "warn":
return zap.WarnLevel
case "error":
return zap.ErrorLevel
default:
return zap.InfoLevel // 默认使用 Info 级别
}
}
// TimeEncoder 自定义时间格式
func TimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.999"))
}

View File

@ -0,0 +1,5 @@
@echo off
echo 生成 Swagger 文档...
echo 生成完成!

View File

@ -0,0 +1,5 @@
#!/bin/bash
echo 生成 Swagger 文档...
echo 生成完成!

57
sql/deploy_file.sql Normal file
View File

@ -0,0 +1,57 @@
-- 部署文件记录表
CREATE TABLE `sys_deploy_file` (
`deploy_id` varchar(64) NOT NULL COMMENT '部署ID',
`file_name` varchar(255) NOT NULL COMMENT '原始文件名',
`project_name` varchar(100) NOT NULL COMMENT '项目名称',
`domain` varchar(255) NOT NULL COMMENT '访问域名',
`deploy_path` varchar(500) NOT NULL COMMENT '部署路径',
`file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
`file_hash` varchar(64) DEFAULT NULL COMMENT '文件哈希值',
`status` char(1) DEFAULT '1' COMMENT '状态0停用 1正常 2部署中 3部署失败',
`deploy_status` char(1) DEFAULT '0' COMMENT '部署状态0未部署 1部署成功 2部署失败',
`error_msg` text COMMENT '错误信息',
`version` varchar(50) DEFAULT NULL COMMENT '版本号',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志0代表存在 1代表删除',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deploy_time` datetime DEFAULT NULL COMMENT '部署时间',
`last_access_time` datetime DEFAULT NULL COMMENT '最后访问时间',
`access_count` bigint DEFAULT '0' COMMENT '访问次数',
PRIMARY KEY (`deploy_id`),
KEY `idx_project_name` (`project_name`),
KEY `idx_domain` (`domain`),
KEY `idx_status` (`status`),
KEY `idx_deploy_status` (`deploy_status`),
KEY `idx_create_time` (`create_time`),
KEY `idx_del_flag` (`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部署文件记录表';
-- 插入示例数据
INSERT INTO `sys_deploy_file` (
`deploy_id`,
`file_name`,
`project_name`,
`domain`,
`deploy_path`,
`file_size`,
`status`,
`deploy_status`,
`description`,
`create_by`,
`create_time`
) VALUES (
'DEPLOY001',
'my-project-v1.0.0.zip',
'my-project',
'my-project.unbug.cn',
'/home/my-project',
1048576,
'1',
'1',
'示例项目部署文件',
'admin',
NOW()
);

56
sql/login_log.sql Normal file
View File

@ -0,0 +1,56 @@
create table if not exists ego.sys_login_log
(
id
varchar
(
64
) not null comment '访问ID' primary key,
user_id varchar
(
64
) default '' null comment '用户ID',
ip_addr varchar
(
128
) default '' null comment '登录IP地址',
login_location varchar
(
255
) default '' null comment '登录地点',
browser varchar
(
50
) default '' null comment '浏览器类型',
os varchar
(
50
) default '' null comment '操作系统',
status varchar
(
10
) default '0' null comment '登录状态0成功 1失败',
msg varchar
(
255
) default '' null comment '提示消息',
login_time datetime null comment '登录时间',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注'
) comment '系统访问记录' charset = utf8mb3;
-- 创建索引
create index idx_user_id on ego.sys_login_log (user_id);
create index idx_login_time on ego.sys_login_log (login_time);
create index idx_status on ego.sys_login_log (status);

638
sql/system.sql Normal file
View File

@ -0,0 +1,638 @@
create table if not exists ego.sys_config
(
config_id
varchar
(
64
) not null comment '参数主键'
primary key,
config_name varchar
(
100
) default '' null comment '参数名称',
config_key varchar
(
100
) default '' null comment '参数键名',
config_value varchar
(
500
) default '' null comment '参数键值',
config_type char default 'N' null comment '系统内置Y是 N否',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注'
)
comment '参数配置表' charset = utf8mb3;
create table if not exists ego.sys_dept
(
dept_id
varchar
(
64
) not null comment '部门id'
primary key,
parent_id varchar(64) default '0' null comment '父部门id',
ancestors varchar
(
50
) default '' null comment '祖级列表',
dept_name varchar
(
30
) default '' null comment '部门名称',
order_num int default 0 null comment '显示顺序',
leader varchar
(
20
) null comment '负责人',
phone varchar
(
11
) null comment '联系电话',
email varchar
(
50
) null comment '邮箱',
status char default '0' null comment '部门状态0正常 1停用',
del_flag char default '0' null comment '删除标志0代表存在 2代表删除',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间'
)
comment '部门表' charset = utf8mb3;
create table if not exists ego.sys_dict_data
(
dict_code
varchar
(
64
) not null comment '字典编码'
primary key,
dict_sort int default 0 null comment '字典排序',
dict_label varchar
(
100
) default '' null comment '字典标签',
dict_value varchar
(
100
) default '' null comment '字典键值',
dict_type varchar
(
100
) default '' null comment '字典类型',
css_class varchar
(
100
) null comment '样式属性(其他样式扩展)',
list_class varchar
(
100
) null comment '表格回显样式',
is_default char default 'N' null comment '是否默认Y是 N否',
status char default '0' null comment '状态0正常 1停用',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注'
)
comment '字典数据表' charset = utf8mb3;
create table if not exists ego.sys_dict_type
(
dict_id
varchar
(
64
) not null comment '字典主键'
primary key,
dict_name varchar
(
100
) default '' null comment '字典名称',
dict_type varchar
(
100
) default '' null comment '字典类型',
status char default '0' null comment '状态0正常 1停用',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注',
constraint dict_type
unique
(
dict_type
)
)
comment '字典类型表' charset = utf8mb3;
create table if not exists ego.sys_file
(
id
varchar
(
64
) not null comment '主键'
primary key,
file_name varchar
(
128
) not null comment '文件名称',
file_type varchar
(
32
) null comment '文件类型',
file_size bigint null comment '文件大小',
file_key varchar
(
128
) null comment '文件Key',
type varchar
(
4
) null comment '业务类型',
business_id varchar
(
64
) null comment '业务主键',
business_type varchar
(
32
) null comment '业务类型',
del_flag varchar(32) default '0' null comment '是否删除',
revision int default 0 null comment '乐观锁',
create_by varchar
(
32
) null comment '创建人',
create_time datetime null comment '创建时间',
update_by varchar
(
32
) null comment '更新人',
update_time datetime null comment '更新时间'
)
comment 'sys_file 文件表' charset = utf8mb3;
create index businessId_index
on ego.sys_file (business_id);
create table if not exists ego.sys_job
(
job_id varchar(64) not null comment '任务ID',
job_name varchar(64) default '' not null comment '任务名称',
job_group varchar(64) default 'DEFAULT' not null comment '任务组名',
invoke_target varchar(500) not null comment '调用目标字符串',
cron_expression varchar
(
255
) default '' null comment 'cron执行表达式',
misfire_policy varchar
(
20
) default '3' null comment '计划执行错误策略1立即执行 2执行一次 3放弃执行',
concurrent char default '1' null comment '是否并发执行0允许 1禁止',
status char default '0' null comment '状态0正常 1暂停',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) default '' null comment '备注信息',
primary key (job_id, job_name, job_group)
)
comment '定时任务调度表' charset = utf8mb3;
create table if not exists ego.sys_job_log
(
job_log_id
varchar
(
64
) not null comment '任务日志ID'
primary key,
job_name varchar
(
64
) not null comment '任务名称',
job_group varchar
(
64
) not null comment '任务组名',
invoke_target varchar
(
500
) not null comment '调用目标字符串',
job_message varchar
(
500
) null comment '日志信息',
status char default '0' null comment '执行状态0正常 1失败',
exception_info varchar
(
2000
) default '' null comment '异常信息',
create_time datetime null comment '创建时间'
)
comment '定时任务调度日志表' charset = utf8mb3;
create table if not exists ego.sys_logininfor
(
info_id
varchar
(
64
) not null comment '访问ID'
primary key,
user_name varchar
(
50
) default '' null comment '用户账号',
ipaddr varchar
(
128
) default '' null comment '登录IP地址',
login_location varchar
(
255
) default '' null comment '登录地点',
browser varchar
(
50
) default '' null comment '浏览器类型',
os varchar
(
50
) default '' null comment '操作系统',
status char default '0' null comment '登录状态0成功 1失败',
msg varchar
(
1024
) default '' null comment '提示消息',
login_time datetime null comment '访问时间'
)
comment '系统访问记录' charset = utf8mb3;
create table if not exists ego.sys_menu
(
menu_id
varchar
(
64
) not null comment '菜单ID'
primary key,
menu_name varchar
(
50
) not null comment '菜单名称',
parent_id varchar(64) default '0' null comment '父菜单ID',
order_num int default 0 null comment '显示顺序',
path varchar
(
200
) default '' null comment '路由地址',
component varchar
(
255
) null comment '组件路径',
query varchar
(
255
) null comment '路由参数',
is_frame char(2) default '1' null comment '是否为外链0是 1否',
is_cache char(2) default '0' null comment '是否缓存0缓存 1不缓存',
menu_type char default '' null comment '菜单类型M目录 C菜单 F按钮',
visible char default '0' null comment '菜单状态0显示 1隐藏',
status char default '0' null comment '菜单状态0正常 1停用',
perms varchar
(
100
) null comment '权限标识',
icon varchar(100) default '#' null comment '菜单图标',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) default '' null comment '备注'
)
comment '菜单权限表' charset = utf8mb3;
create table if not exists ego.sys_notice
(
notice_id
varchar
(
64
) not null comment '公告ID'
primary key,
notice_title varchar
(
50
) not null comment '公告标题',
notice_type char not null comment '公告类型1通知 2公告',
notice_content longtext null comment '公告内容',
status char default '0' null comment '公告状态0正常 1关闭',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
255
) null comment '备注'
)
comment '通知公告表' charset = utf8mb3;
create table if not exists ego.sys_oper_log
(
oper_id
varchar
(
64
) not null comment '日志主键'
primary key,
title varchar(50) default '' null comment '模块标题',
business_type int default 0 null comment '业务类型0其它 1新增 2修改 3删除',
method varchar(100) default '' null comment '方法名称',
request_method varchar(64) default '' null comment '请求方式',
operator_type int default 0 null comment '操作类别0其它 1后台用户 2手机端用户',
oper_name varchar(50) default '' null comment '操作人员',
dept_name varchar(50) default '' null comment '部门名称',
oper_url varchar(255) default '' null comment '请求URL',
oper_ip varchar(128) default '' null comment '主机地址',
oper_location varchar(255) default '' null comment '操作地点',
oper_param varchar(2000) default '' null comment '请求参数',
json_result varchar(2000) default '' null comment '返回参数',
status int default 0 null comment '操作状态0正常 1异常',
error_msg varchar(2000) default '' null comment '错误消息',
oper_time datetime null comment '操作时间'
)
comment '操作日志记录' charset = utf8mb3;
create table if not exists ego.sys_post
(
post_id
varchar
(
64
) not null comment '岗位ID'
primary key,
post_code varchar
(
64
) not null comment '岗位编码',
post_name varchar
(
50
) not null comment '岗位名称',
post_sort int not null comment '显示顺序',
status char not null comment '状态0正常 1停用',
create_by varchar(64) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar(64) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注'
)
comment '岗位信息表' charset = utf8mb3;
create table if not exists ego.sys_role
(
role_id
varchar
(
64
) not null comment '角色ID'
primary key,
role_name varchar
(
30
) not null comment '角色名称',
role_key varchar
(
100
) not null comment '角色权限字符串',
role_sort int not null comment '显示顺序',
data_scope char default '1' null comment '数据范围1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限',
menu_check_strictly tinyint(1) default 1 null comment '菜单树选择项是否关联显示',
dept_check_strictly tinyint(1) default 1 null comment '部门树选择项是否关联显示',
status char not null comment '角色状态0正常 1停用',
del_flag char default '0' null comment '删除标志0代表存在 2代表删除',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注'
)
comment '角色信息表' charset = utf8mb3;
create table if not exists ego.sys_role_dept
(
role_id varchar(64) not null comment '角色ID',
dept_id varchar(64) not null comment '部门ID',
primary key (role_id, dept_id)
)
comment '角色和部门关联表' charset = utf8mb3;
create table if not exists ego.sys_role_menu
(
role_id varchar(64) not null comment '角色ID',
menu_id varchar(64) not null comment '菜单ID',
primary key (role_id, menu_id)
)
comment '角色和菜单关联表' charset = utf8mb3;
create table if not exists ego.sys_sequence
(
table_name varchar(32) not null comment '表名',
seq int not null comment '序列',
prefix varchar(32) not null comment '前缀',
primary key (table_name, seq, prefix)
)
comment '系统序列表' charset = utf8mb3;
create table if not exists ego.sys_user
(
user_id
varchar
(
64
) not null comment '用户ID'
primary key,
dept_id varchar
(
64
) null comment '部门ID',
user_name varchar
(
30
) not null comment '用户账号',
nick_name varchar
(
30
) not null comment '用户昵称',
user_type varchar(2) default '00' null comment '用户类型00系统用户',
email varchar
(
50
) default '' null comment '用户邮箱',
phone_number varchar
(
11
) default '' null comment '手机号码',
solt int null comment '排序',
gender char default '0' null comment '用户性别0男 1女 2未知',
avatar varchar
(
100
) default '' null comment '头像地址',
pass_word varchar
(
100
) default '' null comment '密码',
status char default '0' null comment '帐号状态0正常 1停用',
del_flag char default '0' null comment '删除标志0代表存在 2代表删除',
login_ip varchar
(
128
) default '' null comment '最后登录IP',
login_date datetime null comment '最后登录时间',
resource_invoke varchar
(
255
) null comment '资源来源映射,多个用,分割',
create_by varchar
(
64
) default '' null comment '创建者',
create_time datetime null comment '创建时间',
update_by varchar
(
64
) default '' null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar
(
500
) null comment '备注',
select_key varchar
(
64
) null comment '动态验证'
)
comment '用户信息表' charset = utf8mb3;
create table if not exists ego.sys_user_post
(
user_id varchar(64) not null comment '用户ID',
post_id varchar(64) not null comment '岗位ID',
primary key (user_id, post_id)
)
comment '用户与岗位关联表' charset = utf8mb3;
create table if not exists ego.sys_user_role
(
user_id varchar(64) not null comment '用户ID',
role_id varchar(64) not null comment '角色ID',
primary key (user_id, role_id)
)
comment '用户和角色关联表' charset = utf8mb3;
create
definer = root@`%` function ego.CURRVAL(seq_name varchar(30)) returns varchar(50) deterministic
BEGIN
DECLARE
seq_val INT;
DECLARE
prefix_val VARCHAR(32);
SELECT seq, prefix
INTO seq_val, prefix_val
FROM sys_sequence
WHERE table_name = seq_name;
RETURN concat(prefix_val, LPAD(seq_val, 10, '0'));
END;
create
definer = root@`%` function ego.NEXTVAL(seq_name varchar(30)) returns varchar(30) deterministic
BEGIN
UPDATE sys_sequence
SET SEQ = SEQ + 1
WHERE TABLE_NAME = seq_name;
RETURN CURRVAL(seq_name);
END;