From 9eb977cfbf9c3f5851666e06f17e3d7915561689 Mon Sep 17 00:00:00 2001
From: yumaojun03 <719118794@qq.com>
Date: Sun, 8 Jun 2025 15:11:14 +0800
Subject: [PATCH] update role
---
devcloud/mcenter/apps/endpoint/enum.go | 2 +
devcloud/mcenter/apps/role/api_permission.go | 102 +++++++
devcloud/mcenter/apps/role/design.drawio | 19 ++
devcloud/mcenter/apps/role/enum.go | 18 ++
.../mcenter/apps/role/impl/api_permission.go | 110 ++++++++
.../apps/role/impl/api_permission_test.go | 48 ++++
devcloud/mcenter/apps/role/impl/imp_test.go | 18 ++
devcloud/mcenter/apps/role/impl/impl.go | 31 ++
devcloud/mcenter/apps/role/impl/role.go | 103 +++++++
devcloud/mcenter/apps/role/impl/role_test.go | 59 ++++
.../mcenter/apps/role/impl/view_permission.go | 82 ++++++
devcloud/mcenter/apps/role/interface.go | 265 ++++++++++++++++++
devcloud/mcenter/apps/role/model.go | 66 +++++
devcloud/mcenter/apps/role/view_permission.go | 45 +++
devcloud/mcenter/apps/token/impl/impl_test.go | 2 +-
.../token/issuers/password/issuer_test.go | 2 +-
.../issuers/private_token/issueer_test.go | 2 +-
devcloud/mcenter/apps/user/impl/impl_test.go | 2 +-
devcloud/mcenter/apps/view/model.go | 60 ++++
devcloud/mcenter/apps/view/page.go | 64 +++++
devcloud/mcenter/design.drawio | 14 +-
devcloud/mcenter/test/set_up.go | 2 +-
go.mod | 2 +-
homework/README.md | 6 +-
24 files changed, 1109 insertions(+), 15 deletions(-)
create mode 100644 devcloud/mcenter/apps/role/api_permission.go
create mode 100644 devcloud/mcenter/apps/role/design.drawio
create mode 100644 devcloud/mcenter/apps/role/enum.go
create mode 100644 devcloud/mcenter/apps/role/impl/api_permission.go
create mode 100644 devcloud/mcenter/apps/role/impl/api_permission_test.go
create mode 100644 devcloud/mcenter/apps/role/impl/imp_test.go
create mode 100644 devcloud/mcenter/apps/role/impl/impl.go
create mode 100644 devcloud/mcenter/apps/role/impl/role.go
create mode 100644 devcloud/mcenter/apps/role/impl/role_test.go
create mode 100644 devcloud/mcenter/apps/role/impl/view_permission.go
create mode 100644 devcloud/mcenter/apps/role/interface.go
create mode 100644 devcloud/mcenter/apps/role/model.go
create mode 100644 devcloud/mcenter/apps/role/view_permission.go
create mode 100644 devcloud/mcenter/apps/view/model.go
create mode 100644 devcloud/mcenter/apps/view/page.go
diff --git a/devcloud/mcenter/apps/endpoint/enum.go b/devcloud/mcenter/apps/endpoint/enum.go
index ea9f083..d1826a8 100644
--- a/devcloud/mcenter/apps/endpoint/enum.go
+++ b/devcloud/mcenter/apps/endpoint/enum.go
@@ -3,7 +3,9 @@ package endpoint
type ACCESS_MODE uint8
const (
+ // 只读模式
ACCESS_MODE_READ = iota
+ // 读写模式
ACCESS_MODE_READ_WRITE
)
diff --git a/devcloud/mcenter/apps/role/api_permission.go b/devcloud/mcenter/apps/role/api_permission.go
new file mode 100644
index 0000000..3941869
--- /dev/null
+++ b/devcloud/mcenter/apps/role/api_permission.go
@@ -0,0 +1,102 @@
+package role
+
+import (
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
+ "github.com/infraboard/mcube/v2/tools/pretty"
+ "github.com/infraboard/modules/iam/apps"
+)
+
+func NewApiPermission(roleId uint64, spec *ApiPermissionSpec) *ApiPermission {
+ return &ApiPermission{
+ ResourceMeta: *apps.NewResourceMeta(),
+ RoleId: roleId,
+ ApiPermissionSpec: *spec,
+ }
+}
+
+type ApiPermission struct {
+ // 基础数据
+ apps.ResourceMeta
+ // 角色Id
+ RoleId uint64 `json:"role_id" gorm:"column:role_id;index" description:"角色Id"`
+ // Api权限定义
+ ApiPermissionSpec
+}
+
+func (r *ApiPermission) TableName() string {
+ return "api_permissions"
+}
+
+func (r *ApiPermission) String() string {
+ return pretty.ToJSON(r)
+}
+
+func NewResourceActionApiPermissionSpec(service, resource, action string) *ApiPermissionSpec {
+ return &ApiPermissionSpec{
+ Extras: map[string]string{},
+ MatchBy: MATCH_BY_RESOURCE_ACTION,
+ Service: service,
+ Resource: resource,
+ Action: action,
+ }
+}
+
+type ApiPermissionSpec struct {
+ // 创建者ID
+ CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
+ // 角色描述
+ Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
+ // 权限匹配方式
+ MatchBy MATCH_BY `json:"match_by" gorm:"column:match_by;type:tinyint(1);index" bson:"match_by" description:"权限匹配方式"`
+ // MATCH_BY_ID 时指定的 Endpoint Id
+ EndpointId *uint64 `json:"endpoint_id" gorm:"column:endpoint_id;type:uint;index"`
+ // 操作标签
+ Label string `json:"label" gorm:"column:label;type:varchar(200);index"`
+ // 服务
+ Service string `json:"service" gorm:"column:service;type:varchar(100);index" bson:"service" description:"服务名称"`
+ // 资源列表
+ Resource string `json:"resource" gorm:"column:resource;type:varchar(100);index" bson:"resource" description:"资源名称"`
+ // 资源操作
+ Action string `json:"action" bson:"action" gorm:"column:action;type:varchar(100);index"`
+ // 读或者读写
+ AccessMode endpoint.ACCESS_MODE `json:"access_mode" bson:"access_mode" gorm:"column:access_mode;type:tinyint(1);index"`
+
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
+}
+
+func (a *ApiPermissionSpec) GetEndpointId() uint64 {
+ if a.EndpointId == nil {
+ return 0
+ }
+ return *a.EndpointId
+}
+
+// 判断是否有当前API的访问权限
+func (a *ApiPermissionSpec) IsMatch(target *endpoint.Endpoint) bool {
+ switch a.MatchBy {
+ case MATCH_BY_ID:
+ if a.EndpointId == nil {
+ return false
+ }
+ if *a.EndpointId == target.Id {
+ return true
+ }
+ case MATCH_BY_RESOURCE_ACCESS_MODE:
+ if a.AccessMode == target.AccessMode {
+ return true
+ }
+ case MATCH_BY_RESOURCE_ACTION:
+ if a.Service != "*" && a.Service != target.Service {
+ return false
+ }
+ if a.Resource != "*" && a.Resource != target.Resource {
+ return false
+ }
+ if a.Action != "*" && a.Action != target.Action {
+ return false
+ }
+ return true
+ }
+ return false
+}
diff --git a/devcloud/mcenter/apps/role/design.drawio b/devcloud/mcenter/apps/role/design.drawio
new file mode 100644
index 0000000..2d7cb36
--- /dev/null
+++ b/devcloud/mcenter/apps/role/design.drawio
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/devcloud/mcenter/apps/role/enum.go b/devcloud/mcenter/apps/role/enum.go
new file mode 100644
index 0000000..a7cda63
--- /dev/null
+++ b/devcloud/mcenter/apps/role/enum.go
@@ -0,0 +1,18 @@
+package role
+
+const (
+ ADMIN = "admin"
+)
+
+type MATCH_BY int32
+
+const (
+ // 针对某一个具体的接口进行授权
+ MATCH_BY_ID = iota
+ // 通过标签来进行 API接口授权
+ MATCH_BY_LABLE
+ // 通过资源和动作来进行授权, user::list
+ MATCH_BY_RESOURCE_ACTION
+ // 通过资源的访问模式进行授权
+ MATCH_BY_RESOURCE_ACCESS_MODE
+)
diff --git a/devcloud/mcenter/apps/role/impl/api_permission.go b/devcloud/mcenter/apps/role/impl/api_permission.go
new file mode 100644
index 0000000..a1e5fc3
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/api_permission.go
@@ -0,0 +1,110 @@
+package impl
+
+import (
+ "context"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+ "github.com/infraboard/mcube/v2/exception"
+ "github.com/infraboard/mcube/v2/ioc/config/datasource"
+ "github.com/infraboard/mcube/v2/types"
+ "gorm.io/gorm"
+)
+
+// 添加角色关联API
+func (i *RoleServiceImpl) AddApiPermission(ctx context.Context, in *role.AddApiPermissionRequest) ([]*role.ApiPermission, error) {
+ if err := in.Validate(); err != nil {
+ return nil, exception.NewBadRequest("validate add api permission error, %s", err)
+ }
+
+ perms := []*role.ApiPermission{}
+ if err := datasource.DBFromCtx(ctx).Transaction(func(tx *gorm.DB) error {
+ for i := range in.Items {
+ item := in.Items[i]
+ perm := role.NewApiPermission(in.RoleId, item)
+ if err := tx.Save(perm).Error; err != nil {
+ return err
+ }
+ perms = append(perms, perm)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return perms, nil
+}
+
+// 查询角色关联的权限条目
+func (i *RoleServiceImpl) QueryApiPermission(ctx context.Context, in *role.QueryApiPermissionRequest) ([]*role.ApiPermission, error) {
+ query := datasource.DBFromCtx(ctx).Model(&role.ApiPermission{})
+ if len(in.RoleIds) > 0 {
+ query = query.Where("role_id IN ?", in.RoleIds)
+ }
+ if len(in.ApiPermissionIds) > 0 {
+ query = query.Where("id IN ?", in.ApiPermissionIds)
+ }
+
+ perms := []*role.ApiPermission{}
+ if err := query.
+ Order("created_at desc").
+ Find(&perms).Error; err != nil {
+ return nil, err
+ }
+ return perms, nil
+}
+
+// 移除角色关联API
+func (i *RoleServiceImpl) RemoveApiPermission(ctx context.Context, in *role.RemoveApiPermissionRequest) ([]*role.ApiPermission, error) {
+ if err := in.Validate(); err != nil {
+ return nil, err
+ }
+ perms, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.RoleId).AddPermissionId(in.ApiPermissionIds...))
+ if err != nil {
+ return nil, err
+ }
+
+ if err := datasource.DBFromCtx(ctx).
+ Where("role_id = ?", in.RoleId).
+ Where("id IN ?", in.ApiPermissionIds).
+ Delete(&role.ApiPermission{}).
+ Error; err != nil {
+ return nil, err
+ }
+
+ return perms, nil
+}
+
+// 查询匹配到的Api接口列表
+func (i *RoleServiceImpl) QueryMatchedEndpoint(ctx context.Context, in *role.QueryMatchedEndpointRequest) (*types.Set[*endpoint.Endpoint], error) {
+ set := types.New[*endpoint.Endpoint]()
+
+ // 查询角色的权限
+ perms, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.RoleIds...))
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询服务的Endpoint列表
+ endpointReq := endpoint.NewQueryEndpointRequest()
+ for _, perm := range perms {
+ endpointReq.WithService(perm.Service)
+ }
+ endpoints, err := endpoint.GetService().QueryEndpoint(ctx, endpointReq)
+ if err != nil {
+ return nil, err
+ }
+
+ // 找出能匹配的API
+ endpoints.ForEach(func(t *endpoint.Endpoint) {
+ for _, perm := range perms {
+ if perm.IsMatch(t) {
+ if !endpoint.IsEndpointExist(set, t) {
+ set.Add(t)
+ }
+ }
+ }
+ })
+
+ return set, nil
+}
diff --git a/devcloud/mcenter/apps/role/impl/api_permission_test.go b/devcloud/mcenter/apps/role/impl/api_permission_test.go
new file mode 100644
index 0000000..97d1dc3
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/api_permission_test.go
@@ -0,0 +1,48 @@
+package impl_test
+
+import (
+ "testing"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+)
+
+func TestQueryApiPermission(t *testing.T) {
+ req := role.NewQueryApiPermissionRequest()
+ req.AddRoleId(2)
+ set, err := impl.QueryApiPermission(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(set)
+}
+
+func TestAddApiPermission(t *testing.T) {
+ req := role.NewAddApiPermissionRequest(1)
+ req.Add(role.NewResourceActionApiPermissionSpec("devcloud", "user", "list"))
+ set, err := impl.AddApiPermission(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(set)
+}
+
+func TestQueryMatchedEndpoint(t *testing.T) {
+ req := role.NewQueryMatchedEndpointRequest()
+ req.Add(1)
+ set, err := impl.QueryMatchedEndpoint(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(set)
+}
+
+func TestRemoveApiPermission(t *testing.T) {
+ req := role.NewRemoveApiPermissionRequest(2)
+ req.Add(2)
+ set, err := impl.RemoveApiPermission(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Log(set)
+}
diff --git a/devcloud/mcenter/apps/role/impl/imp_test.go b/devcloud/mcenter/apps/role/impl/imp_test.go
new file mode 100644
index 0000000..8ae453e
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/imp_test.go
@@ -0,0 +1,18 @@
+package impl_test
+
+import (
+ "context"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+ "122.51.31.227/go-course/go18/devcloud/mcenter/test"
+)
+
+var (
+ impl role.Service
+ ctx = context.Background()
+)
+
+func init() {
+ test.DevelopmentSetUp()
+ impl = role.GetService()
+}
diff --git a/devcloud/mcenter/apps/role/impl/impl.go b/devcloud/mcenter/apps/role/impl/impl.go
new file mode 100644
index 0000000..5815382
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/impl.go
@@ -0,0 +1,31 @@
+package impl
+
+import (
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+ "github.com/infraboard/mcube/v2/ioc"
+ "github.com/infraboard/mcube/v2/ioc/config/datasource"
+)
+
+func init() {
+ ioc.Controller().Registry(&RoleServiceImpl{})
+}
+
+var _ role.Service = (*RoleServiceImpl)(nil)
+
+type RoleServiceImpl struct {
+ ioc.ObjectImpl
+}
+
+func (i *RoleServiceImpl) Init() error {
+ if datasource.Get().AutoMigrate {
+ err := datasource.DB().AutoMigrate(&role.Role{}, &role.ApiPermission{}, &role.ViewPermission{})
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (i *RoleServiceImpl) Name() string {
+ return role.AppName
+}
diff --git a/devcloud/mcenter/apps/role/impl/role.go b/devcloud/mcenter/apps/role/impl/role.go
new file mode 100644
index 0000000..2956187
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/role.go
@@ -0,0 +1,103 @@
+package impl
+
+import (
+ "context"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+ "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 *RoleServiceImpl) CreateRole(ctx context.Context, in *role.CreateRoleRequest) (*role.Role, error) {
+ if err := in.Validate(); err != nil {
+ return nil, err
+ }
+
+ ins := role.NewRole()
+ ins.CreateRoleRequest = *in
+
+ if err := datasource.DBFromCtx(ctx).
+ Create(ins).
+ Error; err != nil {
+ return nil, err
+ }
+ return ins, nil
+}
+
+// 列表查询
+func (i *RoleServiceImpl) QueryRole(ctx context.Context, in *role.QueryRoleRequest) (*types.Set[*role.Role], error) {
+ set := types.New[*role.Role]()
+
+ query := datasource.DBFromCtx(ctx).Model(&role.Role{})
+ if len(in.RoleIds) > 0 {
+ query = query.Where("id IN ?", in.RoleIds)
+ in.PageSize = uint64(len(in.RoleIds))
+ }
+ err := query.Count(&set.Total).Error
+ if err != nil {
+ return nil, err
+ }
+
+ err = query.
+ Order("created_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 *RoleServiceImpl) DescribeRole(ctx context.Context, in *role.DescribeRoleRequest) (*role.Role, error) {
+ query := datasource.DBFromCtx(ctx)
+
+ ins := &role.Role{}
+ if err := query.Where("id = ?", in.Id).First(ins).Error; err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, exception.NewNotFound("role %d not found", in.Id)
+ }
+ return nil, err
+ }
+
+ pm, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.Id))
+ if err != nil {
+ return nil, err
+ }
+ ins.ApiPermissions = pm
+
+ return ins, nil
+}
+
+// 更新角色
+func (i *RoleServiceImpl) UpdateRole(ctx context.Context, in *role.UpdateRoleRequest) (*role.Role, error) {
+ descReq := role.NewDescribeRoleRequest()
+ descReq.SetId(in.Id)
+ ins, err := i.DescribeRole(ctx, descReq)
+ if err != nil {
+ return nil, err
+ }
+
+ ins.CreateRoleRequest = in.CreateRoleRequest
+ return ins, datasource.DBFromCtx(ctx).Where("id = ?", in.Id).Updates(ins).Error
+}
+
+// 删除角色
+func (i *RoleServiceImpl) DeleteRole(ctx context.Context, in *role.DeleteRoleRequest) (*role.Role, error) {
+ descReq := role.NewDescribeRoleRequest()
+ descReq.SetId(in.Id)
+ ins, err := i.DescribeRole(ctx, descReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return ins, datasource.DBFromCtx(ctx).
+ Where("id = ?", in.Id).
+ Delete(&role.Role{}).
+ Error
+}
diff --git a/devcloud/mcenter/apps/role/impl/role_test.go b/devcloud/mcenter/apps/role/impl/role_test.go
new file mode 100644
index 0000000..2ddc771
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/role_test.go
@@ -0,0 +1,59 @@
+package impl_test
+
+import (
+ "testing"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+)
+
+func TestQueryRole(t *testing.T) {
+ req := role.NewQueryRoleRequest()
+ set, err := impl.QueryRole(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(set)
+}
+
+func TestDescribeRole(t *testing.T) {
+ req := role.NewDescribeRoleRequest()
+ req.SetId(1)
+ ins, err := impl.DescribeRole(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(ins)
+}
+
+func TestCreateAdminRole(t *testing.T) {
+ req := role.NewCreateRoleRequest()
+ req.Name = "admin"
+ req.Description = "管理员"
+ ins, err := impl.CreateRole(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(ins)
+}
+
+func TestCreateGuestRole(t *testing.T) {
+ req := role.NewCreateRoleRequest()
+ req.Name = "guest"
+ req.Description = "访客"
+ ins, err := impl.CreateRole(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(ins)
+}
+
+func TestCreateDevRole(t *testing.T) {
+ req := role.NewCreateRoleRequest()
+ req.Name = "dev"
+ req.Description = "开发"
+ ins, err := impl.CreateRole(ctx, req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(ins)
+}
diff --git a/devcloud/mcenter/apps/role/impl/view_permission.go b/devcloud/mcenter/apps/role/impl/view_permission.go
new file mode 100644
index 0000000..f79e79b
--- /dev/null
+++ b/devcloud/mcenter/apps/role/impl/view_permission.go
@@ -0,0 +1,82 @@
+package impl
+
+import (
+ "context"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
+ "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 *RoleServiceImpl) AddViewPermission(ctx context.Context, in *role.AddViewPermissionRequest) ([]*role.ViewPermission, error) {
+ if err := in.Validate(); err != nil {
+ return nil, exception.NewBadRequest("validate add view permission error, %s", err)
+ }
+
+ perms := []*role.ViewPermission{}
+ if err := datasource.DBFromCtx(ctx).Transaction(func(tx *gorm.DB) error {
+ for i := range in.Items {
+ item := in.Items[i]
+ perm := role.NewViewPermission(in.RoleId, item)
+ if err := tx.Save(perm).Error; err != nil {
+ return err
+ }
+ perms = append(perms, perm)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+
+ return perms, nil
+}
+
+// 查询角色关联的视图权限
+func (i *RoleServiceImpl) QueryViewPermission(ctx context.Context, in *role.QueryViewPermissionRequest) ([]*role.ViewPermission, error) {
+ query := datasource.DBFromCtx(ctx).Model(&role.ViewPermission{})
+ if len(in.RoleIds) > 0 {
+ query = query.Where("role_id IN ?", in.RoleIds)
+ }
+ if len(in.ViewPermissionIds) > 0 {
+ query = query.Where("in IN ?", in.ViewPermissionIds)
+ }
+
+ perms := []*role.ViewPermission{}
+ if err := query.Order("created_at desc").
+ Where("id IN ?", in.RoleIds).
+ Find(&perms).Error; err != nil {
+ return nil, err
+ }
+ return perms, nil
+}
+
+// 移除角色关联菜单
+func (i *RoleServiceImpl) RemoveViewPermission(ctx context.Context, in *role.RemoveViewPermissionRequest) ([]*role.ViewPermission, error) {
+ if err := in.Validate(); err != nil {
+ return nil, err
+ }
+
+ perms, err := i.QueryViewPermission(ctx, role.NewQueryViewPermissionRequest().AddRoleId(in.RoleId).AddPermissionId(in.ViewPermissionIds...))
+ if err != nil {
+ return nil, err
+ }
+
+ if err := datasource.DBFromCtx(ctx).
+ Where("role_id = ?", in.RoleId).
+ Where("id IN ?", in.ViewPermissionIds).
+ Delete(&role.ViewPermission{}).
+ Error; err != nil {
+ return nil, err
+ }
+
+ return perms, nil
+}
+
+// 查询能匹配到视图菜单
+func (i *RoleServiceImpl) QueryMatchedPage(ctx context.Context, in *role.QueryMatchedPageRequest) (*types.Set[*view.Menu], error) {
+ return nil, nil
+}
diff --git a/devcloud/mcenter/apps/role/interface.go b/devcloud/mcenter/apps/role/interface.go
new file mode 100644
index 0000000..c077a61
--- /dev/null
+++ b/devcloud/mcenter/apps/role/interface.go
@@ -0,0 +1,265 @@
+package role
+
+import (
+ "context"
+ "slices"
+
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
+ "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"
+ "github.com/infraboard/modules/iam/apps"
+)
+
+const (
+ AppName = "role"
+)
+
+func GetService() Service {
+ return ioc.Controller().Get(AppName).(Service)
+}
+
+type Service interface {
+ RoleService
+ ApiPermissionService
+ ViewPermissionService
+}
+
+// 角色管理
+type RoleService interface {
+ // 创建角色
+ CreateRole(context.Context, *CreateRoleRequest) (*Role, error)
+ // 列表查询
+ QueryRole(context.Context, *QueryRoleRequest) (*types.Set[*Role], error)
+ // 详情查询
+ DescribeRole(context.Context, *DescribeRoleRequest) (*Role, error)
+ // 更新角色
+ UpdateRole(context.Context, *UpdateRoleRequest) (*Role, error)
+ // 删除角色
+ DeleteRole(context.Context, *DeleteRoleRequest) (*Role, error)
+}
+
+func NewQueryRoleRequest() *QueryRoleRequest {
+ return &QueryRoleRequest{
+ PageRequest: request.NewDefaultPageRequest(),
+ RoleIds: []uint64{},
+ }
+}
+
+type QueryRoleRequest struct {
+ *request.PageRequest
+ WithMenuPermission bool `json:"with_menu_permission" form:"with_menu_permission"`
+ WithApiPermission bool `json:"with_api_permission" form:"with_api_permission"`
+ RoleIds []uint64 `json:"role_ids" form:"role_ids"`
+}
+
+func (r *QueryRoleRequest) AddRoleId(roleIds ...uint64) *QueryRoleRequest {
+ for _, rid := range roleIds {
+ if !slices.Contains(r.RoleIds, rid) {
+ r.RoleIds = append(r.RoleIds, rid)
+ }
+ }
+ return r
+}
+
+func NewDescribeRoleRequest() *DescribeRoleRequest {
+ return &DescribeRoleRequest{}
+}
+
+type DescribeRoleRequest struct {
+ apps.GetRequest
+}
+
+type UpdateRoleRequest struct {
+ apps.GetRequest
+ CreateRoleRequest
+}
+
+func NewDeleteRoleRequest() *DeleteRoleRequest {
+ return &DeleteRoleRequest{}
+}
+
+type DeleteRoleRequest struct {
+ apps.GetRequest
+}
+
+// 角色API接口管理
+type ApiPermissionService interface {
+ // 查询角色关联的权限条目
+ QueryApiPermission(context.Context, *QueryApiPermissionRequest) ([]*ApiPermission, error)
+ // 添加角色关联API
+ AddApiPermission(context.Context, *AddApiPermissionRequest) ([]*ApiPermission, error)
+ // 移除角色关联API
+ RemoveApiPermission(context.Context, *RemoveApiPermissionRequest) ([]*ApiPermission, error)
+ // 查询匹配到的Api接口列表
+ QueryMatchedEndpoint(context.Context, *QueryMatchedEndpointRequest) (*types.Set[*endpoint.Endpoint], error)
+}
+
+func NewQueryApiPermissionRequest() *QueryApiPermissionRequest {
+ return &QueryApiPermissionRequest{
+ RoleIds: []uint64{},
+ ApiPermissionIds: []uint64{},
+ }
+}
+
+type QueryApiPermissionRequest struct {
+ RoleIds []uint64 `json:"role_ids"`
+ ApiPermissionIds []uint64 `json:"api_permission_ids"`
+}
+
+func (r *QueryApiPermissionRequest) AddRoleId(roleIds ...uint64) *QueryApiPermissionRequest {
+ r.RoleIds = append(r.RoleIds, roleIds...)
+ return r
+}
+
+func (r *QueryApiPermissionRequest) AddPermissionId(permissionIds ...uint64) *QueryApiPermissionRequest {
+ r.ApiPermissionIds = append(r.ApiPermissionIds, permissionIds...)
+ return r
+}
+
+func NewQueryMatchedEndpointRequest() *QueryMatchedEndpointRequest {
+ return &QueryMatchedEndpointRequest{
+ RoleIds: []uint64{},
+ }
+}
+
+type QueryMatchedEndpointRequest struct {
+ RoleIds []uint64 `json:"role_ids" form:"role_ids"`
+}
+
+func (r *QueryMatchedEndpointRequest) Add(roleIds ...uint64) *QueryMatchedEndpointRequest {
+ for _, rid := range roleIds {
+ if !slices.Contains(r.RoleIds, rid) {
+ r.RoleIds = append(r.RoleIds, rid)
+ }
+ }
+ return r
+}
+
+func NewAddApiPermissionRequest(roleId uint64) *AddApiPermissionRequest {
+ return &AddApiPermissionRequest{
+ RoleId: roleId,
+ }
+}
+
+type AddApiPermissionRequest struct {
+ RoleId uint64 `json:"role_id"`
+ Items []*ApiPermissionSpec `json:"items"`
+}
+
+func (r *AddApiPermissionRequest) Validate() error {
+ return validator.Validate(r)
+}
+
+func (r *AddApiPermissionRequest) Add(specs ...*ApiPermissionSpec) *AddApiPermissionRequest {
+ r.Items = append(r.Items, specs...)
+ return r
+}
+
+func NewRemoveApiPermissionRequest(roleId uint64) *RemoveApiPermissionRequest {
+ return &RemoveApiPermissionRequest{
+ RoleId: roleId,
+ ApiPermissionIds: []uint64{},
+ }
+}
+
+type RemoveApiPermissionRequest struct {
+ RoleId uint64 `json:"role_id"`
+ ApiPermissionIds []uint64 `json:"api_permission_ids"`
+}
+
+func (r *RemoveApiPermissionRequest) Add(apiPermissionIds ...uint64) *RemoveApiPermissionRequest {
+ r.ApiPermissionIds = append(r.ApiPermissionIds, apiPermissionIds...)
+ return r
+}
+
+func (r *RemoveApiPermissionRequest) Validate() error {
+ return validator.Validate(r)
+}
+
+type UpdateApiPermissionRequest struct {
+ Items []*ApiPermission `json:"items"`
+}
+
+// 角色菜单管理
+type ViewPermissionService interface {
+ // 查询角色关联的视图权限
+ QueryViewPermission(context.Context, *QueryViewPermissionRequest) ([]*ViewPermission, error)
+ // 添加角色关联菜单
+ AddViewPermission(context.Context, *AddViewPermissionRequest) ([]*ViewPermission, error)
+ // 移除角色关联菜单
+ RemoveViewPermission(context.Context, *RemoveViewPermissionRequest) ([]*ViewPermission, error)
+ // 查询能匹配到视图菜单
+ QueryMatchedPage(context.Context, *QueryMatchedPageRequest) (*types.Set[*view.Menu], error)
+}
+
+func NewQueryViewPermissionRequest() *QueryViewPermissionRequest {
+ return &QueryViewPermissionRequest{
+ RoleIds: []uint64{},
+ ViewPermissionIds: []uint64{},
+ }
+}
+
+type QueryViewPermissionRequest struct {
+ RoleIds []uint64 `json:"role_ids"`
+ ViewPermissionIds []uint64 `json:"view_permission_ids"`
+}
+
+func (r *QueryViewPermissionRequest) AddRoleId(roleIds ...uint64) *QueryViewPermissionRequest {
+ r.RoleIds = append(r.RoleIds, roleIds...)
+ return r
+}
+
+func (r *QueryViewPermissionRequest) AddPermissionId(permissionIds ...uint64) *QueryViewPermissionRequest {
+ r.ViewPermissionIds = append(r.ViewPermissionIds, permissionIds...)
+ return r
+}
+
+func NewQueryMatchedPageRequest() *QueryMatchedPageRequest {
+ return &QueryMatchedPageRequest{}
+}
+
+type QueryMatchedPageRequest struct {
+ apps.GetRequest
+}
+
+func NewAddViewPermissionRequest() *AddViewPermissionRequest {
+ return &AddViewPermissionRequest{
+ Items: []*ViewPermissionSpec{},
+ }
+}
+
+type AddViewPermissionRequest struct {
+ RoleId uint64 `json:"role_id"`
+ Items []*ViewPermissionSpec `json:"items"`
+}
+
+func (r *AddViewPermissionRequest) Validate() error {
+ return validator.Validate(r)
+}
+
+func (r *AddViewPermissionRequest) Add(specs ...*ViewPermissionSpec) *AddViewPermissionRequest {
+ r.Items = append(r.Items, specs...)
+ return r
+}
+
+type UpdateViewPermission struct {
+ Items []ViewPermission `json:"items"`
+}
+
+func NewRemoveViewPermissionRequest() *RemoveViewPermissionRequest {
+ return &RemoveViewPermissionRequest{
+ ViewPermissionIds: []uint64{},
+ }
+}
+
+type RemoveViewPermissionRequest struct {
+ RoleId uint64 `json:"role_id"`
+ ViewPermissionIds []uint64 `json:"menu_permission_ids"`
+}
+
+func (r *RemoveViewPermissionRequest) Validate() error {
+ return validator.Validate(r)
+}
diff --git a/devcloud/mcenter/apps/role/model.go b/devcloud/mcenter/apps/role/model.go
new file mode 100644
index 0000000..87c1589
--- /dev/null
+++ b/devcloud/mcenter/apps/role/model.go
@@ -0,0 +1,66 @@
+package role
+
+import (
+ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
+ "github.com/infraboard/mcube/v2/ioc/config/validator"
+ "github.com/infraboard/mcube/v2/tools/pretty"
+ "github.com/infraboard/modules/iam/apps"
+)
+
+func NewRole() *Role {
+ return &Role{
+ ResourceMeta: *apps.NewResourceMeta(),
+ MenuPermissions: []*ViewPermission{},
+ ApiPermissions: []*ApiPermission{},
+ }
+}
+
+type Role struct {
+ // 基础数据
+ apps.ResourceMeta
+ // 角色创建信息
+ CreateRoleRequest
+ // 菜单权限
+ MenuPermissions []*ViewPermission `json:"menu_permissions,omitempty" gorm:"-" description:"角色关联的菜单权限"`
+ // API权限
+ ApiPermissions []*ApiPermission `json:"api_permissions,omitempty" gorm:"-" description:"角色关联的API权限"`
+}
+
+func (r *Role) TableName() string {
+ return "roles"
+}
+
+func (r *Role) String() string {
+ return pretty.ToJSON(r)
+}
+
+// 该角色是否允许该API访问
+func (r *Role) CheckPerm(re *endpoint.RouteEntry) error {
+ return nil
+}
+
+func NewCreateRoleRequest() *CreateRoleRequest {
+ return &CreateRoleRequest{
+ Extras: map[string]string{},
+ Enabled: true,
+ }
+}
+
+type CreateRoleRequest struct {
+ // 创建者ID
+ CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
+ // 角色名称
+ Name string `json:"name" gorm:"column:name;type:varchar(100);index" bson:"name" description:"角色名称"`
+ // 角色描述
+ Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
+ // 是否启用
+ Enabled bool `json:"enabled" bson:"enabled" gorm:"column:enabled;type:tinyint(1)" description:"是否启用" optional:"true"`
+ // 标签
+ Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"标签" optional:"true"`
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
+}
+
+func (r *CreateRoleRequest) Validate() error {
+ return validator.Validate(r)
+}
diff --git a/devcloud/mcenter/apps/role/view_permission.go b/devcloud/mcenter/apps/role/view_permission.go
new file mode 100644
index 0000000..43f124a
--- /dev/null
+++ b/devcloud/mcenter/apps/role/view_permission.go
@@ -0,0 +1,45 @@
+package role
+
+import (
+ "github.com/infraboard/modules/iam/apps"
+)
+
+func NewViewPermission(roleId uint64, spec *ViewPermissionSpec) *ViewPermission {
+ return &ViewPermission{
+ ResourceMeta: *apps.NewResourceMeta(),
+ RoleId: roleId,
+ ViewPermissionSpec: *spec,
+ }
+}
+
+type ViewPermission struct {
+ // 基础数据
+ apps.ResourceMeta
+ // 角色Id
+ RoleId uint64 `json:"role_id" gorm:"column:role_id;index" description:"Role Id"`
+ // Menu权限定义
+ ViewPermissionSpec
+}
+
+func (r *ViewPermission) TableName() string {
+ return "view_permissions"
+}
+
+func NewViewPermissionSpec() *ViewPermissionSpec {
+ return &ViewPermissionSpec{
+ Extras: map[string]string{},
+ }
+}
+
+type ViewPermissionSpec struct {
+ // 创建者ID
+ CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
+ // 权限描述
+ Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
+ // 页面路径
+ PagePath string `json:"path_path" gorm:"column:path_path;type:varchar(200);index" bson:"path_path" description:"页面路径(可以通配)"`
+ // 组件名称
+ Components []string `json:"components" gorm:"column:components;type:json;serializer:json" bson:"components" description:"页面组件(可以通配)"`
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
+}
diff --git a/devcloud/mcenter/apps/token/impl/impl_test.go b/devcloud/mcenter/apps/token/impl/impl_test.go
index 4a96a50..20033fc 100644
--- a/devcloud/mcenter/apps/token/impl/impl_test.go
+++ b/devcloud/mcenter/apps/token/impl/impl_test.go
@@ -13,6 +13,6 @@ var (
)
func init() {
- test.DevelopmentSet()
+ test.DevelopmentSetUp()
svc = token.GetService()
}
diff --git a/devcloud/mcenter/apps/token/issuers/password/issuer_test.go b/devcloud/mcenter/apps/token/issuers/password/issuer_test.go
index b1b5555..fb6a374 100644
--- a/devcloud/mcenter/apps/token/issuers/password/issuer_test.go
+++ b/devcloud/mcenter/apps/token/issuers/password/issuer_test.go
@@ -18,5 +18,5 @@ func TestPasswordIssuer(t *testing.T) {
}
func init() {
- test.DevelopmentSet()
+ test.DevelopmentSetUp()
}
diff --git a/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go b/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go
index 6a64dd7..14bc93c 100644
--- a/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go
+++ b/devcloud/mcenter/apps/token/issuers/private_token/issueer_test.go
@@ -18,5 +18,5 @@ func TestPasswordIssuer(t *testing.T) {
}
func init() {
- test.DevelopmentSet()
+ test.DevelopmentSetUp()
}
diff --git a/devcloud/mcenter/apps/user/impl/impl_test.go b/devcloud/mcenter/apps/user/impl/impl_test.go
index 8fde448..16935c0 100644
--- a/devcloud/mcenter/apps/user/impl/impl_test.go
+++ b/devcloud/mcenter/apps/user/impl/impl_test.go
@@ -13,6 +13,6 @@ var (
)
func init() {
- test.DevelopmentSet()
+ test.DevelopmentSetUp()
impl = user.GetService()
}
diff --git a/devcloud/mcenter/apps/view/model.go b/devcloud/mcenter/apps/view/model.go
new file mode 100644
index 0000000..2c40099
--- /dev/null
+++ b/devcloud/mcenter/apps/view/model.go
@@ -0,0 +1,60 @@
+package view
+
+import (
+ "github.com/infraboard/mcube/v2/ioc/config/validator"
+ "github.com/infraboard/modules/iam/apps"
+)
+
+func NewMenu() *Menu {
+ return &Menu{
+ ResourceMeta: *apps.NewResourceMeta(),
+ Pages: []*Page{},
+ }
+}
+
+type Menu struct {
+ // 基础数据
+ apps.ResourceMeta
+ // 菜单定义
+ CreateMenuRequest
+ // 用户是否有权限访问该菜单, 只有在策略模块查询时,才会计算出该字段
+ HasPermission *bool `json:"has_permission,omitempty" gorm:"column:has_permission;type:tinyint(1)" optional:"true" description:"用户是否有权限访问该菜单"`
+ // 菜单关联的页面
+ Pages []*Page `json:"pages,omitempty" gorm:"-" description:"菜单关联的页面"`
+}
+
+func (m *Menu) SetHasPermission(v bool) *Menu {
+ m.HasPermission = &v
+ return m
+}
+
+func (m *Menu) TableName() string {
+ return "menus"
+}
+
+func NewCreateMenuRequest() *CreateMenuRequest {
+ return &CreateMenuRequest{
+ Extras: map[string]string{},
+ }
+}
+
+type CreateMenuRequest struct {
+ // 服务
+ Service string `json:"service" gorm:"column:service;type:varchar(100);index" bson:"service" description:"服务名称"`
+ // 父Menu Id
+ ParentId uint64 `json:"parent_id" bson:"parent_id" gorm:"column:parent_id;type:uint;index" description:"父Menu Id" optional:"true"`
+ // 菜单路径
+ Path string `json:"path" bson:"path" gorm:"column:path" description:"菜单路径" unique:"true"`
+ // 菜单名称
+ Name string `json:"name" bson:"name" gorm:"column:name" description:"菜单名称"`
+ // 图标
+ Icon string `json:"icon" bson:"icon" gorm:"column:icon" description:"图标" optional:"true"`
+ // 标签
+ Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"标签" optional:"true"`
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
+}
+
+func (r *CreateMenuRequest) Validate() error {
+ return validator.Validate(r)
+}
diff --git a/devcloud/mcenter/apps/view/page.go b/devcloud/mcenter/apps/view/page.go
new file mode 100644
index 0000000..7262e3c
--- /dev/null
+++ b/devcloud/mcenter/apps/view/page.go
@@ -0,0 +1,64 @@
+package view
+
+import (
+ "github.com/infraboard/mcube/v2/ioc/config/validator"
+ "github.com/infraboard/modules/iam/apps"
+)
+
+func NewPage() *Page {
+ return &Page{
+ ResourceMeta: *apps.NewResourceMeta(),
+ }
+}
+
+type Page struct {
+ // 基础数据
+ apps.ResourceMeta
+ // 菜单定义
+ CreatePageRequest
+ // 用户是否有权限访问该页面, 只有在策略模块查询时,才会计算出该字段
+ HasPermission *bool `json:"has_permission,omitempty" gorm:"column:has_permission;type:tinyint(1)" optional:"true" description:"用户是否有权限访问该页面"`
+}
+
+func (p *Page) TableName() string {
+ return "pages"
+}
+
+func NewCreatePageRequest() *CreatePageRequest {
+ return &CreatePageRequest{
+ Extras: map[string]string{},
+ }
+}
+
+type CreatePageRequest struct {
+ // 菜单Id
+ MenuId uint64 `json:"menu_id" bson:"menu_id" gorm:"column:menu_id;type:uint;index" description:"菜单Id"`
+ // 页面路径
+ Path string `json:"path" bson:"path" gorm:"column:path" description:"页面路径" unique:"true"`
+ // 页面名称
+ Name string `json:"name" bson:"name" gorm:"column:name" description:"页面名称"`
+ // 标签
+ Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"标签" optional:"true"`
+ // 页面组件,比如按钮
+ Components []Component `json:"components" gorm:"column:components;type:json;serializer:json" description:"组件" optional:"true"`
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
+}
+
+func (r *CreatePageRequest) Validate() error {
+ return validator.Validate(r)
+}
+
+// 组件
+type Component struct {
+ // 组件名称
+ Name string `json:"name" bson:"name" description:"组件名称"`
+ // 组件说明
+ Description string `json:"description" optional:"true" description:"组件说明"`
+ // 组件使用文档链接
+ RefDocURL string `json:"ref_doc_url" optional:"true" description:"组件使用文档链接"`
+ // 关联的Api接口
+ RefEndpointId []uint64 `json:"ref_endpoints" description:"该页面管理的Api接口关联的接口" optional:"true"`
+ // 其他扩展信息
+ Extras map[string]string `json:"extras" description:"其他扩展信息" optional:"true"`
+}
diff --git a/devcloud/mcenter/design.drawio b/devcloud/mcenter/design.drawio
index 72c63ea..10767bd 100644
--- a/devcloud/mcenter/design.drawio
+++ b/devcloud/mcenter/design.drawio
@@ -1,6 +1,6 @@
-
+
@@ -127,7 +127,7 @@
-
+
@@ -139,19 +139,19 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/devcloud/mcenter/test/set_up.go b/devcloud/mcenter/test/set_up.go
index 6337f83..343d51a 100644
--- a/devcloud/mcenter/test/set_up.go
+++ b/devcloud/mcenter/test/set_up.go
@@ -10,7 +10,7 @@ import (
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps"
)
-func DevelopmentSet() {
+func DevelopmentSetUp() {
// import 后自动执行的逻辑
// 工具对象的初始化, 需要的是绝对路径
ioc.DevelopmentSetupWithPath(os.Getenv("CONFIG_PATH"))
diff --git a/go.mod b/go.mod
index e7ed942..76699f3 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
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/google/uuid v1.6.0
github.com/infraboard/mcube/v2 v2.0.59
github.com/infraboard/modules v0.0.12
github.com/rs/zerolog v1.34.0
@@ -42,7 +43,6 @@ require (
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
diff --git a/homework/README.md b/homework/README.md
index 716658d..a53938c 100644
--- a/homework/README.md
+++ b/homework/README.md
@@ -15,7 +15,9 @@
项目名称: devcloud
+ [GO18015-鑫](https://github.com/sword-demon/go18)
-+ [GO18012-Leowy](https://gitee.com/leo_wy/GoProjects.git)
+ [GO18005-康](https://github.com/YouthInThinking/GoProject.git)
+ [GO18007-磊](https://gitee.com/wangleisir/mage_go_project.git)
-+ [硕](https://gitee.com/liushuowalk/kubernetersgo)
\ No newline at end of file
+
++ [GO18012-Leowy](https://gitee.com/leo_wy/GoProjects.git)
++ [硕](https://gitee.com/liushuowalk/kubernetersgo)
+