Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
c315c4747c | |||
50d39008de | |||
47e4772618 | |||
426e493967 | |||
cc4729de15 | |||
c176c4086f | |||
c7fa46290c | |||
e05a1cacd6 | |||
7c82afd9e5 | |||
c6054a09bd | |||
6968632db0 | |||
ebfb39bade | |||
4d5b597380 | |||
665fde6609 | |||
43f675d736 | |||
fad998ea15 | |||
66ba346f23 | |||
91acab4489 | |||
a1bd181212 |
10
book/v3/application.yaml
Normal file
10
book/v3/application.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
app:
|
||||
host: 127.0.0.1
|
||||
port: 8080
|
||||
mysql:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
database: go18
|
||||
username: "root"
|
||||
password: "123456"
|
||||
debug: true
|
@ -1,6 +1,5 @@
|
||||
# 程序的配置管理
|
||||
|
||||
|
||||
## 配置的加载
|
||||
```go
|
||||
// 用于加载配置
|
||||
@ -18,3 +17,71 @@ config.C().MySQL.Host
|
||||
|
||||
如何验证我们这个包的 业务逻辑是正确
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
@ -2,10 +2,14 @@ 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"
|
||||
)
|
||||
@ -24,6 +28,9 @@ func Default() *Config {
|
||||
Password: "123456",
|
||||
Debug: true,
|
||||
},
|
||||
Log: &Log{
|
||||
Level: zerolog.DebugLevel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +39,7 @@ func Default() *Config {
|
||||
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 {
|
||||
@ -73,6 +81,7 @@ func (m *mySQL) GetDB() *gorm.DB {
|
||||
m.Port,
|
||||
m.DB,
|
||||
)
|
||||
L().Info().Msgf("Database: %s", m.DB)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
@ -80,7 +89,54 @@ func (m *mySQL) GetDB() *gorm.DB {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -29,6 +30,10 @@ 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 {
|
||||
|
@ -1,3 +1,37 @@
|
||||
# 控制器
|
||||
|
||||
业务处理
|
||||
|
||||

|
||||
|
||||
## 单元测试 (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)
|
||||
}
|
||||
}
|
||||
```
|
@ -15,14 +15,6 @@
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="12">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="依赖这个业务逻辑" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="15">
|
||||
<mxGeometry x="0.401" 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>
|
||||
@ -39,7 +31,7 @@
|
||||
<mxGeometry x="260" y="80" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="<h1 style="margin-top: 0px;">controllers</h1><p>controllers.Book.GetBook(id) -&gt; book</p>" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="270" y="230" width="180" height="120" as="geometry"/>
|
||||
<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"/>
|
||||
@ -47,6 +39,21 @@
|
||||
<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>
|
||||
|
BIN
book/v3/controllers/biz.png
Normal file
BIN
book/v3/controllers/biz.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
@ -4,22 +4,32 @@ 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"
|
||||
)
|
||||
|
||||
var Book = &BookController{}
|
||||
|
||||
type BookController struct {
|
||||
func GetBookService() *BookController {
|
||||
return ioc.Controller.Get("book_controller").(*BookController)
|
||||
}
|
||||
|
||||
func NewGetBookRequest(bookNumber string) *GetBookRequest {
|
||||
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 string
|
||||
BookNumber int
|
||||
// RequestId string
|
||||
// ...
|
||||
}
|
||||
@ -32,11 +42,47 @@ func (c *BookController) GetBook(ctx context.Context, in *GetBookRequest) (*mode
|
||||
// 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
|
||||
}
|
||||
|
40
book/v3/controllers/book_test.go
Normal file
40
book/v3/controllers/book_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -13,13 +13,17 @@ type CommentController struct {
|
||||
}
|
||||
|
||||
type AddCommentRequest struct {
|
||||
BookNumber string
|
||||
BookNumber int
|
||||
}
|
||||
|
||||
func (c *CommentController) AddComment(ctx context.Context, in *AddCommentRequest) (*models.Comment, error) {
|
||||
// 业务处理的细节
|
||||
// 多个业务模块 进行交互
|
||||
book, err := Book.GetBook(ctx, NewGetBookRequest(in.BookNumber))
|
||||
book, err := GetBookService().GetBook(ctx, NewGetBookRequest(in.BookNumber))
|
||||
|
||||
// if exception.IsApiException(err, exception.CODE_NOT_FOUND) {
|
||||
|
||||
// }
|
||||
if err != nil {
|
||||
// 获取查询不到报错
|
||||
return nil, err
|
||||
|
102
book/v3/exception/README.md
Normal file
102
book/v3/exception/README.md
Normal file
@ -0,0 +1,102 @@
|
||||
# 业务异常
|
||||
|
||||
## 异常的定义
|
||||
|
||||
```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()
|
||||
})
|
||||
}
|
||||
```
|
35
book/v3/exception/common.go
Normal file
35
book/v3/exception/common.go
Normal file
@ -0,0 +1,35 @@
|
||||
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,
|
||||
}
|
||||
}
|
59
book/v3/exception/exception.go
Normal file
59
book/v3/exception/exception.go
Normal file
@ -0,0 +1,59 @@
|
||||
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
|
||||
}
|
24
book/v3/exception/exception_test.go
Normal file
24
book/v3/exception/exception_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
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))
|
||||
}
|
17
book/v3/exception/gin_recovery.go
Normal file
17
book/v3/exception/gin_recovery.go
Normal file
@ -0,0 +1,17 @@
|
||||
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()
|
||||
})
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v3/config"
|
||||
"122.51.31.227/go-course/go18/book/v3/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"
|
||||
)
|
||||
|
||||
@ -45,7 +45,7 @@ func (h *BookApiHandler) listBook(ctx *gin.Context) {
|
||||
if pageNumber != "" {
|
||||
pnInt, err := strconv.ParseInt(pageNumber, 10, 64)
|
||||
if err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
pn = int(pnInt)
|
||||
@ -55,7 +55,7 @@ func (h *BookApiHandler) listBook(ctx *gin.Context) {
|
||||
if pageSize != "" {
|
||||
psInt, err := strconv.ParseInt(pageSize, 10, 64)
|
||||
if err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
ps = int(psInt)
|
||||
@ -79,12 +79,12 @@ func (h *BookApiHandler) listBook(ctx *gin.Context) {
|
||||
// 4 offset 3 * 20, 20
|
||||
offset := (pn - 1) * ps
|
||||
if err := query.Count(&set.Total).Offset(int(offset)).Limit(int(ps)).Find(&set.Items).Error; err != nil {
|
||||
ctx.JSON(500, gin.H{"code": 500, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取总数, 总共多少个, 总共有多少页
|
||||
ctx.JSON(200, set)
|
||||
response.OK(ctx, set)
|
||||
}
|
||||
|
||||
func (h *BookApiHandler) createBook(ctx *gin.Context) {
|
||||
@ -110,46 +110,41 @@ func (h *BookApiHandler) createBook(ctx *gin.Context) {
|
||||
// 获取到bookInstance
|
||||
// 参数是不是为空
|
||||
if err := ctx.BindJSON(bookSpecInstance); err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 有没有能够检查某个字段是否是必须填
|
||||
// Gin 集成 validator这个库, 通过 struct tag validate 来表示这个字段是否允许为空
|
||||
// validate:"required"
|
||||
// 在数据Bind的时候,这个逻辑会自动运行
|
||||
// if bookSpecInstance.Author == "" {
|
||||
// ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
// return
|
||||
// }
|
||||
|
||||
bookInstance := &models.Book{BookSpec: *bookSpecInstance}
|
||||
|
||||
// 数据入库(Grom), 补充自增Id的值
|
||||
if err := config.DB().Save(bookInstance).Error; err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 500, "message": err.Error()})
|
||||
book, err := controllers.GetBookService().CreateBook(ctx.Request.Context(), bookSpecInstance)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
ctx.JSON(http.StatusCreated, bookInstance)
|
||||
response.OK(ctx, book)
|
||||
}
|
||||
|
||||
func (h *BookApiHandler) getBook(ctx *gin.Context) {
|
||||
book, err := controllers.Book.GetBook(ctx, controllers.NewGetBookRequest(ctx.Param("bn")))
|
||||
bnInt, err := strconv.ParseInt(ctx.Param("bn"), 10, 64)
|
||||
if err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, book)
|
||||
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 {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -159,22 +154,23 @@ func (h *BookApiHandler) updateBook(ctx *gin.Context) {
|
||||
}
|
||||
// 获取到bookInstance
|
||||
if err := ctx.BindJSON(&bookInstance.BookSpec); err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.DB().Where("id = ?", bookInstance.Id).Updates(bookInstance).Error; err != nil {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, bookInstance)
|
||||
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 {
|
||||
ctx.JSON(400, gin.H{"code": 400, "message": err.Error()})
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusNoContent, "ok")
|
||||
|
||||
response.OK(ctx, "ok")
|
||||
}
|
||||
|
@ -5,12 +5,21 @@ import (
|
||||
"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() {
|
||||
server := gin.Default()
|
||||
// 加载配置
|
||||
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)
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package models
|
||||
|
||||
import "github.com/infraboard/mcube/v2/tools/pretty"
|
||||
|
||||
type BookSet struct {
|
||||
// 总共多少个
|
||||
Total int64 `json:"total"`
|
||||
@ -7,13 +9,6 @@ type BookSet struct {
|
||||
Items []*Book `json:"items"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
// 对象Id
|
||||
Id uint `json:"id" gorm:"primaryKey;column:id"`
|
||||
|
||||
BookSpec
|
||||
}
|
||||
|
||||
type BookSpec struct {
|
||||
// type 用于要使用gorm 来自动创建和更新表的时候 才需要定义
|
||||
Title string `json:"title" gorm:"column:title;type:varchar(200)" validate:"required"`
|
||||
@ -24,6 +19,17 @@ type BookSpec struct {
|
||||
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"
|
||||
|
37
book/v3/response/response.go
Normal file
37
book/v3/response/response.go
Normal file
@ -0,0 +1,37 @@
|
||||
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()
|
||||
}
|
@ -1 +1,28 @@
|
||||
# 业务分区架构(基于mcube)
|
||||
# 业务分区架构(基于mcube)
|
||||
|
||||
mcube 与 Ioc
|
||||
|
||||

|
||||
|
||||
更新部分:
|
||||
1. 单元测试 支持通过环境变量注入,优化单元测试配置,共用一套配置
|
||||
2. 新增Book Api项目, 从简单的脚本开发->配置分离->mvc模式->ioc业务分区 经历4个版本,讲解如何开发复杂项目。
|
||||
3. Vblog项目 新增部署,支持2中部署模式,1.前后端分离部署 与 前后端打包一体的部署。
|
||||
4. 优化其他几个项目,支持 可以通过 import的方式,快速使用。
|
||||
5. cmdb 云商凭证 支持加密存储
|
||||
|
||||
|
||||
## 业务分区的第一步 定义业务(RPC)
|
||||
|
||||
Book/Comment: 这个业务模块提供的功能
|
||||
|
||||
|
||||
## 具体实现
|
||||
|
||||
|
||||
|
||||
## 面向接口
|
||||
|
||||
|
||||
|
||||
|
||||
|
23
book/v4/application.toml
Normal file
23
book/v4/application.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[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
|
1
book/v4/apps/README.md
Normal file
1
book/v4/apps/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 业务分区
|
322
book/v4/apps/book/README.md
Normal file
322
book/v4/apps/book/README.md
Normal file
@ -0,0 +1,322 @@
|
||||
# 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
|
||||
```
|
55
book/v4/apps/book/api/api.go
Normal file
55
book/v4/apps/book/api/api.go
Normal file
@ -0,0 +1,55 @@
|
||||
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{})
|
||||
}
|
61
book/v4/apps/book/api/book.go
Normal file
61
book/v4/apps/book/api/book.go
Normal file
@ -0,0 +1,61 @@
|
||||
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)
|
||||
}
|
70
book/v4/apps/book/api/flow.drawio
Normal file
70
book/v4/apps/book/api/flow.drawio
Normal file
@ -0,0 +1,70 @@
|
||||
<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="读取配置<div>调用Init</div>" 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>
|
3
book/v4/apps/book/impl/README.md
Normal file
3
book/v4/apps/book/impl/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# 业务实现包
|
||||
|
||||
ServiceImpl(book.Service)
|
68
book/v4/apps/book/impl/book.go
Normal file
68
book/v4/apps/book/impl/book.go
Normal file
@ -0,0 +1,68 @@
|
||||
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")
|
||||
}
|
29
book/v4/apps/book/impl/book_test.go
Normal file
29
book/v4/apps/book/impl/book_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
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)
|
||||
}
|
35
book/v4/apps/book/impl/impl.go
Normal file
35
book/v4/apps/book/impl/impl.go
Normal file
@ -0,0 +1,35 @@
|
||||
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{})
|
||||
}
|
17
book/v4/apps/book/impl/impl_test.go
Normal file
17
book/v4/apps/book/impl/impl_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
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()
|
||||
}
|
108
book/v4/apps/book/interface.go
Normal file
108
book/v4/apps/book/interface.go
Normal file
@ -0,0 +1,108 @@
|
||||
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"`
|
||||
}
|
19
book/v4/apps/book/model.go
Normal file
19
book/v4/apps/book/model.go
Normal file
@ -0,0 +1,19 @@
|
||||
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"
|
||||
}
|
1
book/v4/apps/comment/README.md
Normal file
1
book/v4/apps/comment/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 评论模块
|
17
book/v4/apps/comment/impl/comment.go
Normal file
17
book/v4/apps/comment/impl/comment.go
Normal file
@ -0,0 +1,17 @@
|
||||
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")
|
||||
}
|
18
book/v4/apps/comment/impl/comment_test.go
Normal file
18
book/v4/apps/comment/impl/comment_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
41
book/v4/apps/comment/impl/impl.go
Normal file
41
book/v4/apps/comment/impl/impl.go
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
18
book/v4/apps/comment/impl/impl_test.go
Normal file
18
book/v4/apps/comment/impl/impl_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
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")
|
||||
}
|
20
book/v4/apps/comment/interface.go
Normal file
20
book/v4/apps/comment/interface.go
Normal file
@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
4
book/v4/apps/comment/model.go
Normal file
4
book/v4/apps/comment/model.go
Normal file
@ -0,0 +1,4 @@
|
||||
package comment
|
||||
|
||||
type Comment struct {
|
||||
}
|
12
book/v4/apps/registry.go
Normal file
12
book/v4/apps/registry.go
Normal file
@ -0,0 +1,12 @@
|
||||
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"
|
||||
)
|
16
book/v4/arch.drawio
Normal file
16
book/v4/arch.drawio
Normal file
@ -0,0 +1,16 @@
|
||||
<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>
|
BIN
book/v4/image.png
Normal file
BIN
book/v4/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
30
book/v4/main.go
Normal file
30
book/v4/main.go
Normal file
@ -0,0 +1,30 @@
|
||||
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()
|
||||
}
|
7
book/v4/server/http.go
Normal file
7
book/v4/server/http.go
Normal file
@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
var Gin = gin.Default()
|
||||
|
||||
// ObjectRouter
|
53
book/v4/test/flow.drawio
Normal file
53
book/v4/test/flow.drawio
Normal file
@ -0,0 +1,53 @@
|
||||
<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>
|
17
book/v4/test/setup.go
Normal file
17
book/v4/test/setup.go
Normal file
@ -0,0 +1,17 @@
|
||||
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")
|
||||
}
|
7
devcloud/.vscode/settings.json
vendored
Normal file
7
devcloud/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"go.testEnvVars": {
|
||||
"workspaceFolder": "${workspaceFolder}",
|
||||
"CONFIG_PATH": "${workspaceFolder}/etc/application.toml"
|
||||
},
|
||||
"go.testEnvFile": "${workspaceFolder}/etc/unit_test.env"
|
||||
}
|
@ -1,3 +1,12 @@
|
||||
# 项目代码
|
||||
# 研发云
|
||||
|
||||
devcloud: 研发云, 给产研团队(技术团队), 产品经理, 项目经理, 研发人员/测试人员, 运维(上线,维护) 使用的: DevOps
|
||||
+ 审计中心: 平台的所有用户操作,记录下来, 变更审计
|
||||
+ 用户中心: 管理用户认证和鉴权
|
||||
+ 需求管理: Jira, 禅道, ...(x)
|
||||
+ 应用管理: 立项后的 SCM 源代码管理, 应用的元数据, 服务树(服务分组)
|
||||
+ 资源管理: CMDB
|
||||
+ 应用构建: CI, 流水线发布, 应用的持续构建, Jenkins, 新一代的流程, 基于K8s Job自己设计
|
||||
+ 发布中心(Dev/Test/Pre/Pro)CD: 发布, 应用维护, 部署集群的维护
|
||||
|
||||
多业务模块组成, 渐进式微服务开发方式
|
20
devcloud/etc/application.toml
Normal file
20
devcloud/etc/application.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[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"
|
0
devcloud/etc/unit_test.env
Normal file
0
devcloud/etc/unit_test.env
Normal file
27
devcloud/mcenter/README.md
Normal file
27
devcloud/mcenter/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 用户中心
|
||||
|
||||
管理用户认证和鉴权
|
||||
|
||||
## 需求
|
||||
|
||||
认证: 你是谁
|
||||
+ Basic Auth: 通过用户名密码来认证
|
||||
+ 访问令牌: 最灵活的 框架
|
||||
|
||||
鉴权: 你能干什么(范围)
|
||||
|
||||
|
||||
## 概要设计
|
||||
|
||||
针对问题(需求),给出一种解决方案(解题)
|
||||
|
||||

|
||||
|
||||
## 详细设计
|
||||
|
||||
定义业务
|
||||
|
||||
|
||||
|
||||
|
||||
|
2
devcloud/mcenter/apps/endpoint/README.md
Normal file
2
devcloud/mcenter/apps/endpoint/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# 接口管理
|
||||
|
1
devcloud/mcenter/apps/namespace/README.md
Normal file
1
devcloud/mcenter/apps/namespace/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 空间管理
|
1
devcloud/mcenter/apps/policy/README.md
Normal file
1
devcloud/mcenter/apps/policy/README.md
Normal file
@ -0,0 +1 @@
|
||||
# 授权策略
|
12
devcloud/mcenter/apps/registry.go
Normal file
12
devcloud/mcenter/apps/registry.go
Normal file
@ -0,0 +1,12 @@
|
||||
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"
|
||||
)
|
2
devcloud/mcenter/apps/role/README.md
Normal file
2
devcloud/mcenter/apps/role/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# 角色管理
|
||||
|
35
devcloud/mcenter/apps/token/README.md
Normal file
35
devcloud/mcenter/apps/token/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 令牌管理
|
||||
|
||||
+ 颁发访问令牌: 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)
|
||||
}
|
||||
```
|
||||
|
72
devcloud/mcenter/apps/token/api/api.go
Normal file
72
devcloud/mcenter/apps/token/api/api.go
Normal file
@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
8
devcloud/mcenter/apps/token/api/docs/login.md
Normal file
8
devcloud/mcenter/apps/token/api/docs/login.md
Normal file
@ -0,0 +1,8 @@
|
||||
登录接口
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
127
devcloud/mcenter/apps/token/api/token.go
Normal file
127
devcloud/mcenter/apps/token/api/token.go
Normal file
@ -0,0 +1,127 @@
|
||||
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)
|
||||
}
|
21
devcloud/mcenter/apps/token/const.go
Normal file
21
devcloud/mcenter/apps/token/const.go
Normal file
@ -0,0 +1,21 @@
|
||||
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)
|
||||
)
|
36
devcloud/mcenter/apps/token/docs/refresh.drawio
Normal file
36
devcloud/mcenter/apps/token/docs/refresh.drawio
Normal file
@ -0,0 +1,36 @@
|
||||
<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>
|
37
devcloud/mcenter/apps/token/enum.go
Normal file
37
devcloud/mcenter/apps/token/enum.go
Normal file
@ -0,0 +1,37 @@
|
||||
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
|
||||
)
|
56
devcloud/mcenter/apps/token/impl/impl.go
Normal file
56
devcloud/mcenter/apps/token/impl/impl.go
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
||||
}
|
18
devcloud/mcenter/apps/token/impl/impl_test.go
Normal file
18
devcloud/mcenter/apps/token/impl/impl_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
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()
|
||||
}
|
201
devcloud/mcenter/apps/token/impl/token.go
Normal file
201
devcloud/mcenter/apps/token/impl/token.go
Normal file
@ -0,0 +1,201 @@
|
||||
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
|
||||
// }
|
27
devcloud/mcenter/apps/token/impl/token_test.go
Normal file
27
devcloud/mcenter/apps/token/impl/token_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
171
devcloud/mcenter/apps/token/interfaceg.go
Normal file
171
devcloud/mcenter/apps/token/interfaceg.go
Normal file
@ -0,0 +1,171 @@
|
||||
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"`
|
||||
}
|
44
devcloud/mcenter/apps/token/issuer.go
Normal file
44
devcloud/mcenter/apps/token/issuer.go
Normal file
@ -0,0 +1,44 @@
|
||||
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)
|
||||
}
|
12
devcloud/mcenter/apps/token/issuer_test.go
Normal file
12
devcloud/mcenter/apps/token/issuer_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
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))
|
||||
}
|
67
devcloud/mcenter/apps/token/issuers/password/issuer.go
Normal file
67
devcloud/mcenter/apps/token/issuers/password/issuer.go
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
}
|
22
devcloud/mcenter/apps/token/issuers/password/issuer_test.go
Normal file
22
devcloud/mcenter/apps/token/issuers/password/issuer_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
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()
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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()
|
||||
}
|
67
devcloud/mcenter/apps/token/issuers/private_token/issuer.go
Normal file
67
devcloud/mcenter/apps/token/issuers/private_token/issuer.go
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
}
|
6
devcloud/mcenter/apps/token/issuers/registry.go
Normal file
6
devcloud/mcenter/apps/token/issuers/registry.go
Normal file
@ -0,0 +1,6 @@
|
||||
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"
|
||||
)
|
216
devcloud/mcenter/apps/token/model.go
Normal file
216
devcloud/mcenter/apps/token/model.go
Normal file
@ -0,0 +1,216 @@
|
||||
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,
|
||||
}
|
||||
}
|
13
devcloud/mcenter/apps/token/tools.go
Normal file
13
devcloud/mcenter/apps/token/tools.go
Normal file
@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
67
devcloud/mcenter/apps/user/README.md
Normal file
67
devcloud/mcenter/apps/user/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# 用户管理
|
||||
|
||||
+ 创建用户
|
||||
+ 删除用户
|
||||
+ 更新用户
|
||||
+ 用户列表
|
||||
+ 用户详情
|
||||
+ 重置密码
|
||||
|
||||
## 详情设计
|
||||
|
||||
```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会产生不同的结果
|
||||
|
||||

|
||||
|
||||
|
||||
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))
|
||||
}
|
||||
```
|
1
devcloud/mcenter/apps/user/api/api.go
Normal file
1
devcloud/mcenter/apps/user/api/api.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
48
devcloud/mcenter/apps/user/enum.go
Normal file
48
devcloud/mcenter/apps/user/enum.go
Normal file
@ -0,0 +1,48 @@
|
||||
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
|
||||
)
|
BIN
devcloud/mcenter/apps/user/image.png
Normal file
BIN
devcloud/mcenter/apps/user/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 154 KiB |
34
devcloud/mcenter/apps/user/impl/impl.go
Normal file
34
devcloud/mcenter/apps/user/impl/impl.go
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
||||
}
|
18
devcloud/mcenter/apps/user/impl/impl_test.go
Normal file
18
devcloud/mcenter/apps/user/impl/impl_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
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()
|
||||
}
|
109
devcloud/mcenter/apps/user/impl/user.go
Normal file
109
devcloud/mcenter/apps/user/impl/user.go
Normal file
@ -0,0 +1,109 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 创建用户
|
||||
func (i *UserServiceImpl) CreateUser(
|
||||
ctx context.Context,
|
||||
req *user.CreateUserRequest) (
|
||||
*user.User, error) {
|
||||
// 1. 校验用户参数
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 生成一个User对象(ORM对象)
|
||||
ins := user.NewUser(req)
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Create(ins).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 返回结果
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
func (i *UserServiceImpl) DeleteUser(
|
||||
ctx context.Context,
|
||||
req *user.DeleteUserRequest,
|
||||
) (*user.User, error) {
|
||||
u, err := i.DescribeUser(ctx,
|
||||
user.NewDescribeUserRequestById(req.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, datasource.DBFromCtx(ctx).
|
||||
Where("id = ?", req.Id).
|
||||
Delete(&user.User{}).
|
||||
Error
|
||||
}
|
||||
|
||||
// 查询用户列表
|
||||
func (i *UserServiceImpl) QueryUser(
|
||||
ctx context.Context,
|
||||
req *user.QueryUserRequest) (
|
||||
*types.Set[*user.User], error) {
|
||||
set := types.New[*user.User]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&user.User{})
|
||||
|
||||
// 查询总量
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("created_at desc").
|
||||
Offset(int(req.ComputeOffset())).
|
||||
Limit(int(req.PageSize)).
|
||||
Find(&set.Items).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 查询用户详情
|
||||
func (i *UserServiceImpl) DescribeUser(
|
||||
ctx context.Context,
|
||||
req *user.DescribeUserRequest) (
|
||||
*user.User, error) {
|
||||
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
|
||||
// 1. 构造我们的查询条件
|
||||
switch req.DescribeBy {
|
||||
case user.DESCRIBE_BY_ID:
|
||||
query = query.Where("id = ?", req.DescribeValue)
|
||||
case user.DESCRIBE_BY_USERNAME:
|
||||
query = query.Where("user_name = ?", req.DescribeValue)
|
||||
}
|
||||
|
||||
// SELECT * FROM `users` WHERE username = 'admin' ORDER BY `users`.`id` LIMIT 1
|
||||
ins := &user.User{}
|
||||
if err := query.First(ins).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, exception.NewNotFound("user %s not found", req.DescribeValue)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 数据库里面存储的就是Hash
|
||||
ins.SetIsHashed()
|
||||
return ins, nil
|
||||
}
|
91
devcloud/mcenter/apps/user/impl/user_test.go
Normal file
91
devcloud/mcenter/apps/user/impl/user_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
)
|
||||
|
||||
func TestQueryUser(t *testing.T) {
|
||||
req := user.NewQueryUserRequest()
|
||||
set, err := impl.QueryUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestCreateAdminUser(t *testing.T) {
|
||||
req := user.NewCreateUserRequest()
|
||||
req.UserName = "admin"
|
||||
req.Password = "123456"
|
||||
req.EnabledApi = true
|
||||
req.IsAdmin = true
|
||||
u, err := impl.CreateUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(u)
|
||||
}
|
||||
|
||||
func TestCreateAuthor2(t *testing.T) {
|
||||
req := user.NewCreateUserRequest()
|
||||
req.UserName = "张三"
|
||||
req.Password = "123456"
|
||||
req.EnabledApi = true
|
||||
u, err := impl.CreateUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(u)
|
||||
}
|
||||
|
||||
func TestCreateGuestUser(t *testing.T) {
|
||||
req := user.NewCreateUserRequest()
|
||||
req.UserName = "guest"
|
||||
req.Password = "123456"
|
||||
req.EnabledApi = true
|
||||
u, err := impl.CreateUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(u)
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
_, err := impl.DeleteUser(ctx, &user.DeleteUserRequest{
|
||||
Id: "9",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeUserRequestById(t *testing.T) {
|
||||
req := user.NewDescribeUserRequestById("2")
|
||||
ins, err := impl.DescribeUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
|
||||
// SELECT * FROM `users` WHERE username = 'admin' ORDER BY `users`.`id` LIMIT 1
|
||||
func TestDescribeUserRequestByName(t *testing.T) {
|
||||
req := user.NewDescribeUserRequestByUserName("admin")
|
||||
ins, err := impl.DescribeUser(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
|
||||
err = ins.CheckPassword("1234561")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserJson(t *testing.T) {
|
||||
u := user.NewUser(user.NewCreateUserRequest())
|
||||
t.Log(u)
|
||||
}
|
83
devcloud/mcenter/apps/user/interface.go
Normal file
83
devcloud/mcenter/apps/user/interface.go
Normal file
@ -0,0 +1,83 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_NAME = "user"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(APP_NAME).(Service)
|
||||
}
|
||||
|
||||
// 定义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)
|
||||
}
|
||||
|
||||
func NewQueryUserRequest() *QueryUserRequest {
|
||||
return &QueryUserRequest{
|
||||
PageRequest: request.NewDefaultPageRequest(),
|
||||
UserIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryUserRequest struct {
|
||||
*request.PageRequest
|
||||
UserIds []uint64 `form:"user" json:"user"`
|
||||
}
|
||||
|
||||
func (r *QueryUserRequest) AddUser(userIds ...uint64) *QueryUserRequest {
|
||||
for _, uid := range userIds {
|
||||
if !slices.Contains(r.UserIds, uid) {
|
||||
r.UserIds = append(r.UserIds, uid)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewDescribeUserRequestById(id string) *DescribeUserRequest {
|
||||
return &DescribeUserRequest{
|
||||
DescribeValue: id,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDescribeUserRequestByUserName(username string) *DescribeUserRequest {
|
||||
return &DescribeUserRequest{
|
||||
DescribeBy: DESCRIBE_BY_USERNAME,
|
||||
DescribeValue: username,
|
||||
}
|
||||
}
|
||||
|
||||
// 同时支持通过Id来查询,也要支持通过username来查询
|
||||
type DescribeUserRequest struct {
|
||||
DescribeBy DESCRIBE_BY `json:"describe_by"`
|
||||
DescribeValue string `json:"describe_value"`
|
||||
}
|
||||
|
||||
func NewDeleteUserRequest(id string) *DeleteUserRequest {
|
||||
return &DeleteUserRequest{
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户的请求
|
||||
type DeleteUserRequest struct {
|
||||
Id string `json:"id"`
|
||||
}
|
130
devcloud/mcenter/apps/user/model.go
Normal file
130
devcloud/mcenter/apps/user/model.go
Normal file
@ -0,0 +1,130 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func NewUser(req *CreateUserRequest) *User {
|
||||
req.PasswordHash()
|
||||
|
||||
return &User{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
CreateUserRequest: *req,
|
||||
}
|
||||
}
|
||||
|
||||
// 用于存放 存入数据库的对象(PO)
|
||||
type User struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 用户传递过来的请求
|
||||
CreateUserRequest
|
||||
|
||||
// 密码强度
|
||||
PwdIntensity int8 `json:"pwd_intensity" gorm:"column:pwd_intensity;type:tinyint(1);not null" optional:"true"`
|
||||
}
|
||||
|
||||
func (u *User) String() string {
|
||||
return pretty.ToJSON(u)
|
||||
}
|
||||
|
||||
// 判断该用户的密码是否正确
|
||||
func (u *User) CheckPassword(password string) error {
|
||||
// u.Password hash过后的只
|
||||
// (password 原始值 + hash值中提区salt)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
||||
if err != nil {
|
||||
return exception.NewUnauthorized("用户名或者密码对正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 声明你这个对象存储在users表里面
|
||||
// orm 负责调用TableName() 来动态获取你这个对象要存储的表的名称
|
||||
func (u *User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func NewCreateUserRequest() *CreateUserRequest {
|
||||
return &CreateUserRequest{
|
||||
Extras: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
// 账号提供方
|
||||
Provider PROVIDER `json:"provider" gorm:"column:provider;type:tinyint(1);not null;index" description:"账号提供方"`
|
||||
// 创建方式
|
||||
CreateType CEATE_TYPE `json:"create_type" gorm:"column:create_type;type:tinyint(1);not null;index" optional:"true"`
|
||||
// 用户名
|
||||
UserName string `json:"user_name" gorm:"column:user_name;type:varchar(100);not null;uniqueIndex" description:"用户名"`
|
||||
// 密码(Hash过后的)
|
||||
Password string `json:"password" gorm:"column:password;type:varchar(200);not null" description:"用户密码"`
|
||||
// 用户描述
|
||||
Description string `json:"description" gorm:"column:description;type:varchar(200);not null" description:"用户描述"`
|
||||
// 用户类型
|
||||
Type TYPE `json:"type" gorm:"column:type;type:varchar(200);not null" description:"用户类型"`
|
||||
// 用户描述
|
||||
Domain string `json:"domain" gorm:"column:domain;type:varchar(200);" description:"用户所属域"`
|
||||
|
||||
// 支持接口调用
|
||||
EnabledApi bool `json:"enabled_api" gorm:"column:enabled_api;type:tinyint(1)" optional:"true" description:"支持接口调用"`
|
||||
// 是不是管理员
|
||||
IsAdmin bool `json:"is_admin" gorm:"column:is_admin;type:tinyint(1)" optional:"true" description:"是不是管理员"`
|
||||
// 用户状态,01:正常,02:冻结
|
||||
Locked bool `json:"stat" gorm:"column:stat;type:tinyint(1)" optional:"true" description:"用户状态, 01:正常, 02:冻结"`
|
||||
// 激活,1:激活,0:未激活
|
||||
Activate bool `json:"activate" gorm:"column:activate;type:tinyint(1)" optional:"true" description:"激活, 1: 激活, 0: 未激活"`
|
||||
// 生日
|
||||
Birthday *time.Time `json:"birthday" gorm:"column:birthday;type:varchar(200)" optional:"true" description:"生日"`
|
||||
// 昵称
|
||||
NickName string `json:"nick_name" gorm:"column:nick_name;type:varchar(200)" optional:"true" description:"昵称"`
|
||||
// 头像图片
|
||||
UserIcon string `json:"user_icon" gorm:"column:user_icon;type:varchar(500)" optional:"true" description:"头像图片"`
|
||||
// 性别, 1:男,2:女,0:保密
|
||||
Sex SEX `json:"sex" gorm:"column:sex;type:tinyint(1)" optional:"true" description:"性别, 1:男, 2:女, 0: 保密"`
|
||||
|
||||
// 邮箱
|
||||
Email string `json:"email" gorm:"column:email;type:varchar(200);index" description:"邮箱" unique:"true"`
|
||||
// 邮箱是否验证ok
|
||||
IsEmailConfirmed bool `json:"is_email_confirmed" gorm:"column:is_email_confirmed;type:tinyint(1)" optional:"true" description:"邮箱是否验证ok"`
|
||||
// 手机
|
||||
Mobile string `json:"mobile" gorm:"column:mobile;type:varchar(200);index" optional:"true" description:"手机" unique:"true"`
|
||||
// 手机释放验证ok
|
||||
IsMobileConfirmed bool `json:"is_mobile_confirmed" gorm:"column:is_mobile_confirmed;type:tinyint(1)" optional:"true" description:"手机释放验证ok"`
|
||||
// 手机登录标识
|
||||
MobileTGC string `json:"mobile_tgc" gorm:"column:mobile_tgc;type:char(64)" optional:"true" description:"手机登录标识"`
|
||||
// 标签
|
||||
Label string `json:"label" gorm:"column:label;type:varchar(200);index" optional:"true" description:"标签"`
|
||||
// 其他扩展信息
|
||||
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" optional:"true" description:"其他扩展信息"`
|
||||
|
||||
isHashed bool `json:"-"`
|
||||
}
|
||||
|
||||
func (req *CreateUserRequest) SetIsHashed() {
|
||||
req.isHashed = true
|
||||
}
|
||||
|
||||
func (req *CreateUserRequest) Validate() error {
|
||||
if req.UserName == "" || req.Password == "" {
|
||||
return fmt.Errorf("用户名或者密码需要填写")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *CreateUserRequest) PasswordHash() {
|
||||
if req.isHashed {
|
||||
return
|
||||
}
|
||||
|
||||
b, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
req.Password = string(b)
|
||||
req.isHashed = true
|
||||
}
|
160
devcloud/mcenter/design.drawio
Normal file
160
devcloud/mcenter/design.drawio
Normal file
@ -0,0 +1,160 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="kBUSl4Twz2xUubsumQ87" name="第 1 页">
|
||||
<mxGraphModel dx="892" dy="554" 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="24" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="380" y="230" width="330" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="60" y="230" width="320" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="client" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="60" y="90" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="3" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="120" y="226" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="HTTP API" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="4" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1648" y="6" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Basic Auth: base64(用户名:密码)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="160" y="170" width="180" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="7" target="8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="user/pass" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.0981" y="-3" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="权限拦截器" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="70" y="250" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="用户模块" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="250" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="client" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="380" y="90" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="440" y="230" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="基于访问令牌" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="460" y="170" width="180" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="15" target="16" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="用自己的身份信息 换一个凭证<div>1. user/pass</div><div>2. 手机登录</div><div>3. 三分认证: 飞书/支付宝</div><div>4. LDAP 凭证</div><div>...</div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="17" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.3099" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="730" y="60" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="令牌颁发" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="570" y="250" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="业务服务" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="80" y="350" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="业务服务" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="350" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="业务服务" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="580" y="350" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="23" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="22" target="16" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="权限拦截器" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="250" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="操作<div>具体的接口</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="560" y="510" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" style="edgeStyle=none;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" parent="1" source="26" target="29" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="110" y="500" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="Who&nbsp; &nbsp; &nbsp;角色&nbsp; &nbsp;Namespace<div>A&nbsp; 开发 项目A</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="110" y="620" width="330" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="授权" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="610" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" style="edgeStyle=none;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" parent="1" source="29" target="25" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="1:N&nbsp; 多个权限的集合" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="30" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.3254" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-30" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="角色<div><br></div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="293.5" y="510" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="100" y="810" width="790" height="150" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" style="html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;" parent="1" source="35" target="38" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="35" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="50" y="720" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="资源" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="120" y="890" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="39" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="38" target="36" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="41" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="38" target="40" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="权限拦截器" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="120" y="830" width="120" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="授权策略<div>用户的授权策略</div><div>User, Name, 接口</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="294" y="830" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="空间<div>Namepsace</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="460" y="830" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="角色<div>Role</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="620" y="830" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="接口列表" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="760" y="825" width="120" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="1090" width="330" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" value="用户认证" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="1060" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="47" target="48">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="token" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="1119" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="48" value="user" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="1119" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
18
devcloud/mcenter/main.go
Normal file
18
devcloud/mcenter/main.go
Normal file
@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/infraboard/mcube/v2/ioc/server/cmd"
|
||||
|
||||
// 加载的业务对象
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps"
|
||||
|
||||
// 非功能性模块
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/apidoc/restful"
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/health/restful"
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/restful"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 启动
|
||||
cmd.Start()
|
||||
}
|
17
devcloud/mcenter/test/set_up.go
Normal file
17
devcloud/mcenter/test/set_up.go
Normal file
@ -0,0 +1,17 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
// 要注册哪些对象, Book, Comment
|
||||
|
||||
// 加载的业务对象
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps"
|
||||
)
|
||||
|
||||
func DevelopmentSet() {
|
||||
// import 后自动执行的逻辑
|
||||
// 工具对象的初始化, 需要的是绝对路径
|
||||
ioc.DevelopmentSetupWithPath(os.Getenv("CONFIG_PATH"))
|
||||
}
|
22
devcloud/ui.drawio
Normal file
22
devcloud/ui.drawio
Normal file
@ -0,0 +1,22 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="poUq6wZcfCQim5LskJLo" name="第 1 页">
|
||||
<mxGraphModel dx="830" dy="442" 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="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="60" y="40" width="690" height="390" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="130" y="60" width="120" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="60" width="120" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="430" y="60" width="120" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
BIN
docs/Go 语言大型项目开发必备技能之Ioc.pptx
Normal file
BIN
docs/Go 语言大型项目开发必备技能之Ioc.pptx
Normal file
Binary file not shown.
62
go.mod
62
go.mod
@ -4,43 +4,103 @@ go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0
|
||||
github.com/emicklei/go-restful/v3 v3.12.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/infraboard/mcube/v2 v2.0.59
|
||||
github.com/infraboard/modules v0.0.12
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 // indirect
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.72.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gorm.io/driver/postgres v1.5.11 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
178
go.sum
178
go.sum
@ -1,3 +1,7 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
@ -5,18 +9,55 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
|
||||
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0 h1:Ur+yGxoOH/7KRmcj/UoMFqC3VeNc9VOe+/XidumxTvk=
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0/go.mod h1:4CTuOXHFg3jkvCpnXN+Wkw5prVUnP8hIACssJTYorWo=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@ -29,39 +70,112 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infraboard/mcube/v2 v2.0.59 h1:NONiCPjN6xlbGCJx8+e+ZYZfXV58JByEMlzQ6ZZ+pXk=
|
||||
github.com/infraboard/mcube/v2 v2.0.59/go.mod h1:TbYs8cnD8Cg19sTdU0D+vqWAN+LzoxhMYWmAC2pfJkQ=
|
||||
github.com/infraboard/modules v0.0.12 h1:vQqm+JwzmhL+hcD9SV+WVlp9ecInc7NsbGahcTmJ0Wk=
|
||||
github.com/infraboard/modules v0.0.12/go.mod h1:NdgdH/NoeqibJmFPn9th+tisMuR862/crbXeH4FPMaU=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@ -73,29 +187,89 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 h1:Jjn3zoRz13f8b1bR6LrXWglx93Sbh4kYfwgmPju3E2k=
|
||||
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2/go.mod h1:wocb5pNrj/sjhWB9J5jctnC0K2eisSdz/nJJBNFHo+A=
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
|
||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.60.0 h1:jvVaMcOzU4abk6o8VCJJN5wRoFQxe3Afnc8fPcYbO0I=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.60.0/go.mod h1:NMt0e4UA3rY+CV+xyjE4wLhz/6BiydSOZhMeevBDZB0=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs=
|
||||
gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
43
skills/generate/generate.go
Normal file
43
skills/generate/generate.go
Normal file
@ -0,0 +1,43 @@
|
||||
package generate
|
||||
|
||||
func NewBookSet() *BookSet {
|
||||
return &BookSet{}
|
||||
}
|
||||
|
||||
type BookSet struct {
|
||||
// 总共多少个
|
||||
Total int64 `json:"total"`
|
||||
// book清单
|
||||
Items []string `json:"items"`
|
||||
}
|
||||
|
||||
func (b *BookSet) Add(item string) {
|
||||
b.Items = append(b.Items, item)
|
||||
}
|
||||
|
||||
type CommentSet struct {
|
||||
// 总共多少个
|
||||
Total int64 `json:"total"`
|
||||
// book清单
|
||||
Items []int `json:"items"`
|
||||
}
|
||||
|
||||
func (b *CommentSet) Add(item int) {
|
||||
b.Items = append(b.Items, item)
|
||||
}
|
||||
|
||||
func NewSet[T any]() *Set[T] {
|
||||
return &Set[T]{}
|
||||
}
|
||||
|
||||
// 使用[]来声明类型参数
|
||||
type Set[T any] struct {
|
||||
// 总共多少个
|
||||
Total int64 `json:"total"`
|
||||
// book清单
|
||||
Items []T `json:"items"`
|
||||
}
|
||||
|
||||
func (b *Set[T]) Add(item T) {
|
||||
b.Items = append(b.Items, item)
|
||||
}
|
19
skills/generate/generate_test.go
Normal file
19
skills/generate/generate_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package generate_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/skills/generate"
|
||||
)
|
||||
|
||||
func TestStringSet(t *testing.T) {
|
||||
set := generate.NewSet[string]()
|
||||
set.Add("test")
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestIntSet(t *testing.T) {
|
||||
set := generate.NewSet[int]()
|
||||
set.Add(10)
|
||||
t.Log(set)
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := gin.Default()
|
||||
server := gin.New()
|
||||
|
||||
server.GET("/hello", func(ctx *gin.Context) {
|
||||
ctx.String(200, "Gin Hello World!")
|
||||
|
181
skills/gorestful/README.md
Normal file
181
skills/gorestful/README.md
Normal file
@ -0,0 +1,181 @@
|
||||
# gorestful Web框架
|
||||
|
||||
[go-restful](https://github.com/emicklei/go-restful)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
restful "github.com/emicklei/go-restful/v3"
|
||||
"github.com/go-openapi/spec"
|
||||
)
|
||||
|
||||
// UserResource is the REST layer to the User domain
|
||||
type UserResource struct {
|
||||
// normally one would use DAO (data access object)
|
||||
users map[string]User
|
||||
}
|
||||
|
||||
// WebService creates a new service that can handle REST requests for User resources.
|
||||
func (u UserResource) WebService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.
|
||||
Path("/users").
|
||||
Consumes(restful.MIME_XML, restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well
|
||||
|
||||
tags := []string{"users"}
|
||||
|
||||
ws.Route(ws.GET("/").To(u.findAllUsers).
|
||||
// docs
|
||||
Doc("get all users").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Writes([]User{}).
|
||||
Returns(200, "OK", []User{}))
|
||||
|
||||
ws.Route(ws.GET("/{user-id}").To(u.findUser).
|
||||
// docs
|
||||
Doc("get a user").
|
||||
Param(ws.PathParameter("user-id", "identifier of the user").DataType("integer").DefaultValue("1")).
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Writes(User{}). // on the response
|
||||
Returns(200, "OK", User{}).
|
||||
Returns(404, "Not Found", nil))
|
||||
|
||||
ws.Route(ws.PUT("/{user-id}").To(u.upsertUser).
|
||||
// docs
|
||||
Doc("update a user").
|
||||
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(User{})) // from the request
|
||||
|
||||
ws.Route(ws.POST("").To(u.createUser).
|
||||
// docs
|
||||
Doc("create a user").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(User{})) // from the request
|
||||
|
||||
ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
|
||||
// docs
|
||||
Doc("delete a user").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
// GET http://localhost:8080/users
|
||||
func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) {
|
||||
log.Println("findAllUsers")
|
||||
list := []User{}
|
||||
for _, each := range u.users {
|
||||
list = append(list, each)
|
||||
}
|
||||
response.WriteEntity(list)
|
||||
}
|
||||
|
||||
// GET http://localhost:8080/users/1
|
||||
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
|
||||
log.Println("findUser")
|
||||
id := request.PathParameter("user-id")
|
||||
usr := u.users[id]
|
||||
if len(usr.ID) == 0 {
|
||||
response.WriteErrorString(http.StatusNotFound, "User could not be found.")
|
||||
} else {
|
||||
response.WriteEntity(usr)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT http://localhost:8080/users/1
|
||||
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
|
||||
func (u *UserResource) upsertUser(request *restful.Request, response *restful.Response) {
|
||||
log.Println("upsertUser")
|
||||
usr := User{ID: request.PathParameter("user-id")}
|
||||
err := request.ReadEntity(&usr)
|
||||
if err == nil {
|
||||
u.users[usr.ID] = usr
|
||||
response.WriteEntity(usr)
|
||||
} else {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// POST http://localhost:8080/users
|
||||
// <User><Id>1</Id><Name>Melissa</Name></User>
|
||||
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
|
||||
log.Println("createUser")
|
||||
usr := User{ID: fmt.Sprintf("%d", time.Now().Unix())}
|
||||
err := request.ReadEntity(&usr)
|
||||
if err == nil {
|
||||
u.users[usr.ID] = usr
|
||||
response.WriteHeaderAndEntity(http.StatusCreated, usr)
|
||||
} else {
|
||||
response.WriteError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE http://localhost:8080/users/1
|
||||
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
|
||||
log.Println("removeUser")
|
||||
id := request.PathParameter("user-id")
|
||||
delete(u.users, id)
|
||||
}
|
||||
|
||||
func main() {
|
||||
u := UserResource{map[string]User{}}
|
||||
restful.DefaultContainer.Add(u.WebService())
|
||||
|
||||
config := restfulspec.Config{
|
||||
WebServices: restful.RegisteredWebServices(), // you control what services are visible
|
||||
APIPath: "/apidocs.json",
|
||||
PostBuildSwaggerObjectHandler: enrichSwaggerObject}
|
||||
restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))
|
||||
|
||||
// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
|
||||
// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
|
||||
// Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json
|
||||
http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/Projects/swagger-ui/dist"))))
|
||||
|
||||
log.Printf("start listening on localhost:8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func enrichSwaggerObject(swo *spec.Swagger) {
|
||||
swo.Info = &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "UserService",
|
||||
Description: "Resource for managing Users",
|
||||
Contact: &spec.ContactInfo{
|
||||
ContactInfoProps: spec.ContactInfoProps{
|
||||
Name: "john",
|
||||
Email: "john@doe.rp",
|
||||
URL: "http://johndoe.org",
|
||||
},
|
||||
},
|
||||
License: &spec.License{
|
||||
LicenseProps: spec.LicenseProps{
|
||||
Name: "MIT",
|
||||
URL: "http://mit.org",
|
||||
},
|
||||
},
|
||||
Version: "1.0.0",
|
||||
},
|
||||
}
|
||||
swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{
|
||||
Name: "users",
|
||||
Description: "Managing users"}}}
|
||||
}
|
||||
|
||||
// User is just a sample type
|
||||
type User struct {
|
||||
ID string `xml:"id" json:"id" description:"identifier of the user"`
|
||||
Name string `xml:"name" json:"name" description:"name of the user" default:"john"`
|
||||
Age int `xml:"age" json:"age" description:"age of the user" default:"21"`
|
||||
}
|
||||
```
|
85
skills/gorestful/framework.drawio
Normal file
85
skills/gorestful/framework.drawio
Normal file
@ -0,0 +1,85 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="EsWSgBRg6HmPYJxvZoKw" name="第 1 页">
|
||||
<mxGraphModel dx="812" dy="419" 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="*gin.Engine<div>Server</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="140" width="210" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Gin" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="90" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="GoRestful" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="650" y="100" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="<div><font color="#000000">restful.DefaultContainer</font></div><div>Server</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="414" y="140" width="210" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="230" width="210" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Group" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="230" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="group<div>/books</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="140" y="250" width="80" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="group<div>/comments</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="240" y="250" width="80" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="414" y="230" width="210" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="WebService" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="650" y="230" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="ws<div>/books</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="250" width="80" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="ws<div>/comments</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="530" y="250" width="80" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="restful.DefaultContainer.Add(u.WebService())" style="text;whiteSpace=wrap;" vertex="1" parent="1">
|
||||
<mxGeometry x="557" y="50" width="270" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="root.Group("")" style="text;whiteSpace=wrap;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="50" width="270" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="360" width="210" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="Route" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="360" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="route<div>get('/')</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="150" y="380" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="route<div>get('/')</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="245" y="380" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="414" y="360" width="210" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="Route" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="660" y="360" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="ws.GET("/").To(u.findAllUsers)" style="text;whiteSpace=wrap;" vertex="1" parent="1">
|
||||
<mxGeometry x="627" y="330" width="200" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="route<div>ws.GET('/')</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="430" y="380" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="route<div>ws.POST('/')</div>" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="540" y="380" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="findAllUsers(request *restful.Request, response *restful.Response)" style="text;whiteSpace=wrap;" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="20" width="390" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="<div style="color: #f8f8f2;background-color: #272822;font-family: 'Cascadia Code NF', Menlo, Monaco, 'Courier New', monospace, Menlo, Monaco, 'Courier New', monospace;font-weight: normal;font-size: 12px;line-height: 18px;white-space: pre;"><div><span style="color: #a6e22e;">queryBook</span><span style="color: #f8f8f2;">(</span><span style="color: #fd971f;font-style: italic;">ctx</span><span style="color: #f8f8f2;"> </span><span style="color: #f92672;">*</span><span style="color: #a6e22e;text-decoration: underline;">gin</span><span style="color: #f8f8f2;">.</span><span style="color: #a6e22e;text-decoration: underline;">Context</span><span style="color: #f8f8f2;">)</span></div></div>" style="text;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="20" width="220" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user