补充mvc
This commit is contained in:
parent
68861a43f2
commit
048ac6cf39
@ -1 +1,3 @@
|
|||||||
# mvc 功能分层架构
|
# mvc 功能分层架构
|
||||||
|
|
||||||
|
PO, 数据库对象
|
20
book/v3/config/README.md
Normal file
20
book/v3/config/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 程序的配置管理
|
||||||
|
|
||||||
|
|
||||||
|
## 配置的加载
|
||||||
|
```go
|
||||||
|
// 用于加载配置
|
||||||
|
config.LoadConfigFromYaml(yamlConfigFilePath)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 程序内部如何使用配置
|
||||||
|
```go
|
||||||
|
// Get Config --> ConfigObject
|
||||||
|
config.C().MySQL.Host
|
||||||
|
// config.ConfigObjectInstance
|
||||||
|
```
|
||||||
|
|
||||||
|
## 为你的包添加单元测试
|
||||||
|
|
||||||
|
如何验证我们这个包的 业务逻辑是正确
|
||||||
|
|
86
book/v3/config/config.go
Normal file
86
book/v3/config/config.go
Normal file
@ -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
|
||||||
|
}
|
26
book/v3/config/config_test.go
Normal file
26
book/v3/config/config_test.go
Normal file
@ -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())
|
||||||
|
}
|
52
book/v3/config/load.go
Normal file
52
book/v3/config/load.go
Normal file
@ -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)
|
||||||
|
}
|
3
book/v3/controllers/README.md
Normal file
3
book/v3/controllers/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 控制器
|
||||||
|
|
||||||
|
业务处理
|
3
book/v3/handlers/README.md
Normal file
3
book/v3/handlers/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# api handlers
|
||||||
|
|
||||||
|
HTTP RESTful 接口
|
180
book/v3/handlers/book.go
Normal file
180
book/v3/handlers/book.go
Normal file
@ -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")
|
||||||
|
}
|
20
book/v3/main.go
Normal file
20
book/v3/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
1
book/v3/models/README.md
Normal file
1
book/v3/models/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 数据模型
|
30
book/v3/models/book.go
Normal file
30
book/v3/models/book.go
Normal file
@ -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"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user