From 52d50b97aa938fad10ff72bfcc7dde08f2bd751c Mon Sep 17 00:00:00 2001 From: yanlongqi Date: Tue, 4 Nov 2025 01:12:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90PostgreSQL=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E5=AE=9E=E7=8E=B0=E7=94=A8=E6=88=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改动: - 集成 GORM 和 PostgreSQL 驱动 - 创建数据库配置模块 (pkg/config) - 实现数据库连接和初始化 (internal/database) - 更新用户模型支持 GORM 和 bcrypt 密码加密 - 重构用户注册和登录处理器使用数据库存储 - 删除旧的 users.json 文件存储方式 - 更新 README.md 和 CLAUDE.md 文档 技术栈: - GORM v1.31.1 - ORM框架 - PostgreSQL - 数据库 - bcrypt - 密码加密 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 28 ++++++++--- README.md | 41 +++++++++------ go.mod | 14 +++++- go.sum | 17 +++++++ internal/database/database.go | 45 +++++++++++++++++ internal/handlers/user.go | 94 ++++++++++++----------------------- internal/models/user.go | 37 ++++++++++++-- main.go | 8 +++ pkg/config/config.go | 40 +++++++++++++++ users.json | 12 ----- 10 files changed, 232 insertions(+), 104 deletions(-) create mode 100644 internal/database/database.go create mode 100644 pkg/config/config.go delete mode 100644 users.json diff --git a/CLAUDE.md b/CLAUDE.md index 3c66509..8c419dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,9 +55,10 @@ func MiddlewareName() gin.HandlerFunc { ### 模块结构 - **main.go** - 应用程序入口点,服务器配置和路由设置 - **internal/handlers/** - HTTP 请求处理器,全部使用 `*gin.Context` 并返回 JSON 响应 -- **internal/middleware/** - Gin 中间件链(当前:自定义日志记录器) -- **internal/models/** - 数据模型(目录已存在,准备用于数据库模型) -- **pkg/config/** - 配置管理(目录已存在但尚未填充) +- **internal/middleware/** - Gin 中间件链(当前:自定义日志记录器、CORS) +- **internal/models/** - 数据模型(用户模型、问题模型等) +- **internal/database/** - 数据库连接和初始化 +- **pkg/config/** - 配置管理(数据库配置等) ## 常用命令 @@ -106,12 +107,15 @@ go test -v ./... ## 关键实现细节 - **框架**: 使用 Gin v1.11.0 -- **服务器端口**: :8080 (在 [main.go:22](main.go#L22) 中配置) +- **ORM**: 使用 GORM v1.31.1 +- **数据库**: PostgreSQL (配置在 [pkg/config/config.go](pkg/config/config.go)) +- **服务器端口**: :8080 (在 [main.go:42](main.go#L42) 中配置) - **处理器签名**: 所有处理器使用 `func(c *gin.Context)` 模式 - **JSON 响应**: 使用 `c.JSON()` 方法配合 `gin.H{}` 或结构体 - **导入路径**: 使用模块名 `ankao` (在 go.mod 中定义) - **路由注册**: 路由在 [main.go](main.go) 中使用 `r.GET()`、`r.POST()` 等注册 - **中间件**: 使用 `r.Use()` 全局应用或通过路由分组应用到特定路由 +- **密码加密**: 使用 bcrypt 加密存储用户密码 ## 添加新功能 @@ -125,10 +129,18 @@ go test -v ./... 2. 使用 `r.Use(middleware.YourMiddleware())` 全局应用或应用到路由组 ### 数据库集成 -项目已准备好进行数据库集成: -- 在 `internal/models/` 中添加模型 -- 考虑使用 GORM 或类似的 ORM -- 在 main.go 或单独的包中添加数据库初始化 +项目已集成 PostgreSQL 数据库: +- **配置**: 数据库配置在 [pkg/config/config.go](pkg/config/config.go) +- **初始化**: 数据库初始化在 [internal/database/database.go](internal/database/database.go) +- **模型定义**: 在 `internal/models/` 中添加 GORM 模型 +- **迁移**: 使用 `DB.AutoMigrate()` 自动迁移表结构 +- **使用方式**: 通过 `database.GetDB()` 获取数据库实例 + +### 添加新的数据模型 +1. 在 `internal/models/` 中创建模型文件 +2. 定义结构体,使用 GORM 标签 +3. 在 [internal/database/database.go](internal/database/database.go) 的 `InitDB()` 中添加 `AutoMigrate` +4. 在处理器中使用 `database.GetDB()` 进行数据库操作 ## 前端开发规范 diff --git a/README.md b/README.md index f3b315a..d8bffb1 100644 --- a/README.md +++ b/README.md @@ -103,30 +103,35 @@ go build -o bin/server.exe main.go - 自定义日志中间件 - RESTful API 结构 - 健康检查端点 -- 用户登录系统(基于JSON文件存储) +- 用户登录系统(基于PostgreSQL数据库) - 题目练习功能 - 答题统计功能 - React + TypeScript + Vite 前端 - Ant Design Mobile UI组件库 -## 用户系统 +## 数据库配置 -用户数据存储在 [users.json](users.json) 文件中,格式如下: +项目使用 PostgreSQL 数据库存储用户数据。 -```json -{ - "用户名": { - "password": "密码", - "avatar": "头像URL", - "nickname": "昵称" - } -} -``` +### 数据库连接信息 -### 测试账号 +数据库配置位于 [pkg/config/config.go](pkg/config/config.go): +- 主机: pgsql.yuchat.top +- 端口: 5432 +- 数据库: ankao +- 用户: postgres -- 用户名: `admin` / 密码: `123456` -- 用户名: `test` / 密码: `test123` +### 数据库表结构 + +**users 表**: +- `id` - 主键 +- `username` - 用户名(唯一索引) +- `password` - 密码(bcrypt加密) +- `avatar` - 头像URL +- `nickname` - 昵称 +- `created_at` - 创建时间 +- `updated_at` - 更新时间 +- `deleted_at` - 软删除时间 ## 前端开发 @@ -166,7 +171,8 @@ yarn build - 自定义日志中间件 - RESTful API 结构 - 健康检查端点 -- 用户登录和注册系统(基于JSON文件存储) +- 用户登录和注册系统(基于PostgreSQL数据库) +- 密码bcrypt加密存储 - 题目练习功能 - 答题统计功能 @@ -183,6 +189,9 @@ yarn build ### 后端 - **Go** 1.25.1 - **Gin** v1.11.0 - Web 框架 +- **GORM** v1.31.1 - ORM框架 +- **PostgreSQL** - 数据库 +- **bcrypt** - 密码加密 ### 前端 - **React** 18 - UI框架 diff --git a/go.mod b/go.mod index 83479c4..9521dfa 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,12 @@ module ankao go 1.25.1 -require github.com/gin-gonic/gin v1.11.0 +require ( + github.com/gin-gonic/gin v1.11.0 + golang.org/x/crypto v0.43.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) require ( github.com/bytedance/gopkg v0.1.3 // indirect @@ -16,6 +21,12 @@ require ( github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/goccy/go-json v0.10.5 // 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.7.6 // 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/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 @@ -29,7 +40,6 @@ require ( github.com/ugorji/go/codec v1.3.1 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.43.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/go.sum b/go.sum index 1856153..7138b0a 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,18 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk 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.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +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/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= @@ -56,6 +68,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -91,3 +104,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..894d9ca --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,45 @@ +package database + +import ( + "ankao/internal/models" + "ankao/pkg/config" + "fmt" + "log" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +// InitDB 初始化数据库连接 +func InitDB() error { + cfg := config.GetDatabaseConfig() + dsn := cfg.GetDSN() + + var err error + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // 开启SQL日志 + }) + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + + log.Println("Database connected successfully") + + // 自动迁移数据库表结构 + err = DB.AutoMigrate(&models.User{}) + if err != nil { + return fmt.Errorf("failed to migrate database: %w", err) + } + + log.Println("Database migration completed") + + return nil +} + +// GetDB 获取数据库实例 +func GetDB() *gorm.DB { + return DB +} diff --git a/internal/handlers/user.go b/internal/handlers/user.go index 7e36d32..3c643f5 100644 --- a/internal/handlers/user.go +++ b/internal/handlers/user.go @@ -1,49 +1,18 @@ package handlers import ( + "ankao/internal/database" "ankao/internal/models" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "net/http" - "os" "time" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) -const usersFilePath = "users.json" - -// loadUsers 从JSON文件加载用户数据 -func loadUsers() (models.UserData, error) { - file, err := os.ReadFile(usersFilePath) - if err != nil { - return nil, fmt.Errorf("读取用户文件失败: %w", err) - } - - var users models.UserData - if err := json.Unmarshal(file, &users); err != nil { - return nil, fmt.Errorf("解析用户数据失败: %w", err) - } - - return users, nil -} - -// saveUsers 保存用户数据到JSON文件 -func saveUsers(users models.UserData) error { - data, err := json.MarshalIndent(users, "", " ") - if err != nil { - return fmt.Errorf("序列化用户数据失败: %w", err) - } - - if err := os.WriteFile(usersFilePath, data, 0644); err != nil { - return fmt.Errorf("保存用户文件失败: %w", err) - } - - return nil -} - // generateToken 生成简单的token(实际项目应使用JWT) func generateToken(username string) string { data := fmt.Sprintf("%s:%d", username, time.Now().Unix()) @@ -64,29 +33,29 @@ func Login(c *gin.Context) { return } - // 加载用户数据 - users, err := loadUsers() - if err != nil { + // 从数据库查找用户 + var user models.User + db := database.GetDB() + result := db.Where("username = ?", req.Username).First(&user) + + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "用户名或密码错误", + }) + return + } c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "服务器错误", - "error": err.Error(), - }) - return - } - - // 验证用户 - user, exists := users[req.Username] - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "用户名或密码错误", + "error": result.Error.Error(), }) return } // 验证密码 - if user.Password != req.Password { + if !user.CheckPassword(req.Password) { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "用户名或密码错误", @@ -99,7 +68,7 @@ func Login(c *gin.Context) { // 返回用户信息(不包含密码) userInfo := models.UserInfoResponse{ - Username: req.Username, + Username: user.Username, Avatar: user.Avatar, Nickname: user.Nickname, } @@ -127,15 +96,12 @@ func Register(c *gin.Context) { return } - // 加载现有用户数据 - users, err := loadUsers() - if err != nil { - // 如果文件不存在,创建新的用户数据 - users = make(models.UserData) - } + db := database.GetDB() // 检查用户名是否已存在 - if _, exists := users[req.Username]; exists { + var existingUser models.User + result := db.Where("username = ?", req.Username).First(&existingUser) + if result.Error == nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "用户名已存在", @@ -146,7 +112,6 @@ func Register(c *gin.Context) { // 创建新用户 newUser := models.User{ Username: req.Username, - Password: req.Password, // 实际项目应该加密存储 Nickname: req.Nickname, Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=" + req.Username, // 使用用户名生成默认头像 } @@ -156,11 +121,18 @@ func Register(c *gin.Context) { newUser.Nickname = req.Username } - // 添加新用户 - users[req.Username] = newUser + // 加密密码 + if err := newUser.HashPassword(req.Password); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "密码加密失败", + "error": err.Error(), + }) + return + } - // 保存用户数据 - if err := saveUsers(users); err != nil { + // 保存到数据库 + if err := db.Create(&newUser).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "注册失败", diff --git a/internal/models/user.go b/internal/models/user.go index 128f97d..398a4b9 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -1,14 +1,41 @@ package models +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + // User 用户结构 type User struct { - Username string `json:"username"` - Password string `json:"password"` - Avatar string `json:"avatar"` - Nickname string `json:"nickname"` + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"uniqueIndex;not null;size:50" json:"username"` + Password string `gorm:"not null;size:255" json:"-"` // json:"-" 表示在JSON响应中不返回密码 + Avatar string `gorm:"size:255" json:"avatar"` + Nickname string `gorm:"size:50" json:"nickname"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -// UserData 存储所有用户的结构 +// HashPassword 加密密码 +func (u *User) HashPassword(password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.Password = string(hashedPassword) + return nil +} + +// CheckPassword 验证密码 +func (u *User) CheckPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + return err == nil +} + +// UserData 存储所有用户的结构(用于兼容旧的文件存储方式) type UserData map[string]User // LoginRequest 登录请求 diff --git a/main.go b/main.go index e640299..db0e776 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,21 @@ package main import ( + "ankao/internal/database" "ankao/internal/handlers" "ankao/internal/middleware" + "log" "github.com/gin-gonic/gin" ) func main() { + // 初始化数据库连接 + if err := database.InitDB(); err != nil { + log.Fatal("数据库初始化失败:", err) + } + log.Println("数据库连接成功") + // 创建Gin路由器 r := gin.Default() diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..c7a1807 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,40 @@ +package config + +import ( + "fmt" +) + +// DatabaseConfig 数据库配置结构 +type DatabaseConfig struct { + Host string + Port int + User string + Password string + DBName string + SSLMode string +} + +// GetDatabaseConfig 获取数据库配置 +func GetDatabaseConfig() *DatabaseConfig { + return &DatabaseConfig{ + Host: "pgsql.yuchat.top", + Port: 5432, + User: "postgres", + Password: "longqi@1314", + DBName: "ankao", + SSLMode: "disable", + } +} + +// GetDSN 返回数据库连接字符串 +func (c *DatabaseConfig) GetDSN() string { + return fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%d sslmode=%s", + c.Host, + c.User, + c.Password, + c.DBName, + c.Port, + c.SSLMode, + ) +} diff --git a/users.json b/users.json deleted file mode 100644 index 487a9b9..0000000 --- a/users.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin": { - "password": "123456", - "avatar": "", - "nickname": "管理员" - }, - "test": { - "password": "test123", - "avatar": "", - "nickname": "测试用户" - } -}