user-management/
├── cmd/
│ └── server/
│ └── main.go # 应用入口
├── internal/
│ ├── config/ # 配置管理
│ ├── handler/ # HTTP处理器
│ ├── middleware/ # 中间件
│ ├── model/ # 数据模型
│ ├── repository/ # 数据访问层
│ ├── service/ # 业务逻辑层
│ └── utils/ # 工具函数
├── pkg/
│ ├── auth/ # 认证模块
│ ├── database/ # 数据库连接
│ └── logger/ # 日志模块
├── api/
│ └── docs/ # API文档
├── scripts/ # 部署脚本
├── docker-compose.yml # Docker配置
├── go.mod # 依赖管理
└── README.md # 项目说明
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
Logger LoggerConfig `mapstructure:"logger"`
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
ReadTimeout int `mapstructure:"read_timeout"`
WriteTimeout int `mapstructure:"write_timeout"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expire int `mapstructure:"expire"`
}
type LoggerConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
}
// Load 加载配置文件
func Load(configPath string) (*Config, error) {
// 设置配置文件名和路径
if configPath == "" {
configPath = "./config.yaml"
}
// 获取配置文件的绝对路径
absPath, err := filepath.Abs(configPath)
if err != nil {
return nil, fmt.Errorf("获取配置文件绝对路径失败: %w", err)
}
// 检查配置文件是否存在
if _, err := os.Stat(absPath); os.IsNotExist(err) {
return nil, fmt.Errorf("配置文件不存在: %s", absPath)
}
// 设置配置文件名和路径
viper.SetConfigFile(absPath)
// 设置环境变量前缀
viper.SetEnvPrefix("APP")
viper.AutomaticEnv()
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
// 解析配置到结构体
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
// 设置默认值
setDefaults(&config)
// 验证配置
if err := validate(&config); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return &config, nil
}
// setDefaults 设置默认配置值
func setDefaults(config *Config) {
if config.Server.Port == "" {
config.Server.Port = "8080"
}
if config.Server.Mode == "" {
config.Server.Mode = "debug"
}
if config.Server.ReadTimeout == 0 {
config.Server.ReadTimeout = 30
}
if config.Server.WriteTimeout == 0 {
config.Server.WriteTimeout = 30
}
if config.Database.SSLMode == "" {
config.Database.SSLMode = "disable"
}
if config.JWT.Expire == 0 {
config.JWT.Expire = 24 // 24小时
}
if config.Logger.Level == "" {
config.Logger.Level = "info"
}
if config.Logger.Format == "" {
config.Logger.Format = "json"
}
if config.Logger.Output == "" {
config.Logger.Output = "stdout"
}
}
// validate 验证配置
func validate(config *Config) error {
if config.JWT.Secret == "" {
return fmt.Errorf("JWT密钥不能为空")
}
if config.Database.Host == "" {
return fmt.Errorf("数据库主机不能为空")
}
if config.Database.User == "" {
return fmt.Errorf("数据库用户名不能为空")
}
if config.Database.DBName == "" {
return fmt.Errorf("数据库名不能为空")
}
return nil
}
// GetDSN 获取数据库连接字符串
func (c *DatabaseConfig) GetDSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
c.User, c.Password, c.Host, c.Port, c.DBName)
}
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;not null;size:50" json:"username" binding:"required,min=3,max=50"`
Email string `gorm:"uniqueIndex;not null;size:100" json:"email" binding:"required,email"`
Password string `gorm:"not null;size:255" json:"-" binding:"required,min=6"`
FirstName string `gorm:"size:50" json:"first_name"`
LastName string `gorm:"size:50" json:"last_name"`
Avatar string `gorm:"size:255" json:"avatar"`
IsActive bool `gorm:"default:true" json:"is_active"`
LastLogin *time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}
// BeforeCreate GORM钩子 - 创建前
func (u *User) BeforeCreate(tx *gorm.DB) error {
now := time.Now()
u.CreatedAt = now
u.UpdatedAt = now
return nil
}
// BeforeUpdate GORM钩子 - 更新前
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now()
return nil
}
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FirstName string `json:"first_name" binding:"max=50"`
LastName string `json:"last_name" binding:"max=50"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
FirstName *string `json:"first_name" binding:"omitempty,max=50"`
LastName *string `json:"last_name" binding:"omitempty,max=50"`
Avatar *string `json:"avatar"`
IsActive *bool `json:"is_active"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// UserResponse 用户响应
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
IsActive bool `json:"is_active"`
LastLogin *time.Time `json:"last_login"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
// UserListResponse 用户列表响应
type UserListResponse struct {
Users []UserResponse `json:"users"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
// ToResponse 转换为响应结构
func (u *User) ToResponse() UserResponse {
return UserResponse{
ID: u.ID,
Username: u.Username,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Avatar: u.Avatar,
IsActive: u.IsActive,
LastLogin: u.LastLogin,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// UpdateLastLogin 更新最后登录时间
func (u *User) UpdateLastLogin() {
now := time.Now()
u.LastLogin = &now
}
package service
import (
"errors"
"fmt"
"time"
"user-management/internal/model"
"user-management/internal/repository"
"user-management/pkg/logger"
"golang.org/x/crypto/bcrypt"
)
// UserService 用户服务
type UserService struct {
userRepo repository.UserRepository
logger logger.Logger
}
// NewUserService 创建用户服务
func NewUserService(repo repository.UserRepository, logger logger.Logger) *UserService {
return &UserService{
userRepo: repo,
logger: logger,
}
}
// CreateUser 创建用户
func (s *UserService) CreateUser(req model.CreateUserRequest) (*model.UserResponse, error) {
// 检查用户名是否已存在
existingUser, err := s.userRepo.FindByUsername(req.Username)
if err == nil && existingUser != nil {
return nil, errors.New("用户名已存在")
}
// 检查邮箱是否已存在
existingUser, err = s.userRepo.FindByEmail(req.Email)
if err == nil && existingUser != nil {
return nil, errors.New("邮箱已存在")
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("密码加密失败", "error", err)
return nil, fmt.Errorf("密码加密失败: %w", err)
}
// 创建用户
user := &model.User{
Username: req.Username,
Email: req.Email,
Password: string(hashedPassword),
FirstName: req.FirstName,
LastName: req.LastName,
IsActive: true,
}
if err := s.userRepo.Create(user); err != nil {
s.logger.Error("创建用户失败", "username", req.Username, "error", err)
return nil, fmt.Errorf("创建用户失败: %w", err)
}
s.logger.Info("用户创建成功", "username", req.Username, "id", user.ID)
return &user.ToResponse(), nil
}
// Authenticate 用户认证
func (s *UserService) Authenticate(req model.LoginRequest) (*model.User, error) {
// 查找用户
user, err := s.userRepo.FindByUsername(req.Username)
if err != nil {
s.logger.Warn("用户登录失败", "username", req.Username, "reason", "用户不存在")
return nil, errors.New("用户名或密码错误")
}
// 检查用户是否激活
if !user.IsActive {
s.logger.Warn("用户登录失败", "username", req.Username, "reason", "用户未激活")
return nil, errors.New("用户账户已被禁用")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
s.logger.Warn("用户登录失败", "username", req.Username, "reason", "密码错误")
return nil, errors.New("用户名或密码错误")
}
// 更新最后登录时间
user.UpdateLastLogin()
if err := s.userRepo.Update(user); err != nil {
s.logger.Error("更新最后登录时间失败", "username", req.Username, "error", err)
// 不返回错误,因为这不是关键操作
}
s.logger.Info("用户登录成功", "username", req.Username, "id", user.ID)
return user, nil
}
// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(id uint) (*model.UserResponse, error) {
user, err := s.userRepo.FindByID(id)
if err != nil {
s.logger.Error("获取用户失败", "id", id, "error", err)
return nil, fmt.Errorf("获取用户失败: %w", err)
}
return &user.ToResponse(), nil
}
// UpdateUser 更新用户信息
func (s *UserService) UpdateUser(id uint, req model.UpdateUserRequest) (*model.UserResponse, error) {
// 获取现有用户
user, err := s.userRepo.FindByID(id)
if err != nil {
s.logger.Error("获取用户失败", "id", id, "error", err)
return nil, fmt.Errorf("获取用户失败: %w", err)
}
// 检查用户名是否已存在(如果要更新用户名)
if req.Username != nil && *req.Username != user.Username {
existingUser, err := s.userRepo.FindByUsername(*req.Username)
if err == nil && existingUser != nil && existingUser.ID != id {
return nil, errors.New("用户名已存在")
}
user.Username = *req.Username
}
// 检查邮箱是否已存在(如果要更新邮箱)
if req.Email != nil && *req.Email != user.Email {
existingUser, err := s.userRepo.FindByEmail(*req.Email)
if err == nil && existingUser != nil && existingUser.ID != id {
return nil, errors.New("邮箱已存在")
}
user.Email = *req.Email
}
// 更新其他字段
if req.FirstName != nil {
user.FirstName = *req.FirstName
}
if req.LastName != nil {
user.LastName = *req.LastName
}
if req.Avatar != nil {
user.Avatar = *req.Avatar
}
if req.IsActive != nil {
user.IsActive = *req.IsActive
}
// 保存更新
if err := s.userRepo.Update(user); err != nil {
s.logger.Error("更新用户失败", "id", id, "error", err)
return nil, fmt.Errorf("更新用户失败: %w", err)
}
s.logger.Info("用户更新成功", "id", id, "username", user.Username)
return &user.ToResponse(), nil
}
// DeleteUser 删除用户
func (s *UserService) DeleteUser(id uint) error {
// 检查用户是否存在
_, err := s.userRepo.FindByID(id)
if err != nil {
s.logger.Error("获取用户失败", "id", id, "error", err)
return fmt.Errorf("获取用户失败: %w", err)
}
// 删除用户
if err := s.userRepo.Delete(id); err != nil {
s.logger.Error("删除用户失败", "id", id, "error", err)
return fmt.Errorf("删除用户失败: %w", err)
}
s.logger.Info("用户删除成功", "id", id)
return nil
}
// ListUsers 获取用户列表
func (s *UserService) ListUsers(page, size int) (*model.UserListResponse, error) {
if page <= 0 {
page = 1
}
if size <= 0 || size > 100 {
size = 10
}
offset := (page - 1) * size
users, total, err := s.userRepo.FindAll(offset, size)
if err != nil {
s.logger.Error("获取用户列表失败", "error", err)
return nil, fmt.Errorf("获取用户列表失败: %w", err)
}
// 转换为响应格式
userResponses := make([]model.UserResponse, len(users))
for i, user := range users {
userResponses[i] = user.ToResponse()
}
return &model.UserListResponse{
Users: userResponses,
Total: total,
Page: page,
Size: size,
}, nil
}
// ChangePassword 修改密码
func (s *UserService) ChangePassword(id uint, req model.ChangePasswordRequest) error {
// 获取用户
user, err := s.userRepo.FindByID(id)
if err != nil {
s.logger.Error("获取用户失败", "id", id, "error", err)
return fmt.Errorf("获取用户失败: %w", err)
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
s.logger.Warn("修改密码失败", "id", id, "reason", "旧密码错误")
return errors.New("旧密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("密码加密失败", "id", id, "error", err)
return fmt.Errorf("密码加密失败: %w", err)
}
// 更新密码
user.Password = string(hashedPassword)
if err := s.userRepo.Update(user); err != nil {
s.logger.Error("更新密码失败", "id", id, "error", err)
return fmt.Errorf("更新密码失败: %w", err)
}
s.logger.Info("密码修改成功", "id", id)
return nil
}
package handler
import (
"net/http"
"strconv"
"user-management/internal/model"
"user-management/internal/service"
"user-management/pkg/logger"
"user-management/pkg/response"
"github.com/gin-gonic/gin"
)
// UserHandler 用户处理器
type UserHandler struct {
userService *service.UserService
authService *service.AuthService
logger logger.Logger
}
// NewUserHandler 创建用户处理器
func NewUserHandler(userService *service.UserService, authService *service.AuthService, logger logger.Logger) *UserHandler {
return &UserHandler{
userService: userService,
authService: authService,
logger: logger,
}
}
// Register 用户注册
// @Summary 用户注册
// @Tags users
// @Accept json
// @Produce json
// @Param user body model.CreateUserRequest true "用户信息"
// @Success 201 {object} response.Response{data=model.UserResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req model.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("注册请求参数错误", "error", err)
response.Error(c, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
user, err := h.userService.CreateUser(req)
if err != nil {
h.logger.Error("创建用户失败", "error", err)
response.Error(c, http.StatusInternalServerError, "创建用户失败", err.Error())
return
}
h.logger.Info("用户注册成功", "username", req.Username)
response.Success(c, http.StatusCreated, "注册成功", user)
}
// Login 用户登录
// @Summary 用户登录
// @Tags users
// @Accept json
// @Produce json
// @Param credentials body model.LoginRequest true "登录凭证"
// @Success 200 {object} response.Response{data=model.LoginResponse}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/login [post]
func (h *UserHandler) Login(c *gin.Context) {
var req model.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("登录请求参数错误", "error", err)
response.Error(c, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
user, err := h.userService.Authenticate(req)
if err != nil {
h.logger.Warn("用户登录失败", "username", req.Username, "error", err)
response.Error(c, http.StatusUnauthorized, "登录失败", "用户名或密码错误")
return
}
token, err := h.authService.GenerateToken(user.ID)
if err != nil {
h.logger.Error("生成令牌失败", "userID", user.ID, "error", err)
response.Error(c, http.StatusInternalServerError, "登录失败", "生成令牌失败")
return
}
loginResp := model.LoginResponse{
Token: token,
User: user.ToResponse(),
}
h.logger.Info("用户登录成功", "username", req.Username)
response.Success(c, http.StatusOK, "登录成功", loginResp)
}
// GetProfile 获取当前用户信息
// @Summary 获取当前用户信息
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} response.Response{data=model.UserResponse}
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/profile [get]
func (h *UserHandler) GetProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
user, err := h.userService.GetUserByID(userID.(uint))
if err != nil {
h.logger.Error("获取用户信息失败", "userID", userID, "error", err)
response.Error(c, http.StatusInternalServerError, "获取用户信息失败", err.Error())
return
}
response.Success(c, http.StatusOK, "获取成功", user)
}
// UpdateProfile 更新当前用户信息
// @Summary 更新当前用户信息
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param user body model.UpdateUserRequest true "用户信息"
// @Success 200 {object} response.Response{data=model.UserResponse}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/profile [put]
func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
var req model.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("更新用户请求参数错误", "userID", userID, "error", err)
response.Error(c, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
user, err := h.userService.UpdateUser(userID.(uint), req)
if err != nil {
h.logger.Error("更新用户失败", "userID", userID, "error", err)
response.Error(c, http.StatusInternalServerError, "更新用户失败", err.Error())
return
}
h.logger.Info("用户更新成功", "userID", userID)
response.Success(c, http.StatusOK, "更新成功", user)
}
// ChangePassword 修改密码
// @Summary 修改密码
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param password body model.ChangePasswordRequest true "密码信息"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/change-password [post]
func (h *UserHandler) ChangePassword(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
var req model.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("修改密码请求参数错误", "userID", userID, "error", err)
response.Error(c, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
if err := h.userService.ChangePassword(userID.(uint), req); err != nil {
h.logger.Error("修改密码失败", "userID", userID, "error", err)
response.Error(c, http.StatusBadRequest, "修改密码失败", err.Error())
return
}
h.logger.Info("密码修改成功", "userID", userID)
response.Success(c, http.StatusOK, "密码修改成功", nil)
}
// GetUser 获取用户信息(管理员)
// @Summary 获取用户信息(管理员)
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response{data=model.UserResponse}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.Error(c, http.StatusBadRequest, "请求参数错误", "无效的用户ID")
return
}
user, err := h.userService.GetUserByID(uint(id))
if err != nil {
h.logger.Error("获取用户失败", "id", id, "error", err)
response.Error(c, http.StatusNotFound, "用户不存在", err.Error())
return
}
response.Success(c, http.StatusOK, "获取成功", user)
}
// ListUsers 获取用户列表(管理员)
// @Summary 获取用户列表(管理员)
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "页码" default(1)
// @Param size query int false "每页数量" default(10)
// @Success 200 {object} response.Response{data=model.UserListResponse}
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
userList, err := h.userService.ListUsers(page, size)
if err != nil {
h.logger.Error("获取用户列表失败", "error", err)
response.Error(c, http.StatusInternalServerError, "获取用户列表失败", err.Error())
return
}
response.Success(c, http.StatusOK, "获取成功", userList)
}
// DeleteUser 删除用户(管理员)
// @Summary 删除用户(管理员)
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "用户ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.Error(c, http.StatusBadRequest, "请求参数错误", "无效的用户ID")
return
}
if err := h.userService.DeleteUser(uint(id)); err != nil {
h.logger.Error("删除用户失败", "id", id, "error", err)
response.Error(c, http.StatusInternalServerError, "删除用户失败", err.Error())
return
}
h.logger.Info("用户删除成功", "id", id)
response.Success(c, http.StatusOK, "删除成功", nil)
}
package middleware
import (
"net/http"
"strings"
"user-management/pkg/auth"
"user-management/pkg/logger"
"user-management/pkg/response"
"github.com/gin-gonic/gin"
)
// AuthMiddleware JWT认证中间件
func AuthMiddleware(jwtService *auth.JWTService, logger logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
logger.Warn("缺少认证令牌", "path", c.Request.URL.Path)
response.Error(c, http.StatusUnauthorized, "未授权", "缺少认证令牌")
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
logger.Warn("令牌格式错误", "path", c.Request.URL.Path)
response.Error(c, http.StatusUnauthorized, "未授权", "令牌格式错误")
c.Abort()
return
}
token := parts[1]
claims, err := jwtService.ValidateToken(token)
if err != nil {
logger.Warn("无效令牌", "path", c.Request.URL.Path, "error", err)
response.Error(c, http.StatusUnauthorized, "未授权", "无效令牌")
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
// AdminMiddleware 管理员权限中间件
func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 这里简化处理,实际应用中应该检查用户角色
// 可以从数据库或JWT中获取用户角色信息
userID, exists := c.Get("userID")
if !exists {
response.Error(c, http.StatusUnauthorized, "未授权", "用户ID不存在")
c.Abort()
return
}
// 简化处理:假设ID为1的用户是管理员
if userID.(uint) != 1 {
response.Error(c, http.StatusForbidden, "权限不足", "需要管理员权限")
c.Abort()
return
}
c.Next()
}
}
// LoggerMiddleware 日志中间件
func LoggerMiddleware(logger logger.Logger) gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.Info("HTTP请求",
"method", param.Method,
"path", param.Path,
"status", param.StatusCode,
"latency", param.Latency,
"client_ip", param.ClientIP,
"user_agent", param.Request.UserAgent(),
"error", param.ErrorMessage,
)
return ""
})
}
// RecoveryMiddleware 恢复中间件
func RecoveryMiddleware(logger logger.Logger) gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
logger.Error("HTTP请求panic",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"error", recovered,
"stack", gin.Stack(),
)
response.Error(c, http.StatusInternalServerError, "服务器内部错误", "请求处理过程中发生错误")
c.Abort()
})
}
// CORSMiddleware CORS中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length")
c.Header("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// RateLimitMiddleware 简单的限流中间件
func RateLimitMiddleware() gin.HandlerFunc {
// 这里简化处理,实际应用中应该使用更复杂的限流算法
// 可以使用Redis等存储请求计数
return func(c *gin.Context) {
// 简化处理:不做实际限流
c.Next()
}
}
| 方法 | 路径 | 描述 | 认证 |
|---|---|---|---|
| POST | /api/v1/register | 用户注册 | 否 |
| POST | /api/v1/login | 用户登录 | 否 |
| GET | /api/v1/users | 获取用户列表 | 是 |
| GET | /api/v1/users/:id | 获取用户详情 | 是 |
| PUT | /api/v1/users/:id | 更新用户信息 | 是 |
| DELETE | /api/v1/users/:id | 删除用户 | 是 |
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: user-management-app
restart: unless-stopped
ports:
- "8080:8080"
environment:
- APP_SERVER_PORT=8080
- APP_SERVER_MODE=release
- APP_DATABASE_HOST=db
- APP_DATABASE_PORT=3306
- APP_DATABASE_USER=user
- APP_DATABASE_PASSWORD=password
- APP_DATABASE_DBNAME=user_management
- APP_DATABASE_SSLMODE=disable
- APP_JWT_SECRET=your-secret-key-change-in-production
- APP_JWT_EXPIRE=24
- APP_LOGGER_LEVEL=info
- APP_LOGGER_FORMAT=json
- APP_LOGGER_OUTPUT=stdout
depends_on:
db:
condition: service_healthy
networks:
- app-network
db:
image: mysql:8.0
container_name: user-management-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=user_management
- MYSQL_USER=user
- MYSQL_PASSWORD=password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
networks:
- app-network
redis:
image: redis:7-alpine
container_name: user-management-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
timeout: 3s
retries: 5
networks:
- app-network
adminer:
image: adminer
container_name: user-management-adminer
restart: unless-stopped
ports:
- "8081:8080"
environment:
- ADMINER_DEFAULT_SERVER=db
depends_on:
- db
networks:
- app-network
volumes:
db_data:
driver: local
redis_data:
driver: local
networks:
app-network:
driver: bridge
# 构建阶段
FROM golang:1.19-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装必要的包
RUN apk add --no-cache git ca-certificates tzdata
# 复制go mod文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go
# 运行阶段
FROM alpine:latest
# 安装ca-certificates以支持HTTPS请求
RUN apk --no-cache add ca-certificates tzdata
# 创建非root用户
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# 设置工作目录
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/main .
# 复制配置文件
COPY --from=builder /app/config ./config
# 更改文件所有者
RUN chown -R appuser:appgroup /app
# 切换到非root用户
USER appuser
# 暴露端口
EXPOSE 8080
# 运行应用
CMD ["./main"]
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS user_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE user_management;
-- 创建用户表(如果不存在)
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
avatar VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_deleted_at (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 插入默认管理员用户(密码:admin123)
INSERT INTO users (username, email, password, first_name, last_name, is_active)
VALUES ('admin', '[email protected]', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin', 'User', TRUE)
ON DUPLICATE KEY UPDATE username = username;
package service
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"user-management/internal/model"
"user-management/mocks"
)
func TestUserService_CreateUser(t *testing.T) {
// 创建模拟仓库
mockRepo := new(mocks.UserRepository)
mockLogger := new(mocks.Logger)
service := NewUserService(mockRepo, mockLogger)
// 测试用例1:成功创建用户
t.Run("成功创建用户", func(t *testing.T) {
req := model.CreateUserRequest{
Username: "testuser",
Email: "[email protected]",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
// 设置模拟期望
mockRepo.On("FindByUsername", req.Username).Return(nil, errors.New("not found"))
mockRepo.On("FindByEmail", req.Email).Return(nil, errors.New("not found"))
mockRepo.On("Create", mock.AnythingOfType("*model.User")).Return(nil)
mockLogger.On("Info", mock.Anything, mock.Anything, mock.Anything).Return()
// 执行测试
user, err := service.CreateUser(req)
// 断言
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, req.Username, user.Username)
assert.Equal(t, req.Email, user.Email)
// 验证模拟调用
mockRepo.AssertExpectations(t)
})
// 测试用例2:用户名已存在
t.Run("用户名已存在", func(t *testing.T) {
req := model.CreateUserRequest{
Username: "existinguser",
Email: "[email protected]",
Password: "password123",
}
existingUser := &model.User{
Username: req.Username,
Email: "[email protected]",
}
// 设置模拟期望
mockRepo.On("FindByUsername", req.Username).Return(existingUser, nil)
mockLogger.On("Error", mock.Anything, mock.Anything, mock.Anything).Return()
// 执行测试
user, err := service.CreateUser(req)
// 断言
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "用户名已存在")
// 验证模拟调用
mockRepo.AssertExpectations(t)
})
}
func TestUserService_Authenticate(t *testing.T) {
// 创建模拟仓库
mockRepo := new(mocks.UserRepository)
mockLogger := new(mocks.Logger)
service := NewUserService(mockRepo, mockLogger)
// 测试用例1:成功认证
t.Run("成功认证", func(t *testing.T) {
req := model.LoginRequest{
Username: "testuser",
Password: "password123",
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
user := &model.User{
ID: 1,
Username: req.Username,
Email: "[email protected]",
Password: string(hashedPassword),
IsActive: true,
}
// 设置模拟期望
mockRepo.On("FindByUsername", req.Username).Return(user, nil)
mockRepo.On("Update", mock.AnythingOfType("*model.User")).Return(nil)
mockLogger.On("Info", mock.Anything, mock.Anything, mock.Anything).Return()
// 执行测试
result, err := service.Authenticate(req)
// 断言
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, user.ID, result.ID)
assert.Equal(t, user.Username, result.Username)
// 验证模拟调用
mockRepo.AssertExpectations(t)
})
// 测试用例2:用户不存在
t.Run("用户不存在", func(t *testing.T) {
req := model.LoginRequest{
Username: "nonexistentuser",
Password: "password123",
}
// 设置模拟期望
mockRepo.On("FindByUsername", req.Username).Return(nil, errors.New("not found"))
mockLogger.On("Warn", mock.Anything, mock.Anything, mock.Anything).Return()
// 执行测试
result, err := service.Authenticate(req)
// 断言
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "用户名或密码错误")
// 验证模拟调用
mockRepo.AssertExpectations(t)
})
}
package integration
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"user-management/internal/config"
"user-management/internal/handler"
"user-management/internal/service"
"user-management/internal/model"
"user-management/pkg/auth"
"user-management/pkg/database"
"user-management/pkg/logger"
)
type UserAPITestSuite struct {
suite.Suite
router *gin.Engine
db *gorm.DB
userService *service.UserService
authService *service.AuthService
userHandler *handler.UserHandler
}
func (suite *UserAPITestSuite) SetupSuite() {
// 设置测试数据库
db, err := database.NewTestDB()
suite.Require().NoError(err)
suite.db = db
// 自动迁移
err = db.AutoMigrate(&model.User{})
suite.Require().NoError(err)
// 创建服务
testLogger := logger.NewTestLogger()
jwtService := auth.NewJWTService("test-secret", 24)
userRepo := repository.NewUserRepository(db)
suite.userService = service.NewUserService(userRepo, testLogger)
suite.authService = service.NewAuthService(jwtService, userRepo, testLogger)
suite.userHandler = handler.NewUserHandler(suite.userService, suite.authService, testLogger)
// 设置路由
suite.router = gin.New()
api := suite.router.Group("/api/v1")
{
api.POST("/register", suite.userHandler.Register)
api.POST("/login", suite.userHandler.Login)
// 需要认证的路由
authorized := api.Group("")
authorized.Use(middleware.AuthMiddleware(jwtService, testLogger))
{
authorized.GET("/profile", suite.userHandler.GetProfile)
authorized.PUT("/profile", suite.userHandler.UpdateProfile)
authorized.POST("/change-password", suite.userHandler.ChangePassword)
}
// 需要管理员权限的路由
admin := authorized.Group("")
admin.Use(middleware.AdminMiddleware())
{
admin.GET("/users", suite.userHandler.ListUsers)
admin.GET("/users/:id", suite.userHandler.GetUser)
admin.DELETE("/users/:id", suite.userHandler.DeleteUser)
}
}
}
func (suite *UserAPITestSuite) TearDownSuite() {
// 清理测试数据库
sqlDB, _ := suite.db.DB()
sqlDB.Close()
}
func (suite *UserAPITestSuite) TestUserAPI_Register() {
// 准备测试数据
req := model.CreateUserRequest{
Username: "testuser",
Email: "[email protected]",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
reqBody, _ := json.Marshal(req)
// 发送请求
w := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/api/v1/register", bytes.NewBuffer(reqBody))
request.Header.Set("Content-Type", "application/json")
suite.router.ServeHTTP(w, request)
// 断言
assert.Equal(suite.T(), http.StatusCreated, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), float64(201), response["code"])
assert.Equal(suite.T(), "注册成功", response["message"])
assert.NotNil(suite.T(), response["data"])
}
func (suite *UserAPITestSuite) TestUserAPI_Login() {
// 先创建用户
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
user := model.User{
Username: "testuser",
Email: "[email protected]",
Password: string(hashedPassword),
IsActive: true,
}
suite.db.Create(&user)
// 准备登录请求
req := model.LoginRequest{
Username: "testuser",
Password: "password123",
}
reqBody, _ := json.Marshal(req)
// 发送请求
w := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/api/v1/login", bytes.NewBuffer(reqBody))
request.Header.Set("Content-Type", "application/json")
suite.router.ServeHTTP(w, request)
// 断言
assert.Equal(suite.T(), http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), float64(200), response["code"])
assert.Equal(suite.T(), "登录成功", response["message"])
data := response["data"].(map[string]interface{})
assert.NotNil(suite.T(), data["token"])
assert.NotNil(suite.T(), data["user"])
}
func TestUserAPITestSuite(t *testing.T) {
suite.Run(t, new(UserAPITestSuite))
}
package benchmark
import (
"testing"
"user-management/internal/model"
"user-management/internal/service"
"user-management/mocks"
"github.com/stretchr/testify/mock"
)
func BenchmarkUserCreation(b *testing.B) {
// 创建模拟仓库
mockRepo := new(mocks.UserRepository)
mockLogger := new(mocks.Logger)
service := NewUserService(mockRepo, mockLogger)
// 设置模拟期望
mockRepo.On("FindByUsername", mock.AnythingOfType("string")).Return(nil, errors.New("not found"))
mockRepo.On("FindByEmail", mock.AnythingOfType("string")).Return(nil, errors.New("not found"))
mockRepo.On("Create", mock.AnythingOfType("*model.User")).Return(nil)
mockLogger.On("Info", mock.Anything, mock.Anything, mock.Anything).Return()
// 重置计时器
b.ResetTimer()
// 运行基准测试
for i := 0; i < b.N; i++ {
req := model.CreateUserRequest{
Username: "testuser",
Email: "[email protected]",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
_, _ = service.CreateUser(req)
}
}
func BenchmarkUserAuthentication(b *testing.B) {
// 创建模拟仓库
mockRepo := new(mocks.UserRepository)
mockLogger := new(mocks.Logger)
service := NewUserService(mockRepo, mockLogger)
// 准备测试数据
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
user := &model.User{
ID: 1,
Username: "testuser",
Email: "[email protected]",
Password: string(hashedPassword),
IsActive: true,
}
// 设置模拟期望
mockRepo.On("FindByUsername", "testuser").Return(user, nil)
mockRepo.On("Update", mock.AnythingOfType("*model.User")).Return(nil)
mockLogger.On("Info", mock.Anything, mock.Anything, mock.Anything).Return()
// 重置计时器
b.ResetTimer()
// 运行基准测试
for i := 0; i < b.N; i++ {
req := model.LoginRequest{
Username: "testuser",
Password: "password123",
}
_, _ = service.Authenticate(req)
}
}
# 克隆项目
git clone https://github.com/your-username/user-management.git
cd user-management
# 安装依赖
go mod download
# 复制配置文件
cp config/config.example.yaml config/config.yaml
# 启动数据库
docker-compose up -d db redis
# 运行数据库迁移
go run cmd/migrate/main.go up
# 启动应用
go run cmd/server/main.go
# 构建并启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f app
# 停止服务
docker-compose down
# 用户注册
curl -X POST http://localhost:8080/api/v1/register \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "[email protected]",
"password": "password123",
"first_name": "Test",
"last_name": "User"
}'
# 用户登录
curl -X POST http://localhost:8080/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"password": "password123"
}'
# 获取用户信息(需要认证)
curl -X GET http://localhost:8080/api/v1/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
完成本项目后,您将拥有:
这个项目将为您后续的Go开发工作奠定坚实的基础! ```