需求分析
- 後台管理系統
- 商品管理
- 商品列表
- 商品分類
- 品牌管理
- 品牌分類
- 訂單管理
- 訂單列表
- 使用者資訊管理
- 使用者列表
- 使用者地址
- 使用者留言
- 輪播圖管理
- 電商系統
- 登入頁面
- 首頁
- 商品搜尋
- 商品分類導覽
- 輪播圖展示
- 推薦商品展示
- 商品詳情頁
- 商品圖片展示
- 商品描述
- 商品規格選擇
- 加入購物車
- 購物車
- 商品列表
- 數量調整
- 刪除商品
- 結帳功能
- 使用者中心
- 訂單中心
- 我的訂單
- 收件地址管理
- 使用者資訊
- 使用者資料修改
- 我的收藏
- 我的留言
單體應用的微服務演進
- 註冊中心 服務發現 配置中心 鏈路追蹤
- 服務閘道(路由、服務發現、鑑權、熔斷、IP 黑白名單、負載平衡)
介面管理
前後端分離的系統介面管理文件工具 DRF Swagger、YAPI(我覺得 Apifox 更好用一些)
git clone https://github.com/Ryan-Miao/docker-yapi.git
cd docker-yapi
docker-compose up
ORM 學習
1. 什麼是 ORM
ORM 全稱是:Object Relational Mapping(物件關聯對映),其主要作用是在程式設計中,把物件導向的概念跟資料庫中表的概念對應起來。舉例來說就是,我定義一個物件,那就對應著一張表,這個物件的實例,就對應著表中的一條記錄。
2. 常用 ORM
3. ORM 的優缺點
優點
- 提高了開發效率。
- 屏蔽 SQL 細節,可以自動對實體物件(Entity)與資料庫中的表(Table)進行欄位與屬性的對映;不用直接 SQL 編碼。
- 屏蔽各種資料庫之間的差異。
缺點
- ORM 會犧牲程式的執行效率和會固定思維模式。
- 過於依賴 ORM 會導致 SQL 理解不足。
- 對於固定的 ORM 依賴過重,導致切換到其他的 ORM 代價高。
4. 如何正確看待 ORM 和 SQL 之間的關係
- SQL 為主,ORM 為輔。
- ORM 主要目的是為了增加程式碼可維護性和開發效率。
GORM 入門學習
package main
import (
"fmt"
"gorm.io/gorm/logger"
"log"
"os"
"time"
"github.com/bwmarrin/snowflake"
"gopkg.in/yaml.v3"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Config struct {
Database struct {
User string `yaml:"user"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
} `yaml:"database"`
}
var node *snowflake.Node
func init() {
var err error
// 初始化一個 Node (你可以根據不同服務實例,設置不同的node number)
node, err = snowflake.NewNode(1)
if err != nil {
panic(err)
}
}
// Product模型
type Product struct {
Code string `gorm:"primaryKey"`
Price uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// 插入前自動生成Code
func (p *Product) BeforeCreate(tx *gorm.DB) (err error) {
if p.Code == "" {
p.Code = node.Generate().String()
}
return
}
func main() {
// 讀取配置
cfg := loadConfig("config/db/db.yaml")
// URL編碼密碼
//encodedPassword := url.QueryEscape(cfg.Database.Password)
//fmt.Println(encodedPassword)
// 拼接DSN
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Database.User,
cfg.Database.Password,
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.Name,
)
// 設置打印日誌
newLogger := logger.New(
log.New(os.Stdout, "rn", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second * 10,
LogLevel: logger.Info,
Colorful: true,
},
)
// 連接數據庫
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
log.Fatal("failed to connect database:", err)
}
// 設置全局的logger,這個logger在我們執行每個sql語句時都會打印出來
fmt.Println("數據庫連接成功!", db)
// 定義表結構,將表結構直接生成對應的表 自動建表
//db.AutoMigrate(&Product{})
// 創建一條記錄測試
//product := Product{Price: 100}
//result := db.Create(&product)
//if result.Error != nil {
// panic(result.Error)
//}
//
//fmt.Println("Product Created:", product)
// read
var productFind Product
result := db.First(&productFind, "code = ?", "1916479347485577216")
if result.Error != nil {
panic(result.Error)
}
fmt.Println("ProductFind Read:", productFind)
// update
productFind.Price = 300
result = db.Save(&productFind)
// delete 邏輯刪除
db.Delete(&productFind, 1)
}
func loadConfig(path string) Config {
data, err := os.ReadFile(path)
if err != nil {
log.Fatal("failed to read config file:", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
log.Fatal("failed to parse config file:", err)
}
return cfg
}
宣告模型
模型是標準的 struct,由 Go 的基本資料型別、實現了 Scanner 和 Valuer 介面的自訂型別及其指標或別名組成
// 模型定義
type User struct {
ID uint // 主鍵
Name string // 用戶名
Email *string // 郵箱
Age uint8 // 年齡
Birthday *time.Time // 生日
MemberNumber sql.NullString // 會員編號
ActivedAt sql.NullTime // 激活時間
CreatedAt time.Time // 創建時間
UpdatedAt time.Time // 更新時間
}
透過sql.NullString解決 0 或空值不更新的問題
欄位標籤
宣告 model 時,tag 是可選的,GORM 支援以下 tag:tag 大小寫不敏感,但建議使用cameCase風格
| 標籤名 | 說明 |
|---|---|
| column | 指定資料庫欄位名稱 |
| type | 欄位資料型別,推薦使用相容性好的通用型別,例如:所有資料庫都支援 bool、int、uint、float、string、time、bytes,並且可以和其他標籤一起使用,例如:not null、size、autoIncrement。也可以使用資料庫原生型別,但需要是完整的,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT |
| size | 指定欄位大小,例如:size:256 |
| primaryKey | 指定欄位為主鍵 |
| unique | 指定欄位為唯一 |
| default | 指定欄位的預設值 |
| precision | 指定欄位的精度 |
| scale | 指定欄位大小 |
| not null | 指定欄位為 NOT NULL |
| autoIncrement | 指定欄位為自動增長 |
| autoIncrementIncrement | 設定自增步長,控制欄位的自增間隔值 |
| embedded | 將欄位嵌入(embed the field) |
| embeddedPrefix | 為嵌入欄位的欄位名稱添加前綴 |
| autoCreateTime | 建立時記錄當前時間,針對 int 欄位,可使用 nano/milli 單位。例如:autoCreateTime:nano |
| autoUpdateTime | 建立/更新時記錄當前時間,針對 int 欄位,可使用 nano/milli 單位。例如:autoUpdateTime:milli |
| index | 建立索引,可使用相同名稱為多個欄位建立組合索引,參考 Indexes 說明 |
| uniqueIndex | 同 index,但建立唯一索引 |
| check | 建立 Check 約束,例如:check:age > 13,參考 Constraints |
| <- | 設定欄位的寫入權限,例如 <-:create 建立時寫入,<-:update 更新時寫入,<-:false 禁止寫入 |
| -> | 設定欄位的讀取權限,例如 ->:false 禁止讀取 |
| - | 忽略該欄位,不讀寫 |
| comment | 在遷移時為欄位添加註釋 |
查詢
// 第一條記錄,主鍵排序的第一條
db.First
db.Take
db.Last
db.Where("name=?","jinzhu").First(&user) // user指定找哪張表
db.Where(&user{Name:"jinzhu",Age:20}).First(&user)
//主鍵切片
db.Where([]int64{20,21,22}).Find(&users)
Gin(Web 框架)
go get -u github.com/gin-gonic/gin
啟動一個簡單的應用程式
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
// restful 的開發中
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
// 默認啓動的是 8080 端口,也可以通過 router.Run(":端口號") 自定義
router.Run()
}
URL 和路由分組
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run() // listen and serve on
}
URL 中的參數是從 context 中的Param("key")來取得
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Person struct {
ID int `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.GET("/user/:name/:action/", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
c.String(http.StatusOK, "%s is %s", name, action)
})
r.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
c.String(http.StatusOK, "%s is %s", name, action)
})
// 通過struct來約束參數的取值
r.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.Status(http.StatusBadRequest)
}
c.JSON(http.StatusOK, person)
})
r.Run(":8082")
}
取得 GET 和 POST 中的參數
GET
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 匹配的 URL 格式:/welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // 等價於 c.Request.URL.Query().Get("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.POST("/welcome", func(c *gin.Context) {
firstname := c.DefaultPostForm("firstname", "Guest")
lastname := c.PostForm("lastname") // 等價於 c.Request.URL.Query().Get("lastname")
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
router.Run(":8080")
}
POST
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 匹配的 URL 格式:/welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // 等價於 c.Request.URL.Query().Get("lastname")
c.JSON(http.StatusOK, gin.H{
"firstname": firstname,
"lastname": lastname,
})
})
router.POST("/welcome", func(c *gin.Context) {
job := c.DefaultPostForm("job", "Guest")
salary := c.PostForm("salary") // 等價於 c.Request.URL.Query().Get("lastname")
//c.String(http.StatusOK, "Hello %s %s", job, salary)
c.JSON(http.StatusOK, gin.H{
"job": job,
"salary": salary,
})
})
router.Run(":8083")
}
回傳 JSON 和 Protobuf 值
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"GormStart/gin06/proto"
)
func main() {
router := gin.Default()
router.GET("/moreJSON", func(c *gin.Context) {
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "gin"
msg.Message = "hello"
msg.Number = 123
c.JSON(http.StatusOK, msg)
})
//protobuf
router.GET("/moreProtoBuf", func(c *gin.Context) {
c.ProtoBuf(http.StatusOK, &proto.Teacher{
Name: "gin",
Course: []string{"go", "python"},
})
})
router.Run(":8083")
}
// 回傳 Pure JSON
1. 表單的基本驗證
若要將請求主體綁定到結構體中,請使用模型綁定,目前支援 JSON、XML、YAML 和標準表單值(foo=bar&boo=baz)的綁定。
Gin 使用 go-playground/validator 驗證參數,查看完整文件。
需要在綁定的欄位上設定 tag,例如,綁定格式為 JSON,需要這樣設定:json:"fieldname"。
此外,Gin 還提供了兩套綁定方法:
Must bind
- Methods:
Bind、BindJSON、BindXML、BindQuery、BindYAML - Behavior:這些方法底層使用
MustBindWith,如果存在綁定錯誤,請求將被以下指令中止:
go
c.AbortWithError(400, err).SetType(ErrorTypeBind)
- 影響狀態碼會被設定為 400
- Content-Type 被設定為
text/plain; charset=utf-8 -
注意:如果你在此之後設定回應的程式碼,會發出一個警告:
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422 - 如果你希望更好地控制行為,請使用
ShouldBind相關的方法
Should bind
- Methods:
ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML - Behavior:這些方法底層使用
ShouldBindWith,如果存在綁定錯誤,則回傳錯誤,開發人員可以正確處理請求和錯誤。
當我們使用綁定方法時,Gin 會根據 Content-Type 推斷出使用哪種綁定器,如果你確定你綁定的是什麼,
你可以使用 MustBindWith 或者 BindingWith。
你還可以給欄位指定特定規則的修飾符,如果一個欄位用 binding:"required" 修飾,
並且在綁定時該欄位的值為空,那麼將回傳一個錯誤。
範例程式碼(綁定 JSON)
// 綁定為 json
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
type SignUpParam struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}
func main() {
router := gin.Default()
// Example for binding JSON ({"user":"manu", "password":"123"})
}
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translator "github.com/go-playground/validator/v10/translations/en"
zh_translator "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"strings"
)
// 綁定為 json
type LoginForm struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
type RegisterForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"` // 跨字段了
}
var trans ut.Translator
// 翻譯
func InitTrans(locale string) (err error) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4774



