• Golang 企业级web后端框架


    项目架构

    goweb
    ├── bin
    ├── pkg
    └── src
        ├── config
        │   ├── config.go
        │   └── config.yaml
        ├── go.mod
        ├── go.sum
        ├── logger
        │   ├── go.mod
        │   ├── go.sum
        │   ├── logger.go
        │   └── zap.go
        ├── main.go
        ├── middleware
        │   ├── jwt_auth_middleware.go
        │   └── weblog_middleware.go
        ├── model
        │   ├── conn.go
        │   └── user.go
        ├── module
        │   ├── login_router.go
        │   └── user
        │       ├── controller.go
        │       └── router.go
        └── service
            ├── register_api.go
            └── user_router.go
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    说明

    config:项目配置文件

    logger:项目日志的配置

    middleware:项目用到的中间件

    model:MySQL数据库连接以及数据库表对应的结构体

    module:业务核心,实现业务逻辑以及路由

    service:路由的入口, 注册路由

    main.go:项目启动入口

    项目所选第三方库

    数据库ORM

    JSON-API Web框架

    JWT 认证

    日志库

    项目解析

    config/config.yaml

    说明:项目配置文件

    log:
      # 控制台日志参数
      enableConsole: true
      consoleJSONFormat: true
      consoleLevel: Debug
      # 文件日志参数
      enableFile: true
      fileJSONFormat: false
      fileLevel: Debug
      # 文件存放路径
      fileLocation: /home/www-data/logs/base-log.log
      maxAge: 28 # 最大天数
      maxSize: 100 # 文件最大容量
      compress: true # 是否压缩
      fileExport: /home/www-data/logs
    
    # 项目启动地址
    webapi:
      uri: 0.0.0.0:8080
    
    # 数据库连接配置
    mysqlnd:
      username: root
      password: 123123
      host: 127.0.0.1
      port: 3306
      database: kcm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    config/config.go

    说明:解析配置文件的Go文件

    package config
    
    import (
    	"gopkg.in/yaml.v3"
    	"io/ioutil"
    	"logger"
    	"os"
    	"path"
    	"path/filepath"
    	"runtime"
    	"strings"
    )
    
    var configFile []byte
    
    type Config struct {
    	Log struct {
    		EnableConsole     bool   `yaml:"enableConsole"`
    		ConsoleLevel      string `yaml:"consoleLevel"`
    		ConsoleJSONFormat bool   `yaml:"consoleJSONFormat"`
    		EnableFile        bool   `yaml:"enableFile"`
    		FileJSONFormat    bool   `yaml:"fileJSONFormat"`
    		FileLevel         string `yaml:"fileLevel"`
    		FileLocation      string `yaml:"fileLocation"`
    		MaxAge            int    `yaml:"maxAge"`
    		MaxSize           int    `yaml:"maxSize"`
    		Compress          bool   `yaml:"compress"`
    		FileExport        string `yaml:"fileExport"`
    	}
    
    	Webapi struct {
    		Uri string `yaml:"uri"`
    	}
    
    	Mysqlnd struct {
    		Username string `yaml:"username"`
    		Password string `yaml:"password"`
    		Host     string `yaml:"host"`
    		Port     string `yaml:"port"`
    		Database string `yaml:"database"`
    	}
    }
    
    func init() {
    	var err error
    	var configFilePath = filepath.Join(getCurrentAbPathByCaller(), "config.yaml")
    	configFile, err = ioutil.ReadFile(configFilePath)
    	if err != nil {
    		logger.Fatalf("Read config yaml file err %v", err)
    	}
    }
    
    func GetChannelConfig() (e *Config, err error) {
    	err = yaml.Unmarshal(configFile, &e)
    	return e, err
    }
    
    // 获取程序运行路径(go build)
    func getCurrentDirectory() string {
    	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
    	if err != nil {
    		logger.Errorf("Get current path err %v", err)
    	}
    	return strings.Replace(dir, "\\", "/", -1)
    }
    
    // 获取当前执行文件绝对路径(go run)
    func getCurrentAbPathByCaller() string {
    	var abPath string
    	_, filename, _, ok := runtime.Caller(0)
    	if ok {
    		abPath = path.Dir(filename)
    	}
    	return abPath
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    1. Config结构体中结构体,对应yaml文件中的一级配置
    2. 结构体中的变量对应yaml文件中的二级配置
    3. 读取config.yaml文件,需要使用绝对路径

    logger/logger.go

    说明:项目日志相关,日志等级如下,从上而下依次递增

    type Logger interface {
    	Debugf(format string, args ...interface{})
    
    	Infof(format string, args ...interface{})
    
    	Warnf(format string, args ...interface{})
    
    	Errorf(format string, args ...interface{})
    
    	Fatalf(format string, args ...interface{})
    
    	Panicf(format string, args ...interface{})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    middleware/jwt_auth_middleware.go

    说明:JWT登录验证的文件

    package middleware
    
    import (
    	"crypto/md5"
    	"fmt"
    	jwt "github.com/appleboy/gin-jwt/v2"
    	"github.com/gin-gonic/gin"
    	"logger"
    	"main/module/user"
    	"time"
    )
    
    type JwtUser struct {
    	UserName string
    }
    
    var identityKey = "id"
    
    type login struct {
    	Username string `form:"username" json:"username" binding:"required"`
    	Password string `form:"password" json:"password" binding:"required"`
    }
    
    func AuthMiddleWare() *jwt.GinJWTMiddleware {
    	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
    		// 中间件名称
    		Realm: "gin-jwt",
    		Key:   []byte("secret key"),
    		// token 过期时间
    		Timeout: 24 * time.Hour,
    		// token 刷新最大时间
    		MaxRefresh: 24 * time.Hour,
    		// 身份验证的 key 值
    		IdentityKey: identityKey,
    		// 登录期间的回调的函数
    		PayloadFunc: func(data interface{}) jwt.MapClaims {
    			if v, ok := data.(JwtUser); ok {
    				return jwt.MapClaims{
    					identityKey: v.UserName,
    				}
    			}
    			return jwt.MapClaims{}
    		},
    		// 解析并设置用户身份信息
    		IdentityHandler: func(c *gin.Context) interface{} {
    			claims := jwt.ExtractClaims(c)
    			return JwtUser{
    				UserName: claims[identityKey].(string),
    			}
    		},
    		// 根据登录信息对用户进行身份验证的回调函数
    		Authenticator: func(c *gin.Context) (interface{}, error) {
    			var loginVars login
    			if err := c.ShouldBind(&loginVars); err != nil {
    				return "", jwt.ErrMissingLoginValues
    			}
    			userName := loginVars.Username
    			password := loginVars.Password
    			res := user.SelectByUsername(userName)
    			if res != nil && MD5(password) == res.Password {
    				return JwtUser{
    					UserName: userName,
    				}, nil
    			}
    			return nil, jwt.ErrFailedAuthentication
    		},
    		// 接收用户信息并编写授权规则
    		Authorizator: func(data interface{}, c *gin.Context) bool {
    			if _, ok := data.(JwtUser); ok {
    				return true
    			}
    			return false
    		},
    		// 自定义处理未进行授权的逻辑
    		Unauthorized: func(c *gin.Context, code int, message string) {
    			c.JSON(code, gin.H{
    				"code":    code,
    				"message": message,
    			})
    		},
    		// token 检索模式,用于提取 token,默认值为 header:Authorization
    		TokenLookup:   "header: Authorization, query: token, cookie: jwt",
    		TokenHeadName: "Bearer",
    		TimeFunc:      time.Now,
    	})
    
    	if err != nil {
    		logger.Debugf("JWT err: %v" + err.Error())
    	}
    	// https://jwt.io/ 解析
    	return authMiddleware
    }
    
    func MD5(str string) string {
    	data := []byte(str)
    	has := md5.Sum(data)
    	md5str := fmt.Sprintf("%x", has) //将[]byte转成16进制
    	return md5str
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    其中,Authenticator根据数据库中的数据验证登录信息,若符合,生成token返回给前端

    middleware/weblog_middleware.go

    说明:增加日志输出的数据及格式

    package middleware
    
    import (
    	"github.com/gin-gonic/gin"
    	"logger"
    	"time"
    )
    
    func GinWebLog() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		// 开始时间
    		startTime := time.Now()
    		// 处理请求
    		c.Next()
    		// 结束时间
    		endTime := time.Now()
    		// 执行时长
    		latencyTime := endTime.Sub(startTime)
    		// 请求方式
    		reqMethod := c.Request.Method
    		// 请求路由
    		reqUri := c.Request.RequestURI
    		// 状态码
    		statusCode := c.Writer.Status()
    		// 请求IP
    		clientIP := c.ClientIP()
    		// 日志格式
    		logger.Infof("| %3d | %13v | %15s | %s | %s |",
    			statusCode,
    			latencyTime,
    			clientIP,
    			reqMethod,
    			reqUri,
    		)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    效果如下:

    | 200 |    2.803787ms |   172.17.100.16 | GET | /getUserByPage |
    
    • 1

    model/conn.go

    说明:Gorm数据库连接

    package model
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"logger"
    	"main/config"
    )
    
    func MysqlConn() *gorm.DB {
    	configBase, err := config.GetChannelConfig()
    	if err != nil {
    		logger.Fatalf("Get config failed! err: #%v", err)
    		return nil
    	}
    	username := configBase.Mysqlnd.Username
    	password := configBase.Mysqlnd.Password
    	host := configBase.Mysqlnd.Host
    	port := configBase.Mysqlnd.Port
    	dbname := configBase.Mysqlnd.Database
    	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
    	// 连接 mysql
    	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    	if err != nil {
    		logger.Fatalf("MySQL connect failed! err: #%v", err)
    		return nil
    	}
    	// 设置数据库连接池参数
    	sqlDB, _ := db.DB()
    	// 设置数据库连接池最大连接数
    	sqlDB.SetMaxOpenConns(100)
    	// 连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于20,超出的连接会被连接池关闭
    	sqlDB.SetMaxIdleConns(20)
    	return db
    }
    
    var Db *gorm.DB
    
    func init() {
    	Db = MysqlConn()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    model/user.go

    说明:数据库用户表对应的结构体

    package model
    
    type User struct {
    	UserId         int8   `gorm:"column:user_id;AUTO_INCREMENT;comment:用户ID" json:"user_id"`
    	UserName       string `gorm:"column:username;comment:用户名" json:"username"`
    	Password       string `gorm:"column:password;comment:密码" json:"password"`
    	RoleId         int8   `gorm:"column:role_id;comment:角色ID" json:"role_id"`
    	Status         string `gorm:"column:status;comment:用户是否禁用标志位,0为禁用,1为启用" json:"status"`
    	Name           string `gorm:"column:name;comment:用户真实姓名" json:"name"`
    	CreateByUserId int8   `gorm:"column:create_by_user_id;comment:创建者ID" json:"create_by_user_id"`
    }
    
    // TableName 自定义表名
    func (User) TableName() string {
    	return "users"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    其中,user_id是自增主键

    module/user/controller.go

    说明:用户相关的业务处理逻辑,定义各种操作数据库的接口

    package user
    
    import (
    	"errors"
    	"gorm.io/gorm"
    	"logger"
    )
    
    import "main/model"
    
    func SelectByUsername(username string) *model.User {
    	db := model.Db
    	u := model.User{}
    	res := db.Where("username = ?", username).First(&u)
    	if errors.Is(res.Error, gorm.ErrRecordNotFound) {
    		logger.Debugf("Select by username err:" + "未查找到相关数据")
    		return nil
    	}
    	return &u
    }
    
    func GetUserByPage(page, pageSize int) (int64, []*model.User) {
    	db := model.Db
    	var users []*model.User
    	var total int64
    	db.Model(model.User{}).Count(&total)
    	res := db.Limit(pageSize).Offset((page - 1) * pageSize).Find(&users)
    	if errors.Is(res.Error, gorm.ErrRecordNotFound) {
    		logger.Debugf("Get user by page err:" + "未查找到相关数据")
    		return 0, nil
    	}
    	return total, users
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    module/user/router.go

    说明:定义Web请求的接口,接受Restful Api请求,调用controller函数进行处理,并返回结果

    package user
    
    import (
    	"github.com/gin-gonic/gin"
    )
    
    func GetUserByPageHandler(c *gin.Context) {
    	type Param struct {
    		Page     int `form:"page" json:"page" binding:"required"`
    		PageSize int `form:"pageSize" json:"pageSize" binding:"required"`
    	}
    	var param Param
    
    	if err := c.ShouldBind(&param); err != nil {
    		c.JSON(400, gin.H{
    			"status_code": 400,
    			"message":     "参数错误",
    		})
    		return
    	}
    	total, data := GetUserByPage(param.Page, param.PageSize)
    	if data == nil {
    		c.JSON(200, gin.H{
    			"status_code": 200,
    			"message":     "获取用户失败",
    		})
    		return
    	}
    	c.JSON(200, gin.H{
    		"status_code": 200,
    		"message":     "获取用户成功",
    		"total":       total,
    		"data":        data,
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    module/login_router.go

    说明:用户登录的接口

    package module
    
    import (
    	"github.com/gin-gonic/gin"
    	"main/middleware"
    )
    
    func LoginHandler(c *gin.Context) {
    	middleware.AuthMiddleWare().LoginHandler(c)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    service/user_router.go

    说明:注册用户相关的路由

    package service
    
    import (
    	"github.com/gin-gonic/gin"
    	"main/middleware"
    	"main/module/user"
    )
    
    func userRouter(e *gin.Engine) {
    	authMiddleware := middleware.AuthMiddleWare()
    	e.Use(authMiddleware.MiddlewareFunc())
    	{
    		e.GET("/getUserByPage", user.GetUserByPageHandler)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    其中,e.Use(authMiddleware.MiddlewareFunc())表示会根据请求携带的token进行校验,校验失败的请求,不会进入到业务处理逻辑模块

    service/register_api.go

    说明:注册全局路由,初始化Gin

    package service
    
    import (
    	"github.com/gin-gonic/gin"
    	swaggerFiles "github.com/swaggo/files"
    	ginSwagger "github.com/swaggo/gin-swagger"
    	"logger"
    	"main/config"
    	"main/middleware"
    	"main/module"
    )
    
    type Option func(*gin.Engine)
    
    var options []Option
    
    // Include 注册app的路由配置
    func Include(opts ...Option) {
    	options = append(options, opts...)
    }
    
    // Init 初始化
    func Init() *gin.Engine {
    	r := gin.New()
    	// https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies
    	err := r.SetTrustedProxies(nil)
    	if err != nil {
    		logger.Fatalf("Gin set trusted proxies failed! err: #%v", err)
    	}
    	r.Use(middleware.GinWebLog())
    	r.Use(gin.Recovery())
    	swagHandler := ginSwagger.WrapHandler(swaggerFiles.Handler)
    	r.GET("/swagger/*any", swagHandler)
    
    	authMiddleware := middleware.AuthMiddleWare()
    
    	r.POST("/login", module.LoginHandler)
    
    	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
    		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
    	})
    
    	Include(userRouter)
    
    	for _, opt := range options {
    		opt(r)
    	}
    	return r
    }
    
    func StartApi() {
    	// 初始化路由
    	r := Init()
    	configBase, err := config.GetChannelConfig()
    	if err != nil {
    		logger.Fatalf("Get config failed! err: #%v", err)
    	}
    	if err := r.Run(configBase.Webapi.Uri); err != nil {
    		logger.Fatalf("Run web server failed! err: #%v", err)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    main.go

    说明:项目启动入口,初始化全局日志参数配置

    package main
    
    import (
    	"logger"
    	"main/config"
    	"main/service"
    )
    
    func main() {
    	service.StartApi()
    }
    
    func init() {
    	configBase, err := config.GetChannelConfig()
    	if err != nil {
    		logger.Fatalf("Get channel config failed! err: %v", err)
    	}
    	//为日志指定参数
    	configInit := logger.Configuration{
    		EnableConsole:     configBase.Log.EnableConsole,
    		ConsoleJSONFormat: configBase.Log.ConsoleJSONFormat,
    		ConsoleLevel:      logger.GetLevel(configBase.Log.ConsoleLevel),
    		EnableFile:        configBase.Log.EnableFile,
    		FileJSONFormat:    configBase.Log.FileJSONFormat,
    		FileLevel:         logger.GetLevel(configBase.Log.FileLevel),
    		FileLocation:      configBase.Log.FileLocation,
    		MaxAge:            configBase.Log.MaxAge,
    		MaxSize:           configBase.Log.MaxSize,
    		Compress:          configBase.Log.Compress,
    	}
    	err = logger.InitGlobalLogger(configInit, logger.InstanceZapLogger)
    	if err != nil {
    		logger.Fatalf("Could not instantiate log! err: %v", err)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    启动项目

    项目src目录下,执行:

    go run main.go
    
    • 1

    在这里插入图片描述

    请求验证

    /login

    说明:登录请求

    在这里插入图片描述


    在这里插入图片描述

    /getUserByPage

    说明:根据分页获取用户信息请求

    在这里插入图片描述


    在这里插入图片描述

    服务端日志

    在这里插入图片描述

    项目地址

    https://github.com/ximury/goweb

  • 相关阅读:
    我用Redis分布式锁,抢了瓶茅台,然后GG了~~
    知识增强的大语言模型
    数学概念(mathematical concepts)持续更新
    Rust错误处理和Result枚举类异常错误传递
    ATFX汇市:9月非农再超预期,高利率并未导致美国宏观经济收缩
    【Spring Boot 集成应用】Spring Security集成整合配置使用
    蓝桥杯打卡Day9
    php 打包下载
    【QT】Qt Creator生成动态库(DLL)并调用
    linux生产者消费者模型
  • 原文地址:https://blog.csdn.net/WU2629409421perfect/article/details/125451405