├── Exchangeapp_backend ├── config │ ├── config.go │ ├── config.yml │ ├── db.go │ └── redis.go ├── controllers │ ├── article_controller.go │ ├── auth_controller.go │ ├── exchange_rate_controller.go │ └── like_controller.go ├── global │ └── global.go ├── go.mod ├── go.sum ├── main.go ├── middlewares │ └── auth_middleware.go ├── models │ ├── article.go │ ├── exchange_rate.go │ └── user.go ├── router │ └── router.go └── utils │ └── utils.go ├── Exchangeapp_frontend ├── __VLS_types.d.ts ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── axios.ts │ ├── components │ │ ├── Login.vue │ │ └── Register.vue │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── shims-vue.d.ts │ ├── store │ │ └── auth.ts │ ├── types │ │ └── Article.d.ts │ ├── views │ │ ├── CurrencyExchangeView.vue │ │ ├── HomeView.vue │ │ ├── NewsDetailView.vue │ │ └── NewsView.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── Gin感情参考書(课件).md ├── README.md ├── image-1.png ├── image-10.png ├── image-11.png ├── image-12.png ├── image-13.png ├── image-2.png ├── image-3.png ├── image-4.png ├── image-5.png ├── image-6.png ├── image-7.png ├── image-8.png ├── image-9.png └── image.png /Exchangeapp_backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type Config struct { 10 | App struct { 11 | Name string 12 | Port string 13 | } 14 | Database struct { 15 | Dsn string 16 | MaxIdleConns int 17 | MaxOpenConns int 18 | } 19 | Redis struct{ 20 | Addr string 21 | DB int 22 | Password string 23 | } 24 | } 25 | 26 | var AppConfig *Config 27 | 28 | func InitConfig() { 29 | viper.SetConfigName("config") 30 | viper.SetConfigType("yml") 31 | viper.AddConfigPath("./config") 32 | 33 | if err := viper.ReadInConfig(); err !=nil{ 34 | log.Fatalf("Error reading config file: %v", err) 35 | } 36 | 37 | AppConfig = &Config{} 38 | 39 | if err:= viper.Unmarshal(AppConfig); err !=nil{ 40 | log.Fatalf("Unable to decode into struct: %v", err) 41 | } 42 | 43 | initDB() 44 | initRedis() 45 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/config/config.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: CurrencyExchangeApp 3 | port: :3000 4 | 5 | database: 6 | dsn: root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local 7 | MaxIdleConns: 11 8 | MaxOpenCons: 114 9 | 10 | redis: 11 | addr: localhost:6379 12 | DB: 0 13 | Password: "" -------------------------------------------------------------------------------- /Exchangeapp_backend/config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "exchangeapp/global" 5 | "log" 6 | "time" 7 | 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func initDB(){ 13 | dsn := AppConfig.Database.Dsn 14 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 15 | 16 | if err!=nil{ 17 | log.Fatalf("Failed to initialize database, got error: %v", err) 18 | } 19 | 20 | sqlDB, err := db.DB() 21 | 22 | sqlDB.SetMaxIdleConns(AppConfig.Database.MaxIdleConns) 23 | sqlDB.SetMaxOpenConns(AppConfig.Database.MaxOpenConns) 24 | sqlDB.SetConnMaxLifetime(time.Hour) 25 | 26 | if err !=nil{ 27 | log.Fatalf("Failed to configure database, got error: %v", err) 28 | } 29 | 30 | global.Db = db 31 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/config/redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "exchangeapp/global" 5 | "log" 6 | 7 | "github.com/go-redis/redis" 8 | ) 9 | 10 | func initRedis(){ 11 | 12 | addr := AppConfig.Redis.Addr 13 | db := AppConfig.Redis.DB 14 | password := AppConfig.Redis.Password 15 | 16 | RedisClient := redis.NewClient(&redis.Options{ 17 | Addr: addr, 18 | DB: db, 19 | Password: password, 20 | }) 21 | 22 | _, err := RedisClient.Ping().Result() 23 | 24 | if err !=nil{ 25 | log.Fatalf("Failed to connect to Redis, got error: %v", err) 26 | } 27 | 28 | global.RedisDB = RedisClient 29 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/controllers/article_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "exchangeapp/global" 7 | "exchangeapp/models" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/go-redis/redis" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | var cacheKey = "articles" 17 | 18 | func CreateArticle(ctx *gin.Context){ 19 | var article models.Article 20 | 21 | if err := ctx.ShouldBindJSON(&article); err !=nil{ 22 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 23 | return 24 | } 25 | 26 | if err := global.Db.AutoMigrate(&article); err !=nil{ 27 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 28 | return 29 | } 30 | 31 | if err := global.Db.Create(&article).Error; err !=nil{ 32 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 33 | return 34 | } 35 | 36 | if err := global.RedisDB.Del(cacheKey).Err(); err !=nil{ 37 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 38 | return 39 | } 40 | 41 | ctx.JSON(http.StatusCreated, article) 42 | } 43 | 44 | func GetArticles(ctx *gin.Context){ 45 | 46 | cachedData, err := global.RedisDB.Get(cacheKey).Result() 47 | 48 | if err == redis.Nil{ 49 | var articles []models.Article 50 | 51 | if err:= global.Db.Find(&articles).Error; err !=nil{ 52 | if errors.Is(err, gorm.ErrRecordNotFound){ 53 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 54 | }else{ 55 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 56 | } 57 | return 58 | } 59 | 60 | articleJSON, err:= json.Marshal(articles) 61 | if err !=nil{ 62 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 63 | return 64 | } 65 | 66 | if err := global.RedisDB.Set(cacheKey, articleJSON, 10*time.Minute).Err(); err!=nil{ 67 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 68 | return 69 | } 70 | 71 | ctx.JSON(http.StatusOK, articles) 72 | 73 | }else if err !=nil{ 74 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 75 | return 76 | }else{ 77 | var articles []models.Article 78 | 79 | if err:= json.Unmarshal([]byte(cachedData), &articles); err!=nil{ 80 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 81 | return 82 | } 83 | ctx.JSON(http.StatusOK, articles) 84 | } 85 | } 86 | 87 | func GetArticleByID(ctx *gin.Context){ 88 | id := ctx.Param("id") 89 | 90 | var article models.Article 91 | 92 | if err := global.Db.Where("id = ?", id).First(&article).Error; err!=nil{ 93 | if errors.Is(err, gorm.ErrRecordNotFound){ 94 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 95 | }else{ 96 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 97 | } 98 | return 99 | } 100 | 101 | ctx.JSON(http.StatusOK, article) 102 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/controllers/auth_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "exchangeapp/global" 5 | "exchangeapp/models" 6 | "exchangeapp/utils" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func Register(ctx *gin.Context){ 13 | var user models.User 14 | 15 | if err := ctx.ShouldBindJSON(&user); err !=nil{ 16 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 17 | return 18 | } 19 | 20 | hashedPwd, err:= utils.HashPassword(user.Password) 21 | 22 | if err !=nil{ 23 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 24 | return 25 | } 26 | 27 | user.Password = hashedPwd 28 | 29 | token, err := utils.GenerateJWT(user.Username) 30 | 31 | if err !=nil{ 32 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 33 | return 34 | } 35 | 36 | if err:= global.Db.AutoMigrate(&user); err!=nil{ 37 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 38 | return 39 | } 40 | 41 | if err:= global.Db.Create(&user).Error; err!=nil{ 42 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | 46 | ctx.JSON(http.StatusOK, gin.H{"token": token}) 47 | } 48 | 49 | func Login(ctx *gin.Context){ 50 | var input struct{ 51 | Username string `json:"username"` 52 | Password string `json:"password"` 53 | } 54 | 55 | if err := ctx.ShouldBindJSON(&input); err!=nil{ 56 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 57 | return 58 | } 59 | 60 | var user models.User 61 | 62 | if err:= global.Db.Where("username = ?", input.Username).First(&user).Error; err !=nil{ 63 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "wrong credentials"}) 64 | return 65 | } 66 | 67 | if !utils.CheckPassword(input.Password, user.Password){ 68 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "wrong credentials"}) 69 | return 70 | } 71 | 72 | token, err := utils.GenerateJWT(user.Username) 73 | 74 | if err !=nil{ 75 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | ctx.JSON(http.StatusOK, gin.H{"token": token}) 80 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/controllers/exchange_rate_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "exchangeapp/global" 6 | "exchangeapp/models" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func CreateExchangeRate(ctx *gin.Context){ 15 | var exchangeRate models.ExchangeRate 16 | 17 | if err:= ctx.ShouldBindJSON(&exchangeRate); err!=nil{ 18 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | exchangeRate.Date = time.Now() 23 | 24 | if err := global.Db.AutoMigrate(&exchangeRate); err !=nil{ 25 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | if err := global.Db.Create(&exchangeRate).Error; err!=nil{ 30 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 31 | return 32 | } 33 | 34 | ctx.JSON(http.StatusCreated, exchangeRate) 35 | } 36 | 37 | func GetExchangeRates(ctx *gin.Context){ 38 | var exchangeRates []models.ExchangeRate 39 | 40 | if err:= global.Db.Find(&exchangeRates).Error; err!=nil{ 41 | if errors.Is(err, gorm.ErrRecordNotFound){ 42 | ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) 43 | }else{ 44 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 45 | } 46 | return 47 | } 48 | ctx.JSON(http.StatusOK, exchangeRates) 49 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/controllers/like_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "exchangeapp/global" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/go-redis/redis" 9 | ) 10 | 11 | 12 | func LikeArticle(ctx *gin.Context){ 13 | articleID := ctx.Param("id") 14 | 15 | likeKey := "article:" + articleID + ":likes" 16 | 17 | if err := global.RedisDB.Incr(likeKey).Err(); err !=nil{ 18 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | ctx.JSON(http.StatusOK, gin.H{"message": "Successfully liked the article"}) 23 | } 24 | 25 | func GetArticleLikes(ctx *gin.Context){ 26 | articleID := ctx.Param("id") 27 | 28 | likeKey := "article:" + articleID + ":likes" 29 | 30 | likes, err := global.RedisDB.Get(likeKey).Result() 31 | 32 | if err == redis.Nil{ 33 | likes = "0" 34 | }else if err !=nil{ 35 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 36 | return 37 | } 38 | 39 | ctx.JSON(http.StatusOK, gin.H{"likes": likes}) 40 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/go-redis/redis" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | var( 9 | Db *gorm.DB 10 | RedisDB *redis.Client 11 | ) -------------------------------------------------------------------------------- /Exchangeapp_backend/go.mod: -------------------------------------------------------------------------------- 1 | module exchangeapp 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/golang-jwt/jwt v3.2.2+incompatible 8 | github.com/spf13/viper v1.19.0 9 | golang.org/x/crypto v0.27.0 10 | gorm.io/driver/mysql v1.5.7 11 | gorm.io/gorm v1.25.11 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.11.6 // indirect 16 | github.com/bytedance/sonic/loader v0.1.1 // indirect 17 | github.com/cloudwego/base64x v0.1.4 // indirect 18 | github.com/cloudwego/iasm v0.2.0 // indirect 19 | github.com/fsnotify/fsnotify v1.7.0 // indirect 20 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 21 | github.com/gin-contrib/cors v1.7.2 // indirect 22 | github.com/gin-contrib/sse v0.1.0 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/go-playground/validator/v10 v10.20.0 // indirect 26 | github.com/go-redis/redis v6.15.9+incompatible // indirect 27 | github.com/go-sql-driver/mysql v1.7.0 // indirect 28 | github.com/goccy/go-json v0.10.2 // indirect 29 | github.com/hashicorp/hcl v1.0.0 // indirect 30 | github.com/jinzhu/inflection v1.0.0 // indirect 31 | github.com/jinzhu/now v1.1.5 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 34 | github.com/leodido/go-urn v1.4.0 // indirect 35 | github.com/magiconair/properties v1.8.7 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mitchellh/mapstructure v1.5.0 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 41 | github.com/sagikazarmark/locafero v0.4.0 // indirect 42 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 43 | github.com/sourcegraph/conc v0.3.0 // indirect 44 | github.com/spf13/afero v1.11.0 // indirect 45 | github.com/spf13/cast v1.6.0 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | github.com/subosito/gotenv v1.6.0 // indirect 48 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 49 | github.com/ugorji/go/codec v1.2.12 // indirect 50 | go.uber.org/atomic v1.9.0 // indirect 51 | go.uber.org/multierr v1.9.0 // indirect 52 | golang.org/x/arch v0.8.0 // indirect 53 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 54 | golang.org/x/net v0.25.0 // indirect 55 | golang.org/x/sys v0.25.0 // indirect 56 | golang.org/x/text v0.18.0 // indirect 57 | google.golang.org/protobuf v1.34.1 // indirect 58 | gopkg.in/ini.v1 v1.67.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /Exchangeapp_backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 2 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 16 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 17 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 18 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 19 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= 20 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 21 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 22 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 23 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 24 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 25 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 26 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 27 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 28 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 29 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 30 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 31 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 32 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 33 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 34 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 35 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 36 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 37 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 38 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 39 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 40 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 41 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 42 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 43 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 45 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 46 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 47 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 48 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 49 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 50 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 51 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 52 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 53 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 54 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 55 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 56 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 57 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 58 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 59 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 60 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 61 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 62 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 63 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 64 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 65 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 66 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 67 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 68 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 71 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 72 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 73 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 74 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 77 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 79 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 80 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 81 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 82 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 83 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 84 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 85 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 86 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 87 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 88 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 89 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 90 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 91 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 93 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 94 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 95 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 96 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 97 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 98 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 99 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 100 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 101 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 102 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 103 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 104 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 105 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 106 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 107 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 108 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 109 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 110 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 111 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 112 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 113 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 114 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 115 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 116 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 117 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 118 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 119 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 120 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 121 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 122 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 123 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 126 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 127 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 128 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 129 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 130 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 133 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 135 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 136 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 138 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 139 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 140 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 141 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 142 | gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= 143 | gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 144 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 145 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 146 | -------------------------------------------------------------------------------- /Exchangeapp_backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "exchangeapp/config" 12 | "exchangeapp/router" 13 | ) 14 | 15 | 16 | func main() { 17 | config.InitConfig() 18 | 19 | r := router.SetupRouter() 20 | 21 | 22 | 23 | port := config.AppConfig.App.Port 24 | 25 | if port ==""{ 26 | port = ":8080" 27 | } 28 | 29 | srv := &http.Server{ 30 | Addr: port, 31 | Handler: r, 32 | } 33 | 34 | go func() { 35 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 36 | log.Fatalf("listen: %s\n", err) 37 | } 38 | }() 39 | 40 | quit := make(chan os.Signal, 1) 41 | signal.Notify(quit, os.Interrupt) 42 | <-quit 43 | log.Println("Shutdown Server ...") 44 | 45 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 46 | defer cancel() 47 | if err := srv.Shutdown(ctx); err != nil { 48 | log.Fatal("Server Shutdown:", err) 49 | } 50 | log.Println("Server exiting") 51 | 52 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/middlewares/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "exchangeapp/utils" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func AuthMiddleWare() gin.HandlerFunc{ 11 | return func(ctx *gin.Context){ 12 | token := ctx.GetHeader("Authorization") 13 | if token == ""{ 14 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization Header"}) 15 | ctx.Abort() 16 | return 17 | } 18 | username, err := utils.ParseJWT(token) 19 | 20 | if err !=nil{ 21 | ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 22 | ctx.Abort() 23 | return 24 | } 25 | 26 | ctx.Set("username", username) 27 | ctx.Next() 28 | } 29 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Article struct { 6 | gorm.Model 7 | Title string `binding:"required"` 8 | Content string `binding:"required"` 9 | Preview string `binding:"required"` 10 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/models/exchange_rate.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type ExchangeRate struct { 6 | ID uint `gorm:"primarykey" json:"_id"` 7 | FromCurrency string `json:"fromCurrency" binding:"required"` 8 | ToCurrency string `json:"toCurrency" binding:"required"` 9 | Rate float64 `json:"rate" binding:"required"` 10 | Date time.Time `json:"date"` 11 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type User struct{ 6 | gorm.Model 7 | Username string `gorm:"unique"` 8 | Password string 9 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "exchangeapp/controllers" 5 | "exchangeapp/middlewares" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/gin-contrib/cors" 10 | ) 11 | 12 | func SetupRouter() *gin.Engine{ 13 | r := gin.Default() 14 | 15 | r.Use(cors.New(cors.Config{ 16 | AllowOrigins: []string{"http://localhost:5173"}, 17 | AllowMethods: []string{"GET", "POST", "OPTIONS"}, 18 | AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, 19 | ExposeHeaders: []string{"Content-Length"}, 20 | AllowCredentials: true, 21 | MaxAge: 12 * time.Hour, 22 | })) 23 | 24 | auth := r.Group("/api/auth") 25 | { 26 | auth.POST("/login", controllers.Login) 27 | 28 | auth.POST("/register", controllers.Register) 29 | } 30 | 31 | api := r.Group("/api") 32 | api.GET("/exchangeRates", controllers.GetExchangeRates) 33 | api.Use(middlewares.AuthMiddleWare()) 34 | { 35 | api.POST("/exchangeRates", controllers.CreateExchangeRate) 36 | api.POST("/articles", controllers.CreateArticle) 37 | api.GET("/articles", controllers.GetArticles) 38 | api.GET("/articles/:id", controllers.GetArticleByID) 39 | 40 | api.POST("/articles/:id/like", controllers.LikeArticle) 41 | api.GET("/articles/:id/like", controllers.GetArticleLikes) 42 | } 43 | return r 44 | } -------------------------------------------------------------------------------- /Exchangeapp_backend/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt" 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | func HashPassword(pwd string) (string, error) { 12 | hash, err := bcrypt.GenerateFromPassword([]byte(pwd), 12) 13 | return string(hash), err 14 | } 15 | 16 | func GenerateJWT(username string)(string, error){ 17 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 18 | "username": username, 19 | "exp": time.Now().Add(time.Hour * 72).Unix(), 20 | }) 21 | 22 | signedToken, err := token.SignedString([]byte("secret")) 23 | return "Bearer " + signedToken, err 24 | } 25 | 26 | func CheckPassword(password string, hash string) bool{ 27 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 28 | return err == nil 29 | } 30 | 31 | func ParseJWT(tokenString string) (string, error){ 32 | if len(tokenString) > 7 && tokenString[:7] == "Bearer "{ 33 | tokenString = tokenString[7:] 34 | } 35 | 36 | token, err := jwt.Parse(tokenString, func(token *jwt.Token)(interface{}, error){ 37 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok{ 38 | return nil, errors.New("unexpected Signing Method") 39 | } 40 | return []byte("secret"), nil 41 | }) 42 | 43 | if err !=nil{ 44 | return "", err 45 | } 46 | 47 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid{ 48 | username, ok := claims["username"].(string) 49 | if !ok{ 50 | return "", errors.New("username claim is not a string") 51 | } 52 | return username, nil 53 | } 54 | 55 | return "", err 56 | } 57 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/__VLS_types.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | type __VLS_IntrinsicElements = __VLS_PickNotAny>>; 4 | type __VLS_Element = __VLS_PickNotAny; 5 | 6 | type __VLS_IsAny = 0 extends 1 & T ? true : false; 7 | type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; 8 | 9 | type __VLS_Prettify = { [K in keyof T]: T[K]; } & {}; 10 | 11 | type __VLS_OmitKeepDiscriminatedUnion = 12 | T extends any 13 | ? Pick> 14 | : never; 15 | 16 | type __VLS_GlobalComponents = 17 | __VLS_PickNotAny 18 | & __VLS_PickNotAny 19 | & __VLS_PickNotAny 20 | & Pick; 27 | 28 | declare const __VLS_intrinsicElements: __VLS_IntrinsicElements; 29 | 30 | // v-for 31 | declare function __VLS_getVForSourceType(source: number): [number, number, number][]; 32 | declare function __VLS_getVForSourceType(source: string): [string, number, number][]; 33 | declare function __VLS_getVForSourceType(source: T): [ 34 | T[number], // item 35 | number, // key 36 | number, // index 37 | ][]; 38 | declare function __VLS_getVForSourceType }>(source: T): [ 39 | T extends { [Symbol.iterator](): Iterator } ? T1 : never, // item 40 | number, // key 41 | undefined, // index 42 | ][]; 43 | declare function __VLS_getVForSourceType(source: T): [ 44 | T[keyof T], // item 45 | keyof T, // key 46 | number, // index 47 | ][]; 48 | 49 | declare function __VLS_getSlotParams(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>; 50 | declare function __VLS_getSlotParam(slot: T): Parameters<__VLS_PickNotAny, (...args: any[]) => any>>[0]; 51 | declare function __VLS_directiveFunction(dir: T): 52 | T extends import('vue').ObjectDirective | import('vue').FunctionDirective ? (value: V) => void 53 | : T; 54 | declare function __VLS_withScope(ctx: T, scope: K): ctx is T & K; 55 | declare function __VLS_makeOptional(t: T): { [K in keyof T]?: T[K] }; 56 | 57 | type __VLS_SelfComponent = string extends N ? {} : N extends string ? { [P in N]: C } : {}; 58 | type __VLS_WithComponent = 59 | N1 extends keyof LocalComponents ? N1 extends N0 ? Pick : { [K in N0]: LocalComponents[N1] } : 60 | N2 extends keyof LocalComponents ? N2 extends N0 ? Pick : { [K in N0]: LocalComponents[N2] } : 61 | N3 extends keyof LocalComponents ? N3 extends N0 ? Pick : { [K in N0]: LocalComponents[N3] } : 62 | N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0> : { [K in N0]: __VLS_GlobalComponents[N1] } : 63 | N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0> : { [K in N0]: __VLS_GlobalComponents[N2] } : 64 | N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0> : { [K in N0]: __VLS_GlobalComponents[N3] } : 65 | { [K in N0]: unknown } 66 | 67 | type __VLS_FillingEventArg_ParametersLength any> = __VLS_IsAny> extends true ? -1 : Parameters['length']; 68 | type __VLS_FillingEventArg = E extends (...args: any) => any ? __VLS_FillingEventArg_ParametersLength extends 0 ? ($event?: undefined) => ReturnType : E : E; 69 | declare function __VLS_asFunctionalComponent any ? InstanceType : unknown>(t: T, instance?: K): 70 | T extends new (...args: any) => any 71 | ? (props: (K extends { $props: infer Props } ? Props : any) & Record, ctx?: { 72 | attrs?: any, 73 | slots?: K extends { $slots: infer Slots } ? Slots : any, 74 | emit?: K extends { $emit: infer Emit } ? Emit : any 75 | }) => __VLS_Element & { __ctx?: typeof ctx & { props?: typeof props; expose?(exposed: K): void; } } 76 | : T extends () => any ? (props: {}, ctx?: any) => ReturnType 77 | : T extends (...args: any) => any ? T 78 | : (_: {} & Record, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {} & Record } }; 79 | declare function __VLS_elementAsFunctionalComponent(t: T): (_: T & Record, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: T & Record } }; 80 | declare function __VLS_functionalComponentArgsRest any>(t: T): Parameters['length'] extends 2 ? [any] : []; 81 | declare function __VLS_pickEvent(emitEvent: E1, propEvent: E2): __VLS_FillingEventArg< 82 | __VLS_PickNotAny< 83 | __VLS_AsFunctionOrAny, 84 | __VLS_AsFunctionOrAny 85 | > 86 | > | undefined; 87 | declare function __VLS_pickFunctionalComponentCtx(comp: T, compInstance: K): __VLS_PickNotAny< 88 | '__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: infer Ctx } ? Ctx : never : any 89 | , T extends (props: any, ctx: infer Ctx) => any ? Ctx : any 90 | >; 91 | type __VLS_FunctionalComponentProps = 92 | '__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: { props?: infer P } } ? NonNullable

: never 93 | : T extends (props: infer P, ...args: any) => any ? P : 94 | {}; 95 | type __VLS_AsFunctionOrAny = unknown extends F ? any : ((...args: any) => any) extends F ? F : any; 96 | 97 | declare function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; 98 | 99 | /** 100 | * emit 101 | */ 102 | // fix https://github.com/vuejs/language-tools/issues/926 103 | type __VLS_UnionToIntersection = (U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never; 104 | type __VLS_OverloadUnionInner = U & T extends (...args: infer A) => infer R 105 | ? U extends T 106 | ? never 107 | : __VLS_OverloadUnionInner & U & ((...args: A) => R)> | ((...args: A) => R) 108 | : never; 109 | type __VLS_OverloadUnion = Exclude< 110 | __VLS_OverloadUnionInner<(() => never) & T>, 111 | T extends () => never ? never : () => never 112 | >; 113 | type __VLS_ConstructorOverloads = __VLS_OverloadUnion extends infer F 114 | ? F extends (event: infer E, ...args: infer A) => any 115 | ? { [K in E & string]: (...args: A) => void; } 116 | : never 117 | : never; 118 | type __VLS_NormalizeEmits = __VLS_Prettify< 119 | __VLS_UnionToIntersection< 120 | __VLS_ConstructorOverloads & { 121 | [K in keyof T]: T[K] extends any[] ? { (...args: T[K]): void } : never 122 | } 123 | > 124 | >; -------------------------------------------------------------------------------- /Exchangeapp_frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ExchangeApp 8 | 9 | 10 |

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exchangeappdemo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.7.3", 13 | "element-plus": "^2.7.4", 14 | "pinia": "^2.1.7", 15 | "router": "^1.3.8", 16 | "vant": "^4.9.0", 17 | "vue": "^3.4.21", 18 | "vue-router": "^4.3.2" 19 | }, 20 | "devDependencies": { 21 | "@types/axios": "^0.14.0", 22 | "@types/node": "^20.12.13", 23 | "@types/vue-router": "^2.0.0", 24 | "@typescript-eslint/eslint-plugin": "^7.11.0", 25 | "@typescript-eslint/parser": "^7.11.0", 26 | "@vitejs/plugin-vue": "^5.0.4", 27 | "@vue/cli-plugin-typescript": "^5.0.8", 28 | "@vue/eslint-config-typescript": "^13.0.0", 29 | "typescript": "^5.2.2", 30 | "vite": "^5.4.0", 31 | "vue-tsc": "^2.0.6" 32 | }, 33 | "main": "index.js", 34 | "author": "", 35 | "license": "ISC", 36 | "description": "" 37 | } 38 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create({ 4 | baseURL: 'http://localhost:3000/api', 5 | }); 6 | 7 | instance.interceptors.request.use(config => { 8 | const token = localStorage.getItem('token'); 9 | if (token) { 10 | config.headers.Authorization = token; 11 | console.log(token) 12 | } 13 | return config; 14 | }); 15 | 16 | export default instance; 17 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | 41 | 61 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/components/Register.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | 39 | 59 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import ElementPlus from 'element-plus'; 4 | import 'element-plus/dist/index.css'; 5 | import App from './App.vue'; 6 | import router from './router'; 7 | 8 | const app = createApp(App); 9 | app.use(createPinia()); 10 | app.use(ElementPlus); 11 | app.use(router); 12 | app.mount('#app'); 13 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; 2 | import HomeView from '../views/HomeView.vue'; 3 | import CurrencyExchangeView from '../views/CurrencyExchangeView.vue'; 4 | import NewsView from '../views/NewsView.vue'; 5 | import NewsDetailView from '../views/NewsDetailView.vue'; 6 | import Login from '../components/Login.vue'; 7 | import Register from '../components/Register.vue'; 8 | 9 | const routes: RouteRecordRaw[] = [ 10 | { path: '/', name: 'Home', component: HomeView }, 11 | { path: '/exchange', name: 'CurrencyExchange', component: CurrencyExchangeView }, 12 | { path: '/news', name: 'News', component: NewsView }, 13 | { path: '/news/:id', name: 'NewsDetail', component: NewsDetailView }, 14 | { path: '/login', name: 'Login', component: Login }, 15 | { path: '/register', name: 'Register', component: Register }, 16 | ]; 17 | 18 | const router = createRouter({ 19 | history: createWebHistory(), 20 | routes, 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // client/src/shims-vue.d.ts 2 | declare module '*.vue' { 3 | import { DefineComponent } from 'vue'; 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/store/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref, computed } from 'vue'; 3 | import axios from '../axios'; 4 | 5 | export const useAuthStore = defineStore('auth', () => { 6 | const token = ref(localStorage.getItem('token')); 7 | 8 | const isAuthenticated = computed(() => !!token.value); 9 | 10 | const login = async (username: string, password: string) => { 11 | try { 12 | const response = await axios.post('/auth/login', { username, password }); 13 | token.value = response.data.token; 14 | localStorage.setItem('token', token.value || ''); 15 | } catch (error) { 16 | throw new Error(`Login failed! ${error}`); 17 | } 18 | }; 19 | 20 | const register = async (username: string, password: string) => { 21 | try { 22 | const response = await axios.post('/auth/register', { username, password }); 23 | token.value = response.data.token; 24 | localStorage.setItem('token', token.value || ''); 25 | } catch (error) { 26 | throw new Error(`Register failed! ${error}`); 27 | } 28 | }; 29 | 30 | const logout = () => { 31 | token.value = null; 32 | localStorage.removeItem('token'); 33 | }; 34 | 35 | return { 36 | token, 37 | isAuthenticated, 38 | login, 39 | register, 40 | logout 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/types/Article.d.ts: -------------------------------------------------------------------------------- 1 | export interface Article { 2 | ID: string; 3 | Title: string; 4 | Preview: string; 5 | Content: string; 6 | } 7 | 8 | export interface Like{ 9 | likes: number 10 | } -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/views/CurrencyExchangeView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 71 | 72 | 92 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/views/NewsDetailView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 60 | 61 | 72 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/views/NewsView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 47 | 48 | 59 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["src/*"] 15 | }, 16 | "types": ["node", "vite/client"] 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 19 | } 20 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /Exchangeapp_frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | // https://vitejs.dev/config/ 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: 'http://localhost:3000', 11 | changeOrigin: true, 12 | secure: false, 13 | rewrite: (path) => path.replace(/^\/api/, '') 14 | } 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /Gin感情参考書(课件).md: -------------------------------------------------------------------------------- 1 | # Web 003 Gin-01 INKKAPLUM SH 2 | 3 | ## 说明 4 | 5 | 本教程中全部文字版教程和代码为 B 站: [InkkaPlum 频道](https://space.bilibili.com/290859233) 和知乎: [Inkka Plum](https://www.zhihu.com/people/instead-opt)的相关教程所用, 仅供学习。 6 | 7 | 不得二次用于任何机构/个人再次录制 Go / Gin / Gorm / Redis / MySQL / Vue 或其它任何语言, 框架, 架构, 工具等等教程中。 8 | 9 | 此外, 下文有任何问题或者需要改进的点, 请联系 UP 主。 10 | 11 | ## 前文 12 | 13 | 而桌面背景和 Code 背景有变化, Code 背景是 Chinozo 的ベビーデーズ, 而桌面背景是 Harumakigohan 曲绘风格的 PJSK Jump More Jump 内容。 14 | 15 | 讲解方法和上一次的 Fyne 内容没有什么变化。 16 | 17 | ## Up 的话(前言) 18 | 19 | 这是一个比较综合的 Go+Gin+Gorm+Redis+MySQL 教程, Up 顾及到了很多基本概念, 因此没有学习过 Go 语言、没有任何 Web 后端开发经验的朋友亦可以学习此内容, 但是之后依然需要花一定的时间学习 Go 基础, 因此建议观看完成之后通过[本频道(哔哩哔哩(Bilibili): InkkaPlum 频道)](https://space.bilibili.com/290859233)的 Go 基础视频([两期 Fyne 内容](https://www.bilibili.com/video/BV1u142187Ps)) 20 | 21 | 具体链接: 22 | 23 | 24 | 25 | 26 | 27 | 强化 Go 语言知识点, 当然不妨将这个视频作为你的你一个 Go 项目。 28 | 29 | 录制视频不仅是传递知识和解法, 也是对于 Up 自身的一个挑战和知识的再加强, 所以如果有任何不懂的地方, 尤其是视频没有讲清楚的地方, 欢迎私信问 Up 主, Up 主会尽力给出一个可用的解法。 30 | 31 | 当然, 请关注 Up 的 B 站、知乎、GitHub, 同时多多三连, 当然也可以充电支持 Up 主。大家的支持将给 Up 更多的动力, Up 也会努力给大家带来更好的视频。 32 | 33 | 同时, 所有课件和代码都在 GitHub 上开源地分享, 如果感到有帮助, 请给一个 Star, 并且别忘了关注 Up 的 Github。 34 | 35 | ## 再次注意 36 | 37 | 本教程中全部文字版教程和代码为 B 站: [InkkaPlum 频道](https://space.bilibili.com/290859233) 和知乎: [Inkka Plum](https://www.zhihu.com/people/instead-opt)的相关教程所用, 仅供学习。 38 | 39 | 不得二次用于任何机构/个人再次录制 Go / Gin / Gorm / Redis / MySQL / Vue 或其它任何语言, 框架, 架构, 工具等等教程中。 40 | 41 | 此外, 下文有任何问题或者需要改进的点, 请联系 UP 主。 42 | 43 | ## 必要的基础配置 44 | 45 | 需要 MySQL, 46 | 47 | 下载链接如下: 48 | 49 | 50 | 这个配置极其简单, 选第一项即可: 51 | 52 | ![配置步骤01](image.png) 53 | 54 | 不要忘记装 MySQL Workbench, 其提供可视化控制台, 可轻松管理 MySQL 环境(类似于我们之前的 MongoDB Compass)。 55 | 56 | ## 基础概念 57 | 58 | ### 什么是前后端分离 59 | 60 | #### 前后端分离的定义 61 | 62 | 它将传统的 Web 应用从一个整体拆分成两个独立的部分: 前端(Front-end)和后端(Back-end)。 63 | 64 | - 前端:主要负责用户界面(UI)的呈现和交互逻辑 65 | 66 | - 后端:主要负责业务逻辑、数据处理和与数据库的交互 67 | 68 | #### 优点 69 | 70 | - 提高开发效率 71 | - 增强系统可维护性 72 | - 提升用户体验 73 | - 技术栈灵活 74 | 75 | #### 案例判断 76 | 77 | ##### 案例 1:Gin+Gorm+Mysql+Vue 博客项目 78 | 79 | 目录结构: 如: MyBlog-Backend, MyBlog-Frontend。 80 | 81 | ##### 案例 2:Wordpress简单开发 82 | 83 | Wordpress 开发 84 | 85 | 86 | 87 | ![OfficialWebsite](image-13.png) 88 | 89 | ### 前后端分离中 API 的作用 90 | 91 | API 是什么? 92 | 93 | API 即为 Application Programming Interface(应用程序接口)。 94 | 95 | ### RESTful API 简述 96 | 97 | REST 是什么? 98 | 99 | 其含义为: Representational State Transfer (译: 表述性状态转移), 其是一种软件架构风格, 而非标准。故 RESTful API 即为一种 REST 风格的接口, 或者说是满足 REST 风格的接口。 100 | 101 | 在很多年前的案例: 102 | 103 | ```bash 104 | http://www.example.com/get_rates?name=usdeur 105 | http://www.example.com/update_rates?... 106 | http://www.example.com/delete_rates?... 107 | #InkkaPlum频道 108 | ``` 109 | 110 | 这样设计的 API 并不好, 非常混乱, 但是满足 REST 风格的接口是这样的: 111 | 112 | ```bash 113 | GET http://www.example.com/rates 114 | POST http://www.example.com/rates 115 | Data: name=usdeur 116 | ... 117 | #InkkaPlum频道 118 | ``` 119 | 120 | 只需改变请求方法(Method)就可以完成相关的操作, 易于理解、易于调用。 121 | 122 | #### GET、POST、PUT 和 DELETE 123 | 124 | 1. GET: 取出资源(一项或多项) 125 | 2. POST: 新建一个资源 126 | 3. PUT: 更新资源(客户端提供完整资源数据) 127 | 4. PATCH: 更新资源(客户端提供需要修改的资源数据) 128 | 5. DELETE: 删除资源。 129 | 130 | #### RESTful API 核心要点 131 | 132 | 1. 资源 (Resources): 每一个 URL 代表一个资源(将互联网的资源抽象成资源, 将获取资源的方式定义为方法), 如`api/articles/3/`, (以资源为基础, 资源可为一个图片、更多以 JSON 为载体, 如`{"fromCurrency": "USD","toCurrency": "KZT","rate": 479}`)。 133 | 134 | 2. 使用 HTTP 方法(GET、POST、PUT、DELETE 等)表示对资源的操作。 135 | 136 | 3. 使用 HTTP 状态码(Http Status Code)表示请求的结果(响应状态码), 137 | 138 | 如 200(成功)、404(未找到)、500(服务器错误), 以下是更具体情况: 139 | 140 | ![HTTP Status Code](image-1.png) (图源 Yandex) 141 | 142 | 我们案例中还会用到如 `401(Unauthorized)`: 状态码`401 Unauthorized(未经授权)`, `201(Created)` :请求已被成功处理且创建了新的资源。 143 | 144 | #### RESTful API 的建议规范 145 | 146 | 需要注意的是 REST 并没有一个明确的标准, 但一般而言可以有这样的特点, 或者可以称为建议规范: 147 | 148 | URL 的重要概念, 其实读一下 MDN Web Docs 即可 149 | 150 | 151 | 152 | `/path/to/myfile.html` 是 Web 服务器上资源的路径。在 Web 的早期阶段, 像这样的路径表示 Web 服务器上的物理文件位置。如今, 它主要是由没有任何物理现实的 Web 服务器处理的抽象。 153 | 154 | ![URL](image-4.png) (图源 MDN Web Docs) 155 | 156 | 资源路径也就是我们的 Path 这一概念 157 | 158 | 需要注意的是: URL 是 URI 的一个子集, 这是我们上上一期 [Fyne 视频](https://www.bilibili.com/video/BV1u142187Ps)就提及到的。 159 | 160 | `/{version}/{resources}/{resource_id}` 161 | 162 | 例子: `api/v1/articles/3` 163 | 164 | 1. 使用名词表示资源 165 | 166 | 2. 使用层次结构 167 | 168 | 3. 不要使用文件扩展名, 正确使用`/`表示层级关系 169 | 170 | #### 优势 171 | 172 | 通过遵循 RESTful API 的建议规范,可以提高 API 的可读性、可维护性和可扩展性。 173 | 174 | #### 其它概念 175 | 176 | - 请求地址, 基础地址, (API)接口地址 177 | 178 | `请求地址` 在大部分情况下即为 `基础地址` + `api/接口地址` 179 | 180 | 例子: 基础地址: `http://www.example.com` 181 | 182 | 接口地址: `/api/v1/user/inkkaplumchannel` 183 | 184 | 本视频将使用这样的用词。 185 | 186 | - 数据格式 187 | 188 | JSON, 也是目前主流的 189 | 190 | 例子: 191 | 192 | ```json 193 | { 194 | "_id": 1, 195 | "fromCurrency": "USD", 196 | "toCurrency": "KZT", 197 | "rate": 479, 198 | "date": "2024-08-30T22:45:42.8774003+08:00" 199 | } 200 | //InkkaPlum频道 201 | ``` 202 | 203 | ### 什么是 MVC? 204 | 205 | MVC 全称为: Model-View-Controller 206 | 207 | MVC 是一种常用的软件架构模式, 旨在将应用程序的关注点分离, 提高代码的可维护性。 208 | 209 | 假设我们用 Go+gin+gorm+vue 开发了一个简易的博客网站 210 | 211 | ```bash 212 | 哔哩哔哩InkkaPlum频道的个人博客/ 213 | ├── backend/ 214 | │ ├── main.go 215 | │ ├── controllers/ 216 | │ │ ├── auth.go 217 | │ │ ├── article.go 218 | │ │ └── ... 219 | │ ├── models/ 220 | │ │ ├── user.go 221 | │ │ ├── article.go 222 | │ │ └── ... 223 | │ ├── routers/ 224 | │ │ ├── router.go 225 | │ └── config/ 226 | │ └── config.yaml 227 | └── frontend/ 228 | ├── src/ 229 | │ ├── App.vue 230 | │ ├── components/ 231 | │ ├── views/ 232 | │ └── router/ 233 | ├── index.html 234 | └── ... 235 | ``` 236 | 237 | 我们将讨论适用于前后端分离逻辑的MVC内容。 238 | 239 | - Model(模型) 240 | 241 | 例子: 242 | 243 | `models/article.go` 244 | 245 | ```go 246 | type Article struct { 247 | Content string 248 | CreatedAt time.Time 249 | UpdatedAt time.Time 250 | ... 251 | } 252 | ``` 253 | 254 | - View(视图) 255 | 256 | 例子 01: 257 | 258 | `components/ArticleForm.vue` 259 | 260 | ```vue 261 | 268 | 269 | 272 | ``` 273 | 274 | 例子 02: 275 | 276 | ```vue 277 |

{{ article.Title }}

278 |

{{ article.Preview }}

279 | ``` 280 | 281 | - Controller(控制器) 282 | 283 | 例子 01: 284 | 285 | `controllers/article.go` 286 | 287 | ```go 288 | func CreateArticleHandler(c *gin.Context) { 289 | var article models.Article 290 | if err := c.ShouldBindJSON(&article); err != nil { 291 | // ... 处理错误 292 | } 293 | //... 294 | //InkkaPlum频道 295 | 296 | c.JSON(http.StatusCreated, article) 297 | } 298 | ``` 299 | 300 | 我们的例子中, MVC 的工作流程 301 | 302 | 1. 用户对界面进行操作, 如点击点赞按钮 303 | 2. View 感知这些事件, 通知 Controller 进行处理(`POST`) 304 | 3. Controller 处理相关业务, 对 Model 的业务数据进行更新 305 | 4. View 更新用户界面, 赞+1, 306 | 307 | 快速记忆: 用户操作-->View -->Controller-->Model-->View。MVC 通信过程都是单向的。 308 | 309 | ### 前端路由和后端路由 310 | 311 | 代码例子: `api.GET("/articles/:id/like", controllers.GetArticleLikes)` 312 | 313 | - 用户在浏览器中输入一个 URL, 前端路由根据这个 URL 更新页面内容。如: `{ path: '/register', name: 'Register', component: Register },` 如`http://www.example.com/register`或`http://www.example.com/articles/2`等等。 314 | 315 | - 在这个页面中, 代码如有涉及到如`` await axios.get
(`/articles/${id}`) `` 316 | 317 | ### Gin 和 Gorm 介绍 318 | 319 | Go 语言在 Web 后端开发使用广泛, 而其中 Gin 和 Gorm 是非常有名的。 320 | 321 | #### Gin 框架 322 | 323 | Gin 是一个使用 Go 语言开发的 Web 框架。 324 | 325 | 主要特点: 326 | 327 | - 高性能 328 | - 中间件支持 329 | - 路由分组 330 | 331 | #### Gorm 332 | 333 | Gorm 是 Golang 中最流行的 ORM (对象关系映射) 库(ORM Library) 334 | 335 | 什么是 ORM? 336 | 337 | ORM - Object Relational Mapping。 338 | 339 | 优势: 340 | 341 | - 简单易用 342 | 343 | - 自动迁移 344 | 345 | - 支持多种数据库 346 | 347 | #### Gin 和 Gorm 结合使用 348 | 349 | Gin 和 Gorm 经常一起使用来构建 Go Web 应用程序。 350 | 351 | ## 正式开始写代码 352 | 353 | ### 活用 Viper 读取配置文件 354 | 355 | `.yml`文件介绍: 356 | 357 | YAML 是"YAML Ain't a Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据为中心, 而不是以标记语言为重点, 故改变名字。 358 | 359 | 例子: 360 | 361 | ```yml 362 | receipt: InkkaPlum Channel 363 | date: 2024 364 | ``` 365 | 366 | 我们案例中要使用此文件记录关键信息。 367 | 368 | 新建一个文件夹, 叫`config`, 内部创建一个`config.go` 文件, 并且再创建一个`config.yml`文件, 内部先这样写。 369 | 370 | ```yml 371 | app: 372 | name: CurrencyExchangeApp 373 | port: :3000 374 | 375 | database: 376 | host: localhost 377 | port: :3306 378 | user: your_username 379 | password: your_password 380 | name: currency_exchange_db 381 | ``` 382 | 383 | 敲命令: 384 | 385 | ```bash 386 | go mod init 387 | ``` 388 | 389 | ```bash 390 | go get -u github.com/gin-gonic/gin 391 | go get github.com/spf13/viper 392 | ``` 393 | 394 | 结构体 395 | 396 | ```go 397 | type Config struct { 398 | App struct { 399 | Name string 400 | Port string 401 | } 402 | Database struct { 403 | Host string 404 | Port string 405 | User string 406 | Password string 407 | Name string 408 | } 409 | } 410 | ``` 411 | 412 | Go 基础小提示: 413 | 414 | `%v`占位符: 相应值的默认格式 415 | 416 | #### Viper 官方文档重要内容 417 | 418 | 官方文档: 419 | 420 | ```go 421 | /* 422 | Example config: 423 | 424 | module: 425 | enabled: true 426 | token: 89h3f98hbwf987h3f98wenf89ehf 427 | */ 428 | type config struct { 429 | Module struct { 430 | Enabled bool 431 | 432 | moduleConfig `mapstructure:",squash"` 433 | } 434 | } 435 | 436 | // moduleConfig could be in a module specific package 437 | type moduleConfig struct { 438 | Token string 439 | } 440 | 441 | var C config 442 | 443 | err := viper.Unmarshal(&C) 444 | if err != nil { 445 | t.Fatalf("unable to decode into struct, %v", err) 446 | } 447 | ``` 448 | 449 | ### 上手 Gin 450 | 451 | 452 | 453 | ```go 454 | package main 455 | 456 | import "github.com/gin-gonic/gin" 457 | 458 | func main() { 459 | r := gin.Default() 460 | r.GET("/ping", func(c *gin.Context) { 461 | c.JSON(200, gin.H{ 462 | "message": "pong", 463 | }) 464 | }) 465 | r.Run() // listen and serve on 0.0.0.0:8080 466 | } 467 | ``` 468 | 469 | 可以用 `Air` 工具避免不停的`ctrl+c`然后`go run .`, 但本视频将不采用 `Air` 工具。 470 | 471 | ### Router 路由的基本配置 472 | 473 | 创建文件夹`router`, 新建必要文件`router.go`。 474 | 475 | 官方文档案例: 476 | 477 | 478 | 479 | ```go 480 | func main() { 481 | // 禁用控制台颜色 482 | // gin.DisableConsoleColor() 483 | 484 | // 使用默认中间件(logger 和 recovery 中间件)创建 gin 路由 485 | router := gin.Default() 486 | 487 | router.GET("/someGet", getting) 488 | router.POST("/somePost", posting) 489 | router.PUT("/somePut", putting) 490 | router.DELETE("/someDelete", deleting) 491 | router.PATCH("/somePatch", patching) 492 | router.HEAD("/someHead", head) 493 | router.OPTIONS("/someOptions", options) 494 | 495 | // 默认在 8080 端口启动服务,除非定义了一个 PORT 的环境变量。 496 | router.Run() 497 | // router.Run(":3000") hardcode 端口号 498 | } 499 | ``` 500 | 501 | 本项目案例如下: 502 | 503 | 登录, 注册: 504 | 505 | `/api/auth/login`以及`/api/auth/register` 506 | 507 | 文章: 508 | 509 | `/api/articles` 510 | 511 | #### 路由组问题 512 | 513 | 那么在 Gin 的官方文档内, 已经为我们提供了解法 514 | 515 | 516 | 517 | ```go 518 | func main() { 519 | router := gin.Default() 520 | 521 | // 简单的路由组: v1 522 | v1 := router.Group("/v1") 523 | { 524 | v1.POST("/login", loginEndpoint) 525 | v1.POST("/submit", submitEndpoint) 526 | v1.POST("/read", readEndpoint) 527 | } 528 | 529 | // 简单的路由组: v2 530 | v2 := router.Group("/v2") 531 | { 532 | v2.POST("/login", loginEndpoint) 533 | v2.POST("/submit", submitEndpoint) 534 | v2.POST("/read", readEndpoint) 535 | } 536 | 537 | router.Run(":8080") 538 | } 539 | ``` 540 | 541 | ### Go 基础概念 - Map 的重要提示 542 | 543 | 定义: Map 是一种无序的键值对的集合。 544 | 545 | 下面是一些基本例子: 546 | 547 | ```go 548 | m := make(map[string]int) 549 | 550 | m := make(map[string]int, 10) 551 | ``` 552 | 553 | ```go 554 | // 使用字面量创建 Map 555 | m := map[string]int{ 556 | //键值对 - key: value 557 | "Harumakigohan": 10, 558 | "Chinozo": 5, 559 | "Daibakuhasin": 3, 560 | } 561 | 562 | //活用例 563 | v1 := m["Harumakigohan"] 564 | v2, ok := m["DECO27"] 565 | ``` 566 | 567 | `gin.H`源代码: 568 | 569 | ```go 570 | type H map[string]any 571 | ``` 572 | 573 | `any`的说明 574 | 575 | ```go 576 | type any = interface{} 577 | ``` 578 | 579 | 利用案例: 580 | 581 | ```go 582 | m := map[string]any{ 583 | //键值对 - key: value 584 | "Harumakigohan": "10", 585 | "Chinozo": "グッバイ宣言", 586 | "Daibakuhasin": 3, 587 | } 588 | ``` 589 | 590 | ```go 591 | package main 592 | 593 | import "fmt" 594 | 595 | func main() { 596 | m := map[string]int{ 597 | //键值对 - key: value 598 | "Harumakigohan": 10, 599 | "Chinozo": 5, 600 | "Daibakuhasin": 3, 601 | } 602 | 603 | //活用例 604 | v1 := m["Harumakigohan"] 605 | v2, ok := m["DECO27"] 606 | 607 | fmt.Println(v1) 608 | fmt.Println(v2, ok) 609 | } 610 | ``` 611 | 612 | #### 如果配置文件中未对端口进行设定的对策 613 | 614 | ```go 615 | if port == ""{ 616 | port = ":8080" 617 | } 618 | ``` 619 | 620 | ### Gorm+ Mysql 部分 621 | 622 | 官方文档(中文) 623 | 624 | 625 | 626 | GORM 为什么成为了非常主流的选择? 627 | 628 | 1. 简洁的 API 设计 629 | 630 | 2. 强大的功能 631 | 632 | 3. 社区活跃 633 | 634 | 基本配置: 635 | 636 | ```bash 637 | go get -u gorm.io/gorm 638 | ``` 639 | 640 | 官方案例 641 | 642 | ```go 643 | import ( 644 | "gorm.io/driver/mysql" 645 | "gorm.io/gorm" 646 | ) 647 | 648 | func main() { 649 | // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情 650 | dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 651 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 652 | } 653 | 654 | //db *gorm.DB 655 | ``` 656 | 657 | 其它配置: 打开数据库连接的空闲个数、最大连接个数以及其它一些内容。 658 | 659 | [官方文档](https://gorm.io/zh_CN/docs/generic_interface.html) 660 | 661 | ```go 662 | // SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。 663 | sqlDB.SetMaxIdleConns(10) 664 | 665 | // SetMaxOpenConns 设置打开数据库连接的最大数量。 666 | sqlDB.SetMaxOpenConns(100) 667 | 668 | // SetConnMaxLifetime 设置了连接可复用的最大时间。 669 | sqlDB.SetConnMaxLifetime(time.Hour) 670 | ``` 671 | 672 | #### global.go 重要说明 673 | 674 | 案例 675 | 676 | ```go 677 | var ( 678 | Logger *logrus.Logger 679 | Db *gorm.DB 680 | ) 681 | ``` 682 | 683 | ### 实现注册功能 684 | 685 | 我们将实现注册功能。 686 | 687 | #### 模型相关问题的解决 688 | 689 | 690 | 691 | GORM 通过将 Go 结构体(Go structs) 映射到数据库表来简化数据库交互。 692 | 693 | GORM 提供了一个预定义的结构体, 名为 gorm.Model, 其中包含常用字段: 694 | 695 | ```go 696 | // gorm.Model 的定义 697 | type Model struct { 698 | ID uint `gorm:"primaryKey"` 699 | CreatedAt time.Time 700 | // 在创建记录时自动设置为当前时间 701 | UpdatedAt time.Time 702 | //每当记录更新时,自动更新为当前时间 703 | DeletedAt gorm.DeletedAt `gorm:"index"` 704 | //用于软删除 705 | } 706 | ``` 707 | 708 | 可直接在结构体中嵌入`gorm.Model`, 以便自动包含这些字段。 709 | 710 | 这对于在不同模型之间保持一致性并利用 GORM 内置的约定非常有用。 711 | 712 | 最终案例: 713 | 714 | ```json 715 | { 716 | "username": "your_username", 717 | "password": "your_password" 718 | } 719 | ``` 720 | 721 | 字段标签官方文档 722 | 723 | 724 | 725 | #### GORM 的约定 726 | 727 | 根据约定, 默认地, GORM 会使用 ID 作为表的主键。 728 | 729 | 与此同时, 数据表的列名使用的是 struct 字段名的蛇形命名(Snake Case), 730 | 731 | 案例: 732 | 733 | `CreatedAt` => `created_at` 734 | 735 | ```go 736 | type User struct { 737 | ID uint // 列名是 `id` 738 | Name string // 列名是 `name` 739 | Birthday time.Time // 列名是 `birthday` 740 | CreatedAt time.Time // 列名是 `created_at` 741 | } 742 | ``` 743 | 744 | 注册最终代码 745 | 746 | ```go 747 | func Register(ctx *gin.Context) { 748 | var user models.User 749 | if err := ctx.ShouldBindJSON(&user); err != nil { 750 | ctx.JSON(http.StatusBadRequest, gin.H{"Error!": err.Error()}) 751 | return 752 | } 753 | 754 | hashedPwd, err := utils.HashPassword(user.Password) 755 | 756 | if err != nil { 757 | ctx.JSON(http.StatusBadRequest, gin.H{"Error!": err.Error()}) 758 | return 759 | } 760 | 761 | user.Password = hashedPwd 762 | 763 | token, err := utils.GenerateJWT(user.Username) 764 | 765 | if err != nil { 766 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 767 | return 768 | } 769 | 770 | if err := global.DB.AutoMigrate(&user); err !=nil{ 771 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 772 | } 773 | 774 | if err := global.DB.Create(&user).Error; err != nil { 775 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 776 | return 777 | } 778 | 779 | ctx.JSON(http.StatusOK, gin.H{"token": token}) 780 | } 781 | ``` 782 | 783 | Gin 案例代码: 784 | 785 | ```go 786 | func getBook(c *gin.Context) { 787 | bookID := c.Param("id") // Extracting parameter from URL 788 | c.JSON(200, gin.H{"book_id": bookID}) // Sending JSON response 789 | } 790 | ``` 791 | 792 | 将请求体绑定到结构体中, 需要活用模型绑定。 793 | 794 | 案例: 795 | 796 | ```go 797 | type Login struct { 798 | User string `form:"user" json:"user" xml:"user" binding:"required"` 799 | Password string `form:"password" json:"password" xml:"password" binding:"required"` 800 | } 801 | 802 | func main() { 803 | router := gin.Default() 804 | 805 | // 绑定 JSON ({"user": "manu", "password": "123"}) 806 | router.POST("/loginJSON", func(c *gin.Context) { 807 | var json Login 808 | if err := c.ShouldBindJSON(&json); err != nil { 809 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 810 | return 811 | } 812 | 813 | if json.User != "manu" || json.Password != "123" { 814 | c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) 815 | return 816 | } 817 | c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})}) 818 | ... 819 | } 820 | ``` 821 | 822 | 823 | 824 | 利用`ShouldBindJSON`方法。 825 | 826 | \*JSON部分和模型部分大小写可不一致: `UseRName`, `username`是可以的, 但字母不可不一致`UzerNaam`则完全不可。 827 | 828 | ```go 829 | if err := c.ShouldBindJSON(&user); err != nil { 830 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 831 | return 832 | } 833 | ``` 834 | 835 | 官方文档说明: 836 | 837 | Type - Should bind 838 | Methods 839 | 840 | Behavior - 这些方法属于 ShouldBindWith 的具体调用。 如果发生绑定错误, Gin 会返回错误并由开发者处理错误和请求。 841 | 842 | #### 请求头, 请求体相关重要内容 843 | 844 | http 请求报文包含三个部分(请求行 + 请求头 + 请求体) 845 | 846 | 请求行包含三个内容: method(如 GET/POST 等) + request-URI(如) + HTTP-version(如`HTTP/1.1`) 847 | 848 | ##### 请求头(Request Header) 849 | 850 | MDN 文档: 851 | 852 | 853 | 854 | 请求头由 key/value 对 也就是键值对(kv 对)组成,每行为一对, key 和 value 间通过冒号(`:`)分割。 855 | 856 | 常见的请求字段 857 | 858 | 如`Authorization`, 用于对应的认证信息。 859 | 860 | Postman 也可以看到这些重要内容: 861 | 862 | ![Postman Example A](image-2.png) 863 | 864 | 请求体(Request Body) 865 | 866 | 我们写得 JSON 内容, 发送给服务器的数据。 867 | 868 | ##### 响应也由行、头、体这个概念 869 | 870 | Postman 也可以看到这些重要内容: 871 | 872 | ![Postman Example B](image-10.png) 873 | 874 | 状态行(HTTP-version+状态码+状态码的文本描述): 例子: `HTTP/1.1 200 OK` 875 | 876 | 响应头: 如: Date 标头:消息产生的时间 877 | 878 | 响应体: 当 Web 服务器接收到 Web 客户端的请求报文后, 对 HTTP 请求报文进行解析, 将 Web 客户端的请求的对象取出打包, 通过 HTTP 响应报文将数据传回给客户端; 若出现错误, 则返回包含对应错误的错误代码和错误原因。 879 | 880 | #### 加密逻辑- Bcrypt Package 881 | 882 | Bcrypt 是一种用于密码哈希的加密算法,它是基于 Blowfish 算法的加强版, 被广泛应用于存储密码和进行身份验证。 883 | 884 | 优势 885 | 886 | - 安全性高:Bcrypt 采用了 `Salt` 和 `Cost` 两种机制, 可有效地防止彩虹表攻击和暴力破解攻击, 从而保证安全性。 887 | 888 | ```markdown 889 | $2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW 890 | \__/\/ \____________________/\_____________________________/ 891 | Alg Cost Salt Hash 892 | ``` 893 | 894 | 案例: 895 | 896 | ```go 897 | import "golang.org/x/crypto/bcrypt" 898 | func main() { 899 | password := "123456" 900 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 901 | ... 902 | // DefaultCost int = 10 903 | } 904 | ``` 905 | 906 | import 部分 907 | 908 | ```go 909 | import( 910 | "golang.org/x/crypto/bcrypt" 911 | ) 912 | ``` 913 | 914 | ```bash 915 | go get -u golang.org/x/crypto/bcrypt 916 | ``` 917 | 918 | ### JWT 定义 919 | 920 | 官网 921 | 922 | 923 | 924 | JSON Web Token(JWT | json 网络令牌)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。JWT 是一种紧凑且自包含的方式, 用于作为 JSON 对象在各方之间安全地传输信息。由于其信息是经过数字签名的, 所以可以确保发送的数据在传输过程中未被篡改。 925 | 926 | JWT 案例: 927 | 928 | ```bash 929 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpbmtrYXBsdW1jaGFubmVsIiwiZXhwIjoiMTE0NTE0In0.-xET51cCeooNbsZlT0IB0rZruoj37kSOW4FZu_bnPgg 930 | ``` 931 | 932 | 组成部分: 933 | 934 | - Header 935 | - Payload 936 | - Signature 937 | 938 | `xxxxx.yyyyy.zzzzz` 分别对应上面三个部分。 939 | 940 | #### Header 案例 941 | 942 | ```json 943 | { 944 | "alg": "HS256", 945 | "typ": "JWT" 946 | } 947 | ``` 948 | 949 | 进行 base64 加密(可解密),构成了第一部分`eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9` 950 | 951 | #### Payload 案例 952 | 953 | Payload 部分包含所传递的声明(Claims)。 954 | 955 | 有三种: 956 | 957 | 注册声明:这些声明是预定义的,非必须使用的但被推荐使用。官方标准定义的注册声明 958 | 959 | 如`exp`, 即为过期时间 960 | 961 | 公共声明: JWT 签发方可以自定义的声明 962 | 963 | 如`username` 964 | 965 | 私有声明: JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景, 区别于`注册声明`和`公共声明`。 966 | 967 | 然后将其进行 Base64 加密,得到 JWT 的第二部分。 968 | 969 | ### Signature 简述 970 | 971 | 用于验证消息在此过程中没有更改。 972 | 973 | #### 用法 974 | 975 | 请求头里加入 Authorization,并加上 Bearer 前缀。 976 | 977 | 可用的库: 978 | 979 | 980 | 981 | ```bash 982 | go get github.com/golang-jwt/jwt/v5 983 | ``` 984 | 985 | #### 过期时间解释 986 | 987 | ![exp_exp](image-3.png) 988 | 989 | 是 Unix epoch 990 | 991 | ### CRUD 简述 992 | 993 | 代表 Create(创建)、Read(读取)、Update(更新)和 Delete(删除)。 994 | 995 | 在计算机程序设计中,CRUD 是对数据进行的一系列基本操作 996 | 997 | #### 准备 自动迁移 998 | 999 | `AutoMigrate` 会创建表 1000 | 1001 | `db.AutoMigrate(&User{})` 1002 | 1003 | #### GROM Create 部分 1004 | 1005 | GORM 官方文档 Create 部分 1006 | 1007 | 1008 | 1009 | ```go 1010 | user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} 1011 | 1012 | result := db.Create(&user) // 通过数据的指针来创建 1013 | 1014 | user.ID // 返回插入数据的主键 1015 | result.Error // 返回 error 1016 | result.RowsAffected // 返回插入记录的条数 1017 | ``` 1018 | 1019 | ### 实现登录功能 1020 | 1021 | 我们将实现登录功能 1022 | 1023 | #### 结构体标签 1024 | 1025 | 定义: 1026 | 1027 | 在结构体中, 在成员后面使用``,定义一些描述性信息, 这就叫`tag`,也就是标签。 1028 | 1029 | ```go 1030 | var input struct { 1031 | Username string `json:"username"` 1032 | Password string `json:"password"` 1033 | } 1034 | ``` 1035 | 1036 | 由一个或者多个键值对组成。 1037 | 1038 | ```go 1039 | key1:"value1" key2:"value2" key3:"value3"... 1040 | ``` 1041 | 1042 | ##### 具体作用 1043 | 1044 | 标签的常见用途包括控制结构体字段在序列化、反序列化、数据库操作等过程中的行为。 1045 | 1046 | 将结构体转换为 JSON 时 1047 | 1048 | `Username` 字段会被转换为 `"username"`,例如: 1049 | 1050 | ```go 1051 | input := struct { 1052 | Username string `json:"username"` 1053 | Password string `json:"password"` 1054 | }{ 1055 | Username: "B站InkkaPlum频道", 1056 | Password: "114514", 1057 | } 1058 | 1059 | jsonData, _ := json.Marshal(&input) 1060 | 1061 | strJsonData := string(jsonData) 1062 | 1063 | fmt.Println(strJsonData) 1064 | 1065 | //输出: {"username":"B站InkkaPlum频道","password":"114514"} 1066 | ``` 1067 | 1068 | ```go 1069 | jsonData := `{"username":"InkkaPlum频道","password":"114514"}` 1070 | 1071 | var input struct { 1072 | Username string `json:"username"` 1073 | Password string `json:"password"` 1074 | } 1075 | 1076 | _ = json.Unmarshal([]byte(jsonData), &input) 1077 | 1078 | fmt.Println(input) 1079 | ``` 1080 | 1081 | 前端部分: 1082 | 1083 | ```ts 1084 | interface ExchangeRate { 1085 | fromCurrency: string; 1086 | toCurrency: string; 1087 | rate: number; 1088 | } 1089 | ``` 1090 | 1091 | 需要更改。 1092 | 1093 | 假如不写: 1094 | 1095 | ```json 1096 | { 1097 | "ID": 7, 1098 | "FromCurrency": "USD", 1099 | "ToCurrency": "RUB", 1100 | "Rate": 87, 1101 | "Date": "" 1102 | } 1103 | ``` 1104 | 1105 | 假如写了: 1106 | 1107 | ```json 1108 | { 1109 | "_id": 8, 1110 | "fromCurrency": "USD", 1111 | "toCurrency": "KZT", 1112 | "rate": 479, 1113 | "date": "" 1114 | } 1115 | ``` 1116 | 1117 | 1118 | 1119 | #### GORM 进阶 1120 | 1121 | 查看官方文档即可: 1122 | 1123 | ```go 1124 | // Get first matched record 1125 | db.Where("name = ?", "jinzhu").First(&user) 1126 | // SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1; 1127 | ``` 1128 | 1129 | #### 验证密码 1130 | 1131 | 最后代码: 1132 | 1133 | ```go 1134 | func CheckPassword(password, hash string) bool { 1135 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 1136 | return err == nil 1137 | } 1138 | ``` 1139 | 1140 | `auth_controller.go` 1141 | 1142 | ```go 1143 | if !utils.CheckPassword(input.Password, user.Password) { 1144 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) 1145 | return 1146 | } 1147 | ``` 1148 | 1149 | router 部分更改: 1150 | 1151 | ```go 1152 | auth := r.Group("/api/auth") 1153 | { 1154 | auth.POST("/login", controllers.Login) 1155 | auth.POST("/register", controllers.Register) 1156 | } 1157 | ``` 1158 | 1159 | 测试 JSON 1160 | 1161 | ```json 1162 | { 1163 | "username": "inkkaplum123456", 1164 | "password": "123456" 1165 | } 1166 | ``` 1167 | 1168 | ### 汇率兑换功能 1169 | 1170 | `exchange_rate_controller.go` 1171 | 1172 | 最终案例: 1173 | 1174 | ```json 1175 | { 1176 | "_id": "", 1177 | "fromCurrency": "EUR", 1178 | "toCurrency": "USD", 1179 | "rate": 1.1, 1180 | "date": "", 1181 | }, 1182 | ```` 1183 | 1184 | 返回案例: 1185 | 1186 | ```json 1187 | [ 1188 | { 1189 | "_id": 1, 1190 | "fromCurrency": "EUR", 1191 | "toCurrency": "USD", 1192 | "rate": 1.1, 1193 | "date": "" 1194 | }, 1195 | { 1196 | "_id": 2, 1197 | "fromCurrency": "USD", 1198 | "toCurrency": "JPY", 1199 | "rate": 147, 1200 | "date": "" 1201 | } 1202 | ] 1203 | ``` 1204 | 1205 | #### 结构体切片的提示 1206 | 1207 | ```go 1208 | type prices struct{ 1209 | Name string 1210 | CurrentPrice int 1211 | } 1212 | 1213 | var Prices []prices = []prices{ 1214 | { 1215 | Name: "Sberbank", 1216 | CurrentPrice: 3000, 1217 | }, 1218 | { 1219 | Name: "Yandex", 1220 | CurrentPrice: 200, 1221 | }, 1222 | } 1223 | 1224 | fmt.Println(Prices) 1225 | // [{Sberbank 3000} {Yandex 200}] 1226 | fmt.Println(Prices[0]) 1227 | // {Sberbank 3000} 1228 | ``` 1229 | 1230 | 大小是动态的 1231 | 1232 | ```go 1233 | Prices = append(Prices, prices{ 1234 | Name: "Mailru", 1235 | CurrentPrice : 150, 1236 | }) 1237 | fmt.Println(Prices) 1238 | // [{Sberbank 3000} {Yandex 200} {Mailru 150}] 1239 | ``` 1240 | 1241 | #### GORM 文档 1242 | 1243 | ```go 1244 | db.Find(&users, []int{1,2,3}) 1245 | // SELECT * FROM users WHERE id IN (1,2,3); 1246 | ``` 1247 | 1248 | ### 中间件 1249 | 1250 | 要求: 1251 | 1252 | 用户必须要登录或者注册, 有了 jwt 才可以创建对应的汇率内容。 1253 | 1254 | 中间件是为了过滤路由而发明的一种机制, 也就是 http 请求来到时先经过中间件, 再到具体的处理函数。 1255 | 1256 | - 请求到到达我们定义的处理函数之前, 拦截请求并进行相应处理(比如: 权限验证, 数据过滤等), 这个可以类比为前置拦截器或前置过滤器; 1257 | 1258 | 自定义中间件, 请参考 Gin 的官方文档 - 1259 | 1260 | ```go 1261 | func Logger() gin.HandlerFunc { 1262 | return func(c *gin.Context) { 1263 | t := time.Now() 1264 | 1265 | // 设置 example 变量 1266 | c.Set("example", "12345") 1267 | 1268 | // 请求前 1269 | 1270 | c.Next() 1271 | 1272 | // 请求后 1273 | latency := time.Since(t) 1274 | log.Print(latency) 1275 | 1276 | // 获取发送的 status 1277 | status := c.Writer.Status() 1278 | log.Println(status) 1279 | } 1280 | } 1281 | 1282 | func main() { 1283 | r := gin.New() 1284 | r.Use(Logger()) 1285 | 1286 | r.GET("/test", func(c *gin.Context) { 1287 | example := c.MustGet("example").(string) 1288 | 1289 | // 打印:"12345" 1290 | log.Println(example) 1291 | }) 1292 | 1293 | // 监听并在 0.0.0.0:8080 上启动服务 1294 | r.Run(":8080") 1295 | } 1296 | ``` 1297 | 1298 | `gin.HandlerFunc`内容。 1299 | 1300 | ```go 1301 | type HandlerFunc func(*Context) 1302 | ``` 1303 | 1304 | 在`router.go`内 1305 | 1306 | ```go 1307 | router = gin.Default() 1308 | router.Use(middlewares.MyMiddleware()) 1309 | ``` 1310 | 1311 | #### 验证 JWT 1312 | 1313 | 案例: 1314 | 1315 | ```json 1316 | "Bearer your_token" 1317 | ``` 1318 | 1319 | StackOverflow 的答案 1320 | 1321 | 1322 | 1323 | 参考答案: 1324 | 1325 | ```go 1326 | // For example to parse a JWT with HMAC verification. 1327 | tokenString := /* raw JWT string*/ 1328 | 1329 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 1330 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 1331 | return nil, errors.New("unexpected signing method") 1332 | } 1333 | return []byte(/* your JWT secret*/), nil 1334 | }) 1335 | if err != nil { 1336 | // handle err 1337 | } 1338 | 1339 | // validate the essential claims 1340 | if !token.Valid { 1341 | // handle invalid tokebn 1342 | } 1343 | ``` 1344 | 1345 | 源代码内容: 1346 | 1347 | ```go 1348 | type MapClaims map[string]interface{} 1349 | ``` 1350 | 1351 | #### Set 方法 1352 | 1353 | Set 用于存储此上下文专用的新键值对, 如 1354 | 1355 | ```go 1356 | ctx.Set("username", username) 1357 | ``` 1358 | 1359 | `Set`及`Get`逻辑参考: 1360 | 1361 | `c *gin.Context` -> `func1` -> `func2`(`c.Set(key,value)`) => `func3`(`c.Get(key)`) 1362 | 1363 | 这样后续的处理函数可以通过`c.Get("username")` 访问这个值。 1364 | 1365 | `ctx.Next()`的作用 1366 | 1367 | 调用下一个中间件或处理器 1368 | 1369 | 如果 `c.Next()` 之前有 `c.Abort()`,后续的处理将会被终止,不会继续执行 1370 | 1371 | 案例代码 1372 | 1373 | ```go 1374 | package main 1375 | 1376 | import ( 1377 | "fmt" 1378 | "net/http" 1379 | 1380 | "github.com/gin-gonic/gin" 1381 | ) 1382 | 1383 | func fun1(c *gin.Context) { 1384 | fmt.Println("func1 start") 1385 | fmt.Println("func1 end") 1386 | } 1387 | func fun2(c *gin.Context) { 1388 | fmt.Println("func2 start") 1389 | fmt.Println("func2 end") 1390 | } 1391 | func fun3(c *gin.Context) { 1392 | fmt.Println("func3 start") 1393 | fmt.Println("func3 end") 1394 | } 1395 | 1396 | func main() { 1397 | r := gin.Default() 1398 | 1399 | r.Use(fun1, fun2, fun3) 1400 | 1401 | // Example ping request. 1402 | r.GET("/ping", func(c *gin.Context) { 1403 | fmt.Println("ping start") 1404 | 1405 | c.String(http.StatusOK, "pong") 1406 | 1407 | fmt.Println("ping end") 1408 | }) 1409 | 1410 | r.Run(":8080") 1411 | } 1412 | 1413 | //输出 1414 | func1 start 1415 | func1 end 1416 | func2 start 1417 | func2 end 1418 | ... 1419 | ping start 1420 | ping end 1421 | ``` 1422 | 1423 | 用了`c.Next()`后 1424 | 1425 | ```go 1426 | func fun1(c *gin.Context) { 1427 | fmt.Println("func1 start") 1428 | c.Next() 1429 | fmt.Println("func1 end") 1430 | } 1431 | func fun2(c *gin.Context) { 1432 | fmt.Println("func2 start") 1433 | c.Next() 1434 | fmt.Println("func2 end") 1435 | } 1436 | func fun3(c *gin.Context) { 1437 | // ... 1438 | } 1439 | //输出 1440 | func1 start 1441 | func2 start 1442 | func3 start 1443 | ping start 1444 | ping end 1445 | func3 end 1446 | func2 end 1447 | func1 end 1448 | ``` 1449 | 1450 | 如有`c.Abort`: 1451 | 1452 | ```go 1453 | func fun1(c *gin.Context) { 1454 | fmt.Println("func1 start") 1455 | c.Abort() 1456 | c.Next() 1457 | fmt.Println("func1 end") 1458 | } 1459 | func fun2(c *gin.Context) { 1460 | // ... 1461 | } 1462 | func fun3(c *gin.Context) { 1463 | // ... 1464 | } 1465 | 1466 | //输出: 1467 | func1 start 1468 | func1 end 1469 | ``` 1470 | 1471 | #### 路由部分最终配置 1472 | 1473 | ```go 1474 | api.Use(middlewares.AuthMiddleware()) 1475 | ``` 1476 | 1477 | 测试案例 1478 | 1479 | ```json 1480 | { 1481 | "fromCurrency": "EUR", 1482 | "toCurrency": "USD", 1483 | "rate": 1.1 1484 | } 1485 | ``` 1486 | 1487 | ### 文章逻辑 1488 | 1489 | ```go 1490 | type Article struct { 1491 | gorm.Model 1492 | Title string 1493 | Content string 1494 | Preview string 1495 | Likes int `gorm:"default:0"` // 点赞数,默认为0 1496 | } 1497 | ``` 1498 | 1499 | 使用`global.DB.Create(&article)`将新文章插入到数据库中。 1500 | 1501 | 前端部分提示 1502 | 1503 | ```ts 1504 | export interface Article { 1505 | ID: string; 1506 | Title: string; 1507 | Preview: string; 1508 | Content: string; 1509 | } 1510 | ``` 1511 | 1512 | ```ts 1513 | const response = await axios.get
(`/articles/${id}`); 1514 | ``` 1515 | 1516 | 案例 1517 | 1518 | 1519 | 1520 | ### 路由参数 1521 | 1522 | 静态和参数路由概念: 1523 | 1524 | 1. 静态路由:完全匹配的路由, 如`/articles`。 1525 | 2. 参数路由:在路径中带上了参数(Params)的路由, 如`/articles/:id`。 1526 | 1527 | 在 Gin 中, `c.Param` 方法可以获取路径中的参数, 那么其会返回 URL 参数的值, 比如说 1。 1528 | 1529 | Gorm, 找到第一条匹配的记录 1530 | 1531 | ```go 1532 | db.Where("name = ?", "jinzhu").First(&user) 1533 | ``` 1534 | 1535 | 测试用: 1536 | 1537 | ```json 1538 | { 1539 | "Title": "欧央行加息了!", 1540 | "Preview": "加息!", 1541 | "Content": "加息, 重要内容" 1542 | } 1543 | ``` 1544 | 1545 | ### Redis 相关重要概念 1546 | 1547 | Redis 官网 1548 | 1549 | 安装 Redis 1550 | 1551 | 1552 | 1553 | 选择`Redis-x64-5.0.14.1.msi`即可 1554 | 1555 | 定义: Redis 是一个高性能 NoSQL 的, 用 C 实现, 可基于内存亦可持久化的 Key-Value 数据库(键值对存储数据库), 并提供多种语言的 API。 1556 | 1557 | 根据月度排行网站`DB-Engines.com`的数据,Redis 是现在最受欢迎的 NoSQL 数据库之一(MongoDB 亦为 NoSQL 数据库)。 1558 | 1559 | 与 MySQL 数据库不同的是(为了实现数据的持久化存储, Mysql 将数据存储到了磁盘中), Redis 的数据是存在内存中的。它的读写速度非常快。 1560 | 1561 | 一些基础概念: 1562 | 1563 | Redis 有如字符串(String)、列表(List)、集合(Set)、有序集合(ZSet/Sorted Set)这样的基础数据类型。 1564 | 1565 | 优势: 1566 | 1567 | 性能好、丰富的数据类型。 1568 | 1569 | #### 持久化 1570 | 1571 | 定义: 持久化是指将数据写入持久存储(durable storage), 如固态硬盘(SSD)。 1572 | 1573 | Redis 提供了一系列选项。 1574 | 1575 | 1. RDB(Redis Database): RDB 持久化通过在指定的时间间隔内创建数据集的快照来保存数据。 1576 | 1577 | 2. AOF(Append Only File): AOF 持久化记录服务器接收到的每一个写操作,并将这些操作追加到日志文件中。 1578 | 1579 | 3. 无持久化: 完全禁用 Redis 的持久化机制, 这意味着 Redis 只会在内存中存储数据。 1580 | 1581 | 4. AOF + RDB 组合: 可以在同一个实例中同时启用 RDB 和 AOF 持久化。 1582 | 1583 | 设置方法 1584 | 1585 | 在 redis 目录下, 找到`redis.windows.conf`即可。 1586 | 1587 | RDB 部分: 1588 | 1589 | 默认情况下, Redis 会将数据集的快照保存在磁盘上一个名为 `dump.rdb`的二进制文件中。 1590 | 1591 | 目录(一般情况) 1592 | 1593 | `C:\Program Files\Redis` 1594 | 1595 | 在`redis.windows.conf`文件内 1596 | 1597 | 找到 1598 | 1599 | ```conf 1600 | save 60 1000 1601 | ``` 1602 | 1603 | AOF 部分 1604 | 1605 | 找到 1606 | 1607 | 将`no`改为`yes`, 开启 AOF 1608 | 1609 | ```conf 1610 | appendonly yes 1611 | ``` 1612 | 1613 | 它在我们的项目中的作用 1614 | 1615 | 典型的案例 1616 | 1617 | 如一个帖子: 1618 | 1619 | ![Post](image-5.png) 1620 | 1621 | ![Post01](image-11.png) 1622 | 1623 | 这个帖子下面的点赞、转发、评论数内容都是一个典型的案例, 在我们的案例中就是点赞功能。 1624 | 1625 | Go-redis 定义: Go-redis 是 Golang 中用于与 Redis 交互的强大工具, 支持 Redis Server 的 Golang 客户端。 1626 | 1627 | GitHub: 1628 | 1629 | 文档: 1630 | 1631 | ```bash 1632 | go get -u github.com/go-redis/redis 1633 | ``` 1634 | 1635 | `redis.go` 1636 | 1637 | ```go 1638 | package config 1639 | 1640 | import ( 1641 | "github.com/go-redis/redis" 1642 | ) 1643 | 1644 | var RedisClient *redis.Client 1645 | 1646 | func InitRedis() { 1647 | RedisClient = redis.NewClient(&redis.Options{ 1648 | Addr: "localhost:6379", 1649 | DB: 0, // 默认数据库(use default DB) 1650 | Password: "", 1651 | }) 1652 | 1653 | _, err := RedisClient.Ping().Result() 1654 | if err != nil { 1655 | panic("Failed to connect to Redis") 1656 | } 1657 | } 1658 | ``` 1659 | 1660 | `localhost:6379`——在安装过程中设置的端口。 1661 | 1662 | 默认情况下就是 6379, 截屏参考 1663 | 1664 | ![defaultport](image-6.png) 1665 | 1666 | `global.go` 1667 | 1668 | ```go 1669 | var ( 1670 | DB *gorm.DB 1671 | RedisDB *redis.Client 1672 | ) 1673 | ``` 1674 | 1675 | ### 实现点赞功能 1676 | 1677 | `like_controller.go` 1678 | 1679 | 给文章增加点赞数 1680 | 1681 | #### redis key 命名规范的设计 1682 | 1683 | key 单词与单词之间以 (:)分割, 如`user:userinfo`, `article:1:likes` 1684 | 1685 | #### SET,INCR,DECR,GET命令 1686 | 1687 | `SET` 命令用于设置给定 key 的值。 1688 | 1689 | `INCR` 将 key 中储存的数字值增一。 1690 | 1691 | 若 key 不存在,那么 key 的值会先被初始化为 0,然后再执行操作。(`0` -> `1`) 1692 | 1693 | `DECR` 则将 key 中储存的数字减一。 1694 | 1695 | `GET` 命令用于获取指定 key 的值。 1696 | 1697 | Go-redis活用: 1698 | 1699 | ```go 1700 | // 执行 Redis 命令: 1701 | val, err := rdb.Get("key").Result() 1702 | fmt.Println(val) 1703 | ``` 1704 | 1705 | ### 重要概念: 原子性 1706 | 1707 | 原子性确保一个操作在执行过程中是不可被打断的。对于原子操作, 要么所有的步骤都成功完成并对外可见, 要么这些步骤都不执行, 系统的状态保持不变。 1708 | 1709 | `INCR`和`DECR`操作在 Redis 中就是原子操作(原子性的)。 1710 | 1711 | 例子: 用户 A 发起点赞请求, Redis 执行 INCR 命令, 将点赞数从 10 增加到 11。 1712 | 用户 B 几乎同时发起点赞请求, Redis 执行 INCR 命令, 将点赞数从 11 增加到 12。 1713 | 1714 | 因此, 若多个操作只是单纯的对数据进行增加或减少值, Redis 提供的`INCR`和`DECR`命令可以直接帮助我们进行并发控制。 1715 | 1716 | 但是, 若多个操作不只是单纯的进行数据增减值, 还包括更复杂的操作, 如: 逻辑判断, 此时 Redis 的单命令操作无法保证多个操作互斥执行, 故可用 Lua 脚本来解决此问题, 本教程不涉及这一点, 以后会有专门的分析。 1717 | 1718 | ### 路由部分最后配置 1719 | 1720 | `router.go` 1721 | 1722 | ```go 1723 | api.POST("/articles/:id/like", controllers.LikeArticle) 1724 | api.GET("/articles/:id/like", controllers.GetArticleLikes) 1725 | ``` 1726 | 1727 | 到此, 我们后端部分严格来说就算彻底完成了。 1728 | 1729 | 测试用: 1730 | 1731 | `GET` 1732 | 1733 | ```bash 1734 | http://localhost:3000/api/articles/1/like 1735 | ``` 1736 | 1737 | 可以看到, 正确地得到了 likes 为 0 的响应内容。 1738 | 1739 | ### 增加前端功能 1740 | 1741 | 如果是直接用 GitHub 对应课件上面得到的前端内容, 无需进行任何更改、添加 1742 | 1743 | 如果是继续基于上一次本频道(B 站: InkkaPlum 频道)的[Vue 教程](https://www.bilibili.com/video/BV1c142117Fz)的对应源码进行的后端部分学习, 则需按下面内容进行更改。 1744 | 1745 | 在`article.d.ts`中, 添加。 1746 | 1747 | ```ts 1748 | export interface Like { 1749 | likes: number; 1750 | } 1751 | ``` 1752 | 1753 | 在`NewsDetailView.vue`中, 修改并添加代码 1754 | 1755 | 修改: 1756 | 1757 | ```ts 1758 | import type { Article, Like } from "../types/Article"; 1759 | 1760 | // 而非原来的 1761 | import type { Article } from "../types/Article"; 1762 | ``` 1763 | 1764 | 添加: 1765 | 1766 | ```ts 1767 | const likeArticle = async () => { 1768 | try { 1769 | const res = await axios.post(`articles/${id}/like`); 1770 | likes.value = res.data.likes; 1771 | await fetchLike(); 1772 | } catch (error) { 1773 | console.log("Error Liking article:", error); 1774 | } 1775 | }; 1776 | const fetchLike = async () => { 1777 | try { 1778 | const res = await axios.get(`articles/${id}/like`); 1779 | likes.value = res.data.likes; 1780 | } catch (error) { 1781 | console.log("Error fetching likes:", error); 1782 | } 1783 | }; 1784 | 1785 | onMounted(fetchLike); 1786 | ``` 1787 | 1788 | 由于后端逻辑有变, 需要修改的地方 1789 | 1790 | 这一次的 JWT 因为在后端部分就带有`Bearer`前缀, 所以前端部分`axios.ts`无需再加上`Bearer`前缀 1791 | 1792 | 否则会出现: 1793 | 1794 | ```json 1795 | Bearer Bearer ...(jwt) 1796 | ``` 1797 | 1798 | 的现象, 所以在`axios.ts`由 1799 | 1800 | ```ts 1801 | config.headers.Authorization = "Bearer " + token; 1802 | ``` 1803 | 1804 | 修改为 1805 | 1806 | ```ts 1807 | config.headers.Authorization = token; 1808 | ``` 1809 | 1810 | 即可。 1811 | 1812 | 可修改的地方: 1813 | 1814 | 由于这一次的后端逻辑是`GET articles`, 即为获取所有的文章, 也必须要登录/注册(文章预览页若未登录/注册, 亦无法显示任何内容), 不存在点击查看文章提示登录/注册后再看的逻辑, 所以前端部分可以微小改动, 使逻辑更合理, 当然也可以进一步改动。 1815 | 1816 | 将`NewsView.vue`中的 1817 | 1818 | ```vue 1819 |
No data
1820 | ``` 1821 | 1822 | 改成 1823 | 1824 | ```vue 1825 |
您必须登录/注册才可以阅读文章
1826 | ``` 1827 | 1828 | 自然也可以调整后端代码, 维持原先的逻辑。 1829 | 1830 | 敲命令: 1831 | 1832 | ```bash 1833 | npm run dev 1834 | ``` 1835 | 1836 | ### Redis 案例, 缓存实战——基础知识 常见的缓存设计模式 1837 | 1838 | 为何要有缓存技术? 1839 | 1840 | 减轻数据库访问压力, 加快请求响应速度。 1841 | 1842 | 缓存读取速度比从数据库中读取快得多。亦可大幅减少数据库查询的次数, 提高应用程序的响应速度。 1843 | 1844 | 案例: 1845 | 1846 | 文章预览页面, 若有非常多的文章内容(几千条内容), 且此项目用户量较大 1847 | 1848 | 若无缓存, 所有的请求都需要直接访问数据库, 1849 | 1850 | 可能带来的风险: 应用程序响应变慢, 数据库容易被大量查询拖慢, 影响整个系统的稳定性。 1851 | 1852 | 常见的缓存设计模式 1853 | 1854 | 1. **旁路缓存模式**(**Cache-Aside 模式**): 1855 | 1856 | 应用程序直接与缓存和数据库交互, 并负责管理缓存的内容。使用该模式的应用程序, 会同时操作缓存与数据库 1857 | 具体流程如下: 1858 | 1859 | 先尝试从缓存中读取数据。若缓存命中, 直接返回缓存中的数据, 1860 | 1861 | 若缓存未命中, 则从数据库中读取数据, 并将数据存入缓存中, 然后返回给客户端。 1862 | 1863 | 简单概念图:(图源: Yandex) 1864 | ![Cache-Aside](image-9.png) 1865 | 1866 | 代码逻辑: 1867 | 1868 | 1. **缓存未命中**: 1869 | 1870 | - 如果 Redis 中没有找到对应的缓存(缓存未命中), 代码会从数据库中获取文章数据。 1871 | - 获取到数据后, 代码将数据序列化为 JSON, 并将其存储在 Redis 缓存中, 同时设置一个过期时间。 1872 | - 最后, 返回数据库中的数据给客户端。 1873 | 1874 | 2. **缓存命中**: 1875 | - 如果缓存命中(Redis 中找到了对应的缓存数据), 代码直接从缓存中获取数据。 1876 | - 然后, 将缓存中的数据反序列化为文章列表,返回给客户端。 1877 | 1878 | (在读取数据时, 只有在缓存未命中的情况下, 才会查询数据库并将结果写入缓存。) 1879 | 1880 | 此外, 还有如**读写穿透模式**等等模式。 1881 | 1882 | #### 解决下一个问题 1883 | 1884 | 若在缓存有效期内, 用户又新增了一些文章, 此时用户通过缓存得到文章, 将看不到变化。 1885 | 1886 | 解决方法案例: 常见的缓存失效策略 1887 | 1888 | 1. 设置过期时间(我们已经做过了) 1889 | 1890 | 2. 主动更新缓存 1891 | 1892 | 如: 当新增文章时, 除了更新数据库, 还要同步更新或删除缓存中的对应数据。这样, 下一次读取时, 缓存中的数据就是最新的。 1893 | 1894 | 或者新增文章时, 不直接更新缓存, 而是删除缓存中的旧数据。 1895 | 1896 | 下次读取时, 由于缓存没有命中, 从数据库中读取最新数据并重新写入缓存。 1897 | 1898 | Redis `DEL`命令: 用于删除已存在的键。 1899 | 1900 | ### CORS 1901 | 1902 | 发现问题: 1903 | 1904 | ![CORS](image-7.png) 1905 | 1906 | CORS 的概念: 1907 | 1908 | CORS, 全称为"跨域(跨源)资源共享"(Cross-Origin Resource Sharing), 是一种机制, 使用额外的 HTTP 头来告诉浏览器允许一个网页从另一个域(不同于该网页所在的域)请求资源。这样可以在服务器和客户端之间进行安全的跨域通信。 1909 | 1910 | 浏览器将 CORS 请求分成两类: 1911 | 1912 | - 简单请求(simple req) 1913 | 1914 | 方法如`GET`, `POST` 1915 | 1916 | 头部字段如: `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`(需要注意额外的限制)等 1917 | 1918 | 在 Postman 中检查 `content-type` 及 `content-length` 1919 | 1920 | ![Content-Type](image-8.png) 1921 | 1922 | - 非简单请求(not-so-simple req)。 1923 | 1924 | 方法如`PUT` 1925 | 1926 | 在正式通信之前, 增加一次`HTTP`查询请求, 称为`预检(Preflight)`请求 1927 | 1928 | 该请求是`Option`方法的, 通过该请求来知道服务端是否允许跨域请求。 1929 | 1930 | 我们可以在 Code 内看到: 1931 | 1932 | ![VS Code Example](image-12.png) 1933 | 1934 | 对于简单请求, 浏览器直接发出 CORS 请求。 1935 | 1936 | #### CORS 相关的问题 1937 | 1938 | 域是什么? 1939 | 1940 | Origin, 可译为源, 亦可译为域, 在 CORS 上下文中 Origin 由三个元素组成: 1941 | 1942 | Origin 即为 `协议(如https)` + `域名(如www.example.com)` + `端口(如80)` 1943 | 1944 | 同源策略(Same-Origin Policy, SOP) 1945 | 1946 | 浏览器的一种安全机制, 用于防止恶意网站通过脚本对其他网站的内容进行访问。 1947 | 1948 | 以下 URL 属于同源地址: 1949 | 1950 | 如: `example.com:443` 和 `example.com:443/articles` 1951 | 1952 | 但`inkkaplum频道B站.example.com/articles` 和 `example.com/articles`则不是。 1953 | 1954 | 自然, `example.com:80`和`example.com:443`则不是 1955 | 1956 | 跨域请求 1957 | 1958 | 指从一个域向另一个域发起的 HTTP 请求。 1959 | 1960 | 如从前端应用向不同的后端 API 服务器请求数据, 但是同源策略默认会阻止这些请求。 1961 | 1962 | 所以需要 CORS 机制来显式允许跨域访问。 1963 | 1964 | 本案例的 URL: 1965 | 1966 | (前端) 和(后端) 1967 | 1968 | 浏览器默认允许同源请求, 但是默认会阻止这些跨域请求, 除非服务器明确允许。 1969 | 1970 | 要解决这个问题, 需在后端应用中配置 CORS, 允许前端应用访问后端 API。 1971 | 1972 | #### 安装 CORS 中间件 1973 | 1974 | 使用 `Gin CORS middleware`, 可以很方便地在 Gin 中配置 CORS。 1975 | 1976 | 官方案例: 1977 | 1978 | 1979 | 1980 | 命令 1981 | 1982 | ```bash 1983 | go get github.com/gin-contrib/cors 1984 | ``` 1985 | 1986 | ```go 1987 | package main 1988 | 1989 | import ( 1990 | "time" 1991 | 1992 | "github.com/gin-contrib/cors" 1993 | "github.com/gin-gonic/gin" 1994 | ) 1995 | 1996 | func main() { 1997 | router := gin.Default() 1998 | // CORS for https://foo.com and https://github.com origins, allowing: 1999 | // - PUT and PATCH methods 2000 | // - Origin header 2001 | // - Credentials share 2002 | // - Preflight requests cached for 12 hours 2003 | router.Use(cors.New(cors.Config{ 2004 | AllowOrigins: []string{"https://....com"}, 2005 | AllowMethods: []string{"PUT", "PATCH"}, 2006 | AllowHeaders: []string{"Origin"}, 2007 | ExposeHeaders: []string{"Content-Length"}, 2008 | AllowCredentials: true, 2009 | AllowOriginFunc: func(origin string) bool { 2010 | return origin == "https://github.com" 2011 | }, 2012 | MaxAge: 12 * time.Hour, 2013 | })) 2014 | router.Run() 2015 | } 2016 | ``` 2017 | 2018 | ### 优雅地退出我们的应用 2019 | 2020 | 我们目前可以看到 2021 | 2022 | ```bash 2023 | exit status 0xc000013a 2024 | ``` 2025 | 2026 | 我们需要进行处理。 2027 | 2028 | 参考此文档: 2029 | 2030 | 2031 | 2032 | #### Go 语言 Channel 2033 | 2034 | 前置知识: 2035 | 2036 | Go 语言的并发模型是 CSP(Communicating Sequential Processes/译为通信顺序进程), 2037 | 2038 | 提倡通过通信共享内存而非通过共享内存而实现通信。 2039 | 2040 | Channel 类型: 通道在 Go 中是一种特殊的类型。 2041 | 2042 | 通道像一个队列(Queue), 总是遵循先入先出(FIFO)的规则以保证收发数据的顺序。 2043 | 2044 | 例子: 2045 | 2046 | ```go 2047 | var channelExample chan int // 声明一个传递整型的通道 2048 | ``` 2049 | 2050 | 判断: 2051 | 2052 | `chan T` `chan<- T` `<-chan T` 2053 | 2054 | 通道是引用类型, 故空值为`nil`。 2055 | 2056 | 案例: 2057 | 2058 | ```go 2059 | var channelExample chan int 2060 | fmt.Println(channelExample) //输出 2061 | ``` 2062 | 2063 | 声明的 Channel, 需用 make func 初始化之后才能使用。格式如下: 2064 | 2065 | ```go 2066 | make(chan 元素类型, [缓冲(Buffer)大小(可选)]) 2067 | // 例子: 2068 | channelExample01 := make(chan []int) 2069 | ``` 2070 | 2071 | 操作: 2072 | 2073 | 有发送(Send)、接收(Receive)和关闭(Close)三种操作。 2074 | 2075 | 发送的案例: 2076 | 2077 | ```go 2078 | var channelExample chan int 2079 | 2080 | channelExample <- 114 2081 | ``` 2082 | 2083 | 接收的案例 2084 | 2085 | ```go 2086 | example := <- channelExample 2087 | <-channelExample 2088 | ``` 2089 | 2090 | 关闭的案例 2091 | 2092 | 通过调用内置的 `close` func 来关闭 channel。 2093 | 2094 | ```go 2095 | close(channelExample) 2096 | ``` 2097 | 2098 | 关闭后的通道有如下特点: 2099 | 2100 | - 再发送值就会导致 panic。 2101 | - 对其进行接收会一直获取值直到通道为空。 2102 | - 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。 2103 | - 关闭一个已经关闭的通道会导致 panic。 2104 | 2105 | 判断 channel 是否关闭 2106 | 2107 | ```go 2108 | v, ok := <-channel 2109 | ``` 2110 | 2111 | #### 死锁(Deadlock) 2112 | 2113 | ```bash 2114 | fatal error: all goroutines are asleep - deadlock! 2115 | ``` 2116 | 2117 | 具体案例分析: 2118 | 2119 | 第一种情形 2120 | 2121 | ```go 2122 | func main() { 2123 | channel := make(chan int, 0) 2124 | 2125 | channel <- 114 2126 | 2127 | x := <- channel 2128 | 2129 | fmt.Println(x) 2130 | } 2131 | ``` 2132 | 2133 | 解决方法, 缓冲大小改变, 如`channel := make(chan int, 1)` 2134 | 2135 | 但是: 2136 | 2137 | ```go 2138 | func main() { 2139 | channel := make(chan int, 1) 2140 | channel <- 10 2141 | channel <- 10 2142 | x:= <- channel 2143 | 2144 | fmt.Println(x) 2145 | } 2146 | 2147 | ``` 2148 | 2149 | 或者, 2150 | 2151 | ```go 2152 | func main() { 2153 | channel := make(chan int, 0) 2154 | 2155 | go func(){ 2156 | channel <- 114 2157 | }() 2158 | x := <- channel 2159 | 2160 | fmt.Println(x) 2161 | } 2162 | ``` 2163 | 2164 | 第二种情形 2165 | 2166 | ```go 2167 | func main() { 2168 | channel := make(chan int) 2169 | x := <- channel 2170 | go func() { 2171 | channel <-114 2172 | }() 2173 | fmt.Println(x) 2174 | } 2175 | ``` 2176 | 2177 | 以及: 2178 | 2179 | 第三种情形 2180 | 2181 | ```go 2182 | func main() { 2183 | channel01 := make(chan int) 2184 | channel02 := make(chan int) 2185 | 2186 | go func() { 2187 | select { 2188 | case <- channel01: 2189 | channel02<-114 2190 | } 2191 | }() 2192 | 2193 | select { 2194 | case <- channel02: 2195 | channel01 <- 114 2196 | } 2197 | } 2198 | 2199 | //结论: fatal error: all goroutines are asleep - deadlock! 2200 | ``` 2201 | 2202 | `Select`说明: 2203 | select 是 Go 中的一个控制结构。 2204 | 2205 | #### 官方文档关闭案例 2206 | 2207 | 2208 | 2209 | ```go 2210 | // +build go1.8 2211 | 2212 | package main 2213 | 2214 | import ( 2215 | "context" 2216 | "log" 2217 | "net/http" 2218 | "os" 2219 | "os/signal" 2220 | "time" 2221 | 2222 | "github.com/gin-gonic/gin" 2223 | ) 2224 | 2225 | func main() { 2226 | router := gin.Default() 2227 | router.GET("/", func(c *gin.Context) { 2228 | time.Sleep(5 * time.Second) 2229 | c.String(http.StatusOK, "Welcome Gin Server") 2230 | }) 2231 | 2232 | srv := &http.Server{ 2233 | Addr: ":8080", 2234 | Handler: router, 2235 | } 2236 | 2237 | go func() { 2238 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 2239 | log.Fatalf("listen: %s\n", err) 2240 | } 2241 | }() 2242 | 2243 | quit := make(chan os.Signal, 1) 2244 | signal.Notify(quit, os.Interrupt) 2245 | <-quit 2246 | log.Println("Shutdown Server ...") 2247 | 2248 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 2249 | defer cancel() 2250 | if err := srv.Shutdown(ctx); err != nil { 2251 | log.Fatal("Server Shutdown:", err) 2252 | } 2253 | log.Println("Server exiting") 2254 | } 2255 | ``` 2256 | 2257 | 然后即可看到改变: 2258 | 2259 | ```bash 2260 | 2024/08/30 23:34:22 Shutdown Server... 2261 | 2024/08/30 23:34:22 Server exiting 2262 | ``` 2263 | 2264 | #### WaitGroup 2265 | 2266 | `WaitGroup`的重要说明: `WatiGroup`是`sync` package 中的一个`struct`, 用来收集需要等待执行完成的`goroutine`。 2267 | 2268 | 三个方法: 2269 | 2270 | `Add()`: 用来设置或添加要等待完成的 goroutine 数量 2271 | 例子: 2272 | 2273 | `Add(2)`或两次调用`Add(1)`都会设置等待计数器的值为 2, 即为要等待 2 个 goroutine 完成。 2274 | 2275 | 自然, `Done()`则表示需要等待的 goroutine 在真正完成之前, 应调用该方法来人为表示 goroutine 完成了, 重要地是: 该方法会对等待计数器减 1。 2276 | 2277 | 最后, `Wait()`则意味着在等待计数器减为 0 之前, `Wait()`会一直阻塞当前的 goroutine 2278 | 2279 | 案例代码: 2280 | 2281 | ```go 2282 | func TestTaskControdl(t *testing.T) { 2283 | taskNum := 5 2284 | 2285 | wg := sync.WaitGroup{} 2286 | wg.Add(taskNum) 2287 | 2288 | 2289 | for i:=0; i< taskNum; i++{ 2290 | go func(i int){ 2291 | fmt.Println("info", i) 2292 | wg.Done() 2293 | }(i) 2294 | } 2295 | 2296 | wg.Wait() 2297 | } 2298 | ``` 2299 | 2300 | 结论: 2301 | 2302 | ```bash 2303 | info 4 2304 | info 3 2305 | info 1 2306 | info 0 2307 | info 2 2308 | ``` 2309 | 2310 | #### Close案例 2311 | 2312 | ```go 2313 | func Test(t *testing.T){ 2314 | test := make(chan int, 10) 2315 | 2316 | go func(info chan int){ 2317 | for{ 2318 | select{ 2319 | case val, ok := <- test: 2320 | if !ok{ 2321 | t.Logf("Channel Closed!") 2322 | return 2323 | } 2324 | 2325 | t.Logf("data %d\n", val) 2326 | } 2327 | } 2328 | }(test) 2329 | 2330 | go func(){ 2331 | test <- 1 2332 | time.Sleep(1 * time.Second) 2333 | test <- 2 2334 | 2335 | close(test) 2336 | }() 2337 | 2338 | time.Sleep(5 *time.Second) 2339 | } 2340 | ``` 2341 | 2342 | 输出 2343 | 2344 | ```bash 2345 | data 1 2346 | data 2 2347 | Context Closed! 2348 | ``` 2349 | 2350 | #### 单独退出通道 2351 | 2352 | 传输数据不共用一个 channel 2353 | 2354 | ```go 2355 | func TestA(t *testing.T) { 2356 | test := make(chan int, 5) 2357 | exit := make(chan struct{}) 2358 | 2359 | go func(info chan int, exit chan struct{}) { 2360 | for { 2361 | select { 2362 | case val := <-info: 2363 | t.Logf("data %d\n", val) 2364 | 2365 | case <-exit: 2366 | t.Logf("Task Exit!!\n") 2367 | return 2368 | } 2369 | } 2370 | }(test, exit) 2371 | 2372 | go func() { 2373 | test <- 1 2374 | time.Sleep(1 * time.Second) 2375 | test <- 2 2376 | close(exit) 2377 | }() 2378 | time.Sleep(5 *time.Second) 2379 | } 2380 | ``` 2381 | 2382 | #### 超时任务控制 2383 | 2384 | ```go 2385 | func Test(t *testing.T){ 2386 | test := make(chan int, 5) 2387 | 2388 | go func(info chan int){ 2389 | for{ 2390 | select{ 2391 | case val := <- info: 2392 | t.Logf("Data %d\n", val) 2393 | 2394 | case <- time.After(2 * time.Second): 2395 | t.Logf("Time out!\n") 2396 | return 2397 | } 2398 | } 2399 | }(test) 2400 | 2401 | go func(){ 2402 | test <- 1 2403 | time.Sleep(2 * time.Second) //>=2 2404 | test <- 2 2405 | }() 2406 | 2407 | time.Sleep(5 *time.Second) 2408 | } 2409 | ``` 2410 | 2411 | 结论: 2412 | 2413 | ```bash 2414 | main_test.go:98: Data 1 2415 | main_test.go:101: Time out! 2416 | ``` 2417 | 2418 | #### Context 的场景 2419 | 2420 | Context 是用来让多级 Goroutine 实现通信的一种工具, 并发安全。 2421 | 2422 | 多级嵌套: 父任务停止, 子任务停止、 控制停止顺序(如 ABCDEFG, 可以让顺序为 EFG, BC...) 2423 | 2424 | `context.Context` 该接口定义了四个需要实现的方法 2425 | 2426 | 源码 2427 | 2428 | ```go 2429 | type Context interface { 2430 | 2431 | Deadline() (deadline time.Time, ok bool) 2432 | 2433 | Done() <-chan struct{} 2434 | 2435 | Err() error 2436 | 2437 | Value(key any) any 2438 | } 2439 | ``` 2440 | 2441 | ```go 2442 | func Background() Context { 2443 | return backgroundCtx{} 2444 | } 2445 | 2446 | // TODO returns a non-nil, empty [Context]. Code should use context.TODO when 2447 | // it's unclear which Context to use or it is not yet available (because the 2448 | // surrounding function has not yet been extended to accept a Context 2449 | // parameter). 2450 | func TODO() Context { 2451 | return todoCtx{} 2452 | } 2453 | ``` 2454 | 2455 | ```go 2456 | type backgroundCtx struct{ emptyCtx } 2457 | type todoCtx struct{ emptyCtx } 2458 | type emptyCtx struct{} 2459 | ``` 2460 | 2461 | ```go 2462 | func (emptyCtx) Deadline() (deadline time.Time, ok bool) { 2463 | return 2464 | } 2465 | 2466 | func (emptyCtx) Done() <-chan struct{} { 2467 | return nil 2468 | } 2469 | 2470 | func (emptyCtx) Err() error { 2471 | return nil 2472 | } 2473 | 2474 | func (emptyCtx) Value(key any) any { 2475 | return nil 2476 | } 2477 | ``` 2478 | 2479 | 通过`context.WithTimeout()` 设置上下文的超时时间, 在到达超时之后自动结束。 2480 | 2481 | 而`WithDeadline()`则将设置上下文需要完成的截止时间。 2482 | 2483 | 例子: 2484 | 2485 | ```go 2486 | ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Millisecond), 2487 | ``` 2488 | 2489 | `WithValue()`函数 2490 | 2491 | ```go 2492 | func TestContext(t *testing.T){ 2493 | a := context.Background() 2494 | b := context.WithValue(a, "k1", "val1") 2495 | c := context.WithValue(b, "key1", "val1") 2496 | d := context.WithValue(c, "key2", "val2") 2497 | e := context.WithValue(d, "key3", "val3") 2498 | f := context.WithValue(e, "key3", "val4") 2499 | fmt.Printf(" %s\n", f.Value("key3")) 2500 | // 输出: val4 2501 | } 2502 | ``` 2503 | 2504 | #### 传递取消信号 2505 | 2506 | 四个方法中, 我们说明了`Value`的重要内容, 而其它都是和取消有一定的关系, 所以我们需要分析。 2507 | 2508 | 上下文是可以结束的 2509 | 2510 | `Done()` 确定上下文是否完成 2511 | 2512 | 而取消上下文则是最直接的方式, 之前进行了`context.WithCancel`已经进行了演示。 2513 | 2514 | #### 基本使用的案例 2515 | 2516 | ```go 2517 | func Test(t *testing.T) { 2518 | ctx, cancel := context.WithCancel(context.Background()) 2519 | defer cancel() 2520 | 2521 | go func() { 2522 | for { 2523 | select { 2524 | case <-ctx.Done(): 2525 | t.Log("Context cancelled!") 2526 | return 2527 | } 2528 | } 2529 | }() 2530 | 2531 | go func() { 2532 | time.Sleep(1 * time.Second) 2533 | cancel() 2534 | }() 2535 | 2536 | time.Sleep(2 * time.Second) 2537 | } 2538 | ``` 2539 | 2540 | ### 场景描述 2541 | 2542 | 我们有一个父任务 `A`, 它启动了三个子任务 `B`、`C` 和 `D`。每个子任务还会进一步启动自己的子任务, 例如,`B` 启动 `E` 和 `F`, `C` 启动 `G`。我们希望通过使用 `context` 来实现以下功能: 2543 | 2544 | 1. 多级嵌套控制 2545 | 2. 控制停止顺序 2546 | 2547 | 顺序 `E->F->B->G->C->D->A` 2548 | 2549 | ### 示例代码 2550 | 2551 | ```go 2552 | package main 2553 | 2554 | import ( 2555 | "context" 2556 | "fmt" 2557 | "sync" 2558 | "time" 2559 | ) 2560 | 2561 | func task(name string, ctx context.Context, wg *sync.WaitGroup) { 2562 | defer wg.Done() 2563 | 2564 | fmt.Printf("Task %s started\n", name) 2565 | 2566 | for { 2567 | select { 2568 | case <-ctx.Done(): 2569 | fmt.Printf("Task %s stopped\n", name) 2570 | return 2571 | default: 2572 | 2573 | time.Sleep(500 * time.Millisecond) 2574 | } 2575 | } 2576 | } 2577 | 2578 | func main() { 2579 | 2580 | ctxA, cancelA := context.WithCancel(context.Background()) 2581 | 2582 | 2583 | ctxB, cancelB := context.WithCancel(ctxA) 2584 | ctxC, cancelC := context.WithCancel(ctxA) 2585 | ctxD, _ := context.WithCancel(ctxA) 2586 | 2587 | ctxE, _ := context.WithCancel(ctxB) 2588 | ctxF, _ := context.WithCancel(ctxB) 2589 | 2590 | ctxG, _ := context.WithCancel(ctxC) 2591 | 2592 | wg := sync.WaitGroup{} 2593 | 2594 | wg.Add(1) 2595 | go task("A", ctxA, &wg) 2596 | 2597 | wg.Add(1) 2598 | go task("B", ctxB, &wg) 2599 | 2600 | wg.Add(1) 2601 | go task("C", ctxC, &wg) 2602 | 2603 | wg.Add(1) 2604 | go task("D", ctxD, &wg) 2605 | 2606 | wg.Add(1) 2607 | go task("E", ctxE, &wg) 2608 | 2609 | wg.Add(1) 2610 | go task("F", ctxF, &wg) 2611 | 2612 | wg.Add(1) 2613 | go task("G", ctxG, &wg) 2614 | 2615 | time.Sleep(2 * time.Second) 2616 | 2617 | cancelB() 2618 | time.Sleep(1 * time.Second) 2619 | 2620 | cancelC() 2621 | time.Sleep(1 * time.Second) 2622 | 2623 | cancelA() 2624 | time.Sleep(1 * time.Second) 2625 | 2626 | wg.Wait() 2627 | fmt.Println("All tasks stopped") 2628 | } 2629 | ``` 2630 | 2631 | 输出 2632 | 2633 | ```bash 2634 | Task B started 2635 | Task C started 2636 | Task D started 2637 | Task E started 2638 | Task F started 2639 | Task G started 2640 | Task E stopped 2641 | Task F stopped 2642 | Task B stopped 2643 | Task G stopped 2644 | Task C stopped 2645 | Task D stopped 2646 | Task A stopped 2647 | All tasks stopped 2648 | ``` 2649 | 2650 | #### 和 gin.Context 的关联 2651 | 2652 | go context 和`gin.Context`有一定关联。 2653 | 2654 | 源码部分: 2655 | 2656 | ```go 2657 | type Context struct { 2658 | writermem responseWriter 2659 | Request *http.Request 2660 | Writer ResponseWriter 2661 | 2662 | Params Params 2663 | handlers HandlersChain 2664 | index int8 2665 | fullPath string 2666 | 2667 | engine *Engine 2668 | params *Params 2669 | skippedNodes *[]skippedNode 2670 | 2671 | // This mutex protects Keys map. 2672 | mu sync.RWMutex 2673 | 2674 | // Keys is a key/value pair exclusively for the context of each request. 2675 | Keys map[string]any 2676 | 2677 | // Errors is a list of errors attached to all the handlers/middlewares who used this context. 2678 | Errors errorMsgs 2679 | 2680 | // Accepted defines a list of manually accepted formats for content negotiation. 2681 | Accepted []string 2682 | 2683 | // queryCache caches the query result from c.Request.URL.Query(). 2684 | queryCache url.Values 2685 | 2686 | // formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH, 2687 | // or PUT body parameters. 2688 | formCache url.Values 2689 | 2690 | // SameSite allows a server to define a cookie attribute making it impossible for 2691 | // the browser to send this cookie along with cross-site requests. 2692 | sameSite http.SameSite 2693 | } 2694 | ``` 2695 | 2696 | 继续阅读: 2697 | 2698 | ```go 2699 | func (c *Context) Deadline() (deadline time.Time, ok bool) { 2700 | if !c.hasRequestContext() { 2701 | return 2702 | } 2703 | return c.Request.Context().Deadline() 2704 | } 2705 | 2706 | // Done returns nil (chan which will wait forever) when c.Request has no Context. 2707 | func (c *Context) Done() <-chan struct{} { 2708 | if !c.hasRequestContext() { 2709 | return nil 2710 | } 2711 | return c.Request.Context().Done() 2712 | } 2713 | 2714 | // Err returns nil when c.Request has no Context. 2715 | func (c *Context) Err() error { 2716 | if !c.hasRequestContext() { 2717 | return nil 2718 | } 2719 | return c.Request.Context().Err() 2720 | } 2721 | 2722 | // Value returns the value associated with this context for key, or nil 2723 | // if no value is associated with key. Successive calls to Value with 2724 | // the same key returns the same result. 2725 | func (c *Context) Value(key any) any { 2726 | if key == ContextRequestKey { 2727 | return c.Request 2728 | } 2729 | if key == ContextKey { 2730 | return c 2731 | } 2732 | if keyAsString, ok := key.(string); ok { 2733 | if val, exists := c.Get(keyAsString); exists { 2734 | return val 2735 | } 2736 | } 2737 | if !c.hasRequestContext() { 2738 | return nil 2739 | } 2740 | return c.Request.Context().Value(key) 2741 | } 2742 | ``` 2743 | 2744 | Gin 框架里的 Context 也是对 Context 接口的实现, 并增加了许多其他信息。 2745 | 2746 | Go-redis的提示: 2747 | 2748 | 最新版本的客户端在操作redis时, 相关函数需要传递上下文(context.Context) 2749 | 2750 | ```bash 2751 | go get github.com/go-redis/redis/v8 2752 | ``` 2753 | 2754 | ## 添加内容 2755 | 2756 | 视频提及到的建议修改内容 2757 | 2758 | ### Yml文件中进行更改 2759 | 2760 | ```yml 2761 | redis: 2762 | addr: localhost:6379 2763 | DB: 0 2764 | Password: "" 2765 | ``` 2766 | 2767 | 空值 `""`或留空 2768 | 2769 | 如: 2770 | 2771 | ```yml 2772 | Password: 2773 | ``` 2774 | 2775 | ## 总结 2776 | 2777 | 以上就是全部内容, 如果有任何问题, 欢迎私信 UP 主反馈! 2778 | 2779 | ## 内容结尾提示 2780 | 2781 | 本教程中全部文字版教程和代码为 B 站: [InkkaPlum 频道](https://space.bilibili.com/290859233) 和知乎: [Inkka Plum](https://www.zhihu.com/people/instead-opt)的相关教程所用, 仅供学习。 2782 | 2783 | 不得二次用于任何机构/个人再次录制 Go / Gin / Gorm / Redis / MySQL / Vue 或其它任何语言, 框架, 架构, 工具等等教程中。 2784 | 2785 | ## 结语 2786 | 2787 | 这是一个比较综合的 Go+Gin+Gorm+Redis+MySQL 教程, Up 顾及到了很多基本概念, 因此没有学习过 Go 语言的朋友亦可以学习此内容, 但是之后可能需要花一定的时间学习 Go 基础, 请观看 Up(B 站 InkkaPlum 频道)的前两期 Go 视频。 2788 | 2789 | 2790 | 2791 | 2792 | 2793 | 此外, 请关注 Up 的 B 站频道和知乎, 并且别忘了一键三连, 当然如果愿意, 欢迎给 Up 充电支持, 您的支持是 Up 前进的动力, 将会鼓励 Up 给各位带来更好的视频。 2794 | 2795 | 同时, 所有课件和代码都在 GitHub 上分享, 如果感到有帮助, 请给一个 Star 并关注 Up 的 Github。 2796 | 2797 | 扩充内容: 之后还会有一个微服务教程, 敬请期待! 2798 | 2799 | Up B 站 InkkaPlum 频道 2800 | 2801 | 2802 | 2803 | Up 知乎 2804 | 2805 | 2806 | 2807 | Up 掘金 2808 | 2809 | 2810 | 2811 | Up GitHub 2812 | 2813 | 2814 | 2815 | 以上 祝学习成功! 2816 | 2817 | Inkka Plum 2818 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web003Gin-01_gingormtutorials 2 | Code and text version of the tutorial on Gin, Gorm, Golang, Vue, Redis, MySQL, InkkaPlumChannel. | InkkaPlum频道的Gin, Gorm, Vue, Redis, MySQL教程的代码和文字版教程。 3 | 4 | 这是[B站InkkaPlum频道](https://space.bilibili.com/290859233), 知乎[Inkka Plum](https://www.zhihu.com/people/instead-opt)的Go+Gin+Gorm+Vue+Redis+MySQL教程对应的项目源码和课件(文字版教程), 具体教程/操作方法请查看文件[Gin感情参考書](Gin感情参考書(课件).md) 5 | 6 | # 注意: 7 | 8 | 觉得有帮助, 请给一个Star, 并且别忘了关注B站频道知乎和GitHub, 给一个三连, 感谢! 9 | 10 | 本教程中全部文字版教程和代码为 B 站: [InkkaPlum 频道](https://space.bilibili.com/290859233) 和知乎: [Inkka Plum](https://www.zhihu.com/people/instead-opt)的相关教程所用, 仅供学习。 11 | 12 | 不得二次用于任何机构/个人再次录制 Go / Gin / Gorm / Redis / MySQL / Vue 或其它任何语言, 框架, 架构, 工具等等教程中。 13 | 14 | 但是非常欢迎修改案例项目或者添加更多功能。这个项目只是实现了基本的功能, 也有很多可扩充、完善的点。 15 | 16 | 此外, 下文有任何问题或者需要改进的点, 请联系 UP 主。 17 | 18 | 以上就是全部内容, 如果有任何问题, 欢迎私信UP主反馈! 19 | 20 | 以上 祝学习成功! 21 | 22 | Inkka Plum 23 | -------------------------------------------------------------------------------- /image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-1.png -------------------------------------------------------------------------------- /image-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-10.png -------------------------------------------------------------------------------- /image-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-11.png -------------------------------------------------------------------------------- /image-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-12.png -------------------------------------------------------------------------------- /image-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-13.png -------------------------------------------------------------------------------- /image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-2.png -------------------------------------------------------------------------------- /image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-3.png -------------------------------------------------------------------------------- /image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-4.png -------------------------------------------------------------------------------- /image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-5.png -------------------------------------------------------------------------------- /image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-6.png -------------------------------------------------------------------------------- /image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-7.png -------------------------------------------------------------------------------- /image-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-8.png -------------------------------------------------------------------------------- /image-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image-9.png -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slumhee/Web003Gin-01_gingormtutorials/1a622ab40b52bf4a031345a0804cd572f1d3ee18/image.png --------------------------------------------------------------------------------