需求分析
- 后台管理系统
- 商品管理
- 商品列表
- 商品分类
- 品牌管理
- 品牌分类
- 订单管理
- 订单列表
- 用户信息管理
- 用户列表
- 用户地址
- 用户留言
- 轮播图管理
- 电商系统
- 登录页面
- 首页
- 商品搜索
- 商品分类导航
- 轮播图展示
- 推荐商品展示
- 商品详情页
- 商品图片展示
- 商品描述
- 商品规格选择
- 加入购物车
- 购物车
- 商品列表
- 数量调整
- 删除商品
- 结算功能
- 用户中心
- 订单中心
- 我的订单
- 收货地址管理
- 用户信息
- 用户资料修改
- 我的收藏
- 我的留言
单体应用的微服务的演进
- 注册中心 服务发现 配置中心 链路追踪
- 服务网关(路由,服务发现 鉴权 熔断, 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 | 指定 db 列名 |
| 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")
}
// 返回pureJson
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]
if name == "-" {
return ""
}
return name
})
zhT := zh.New()
enT := en.New()
uni := ut.New(enT, zhT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("locale %s not found", locale)
}
switch locale {
case "en":
en_translator.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translator.RegisterDefaultTranslations(v, trans)
default:
en_translator.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println(err.Error())
return
}
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})
})
r.POST("/register", func(c *gin.Context) {
var signUpForm RegisterForm
if err := c.ShouldBind(&signUpForm); err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "注册成功",
})
})
r.Run(":8083")
}
中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func MyLogger() gin.HandlerFunc {
return func(context *gin.Context) {
t := time.Now()
context.Set("example", "12345")
context.Next()
latency := time.Since(t)
fmt.Printf("%s - %s - %s - %sn", context.ClientIP(), context.Request.Method, context.Request.URL, latency)
fmt.Printf("%dn", context.Writer.Status())
}
}
func main() {
//router := gin.New()
// 使用logger中间件
//router.Use(gin.Logger())
// 使用recovery中间件
//router.Use(gin.Recovery())
router := gin.Default()
// 以上是使用default的方式是一样的
authrized := router.Group("/auth")
authrized.Use(MyLogger())
authrized.GET("/ping", func(context *gin.Context) {
example := context.MustGet("example").(string)
context.JSON(http.StatusOK, gin.H{
"message": example,
})
})
router.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.Run(":8083")
}
优雅的退出程序
// 优雅退出,当我们关闭程序的时候应该做的后续处理
// 微服务 启动之前 或者启动之后会做一件事:将当前的服务的 ip 地址和端口号注册到注册中心
// 我们当前的服务停止了以后并没有告知注册中心
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
})
// 创建一个 HTTP 服务器
srv := &http.Server{
Addr: ":8083",
Handler: router,
}
// 启动服务放到协程中
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("listen: %sn", err)
}
}()
// 创建退出通道监听系统信号
quit := make(chan os.Signal, 1)
// 监听中断信号或终止信号
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 等待信号
fmt.Println("正在优雅退出服务器...")
// 创建上下文设置最大超时时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 优雅关闭服务
if err := srv.Shutdown(ctx); err != nil {
fmt.Println("服务器强制关闭:", err)
} else {
fmt.Println("服务器优雅退出")
}
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4774



