init
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 忽略项目根目录下的 .idea 文件夹
|
||||||
|
/.idea/
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Skeleton
|
||||||
|
|
||||||
|
`skeleton/` 是基于 HeTianXia 项目整理出来的最小骨架目录,用来作为后续新项目的起点。
|
||||||
|
|
||||||
|
## 当前包含
|
||||||
|
|
||||||
|
- `main.go`:应用启动入口
|
||||||
|
- `config/`:配置结构与 YAML 示例
|
||||||
|
- `middlewares/`:日志、CORS、JWT 等基础中间件
|
||||||
|
- `database/`:PostgreSQL 和 Redis 连接层
|
||||||
|
- `routes/`:主路由、REST 注册器、WS 挂载点
|
||||||
|
- `utils/`:公共工具,如时区、分页、统一响应
|
||||||
|
- `modules/example/`:最小示例模块
|
||||||
|
- `models/`:最小模型占位,供 Atlas 和后续模块使用
|
||||||
|
- `atlas.hcl` 和 `atlas_loader.go`:Atlas CLI 迁移配置与模型加载入口
|
||||||
|
- `cmd/migrate/`:迁移命令入口占位
|
||||||
|
|
||||||
|
## 目录说明
|
||||||
|
|
||||||
|
### `main.go`
|
||||||
|
保留启动流程骨架:
|
||||||
|
|
||||||
|
- 加载配置
|
||||||
|
- 初始化日志
|
||||||
|
- 初始化 JWT
|
||||||
|
- 初始化时区
|
||||||
|
- 初始化数据库
|
||||||
|
- 初始化 Redis
|
||||||
|
- 挂载路由
|
||||||
|
- 启动 HTTP 服务
|
||||||
|
- 优雅关闭
|
||||||
|
|
||||||
|
### `models/`
|
||||||
|
这里放最小模型占位即可。当前只保留了 `User`,用于:
|
||||||
|
|
||||||
|
- 让 Atlas 有可扫描的模型
|
||||||
|
- 给后续业务模块提供基础实体
|
||||||
|
|
||||||
|
### `atlas_loader.go`
|
||||||
|
这个文件的作用是告诉 Atlas CLI:
|
||||||
|
|
||||||
|
- 当前项目有哪些 GORM 模型
|
||||||
|
- 这些模型对应哪些表结构
|
||||||
|
|
||||||
|
它不是运行时初始化文件,也不是业务逻辑文件。
|
||||||
|
|
||||||
|
## 下一步如何扩展
|
||||||
|
|
||||||
|
1. 新增业务模块时,在 `modules/` 下新建目录,例如 `modules/order`
|
||||||
|
2. 模块内部按需添加 `controller.go`、`service.go`、`types.go`、`utils.go`
|
||||||
|
3. 如果模型变化,先更新 `models/`,再更新 `atlas_loader.go`
|
||||||
|
4. 用 Atlas CLI 生成和应用迁移
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
这个骨架的目标不是一次性把所有能力都放齐,而是先建立稳定的项目边界:
|
||||||
|
|
||||||
|
- 公共能力放 `utils/`
|
||||||
|
- HTTP 行为放 `middlewares/` 和 `routes/`
|
||||||
|
- 数据连接放 `database/`
|
||||||
|
- 业务放 `modules/`
|
||||||
|
- 数据结构放 `models/`
|
||||||
|
|
||||||
|
## 📊 数据库迁移管理
|
||||||
|
|
||||||
|
本项目集成了 **Atlas + GORM Provider** 来实现自动化的数据库迁移管理。
|
||||||
|
|
||||||
|
### 安装 Atlas CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (推荐)
|
||||||
|
brew install ariga/tap/atlas
|
||||||
|
|
||||||
|
# 或使用 go install
|
||||||
|
go install ariga.io/atlas/cmd/atlas@latest
|
||||||
|
|
||||||
|
# 验证安装
|
||||||
|
atlas version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装 GORM Provider 依赖
|
||||||
|
|
||||||
|
需要单独执行,不会被`go mod tidy`命令检测到,因为构建排除了`atlas_loader.go`文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get ariga.io/atlas-provider-gorm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 迁移命令
|
||||||
|
|
||||||
|
#### 0. 基线迁移(首次初始化)
|
||||||
|
|
||||||
|
如果你有一个现有的数据库需要纳入迁移管理,需要先创建基线:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
atlas migrate diff baseline --env local
|
||||||
|
|
||||||
|
atlas migrate hash --env local
|
||||||
|
|
||||||
|
atlas migrate set $(ls migrations/*.sql | head -1 | grep -o '[0-9]\{14\}') --env local
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **基线迁移注意事项**:
|
||||||
|
- **首次使用**: 如果数据库是全新的,直接使用 `apply` 命令即可
|
||||||
|
- **现有数据库**: 必须先设置基线,告诉Atlas当前数据库的状态
|
||||||
|
- **团队协作**: 确保所有团队成员都从同一个基线开始
|
||||||
|
|
||||||
|
**使用场景**:
|
||||||
|
- **场景1 - 全新项目**: 创建数据库 → 直接执行 `apply` 应用所有迁移
|
||||||
|
- **场景2 - 现有数据库**: 已有表结构 → 使用 `hash` 或 `set` 建立基线 → 继续正常迁移
|
||||||
|
- **场景3 - 团队加入**: 新成员 → 克隆代码 → 创建本地数据库 → 执行 `apply`
|
||||||
|
- **场景4 - 生产部署**: 已有生产数据 → 谨慎设置基线 → 应用新迁移
|
||||||
|
|
||||||
|
#### 1. 查看迁移状态
|
||||||
|
```bash
|
||||||
|
# 检查当前迁移状态
|
||||||
|
go run cmd/migrate/main.go -action status
|
||||||
|
|
||||||
|
# 指定环境
|
||||||
|
go run cmd/migrate/main.go -action status -env production
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 生成迁移文件
|
||||||
|
```bash
|
||||||
|
# 基于当前模型生成迁移(自动命名)
|
||||||
|
go run cmd/migrate/main.go -action diff
|
||||||
|
|
||||||
|
# 生成带自定义名称的迁移
|
||||||
|
go run cmd/migrate/main.go -action diff -name create_users_table
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 应用迁移
|
||||||
|
```bash
|
||||||
|
# 应用所有待执行的迁移
|
||||||
|
go run cmd/migrate/main.go -action apply
|
||||||
|
|
||||||
|
# 模拟执行(显示将要执行的SQL,不实际执行)
|
||||||
|
go run cmd/migrate/main.go -action apply -dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 验证迁移
|
||||||
|
```bash
|
||||||
|
# 验证迁移文件的有效性
|
||||||
|
go run cmd/migrate/main.go -action validate
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 重置迁移历史(危险操作)
|
||||||
|
```bash
|
||||||
|
# 显示重置指导(不会直接执行)
|
||||||
|
go run cmd/migrate/main.go -action reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
- **atlas.hcl**: Atlas 主配置文件,定义数据源和环境
|
||||||
|
- **atlas_loader.go**: GORM 模型加载器,需要在此文件中注册所有数据模型
|
||||||
|
- **migrations/**: 存储生成的迁移文件
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
|
||||||
|
#### 本地开发环境 (local)
|
||||||
|
修改 `atlas.hcl` 中的 `env "local"` 配置:
|
||||||
|
```hcl
|
||||||
|
env "local" {
|
||||||
|
url = "postgres://username:password@localhost:5432/your_database?sslmode=disable" // 开发数据库
|
||||||
|
dev = "postgres://username:password@localhost:5432/dev_database?sslmode=disable" // 计算差异数据库
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 生产环境 (production)
|
||||||
|
```bash
|
||||||
|
# 设置环境变量
|
||||||
|
export DATABASE_URL="postgres://user:pass@host:port/db?sslmode=require"
|
||||||
|
|
||||||
|
# 应用迁移
|
||||||
|
go run cmd/migrate/main.go -action apply -env production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新模型
|
||||||
|
|
||||||
|
1. 在 `models/` 目录下创建新的模型文件
|
||||||
|
2. 在 `atlas_loader.go` 中注册新模型:
|
||||||
|
```go
|
||||||
|
stmts, err := gormschema.New("postgres").Load(
|
||||||
|
&models.User{},
|
||||||
|
&models.NewModel{}, // 添加新模型
|
||||||
|
)
|
||||||
|
```
|
||||||
|
3. 生成迁移:`go run cmd/migrate/main.go -action diff -name add_new_model`
|
||||||
|
4. 应用迁移:`go run cmd/migrate/main.go -action apply`
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
⚠️ **重要提醒**:
|
||||||
|
|
||||||
|
1. **备份数据库**: 在生产环境应用迁移前务必备份数据库
|
||||||
|
2. **测试迁移**: 先在开发环境测试迁移的正确性
|
||||||
|
3. **版本控制**: 迁移文件应纳入版本控制系统
|
||||||
|
4. **环境隔离**: 不同环境使用不同的数据库连接
|
||||||
|
5. **回滚策略**: Atlas 支持迁移回滚,但需要谨慎操作
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**Q: 如何回滚迁移?**
|
||||||
|
```bash
|
||||||
|
# 回滚到指定版本
|
||||||
|
atlas migrate down --env local --to-version 20231201120000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 如何重置迁移历史?**
|
||||||
|
```bash
|
||||||
|
# 删除迁移历史表(谨慎操作)
|
||||||
|
atlas migrate reset --env local
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 生产环境迁移失败怎么办?**
|
||||||
|
1. 检查迁移文件语法
|
||||||
|
2. 确认数据库权限
|
||||||
|
3. 查看详细错误日志
|
||||||
|
4. 必要时手动修复数据库状态
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
data "external_schema" "gorm" {
|
||||||
|
program = [
|
||||||
|
"go",
|
||||||
|
"run",
|
||||||
|
"-mod=mod",
|
||||||
|
"atlas_loader.go",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
env "local" {
|
||||||
|
src = data.external_schema.gorm.url
|
||||||
|
dev = getenv("DATABASE_DEV_URL")
|
||||||
|
url = getenv("DATABASE_URL")
|
||||||
|
exclude = []
|
||||||
|
migration {
|
||||||
|
dir = "file://migrations"
|
||||||
|
}
|
||||||
|
format {
|
||||||
|
migrate {
|
||||||
|
diff = "{{ sql . \" \" }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dev_url_clean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
env "production" {
|
||||||
|
src = data.external_schema.gorm.url
|
||||||
|
url = getenv("DATABASE_URL")
|
||||||
|
exclude = []
|
||||||
|
migration {
|
||||||
|
dir = "file://migrations"
|
||||||
|
}
|
||||||
|
format {
|
||||||
|
migrate {
|
||||||
|
diff = "{{ sql . \" \" }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "DATABASE_URL" {
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "DATABASE_DEV_URL" {
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"ariga.io/atlas-provider-gorm/gormschema"
|
||||||
|
|
||||||
|
"skeleton/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
stmts, err := gormschema.New("postgres").Load(
|
||||||
|
&models.User{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to load gorm schema: %v", err)
|
||||||
|
}
|
||||||
|
io.WriteString(os.Stdout, stmts)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
env = flag.String("env", "local", "环境配置 (local/production)")
|
||||||
|
action = flag.String("action", "", "操作类型: status, diff, apply, validate, reset")
|
||||||
|
name = flag.String("name", "", "迁移名称 (仅用于 diff 操作)")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *action == "" {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *action {
|
||||||
|
case "status":
|
||||||
|
fmt.Printf("atlas migrate status --env %s\n", *env)
|
||||||
|
case "diff":
|
||||||
|
if *name != "" {
|
||||||
|
fmt.Printf("atlas migrate diff %s --env %s\n", *name, *env)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("atlas migrate diff --env %s\n", *env)
|
||||||
|
}
|
||||||
|
case "apply":
|
||||||
|
fmt.Printf("atlas migrate apply --env %s\n", *env)
|
||||||
|
case "validate":
|
||||||
|
fmt.Printf("atlas migrate validate --env %s\n", *env)
|
||||||
|
case "reset":
|
||||||
|
fmt.Printf("atlas migrate reset --env %s\n", *env)
|
||||||
|
default:
|
||||||
|
fmt.Printf("未知操作: %s\n", *action)
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println("数据库迁移管理工具")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("用法:")
|
||||||
|
fmt.Println(" go run cmd/migrate/main.go -action <操作> [选项]")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("操作:")
|
||||||
|
fmt.Println(" status 检查迁移状态")
|
||||||
|
fmt.Println(" diff 生成迁移文件 (可选: -name <迁移名称>)")
|
||||||
|
fmt.Println(" apply 应用迁移")
|
||||||
|
fmt.Println(" validate 验证迁移文件")
|
||||||
|
fmt.Println(" reset 重置迁移历史 (仅显示提示)")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("选项:")
|
||||||
|
fmt.Println(" -env 环境配置 (默认: local)")
|
||||||
|
fmt.Println(" -name 迁移名称 (仅用于 diff)")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("示例:")
|
||||||
|
fmt.Println(" go run cmd/migrate/main.go -action status")
|
||||||
|
fmt.Println(" go run cmd/migrate/main.go -action diff -name create_users")
|
||||||
|
fmt.Println(" go run cmd/migrate/main.go -action apply")
|
||||||
|
fmt.Println(" go run cmd/migrate/main.go -action validate")
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
app:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 8080
|
||||||
|
version: "0.1.0"
|
||||||
|
debug: true
|
||||||
|
timezone: "UTC"
|
||||||
|
environment: "development"
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: "info"
|
||||||
|
format: "console" # console, json
|
||||||
|
output: "stdout" # stdout, file
|
||||||
|
filename: "logs/app.log"
|
||||||
|
max_size: 100
|
||||||
|
max_age: 30
|
||||||
|
max_backups: 3
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
database:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 2000
|
||||||
|
username: "root"
|
||||||
|
password: "123456"
|
||||||
|
dbname: "skeleton"
|
||||||
|
sslmode: "disable"
|
||||||
|
max_idle_conns: 10
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 60
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 2001
|
||||||
|
password: "123456"
|
||||||
|
database: 0
|
||||||
|
pool_size: 10
|
||||||
|
min_idle_conns: 5
|
||||||
|
max_retries: 3
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: "change-this-secret-key-in-production"
|
||||||
|
expire_hours: 24
|
||||||
|
issuer: "HeTianXia"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
Timezone string `yaml:"timezone"`
|
||||||
|
Environment string `yaml:"environment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggerConfig struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
Output string `yaml:"output"`
|
||||||
|
Filename string `yaml:"filename"`
|
||||||
|
MaxSize int `yaml:"max_size"`
|
||||||
|
MaxAge int `yaml:"max_age"`
|
||||||
|
MaxBackups int `yaml:"max_backups"`
|
||||||
|
Compress bool `yaml:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
DBName string `yaml:"dbname"`
|
||||||
|
SSLMode string `yaml:"sslmode"`
|
||||||
|
MaxIdleConns int `yaml:"max_idle_conns"`
|
||||||
|
MaxOpenConns int `yaml:"max_open_conns"`
|
||||||
|
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Database int `yaml:"database"`
|
||||||
|
PoolSize int `yaml:"pool_size"`
|
||||||
|
MinIdleConns int `yaml:"min_idle_conns"`
|
||||||
|
MaxRetries int `yaml:"max_retries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
ExpireHours int `yaml:"expire_hours"`
|
||||||
|
Issuer string `yaml:"issuer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
App AppConfig `yaml:"app"`
|
||||||
|
Logger LoggerConfig `yaml:"logger"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
Redis RedisConfig `yaml:"redis"`
|
||||||
|
JWT JWTConfig `yaml:"jwt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
data, err := os.ReadFile("config/app.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.setDefaults()
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) setDefaults() {
|
||||||
|
if c.App.Host == "" {
|
||||||
|
c.App.Host = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if c.App.Port == 0 {
|
||||||
|
c.App.Port = 8080
|
||||||
|
}
|
||||||
|
if c.App.Version == "" {
|
||||||
|
c.App.Version = "0.1.0"
|
||||||
|
}
|
||||||
|
if c.App.Timezone == "" {
|
||||||
|
c.App.Timezone = "UTC"
|
||||||
|
}
|
||||||
|
if c.App.Environment == "" {
|
||||||
|
c.App.Environment = "development"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Logger.Level == "" {
|
||||||
|
c.Logger.Level = "info"
|
||||||
|
}
|
||||||
|
if c.Logger.Format == "" {
|
||||||
|
c.Logger.Format = "console"
|
||||||
|
}
|
||||||
|
if c.Logger.Output == "" {
|
||||||
|
c.Logger.Output = "stdout"
|
||||||
|
}
|
||||||
|
if c.Logger.MaxSize == 0 {
|
||||||
|
c.Logger.MaxSize = 100
|
||||||
|
}
|
||||||
|
if c.Logger.MaxAge == 0 {
|
||||||
|
c.Logger.MaxAge = 30
|
||||||
|
}
|
||||||
|
if c.Logger.MaxBackups == 0 {
|
||||||
|
c.Logger.MaxBackups = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Database.SSLMode == "" {
|
||||||
|
c.Database.SSLMode = "disable"
|
||||||
|
}
|
||||||
|
if c.Database.MaxIdleConns == 0 {
|
||||||
|
c.Database.MaxIdleConns = 10
|
||||||
|
}
|
||||||
|
if c.Database.MaxOpenConns == 0 {
|
||||||
|
c.Database.MaxOpenConns = 100
|
||||||
|
}
|
||||||
|
if c.Database.ConnMaxLifetime == 0 {
|
||||||
|
c.Database.ConnMaxLifetime = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Redis.Host == "" {
|
||||||
|
c.Redis.Host = "localhost"
|
||||||
|
}
|
||||||
|
if c.Redis.Port == 0 {
|
||||||
|
c.Redis.Port = 6379
|
||||||
|
}
|
||||||
|
if c.Redis.Database == 0 {
|
||||||
|
c.Redis.Database = 0
|
||||||
|
}
|
||||||
|
if c.Redis.PoolSize == 0 {
|
||||||
|
c.Redis.PoolSize = 10
|
||||||
|
}
|
||||||
|
if c.Redis.MinIdleConns == 0 {
|
||||||
|
c.Redis.MinIdleConns = 5
|
||||||
|
}
|
||||||
|
if c.Redis.MaxRetries == 0 {
|
||||||
|
c.Redis.MaxRetries = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.JWT.Secret == "" {
|
||||||
|
c.JWT.Secret = "change-this-secret-key-in-production"
|
||||||
|
}
|
||||||
|
if c.JWT.ExpireHours == 0 {
|
||||||
|
c.JWT.ExpireHours = 24
|
||||||
|
}
|
||||||
|
if c.JWT.Issuer == "" {
|
||||||
|
c.JWT.Issuer = "HeTianXia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetAddr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.App.Host, c.App.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetDSN() string {
|
||||||
|
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
c.Database.Host,
|
||||||
|
c.Database.Port,
|
||||||
|
c.Database.Username,
|
||||||
|
c.Database.Password,
|
||||||
|
c.Database.DBName,
|
||||||
|
c.Database.SSLMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"skeleton/config"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
// Init 初始化数据库连接
|
||||||
|
func Init(cfg *config.DatabaseConfig, log *zap.Logger) error {
|
||||||
|
// 构建DSN
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.DBName, cfg.SSLMode)
|
||||||
|
|
||||||
|
// 配置GORM日志
|
||||||
|
gormLogger := logger.New(
|
||||||
|
&GormZapWriter{Logger: log},
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Second,
|
||||||
|
LogLevel: logger.Info,
|
||||||
|
Colorful: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 打开数据库连接
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接数据库失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取底层的sql.DB以配置连接池
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取底层数据库连接失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置连接池
|
||||||
|
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Minute)
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := sqlDB.PingContext(ctx); err != nil {
|
||||||
|
return fmt.Errorf("数据库连接测试失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DB = db
|
||||||
|
|
||||||
|
log.Info("数据库连接初始化成功",
|
||||||
|
zap.String("host", cfg.Host),
|
||||||
|
zap.Int("port", cfg.Port),
|
||||||
|
zap.String("database", cfg.DBName),
|
||||||
|
zap.Int("max_idle_conns", cfg.MaxIdleConns),
|
||||||
|
zap.Int("max_open_conns", cfg.MaxOpenConns),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func Close() error {
|
||||||
|
if DB != nil {
|
||||||
|
sqlDB, err := DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB 获取数据库实例
|
||||||
|
func GetDB() *gorm.DB {
|
||||||
|
return DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// GormZapWriter GORM的Zap日志写入器
|
||||||
|
type GormZapWriter struct {
|
||||||
|
Logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GormZapWriter) Printf(format string, args ...interface{}) {
|
||||||
|
g.Logger.Info(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"skeleton/config"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisClient Redis客户端封装
|
||||||
|
type RedisClient struct {
|
||||||
|
client *redis.Client
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var redisClient *RedisClient
|
||||||
|
|
||||||
|
// InitRedis 初始化Redis客户端
|
||||||
|
func InitRedis(cfg *config.Config, logger *zap.Logger) error {
|
||||||
|
// 创建Redis客户端配置
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port),
|
||||||
|
Password: cfg.Redis.Password,
|
||||||
|
DB: cfg.Redis.Database,
|
||||||
|
PoolSize: cfg.Redis.PoolSize,
|
||||||
|
MinIdleConns: cfg.Redis.MinIdleConns,
|
||||||
|
MaxRetries: cfg.Redis.MaxRetries,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := rdb.Ping(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Redis连接失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局Redis客户端实例
|
||||||
|
redisClient = &RedisClient{
|
||||||
|
client: rdb,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Redis客户端初始化成功",
|
||||||
|
zap.String("host", cfg.Redis.Host),
|
||||||
|
zap.Int("port", cfg.Redis.Port),
|
||||||
|
zap.Int("database", cfg.Redis.Database),
|
||||||
|
zap.Int("pool_size", cfg.Redis.PoolSize))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedisClient 获取Redis客户端实例
|
||||||
|
func GetRedisClient() *RedisClient {
|
||||||
|
return redisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭Redis连接
|
||||||
|
func (r *RedisClient) Close() error {
|
||||||
|
if r != nil && r.client != nil {
|
||||||
|
return r.client.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
module skeleton
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/MUKE-coder/gin-docs v0.0.0-20260222113017-4d647cb4e7aa
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.14.0 // 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.6 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // 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/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.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
github.com/MUKE-coder/gin-docs v0.0.0-20260222113017-4d647cb4e7aa h1:4V13TcfRa3Y3eM0PQ/vbuB4Af9xQFgcAXMGnVZmb8Kg=
|
||||||
|
github.com/MUKE-coder/gin-docs v0.0.0-20260222113017-4d647cb4e7aa/go.mod h1:AZfgA2X2WWyAXvKox43IZdAi4fo+vvjtZnS0/NdHie0=
|
||||||
|
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.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
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.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
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/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
|
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
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.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYrUF0QU=
|
||||||
|
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
|
||||||
|
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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
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=
|
||||||
|
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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
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.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/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"skeleton/config"
|
||||||
|
"skeleton/database"
|
||||||
|
"skeleton/middlewares"
|
||||||
|
"skeleton/models"
|
||||||
|
"skeleton/routes"
|
||||||
|
_ "skeleton/routes/rest" // 导入触发 init() 自动注册路由
|
||||||
|
"skeleton/utils"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/MUKE-coder/gin-docs/gindocs"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 加载配置
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("加载配置失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化日志系统
|
||||||
|
if err := middlewares.InitLogger(&cfg.Logger); err != nil {
|
||||||
|
log.Fatal("初始化日志系统失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化JWT配置
|
||||||
|
middlewares.InitJWT(cfg.JWT.Secret, cfg.JWT.ExpireHours, cfg.JWT.Issuer)
|
||||||
|
|
||||||
|
// 确保在程序退出时同步日志缓冲区
|
||||||
|
defer middlewares.Sync()
|
||||||
|
|
||||||
|
// 使用结构化日志记录启动信息
|
||||||
|
zap.L().Info("应用程序启动",
|
||||||
|
zap.String("version", cfg.App.Version),
|
||||||
|
zap.String("environment", cfg.App.Environment),
|
||||||
|
zap.Bool("debug", cfg.App.Debug),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化时区
|
||||||
|
if err := utils.InitTimezone(cfg.App.Timezone); err != nil {
|
||||||
|
zap.L().Warn("时区初始化警告", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zap.L().Info("时区设置成功", zap.String("timezone", cfg.App.Timezone))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
if err := database.Init(&cfg.Database, zap.L()); err != nil {
|
||||||
|
zap.L().Fatal("数据库初始化失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Redis服务
|
||||||
|
if err := database.InitRedis(cfg, zap.L()); err != nil {
|
||||||
|
zap.L().Fatal("Redis服务初始化失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置路由
|
||||||
|
r := routes.SetupRoutes(cfg)
|
||||||
|
|
||||||
|
// 挂载接口文档
|
||||||
|
gindocs.Mount(r, database.GetDB(), gindocs.Config{
|
||||||
|
Title: "Skeleton API",
|
||||||
|
Description: "Skeleton API documentation",
|
||||||
|
Version: cfg.App.Version,
|
||||||
|
Models: []interface{}{
|
||||||
|
models.User{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建HTTP服务器
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: cfg.GetAddr(),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
go func() {
|
||||||
|
zap.L().Info("HTTP 服务器启动",
|
||||||
|
zap.String("address", cfg.GetAddr()),
|
||||||
|
zap.String("version", cfg.App.Version),
|
||||||
|
zap.String("environment", cfg.App.Environment),
|
||||||
|
zap.Bool("debug", cfg.App.Debug),
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Printf("服务器启动在: %s (版本: %s, 环境: %s)\n",
|
||||||
|
cfg.GetAddr(), cfg.App.Version, cfg.App.Environment)
|
||||||
|
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
zap.L().Fatal("启动服务器失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待关闭信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
zap.L().Info("正在关闭服务器...")
|
||||||
|
gracefulShutdown(srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gracefulShutdown 执行优雅关闭
|
||||||
|
func gracefulShutdown(srv *http.Server) {
|
||||||
|
// 设置关闭超时
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 1. 关闭HTTP服务器
|
||||||
|
zap.L().Info("正在关闭HTTP服务器...")
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
zap.L().Error("HTTP服务器关闭失败", zap.Error(err))
|
||||||
|
// 强制关闭
|
||||||
|
if err := srv.Close(); err != nil {
|
||||||
|
zap.L().Error("强制关闭HTTP服务器失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zap.L().Info("HTTP服务器关闭成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 关闭数据库连接
|
||||||
|
if err := database.Close(); err != nil {
|
||||||
|
zap.L().Error("关闭数据库连接失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zap.L().Info("数据库连接已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 关闭Redis连接
|
||||||
|
if redisClient := database.GetRedisClient(); redisClient != nil {
|
||||||
|
if err := redisClient.Close(); err != nil {
|
||||||
|
zap.L().Error("关闭Redis连接失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zap.L().Info("Redis连接已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 同步日志缓冲区
|
||||||
|
middlewares.Sync()
|
||||||
|
|
||||||
|
zap.L().Info("应用程序已优雅关闭")
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORS 跨域中间件
|
||||||
|
func CORS() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
|
||||||
|
// 设置CORS头部
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
// 处理预检请求
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
jwtSecret []byte
|
||||||
|
jwtIssuer string
|
||||||
|
expireHours int
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTClaims JWT声明结构
|
||||||
|
type JWTClaims struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitJWT 初始化JWT配置
|
||||||
|
func InitJWT(secret string, expire int, issuer string) {
|
||||||
|
jwtSecret = []byte(secret)
|
||||||
|
expireHours = expire
|
||||||
|
jwtIssuer = issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken 生成JWT token
|
||||||
|
func GenerateToken(userID int, username string) (string, error) {
|
||||||
|
claims := JWTClaims{
|
||||||
|
UserID: userID,
|
||||||
|
Username: username,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: jwtIssuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToken 解析JWT token
|
||||||
|
func ParseToken(tokenString string) (*JWTClaims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return jwtSecret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, jwt.ErrTokenInvalidClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTAuth JWT认证中间件
|
||||||
|
func JWTAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 从Header获取token
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
Logger.Warn("JWT认证失败:缺少Authorization头部",
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("client_ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 401,
|
||||||
|
"message": "缺少认证token",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer token格式检查
|
||||||
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
Logger.Warn("JWT认证失败:token格式错误",
|
||||||
|
zap.String("auth_header", authHeader),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("client_ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 401,
|
||||||
|
"message": "token格式错误",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
|
// 解析token
|
||||||
|
claims, err := ParseToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
Logger.Warn("JWT认证失败:token解析错误",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("client_ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 401,
|
||||||
|
"message": "token无效或已过期",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息存储到上下文
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Set("username", claims.Username)
|
||||||
|
|
||||||
|
Logger.Debug("JWT认证成功",
|
||||||
|
zap.Int("user_id", claims.UserID),
|
||||||
|
zap.String("username", claims.Username),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentUser 从上下文获取当前用户信息
|
||||||
|
func GetCurrentUser(c *gin.Context) (int, string, bool) {
|
||||||
|
userID, userExists := c.Get("user_id")
|
||||||
|
username, nameExists := c.Get("username")
|
||||||
|
|
||||||
|
if !userExists || !nameExists {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID.(int), username.(string), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"skeleton/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Logger *zap.Logger
|
||||||
|
|
||||||
|
// InitLogger 初始化日志系统
|
||||||
|
func InitLogger(cfg *config.LoggerConfig) error {
|
||||||
|
// 配置编码器
|
||||||
|
var encoderConfig zapcore.EncoderConfig
|
||||||
|
if cfg.Format == "json" {
|
||||||
|
encoderConfig = zap.NewProductionEncoderConfig()
|
||||||
|
} else {
|
||||||
|
encoderConfig = zap.NewDevelopmentEncoderConfig()
|
||||||
|
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
|
}
|
||||||
|
encoderConfig.TimeKey = "timestamp"
|
||||||
|
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||||
|
|
||||||
|
// 选择编码器
|
||||||
|
var encoder zapcore.Encoder
|
||||||
|
if cfg.Format == "json" {
|
||||||
|
encoder = zapcore.NewJSONEncoder(encoderConfig)
|
||||||
|
} else {
|
||||||
|
encoder = zapcore.NewConsoleEncoder(encoderConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置日志级别
|
||||||
|
level := zap.InfoLevel
|
||||||
|
switch strings.ToLower(cfg.Level) {
|
||||||
|
case "debug":
|
||||||
|
level = zap.DebugLevel
|
||||||
|
case "info":
|
||||||
|
level = zap.InfoLevel
|
||||||
|
case "warn":
|
||||||
|
level = zap.WarnLevel
|
||||||
|
case "error":
|
||||||
|
level = zap.ErrorLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置输出
|
||||||
|
var writeSyncer zapcore.WriteSyncer
|
||||||
|
if cfg.Output == "file" {
|
||||||
|
// 创建日志目录
|
||||||
|
logDir := filepath.Dir(cfg.Filename)
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建带日期的文件名模式
|
||||||
|
ext := filepath.Ext(cfg.Filename)
|
||||||
|
nameWithoutExt := strings.TrimSuffix(cfg.Filename, ext)
|
||||||
|
|
||||||
|
// 创建按日期轮转的日志写入器
|
||||||
|
writer, err := rotatelogs.New(
|
||||||
|
nameWithoutExt+".%Y%m%d"+ext,
|
||||||
|
rotatelogs.WithLinkName(cfg.Filename),
|
||||||
|
rotatelogs.WithRotationTime(24*time.Hour),
|
||||||
|
rotatelogs.WithMaxAge(time.Duration(cfg.MaxAge)*24*time.Hour),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSyncer = zapcore.AddSync(writer)
|
||||||
|
} else {
|
||||||
|
writeSyncer = zapcore.AddSync(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建core
|
||||||
|
core := zapcore.NewCore(encoder, writeSyncer, level)
|
||||||
|
|
||||||
|
// 创建logger
|
||||||
|
Logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||||
|
|
||||||
|
// 设置为全局logger,这样就可以使用 zap.L() 访问
|
||||||
|
zap.ReplaceGlobals(Logger)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinLogger 返回gin的日志中间件
|
||||||
|
func GinLogger() gin.HandlerFunc {
|
||||||
|
return gin.LoggerWithWriter(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinRecovery 返回gin的恢复中间件
|
||||||
|
func GinRecovery() gin.HandlerFunc {
|
||||||
|
return gin.RecoveryWithWriter(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorLogging 错误响应日志中间件
|
||||||
|
func ErrorLogging() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 记录错误状态码的响应
|
||||||
|
if c.Writer.Status() >= 400 {
|
||||||
|
Logger.Warn("HTTP错误响应",
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("path", c.Request.URL.Path),
|
||||||
|
zap.String("client_ip", c.ClientIP()),
|
||||||
|
zap.Int("status_code", c.Writer.Status()),
|
||||||
|
zap.String("user_agent", c.GetHeader("User-Agent")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync 同步日志缓冲区
|
||||||
|
func Sync() {
|
||||||
|
if Logger != nil {
|
||||||
|
Logger.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// User 最小用户模型占位
|
||||||
|
type User struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Username string `gorm:"size:64;not null;uniqueIndex" json:"username"`
|
||||||
|
Password string `gorm:"size:255;not null" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回表名
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"skeleton/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExampleController 示例控制器
|
||||||
|
type ExampleController struct {
|
||||||
|
service *ExampleService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExampleController 创建示例控制器
|
||||||
|
func NewExampleController() *ExampleController {
|
||||||
|
return &ExampleController{
|
||||||
|
service: NewExampleService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hello 示例接口
|
||||||
|
func (c *ExampleController) Hello(ctx *gin.Context) {
|
||||||
|
ctx.JSON(http.StatusOK, utils.Success(c.service.Hello()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
// ExampleService 示例服务
|
||||||
|
type ExampleService struct{}
|
||||||
|
|
||||||
|
// NewExampleService 创建示例服务
|
||||||
|
func NewExampleService() *ExampleService {
|
||||||
|
return &ExampleService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hello 返回示例消息
|
||||||
|
func (s *ExampleService) Hello() HelloResponse {
|
||||||
|
return HelloResponse{
|
||||||
|
Message: "hello from skeleton example",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
// HelloResponse 示例响应
|
||||||
|
type HelloResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
// normalizeMessage 示例模块私有工具
|
||||||
|
func normalizeMessage(message string) string {
|
||||||
|
return message
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"skeleton/modules/example"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterPublic(registerExampleRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerExampleRoutes(r *gin.RouterGroup) {
|
||||||
|
ctrl := example.NewExampleController()
|
||||||
|
|
||||||
|
group := r.Group("/example")
|
||||||
|
group.GET("/hello", ctrl.Hello)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterPublic(registerHealthRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerHealthRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/ping", func(ctx *gin.Context) {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "pong",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/health", func(ctx *gin.Context) {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 子路由注册函数签名
|
||||||
|
type Registrar func(*gin.RouterGroup)
|
||||||
|
|
||||||
|
// 两类注册器:公开/私有(是否需要 JWT)
|
||||||
|
var (
|
||||||
|
publicRegistrars []Registrar
|
||||||
|
privateRegistrars []Registrar
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterPublic 注册公开路由(无需认证)
|
||||||
|
func RegisterPublic(fn Registrar) {
|
||||||
|
publicRegistrars = append(publicRegistrars, fn)
|
||||||
|
zap.L().Debug("公开路由注册器已添加",
|
||||||
|
zap.Int("total_public", len(publicRegistrars)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterPrivate 注册私有路由(需要 JWT 认证)
|
||||||
|
func RegisterPrivate(fn Registrar) {
|
||||||
|
privateRegistrars = append(privateRegistrars, fn)
|
||||||
|
zap.L().Debug("私有路由注册器已添加",
|
||||||
|
zap.Int("total_private", len(privateRegistrars)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyPublic 应用所有公开路由
|
||||||
|
func ApplyPublic(group *gin.RouterGroup) {
|
||||||
|
zap.L().Info("开始应用公开路由",
|
||||||
|
zap.Int("count", len(publicRegistrars)),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, fn := range publicRegistrars {
|
||||||
|
fn(group)
|
||||||
|
zap.L().Debug("公开路由已应用",
|
||||||
|
zap.Int("index", i+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Info("公开路由应用完成",
|
||||||
|
zap.Int("applied_count", len(publicRegistrars)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyPrivate 应用所有私有路由(带 JWT 中间件)
|
||||||
|
func ApplyPrivate(group *gin.RouterGroup) {
|
||||||
|
zap.L().Info("开始应用私有路由",
|
||||||
|
zap.Int("count", len(privateRegistrars)),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, fn := range privateRegistrars {
|
||||||
|
fn(group)
|
||||||
|
zap.L().Debug("私有路由已应用",
|
||||||
|
zap.Int("index", i+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Info("私有路由应用完成",
|
||||||
|
zap.Int("applied_count", len(privateRegistrars)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegistrarStats 获取注册器统计信息(调试用)
|
||||||
|
func GetRegistrarStats() map[string]int {
|
||||||
|
return map[string]int{
|
||||||
|
"public": len(publicRegistrars),
|
||||||
|
"private": len(privateRegistrars),
|
||||||
|
"total": len(publicRegistrars) + len(privateRegistrars),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintStats 打印注册器统计信息
|
||||||
|
func PrintStats() {
|
||||||
|
stats := GetRegistrarStats()
|
||||||
|
zap.L().Info("路由注册器统计",
|
||||||
|
zap.Int("public_count", stats["public"]),
|
||||||
|
zap.Int("private_count", stats["private"]),
|
||||||
|
zap.Int("total_count", stats["total"]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"skeleton/config"
|
||||||
|
"skeleton/middlewares"
|
||||||
|
"skeleton/routes/rest"
|
||||||
|
"skeleton/routes/ws"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupRoutes 设置主路由
|
||||||
|
func SetupRoutes(cfg *config.Config) *gin.Engine {
|
||||||
|
// 根据配置设置 Gin 模式
|
||||||
|
if !cfg.App.Debug {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Gin 路由器(不使用默认中间件)
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
// 设置 multipart form 内存限制为 100MB
|
||||||
|
r.MaxMultipartMemory = 100 << 20 // 100 MB
|
||||||
|
|
||||||
|
// 添加自定义中间件
|
||||||
|
r.Use(middlewares.CORS()) // CORS跨域处理(需要在其他中间件之前)
|
||||||
|
r.Use(middlewares.GinLogger()) // 结构化日志
|
||||||
|
r.Use(middlewares.GinRecovery()) // 异常恢复
|
||||||
|
r.Use(middlewares.ErrorLogging()) // 错误响应日志(用于记录逻辑异常)
|
||||||
|
|
||||||
|
// API 路由组
|
||||||
|
api := r.Group("/api")
|
||||||
|
|
||||||
|
// 公开路由组(无需认证)
|
||||||
|
public := api.Group("")
|
||||||
|
rest.ApplyPublic(public)
|
||||||
|
|
||||||
|
// 私有路由组(需要 JWT 认证)
|
||||||
|
private := api.Group("/private")
|
||||||
|
private.Use(middlewares.JWTAuth()) // 启用JWT认证中间件
|
||||||
|
rest.ApplyPrivate(private)
|
||||||
|
|
||||||
|
// WebSocket 路由组(无需认证)
|
||||||
|
ws.ApplyWebSocketRoutes(api)
|
||||||
|
|
||||||
|
// 打印路由统计信息
|
||||||
|
rest.PrintStats()
|
||||||
|
|
||||||
|
zap.L().Info("主路由设置完成",
|
||||||
|
zap.Any("rest_stats", rest.GetRegistrarStats()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ws
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
// ApplyWebSocketRoutes WebSocket路由挂载点
|
||||||
|
func ApplyWebSocketRoutes(r *gin.RouterGroup) {
|
||||||
|
_ = r
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaginationParams 分页参数
|
||||||
|
type PaginationParams struct {
|
||||||
|
Page int `form:"page" json:"page"` // 页码,从1开始
|
||||||
|
PageSize int `form:"page_size" json:"page_size"` // 每页数量
|
||||||
|
Search string `form:"search" json:"search"` // 搜索关键词
|
||||||
|
}
|
||||||
|
|
||||||
|
// WelcomeQueryParams 欢迎语查询参数
|
||||||
|
type WelcomeQueryParams struct {
|
||||||
|
PaginationParams
|
||||||
|
IsActive *bool `form:"is_active" json:"is_active"` // 启用状态筛选
|
||||||
|
MessageType string `form:"message_type" json:"message_type"` // 消息类型筛选
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonQueryParams 通用查询参数(关键词/快捷回复/群发)
|
||||||
|
type CommonQueryParams struct {
|
||||||
|
PaginationParams
|
||||||
|
IsActive *bool `form:"is_active" json:"is_active"` // 启用状态筛选
|
||||||
|
MessageType string `form:"message_type" json:"message_type"` // 消息类型筛选
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationResponse 分页响应
|
||||||
|
type PaginationResponse struct {
|
||||||
|
List interface{} `json:"list"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
Pages int `json:"pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaginationParams 从请求中获取分页参数
|
||||||
|
func GetPaginationParams(ctx *gin.Context) PaginationParams {
|
||||||
|
params := PaginationParams{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 20,
|
||||||
|
Search: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := ctx.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
|
||||||
|
params.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageSizeStr := ctx.Query("page_size"); pageSizeStr != "" {
|
||||||
|
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 && pageSize <= 100 {
|
||||||
|
params.PageSize = pageSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Search = ctx.Query("search")
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWelcomeQueryParams 从请求中获取欢迎语查询参数
|
||||||
|
func GetWelcomeQueryParams(ctx *gin.Context) WelcomeQueryParams {
|
||||||
|
params := WelcomeQueryParams{
|
||||||
|
PaginationParams: GetPaginationParams(ctx),
|
||||||
|
MessageType: ctx.Query("message_type"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := ctx.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
params.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommonQueryParams 从请求中获取通用查询参数
|
||||||
|
func GetCommonQueryParams(ctx *gin.Context) CommonQueryParams {
|
||||||
|
params := CommonQueryParams{
|
||||||
|
PaginationParams: GetPaginationParams(ctx),
|
||||||
|
MessageType: ctx.Query("message_type"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := ctx.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
params.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyPagination 应用分页到查询
|
||||||
|
func ApplyPagination(db *gorm.DB, params PaginationParams) *gorm.DB {
|
||||||
|
offset := (params.Page - 1) * params.PageSize
|
||||||
|
return db.Offset(offset).Limit(params.PageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePaginationResponse 创建分页响应
|
||||||
|
func CreatePaginationResponse(list interface{}, total int64, params PaginationParams) PaginationResponse {
|
||||||
|
pages := int((total + int64(params.PageSize) - 1) / int64(params.PageSize))
|
||||||
|
if pages == 0 {
|
||||||
|
pages = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginationResponse{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: params.Page,
|
||||||
|
PageSize: params.PageSize,
|
||||||
|
Pages: pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
// APIResponse 通用响应结构
|
||||||
|
type APIResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success 返回成功响应
|
||||||
|
func Success(data interface{}) gin.H {
|
||||||
|
return gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failure 返回失败响应
|
||||||
|
func Failure(code int, message string) gin.H {
|
||||||
|
return gin.H{
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var localTimezone *time.Location
|
||||||
|
|
||||||
|
// InitTimezone 初始化时区设置
|
||||||
|
func InitTimezone(timezone string) error {
|
||||||
|
loc, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载时区失败 %s: %w", timezone, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
localTimezone = loc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentTime 获取当前时间(使用设置的时区)
|
||||||
|
func GetCurrentTime() time.Time {
|
||||||
|
if localTimezone != nil {
|
||||||
|
return time.Now().In(localTimezone)
|
||||||
|
}
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentTimeString 获取当前时间字符串
|
||||||
|
func GetCurrentTimeString() string {
|
||||||
|
return GetCurrentTime().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentDate 获取当前日期字符串
|
||||||
|
func GetCurrentDate() string {
|
||||||
|
return GetCurrentTime().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTime 解析时间字符串到本地时区
|
||||||
|
func ParseTime(layout, value string) (time.Time, error) {
|
||||||
|
t, err := time.Parse(layout, value)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localTimezone != nil {
|
||||||
|
return t.In(localTimezone), nil
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTime 将时间格式化为字符串(使用本地时区)
|
||||||
|
func FormatTime(t time.Time, layout string) string {
|
||||||
|
if localTimezone != nil {
|
||||||
|
return t.In(localTimezone).Format(layout)
|
||||||
|
}
|
||||||
|
return t.Format(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimezone 获取当前时区名称
|
||||||
|
func GetTimezone() string {
|
||||||
|
if localTimezone != nil {
|
||||||
|
return localTimezone.String()
|
||||||
|
}
|
||||||
|
return time.Local.String()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user