Compare commits

..

No commits in common. "main" and "day01.06" have entirely different histories.

128 changed files with 64 additions and 5604 deletions

16
.vscode/launch.json vendored
View File

@ -1,16 +0,0 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
}
]
}

View File

@ -1,7 +0,0 @@
{
"go.testEnvVars": {
"workspaceFolder": "${workspaceFolder}",
"CONFIG_PATH": "${workspaceFolder}/application.yaml"
},
"go.testEnvFile": "${workspaceFolder}/etc/unit_test.env"
}

View File

@ -7,6 +7,7 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/infraboard/mcube/v2/types"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
@ -65,7 +66,7 @@ func (h *BookApiHandler) ListBook(ctx *gin.Context) {
// List<*Book>
// *Set[T]
// types.New[*Book]()
types.New[*Book]()
// 给默认值
pn, ps := 1, 20
@ -95,7 +96,7 @@ func (h *BookApiHandler) ListBook(ctx *gin.Context) {
kws := ctx.Query("keywords")
if kws != "" {
// where title like %kws%
query = query.Where("title LIKE ?", "%"+kws+"%")
query = query.Where("title LIKE ?", "%"+kws+"%x")
}
// 其他过滤条件

View File

@ -1,10 +0,0 @@
app:
host: 127.0.0.1
port: 8081
mysql:
host: 127.0.0.1
port: 3306
database: test
username: "root"
password: "123456"
debug: true

View File

@ -1,20 +0,0 @@
# 程序的配置管理
## 配置的加载
```go
// 用于加载配置
config.LoadConfigFromYaml(yamlConfigFilePath)
```
## 程序内部如何使用配置
```go
// Get Config --> ConfigObject
config.C().MySQL.Host
// config.ConfigObjectInstance
```
## 为你的包添加单元测试
如何验证我们这个包的 业务逻辑是正确

View File

@ -1,47 +0,0 @@
package config
import "github.com/infraboard/mcube/v2/tools/pretty"
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"`
}

View File

@ -1,26 +0,0 @@
package config_test
import (
"fmt"
"os"
"testing"
"122.51.31.227/go-course/go18/book/v2/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())
}

View File

@ -1,47 +0,0 @@
package config
import (
"os"
"github.com/caarlos0/env/v6"
"gopkg.in/yaml.v3"
)
// 配置加载
// file/env/... ---> Config
// 全局一份
// config 全局变量, 通过函数对我提供访问
var config *Config
func C() *Config {
// 没有配置文件怎么办?
// 默认配置, 方便开发者
if config == nil {
config = Default()
}
return config
}
// 加载配置 把外部配置读到 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

@ -1,111 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"122.51.31.227/go-course/go18/book/v2/config"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// Book 结构体定义
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Author string `json:"author"`
Price float64 `json:"price"`
}
// 初始化数据库
func setupDatabase() *gorm.DB {
mc := config.C().MySQL
// 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",
mc.Username,
mc.Password,
mc.Host,
mc.Port,
mc.DB,
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
db.AutoMigrate(&Book{}) // 自动迁移
return db
}
func main() {
// 加载配置
path := os.Getenv("CONFIG_PATH")
if path == "" {
path = "application.yaml"
}
config.LoadConfigFromYaml(path)
r := gin.Default()
db := setupDatabase()
// 创建书籍
r.POST("/books", func(c *gin.Context) {
var book Book
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Create(&book)
c.JSON(http.StatusCreated, book)
})
// 获取所有书籍
r.GET("/books", func(c *gin.Context) {
var books []Book
db.Find(&books)
c.JSON(http.StatusOK, books)
})
// 根据 ID 获取书籍
r.GET("/books/:id", func(c *gin.Context) {
var book Book
id := c.Param("id")
if err := db.First(&book, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Book not found"})
return
}
c.JSON(http.StatusOK, book)
})
// 更新书籍
r.PUT("/books/:id", func(c *gin.Context) {
var book Book
id := c.Param("id")
if err := db.First(&book, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Book not found"})
return
}
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Save(&book)
c.JSON(http.StatusOK, book)
})
// 删除书籍
r.DELETE("/books/:id", func(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&Book{}, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Book not found"})
return
}
c.JSON(http.StatusNoContent, nil)
})
ac := config.C().Application
r.Run(fmt.Sprintf("%s:%d", ac.Host, ac.Port)) // 启动服务
}

View File

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

View File

@ -1,10 +0,0 @@
app:
host: 127.0.0.1
port: 8080
mysql:
host: 127.0.0.1
port: 3306
database: go18
username: "root"
password: "123456"
debug: true

View File

@ -1,87 +0,0 @@
# 程序的配置管理
## 配置的加载
```go
// 用于加载配置
config.LoadConfigFromYaml(yamlConfigFilePath)
```
## 程序内部如何使用配置
```go
// Get Config --> ConfigObject
config.C().MySQL.Host
// config.ConfigObjectInstance
```
## 为你的包添加单元测试
如何验证我们这个包的 业务逻辑是正确
```go
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())
}
```
## 补充日志配置
```go
// 如果是文件,结合该库使用"gopkg.in/natefinch/lumberjack.v2"
// 自己的作业: 添加日志轮转配置,结合 gopkg.in/natefinch/lumberjack.v2 使用
// 可以参考: https://github.com/infraboard/mcube/blob/master/ioc/config/log/logger.go
type Log struct {
Level zerolog.Level `json:"level" yaml:"level" toml:"level" env:"LOG_LEVEL"`
logger *zerolog.Logger
lock sync.Mutex
}
func (l *Log) SetLogger(logger zerolog.Logger) {
l.logger = &logger
}
func (l *Log) Logger() *zerolog.Logger {
l.lock.Lock()
defer l.lock.Unlock()
if l.logger == nil {
l.SetLogger(zerolog.New(l.ConsoleWriter()).Level(l.Level).With().Caller().Timestamp().Logger())
}
return l.logger
}
func (c *Log) ConsoleWriter() io.Writer {
output := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.NoColor = false
w.TimeFormat = time.RFC3339
})
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%-6s", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("%s", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s:", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
return output
}
```

View File

@ -1,142 +0,0 @@
package config
import (
"fmt"
"io"
"strings"
"sync"
"time"
"122.51.31.227/go-course/go18/book/v3/models"
"github.com/infraboard/mcube/v2/tools/pretty"
"github.com/rs/zerolog"
"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,
},
Log: &Log{
Level: zerolog.DebugLevel,
},
}
}
// 这歌对象就是程序配置
// yaml, toml
type Config struct {
Application *application `toml:"app" yaml:"app" json:"app"`
MySQL *mySQL `toml:"mysql" yaml:"mysql" json:"mysql"`
Log *Log `toml:"log" yaml:"log" json:"log"`
}
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,
)
L().Info().Msgf("Database: %s", 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
}
// 如果是文件,结合该库使用"gopkg.in/natefinch/lumberjack.v2"
// 自己的作业: 添加日志轮转配置,结合 gopkg.in/natefinch/lumberjack.v2 使用
// 可以参考:
type Log struct {
Level zerolog.Level `json:"level" yaml:"level" toml:"level" env:"LOG_LEVEL"`
logger *zerolog.Logger
lock sync.Mutex
}
func (l *Log) SetLogger(logger zerolog.Logger) {
l.logger = &logger
}
func (l *Log) Logger() *zerolog.Logger {
l.lock.Lock()
defer l.lock.Unlock()
if l.logger == nil {
l.SetLogger(zerolog.New(l.ConsoleWriter()).Level(l.Level).With().Caller().Timestamp().Logger())
}
return l.logger
}
func (c *Log) ConsoleWriter() io.Writer {
output := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.NoColor = false
w.TimeFormat = time.RFC3339
})
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%-6s", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("%s", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s:", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
return output
}

View File

@ -1,26 +0,0 @@
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())
}

View File

@ -1,57 +0,0 @@
package config
import (
"os"
"github.com/caarlos0/env/v6"
"github.com/rs/zerolog"
"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()
}
func L() *zerolog.Logger {
return C().Log.Logger()
}
// 加载配置 把外部配置读到 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

@ -1,37 +0,0 @@
# 控制器
业务处理
![](./biz.png)
## 单元测试 (TDD)
```go
func TestGetBook(t *testing.T) {
book, err := controllers.Book.GetBook(context.Background(), controllers.NewGetBookRequest(3))
if err != nil {
t.Fatal(err)
}
t.Log(book)
}
func TestCreateBook(t *testing.T) {
book, err := controllers.Book.CreateBook(context.Background(), &models.BookSpec{
Title: "unit test for go controller obj",
Author: "will",
Price: 99.99,
})
if err != nil {
t.Fatal(err)
}
t.Log(book)
}
func init() {
// 执行配置的加载
err := config.LoadConfigFromYaml(fmt.Sprintf("%s/book/v3/application.yaml", os.Getenv("workspaceFolder")))
if err != nil {
panic(err)
}
}
```

View File

@ -1,60 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="BHjv1J8RDqaP8pKBLOqJ" name="第 1 页">
<mxGraphModel dx="777" dy="395" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="book handlers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="140" y="130" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="7" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="3" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="8" value="接口" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="7">
<mxGeometry x="-0.0238" y="-2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="3" value="comment handlers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="330" y="130" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="5" style="edgeStyle=none;html=1;exitX=0;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="4" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1">
<mxGeometry x="550" y="20" width="40" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" value="..." style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="500" y="130" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="9" value="对外提供的功能" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="260" y="80" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="11" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;controllers&lt;/h1&gt;&lt;p&gt;controllers.Book.GetBook(id) -&amp;gt; book&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="1">
<mxGeometry x="640" y="200" width="180" height="120" as="geometry"/>
</mxCell>
<mxCell id="12" value="Book Controller" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="150" y="340" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="14" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.565;entryY=0.997;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="12" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="19" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="18" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="20" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="18" target="12">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="18" value="Comment Controller" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="330" y="340" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="22" style="edgeStyle=none;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="21" target="6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="21" value="Controllers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="500" y="340" width="120" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

View File

@ -1,88 +0,0 @@
package controllers
import (
"context"
"122.51.31.227/go-course/go18/book/v3/config"
"122.51.31.227/go-course/go18/book/v3/exception"
"122.51.31.227/go-course/go18/book/v3/models"
"122.51.31.227/go-course/go18/skills/ioc"
"gorm.io/gorm"
)
func GetBookService() *BookController {
return ioc.Controller.Get("book_controller").(*BookController)
}
func init() {
ioc.Controller.Registry("book_controller", &BookController{})
}
type BookController struct {
ioc.ObjectImpl
}
func NewGetBookRequest(bookNumber int) *GetBookRequest {
return &GetBookRequest{
BookNumber: bookNumber,
}
}
type GetBookRequest struct {
BookNumber int
// RequestId string
// ...
}
// 核心功能
// ctx: Trace, 支持请求的取消, request_id
// GetBookRequest 为什么要把他封装为1个对象, GetBook(ctx context.Context, BookNumber string), 保证你的接口的签名的兼容性
// BookController.GetBook(, "")
func (c *BookController) GetBook(ctx context.Context, in *GetBookRequest) (*models.Book, error) {
// context.WithValue(ctx, "request_id", 111)
// ctx.Value("request_id")
config.L().Debug().Msgf("get book: %d", in.BookNumber)
bookInstance := &models.Book{}
// 需要从数据库中获取一个对象
if err := config.DB().Where("id = ?", in.BookNumber).Take(bookInstance).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, exception.ErrNotFound("book number: %d not found", in.BookNumber)
}
return nil, err
}
return bookInstance, nil
}
func (c *BookController) CreateBook(ctx context.Context, in *models.BookSpec) (*models.Book, error) {
// 有没有能够检查某个字段是否是必须填
// 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: *in}
// 数据入库(Grom), 补充自增Id的值
if err := config.DB().Save(bookInstance).Error; err != nil {
return nil, err
}
return bookInstance, nil
}
func (c *BookController) UpdateBook() {
// update(obj)
// config.DB().Updates()
}
func (c *BookController) update(ctx context.Context, obj models.Book) error {
// obj.UpdateTime = now()
// config.DB().Updates()
return nil
}

View File

@ -1,40 +0,0 @@
package controllers_test
import (
"context"
"fmt"
"os"
"testing"
"122.51.31.227/go-course/go18/book/v3/config"
"122.51.31.227/go-course/go18/book/v3/controllers"
"122.51.31.227/go-course/go18/book/v3/models"
)
func TestGetBook(t *testing.T) {
book, err := controllers.GetBookService().GetBook(context.Background(), controllers.NewGetBookRequest(3))
if err != nil {
t.Fatal(err)
}
t.Log(book)
}
func TestCreateBook(t *testing.T) {
book, err := controllers.GetBookService().CreateBook(context.Background(), &models.BookSpec{
Title: "unit test for go controller obj",
Author: "will",
Price: 99.99,
})
if err != nil {
t.Fatal(err)
}
t.Log(book)
}
func init() {
// 执行配置的加载
err := config.LoadConfigFromYaml(fmt.Sprintf("%s/book/v3/application.yaml", os.Getenv("workspaceFolder")))
if err != nil {
panic(err)
}
}

View File

@ -1,34 +0,0 @@
package controllers
import (
"context"
"fmt"
"122.51.31.227/go-course/go18/book/v3/models"
)
var Comment = &CommentController{}
type CommentController struct {
}
type AddCommentRequest struct {
BookNumber int
}
func (c *CommentController) AddComment(ctx context.Context, in *AddCommentRequest) (*models.Comment, error) {
// 业务处理的细节
// 多个业务模块 进行交互
book, err := GetBookService().GetBook(ctx, NewGetBookRequest(in.BookNumber))
// if exception.IsApiException(err, exception.CODE_NOT_FOUND) {
// }
if err != nil {
// 获取查询不到报错
return nil, err
}
// 判断book的状态
fmt.Println(book)
return nil, nil
}

View File

@ -1,102 +0,0 @@
# 业务异常
## 异常的定义
```go
// 用于描述业务异常
// 实现自定义异常
// return error
type ApiException struct {
// 业务异常的编码, 50001 表示Token过期
Code int `json:"code"`
// 异常描述信息
Message string `json:"message"`
// 不会出现在Boyd里面, 序列画成JSON, http response 进行set
HttpCode int `json:"-"`
}
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
//
// type error interface {
// Error() string
// }
func (e *ApiException) Error() string {
return e.Message
}
```
## 异常的比对, 对于 Error Code 更加准确
```go
// 通过 Code 来比较错误
func IsApiException(err error, code int) bool {
var apiErr *ApiException
if errors.As(err, &apiErr) {
return apiErr.Code == code
}
return false
}
```
## 内置了一些全局异常,方便快速使用
```go
func ErrServerInternal(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_SERVER_ERROR,
Message: fmt.Sprintf(format, a...),
HttpCode: 500,
}
}
func ErrNotFound(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_NOT_FOUND,
Message: fmt.Sprintf(format, a...),
HttpCode: 404,
}
}
func ErrValidateFailed(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_PARAM_INVALIDATE,
Message: fmt.Sprintf(format, a...),
HttpCode: 400,
}
}
```
## 返回自定义异常
```go
// 需要从数据库中获取一个对象
if err := config.DB().Where("id = ?", in.BookNumber).Take(bookInstance).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, exception.ErrNotFound("book number: %d not found", in.BookNumber)
}
return nil, err
}
```
## 判断自定义异常
```go
if exception.IsApiException(err, exception.CODE_NOT_FOUND) {
// 异常处理逻辑
}
```
## Gin Revovery 结合
```go
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// 自定义异常处理机制
func Recovery() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, err any) {
// 非业务异常
c.JSON(500, NewApiException(500, fmt.Sprintf("%#v", err)))
c.Abort()
})
}
```

View File

@ -1,35 +0,0 @@
package exception
import (
"fmt"
)
const (
CODE_SERVER_ERROR = 5000
CODE_NOT_FOUND = 404
CODE_PARAM_INVALIDATE = 400
)
func ErrServerInternal(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_SERVER_ERROR,
Message: fmt.Sprintf(format, a...),
HttpCode: 500,
}
}
func ErrNotFound(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_NOT_FOUND,
Message: fmt.Sprintf(format, a...),
HttpCode: 404,
}
}
func ErrValidateFailed(format string, a ...any) *ApiException {
return &ApiException{
Code: CODE_PARAM_INVALIDATE,
Message: fmt.Sprintf(format, a...),
HttpCode: 400,
}
}

View File

@ -1,59 +0,0 @@
package exception
import (
"errors"
"github.com/infraboard/mcube/v2/tools/pretty"
)
func NewApiException(code int, message string) *ApiException {
return &ApiException{
Code: code,
Message: message,
}
}
// 用于描述业务异常
// 实现自定义异常
// return error
type ApiException struct {
// 业务异常的编码, 50001 表示Token过期
Code int `json:"code"`
// 异常描述信息
Message string `json:"message"`
// 不会出现在Boyd里面, 序列画成JSON, http response 进行set
HttpCode int `json:"-"`
}
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
//
// type error interface {
// Error() string
// }
func (e *ApiException) Error() string {
return e.Message
}
func (e *ApiException) String() string {
return pretty.ToJSON(e)
}
func (e *ApiException) WithMessage(msg string) *ApiException {
e.Message = msg
return e
}
func (e *ApiException) WithHttpCode(httpCode int) *ApiException {
e.HttpCode = httpCode
return e
}
// 通过 Code 来比较错误
func IsApiException(err error, code int) bool {
var apiErr *ApiException
if errors.As(err, &apiErr) {
return apiErr.Code == code
}
return false
}

View File

@ -1,24 +0,0 @@
package exception_test
import (
"testing"
"122.51.31.227/go-course/go18/book/v3/exception"
)
func CheckIsError() error {
return exception.ErrNotFound("book %d not found", 1)
}
func TestException(t *testing.T) {
err := CheckIsError()
t.Log(err)
// 怎么获取ErrorCode, 断言这个接口的对象的具体类型
if v, ok := err.(*exception.ApiException); ok {
t.Log(v.Code)
t.Log(v.String())
}
t.Log(exception.IsApiException(err, exception.CODE_NOT_FOUND))
}

View File

@ -1,17 +0,0 @@
package exception
import (
"fmt"
"github.com/gin-gonic/gin"
)
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// 自定义异常处理机制
func Recovery() gin.HandlerFunc {
return gin.CustomRecovery(func(c *gin.Context, err any) {
// 非业务异常
c.JSON(500, NewApiException(500, fmt.Sprintf("%#v", err)))
c.Abort()
})
}

View File

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

View File

@ -1,176 +0,0 @@
package handlers
import (
"strconv"
"122.51.31.227/go-course/go18/book/v3/config"
"122.51.31.227/go-course/go18/book/v3/controllers"
"122.51.31.227/go-course/go18/book/v3/models"
"122.51.31.227/go-course/go18/book/v3/response"
"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 {
response.Failed(ctx, err)
return
}
pn = int(pnInt)
}
pageSize := ctx.Query("page_size")
if pageSize != "" {
psInt, err := strconv.ParseInt(pageSize, 10, 64)
if err != nil {
response.Failed(ctx, err)
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 {
response.Failed(ctx, err)
return
}
// 获取总数, 总共多少个, 总共有多少页
response.OK(ctx, 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 {
response.Failed(ctx, err)
return
}
book, err := controllers.GetBookService().CreateBook(ctx.Request.Context(), bookSpecInstance)
if err != nil {
response.Failed(ctx, err)
return
}
// 返回响应
response.OK(ctx, book)
}
func (h *BookApiHandler) getBook(ctx *gin.Context) {
bnInt, err := strconv.ParseInt(ctx.Param("bn"), 10, 64)
if err != nil {
response.Failed(ctx, err)
return
}
book, err := controllers.GetBookService().GetBook(ctx, controllers.NewGetBookRequest(int(bnInt)))
if err != nil {
response.Failed(ctx, err)
return
}
response.OK(ctx, book)
}
func (h *BookApiHandler) updateBook(ctx *gin.Context) {
bnStr := ctx.Param("bn")
bn, err := strconv.ParseInt(bnStr, 10, 64)
if err != nil {
response.Failed(ctx, err)
return
}
// 读取body里面的参数
bookInstance := &models.Book{
Id: uint(bn),
}
// 获取到bookInstance
if err := ctx.BindJSON(&bookInstance.BookSpec); err != nil {
response.Failed(ctx, err)
return
}
if err := config.DB().Where("id = ?", bookInstance.Id).Updates(bookInstance).Error; err != nil {
response.Failed(ctx, err)
return
}
response.OK(ctx, bookInstance)
}
func (h *BookApiHandler) deleteBook(ctx *gin.Context) {
if err := config.DB().Where("id = ?", ctx.Param("bn")).Delete(&models.Book{}).Error; err != nil {
response.Failed(ctx, err)
return
}
response.OK(ctx, "ok")
}

View File

@ -1,16 +0,0 @@
package handlers
import (
"github.com/gin-gonic/gin"
)
var Comment = &CommentApiHandler{}
type CommentApiHandler struct {
}
func (h *CommentApiHandler) AddComment(ctx *gin.Context) {
// Book.getBook()
// Book.GetBook(id) -> BookInstance
// controllers.Book.GetBook(ctx, controllers.NewGetBookRequest(ctx.Param("bn")))
}

View File

@ -1 +0,0 @@
package handlers

View File

@ -1,32 +0,0 @@
package main
import (
"fmt"
"os"
"122.51.31.227/go-course/go18/book/v3/config"
"122.51.31.227/go-course/go18/book/v3/exception"
"122.51.31.227/go-course/go18/book/v3/handlers"
"github.com/gin-gonic/gin"
)
func main() {
// 加载配置
path := os.Getenv("CONFIG_PATH")
if path == "" {
path = "application.yaml"
}
config.LoadConfigFromYaml(path)
server := gin.New()
server.Use(gin.Logger(), exception.Recovery())
handlers.Book.Registry(server)
ac := config.C().Application
// 启动服务
if err := server.Run(fmt.Sprintf("%s:%d", ac.Host, ac.Port)); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

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

View File

@ -1,36 +0,0 @@
package models
import "github.com/infraboard/mcube/v2/tools/pretty"
type BookSet struct {
// 总共多少个
Total int64 `json:"total"`
// book清单
Items []*Book `json:"items"`
}
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"`
}
type Book struct {
// 对象Id
Id uint `json:"id" gorm:"primaryKey;column:id"`
BookSpec
}
func (b *Book) String() string {
return pretty.ToJSON(b)
}
// books
func (b *Book) TableName() string {
return "books"
}

View File

@ -1,4 +0,0 @@
package models
type Comment struct {
}

View File

@ -1,37 +0,0 @@
package response
import (
"122.51.31.227/go-course/go18/book/v3/exception"
"github.com/gin-gonic/gin"
)
// 当前请求成功的时候,我们应用返回的数据
// 1. {code: 0, data: {}}
// 2. 正常直接返回数据, Restful接口 怎么知道这些请求是成功还是失败喃? 通过HTTP判断 2xx
// 如果后面 所有的返回数据 要进过特殊处理,都在这个函数内进行扩展,方便维护,比如 数据脱敏
func OK(ctx *gin.Context, data any) {
// v, ok := data.(Densener)
ctx.JSON(200, data)
ctx.Abort()
}
// 当前请求失败的时候, 我们返回的数据格式
// 1. {code: xxxx, data: null, message: "错误信息"}
// 请求HTTP Code 非 2xx 就返回我们自定义的异常
//
// {
// "code": 404,
// "message": "book 1 not found"
// }
func Failed(ctx *gin.Context, err error) {
// 一种是我们自己的业务异常
if e, ok := err.(*exception.ApiException); ok {
ctx.JSON(e.HttpCode, e)
ctx.Abort()
return
}
// 非业务异常
ctx.JSON(500, exception.NewApiException(500, err.Error()))
ctx.Abort()
}

View File

@ -1,28 +1 @@
# 业务分区架构(基于mcube)
mcube 与 Ioc
![业务分区架构](image.png)
更新部分:
1. 单元测试 支持通过环境变量注入,优化单元测试配置,共用一套配置
2. 新增Book Api项目, 从简单的脚本开发->配置分离->mvc模式->ioc业务分区 经历4个版本讲解如何开发复杂项目。
3. Vblog项目 新增部署支持2中部署模式1.前后端分离部署 与 前后端打包一体的部署。
4. 优化其他几个项目,支持 可以通过 import的方式快速使用。
5. cmdb 云商凭证 支持加密存储
## 业务分区的第一步 定义业务(RPC)
Book/Comment: 这个业务模块提供的功能
## 具体实现
## 面向接口
# 业务分区架构(基于mcube)

View File

@ -1,23 +0,0 @@
[app]
name = "simple_api"
description = "app desc"
address = "localhost"
encrypt_key = "defualt app encrypt key"
[datasource]
provider = "mysql"
host = "127.0.0.1"
port = 3306
database = "go18"
username = "root"
password = "123456"
auto_migrate = false
debug = false
[http]
host = "127.0.0.1"
port = 8010
path_prefix = "api"
[comment]
max_comment_per_book = 200

View File

@ -1 +0,0 @@
# 业务分区

View File

@ -1,322 +0,0 @@
# Book 业务分区
## 定义Book业务逻辑
业务功能: CRUD
1. 创建书籍(录入)
2. Book列表查询
3. Book详情查询
4. Book更新
5. Book删除
通过Go语言的里面的接口 来定义描述业务功能
```go
// book.Service, Book的业务定义
type Service interface {
// 1. 创建书籍(录入)
CreateBook(context.Context, *CreateBookRequest) (*Book, error)
// 2. Book列表查询
QueryBook(context.Context, *QueryBookRequest) (*types.Set[*Book], error)
// 3. Book详情查询
DescribeBook(context.Context, *DescribeBookRequest) (*Book, error)
// 4. Book更新
UpdateBook(context.Context, *UpdateBookRequest) (*Book, error)
// 5. Book删除
DeleteBook(context.Context, *DeleteBookRequest) error
}
```
## 业务的具体实现(TDD Test Drive Develop)
1. BookServiceImpl
```go
// CreateBook implements book.Service.
func (b *BookServiceImpl) CreateBook(context.Context, *book.CreateBookRequest) (*book.Book, error) {
panic("unimplemented")
}
// DeleteBook implements book.Service.
func (b *BookServiceImpl) DeleteBook(context.Context, *book.DeleteBookRequest) error {
panic("unimplemented")
}
// DescribeBook implements book.Service.
func (b *BookServiceImpl) DescribeBook(context.Context, *book.DescribeBookRequest) (*book.Book, error) {
panic("unimplemented")
}
// QueryBook implements book.Service.
func (b *BookServiceImpl) QueryBook(context.Context, *book.QueryBookRequest) (*types.Set[*book.Book], error) {
panic("unimplemented")
}
// UpdateBook implements book.Service.
func (b *BookServiceImpl) UpdateBook(context.Context, *book.UpdateBookRequest) (*book.Book, error) {
panic("unimplemented")
}
```
## 编写单元测试
```go
func TestCreateBook(t *testing.T) {
req := book.NewCreateBookRequest()
req.SetIsSale(true)
req.Title = "Go语言V4"
req.Author = "will"
req.Price = 10
ins, err := svc.CreateBook(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(ins)
}
```
## 业务对象注册(ioc controller)
手动维护
```sh
pkg gloab
bookContrller = xxx
commentContrller = xx
...
```
通过容器来维护对象
```go
// Book业务的具体实现
type BookServiceImpl struct {
ioc.ObjectImpl
}
// 返回对象的名称, 因此我需要 服务名称
// 当前的MySQLBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
// 当前的MongoDBBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
func (s *BookServiceImpl) Name() string {
return book.APP_NAME
}
func init() {
ioc.Controller().Registry(&BookServiceImpl{})
}
```
## 面向接口
对象取处理, 断言他满足业务接口,然后我们以接口的方式来使用
```go
func GetService() Service {
return ioc.Controller().Get(APP_NAME).(Service)
}
const (
APP_NAME = "book"
)
```
第三方模块,可以依赖 接口进行开发
```go
// AddComment implements comment.Service.
func (c *CommentServiceImpl) AddComment(ctx context.Context, in *comment.AddCommentRequest) (*comment.Comment, error) {
// 能不能 直接Book Service的具体实现
// (&impl.BookServiceImpl{}).DescribeBook(ctx, nil)
// 依赖接口,面向接口编程, 不依赖具体实现
book.GetService().DescribeBook(ctx, nil)
panic("unimplemented")
}
```
## 开发API
接口是需求, 对业务进行设计, 可以选择把这些能力 以那种接口的访问对外提供服务
1. 不对外提供接口,仅仅作为其他的业务的依赖
2. (API)对外提供 HTTP接口, RESTful接口
3. (API)对内提供 RPC接口(JSON RPC/GRPC/thrift)
1. 开发业务功能
```go
func (h *BookApiHandler) createBook(ctx *gin.Context) {
req := book.NewCreateBookRequest()
if err := ctx.BindJSON(req); err != nil {
response.Failed(ctx, err)
return
}
ins, err := h.svc.CreateBook(ctx.Request.Context(), req)
if err != nil {
response.Failed(ctx, err)
return
}
// 返回响应
response.Success(ctx, ins)
}
```
2. 注册路由
```go
type BookApiHandler struct {
ioc.ObjectImpl
// 业务依赖
svc book.Service
}
func (h *BookApiHandler) Name() string {
return "books"
}
// 对象的初始化, 初始化对象的一些熟悉 &BookApiHandler{}
// 构造函数
// 当你这个对象初始化的时候,直接把的处理函数(ApiHandler注册给Server)
func (h *BookApiHandler) Init() error {
h.svc = book.GetService()
r := ioc_gin.ObjectRouter(h)
r.GET("", h.queryBook)
r.POST("", h.createBook)
return nil
}
func init() {
ioc.Api().Registry(&BookApiHandler{})
}
```
## 业务注册
每写完一个业务,就需要在 注册到ioc(注册表)
```go
// 业务加载区, 选择性的价值的业务处理对象
import (
// Api Impl
_ "122.51.31.227/go-course/go18/book/v4/apps/book/api"
// Service Impl
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
)
```
## 启动服务
```go
import (
"github.com/infraboard/mcube/v2/ioc/server/cmd"
// 业务对象
_ "122.51.31.227/go-course/go18/book/v4/apps"
)
func main() {
// ioc框架 加载对象, 注入对象, 配置对象
// server.Gin.Run()
// application.Get().AppName
// http.Get().Host
// server.DefaultConfig.ConfigFile.Enabled = true
// server.DefaultConfig.ConfigFile.Path = "application.toml"
// server.Run(context.Background())
// 不能指定配置文件逻辑
// 使用者来说,体验不佳
// ioc 直接提供server, 直接run就行了
// mcube 包含 一个 gin Engine
// CLI, start 指令 -f 指定配置文件
cmd.Start()
}
```
```toml
[app]
name = "simple_api"
description = "app desc"
address = "localhost"
encrypt_key = "defualt app encrypt key"
[datasource]
provider = "mysql"
host = "127.0.0.1"
port = 3306
database = "go18"
username = "root"
password = "123456"
auto_migrate = false
debug = false
[http]
host = "127.0.0.1"
port = 8010
path_prefix = "api"
[comment]
max_comment_per_book = 200
```
```sh
➜ v4 git:(main) ✗ go run main.go -f application.toml start
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
2025-05-25T15:59:33+08:00 INFO config/gin/framework.go:41 > enable gin recovery component:GIN_WEBFRAMEWORK
200
[GIN-debug] GET /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).queryBook-fm (4 handlers)
[GIN-debug] POST /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).createBook-fm (4 handlers)
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:74 > loaded configs: [app.v1 trace.v1 log.v1 validator.v1 gin_webframework.v1 datasource.v1 grpc.v1 http.v1] component:SERVER
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:75 > loaded controllers: [comment.v1 book.v1] component:SERVER
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:76 > loaded apis: [books.v1] component:SERVER
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:77 > loaded defaults: [] component:SERVER
2025-05-25T15:59:33+08:00 INFO config/http/http.go:144 > HTTP服务启动成功, 监听地址: 127.0.0.1:8010 component:HTTP
```
## 总结
业务分区框架, 我们专注于业务对象的开发, mcube相对于一个工具箱承接其他非业务的公共功能
## 其他非功能需求
工具箱 提供很多工具,开箱即用, 比如health check, 比如metrics
```go
// 健康检查
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
// metrics
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
```
[metric.v1 books.v1 health.v1] metric, health 使用注入的对象
```sh
➜ v4 git:(main) ✗ go run main.go -f application.toml start
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
2025-05-25T16:06:42+08:00 INFO config/gin/framework.go:41 > enable gin recovery component:GIN_WEBFRAMEWORK
200
[GIN-debug] GET /metrics/ --> github.com/infraboard/mcube/v2/ioc/apps/metric/gin.(*ginHandler).Registry.func1 (5 handlers)
2025-05-25T16:06:42+08:00 INFO metric/gin/metric.go:89 > Get the Metric using http://127.0.0.1:8010/metrics component:METRIC
[GIN-debug] GET /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).queryBook-fm (5 handlers)
[GIN-debug] POST /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).createBook-fm (5 handlers)
[GIN-debug] GET /healthz/ --> github.com/infraboard/mcube/v2/ioc/apps/health/gin.(*HealthChecker).HealthHandleFunc-fm (5 handlers)
2025-05-25T16:06:42+08:00 INFO health/gin/check.go:55 > Get the Health using http://127.0.0.1:8010/healthz component:HEALTH_CHECK
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:74 > loaded configs: [app.v1 trace.v1 log.v1 validator.v1 gin_webframework.v1 datasource.v1 grpc.v1 http.v1] component:SERVER
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:75 > loaded controllers: [comment.v1 book.v1] component:SERVER
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:76 > loaded apis: [metric.v1 books.v1 health.v1] component:SERVER
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:77 > loaded defaults: [] component:SERVER
2025-05-25T16:06:42+08:00 INFO config/http/http.go:144 > HTTP服务启动成功, 监听地址: 127.0.0.1:8010 component:HTTP
```

View File

@ -1,55 +0,0 @@
package api
import (
"122.51.31.227/go-course/go18/book/v4/apps/book"
"github.com/infraboard/mcube/v2/ioc"
// 引入Gin Root Router: *gin.Engine
ioc_gin "github.com/infraboard/mcube/v2/ioc/config/gin"
// 引入Gin Root Router: *gin.Engine
)
type BookApiHandler struct {
ioc.ObjectImpl
// 业务依赖
svc book.Service
}
// 这个就是 API 的资源名称
// /api/book/v1/books
func (h *BookApiHandler) Name() string {
return "books"
}
// 对象的初始化, 初始化对象的一些熟悉 &BookApiHandler{}
// 构造函数
// 当你这个对象初始化的时候,直接把的处理函数(ApiHandler注册给Server)
func (h *BookApiHandler) Init() error {
h.svc = book.GetService()
// 本地依赖
// r := server.Gin
// 框架托管, 通过容器获取 Server对象
// 获取的 Gin Engine对象
// ioc_gin.RootRouter()
// URL 容器冲突, book/comment
// 怎么避免 2个业务API 不不冲突,加上业务板块的前缀,或者 对的名称
// /<prefix>/<service_name>/<object_version>/<object_name>
// http 接口前缀
r := ioc_gin.ObjectRouter(h)
// Book Restful API
// List of books
// /api/simple_api/v1/books
r.GET("", h.queryBook)
// Create new book
// Body: HTTP Entity
// /api/simple_api/v1/books
r.POST("", h.createBook)
return nil
}
func init() {
ioc.Api().Registry(&BookApiHandler{})
}

View File

@ -1,61 +0,0 @@
package api
import (
"strconv"
"122.51.31.227/go-course/go18/book/v4/apps/book"
"github.com/gin-gonic/gin"
"github.com/infraboard/mcube/v2/http/gin/response"
)
func (h *BookApiHandler) queryBook(ctx *gin.Context) {
// 给默认值
req := book.NewQueryBookRequest()
req.Keywords = ctx.Query("keywords")
// /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 {
response.Failed(ctx, err)
return
}
req.PageNumber = uint64(pnInt)
}
pageSize := ctx.Query("page_size")
if pageSize != "" {
psInt, err := strconv.ParseInt(pageSize, 10, 64)
if err != nil {
response.Failed(ctx, err)
return
}
req.PageSize = uint64(psInt)
}
set, err := h.svc.QueryBook(ctx.Request.Context(), req)
if err != nil {
// 针对Response的统一封装, 已经落到 mcube
response.Failed(ctx, err)
return
}
response.Success(ctx, set)
}
func (h *BookApiHandler) createBook(ctx *gin.Context) {
req := book.NewCreateBookRequest()
if err := ctx.BindJSON(req); err != nil {
response.Failed(ctx, err)
return
}
ins, err := h.svc.CreateBook(ctx.Request.Context(), req)
if err != nil {
response.Failed(ctx, err)
return
}
// 返回响应
response.Success(ctx, ins)
}

View File

@ -1,70 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="j5aaZ-Vtlo4HorS1MuuZ" name="第 1 页">
<mxGraphModel dx="1657" dy="1468" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="3" value="BookServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="70" y="210" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="4" value="CommentServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="250" y="210" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="5" value="读取配置&lt;div&gt;调用Init&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="6" target="12">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="70" y="-90" width="460" height="240" as="geometry"/>
</mxCell>
<mxCell id="7" value="Controllers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="150" y="65" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="8" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="9" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
<mxGeometry x="-0.3065" y="1" relative="1" as="geometry">
<mxPoint y="1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="10" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="10">
<mxGeometry x="0.1423" y="-4" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="12" value="application.toml" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="610" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="13" value="Config" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="384" y="65" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="14" value="面向接口" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="140" y="300" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="15" value="面向具体对象" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="430" y="300" width="80" height="30" as="geometry"/>
</mxCell>
<mxCell id="16" value="Api" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="240" y="-60" width="160" height="80" as="geometry"/>
</mxCell>
<mxCell id="18" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="17" target="16">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="19" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="17" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="21" value="依赖Book Service" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="19">
<mxGeometry x="-0.0899" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="17" value="BookApiHandler" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-90" y="-50" width="120" height="60" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -1,3 +0,0 @@
# 业务实现包
ServiceImpl(book.Service)

View File

@ -1,68 +0,0 @@
package impl
import (
"context"
"122.51.31.227/go-course/go18/book/v4/apps/book"
"github.com/infraboard/mcube/v2/exception"
"github.com/infraboard/mcube/v2/types"
// 自动解析配置文件里面, 相应的部分
"github.com/infraboard/mcube/v2/ioc/config/datasource"
)
// CreateBook implements book.Service.
func (b *BookServiceImpl) CreateBook(ctx context.Context, in *book.CreateBookRequest) (*book.Book, error) {
// 自定义异常改造, 放到mcube
// 自定义异常, exception 包, 统一放到一个公共库里面, mcube
if err := in.Validate(); err != nil {
return nil, exception.NewBadRequest("校验Book创建失败, %s", err)
}
bookInstance := &book.Book{CreateBookRequest: *in}
// config对象改造
// 数据入库(Grom), 补充自增Id的值
if err := datasource.DBFromCtx(ctx).Save(bookInstance).Error; err != nil {
return nil, err
}
return bookInstance, nil
}
// DeleteBook implements book.Service.
func (b *BookServiceImpl) DeleteBook(context.Context, *book.DeleteBookRequest) error {
panic("unimplemented")
}
// DescribeBook implements book.Service.
func (b *BookServiceImpl) DescribeBook(context.Context, *book.DescribeBookRequest) (*book.Book, error) {
panic("unimplemented")
}
// QueryBook implements book.Service.
func (b *BookServiceImpl) QueryBook(ctx context.Context, in *book.QueryBookRequest) (*types.Set[*book.Book], error) {
set := types.New[*book.Book]()
query := datasource.DBFromCtx(ctx).Model(&book.Book{})
// 关键字过滤
if in.Keywords != "" {
query = query.Where("title LIKE ?", "%"+in.Keywords+"%")
}
if err := query.
Count(&set.Total).
Offset(int(in.ComputeOffset())).
Limit(int(in.PageSize)).
Find(&set.Items).
Error; err != nil {
return nil, err
}
return set, nil
}
// UpdateBook implements book.Service.
func (b *BookServiceImpl) UpdateBook(context.Context, *book.UpdateBookRequest) (*book.Book, error) {
panic("unimplemented")
}

View File

@ -1,29 +0,0 @@
package impl_test
import (
"testing"
"122.51.31.227/go-course/go18/book/v4/apps/book"
)
func TestCreateBook(t *testing.T) {
req := book.NewCreateBookRequest()
req.SetIsSale(true)
req.Title = "Go语言V4"
req.Author = "will"
req.Price = 10
ins, err := svc.CreateBook(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(ins)
}
func TestQueryBook(t *testing.T) {
req := book.NewQueryBookRequest()
ins, err := svc.QueryBook(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(ins)
}

View File

@ -1,35 +0,0 @@
package impl
import (
"122.51.31.227/go-course/go18/book/v4/apps/book"
"github.com/infraboard/mcube/v2/ioc"
)
// 写好一个业务对象(业务实现),就把这个对象,注册到一个公共空间(ioc Controller Namespace)
// mcube 提供这个空间 ioc.Controller().Registry 把对象注册过去
// 提供对象的名称, 对象的初始化方法
// 怎么知道他有没有实现该业务, 可以通过类型约束
// var _ book.Service = &BookServiceImpl{}
// &BookServiceImpl 的 nil对象
//
// int64(1) int64 1
// *BookServiceImpl(nil)
var _ book.Service = (*BookServiceImpl)(nil)
// Book业务的具体实现
type BookServiceImpl struct {
ioc.ObjectImpl
}
// 返回对象的名称, 因此我需要 服务名称
// 当前的MySQLBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
// 当前的MongoDBBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
func (s *BookServiceImpl) Name() string {
return book.APP_NAME
}
func init() {
ioc.Controller().Registry(&BookServiceImpl{})
}

View File

@ -1,17 +0,0 @@
package impl_test
import (
"context"
"122.51.31.227/go-course/go18/book/v4/apps/book"
"122.51.31.227/go-course/go18/book/v4/test"
)
var ctx = context.Background()
var svc book.Service
func init() {
test.DevelopmentSet()
svc = book.GetService()
}

View File

@ -1,108 +0,0 @@
package book
import (
"context"
"github.com/infraboard/mcube/v2/http/request"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/ioc/config/validator"
"github.com/infraboard/mcube/v2/types"
)
func GetService() Service {
return ioc.Controller().Get(APP_NAME).(Service)
}
const (
APP_NAME = "book"
)
// book.Service, Book的业务定义
type Service interface {
// 1. 创建书籍(录入)
CreateBook(context.Context, *CreateBookRequest) (*Book, error)
// 2. Book列表查询
QueryBook(context.Context, *QueryBookRequest) (*types.Set[*Book], error)
// 3. Book详情查询
DescribeBook(context.Context, *DescribeBookRequest) (*Book, error)
// 4. Book更新
UpdateBook(context.Context, *UpdateBookRequest) (*Book, error)
// 5. Book删除
DeleteBook(context.Context, *DeleteBookRequest) error
}
type DeleteBookRequest struct {
DescribeBookRequest
}
type UpdateBookRequest struct {
DescribeBookRequest
CreateBookRequest
}
type DescribeBookRequest struct {
Id uint
}
type BookSet struct {
// 总共多少个
Total int64 `json:"total"`
// book清单
Items []*Book `json:"items"`
}
func (b *BookSet) Add(item *Book) {
b.Items = append(b.Items, item)
}
// type CommentSet struct {
// // 总共多少个
// Total int64 `json:"total"`
// // book清单
// Items []*Comment `json:"items"`
// }
// func (b *CommentSet) Add(item *Comment) {
// b.Items = append(b.Items, item)
// }
func NewCreateBookRequest() *CreateBookRequest {
return (&CreateBookRequest{}).SetIsSale(false)
}
type CreateBookRequest 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"`
}
// 这个请求对象的教育
func (r *CreateBookRequest) Validate() error {
// validate := validator.New()
// validate.Struct(r)
return validator.Validate(r)
}
func (r *CreateBookRequest) SetIsSale(v bool) *CreateBookRequest {
r.IsSale = &v
return r
}
func NewQueryBookRequest() *QueryBookRequest {
return &QueryBookRequest{
// PageRequest{PageSize:20, PageNumber: 1}
PageRequest: *request.NewDefaultPageRequest(),
}
}
type QueryBookRequest struct {
// PageSize uint
// PageNumber uint
request.PageRequest
// 关键字参数
Keywords string `json:"keywords"`
}

View File

@ -1,19 +0,0 @@
package book
import "github.com/infraboard/mcube/v2/tools/pretty"
type Book struct {
// 对象Id
Id uint `json:"id" gorm:"primaryKey;column:id"`
CreateBookRequest
}
func (b *Book) String() string {
return pretty.ToJSON(b)
}
// books
func (b *Book) TableName() string {
return "books"
}

View File

@ -1 +0,0 @@
# 评论模块

View File

@ -1,17 +0,0 @@
package impl
import (
"context"
"122.51.31.227/go-course/go18/book/v4/apps/book"
"122.51.31.227/go-course/go18/book/v4/apps/comment"
)
// AddComment implements comment.Service.
func (c *CommentServiceImpl) AddComment(ctx context.Context, in *comment.AddCommentRequest) (*comment.Comment, error) {
// 能不能 直接Book Service的具体实现
// (&impl.BookServiceImpl{}).DescribeBook(ctx, nil)
// 依赖接口,面向接口编程, 不依赖具体实现
book.GetService().DescribeBook(ctx, nil)
panic("unimplemented")
}

View File

@ -1,18 +0,0 @@
package impl_test
import (
"testing"
"122.51.31.227/go-course/go18/book/v4/apps/comment"
)
func TestAddComment(t *testing.T) {
ins, err := svc.AddComment(ctx, &comment.AddCommentRequest{
BookId: 10,
Comment: "评论测试",
})
if err != nil {
t.Fatal(err)
}
t.Log(ins)
}

View File

@ -1,41 +0,0 @@
package impl
import (
"fmt"
"122.51.31.227/go-course/go18/book/v4/apps/comment"
"github.com/infraboard/mcube/v2/ioc"
)
func init() {
ioc.Controller().Registry(&CommentServiceImpl{
MaxCommentPerBook: 100,
})
}
// 怎么知道他有没有实现该业务, 可以通过类型约束
// var _ book.Service = &BookServiceImpl{}
// &BookServiceImpl 的 nil对象
//
// int64(1) int64 1
// *BookServiceImpl(nil)
var _ comment.Service = (*CommentServiceImpl)(nil)
// Book业务的具体实现
type CommentServiceImpl struct {
ioc.ObjectImpl
// Comment最大限制
MaxCommentPerBook int `toml:"max_comment_per_book"`
}
func (s *CommentServiceImpl) Init() error {
// 当前对象,已经读取了配置文件
fmt.Println(s.MaxCommentPerBook)
return nil
}
func (s *CommentServiceImpl) Name() string {
return comment.APP_NAME
}

View File

@ -1,18 +0,0 @@
package impl_test
import (
"context"
"os"
"122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
"github.com/infraboard/mcube/v2/ioc"
)
var ctx = context.Background()
var svc = impl.CommentServiceImpl{}
func init() {
// import 后自动执行的逻辑
// 工具对象的初始化, 需要的是绝对路径
ioc.DevelopmentSetupWithPath(os.Getenv("workspaceFolder") + "/book/v4/application.toml")
}

View File

@ -1,20 +0,0 @@
package comment
import (
"context"
)
const (
APP_NAME = "comment"
)
// comment.Service, Comment的业务定义
type Service interface {
// 为书籍添加评论
AddComment(context.Context, *AddCommentRequest) (*Comment, error)
}
type AddCommentRequest struct {
BookId uint
Comment string
}

View File

@ -1,4 +0,0 @@
package comment
type Comment struct {
}

View File

@ -1,12 +0,0 @@
package apps
// 业务加载区, 选择性的价值的业务处理对象
import (
// Api Impl
_ "122.51.31.227/go-course/go18/book/v4/apps/book/api"
// Service Impl
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
)

View File

@ -1,16 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="Ak93bPvnUy-TIIMCp3wx" name="第 1 页">
<mxGraphModel dx="777" dy="534" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Book" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="90" y="260" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="3" value="Comment" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="380" y="260" width="170" height="90" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,30 +0,0 @@
package main
import (
"github.com/infraboard/mcube/v2/ioc/server/cmd"
// 业务对象
_ "122.51.31.227/go-course/go18/book/v4/apps"
// 健康检查
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
// metrics
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
)
func main() {
// ioc框架 加载对象, 注入对象, 配置对象
// server.Gin.Run()
// application.Get().AppName
// http.Get().Host
// server.DefaultConfig.ConfigFile.Enabled = true
// server.DefaultConfig.ConfigFile.Path = "application.toml"
// server.Run(context.Background())
// 不能指定配置文件逻辑
// 使用者来说,体验不佳
// ioc 直接提供server, 直接run就行了
// mcube 包含 一个 gin Engine
// CLI, start 指令 -f 指定配置文件
cmd.Start()
}

View File

@ -1,7 +0,0 @@
package server
import "github.com/gin-gonic/gin"
var Gin = gin.Default()
// ObjectRouter

View File

@ -1,53 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="6JtAa5eU4ERA-y8NrUjI" name="第 1 页">
<mxGraphModel dx="777" dy="471" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="BookServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="210" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="CommentServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="220" y="210" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="10" value="init" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="460" height="110" as="geometry"/>
</mxCell>
<mxCell id="5" value="Controllers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="120" y="65" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="2" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="8" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6">
<mxGeometry x="-0.3065" y="1" relative="1" as="geometry">
<mxPoint y="1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="7" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="7">
<mxGeometry x="0.1423" y="-4" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="9" value="application.toml" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="65" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="11" value="Config" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="354" y="65" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="15" value="面向接口" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="110" y="300" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="16" value="面向具体对象" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="400" y="300" width="80" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -1,17 +0,0 @@
package test
import (
"os"
"github.com/infraboard/mcube/v2/ioc"
// 要注册哪些对象, Book, Comment
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
)
func DevelopmentSet() {
// import 后自动执行的逻辑
// 工具对象的初始化, 需要的是绝对路径
ioc.DevelopmentSetupWithPath(os.Getenv("workspaceFolder") + "/book/v4/application.toml")
}

View File

@ -1,7 +0,0 @@
{
"go.testEnvVars": {
"workspaceFolder": "${workspaceFolder}",
"CONFIG_PATH": "${workspaceFolder}/etc/application.toml"
},
"go.testEnvFile": "${workspaceFolder}/etc/unit_test.env"
}

View File

@ -1,12 +1,3 @@
# 研发云
devcloud: 研发云, 给产研团队(技术团队), 产品经理, 项目经理, 研发人员/测试人员, 运维(上线,维护) 使用的: DevOps
+ 审计中心: 平台的所有用户操作,记录下来, 变更审计
+ 用户中心: 管理用户认证和鉴权
+ 需求管理: Jira, 禅道, ...(x)
+ 应用管理: 立项后的 SCM 源代码管理, 应用的元数据, 服务树(服务分组)
+ 资源管理: CMDB
+ 应用构建: CI, 流水线发布, 应用的持续构建, Jenkins, 新一代的流程, 基于K8s Job自己设计
+ 发布中心(Dev/Test/Pre/Pro)CD: 发布, 应用维护, 部署集群的维护
# 项目代码
多业务模块组成, 渐进式微服务开发方式

View File

@ -1,20 +0,0 @@
[app]
name = "devcloud"
description = "app desc"
address = "http://127.0.0.1:8080"
encrypt_key = "defualt app encrypt key"
[datasource]
provider = "mysql"
host = "127.0.0.1"
port = 3306
database = "devcloud_go18"
username = "root"
password = "123456"
auto_migrate = false
debug = true
[http]
host = "127.0.0.1"
port = 8080
path_prefix = "api"

View File

@ -1,27 +0,0 @@
# 用户中心
管理用户认证和鉴权
## 需求
认证: 你是谁
+ Basic Auth: 通过用户名密码来认证
+ 访问令牌: 最灵活的 框架
鉴权: 你能干什么(范围)
## 概要设计
针对问题(需求),给出一种解决方案(解题)
![](./design.drawio)
## 详细设计
定义业务

View File

@ -1,2 +0,0 @@
# 接口管理

View File

@ -1 +0,0 @@
# 空间管理

View File

@ -1 +0,0 @@
# 授权策略

View File

@ -1,12 +0,0 @@
package apps
import (
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/api"
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/impl"
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/api"
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/impl"
// 颁发器
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers"
)

View File

@ -1,2 +0,0 @@
# 角色管理

View File

@ -1,35 +0,0 @@
# 令牌管理
+ 颁发访问令牌: Login
+ 撤销访问令牌: 令牌失效了 Logout
+ 校验访问令牌:检查令牌的合法性, 是不是伪造的
## 详情设计
字段(业务需求)
令牌:
+ 过期时间
+ 颁发时间
+ 被颁发的人
+ ...
问题: 无刷新功能, 令牌到期了,自动退出了, 过期时间设置长一点, 长时间不过期 又有安全问题
1. 业务功能: 令牌的刷新, 令牌过期了过后,允许用户进行刷新(需要使用刷新Token来刷新 刷新Token也是需要有过期时间 这个时间决定回话长度)有了刷新token用户不会出现 使用中被中断的情况, 并且长时间未使用,系统也户自动退出(刷新Token过期)
## 转化为接口定义
```go
type Service interface {
// 颁发访问令牌: Login
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
// 撤销访问令牌: 令牌失效了 Logout
RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error)
// 校验访问令牌:检查令牌的合法性, 是不是伪造的
ValiateToken(context.Context, *ValiateTokenRequest) (*Token, error)
}
```

View File

@ -1,72 +0,0 @@
package api
import (
_ "embed"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/ioc/config/gorestful"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
)
func init() {
ioc.Api().Registry(&TokenRestulApiHandler{})
}
type TokenRestulApiHandler struct {
ioc.ObjectImpl
// 依赖控制器
svc token.Service
}
func (h *TokenRestulApiHandler) Name() string {
return token.APP_NAME
}
//go:embed docs/login.md
var loginApiDocNotes string
func (h *TokenRestulApiHandler) Init() error {
h.svc = token.GetService()
tags := []string{"用户登录"}
ws := gorestful.ObjectRouter(h)
ws.Route(ws.POST("").To(h.Login).
Doc("颁发令牌(登录)").
Notes(loginApiDocNotes).
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(token.IssueTokenRequest{}).
Writes(token.Token{}).
Returns(200, "OK", token.Token{}))
ws.Route(ws.POST("/validate").To(h.ValiateToken).
Doc("校验令牌").
// Metadata(permission.Auth(true)).
// Metadata(permission.Permission(false)).
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(token.ValiateTokenRequest{}).
Writes(token.Token{}).
Returns(200, "OK", token.Token{}))
// ws.Route(ws.POST("/change_namespace").To(h.ChangeNamespce).
// Doc("切换令牌访问空间").
// // Metadata(permission.Auth(true)).
// // Metadata(permission.Permission(false)).
// Metadata(restfulspec.KeyOpenAPITags, tags).
// Reads(token.ChangeNamespceRequest{}).
// Writes(token.Token{}).
// Returns(200, "OK", token.Token{}))
ws.Route(ws.DELETE("").To(h.Logout).
Doc("撤销令牌(退出)").
// Metadata(permission.Auth(true)).
// Metadata(permission.Permission(false)).
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(token.IssueTokenRequest{}).
Writes(token.Token{}).
Returns(200, "OK", token.Token{}).
Returns(404, "Not Found", nil))
return nil
}

View File

@ -1,8 +0,0 @@
登录接口
```json
{
"username": "admin",
"password": "123456"
}
```

View File

@ -1,127 +0,0 @@
package api
import (
"net/http"
"net/url"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"github.com/emicklei/go-restful/v3"
"github.com/infraboard/mcube/v2/http/restful/response"
"github.com/infraboard/mcube/v2/ioc/config/application"
)
func (h *TokenRestulApiHandler) Login(r *restful.Request, w *restful.Response) {
// 1. 获取用户的请求参数, 参数在Body里面
req := token.NewIssueTokenRequest()
// 获取用户通过body传入的参数
err := r.ReadEntity(req)
if err != nil {
response.Failed(w, err)
return
}
// 设置当前调用者的Token
// Private 用户自己的Token
// 如果你是user/password 这种方式token 直接放到body
switch req.Issuer {
case token.ISSUER_PRIVATE_TOKEN:
req.Parameter.SetAccessToken(token.GetAccessTokenFromHTTP(r.Request))
}
// 2. 执行逻辑
tk, err := h.svc.IssueToken(r.Request.Context(), req)
if err != nil {
response.Failed(w, err)
return
}
// access_token 通过SetCookie 直接写到浏览器客户端(Web)
http.SetCookie(w, &http.Cookie{
Name: token.ACCESS_TOKEN_COOKIE_NAME,
Value: url.QueryEscape(tk.AccessToken),
MaxAge: 0,
Path: "/",
Domain: application.Get().Domain(),
SameSite: http.SameSiteDefaultMode,
Secure: false,
HttpOnly: true,
})
// 在Header头中也添加Token
w.Header().Set(token.ACCESS_TOKEN_RESPONSE_HEADER_NAME, tk.AccessToken)
// 3. Body中返回Token对象
response.Success(w, tk)
}
// func (h *TokenRestulApiHandler) ChangeNamespce(r *restful.Request, w *restful.Response) {
// // 1. 获取用户的请求参数, 参数在Body里面
// req := token.NewChangeNamespceRequest()
// err := r.ReadEntity(req)
// if err != nil {
// response.Failed(w, err)
// return
// }
// tk := token.GetTokenFromCtx(r.Request.Context())
// req.UserId = tk.UserId
// // 2. 执行逻辑
// tk, err = h.svc.ChangeNamespce(r.Request.Context(), req)
// if err != nil {
// response.Failed(w, err)
// return
// }
// // 3. Body中返回Token对象
// response.Success(w, tk)
// }
// Logout HandleFunc
func (h *TokenRestulApiHandler) Logout(r *restful.Request, w *restful.Response) {
req := token.NewRevolkTokenRequest(
token.GetAccessTokenFromHTTP(r.Request),
token.GetRefreshTokenFromHTTP(r.Request),
)
tk, err := h.svc.RevolkToken(r.Request.Context(), req)
if err != nil {
response.Failed(w, err)
return
}
// access_token 通过SetCookie 直接写到浏览器客户端(Web)
http.SetCookie(w, &http.Cookie{
Name: token.ACCESS_TOKEN_COOKIE_NAME,
Value: "",
MaxAge: 0,
Path: "/",
Domain: application.Get().Domain(),
SameSite: http.SameSiteDefaultMode,
Secure: false,
HttpOnly: true,
})
// 3. 返回响应
response.Success(w, tk)
}
func (h *TokenRestulApiHandler) ValiateToken(r *restful.Request, w *restful.Response) {
// 1. 获取用户的请求参数, 参数在Body里面
req := token.NewValiateTokenRequest("")
err := r.ReadEntity(req)
if err != nil {
response.Failed(w, err)
return
}
// 2. 执行逻辑
tk, err := h.svc.ValiateToken(r.Request.Context(), req)
if err != nil {
response.Failed(w, err)
return
}
// 3. Body中返回Token对象
response.Success(w, tk)
}

View File

@ -1,21 +0,0 @@
package token
import "github.com/infraboard/mcube/v2/exception"
const (
ACCESS_TOKEN_HEADER_NAME = "Authorization"
ACCESS_TOKEN_COOKIE_NAME = "access_token"
ACCESS_TOKEN_RESPONSE_HEADER_NAME = "X-OAUTH-TOKEN"
REFRESH_TOKEN_HEADER_NAME = "X-REFRUSH-TOKEN"
)
// 自定义非导出类型,避免外部包直接实例化
type tokenContextKey struct{}
var (
CTX_TOKEN_KEY = tokenContextKey{}
)
var (
CookieNotFound = exception.NewUnauthorized("cookie %s not found", ACCESS_TOKEN_COOKIE_NAME)
)

View File

@ -1,36 +0,0 @@
<mxfile host="65bd71144e">
<diagram id="rJ2wD46cwpVMIQue_TYe" name="第 1 页">
<mxGraphModel dx="934" dy="434" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="user" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="250" width="220" height="60" as="geometry"/>
</mxCell>
<mxCell id="9" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="10" value="token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9">
<mxGeometry x="-0.0681" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="3" value="token" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="380" y="250" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="460" y="10" width="350" height="140" as="geometry"/>
</mxCell>
<mxCell id="5" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="570" y="40" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="570" y="80" width="120" height="20" as="geometry"/>
</mxCell>
<mxCell id="8" value="记住登录" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="720" y="100" width="50" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -1,37 +0,0 @@
package token
type SOURCE int
const (
// 未知
SOURCE_UNKNOWN SOURCE = iota
// Web
SOURCE_WEB
// IOS
SOURCE_IOS
// ANDROID
SOURCE_ANDROID
// PC
SOURCE_PC
// API 调用
SOURCE_API SOURCE = 10
)
type LOCK_TYPE int
const (
// 用户退出登录
LOCK_TYPE_REVOLK LOCK_TYPE = iota
// 刷新Token过期, 回话中断
LOCK_TYPE_TOKEN_EXPIRED
// 异地登陆
LOCK_TYPE_OTHER_PLACE_LOGGED_IN
// 异常Ip登陆
LOCK_TYPE_OTHER_IP_LOGGED_IN
)
type DESCRIBE_BY int
const (
DESCRIBE_BY_ACCESS_TOKEN DESCRIBE_BY = iota
)

View File

@ -1,56 +0,0 @@
package impl
import (
"time"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/ioc/config/datasource"
"github.com/infraboard/mcube/v2/ioc/config/log"
"github.com/rs/zerolog"
)
func init() {
ioc.Controller().Registry(&TokenServiceImpl{
AutoRefresh: true,
RereshTTLSecond: 1 * 60 * 60,
})
}
var _ token.Service = (*TokenServiceImpl)(nil)
type TokenServiceImpl struct {
ioc.ObjectImpl
user user.Service
log *zerolog.Logger
// policy policy.PermissionService
// 自动刷新, 直接刷新Token的过期时间而不是生成一个新Token
AutoRefresh bool `json:"auto_refresh" toml:"auto_refresh" yaml:"auto_refresh" env:"AUTO_REFRESH"`
// 刷新TTL
RereshTTLSecond uint64 `json:"refresh_ttl" toml:"refresh_ttl" yaml:"refresh_ttl" env:"REFRESH_TTL"`
// Api最多多少个, 这种Token往往过期时间比较长, 为了安全不要申请太多
MaxActiveApiToken uint8 `json:"max_active_api_token" toml:"max_active_api_token" yaml:"max_active_api_token" env:"MAX_ACTIVE_API_TOKEN"`
refreshDuration time.Duration
}
func (i *TokenServiceImpl) Init() error {
i.log = log.Sub(i.Name())
i.user = user.GetService()
// i.policy = policy.GetService()
i.refreshDuration = time.Duration(i.RereshTTLSecond) * time.Second
if datasource.Get().AutoMigrate {
err := datasource.DB().AutoMigrate(&token.Token{})
if err != nil {
return err
}
}
return nil
}
func (i *TokenServiceImpl) Name() string {
return token.APP_NAME
}

View File

@ -1,18 +0,0 @@
package impl_test
import (
"context"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
)
var (
svc token.Service
ctx = context.Background()
)
func init() {
test.DevelopmentSet()
svc = token.GetService()
}

View File

@ -1,201 +0,0 @@
package impl
import (
"context"
"time"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"github.com/infraboard/mcube/v2/desense"
"github.com/infraboard/mcube/v2/exception"
"github.com/infraboard/mcube/v2/ioc/config/datasource"
"github.com/infraboard/mcube/v2/types"
)
// 登录接口(颁发Token)
func (i *TokenServiceImpl) IssueToken(ctx context.Context, in *token.IssueTokenRequest) (*token.Token, error) {
// 颁发Token
// user/password
// ldap
// 飞书,企业微信 ...
issuer := token.GetIssuer(in.Issuer)
if issuer == nil {
return nil, exception.NewBadRequest("issuer %s no support", in.Issuer)
}
tk, err := issuer.IssueToken(ctx, in.Parameter)
if err != nil {
return nil, err
}
tk.SetIssuer(in.Issuer).SetSource(in.Source)
// 判断当前数据库有没有已经存在的Token
activeTokenQueryReq := token.NewQueryTokenRequest().
AddUserId(tk.UserId).
SetSource(in.Source).
SetActive(true)
tks, err := i.QueryToken(ctx, activeTokenQueryReq)
if err != nil {
return nil, err
}
switch in.Source {
// 每个端只能有1个活跃登录
case token.SOURCE_WEB, token.SOURCE_IOS, token.SOURCE_ANDROID, token.SOURCE_PC:
if tks.Len() > 0 {
i.log.Debug().Msgf("use exist active token: %s", desense.Default().DeSense(tk.AccessToken, "4", "3"))
return tks.Items[0], nil
}
case token.SOURCE_API:
if tks.Len() > int(i.MaxActiveApiToken) {
return nil, exception.NewBadRequest("max active api token overflow")
}
}
// 保持Token
if err := datasource.DBFromCtx(ctx).
Create(tk).
Error; err != nil {
return nil, err
}
return tk, nil
}
// 校验Token 是给内部中间层使用 身份校验层
func (i *TokenServiceImpl) ValiateToken(ctx context.Context, req *token.ValiateTokenRequest) (*token.Token, error) {
// 1. 查询Token (是不是我们这个系统颁发的)
tk := token.NewToken()
err := datasource.DBFromCtx(ctx).
Where("access_token = ?", req.AccessToken).
First(tk).
Error
if err != nil {
return nil, err
}
// 2.1 判断Ak是否过期
if err := tk.IsAccessTokenExpired(); err != nil {
// 判断刷新Token是否过期
if err := tk.IsRreshTokenExpired(); err != nil {
return nil, err
}
// 如果开启了自动刷新
if i.AutoRefresh {
tk.SetRefreshAt(time.Now())
tk.SetExpiredAtByDuration(i.refreshDuration, 4)
if err := datasource.DBFromCtx(ctx).Save(tk); err != nil {
i.log.Error().Msgf("auto refresh token error, %s", err.Error)
}
}
return nil, err
}
return tk, nil
}
func (i *TokenServiceImpl) DescribeToken(ctx context.Context, in *token.DescribeTokenRequest) (*token.Token, error) {
query := datasource.DBFromCtx(ctx)
switch in.DescribeBy {
case token.DESCRIBE_BY_ACCESS_TOKEN:
query = query.Where("access_token = ?", in.DescribeValue)
default:
return nil, exception.NewBadRequest("unspport describe type %s", in.DescribeValue)
}
tk := token.NewToken()
if err := query.First(tk).Error; err != nil {
return nil, err
}
return tk, nil
}
// 退出接口(销毁Token)
func (i *TokenServiceImpl) RevolkToken(ctx context.Context, in *token.RevolkTokenRequest) (*token.Token, error) {
tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken))
if err != nil {
return nil, err
}
if err := tk.CheckRefreshToken(in.RefreshToken); err != nil {
return nil, err
}
tk.Lock(token.LOCK_TYPE_REVOLK, "user revolk token")
err = datasource.DBFromCtx(ctx).Model(&token.Token{}).
Where("access_token = ?", in.AccessToken).
Where("refresh_token = ?", in.RefreshToken).
Updates(tk.Status.ToMap()).
Error
if err != nil {
return nil, err
}
return tk, err
}
// 查询已经颁发出去的Token
func (i *TokenServiceImpl) QueryToken(ctx context.Context, in *token.QueryTokenRequest) (*types.Set[*token.Token], error) {
set := types.New[*token.Token]()
query := datasource.DBFromCtx(ctx).Model(&token.Token{})
if in.Active != nil {
if *in.Active {
query = query.
Where("lock_at IS NULL AND refresh_token_expired_at > ?", time.Now())
} else {
query = query.
Where("lock_at IS NOT NULL OR refresh_token_expired_at <= ?", time.Now())
}
}
if in.Source != nil {
query = query.Where("source = ?", *in.Source)
}
if len(in.UserIds) > 0 {
query = query.Where("user_id IN ?", in.UserIds)
}
// 查询总量
err := query.Count(&set.Total).Error
if err != nil {
return nil, err
}
err = query.
Order("issue_at desc").
Offset(int(in.ComputeOffset())).
Limit(int(in.PageSize)).
Find(&set.Items).
Error
if err != nil {
return nil, err
}
return set, nil
}
// 用户切换空间
// func (i *TokenServiceImpl) ChangeNamespce(ctx context.Context, in *token.ChangeNamespceRequest) (*token.Token, error) {
// set, err := i.policy.QueryNamespace(ctx, policy.NewQueryNamespaceRequest().SetUserId(in.UserId).SetNamespaceId(in.NamespaceId))
// if err != nil {
// return nil, err
// }
// ns := set.First()
// if ns == nil {
// return nil, exception.NewPermissionDeny("你没有该空间访问权限")
// }
// // 更新Token
// tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken))
// if err != nil {
// return nil, err
// }
// tk.NamespaceId = ns.Id
// tk.NamespaceName = ns.Name
// // 保存状态
// if err := datasource.DBFromCtx(ctx).
// Updates(tk).
// Error; err != nil {
// return nil, err
// }
// return tk, nil
// }

View File

@ -1,27 +0,0 @@
package impl_test
import (
"testing"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
)
func TestIssueToken(t *testing.T) {
req := token.NewIssueTokenRequest()
req.IssueByPassword("admin", "123456")
req.Source = token.SOURCE_WEB
set, err := svc.IssueToken(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(set)
}
func TestQueryToken(t *testing.T) {
req := token.NewQueryTokenRequest()
set, err := svc.QueryToken(ctx, req)
if err != nil {
t.Fatal(err)
}
t.Log(set)
}

View File

@ -1,171 +0,0 @@
package token
import (
"context"
"time"
"github.com/infraboard/mcube/v2/http/request"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/types"
)
const (
APP_NAME = "token"
)
func GetService() Service {
return ioc.Controller().Get(APP_NAME).(Service)
}
type Service interface {
// 颁发访问令牌: Login
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
// 撤销访问令牌: 令牌失效了 Logout
RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error)
// 查询已经颁发出去的Token
QueryToken(context.Context, *QueryTokenRequest) (*types.Set[*Token], error)
// 查询Token详情
DescribeToken(context.Context, *DescribeTokenRequest) (*Token, error)
// 校验访问令牌:检查令牌的合法性, 是不是伪造的
ValiateToken(context.Context, *ValiateTokenRequest) (*Token, error)
}
func NewDescribeTokenRequest(accessToken string) *DescribeTokenRequest {
return &DescribeTokenRequest{
DescribeBy: DESCRIBE_BY_ACCESS_TOKEN,
DescribeValue: accessToken,
}
}
type DescribeTokenRequest struct {
DescribeBy DESCRIBE_BY `json:"describe_by"`
DescribeValue string `json:"describe_value"`
}
func NewQueryTokenRequest() *QueryTokenRequest {
return &QueryTokenRequest{
PageRequest: request.NewDefaultPageRequest(),
UserIds: []uint64{},
}
}
type QueryTokenRequest struct {
*request.PageRequest
// 当前可用的没过期的Token
Active *bool `json:"active"`
// 用户来源
Source *SOURCE `json:"source"`
// Uids
UserIds []uint64 `json:"user_ids"`
}
func (r *QueryTokenRequest) SetActive(v bool) *QueryTokenRequest {
r.Active = &v
return r
}
func (r *QueryTokenRequest) SetSource(v SOURCE) *QueryTokenRequest {
r.Source = &v
return r
}
func (r *QueryTokenRequest) AddUserId(uids ...uint64) *QueryTokenRequest {
r.UserIds = append(r.UserIds, uids...)
return r
}
func NewIssueTokenRequest() *IssueTokenRequest {
return &IssueTokenRequest{
Parameter: make(IssueParameter),
}
}
// 用户会给我们 用户的身份凭证用于换取Token
type IssueTokenRequest struct {
// 端类型
Source SOURCE `json:"source"`
// 认证方式
Issuer string `json:"issuer"`
// 参数
Parameter IssueParameter `json:"parameter"`
}
func (i *IssueTokenRequest) IssueByPassword(username, password string) {
i.Issuer = ISSUER_PASSWORD
i.Parameter.SetUsername(username)
i.Parameter.SetPassword(password)
}
func NewIssueParameter() IssueParameter {
return make(IssueParameter)
}
type IssueParameter map[string]any
/*
password issuer parameter
*/
func (p IssueParameter) Username() string {
return GetIssueParameterValue[string](p, "username")
}
func (p IssueParameter) Password() string {
return GetIssueParameterValue[string](p, "password")
}
func (p IssueParameter) SetUsername(v string) IssueParameter {
p["username"] = v
return p
}
func (p IssueParameter) SetPassword(v string) IssueParameter {
p["password"] = v
return p
}
/*
private token issuer parameter
*/
func (p IssueParameter) AccessToken() string {
return GetIssueParameterValue[string](p, "access_token")
}
func (p IssueParameter) ExpireTTL() time.Duration {
return time.Second * time.Duration(GetIssueParameterValue[int64](p, "expired_ttl"))
}
func (p IssueParameter) SetAccessToken(v string) IssueParameter {
p["access_token"] = v
return p
}
func (p IssueParameter) SetExpireTTL(v int64) IssueParameter {
p["expired_ttl"] = v
return p
}
func NewRevolkTokenRequest(at, rk string) *RevolkTokenRequest {
return &RevolkTokenRequest{
AccessToken: at,
RefreshToken: rk,
}
}
// 万一的Token泄露, 不知道refresh_token也没法推出
type RevolkTokenRequest struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func NewValiateTokenRequest(accessToken string) *ValiateTokenRequest {
return &ValiateTokenRequest{
AccessToken: accessToken,
}
}
type ValiateTokenRequest struct {
AccessToken string `json:"access_token"`
}

View File

@ -1,44 +0,0 @@
package token
import (
"context"
"fmt"
"math/rand/v2"
)
const (
ISSUER_LDAP = "ldap"
ISSUER_FEISHU = "feishu"
ISSUER_PASSWORD = "password"
ISSUER_PRIVATE_TOKEN = "private_token"
)
var issuers = map[string]Issuer{}
func RegistryIssuer(name string, p Issuer) {
issuers[name] = p
}
func GetIssuer(name string) Issuer {
fmt.Println(issuers)
return issuers[name]
}
type Issuer interface {
IssueToken(context.Context, IssueParameter) (*Token, error)
}
var (
charlist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// MakeBearer https://tools.ietf.org/html/rfc6750#section-2.1
// b64token = 1*( ALPHA / DIGIT /"-" / "." / "_" / "~" / "+" / "/" ) *"="
func MakeBearer(lenth int) string {
t := make([]byte, 0)
for range lenth {
rn := rand.IntN(len(charlist))
t = append(t, charlist[rn])
}
return string(t)
}

View File

@ -1,12 +0,0 @@
package token_test
import (
"testing"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
)
func TestMakeBearer(t *testing.T) {
t.Log(token.MakeBearer(24))
t.Log(token.MakeBearer(24))
}

View File

@ -1,67 +0,0 @@
package password
import (
"context"
"time"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
"github.com/infraboard/mcube/v2/exception"
"github.com/infraboard/mcube/v2/ioc"
)
func init() {
ioc.Config().Registry(&PasswordTokenIssuer{
ExpiredTTLSecond: 1 * 60 * 60,
})
}
type PasswordTokenIssuer struct {
ioc.ObjectImpl
// 通过用户模块 来判断用户凭证是否正确
user user.Service
// Password颁发的Token 过去时间由系统配置, 不允许用户自己设置
ExpiredTTLSecond int `json:"expired_ttl_second" toml:"expired_ttl_second" yaml:"expired_ttl_second" env:"EXPIRED_TTL_SECOND"`
expiredDuration time.Duration
}
func (p *PasswordTokenIssuer) Name() string {
return "password_token_issuer"
}
func (p *PasswordTokenIssuer) Init() error {
p.user = user.GetService()
p.expiredDuration = time.Duration(p.ExpiredTTLSecond) * time.Second
token.RegistryIssuer(token.ISSUER_PASSWORD, p)
return nil
}
func (p *PasswordTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) {
// 1. 查询用户
uReq := user.NewDescribeUserRequestByUserName(parameter.Username())
u, err := p.user.DescribeUser(ctx, uReq)
if err != nil {
if exception.IsNotFoundError(err) {
return nil, exception.NewUnauthorized("%s", err)
}
return nil, err
}
// 2. 比对密码
err = u.CheckPassword(parameter.Password())
if err != nil {
return nil, err
}
// 3. 颁发token
tk := token.NewToken()
tk.UserId = u.Id
tk.UserName = u.UserName
tk.IsAdmin = u.IsAdmin
tk.SetExpiredAtByDuration(p.expiredDuration, 4)
return tk, nil
}

View File

@ -1,22 +0,0 @@
package password_test
import (
"context"
"testing"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
)
func TestPasswordIssuer(t *testing.T) {
issuer := token.GetIssuer(token.ISSUER_PASSWORD)
tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetUsername("admin").SetPassword("123456"))
if err != nil {
t.Fatal(err)
}
t.Log(tk)
}
func init() {
test.DevelopmentSet()
}

View File

@ -1,22 +0,0 @@
package privatetoken_test
import (
"context"
"testing"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
)
func TestPasswordIssuer(t *testing.T) {
issuer := token.GetIssuer(token.ISSUER_PRIVATE_TOKEN)
tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetAccessToken("LccvuTwISJRheu8PtqAFTJBy").SetExpireTTL(24*60*60))
if err != nil {
t.Fatal(err)
}
t.Log(tk)
}
func init() {
test.DevelopmentSet()
}

View File

@ -1,67 +0,0 @@
package privatetoken
import (
"context"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
"github.com/infraboard/mcube/v2/exception"
"github.com/infraboard/mcube/v2/ioc"
)
func init() {
ioc.Config().Registry(&PrivateTokenIssuer{})
}
type PrivateTokenIssuer struct {
ioc.ObjectImpl
user user.Service
token token.Service
}
func (p *PrivateTokenIssuer) Name() string {
return "private_token_issuer"
}
func (p *PrivateTokenIssuer) Init() error {
p.user = user.GetService()
p.token = token.GetService()
token.RegistryIssuer(token.ISSUER_PRIVATE_TOKEN, p)
return nil
}
func (p *PrivateTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) {
// 1. 校验Token合法
oldTk, err := p.token.ValiateToken(ctx, token.NewValiateTokenRequest(parameter.AccessToken()))
if err != nil {
return nil, err
}
// 2. 查询用户
uReq := user.NewDescribeUserRequestById(oldTk.UserIdString())
u, err := p.user.DescribeUser(ctx, uReq)
if err != nil {
if exception.IsNotFoundError(err) {
return nil, exception.NewUnauthorized("%s", err)
}
return nil, err
}
if !u.EnabledApi {
return nil, exception.NewPermissionDeny("未开启接口登录")
}
// 3. 颁发token
tk := token.NewToken()
tk.UserId = u.Id
tk.UserName = u.UserName
tk.IsAdmin = u.IsAdmin
expiredTTL := parameter.ExpireTTL()
if expiredTTL > 0 {
tk.SetExpiredAtByDuration(expiredTTL, 4)
}
return tk, nil
}

View File

@ -1,6 +0,0 @@
package issuers
import (
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/password"
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/private_token"
)

View File

@ -1,216 +0,0 @@
package token
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/infraboard/mcube/v2/exception"
"github.com/infraboard/mcube/v2/tools/pretty"
)
func GetAccessTokenFromHTTP(r *http.Request) string {
// 先从Token中获取
tk := r.Header.Get(ACCESS_TOKEN_HEADER_NAME)
// 1. 获取Token
if tk == "" {
cookie, err := r.Cookie(ACCESS_TOKEN_COOKIE_NAME)
if err != nil {
return ""
}
tk, _ = url.QueryUnescape(cookie.Value)
} else {
// 处理 带格式: Bearer <Your API key>
ft := strings.Split(tk, " ")
if len(ft) > 1 {
tk = ft[1]
}
}
return tk
}
func GetTokenFromCtx(ctx context.Context) *Token {
if v := ctx.Value(CTX_TOKEN_KEY); v != nil {
return v.(*Token)
}
return nil
}
func GetRefreshTokenFromHTTP(r *http.Request) string {
// 先从Token中获取
tk := r.Header.Get(REFRESH_TOKEN_HEADER_NAME)
return tk
}
func NewToken() *Token {
tk := &Token{
// 生产一个UUID的字符串
AccessToken: MakeBearer(24),
RefreshToken: MakeBearer(32),
IssueAt: time.Now(),
Status: NewStatus(),
Extras: map[string]string{},
Scope: map[string]string{},
}
return tk
}
// 需要存储到数据库里面的对象(表)
type Token struct {
// 在添加数据需要, 主键
Id uint64 `json:"id" gorm:"column:id;type:uint;primary_key;"`
// 用户来源
Source SOURCE `json:"source" gorm:"column:source;type:tinyint(1);index" description:"用户来源"`
// 颁发器, 办法方式(user/pass )
Issuer string `json:"issuer" gorm:"column:issuer;type:varchar(100);index" description:"颁发器"`
// 该Token属于哪个用户
UserId uint64 `json:"user_id" gorm:"column:user_id;index" description:"持有该Token的用户Id"`
// 用户名
UserName string `json:"user_name" gorm:"column:user_name;type:varchar(100);not null;index" description:"持有该Token的用户名称"`
// 是不是管理员
IsAdmin bool `json:"is_admin" gorm:"column:is_admin;type:tinyint(1)" description:"是不是管理员"`
// 令牌生效空间Id
NamespaceId uint64 `json:"namespace_id" gorm:"column:namespace_id;type:uint;index" description:"令牌所属空间Id"`
// 令牌生效空间名称
NamespaceName string `json:"namespace_name" gorm:"column:namespace_name;type:varchar(100);index" description:"令牌所属空间"`
// 访问范围定义, 鉴权完成后补充
Scope map[string]string `json:"scope" gorm:"column:scope;type:varchar(100);serializer:json" description:"令牌访问范围定义"`
// 颁发给用户的访问令牌(用户需要携带Token来访问接口)
AccessToken string `json:"access_token" gorm:"column:access_token;type:varchar(100);not null;uniqueIndex" description:"访问令牌"`
// 访问令牌过期时间
AccessTokenExpiredAt *time.Time `json:"access_token_expired_at" gorm:"column:access_token_expired_at;type:timestamp;index" description:"访问令牌的过期时间"`
// 刷新Token
RefreshToken string `json:"refresh_token" gorm:"column:refresh_token;type:varchar(100);not null;uniqueIndex" description:"刷新令牌"`
// 刷新Token过期时间
RefreshTokenExpiredAt *time.Time `json:"refresh_token_expired_at" gorm:"column:refresh_token_expired_at;type:timestamp;index" description:"刷新令牌的过期时间"`
// 创建时间
IssueAt time.Time `json:"issue_at" gorm:"column:issue_at;type:timestamp;default:current_timestamp;not null;index" description:"令牌颁发时间"`
// 更新时间
RefreshAt *time.Time `json:"refresh_at" gorm:"column:refresh_at;type:timestamp" description:"令牌刷新时间"`
// 令牌状态
Status *Status `json:"status" gorm:"embedded" modelDescription:"令牌状态"`
// 其他扩展信息
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息"`
}
func (t *Token) TableName() string {
return "tokens"
}
// 判断访问令牌是否过期,没设置代表用不过期
func (t *Token) IsAccessTokenExpired() error {
if t.AccessTokenExpiredAt != nil {
// now expiredTime
expiredSeconds := time.Since(*t.AccessTokenExpiredAt).Seconds()
if expiredSeconds > 0 {
return exception.NewAccessTokenExpired("access token %s 过期了 %f秒",
t.AccessToken, expiredSeconds)
}
}
return nil
}
// 判断刷新Token是否过期
func (t *Token) IsRreshTokenExpired() error {
if t.RefreshTokenExpiredAt != nil {
expiredSeconds := time.Since(*t.RefreshTokenExpiredAt).Seconds()
if expiredSeconds > 0 {
return exception.NewRefreshTokenExpired("refresh token %s 过期了 %f秒",
t.RefreshToken, expiredSeconds)
}
}
return nil
}
// 刷新Token的过期时间 是一个系统配置, 刷新token的过期时间 > 访问token的时间
// 给一些默认设置: 刷新token的过期时间 = 访问token的时间 * 4
func (t *Token) SetExpiredAtByDuration(duration time.Duration, refreshMulti uint) {
t.SetAccessTokenExpiredAt(time.Now().Add(duration))
t.SetRefreshTokenExpiredAt(time.Now().Add(duration * time.Duration(refreshMulti)))
}
func (t *Token) SetAccessTokenExpiredAt(v time.Time) {
t.AccessTokenExpiredAt = &v
}
func (t *Token) SetRefreshAt(v time.Time) {
t.RefreshAt = &v
}
func (t *Token) AccessTokenExpiredTTL() int {
if t.AccessTokenExpiredAt != nil {
return int(t.AccessTokenExpiredAt.Sub(t.IssueAt).Seconds())
}
return 0
}
func (t *Token) SetRefreshTokenExpiredAt(v time.Time) {
t.RefreshTokenExpiredAt = &v
}
func (t *Token) String() string {
return pretty.ToJSON(t)
}
func (t *Token) SetIssuer(issuer string) *Token {
t.Issuer = issuer
return t
}
func (t *Token) SetSource(source SOURCE) *Token {
t.Source = source
return t
}
func (t *Token) UserIdString() string {
return fmt.Sprintf("%d", t.UserId)
}
func (t *Token) CheckRefreshToken(refreshToken string) error {
if t.RefreshToken != refreshToken {
return exception.NewPermissionDeny("refresh token not conrect")
}
return nil
}
func (t *Token) Lock(l LOCK_TYPE, reason string) {
if t.Status == nil {
t.Status = NewStatus()
}
t.Status.LockType = l
t.Status.LockReason = reason
t.Status.SetLockAt(time.Now())
}
func NewStatus() *Status {
return &Status{}
}
type Status struct {
// 冻结时间
LockAt *time.Time `json:"lock_at" bson:"lock_at" gorm:"column:lock_at;type:timestamp;index" description:"冻结时间"`
// 冻结类型
LockType LOCK_TYPE `json:"lock_type" bson:"lock_type" gorm:"column:lock_type;type:tinyint(1)" description:"冻结类型 0:用户退出登录, 1:刷新Token过期, 回话中断, 2:异地登陆, 异常Ip登陆" enum:"0|1|2|3"`
// 冻结原因
LockReason string `json:"lock_reason" bson:"lock_reason" gorm:"column:lock_reason;type:text" description:"冻结原因"`
}
func (s *Status) SetLockAt(v time.Time) {
s.LockAt = &v
}
func (s *Status) ToMap() map[string]any {
return map[string]any{
"lock_at": s.LockAt,
"lock_type": s.LockType,
"lock_reason": s.LockReason,
}
}

View File

@ -1,13 +0,0 @@
package token
// 泛型函数
func GetIssueParameterValue[T any](p IssueParameter, key string) T {
v := p[key]
if v != nil {
if value, ok := v.(T); ok {
return value
}
}
var zero T
return zero
}

View File

@ -1,67 +0,0 @@
# 用户管理
+ 创建用户
+ 删除用户
+ 更新用户
+ 用户列表
+ 用户详情
+ 重置密码
## 详情设计
```go
// 定义User包的能力 就是接口定义
// 站在使用放的角度来定义的 userSvc.Create(ctx, req), userSvc.DeleteUser(id)
// 接口定义好了,不要试图 随意修改接口, 要保证接口的兼容性
type Service interface {
// 创建用户
CreateUser(context.Context, *CreateUserRequest) (*User, error)
// 删除用户
DeleteUser(context.Context, *DeleteUserRequest) (*User, error)
// 查询用户详情
DescribeUser(context.Context, *DescribeUserRequest) (*User, error)
// 查询用户列表
QueryUser(context.Context, *QueryUserRequest) (*types.Set[*User], error)
}
```
### 业务功能
加解迷的方式有分类:
1. Hash (消息摘要): 单向Hash, 可以通过原文 获取摘要信息,但是无法通过摘要信息推断原文, 只要摘要信息相同,原文就相同: md5, sha*, bcrypt ...
2. 对称加解密: 加密和解密的秘密(key) 用于数据的加解密文
3. 非对称加解密: 加密(公钥)和解密(私用)不是用的同一个秘密, 用于密码的加解密
1. 用户密码怎么存储的问题, 存储用户密码的hash避免直接存储用户密码。 11111 -> abcd, 可能导致 用户的秘密在其他平台泄露 知道了这个影视关系abcd --> 1111,
能不能有什么办法解决这个问题, 加盐: 相同密码 --> 每次hash会产生不同的结果
![alt text](image.png)
password --> hash
password + 随机字符串(salt) --> (salt)hash它也是随机
输入: password + 随机字符串(salt: 原来hash中的sal部分) == hash它也是随机(数据库)
```go
import (
"golang.org/x/crypto/bcrypt"
)
func (req *CreateUserRequest) PasswordHash() {
if req.isHashed {
return
}
b, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
req.Password = string(b)
req.isHashed = true
}
// 判断该用户的密码是否正确
func (u *User) CheckPassword(password string) error {
// u.Password hash过后的只
// (password 原始值 + hash值中提区salt)
return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
}
```

View File

@ -1 +0,0 @@
package api

View File

@ -1,48 +0,0 @@
package user
type PROVIDER int32
const (
// 本地数据库
PROVIDER_LOCAL PROVIDER = 0
// 来源LDAP
PROVIDER_LDAP PROVIDER = 1
// 来源飞书
PROVIDER_FEISHU PROVIDER = 2
// 来源钉钉
PROVIDER_DINGDING PROVIDER = 3
// 来源企业微信
PROVIDER_WECHAT_WORK PROVIDER = 4
)
type CEATE_TYPE int
const (
// 系统初始化
CREATE_TYPE_INIT = iota
// 管理员创建
CREATE_TYPE_ADMIN
// 用户自己注册
CREATE_TYPE_REGISTRY
)
type TYPE int32
const (
TYPE_SUB TYPE = 0
)
type SEX int
const (
SEX_UNKNOWN = iota
SEX_MALE
SEX_FEMALE
)
type DESCRIBE_BY int
const (
DESCRIBE_BY_ID DESCRIBE_BY = iota
DESCRIBE_BY_USERNAME
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

View File

@ -1,34 +0,0 @@
package impl
import (
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/ioc/config/datasource"
)
func init() {
ioc.Controller().Registry(&UserServiceImpl{})
}
var _ user.Service = (*UserServiceImpl)(nil)
// 他是user service 服务的控制器
type UserServiceImpl struct {
ioc.ObjectImpl
}
func (i *UserServiceImpl) Init() error {
// 自动创建表
if datasource.Get().AutoMigrate {
err := datasource.DB().AutoMigrate(&user.User{})
if err != nil {
return err
}
}
return nil
}
// 定义托管到Ioc里面的名称
func (i *UserServiceImpl) Name() string {
return user.APP_NAME
}

View File

@ -1,18 +0,0 @@
package impl_test
import (
"context"
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
)
var (
impl user.Service
ctx = context.Background()
)
func init() {
test.DevelopmentSet()
impl = user.GetService()
}

Some files were not shown because too many files have changed in this diff Show More