commit 04d920aa9220eae49097be5fbb6e568122c3000d Author: mayiming <1627832236@qq.com> Date: Wed Dec 10 19:03:24 2025 +0800 项目初始化 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdfcea2 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# RelationshipManagement-backend + +基于 Gin 框架的分层架构后端项目 + +## 项目结构 + +``` +RelationshipManagement-backend/ +├── main.go # 应用入口 +├── go.mod # Go 模块依赖 +├── config/ # 配置层 +│ └── config.go # 配置管理 +├── model/ # 数据模型层 +│ └── user.go # 用户模型 +├── dao/ # 数据访问层 (Data Access Object) +│ ├── db.go # 数据库连接 +│ └── user_dao.go # 用户数据访问 +├── service/ # 业务逻辑层 +│ └── user_service.go # 用户服务 +├── handler/ # 处理器层 (Controller) +│ └── user_handler.go # 用户处理器 +├── router/ # 路由层 +│ └── router.go # 路由配置 +└── middleware/ # 中间件 + ├── cors.go # 跨域中间件 + ├── logger.go # 日志中间件 + └── recovery.go # 恢复中间件 +``` + +## 架构说明 + +### 分层架构 + +1. **Router 层** (`router/`) + - 负责路由配置和请求分发 + - 将 HTTP 请求路由到对应的 Handler + +2. **Handler 层** (`handler/`) + - 处理 HTTP 请求和响应 + - 参数验证和绑定 + - 调用 Service 层处理业务逻辑 + +3. **Service 层** (`service/`) + - 业务逻辑处理 + - 数据验证和业务规则 + - 调用 DAO 层进行数据操作 + +4. **DAO 层** (`dao/`) + - 数据访问对象 + - 数据库 CRUD 操作 + - 数据库连接管理 + +5. **Model 层** (`model/`) + - 数据模型定义 + - 数据库表结构映射 + +6. **Config 层** (`config/`) + - 配置管理 + - 环境变量读取 + +7. **Middleware 层** (`middleware/`) + - 全局中间件 + - CORS、日志、错误恢复等 + +## 环境配置 + +通过环境变量配置(可选,有默认值): + +- `SERVER_PORT`: 服务器端口(默认: 8080) +- `GIN_MODE`: Gin 模式(debug/release/test,默认: debug) +- `DB_HOST`: 数据库主机(默认: localhost) +- `DB_PORT`: 数据库端口(默认: 3306) +- `DB_USER`: 数据库用户(默认: root) +- `DB_PASSWORD`: 数据库密码(默认: 空) +- `DB_NAME`: 数据库名称(默认: relationship_db) + +## 运行项目 + +1. 安装依赖: +```bash +go mod tidy +``` + +2. 配置数据库(可选,修改环境变量或代码中的默认值) + +3. 运行项目: +```bash +go run main.go +``` + +4. 访问健康检查接口: +``` +GET http://localhost:8080/health +``` + +## API 接口 + +### 用户相关接口 + +- `POST /api/users` - 创建用户 +- `GET /api/users` - 获取用户列表(支持分页) +- `GET /api/users/:id` - 根据ID获取用户 +- `PUT /api/users/:id` - 更新用户 +- `DELETE /api/users/:id` - 删除用户 + +## 依赖说明 + +- **gin**: Web 框架 +- **gorm**: ORM 框架 +- **mysql**: MySQL 数据库驱动 +- **logrus**: 日志库 + +## 开发建议 + +1. 添加新的业务模块时,按照相同的分层结构创建文件 +2. 在 `dao/db.go` 的 `AutoMigrate()` 函数中添加新模型的自动迁移 +3. 根据实际需求调整中间件和配置 +4. 生产环境建议使用配置文件(如 YAML、JSON)替代环境变量 + diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f5c899a --- /dev/null +++ b/config/config.go @@ -0,0 +1,52 @@ +package config + +import ( + "os" +) + +// Config 应用配置 +type Config struct { + Server ServerConfig + DB DBConfig +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + Port string + Mode string // debug, release, test +} + +// DBConfig 数据库配置 +type DBConfig struct { + Host string + Port string + User string + Password string + DBName string +} + +// LoadConfig 加载配置 +func LoadConfig() *Config { + return &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Mode: getEnv("GIN_MODE", "debug"), + }, + DB: DBConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "3306"), + User: getEnv("DB_USER", "root"), + Password: getEnv("DB_PASSWORD", ""), + DBName: getEnv("DB_NAME", "relationship_db"), + }, + } +} + +// getEnv 获取环境变量,如果不存在则返回默认值 +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + diff --git a/dao/db.go b/dao/db.go new file mode 100644 index 0000000..b6bfadc --- /dev/null +++ b/dao/db.go @@ -0,0 +1,53 @@ +package dao + +import ( + "RelationshipManagement-backend/config" + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +// InitDB 初始化数据库连接 +func InitDB(cfg *config.Config) error { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.DB.User, + cfg.DB.Password, + cfg.DB.Host, + cfg.DB.Port, + cfg.DB.DBName, + ) + + var err error + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + + if err != nil { + return fmt.Errorf("failed to connect database: %w", err) + } + + // 自动迁移(开发环境使用,生产环境建议使用迁移工具) + // AutoMigrate() + + return nil +} + +// AutoMigrate 自动迁移数据库表 +func AutoMigrate() error { + // 在这里添加需要自动迁移的模型 + // return DB.AutoMigrate(&model.User{}) + return nil +} + +// CloseDB 关闭数据库连接 +func CloseDB() error { + sqlDB, err := DB.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + diff --git a/dao/user_dao.go b/dao/user_dao.go new file mode 100644 index 0000000..0b7f1a4 --- /dev/null +++ b/dao/user_dao.go @@ -0,0 +1,69 @@ +package dao + +import ( + "RelationshipManagement-backend/model" + "gorm.io/gorm" +) + +// UserDAO 用户数据访问对象 +type UserDAO struct { + db *gorm.DB +} + +// NewUserDAO 创建用户DAO实例 +func NewUserDAO(db *gorm.DB) *UserDAO { + return &UserDAO{db: db} +} + +// Create 创建用户 +func (dao *UserDAO) Create(user *model.User) error { + return dao.db.Create(user).Error +} + +// GetByID 根据ID获取用户 +func (dao *UserDAO) GetByID(id uint) (*model.User, error) { + var user model.User + err := dao.db.First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// GetByUsername 根据用户名获取用户 +func (dao *UserDAO) GetByUsername(username string) (*model.User, error) { + var user model.User + err := dao.db.Where("username = ?", username).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// GetByEmail 根据邮箱获取用户 +func (dao *UserDAO) GetByEmail(email string) (*model.User, error) { + var user model.User + err := dao.db.Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// Update 更新用户 +func (dao *UserDAO) Update(user *model.User) error { + return dao.db.Save(user).Error +} + +// Delete 删除用户 +func (dao *UserDAO) Delete(id uint) error { + return dao.db.Delete(&model.User{}, id).Error +} + +// List 获取用户列表 +func (dao *UserDAO) List(offset, limit int) ([]*model.User, error) { + var users []*model.User + err := dao.db.Offset(offset).Limit(limit).Find(&users).Error + return users, err +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..472b07a --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module RelationshipManagement-backend + +go 1.25.4 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/sirupsen/logrus v1.9.3 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.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.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6f6340f --- /dev/null +++ b/go.sum @@ -0,0 +1,103 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +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/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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handler/user_handler.go b/handler/user_handler.go new file mode 100644 index 0000000..3e45f21 --- /dev/null +++ b/handler/user_handler.go @@ -0,0 +1,217 @@ +package handler + +import ( + "RelationshipManagement-backend/model" + "RelationshipManagement-backend/service" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +// UserHandler 用户处理器 +type UserHandler struct { + userService *service.UserService +} + +// NewUserHandler 创建用户处理器实例 +func NewUserHandler(userService *service.UserService) *UserHandler { + return &UserHandler{userService: userService} +} + +// CreateUser 创建用户 +// @Summary 创建用户 +// @Description 创建新用户 +// @Tags 用户 +// @Accept json +// @Produce json +// @Param user body model.User true "用户信息" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/users [post] +func (h *UserHandler) CreateUser(c *gin.Context) { + var user model.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "参数错误", + "error": err.Error(), + }) + return + } + + if err := h.userService.CreateUser(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "创建用户失败", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "创建成功", + "data": user, + }) +} + +// GetUserByID 根据ID获取用户 +// @Summary 获取用户 +// @Description 根据ID获取用户信息 +// @Tags 用户 +// @Accept json +// @Produce json +// @Param id path int true "用户ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/users/{id} [get] +func (h *UserHandler) GetUserByID(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的用户ID", + }) + return + } + + user, err := h.userService.GetUserByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "code": 404, + "message": "用户不存在", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "获取成功", + "data": user, + }) +} + +// ListUsers 获取用户列表 +// @Summary 获取用户列表 +// @Description 分页获取用户列表 +// @Tags 用户 +// @Accept json +// @Produce json +// @Param page query int false "页码" default(1) +// @Param pageSize query int false "每页数量" default(10) +// @Success 200 {object} map[string]interface{} +// @Router /api/users [get] +func (h *UserHandler) ListUsers(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + + users, total, err := h.userService.ListUsers(page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "获取用户列表失败", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "获取成功", + "data": gin.H{ + "list": users, + "total": total, + "page": page, + "pageSize": pageSize, + }, + }) +} + +// UpdateUser 更新用户 +// @Summary 更新用户 +// @Description 更新用户信息 +// @Tags 用户 +// @Accept json +// @Produce json +// @Param id path int true "用户ID" +// @Param user body model.User true "用户信息" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/users/{id} [put] +func (h *UserHandler) UpdateUser(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的用户ID", + }) + return + } + + var user model.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "参数错误", + "error": err.Error(), + }) + return + } + + user.ID = uint(id) + if err := h.userService.UpdateUser(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "更新用户失败", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "更新成功", + "data": user, + }) +} + +// DeleteUser 删除用户 +// @Summary 删除用户 +// @Description 根据ID删除用户 +// @Tags 用户 +// @Accept json +// @Produce json +// @Param id path int true "用户ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/users/{id} [delete] +func (h *UserHandler) DeleteUser(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "无效的用户ID", + }) + return + } + + if err := h.userService.DeleteUser(uint(id)); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "删除用户失败", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "message": "删除成功", + }) +} + diff --git a/main.go b/main.go new file mode 100644 index 0000000..59fbc4d --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "RelationshipManagement-backend/config" + "RelationshipManagement-backend/dao" + "RelationshipManagement-backend/router" + "fmt" + "log" + + "github.com/gin-gonic/gin" +) + +func main() { + // 加载配置 + cfg := config.LoadConfig() + + // 设置Gin模式 + gin.SetMode(cfg.Server.Mode) + + // 初始化数据库 + if err := dao.InitDB(cfg); err != nil { + log.Fatalf("数据库初始化失败: %v", err) + } + defer dao.CloseDB() + + // 设置路由 + r := router.SetupRouter() + + // 启动服务器 + addr := fmt.Sprintf(":%s", cfg.Server.Port) + log.Printf("服务器启动在端口 %s", cfg.Server.Port) + if err := r.Run(addr); err != nil { + log.Fatalf("服务器启动失败: %v", err) + } +} diff --git a/middleware/cors.go b/middleware/cors.go new file mode 100644 index 0000000..767aec6 --- /dev/null +++ b/middleware/cors.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + diff --git a/middleware/logger.go b/middleware/logger.go new file mode 100644 index 0000000..3d4b4c3 --- /dev/null +++ b/middleware/logger.go @@ -0,0 +1,37 @@ +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// Logger 日志中间件 +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + c.Next() + + latency := time.Since(start) + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + + if raw != "" { + path = path + "?" + raw + } + + logrus.WithFields(logrus.Fields{ + "status_code": statusCode, + "latency": latency, + "client_ip": clientIP, + "method": method, + "path": path, + }).Info("HTTP Request") + } +} + diff --git a/middleware/recovery.go b/middleware/recovery.go new file mode 100644 index 0000000..c9db80f --- /dev/null +++ b/middleware/recovery.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// Recovery 恢复中间件 +func Recovery() gin.HandlerFunc { + return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { + logrus.Errorf("Panic recovered: %v", recovered) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "服务器内部错误", + }) + c.Abort() + }) +} + diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..2528aad --- /dev/null +++ b/model/user.go @@ -0,0 +1,19 @@ +package model + +import "time" + +// User 用户模型 +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"uniqueIndex;not null"` + Email string `json:"email" gorm:"uniqueIndex;not null"` + Password string `json:"-" gorm:"not null"` // 密码不返回给前端 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TableName 指定表名 +func (User) TableName() string { + return "users" +} + diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..4557f63 --- /dev/null +++ b/router/router.go @@ -0,0 +1,50 @@ +package router + +import ( + "RelationshipManagement-backend/handler" + "RelationshipManagement-backend/middleware" + "RelationshipManagement-backend/service" + "RelationshipManagement-backend/dao" + + "github.com/gin-gonic/gin" +) + +// SetupRouter 设置路由 +func SetupRouter() *gin.Engine { + // 初始化依赖 + userDAO := dao.NewUserDAO(dao.DB) + userService := service.NewUserService(userDAO) + userHandler := handler.NewUserHandler(userService) + + r := gin.Default() + + // 全局中间件 + r.Use(middleware.CORS()) + r.Use(middleware.Logger()) + r.Use(middleware.Recovery()) + + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + "message": "服务运行正常", + }) + }) + + // API路由组 + api := r.Group("/api") + { + // 用户相关路由 + users := api.Group("/users") + { + users.POST("", userHandler.CreateUser) + users.GET("", userHandler.ListUsers) + users.GET("/:id", userHandler.GetUserByID) + users.PUT("/:id", userHandler.UpdateUser) + users.DELETE("/:id", userHandler.DeleteUser) + } + } + + return r +} + diff --git a/service/user_service.go b/service/user_service.go new file mode 100644 index 0000000..0936730 --- /dev/null +++ b/service/user_service.go @@ -0,0 +1,69 @@ +package service + +import ( + "RelationshipManagement-backend/dao" + "RelationshipManagement-backend/model" + "errors" +) + +// UserService 用户服务 +type UserService struct { + userDAO *dao.UserDAO +} + +// NewUserService 创建用户服务实例 +func NewUserService(userDAO *dao.UserDAO) *UserService { + return &UserService{userDAO: userDAO} +} + +// CreateUser 创建用户 +func (s *UserService) CreateUser(user *model.User) error { + // 检查用户名是否已存在 + _, err := s.userDAO.GetByUsername(user.Username) + if err == nil { + return errors.New("用户名已存在") + } + + // 检查邮箱是否已存在 + _, err = s.userDAO.GetByEmail(user.Email) + if err == nil { + return errors.New("邮箱已存在") + } + + // 创建用户 + return s.userDAO.Create(user) +} + +// GetUserByID 根据ID获取用户 +func (s *UserService) GetUserByID(id uint) (*model.User, error) { + return s.userDAO.GetByID(id) +} + +// GetUserByUsername 根据用户名获取用户 +func (s *UserService) GetUserByUsername(username string) (*model.User, error) { + return s.userDAO.GetByUsername(username) +} + +// UpdateUser 更新用户 +func (s *UserService) UpdateUser(user *model.User) error { + return s.userDAO.Update(user) +} + +// DeleteUser 删除用户 +func (s *UserService) DeleteUser(id uint) error { + return s.userDAO.Delete(id) +} + +// ListUsers 获取用户列表 +func (s *UserService) ListUsers(page, pageSize int) ([]*model.User, int64, error) { + offset := (page - 1) * pageSize + users, err := s.userDAO.List(offset, pageSize) + if err != nil { + return nil, 0, err + } + + // 获取总数(这里简化处理,实际应该从DAO获取) + total := int64(len(users)) + return users, total, nil +} +