Compare commits

...

7 Commits

Author SHA1 Message Date
c315c4747c 补充API Doc 2025-05-31 18:09:24 +08:00
50d39008de 补充token模块 2025-05-31 17:20:28 +08:00
47e4772618 添加token颁发器 2025-05-31 16:14:34 +08:00
426e493967 补充hash 2025-05-31 12:03:22 +08:00
cc4729de15 token设计 2025-05-31 10:50:37 +08:00
c176c4086f 补充mcenter业务板块 2025-05-25 18:05:38 +08:00
c7fa46290c 实现ioc 2025-05-25 17:01:17 +08:00
53 changed files with 2508 additions and 5 deletions

View File

@ -1,6 +1,6 @@
<mxfile host="65bd71144e"> <mxfile host="65bd71144e">
<diagram id="j5aaZ-Vtlo4HorS1MuuZ" name="第 1 页"> <diagram id="j5aaZ-Vtlo4HorS1MuuZ" name="第 1 页">
<mxGraphModel dx="1604" dy="1564" 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"> <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> <root>
<mxCell id="0"/> <mxCell id="0"/>
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/>
@ -10,7 +10,7 @@
<mxCell id="4" value="CommentServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <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"/> <mxGeometry x="250" y="210" width="120" height="60" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="5" 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="6" target="12"> <mxCell id="5" value="读取配置&lt;div&gt;调用Init&lt;/div&gt;" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="6" target="12">
<mxGeometry relative="1" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">

7
devcloud/.vscode/settings.json vendored Normal file
View File

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

View File

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

View 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"

View File

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

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

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

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

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

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

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

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

View 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"`
}

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

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

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

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

View File

@ -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()
}

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

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

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

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

View 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会产生不同的结果
![alt text](image.png)
password --> hash
password + 随机字符串(salt) --> (salt)hash它也是随机
输入: password + 随机字符串(salt: 原来hash中的sal部分) == hash它也是随机(数据库)
```go
import (
"golang.org/x/crypto/bcrypt"
)
func (req *CreateUserRequest) PasswordHash() {
if req.isHashed {
return
}
b, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
req.Password = string(b)
req.isHashed = true
}
// 判断该用户的密码是否正确
func (u *User) CheckPassword(password string) error {
// u.Password hash过后的只
// (password 原始值 + hash值中提区salt)
return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
}
```

View File

@ -0,0 +1 @@
package api

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

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

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

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

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

View 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"`
}

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

View 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="用自己的身份信息 换一个凭证&lt;div&gt;1. user/pass&lt;/div&gt;&lt;div&gt;2. 手机登录&lt;/div&gt;&lt;div&gt;3. 三分认证: 飞书/支付宝&lt;/div&gt;&lt;div&gt;4. LDAP 凭证&lt;/div&gt;&lt;div&gt;...&lt;/div&gt;" 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="操作&lt;div&gt;具体的接口&lt;/div&gt;" 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&amp;nbsp; &amp;nbsp; &amp;nbsp;角色&amp;nbsp; &amp;nbsp;Namespace&lt;div&gt;A&amp;nbsp; 开发 项目A&lt;/div&gt;" 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&amp;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="角色&lt;div&gt;&lt;br&gt;&lt;/div&gt;" 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="授权策略&lt;div&gt;用户的授权策略&lt;/div&gt;&lt;div&gt;User, Name, 接口&lt;/div&gt;" 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="空间&lt;div&gt;Namepsace&lt;/div&gt;" 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="角色&lt;div&gt;Role&lt;/div&gt;" 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
View 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()
}

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

6
go.mod
View File

@ -4,10 +4,14 @@ go 1.24.1
require ( require (
github.com/caarlos0/env/v6 v6.10.1 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/gin-gonic/gin v1.10.0
github.com/infraboard/mcube/v2 v2.0.59 github.com/infraboard/mcube/v2 v2.0.59
github.com/infraboard/modules v0.0.12
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.38.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.26.0 gorm.io/gorm v1.26.0
@ -73,6 +77,7 @@ require (
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql 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/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/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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect
@ -83,7 +88,6 @@ require (
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/arch v0.15.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/net v0.38.0 // indirect
golang.org/x/sync v0.14.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect

31
go.sum
View File

@ -18,11 +18,17 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 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/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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 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 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
@ -38,12 +44,18 @@ 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/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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 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 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 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 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 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 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -75,6 +87,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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 h1:NONiCPjN6xlbGCJx8+e+ZYZfXV58JByEMlzQ6ZZ+pXk=
github.com/infraboard/mcube/v2 v2.0.59/go.mod h1:TbYs8cnD8Cg19sTdU0D+vqWAN+LzoxhMYWmAC2pfJkQ= 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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -97,14 +111,20 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 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/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/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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -122,6 +142,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -154,6 +175,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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/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.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.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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -171,10 +193,14 @@ github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/Oa
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= 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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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 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/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 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/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 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 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 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
@ -217,11 +243,16 @@ google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=

181
skills/gorestful/README.md Normal file
View 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"`
}
```

View 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&lt;div&gt;Server&lt;/div&gt;" 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="&lt;div&gt;&lt;font color=&quot;#000000&quot;&gt;restful.DefaultContainer&lt;/font&gt;&lt;/div&gt;&lt;div&gt;Server&lt;/div&gt;" 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&lt;div&gt;/books&lt;/div&gt;" 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&lt;div&gt;/comments&lt;/div&gt;" 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&lt;div&gt;/books&lt;/div&gt;" 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&lt;div&gt;/comments&lt;/div&gt;" 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(&quot;&quot;)" 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&lt;div&gt;get(&#39;/&#39;)&lt;/div&gt;" 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&lt;div&gt;get(&#39;/&#39;)&lt;/div&gt;" 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(&quot;/&quot;).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&lt;div&gt;ws.GET(&#39;/&#39;)&lt;/div&gt;" 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&lt;div&gt;ws.POST(&#39;/&#39;)&lt;/div&gt;" 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="&lt;div style=&quot;color: #f8f8f2;background-color: #272822;font-family: &#39;Cascadia Code NF&#39;, Menlo, Monaco, &#39;Courier New&#39;, monospace, Menlo, Monaco, &#39;Courier New&#39;, monospace;font-weight: normal;font-size: 12px;line-height: 18px;white-space: pre;&quot;&gt;&lt;div&gt;&lt;span style=&quot;color: #a6e22e;&quot;&gt;queryBook&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #fd971f;font-style: italic;&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #f92672;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #a6e22e;text-decoration: underline;&quot;&gt;gin&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e;text-decoration: underline;&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2;&quot;&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;" 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>

View File

@ -2,4 +2,47 @@
![alt text](image.png) ![alt text](image.png)
# 没有Ioc # 简易版本Ioc
```go
// ioc 容器功能定义
type Contaienr interface {
Registry(name string, obj Object)
Get(name string) Object
// 初始化所有已经注册的对象
Init() error
}
// 注册对象的约束
type Object interface {
Init() error
}
```
```go
// ioc 容器
type MapContainer struct {
name string
storage map[string]Object
}
func (m *MapContainer) Registry(name string, obj Object) {
m.storage[name] = obj
}
func (m *MapContainer) Get(name string) Object {
return m.storage[name]
}
// 初始化所有已经注册的对象
func (m *MapContainer) Init() error {
for _, v := range m.storage {
if err := v.Init(); err != nil {
return err
}
}
return nil
}
```

View File

@ -27,6 +27,8 @@ func (m *MapContainer) Get(name string) Object {
} }
// 初始化所有已经注册的对象 // 初始化所有已经注册的对象
// yaml, toml map[key]obj
// [object_name]
func (m *MapContainer) Init() error { func (m *MapContainer) Init() error {
for _, v := range m.storage { for _, v := range m.storage {
if err := v.Init(); err != nil { if err := v.Init(); err != nil {