补充mvc

This commit is contained in:
yumaojun03 2025-05-11 12:08:25 +08:00
parent 68861a43f2
commit 048ac6cf39
11 changed files with 424 additions and 1 deletions

View File

@ -1 +1,3 @@
# mvc 功能分层架构
# mvc 功能分层架构
PO, 数据库对象

20
book/v3/config/README.md Normal file
View 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
View 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
}

View 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
View 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)
}

View File

@ -0,0 +1,3 @@
# 控制器
业务处理

View File

@ -0,0 +1,3 @@
# api handlers
HTTP RESTful 接口

180
book/v3/handlers/book.go Normal file
View 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
View 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
View File

@ -0,0 +1 @@
# 数据模型

30
book/v3/models/book.go Normal file
View 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"
}