Go web 实战

注:go 基础学完后,通过一个用户登陆来检验之前的学习成果

1. 引入第三方工具包

go.mod 文件中

# 使用go m
module gin_demo

go 1.22

require (
	github.com/gin-contrib/cors v1.7.3     # 跨域
	github.com/gin-gonic/gin v1.10.0    # Gin 框架
	github.com/golang-jwt/jwt/v5 v5.2.1  # jwt 生成器
	golang.org/x/crypto v0.31.0   # 密码加密工具
	gorm.io/driver/mysql v1.5.7  # mysql数据库
	gorm.io/gorm v1.25.12   # gorm 数据库框架
)

2. 准备项目目录

在这里插入图片描述

3. 准备数据库

准备一张用户表,其它省略

CREATE TABLE `sys_user` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
  `user_name` varchar(30) NOT NULL COMMENT '用户账号',
  `nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
  `user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)',
  `email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
  `phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
  `sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
  `avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
  `password` varchar(100) DEFAULT '' COMMENT '密码',
  `status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

4. 配置数据连接操作

4.1. 配置数据连接操作

settings/setting.go

package settings

type Setting struct {
	Server   ServerConfig
	Database DatabaseConfig
	RedisDb  RedisConfig
}

type ServerConfig struct {
	Host string
	Port string
}

type DatabaseConfig struct {
	Host     string
	Port     int64
	UserName string
	Password string
	DbName   string
}

type RedisConfig struct {
	Host     string
	Port     int64
	UserName string
	Password string
}

var MyConfig *Setting

func init() {
	MyConfig = &Setting{
		Server: ServerConfig{
			Host: "127.0.0.1",
			Port: ":8080",
		},
		Database: DatabaseConfig{
			Host:     "127.0.0.1",
			Port:    3306,
			UserName: "root",
			Password: "123456",   // 这里换成实际数据库密码
			DbName:   "demo",   // 
		},
	}
}

4.2. 配置日志(每天生成日志信息)

settings/logger.go
注意: 如果想打印信息输入到日志中,需要调用全局变量 如: settings.Log.info(‘打印信息。。。。’)

package settings

import (
	"github.com/sirupsen/logrus"
	"io"
	"os"
	"path/filepath"
	"time"
)

var Log = logrus.New()

func init() {
	// 设置日志格式为json
	Log.SetFormatter(&logrus.TextFormatter{
		TimestampFormat: "2006-01-02 15:04:05,000",
		FullTimestamp:   true,
	})

	// 设置日志输出为文件
	logDir := "logs"
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		err := os.Mkdir(logDir, 0755)
		if err != nil {
			Log.Error("创建文件失败。。。")
		}
	}
	// 按天生成日志的名称
	logFileName := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log")
	file, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		Log.Error("打开日志文件失败。。。")
	}

	Log.SetOutput(io.MultiWriter(file, os.Stdout))
	Log.SetLevel(logrus.InfoLevel)
}

5. mysql 连接配置

在 db/mysql.go 中添加如下代码:

package db

import (
	"database/sql"
	"fmt"
	"gin_demo/settings"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"time"
)

var SessionSql *gorm.DB

func init() {

	dsn := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		settings.MyConfig.Database.UserName,
		settings.MyConfig.Database.Password,
		settings.MyConfig.Database.Host,
		settings.MyConfig.Database.Port,
		settings.MyConfig.Database.DbName,
	)

	sqlDB, err := sql.Open("mysql", dsn)
	if err != nil {
		fmt.Println("open mysql err:", err)
		return
	}
	sqlDB.SetMaxIdleConns(10)
	sqlDB.SetMaxOpenConns(100)
	sqlDB.SetConnMaxLifetime(time.Hour)

	SessionSql, err = gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		fmt.Println("open mysql err:", err)
		return
	}
}

6. common 公共方法

common/result.go

package common
// 封装统一返回类
type ResultJson struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data any    `json:"data"`
}

func (r *ResultJson) Success(result any) *ResultJson {
	r.Code = 200
	r.Msg = "ok"
	r.Data = result
	return r
}

func (r *ResultJson) Fail(code int, msg string) *ResultJson {
	r.Code = code
	r.Msg = msg
	return r
}

7. 创建用户数据库操作实例

7.1 创建分页实例

models/page.go

package models

type Paper struct {
	List  interface{} `json:"list"`  // 数据集合
	Total int64       `json:"total"` // 总条数
	Page  int         `json:"page"`  //当前页
	Size  int         `json:"size"`  //数据条数
}

7.2 创建时间格式化类

models/localTime.go
注:如果想让读取的时间 以 ”yyyy-MM-dd HH:mm:ss“ 格式显示,则需要重写time 方法

package models

import (
	"database/sql/driver"
	"errors"
	"fmt"
	"strings"
	"time"
)

type LocalTime time.Time

func (t *LocalTime) MarshalJSON() ([]byte, error) {
	// LocalTime 转换成 time.Time 类型
	tTime := time.Time(*t)
	return []byte(fmt.Sprintf("\"%v\"", tTime.Format("2006-01-02 15:04:05"))), nil
}

// UnmarshalJSON 实现了json.Unmarshaler接口,用于自定义JSON格式的反序列化。
// 该方法将JSON时间字符串转换为LocalTime类型,考虑到了时区差异。
// 参数 data: 从JSON中读取的时间字符串,格式为"2006-01-02 15:04:05"。
// 返回值 error: 如果转换失败,则返回错误信息。
func (t *LocalTime) UnmarshalJSON(data []byte) error {
	// 去除接收的str收尾多余的"
	timeStr := strings.Trim(string(data), "\"")
	// 转为当前服务器所在时区时间
	t1, err := time.ParseInLocation("2006-01-02 15:04:05", timeStr, time.Local)
	// time.Time转换为LocalTime类型
	*t = LocalTime(t1)
	if err != nil {
		return errors.New("时间格式有误,转换失败")
	}
	return nil
}

func (t LocalTime) Value() (driver.Value, error) {
	var zeroTime time.Time
	tlt := time.Time(t)
	//判断给定时间是否和默认零时间的时间戳相同
	if tlt.UnixMicro() == zeroTime.UnixMicro() {
		return nil, nil
	}
	return tlt, nil
}

func (t *LocalTime) Scan(v any) (err error) {
	if value, ok := v.(time.Time); ok {
		*t = LocalTime(value)
		return nil
	}
	return fmt.Errorf("不能转换 %V 为时间戳", v)
}

7.3 创建用户实体类

models/user.go

package models

import (
	"errors"
	"gin_demo/db"
	"gin_demo/handle"
	"gin_demo/utils"
	"golang.org/x/crypto/bcrypt"
	"net/url"
	"strconv"
	"time"
)

type User struct {
	UserId      int        `gorm:"primary_key;AUTO_INCREMENT" json:"user_id"`
	DeptId      int        `gorm:"column:dept_id" json:"dept_id"`
	Username    string     `gorm:"column:user_name" json:"username"`
	Nickname    string     `gorm:"column:nick_name" json:"nickname"`
	UserType    int        `gorm:"column:user_type" json:"user_type"`
	Email       string     `gorm:"column:email" json:"email"`
	PhoneNumber string     `gorm:"column:phonenumber" json:"phonenumber"`
	Sex         int        `gorm:"column:sex" json:"sex"`
	Avatar      string     `gorm:"column:avatar" json:"avatar"`
	Password    string     `gorm:"column:password" json:"password"`
	Status      int        `gorm:"column:status" json:"status"`
	DelFlag     int        `gorm:"default:0;column:del_flag" json:"del_flag"`
	LoginIp     string     `gorm:"column:login_ip" json:"login_ip"`
	LoginDate   *LocalTime `gorm:"column:login_date;autoCreateTime" json:"login_date"`
	CreateBy    string     `gorm:"column:create_by" json:"create_by"`
	CreateTime  *LocalTime `gorm:"column:create_time;autoCreateTime" json:"create_time"`
	UpdateBy    string     `gorm:"column:update_by" json:"update_by"`
	UpdateTime  *LocalTime `gorm:"column:update_time;autoUpdateTime" json:"update_time"`
	Remark      string     `gorm:"column:remark" json:"remark"`
}

func (u *User) TableName() string {
	return "sys_user"
}

// AddUser 添加用户信息
func (u *User) AddUser(user *User) (id int, err error) {
	// 检查用户名是否唯一
	err = db.SessionSql.Where("user_name = ?", user.Username).Take(&User{}).Error
	if err == nil {
		return 0, errors.New("用户名已存在")
	}
	// 检查手机号是否唯一
	err = db.SessionSql.Where("phonenumber = ?", user.PhoneNumber).Take(&User{}).Error
	if err == nil {
		return 0, errors.New("手机号已存在")
	}

	// 密码加密
	password := user.Password
	if password != "" {
		hashPwd, err := hashPassword(password)
		if err != nil {
			return 0, err
		}
		user.Password = hashPwd
	}

	username := handle.Claims.UserName
	user.CreateBy = username

	result := db.SessionSql.Create(&user)
	err = result.Error
	if err != nil {
		return 0, err
	}
	return user.UserId, nil
}

// GetAll 获取所有用户信息
func (u *User) GetAll(params *url.Values) (paper *Paper, err error) {
	var (
		users []*User
		count int64
	)

	limit, _ := strconv.Atoi(params.Get("limit"))
	page, _ := strconv.Atoi(params.Get("page"))

	pageSize := (page - 1) * limit

	// 获取总条数
	db.SessionSql.Model(&User{}).Count(&count)

	err = db.SessionSql.Model(&u).Limit(limit).Offset(pageSize).Scan(&users).Error
	if err != nil {
		return nil, err
	}
	for i := range users {
		users[i] = DesensitizeUser(users[i])
	}

	return &Paper{Total: count, List: users}, nil
}

// DeleteUser 删除用户
func (u *User) DeleteUser(id int) (b bool) {
	if id == 0 {
		return false
	}
	err := db.SessionSql.Where("user_id = ?", id).Delete(&User{}).Error
	if err != nil {
		return false
	}
	return true
}

// CheckUser 验证用户
func (u *User) CheckUser(username, password string) (token string, err error) {
	if username != "" && password != "" {
		if err = db.SessionSql.Where("user_name", username).Or("phonenumber", username).Take(&u).Error; err != nil {
			return "", errors.New("用户名或密码错误")
		}
		err = comparePasswords(u.Password, password)
		if err != nil {
			return "", errors.New("密码不正确")
		}

		util := utils.JwtUtil{}
		token, err = util.GenerateToken(u.UserId, u.Username, 7*24*time.Hour)
		if err != nil {
			return "", err
		}
		return token, nil
	}

	return "", errors.New("用户名或密码错误")
}

// UpdateUser 更新用户信息
func (u *User) UpdateUser(user *User) (err error) {
	if user.UserId == 0 {
		return errors.New("缺少必要参数ID")
	}
	username := handle.Claims.UserName
	user.UpdateBy = username
	err = db.SessionSql.Where("user_id = ?", user.UserId).Updates(user).Error
	if err != nil {
		return errors.New("修改失败")
	}
	return nil
}

func hashPassword(password string) (string, error) {
	// 使用bcrypt库的GenerateFromPassword函数进行哈希处理
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(hashedPassword), nil
}

func comparePasswords(hashedPassword, inputPassword string) error {
	// 使用bcrypt库的CompareHashAndPassword函数比较密码
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(inputPassword))
	return err
}

// DesensitizeUser 用户脱敏
func DesensitizeUser(user *User) *User {
	user.Password = "******"
	// user.PhoneNumber = strings.Replace(user.PhoneNumber, user.PhoneNumber[3:7], "****", -1)
	return user
}

8. 创建用户控制器

8.1. 创建登陆控制器

controller/login.go

package controller

import (
	"fmt"
	"gin_demo/common"
	"gin_demo/handle"
	"gin_demo/models"
	"github.com/gin-gonic/gin"
	"net/http"
)

type LoginController struct {
}

func (l LoginController) Login(c *gin.Context) {
	res := new(common.ResultJson)
	user := new(models.User)
	var body struct {
		Username string `form:"username" json:"username" binding:"required"`
		Password string `form:"password" json:"password" binding:"required"`
	}
	err := c.ShouldBind(&body)
	if err != nil {
		res.Fail(1001, err.Error())
	} else {
		token, err := user.CheckUser(body.Username, body.Password)
		if err != nil {
			res.Fail(1001, err.Error())
		} else {
			res.Success(map[string]string{"access_token": token})
		}
	}
	c.JSON(http.StatusOK, res)
}

func (l LoginController) Logout(c *gin.Context) {
	res := new(common.ResultJson)
	id := handle.Claims.Id
	fmt.Println("id:", id)
	res.Success(id)
	c.JSON(http.StatusOK, res)
}

8.2. 创建用户控制器

controller/user.go

package controller

import (
	"gin_demo/common"
	"gin_demo/models"
	"github.com/gin-gonic/gin"
	_ "github.com/golang-jwt/jwt/v5"
	"net/http"
	"strconv"
)

type UserController struct {
	Account  string `json:"account" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func (UserController) Create(c *gin.Context) {
	res := new(common.ResultJson)
	user := new(models.User)
	if err := c.ShouldBind(&user); err != nil {
		res.Fail(4001, "参数不正确")
	}
	id, err := user.AddUser(user)
	if err != nil {
		res.Fail(-1, err.Error())
	} else {
		res.Success(id)
	}
	c.JSON(http.StatusOK, res)
}

func (UserController) GetPage(c *gin.Context) {
	res := new(common.ResultJson)
	user := new(models.User)
	params := c.Request.URL.Query()

	pager, err := user.GetAll(&params)
	if err != nil {
		res.Fail(40001, "请求失败。。。")
	} else {
		res.Success(pager)
	}
	c.JSON(http.StatusOK, res)
}

// Update 修改用户
func (UserController) Update(c *gin.Context) {
	res := new(common.ResultJson)
	user := new(models.User)
	if err := c.ShouldBind(&user); err != nil {
		res.Fail(4001, "参数不正确")
	}
	err := user.UpdateUser(user)
	if err != nil {
		res.Fail(-1, err.Error())
	} else {
		res.Success("修改成功")
	}
	c.JSON(http.StatusOK, res)
}

// Delete 删除用户
func (UserController) Delete(c *gin.Context) {
	res := new(common.ResultJson)
	user := new(models.User)
	id := c.Param("id")
	userId, err := strconv.Atoi(id)
	if err != nil {
		res.Fail(40001, "参数不正确")
	} else {
		f := user.DeleteUser(userId)
		if f {
			res.Success("删除成功")
		} else {
			res.Fail(-1, "删除失败")
		}
	}

	c.JSON(http.StatusOK, res)
}

9. 配置路由

routers/router.go

package routers

import (
	"gin_demo/controller"
	"gin_demo/handle"
	_ "gin_demo/models"
	"github.com/gin-gonic/gin"
)

func RegisterRouter(route *gin.Engine) {
	route.POST("/api/login", controller.LoginController{}.Login)

	api := route.Group("/api").Use(handle.JwtHandle())
	{
		// 登出
		api.GET("/logout", controller.LoginController{}.Logout)
		// 用户管理
		api.POST("/user/create", controller.UserController{}.Create)
		api.GET("/user/getPage", controller.UserController{}.GetPage)
		api.POST("/user/update", controller.UserController{}.Update)
		api.DELETE("/user/delete/:id", controller.UserController{}.Delete)
	}

}

10. 创建JWT 工具类

utils/jwt_util.go

package utils

import (
	"github.com/golang-jwt/jwt/v5"
	"time"
)

type JwtUtil struct {
}

type MyClaims struct {
	Id       int    `json:"id"`
	UserName string `json:"userName"`
	jwt.RegisteredClaims
}

var key = []byte("这里设置jwt加密的盐值")   // 不要设置得过于简单

// GenerateToken 创建token
func (u *JwtUtil) GenerateToken(id int, userName string, d time.Duration) (string, error) {
	claims := MyClaims{
		Id:       id,
		UserName: userName,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(d)), // 有效时间
			Issuer:    "Sun_yp",
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(key)
}

// ParseWithClaims 解析token
func (u *JwtUtil) ParseWithClaims(jwtStr string) (*MyClaims, error) {
	tokenObj, err := jwt.ParseWithClaims(jwtStr, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
		return key, nil
	})
	if err != nil {
		return nil, err
	}

	if claims, ok := tokenObj.Claims.(*MyClaims); ok && tokenObj.Valid {
		return claims, nil
	}
	return nil, err
}

11. 配置 jwt 令牌校验中间件

handle/jwt_handle.go

package handle

import (
	"gin_demo/common"
	"gin_demo/utils"
	"github.com/gin-gonic/gin"
	"net/http"
)

var Claims *utils.MyClaims

// JwtHandle jwt 检测
func JwtHandle() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.GetHeader("access_token")
		method := c.Request.Method
		rs := common.ResultJson{}
		// token 为空
		if token == "" && method != "OPTIONS" {
			c.Abort()
			rs.Fail(-1, "缺少令牌")
			c.JSON(http.StatusOK, rs)
			return
		}
		util := utils.JwtUtil{}
		var err error
		Claims, err = util.ParseWithClaims(token)
		if err != nil {
			c.Abort()
			rs.Fail(-1, "令牌错误")
			c.JSON(http.StatusOK, rs)
			return
		}
		c.Next()
	}
}

12. 主入口文件

main.go

package main

import (
	"gin_demo/routers"
	"gin_demo/settings"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {

	// 配置
	gin.SetMode(gin.DebugMode)

	r := gin.Default()

	r.Use(gin.LoggerWithWriter(settings.Log.Writer()))

	// 解决跨域
	config := cors.DefaultConfig()
	config.AllowAllOrigins = true
	config.AddAllowHeaders("access_token")
	config.AllowCredentials = true
	r.Use(cors.New(config))

	_ = r.SetTrustedProxies([]string{settings.MyConfig.Server.Host})

	// 注册路由
	routers.RegisterRouter(r)

	_ = r.Run(settings.MyConfig.Server.Port)
}

使用Apipost 工具测试登陆:
在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐