diff --git a/book/v3/ README.md b/book/v3/ README.md index 76f1166..4f2dec6 100644 --- a/book/v3/ README.md +++ b/book/v3/ README.md @@ -1 +1,3 @@ -# mvc 功能分层架构 \ No newline at end of file +# mvc 功能分层架构 + +PO, 数据库对象 \ No newline at end of file diff --git a/book/v3/config/README.md b/book/v3/config/README.md new file mode 100644 index 0000000..f3d7d23 --- /dev/null +++ b/book/v3/config/README.md @@ -0,0 +1,20 @@ +# 程序的配置管理 + + +## 配置的加载 +```go +// 用于加载配置 +config.LoadConfigFromYaml(yamlConfigFilePath) +``` + +## 程序内部如何使用配置 +```go +// Get Config --> ConfigObject +config.C().MySQL.Host +// config.ConfigObjectInstance +``` + +## 为你的包添加单元测试 + +如何验证我们这个包的 业务逻辑是正确 + diff --git a/book/v3/config/config.go b/book/v3/config/config.go new file mode 100644 index 0000000..12981aa --- /dev/null +++ b/book/v3/config/config.go @@ -0,0 +1,86 @@ +package config + +import ( + "fmt" + "sync" + + "122.51.31.227/go-course/go18/book/v3/models" + "github.com/infraboard/mcube/v2/tools/pretty" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func Default() *Config { + return &Config{ + Application: &application{ + Host: "127.0.0.1", + Port: 8080, + }, + MySQL: &mySQL{ + Host: "127.0.0.1", + Port: 3306, + DB: "test", + Username: "root", + Password: "123456", + Debug: true, + }, + } +} + +// 这歌对象就是程序配置 +// yaml, toml +type Config struct { + Application *application `toml:"app" yaml:"app" json:"app"` + MySQL *mySQL `toml:"mysql" yaml:"mysql" json:"mysql"` +} + +func (c *Config) String() string { + return pretty.ToJSON(c) +} + +// 应用服务 +type application struct { + Host string `toml:"host" yaml:"host" json:"host" env:"HOST"` + Port int `toml:"port" yaml:"port" json:"port" env:"PORT"` +} + +// db对象也是一个单列模式 +type mySQL struct { + Host string `json:"host" yaml:"host" toml:"host" env:"DATASOURCE_HOST"` + Port int `json:"port" yaml:"port" toml:"port" env:"DATASOURCE_PORT"` + DB string `json:"database" yaml:"database" toml:"database" env:"DATASOURCE_DB"` + Username string `json:"username" yaml:"username" toml:"username" env:"DATASOURCE_USERNAME"` + Password string `json:"password" yaml:"password" toml:"password" env:"DATASOURCE_PASSWORD"` + Debug bool `json:"debug" yaml:"debug" toml:"debug" env:"DATASOURCE_DEBUG"` + + // gorm db对象, 只需要有1个,不运行重复生成 + db *gorm.DB + // 互斥锁 + lock sync.Mutex +} + +func (m *mySQL) GetDB() *gorm.DB { + m.lock.Lock() + defer m.lock.Unlock() + + if m.db == nil { + // 初始化数据库 + // dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local" + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + m.Username, + m.Password, + m.Host, + m.Port, + m.DB, + ) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + db.AutoMigrate(&models.Book{}) // 自动迁移 + m.db = db + } + + return m.db +} diff --git a/book/v3/config/config_test.go b/book/v3/config/config_test.go new file mode 100644 index 0000000..8d36673 --- /dev/null +++ b/book/v3/config/config_test.go @@ -0,0 +1,26 @@ +package config_test + +import ( + "fmt" + "os" + "testing" + + "122.51.31.227/go-course/go18/book/v3/config" +) + +func TestLoadConfigFromYaml(t *testing.T) { + err := config.LoadConfigFromYaml(fmt.Sprintf("%s/book/v2/application.yaml", os.Getenv("workspaceFolder"))) + if err != nil { + t.Fatal(err) + } + t.Log(config.C()) +} + +func TestLoadConfigFromEnv(t *testing.T) { + os.Setenv("DATASOURCE_HOST", "localhost") + err := config.LoadConfigFromEnv() + if err != nil { + t.Fatal(err) + } + t.Log(config.C()) +} diff --git a/book/v3/config/load.go b/book/v3/config/load.go new file mode 100644 index 0000000..8edcf84 --- /dev/null +++ b/book/v3/config/load.go @@ -0,0 +1,52 @@ +package config + +import ( + "os" + + "github.com/caarlos0/env/v6" + "gopkg.in/yaml.v3" + "gorm.io/gorm" +) + +// 配置加载 +// file/env/... ---> Config +// 全局一份 + +// config 全局变量, 通过函数对我提供访问 +var config *Config + +func C() *Config { + // 没有配置文件怎么办? + // 默认配置, 方便开发者 + if config == nil { + config = Default() + } + + return config +} + +func DB() *gorm.DB { + return C().MySQL.GetDB() +} + +// 加载配置 把外部配置读到 config全局变量里面来 +// yaml 文件yaml --> conf +func LoadConfigFromYaml(configPath string) error { + content, err := os.ReadFile(configPath) + if err != nil { + return err + } + + // 默认值 + config = C() + return yaml.Unmarshal(content, config) +} + +// 从环境变量读取配置 +// "github.com/caarlos0/env/v6" +func LoadConfigFromEnv() error { + config = C() + // MYSQL_DB <---> DB + // config.MySQL.DB = os.Getenv("MYSQL_DB") + return env.Parse(config) +} diff --git a/book/v3/controllers/README.md b/book/v3/controllers/README.md new file mode 100644 index 0000000..1b8ac10 --- /dev/null +++ b/book/v3/controllers/README.md @@ -0,0 +1,3 @@ +# 控制器 + +业务处理 diff --git a/book/v3/handlers/README.md b/book/v3/handlers/README.md new file mode 100644 index 0000000..4afc648 --- /dev/null +++ b/book/v3/handlers/README.md @@ -0,0 +1,3 @@ +# api handlers + +HTTP RESTful 接口 \ No newline at end of file diff --git a/book/v3/handlers/book.go b/book/v3/handlers/book.go new file mode 100644 index 0000000..ecbf636 --- /dev/null +++ b/book/v3/handlers/book.go @@ -0,0 +1,180 @@ +package handlers + +import ( + "net/http" + "strconv" + + "122.51.31.227/go-course/go18/book/v3/config" + "122.51.31.227/go-course/go18/book/v3/models" + "github.com/gin-gonic/gin" +) + +var Book = &BookApiHandler{} + +type BookApiHandler struct { +} + +func (h *BookApiHandler) Registry(r gin.IRouter) { + // Book Restful API + // List of books + r.GET("/api/books", h.listBook) + // Create new book + // Body: HTTP Entity + r.POST("/api/books", h.createBook) + // Get book by book number + r.GET("/api/books/:bn", h.getBook) + // Update book + r.PUT("/api/books/:bn", h.updateBook) + // Delete book + r.DELETE("/api/books/:bn", h.deleteBook) +} + +// 实现后端分页的 +func (h *BookApiHandler) listBook(ctx *gin.Context) { + set := &models.BookSet{} + + // List<*Book> + // *Set[T] + // types.New[*Book]() + + // 给默认值 + pn, ps := 1, 20 + // /api/books?page_number=1&page_size=20 + pageNumber := ctx.Query("page_number") + if pageNumber != "" { + pnInt, err := strconv.ParseInt(pageNumber, 10, 64) + if err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + pn = int(pnInt) + } + + pageSize := ctx.Query("page_size") + if pageSize != "" { + psInt, err := strconv.ParseInt(pageSize, 10, 64) + if err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + ps = int(psInt) + } + + query := config.DB().Model(&models.Book{}) + // 关键字过滤 + kws := ctx.Query("keywords") + if kws != "" { + // where title like %kws% + query = query.Where("title LIKE ?", "%"+kws+"%") + } + + // 其他过滤条件 + + // select * from books + // 通过sql的offset limte 来实现分页 + // offset (page_number -1) * page_size, limit page_size + // 2 offset 20, 20 + // 3 offset 40, 20 + // 4 offset 3 * 20, 20 + offset := (pn - 1) * ps + if err := query.Count(&set.Total).Offset(int(offset)).Limit(int(ps)).Find(&set.Items).Error; err != nil { + ctx.JSON(500, gin.H{"code": 500, "message": err.Error()}) + return + } + + // 获取总数, 总共多少个, 总共有多少页 + ctx.JSON(200, set) +} + +func (h *BookApiHandler) createBook(ctx *gin.Context) { + // payload, err := io.ReadAll(ctx.Request.Body) + // if err != nil { + // ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + // return + // } + // defer ctx.Request.Body.Close() + // // {"title": "Go语言"} + + // c.Request.Header.Get(key) + // ctx.GetHeader("Authincation") + + // new(Book) + bookSpecInstance := &models.BookSpec{} + // // 通过JSON的 Struct Tag + // // bookInstance.Title = "Go语言" + // if err := json.Unmarshal(payload, bookInstance); err != nil { + // ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + // return + // } + // 获取到bookInstance + // 参数是不是为空 + if err := ctx.BindJSON(bookSpecInstance); err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + + // 有没有能够检查某个字段是否是必须填 + // Gin 集成 validator这个库, 通过 struct tag validate 来表示这个字段是否允许为空 + // validate:"required" + // 在数据Bind的时候,这个逻辑会自动运行 + // if bookSpecInstance.Author == "" { + // ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + // return + // } + + bookInstance := &models.Book{BookSpec: *bookSpecInstance} + + // 数据入库(Grom), 补充自增Id的值 + if err := config.DB().Save(bookInstance).Error; err != nil { + ctx.JSON(400, gin.H{"code": 500, "message": err.Error()}) + return + } + + // 返回响应 + ctx.JSON(http.StatusCreated, bookInstance) +} + +func (h *BookApiHandler) getBook(ctx *gin.Context) { + bookInstance := &models.Book{} + // 需要从数据库中获取一个对象 + if err := config.DB().Where("id = ?", ctx.Param("bn")).Take(bookInstance).Error; err != nil { + ctx.JSON(400, gin.H{"code": 500, "message": err.Error()}) + return + } + + ctx.JSON(200, bookInstance) +} + +func (h *BookApiHandler) updateBook(ctx *gin.Context) { + bnStr := ctx.Param("bn") + bn, err := strconv.ParseInt(bnStr, 10, 64) + if err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + + // 读取body里面的参数 + bookInstance := &models.Book{ + Id: uint(bn), + } + // 获取到bookInstance + if err := ctx.BindJSON(&bookInstance.BookSpec); err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + + if err := config.DB().Where("id = ?", bookInstance.Id).Updates(bookInstance).Error; err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + + ctx.JSON(200, bookInstance) +} + +func (h *BookApiHandler) deleteBook(ctx *gin.Context) { + if err := config.DB().Where("id = ?", ctx.Param("bn")).Delete(&models.Book{}).Error; err != nil { + ctx.JSON(400, gin.H{"code": 400, "message": err.Error()}) + return + } + ctx.JSON(http.StatusNoContent, "ok") +} diff --git a/book/v3/main.go b/book/v3/main.go new file mode 100644 index 0000000..0bab28f --- /dev/null +++ b/book/v3/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + + "122.51.31.227/go-course/go18/book/v3/handlers" + "github.com/gin-gonic/gin" +) + +func main() { + server := gin.Default() + + handlers.Book.Registry(server) + + if err := server.Run(":8080"); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/book/v3/models/README.md b/book/v3/models/README.md new file mode 100644 index 0000000..0543d93 --- /dev/null +++ b/book/v3/models/README.md @@ -0,0 +1 @@ +# 数据模型 \ No newline at end of file diff --git a/book/v3/models/book.go b/book/v3/models/book.go new file mode 100644 index 0000000..4ccc402 --- /dev/null +++ b/book/v3/models/book.go @@ -0,0 +1,30 @@ +package models + +type BookSet struct { + // 总共多少个 + Total int64 `json:"total"` + // book清单 + Items []*Book `json:"items"` +} + +type Book struct { + // 对象Id + Id uint `json:"id" gorm:"primaryKey;column:id"` + + BookSpec +} + +type BookSpec struct { + // type 用于要使用gorm 来自动创建和更新表的时候 才需要定义 + Title string `json:"title" gorm:"column:title;type:varchar(200)" validate:"required"` + Author string `json:"author" gorm:"column:author;type:varchar(200);index" validate:"required"` + Price float64 `json:"price" gorm:"column:price" validate:"required"` + // bool false + // nil 是零值, false + IsSale *bool `json:"is_sale" gorm:"column:is_sale"` +} + +// books +func (b *Book) TableName() string { + return "books" +}