sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
+20 -11
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -195,7 +195,7 @@ func (s *sSysAuth) GenerateToken(ctx context.Context, user *model.LoginUserOut)
ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "7d").String()
bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String()
iss := g.Cfg().MustGet(ctx, "jwt.issuer", "IotLCM").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
et, _ := utility.ParseDuration(ets)
bt, _ := utility.ParseDuration(bts)
g.Log().Infof(ctx, "sSysAuth.GenerateToken 生成Token: user:%+v, et:%s, bt:%s, iss:%s, sk:%s", user, et, bt, iss, sk)
@@ -265,7 +265,7 @@ func (s *sSysAuth) GenerateToken(ctx context.Context, user *model.LoginUserOut)
// 解析Token
func (s *sSysAuth) ParseToken(ctx context.Context, token string) (claims *model.CustomClaims, err error) {
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(sk), nil
})
@@ -287,7 +287,7 @@ func (s *sSysAuth) ParseToken(ctx context.Context, token string) (claims *model.
// 删除Token
func (s *sSysAuth) DeleteToken(ctx context.Context, token string) (err error) {
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(sk), nil
})
@@ -341,11 +341,6 @@ func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (acc
accessCodeList = make([]string, 0)
menuRoleAccessCodeList = make([]string, 0)
roleDataScopeMap := make(map[int64]string)
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return nil, nil, templateErr
}
managed := template != nil && template.TenantId == contexts.GetTenantId(ctx) && template.DeptId != contexts.GetDeptId(ctx)
// 角色权限码
roleIds, err := service.SysUser().GetUserRoleIds(ctx, userId)
if err != nil {
@@ -353,14 +348,28 @@ func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (acc
}
var effectiveRoles []*entity.SysRole
if len(roleIds) > 0 {
if err := dao.SysRole.Ctx(ctx).Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&effectiveRoles); err != nil {
if err := dao.SysRole.Ctx(ctx).Master().Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&effectiveRoles); err != nil {
return nil, nil, err
}
}
roleIds = nil
if contexts.GetTenantId(ctx) != consts.DefaultSystemTenantCode {
effectiveRoles = slices.DeleteFunc(effectiveRoles, func(role *entity.SysRole) bool {
return role.RoleId == consts.SuperAdminRoleId
})
}
for _, role := range effectiveRoles {
roleIds = append(roleIds, role.RoleId)
}
isPlatformSuperAdmin := contexts.GetTenantId(ctx) == consts.DefaultSystemTenantCode && slices.Contains(roleIds, consts.SuperAdminRoleId)
managed := false
if !isPlatformSuperAdmin {
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return nil, nil, templateErr
}
managed = template != nil && template.TenantId == contexts.GetTenantId(ctx) && template.DeptId != contexts.GetDeptId(ctx)
}
for _, role := range effectiveRoles {
accessCodeList = append(accessCodeList, consts.SysCheckPermissionRolePrefix+role.RoleKey)
@@ -391,7 +400,7 @@ func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (acc
for _, menu := range rmList {
menuIds = append(menuIds, menu.MenuId)
}
if !contexts.IsSuperAdmin(ctx) {
if !isPlatformSuperAdmin {
menuIds, err = rolepolicy.RestrictMenus(ctx, contexts.GetTenantId(ctx), contexts.GetDeptId(ctx), menuIds)
if err != nil {
return nil, nil, err
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+7 -22
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -131,33 +131,25 @@ func (s *sSysConfig) Add(ctx context.Context, param *model.SysConfigAddParam) (o
if insertErr != nil {
return insertErr
}
if param.ConfigKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return resolveErr
}
_ = template
return nil
}
return nil
})
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, lastInsertId, data.TenantId, data.ConfigKey)
if param.ConfigKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return nil, resolveErr
}
if err = rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); err != nil {
if err := rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); err != nil {
return nil, err
}
}
output = &model.SysConfigAddModel{
ConfigId: lastInsertId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, lastInsertId, data.TenantId, data.ConfigKey)
return
}
@@ -207,32 +199,25 @@ func (s *sSysConfig) Edit(ctx context.Context, param *model.SysConfigEditParam)
if _, updateErr := m.Ctx(ctx).Data(data).Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Update(); updateErr != nil {
return updateErr
}
if nextKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return resolveErr
}
return rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx))
}
return nil
})
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigUpdate, param.ConfigId, contexts.GetTenantId(ctx), nextKey)
if nextKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return nil, resolveErr
}
if err = rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); err != nil {
if err := rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); err != nil {
return nil, err
}
}
output = &model.SysConfigEditModel{
ConfigId: param.ConfigId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigUpdate, param.ConfigId, contexts.GetTenantId(ctx), nextKey)
return
}
+117 -23
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -145,6 +145,9 @@ func (l *sSysDept) GetDeptFullTree(ctx context.Context, query model.SysDeptFullT
if err != nil {
return nil, err
}
if err = validateDeptHierarchy(depts); err != nil {
return nil, err
}
return buildSysDeptFullTree(depts), nil
}
@@ -271,6 +274,9 @@ func (l *sSysDept) GetDeptTree(ctx context.Context, query model.SysDeptTreeParam
if err != nil {
return nil, err
}
if err = validateDeptHierarchy(depts); err != nil {
return nil, err
}
if len(depts) == 0 {
return make([]*model.SysDeptTreeModel, 0), nil
}
@@ -401,16 +407,46 @@ func (l *sSysDept) deptTreeItem(dept *model.SysDeptListModel, ancestors string)
// 递归构建结构
func (l *sSysDept) RecursionDeptIds(ctx context.Context, parentId int64, deptList []*model.SysDeptListModel, data *[]int64) (err error) {
for _, dept := range deptList {
if dept.ParentId == parentId {
*data = append(*data, dept.DeptId)
err := l.RecursionDeptIds(ctx, dept.DeptId, deptList, data)
if err != nil {
return err
if err = validateDeptHierarchy(deptList); err != nil {
return err
}
var walk func(int64)
walk = func(currentId int64) {
for _, dept := range deptList {
if dept.ParentId == currentId {
*data = append(*data, dept.DeptId)
walk(dept.DeptId)
}
}
}
return
walk(parentId)
return nil
}
func validateDeptHierarchy(depts []*model.SysDeptListModel) error {
parents := make(map[int64]int64, len(depts))
for _, dept := range depts {
parents[dept.DeptId] = dept.ParentId
}
checked := make(map[int64]bool, len(depts))
for deptId := range parents {
seen := make(map[int64]bool)
for currentId := deptId; currentId != 0 && !checked[currentId]; {
if seen[currentId] {
return gerror.New("组织层级存在循环引用")
}
seen[currentId] = true
parentId, exists := parents[currentId]
if !exists {
break
}
currentId = parentId
}
for visitedId := range seen {
checked[visitedId] = true
}
}
return nil
}
// 根据父部门id获取部门列表
@@ -434,7 +470,7 @@ func (l *sSysDept) AddDept(ctx context.Context, dept *model.SysDeptAddModel) (de
if user == nil {
return 0, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
if dept.DeptType == 1 {
if dept.DeptType == 1 && dept.ParentId != 0 {
isCompany, err := l.ValidateParentIsCompany(ctx, dept.ParentId)
if err != nil {
g.Log().Errorf(ctx, "sSysDept.AddDept ValidateParentIsCompany err: %v, parentId:%d, dept:%+v", err, dept.ParentId, dept)
@@ -486,8 +522,20 @@ func (l *sSysDept) EditDept(ctx context.Context, dept *model.SysDeptEditModel) (
newParentId := existingDept.ParentId
if dept.ParentId != nil {
newParentId = *dept.ParentId
var parentDepts []*model.SysDeptViewModel
err = l.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).Scan(&parentDepts)
if err != nil {
return 0, err
}
if err = validateDeptParent(dept.DeptId, newParentId, parentDepts); err != nil {
return 0, err
}
}
if dept.DeptType != nil && *dept.DeptType == 1 && *dept.ParentId != 0 {
newDeptType := existingDept.DeptType
if dept.DeptType != nil {
newDeptType = *dept.DeptType
}
if newDeptType == 1 && newParentId != 0 {
isCompany, err := l.ValidateParentIsCompany(ctx, newParentId)
if err != nil {
return 0, err
@@ -576,7 +624,9 @@ func (l *sSysDept) RefreshDeptAncestors(ctx context.Context) (err error) {
}
for _, dept := range depts {
ancestors := ""
l.GetParentIDAncestors(ctx, depts, dept.ParentId, &ancestors)
if err = l.GetParentIDAncestors(ctx, depts, dept.ParentId, &ancestors); err != nil {
return err
}
if ancestors != dept.Ancestors {
_, err = l.Model(ctx).Data(do.SysDept{
@@ -594,17 +644,50 @@ func (l *sSysDept) RefreshDeptAncestors(ctx context.Context) (err error) {
// GetParentIDAncestors 获取部门祖先
func (l *sSysDept) GetParentIDAncestors(ctx context.Context, depts []*model.SysDeptViewModel, printId int64, ancestors *string) (err error) {
parents := make(map[int64]int64, len(depts))
for _, dept := range depts {
if dept.DeptId == printId {
*ancestors = fmt.Sprintf("%d", dept.DeptId) + "," + *ancestors
l.GetParentIDAncestors(ctx, depts, dept.ParentId, ancestors)
return nil
parents[dept.DeptId] = dept.ParentId
}
seen := make(map[int64]bool)
for printId != 0 {
if seen[printId] {
return gerror.New("组织层级存在循环引用")
}
seen[printId] = true
parentId, exists := parents[printId]
if !exists {
break
}
*ancestors = fmt.Sprintf("%d,", printId) + *ancestors
printId = parentId
}
*ancestors = "0," + *ancestors
return nil
}
func validateDeptParent(deptId, parentId int64, depts []*model.SysDeptViewModel) error {
parents := make(map[int64]int64, len(depts))
for _, dept := range depts {
parents[dept.DeptId] = dept.ParentId
}
seen := make(map[int64]bool)
for parentId != 0 {
if parentId == deptId {
return gerror.New("上级组织不能是当前组织或其下级组织")
}
if parentId < 0 || seen[parentId] {
return gerror.New("组织层级存在循环引用")
}
seen[parentId] = true
next, exists := parents[parentId]
if !exists {
return gerror.New("上级组织不存在")
}
parentId = next
}
return nil
}
// GetDeptListByIds 根据部门ID列表获取部门列表
func (l *sSysDept) GetDeptListByIds(ctx context.Context, ids []int64) (depts []*model.SysDeptListModel, err error) {
err = l.ModelQuery(ctx).WhereIn(dao.SysDept.Columns().DeptId, ids).Scan(&depts)
@@ -620,25 +703,36 @@ func (l *sSysDept) ValidateParentIsCompany(ctx context.Context, parentId int64)
if err != nil {
return false, err
}
if dept == nil || dept.DeptId == 0 {
return false, gerror.New("上级组织不存在")
}
return dept.DeptType == 1 || dept.ParentId == 0, nil
}
// 获取上级公司名称
func (l *sSysDept) getParentCompanyInfo(ctx context.Context, depts []*model.SysDeptListModel, deptId int64) *model.SysDeptListModel {
var topDept *model.SysDeptListModel
byID := make(map[int64]*model.SysDeptListModel, len(depts))
for _, dept := range depts {
byID[dept.DeptId] = dept
if dept.ParentId == 0 {
topDept = dept
}
if dept.DeptId == deptId {
if dept.ParentId == 0 {
return dept
}
if dept.DeptType == 1 {
return dept
}
return l.getParentCompanyInfo(ctx, depts, dept.ParentId)
}
seen := make(map[int64]bool)
for deptId != 0 {
if seen[deptId] {
return nil
}
seen[deptId] = true
dept, exists := byID[deptId]
if !exists {
break
}
if dept.ParentId == 0 || dept.DeptType == 1 {
return dept
}
deptId = dept.ParentId
}
return topDept
}
@@ -0,0 +1,90 @@
package system
import (
"context"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
func TestDeptHierarchyReadGuards(t *testing.T) {
for _, tc := range []struct {
name string
depts []*model.SysDeptListModel
invalid bool
}{
{"normal", []*model.SysDeptListModel{{DeptId: 1}, {DeptId: 2, ParentId: 1}}, false},
{"orphan", []*model.SysDeptListModel{{DeptId: 2, ParentId: 99}}, false},
{"self", []*model.SysDeptListModel{{DeptId: 1, ParentId: 1}}, true},
{"cycle", []*model.SysDeptListModel{{DeptId: 1, ParentId: 2}, {DeptId: 2, ParentId: 1}}, true},
} {
t.Run(tc.name, func(t *testing.T) {
if err := validateDeptHierarchy(tc.depts); (err != nil) != tc.invalid {
t.Fatalf("unexpected hierarchy error: %v", err)
}
var ids []int64
if err := NewSysDept().RecursionDeptIds(context.Background(), 1, tc.depts, &ids); (err != nil) != tc.invalid {
t.Fatalf("unexpected traversal error: %v", err)
}
company := NewSysDept().getParentCompanyInfo(context.Background(), tc.depts, 1)
if tc.invalid && company != nil {
t.Fatal("cyclic company lookup must return nil")
}
})
}
}
func TestValidateDeptParent(t *testing.T) {
depts := []*model.SysDeptViewModel{
{DeptId: 100, ParentId: 0},
{DeptId: 117, ParentId: 100},
{DeptId: 118, ParentId: 117},
{DeptId: 196, ParentId: 100},
}
for _, test := range []struct {
name string
parentId int64
invalid bool
}{
{"root", 0, false},
{"sibling", 196, false},
{"self", 117, true},
{"descendant", 118, true},
{"missing", 999, true},
{"negative", -1, true},
} {
t.Run(test.name, func(t *testing.T) {
err := validateDeptParent(117, test.parentId, depts)
if (err != nil) != test.invalid {
t.Fatalf("parent %d: unexpected error %v", test.parentId, err)
}
})
}
cycle := []*model.SysDeptViewModel{{DeptId: 1, ParentId: 2}, {DeptId: 2, ParentId: 1}}
if err := validateDeptParent(3, 1, cycle); err == nil {
t.Fatal("existing cycle was not rejected")
}
}
func TestGetParentIDAncestorsCycle(t *testing.T) {
dept := NewSysDept()
for _, test := range []struct {
name string
depts []*model.SysDeptViewModel
parentId int64
expected string
invalid bool
}{
{"normal", []*model.SysDeptViewModel{{DeptId: 100}, {DeptId: 117, ParentId: 100}}, 117, "0,100,117,", false},
{"self", []*model.SysDeptViewModel{{DeptId: 117, ParentId: 117}}, 117, "", true},
{"cycle", []*model.SysDeptViewModel{{DeptId: 1, ParentId: 2}, {DeptId: 2, ParentId: 1}}, 1, "", true},
} {
t.Run(test.name, func(t *testing.T) {
var ancestors string
err := dept.GetParentIDAncestors(context.Background(), test.depts, test.parentId, &ancestors)
if (err != nil) != test.invalid || !test.invalid && ancestors != test.expected {
t.Fatalf("ancestors=%q error=%v, expected=%q invalid=%v", ancestors, err, test.expected, test.invalid)
}
})
}
}
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -13,8 +13,8 @@ func TestDocumentCenterAppearsAfterExecutionLog(t *testing.T) {
{MenuId: 1, MenuName: "系统管理", Path: "system", MenuType: "M", OrderNum: 900},
{MenuId: 1836, MenuName: "执行日志", Path: "batchExecutionLog", MenuType: "C", OrderNum: 32},
{MenuId: 2153, MenuName: "文档中心", ParentId: 1, Path: "docs", MenuType: "M", OrderNum: 13},
{MenuId: 2155, MenuName: "Web 使用手册", ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/manual/web/", MenuType: "C", IsFrame: 0, OrderNum: 1},
{MenuId: 2154, MenuName: "OpenAPI 文档", ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/openapi/v3/", MenuType: "C", IsFrame: 0, OrderNum: 2},
{MenuId: 2155, MenuName: "Web 使用手册", ParentId: 2153, Path: "https://admin.example.com/docs/manual/web/", MenuType: "C", IsFrame: 0, OrderNum: 1},
{MenuId: 2154, MenuName: "OpenAPI 文档", ParentId: 2153, Path: "https://admin.example.com/docs/openapi/v3/", MenuType: "C", IsFrame: 0, OrderNum: 2},
}
menus, err := NewSysMenu().BuildUserMenuTree(context.Background(), nil, placeDocumentCenterAfterExecutionLog(menuList), "")
if err != nil {
@@ -35,8 +35,8 @@ func TestDocumentCenterMenuListHierarchy(t *testing.T) {
menus := []*model.SysMenuListModel{
{MenuId: 1, MenuName: "system", ParentId: 0, Path: "system", OrderNum: 900},
{MenuId: 2153, MenuName: "docs", ParentId: 1, Path: "docs", MenuType: "M", OrderNum: 13, Perms: "docs:menu"},
{MenuId: 2155, ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/manual/web/", OrderNum: 1},
{MenuId: 2154, ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/openapi/v3/", OrderNum: 2},
{MenuId: 2155, ParentId: 2153, Path: "https://admin.example.com/docs/manual/web/", OrderNum: 1},
{MenuId: 2154, ParentId: 2153, Path: "https://admin.example.com/docs/openapi/v3/", OrderNum: 2},
{MenuId: 1836, ParentId: 0, Path: "batchExecutionLog", OrderNum: 32},
}
placeDocumentCenterInMenuList(menus)
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package system
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+24 -4
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -9,6 +9,7 @@ import (
"context"
"errors"
"fmt"
"io"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
@@ -19,7 +20,6 @@ import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/hash"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
"io"
"net/http"
"os"
"path/filepath"
@@ -56,7 +56,10 @@ func (s *sSysOss) Model(ctx context.Context, option ...*handler.Option) *gdb.Mod
FilterAuth: true,
})
}
return handler.Model(dao.SysOss.Ctx(ctx), option...)
return handler.Model(dao.SysOss.Ctx(ctx), option...).
WhereNotIn(dao.SysOss.Columns().OssId, dao.TExportTask.Ctx(ctx).Fields(dao.TExportTask.Columns().OssId).WhereGT(dao.TExportTask.Columns().OssId, 0)).
Where("LOWER(REPLACE(COALESCE(path, ''), CHAR(92), '/')) NOT REGEXP ?", `(^|/)private-export(/|$)|(^|/)user/file/([^/]+/)?export(/|$)`).
Where("LOWER(REPLACE(COALESCE(url, ''), CHAR(92), '/')) NOT REGEXP ?", `(^|/)private-export(/|$)|(^|/)user/file/([^/]+/)?export(/|$)`)
}
func (s *sSysOss) List(ctx context.Context, param *model.SysOssListParam, pageInfo *request.PageInfo) (items []*model.SysOssListModel, total int, err error) {
@@ -683,7 +686,7 @@ func joinUploadURL(base string, suffix string) string {
return base + "/" + suffix
}
func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss) (err error) {
func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss, beforeSend ...func() error) (err error) {
if file == nil {
err = errors.New("文件必须")
return
@@ -697,6 +700,23 @@ func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss) (err e
err = gerror.New("ctx not http request")
return
}
localFile, err := os.Open(finalPath)
if err != nil {
return gerror.Wrap(err, "本地文件不可读")
}
defer localFile.Close()
info, err := localFile.Stat()
if err != nil {
return gerror.Wrap(err, "获取本地文件信息失败")
}
if !info.Mode().IsRegular() {
return gerror.New("本地文件不是普通文件")
}
for _, callback := range beforeSend {
if err = callback(); err != nil {
return err
}
}
r.Response.ServeFileDownload(finalPath)
r.Response.Status = http.StatusOK
r.Response.Flush()
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+26 -33
View File
@@ -1,28 +1,25 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
@@ -180,35 +177,31 @@ func (l *sSysPost) Delete(ctx context.Context, param *model.SysPostDeleteParam)
return
}
// Export 导出岗位管理(异步:提交任务后到下载中心取文件)
func (l *sSysPost) Export(ctx context.Context, param *model.SysPostExportParam) (post *model.SysPostExportModel, err error) {
if param.Page == 0 {
param.Page = 1
}
if param.PageSize == 0 {
param.PageSize = 1000
}
items, total, err := l.List(ctx, param.SysPostListParam)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.SysPostExportModel{})
if err != nil {
return
}
var (
fileName = "导出CURD列表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", total, total+param.PageSize-1/param.PageSize, param.Page, len(items))
exports []model.SysPostExportModel
)
g.Log().Info(ctx, " sSysPost.Export total", total)
if err = gconv.Scan(items, &exports); err != nil {
return
}
g.Log().Info(ctx, " sSysPost.Export exports", len(exports))
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
g.Log().Info(ctx, " sSysPost.Export err", err)
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "post",
BizName: "岗位管理",
PermCode: "cpm:system:post:export",
QueryParam: param,
Tags: func() ([]string, error) {
return convert.GetEntityDescTags(model.SysPostListModel{})
},
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
query := param.SysPostListParam
query.Page = page
query.PageSize = pageSize
items, count, err := l.List(ctx, query)
if err != nil {
return nil, 0, err
}
rows = make([]interface{}, 0, len(items))
for _, item := range items {
rows = append(rows, item)
}
return rows, count, nil
},
})
post = &model.SysPostExportModel{}
return
}
+1 -1
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
@@ -16,7 +17,6 @@ import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"io"
"strconv"
"strings"
"time"
@@ -2,8 +2,8 @@ package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"io"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"net/http"
"net/http/httptest"
"testing"
@@ -74,7 +74,7 @@ func TestUserCodeHTTPTimeoutAndStatus(t *testing.T) {
started := time.Now()
err := callUserCodeHTTP(context.Background(), model.UserCodeHttpSenderConfig{
Url: server.URL, Timeout: "70ms", SuccessStatus: http.StatusAccepted,
}, "user@example.com", "123456")
}, "xxx@example.com", "123456")
if (err != nil) != (scenario != "success") || time.Since(started) > time.Second {
t.Fatalf("unexpected sender result: %v", err)
}
+64 -31
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -47,7 +47,7 @@ func (l *sSysRole) Model(ctx context.Context, option ...*handler.Option) *gdb.Mo
FilterAuth: false, // 角色表不验证数据权限
})
}
return handler.Model(dao.SysRole.Ctx(ctx), option...)
return handler.Model(dao.SysRole.Ctx(ctx).Master(), option...)
}
func (l *sSysRole) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
@@ -57,13 +57,12 @@ func (l *sSysRole) ModelQuery(ctx context.Context, option ...*handler.Option) *g
FilterAuth: false, // 角色表不验证数据权限
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysRole.Table()), option...)
return handler.Model(dao.SysRole.Ctx(ctx).Master(), option...)
}
// 获取租户下角色列表
func (s *sSysRole) List(ctx context.Context, param *model.SysRoleListParam) (res []*model.SysRoleListModel, total int, err error) {
db := s.ModelQuery(ctx)
// 注意:ModelQuery使用内存数据库,删除的记录会被物理删除,所以不需要过滤deleted_at
if param.RoleName != "" {
db = db.WhereLike(dao.SysRole.Columns().RoleName, "%"+param.RoleName+"%")
}
@@ -109,6 +108,19 @@ func (s *sSysRole) View(ctx context.Context, param *model.SysRoleViewParam) (res
if err != nil {
return nil, err
}
if res == nil {
return nil, gerror.New("角色不存在或已删除")
}
template, err := rolepolicy.TemplateForRoles(ctx, res.RoleId)
if err != nil {
return nil, err
}
res.IsTemplateRole = template != nil
defaultRoleCount, err := dao.SysDept.Ctx(ctx).Master().Where("tenant_id", res.TenantId).Where("dept_id", res.DeptId).Where("dept_role", res.RoleId).Count()
if err != nil {
return nil, err
}
res.IsDeptDefaultRole = defaultRoleCount > 0
// 获取角色菜单
roleMenus, err := s.getRoleMenuRelations(ctx, param.RoleId)
if err != nil {
@@ -136,7 +148,7 @@ func (s *sSysRole) View(ctx context.Context, param *model.SysRoleViewParam) (res
}
func (s *sSysRole) getRoleMenuRelations(ctx context.Context, id int64) (res []*entity.SysRoleMenu, err error) {
err = dao.SysRoleMenu.Ctx(ctx).
err = dao.SysRoleMenu.Ctx(ctx).Master().
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
return
@@ -144,7 +156,7 @@ func (s *sSysRole) getRoleMenuRelations(ctx context.Context, id int64) (res []*e
// 获取角色菜单
func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.SysMenu, err error) {
err = dao.SysRoleMenu.Ctx(ctx).
err = dao.SysRoleMenu.Ctx(ctx).Master().
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
return
@@ -152,7 +164,7 @@ func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.Sys
// 获取角色部门
func (s *sSysRole) GetRoleDept(ctx context.Context, id int64) (res []*entity.SysDept, err error) {
err = dao.SysRoleDept.Ctx(ctx).
err = dao.SysRoleDept.Ctx(ctx).Master().
Where(dao.SysRoleDept.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysRoleDept.Columns().RoleId, id).Scan(&res)
return
@@ -177,7 +189,7 @@ func (s *sSysRole) GetRoleListMenu(ctx context.Context, ids []int64) (res []*ent
}
return res, nil
}
err = dao.SysRoleMenu.Ctx(ctx).
err = dao.SysRoleMenu.Ctx(ctx).Master().
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
WhereIn(dao.SysRoleMenu.Columns().RoleId, ids).Scan(&res)
return
@@ -194,8 +206,8 @@ func (s *sSysRole) Add(ctx context.Context, param *model.SysRoleAddParam) (role
return nil
})
if err == nil && role != nil {
err = rolesync.EnqueueRole(ctx, role.RoleId, contexts.GetUserId(ctx))
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, role.RoleId)
err = rolesync.EnqueueRole(ctx, role.RoleId, contexts.GetUserId(ctx))
}
return
}
@@ -247,8 +259,8 @@ func (s *sSysRole) Edit(ctx context.Context, param *model.SysRoleEditParam) (rol
return nil
})
if err == nil {
err = rolesync.EnqueueRole(ctx, param.RoleId, contexts.GetUserId(ctx))
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, param.RoleId)
err = rolesync.EnqueueRole(ctx, param.RoleId, contexts.GetUserId(ctx))
}
return
}
@@ -264,7 +276,7 @@ func (s *sSysRole) editRoleAtomic(ctx context.Context, param *model.SysRoleEditP
if user == nil {
return role, gerror.New("登录已过期")
}
template, templateErr := rolepolicy.Template(ctx)
template, templateErr := rolepolicy.TemplateForRoles(ctx, param.RoleId)
if templateErr != nil {
return role, templateErr
}
@@ -299,7 +311,11 @@ func (s *sSysRole) editRoleAtomic(ctx context.Context, param *model.SysRoleEditP
// 删除角色
func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam) (role *model.SysRoleDeleteModel, err error) {
template, templateErr := rolepolicy.Template(ctx)
targetIDs := []int64{param.RoleId}
if param.RoleId == 0 {
targetIDs = param.RoleIds
}
template, templateErr := rolepolicy.TemplateForRoles(ctx, targetIDs...)
if templateErr != nil {
return nil, templateErr
}
@@ -331,19 +347,32 @@ func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam)
}
data := do.SysRole{}
data.DeletedBy = user.ID
data.DeletedAt = gtime.Now()
_, err = s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Data(data).OmitNil().Update()
err = g.DB().Transaction(ctx, func(ctx context.Context, _ gdb.TX) error {
if _, deleteErr := s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Data(data).OmitNil().Update(); deleteErr != nil {
return deleteErr
}
if _, deleteErr := s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Delete(); deleteErr != nil {
return deleteErr
}
return nil
})
if err != nil {
return role, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleDelete, roleIds)
clearRoleUsersAccessCache(ctx, roleIds...)
return
}
func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64, dataScopes map[int64]string) (err error) {
template, templateErr := rolepolicy.Template(ctx)
template, templateErr := rolepolicy.TemplateForRoles(ctx, roleId)
if templateErr != nil {
return templateErr
}
if template != nil && template.RoleId == roleId {
if !contexts.IsSuperAdmin(ctx) {
return gerror.New("仅平台超级管理员可修改代理模板")
}
for _, scope := range dataScopes {
if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom {
return gerror.New("模板按钮不允许全部或自定义数据范围")
@@ -352,15 +381,11 @@ func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64,
}
tenantId := contexts.GetTenantId(ctx)
if !contexts.IsSuperAdmin(ctx) {
allowed, err := rolepolicy.RestrictMenus(ctx, tenantId, contexts.GetDeptId(ctx), menuIds)
allowed, err := rolepolicy.NormalizeAssignableMenus(ctx, tenantId, contexts.GetDeptId(ctx), contexts.GetUserId(ctx), menuIds)
if err != nil {
return err
}
for _, id := range menuIds {
if !slices.Contains(allowed, id) {
return gerror.New("角色授权超出机构或模板范围")
}
}
menuIds = allowed
for _, scope := range dataScopes {
if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom {
return gerror.New("不允许授予全部或自定义数据范围")
@@ -369,7 +394,7 @@ func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64,
}
// 查询角色菜单
roleMenu := make([]*entity.SysRoleMenu, 0)
err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().TenantId, tenantId).Where(dao.SysRoleMenu.Columns().RoleId, roleId).Scan(&roleMenu)
err = dao.SysRoleMenu.Ctx(ctx).Master().Where(dao.SysRoleMenu.Columns().TenantId, tenantId).Where(dao.SysRoleMenu.Columns().RoleId, roleId).Scan(&roleMenu)
if err != nil {
return err
}
@@ -407,7 +432,7 @@ func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64,
}
}
for _, menuId := range updateMenuIds {
_, err = dao.SysRoleMenu.Ctx(ctx).
_, err = dao.SysRoleMenu.Ctx(ctx).Master().
Where(dao.SysRoleMenu.Columns().TenantId, tenantId).
Where(dao.SysRoleMenu.Columns().RoleId, roleId).
Where(dao.SysRoleMenu.Columns().MenuId, menuId).
@@ -443,7 +468,7 @@ func clearRoleUsersAccessCache(ctx context.Context, roleIds ...int64) {
return
}
relations := make([]*entity.SysUserRole, 0)
if err := dao.SysUserRole.Ctx(ctx).
if err := dao.SysUserRole.Ctx(ctx).Master().
Fields(dao.SysUserRole.Columns().UserId).
Where(dao.SysUserRole.Columns().TenantId, contexts.GetTenantId(ctx)).
WhereIn(dao.SysUserRole.Columns().RoleId, roleIds).
@@ -474,7 +499,7 @@ func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64)
tenantId := contexts.GetTenantId(ctx)
// 查询角色部门
roleDept := make([]*entity.SysRoleDept, 0)
err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().TenantId, tenantId).Where(dao.SysRoleDept.Columns().RoleId, roleId).Scan(&roleDept)
err = dao.SysRoleDept.Ctx(ctx).Master().Where(dao.SysRoleDept.Columns().TenantId, tenantId).Where(dao.SysRoleDept.Columns().RoleId, roleId).Scan(&roleDept)
if err != nil {
return err
}
@@ -511,10 +536,13 @@ func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64)
// 编辑角色数据权限
func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDataScopeEditParam) (err error) {
template, templateErr := rolepolicy.Template(ctx)
template, templateErr := rolepolicy.TemplateForRoles(ctx, model.RoleId)
if templateErr != nil {
return templateErr
}
if template != nil && !contexts.IsSuperAdmin(ctx) {
return gerror.New("仅平台超级管理员可修改代理模板")
}
if !contexts.IsSuperAdmin(ctx) || (template != nil && template.RoleId == model.RoleId) {
if model.DataScope == consts.SysRoleDataScopeAll || model.DataScope == consts.SysRoleDataScopeCustom || model.DataScope == "" {
return gerror.New("不允许全部、自定义或空数据权限")
@@ -544,10 +572,8 @@ func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDa
if err != nil {
return err
}
if err = rolesync.EnqueueRole(ctx, model.RoleId, contexts.GetUserId(ctx)); err != nil {
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, model.RoleId)
err = rolesync.EnqueueRole(ctx, model.RoleId, contexts.GetUserId(ctx))
return
}
@@ -558,7 +584,7 @@ func (s *sSysRole) ensureRolesNotInherited(ctx context.Context, roleIds []int64)
if contexts.IsSuperAdmin(ctx) {
return nil
}
count, err := g.Model(dao.SysRole.Table()+" r").
count, err := g.Model(dao.SysRole.Table()+" r").Ctx(ctx).
LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id").
WhereIn("r.role_id", roleIds).
Where("r.deleted_at IS NULL").
@@ -581,7 +607,14 @@ func (s *sSysRole) ensureRolesHaveNoInheritedChildren(ctx context.Context, roleI
if len(roleIds) == 0 {
return nil
}
count, err := g.Model(dao.SysRole.Table()+" r").
configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
if err != nil {
return err
}
if configured > 0 {
return nil
}
count, err := g.Model(dao.SysRole.Table()+" r").Ctx(ctx).
LeftJoin(dao.SysDept.Table()+" child_dept", "child_dept.dept_id = r.dept_id AND child_dept.tenant_id = r.tenant_id").
Where("r.created_dept IN (SELECT dept_id FROM sys_role WHERE role_id IN (?) AND is_pass = 1 AND deleted_at IS NULL)", roleIds).
Where("r.deleted_at IS NULL").
@@ -0,0 +1,437 @@
package system
import (
"context"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/agent"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
func TestRoleMenuTemplateValidationIsolation(t *testing.T) {
if os.Getenv("ROLE_TEMPLATE_TEST_CHILD") != "1" {
configPath := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(configPath, []byte("database:\n default:\n type: sqlite\n link: sqlite::@file(:memory:)\n memorydb:\n link: \"\"\n"), 0600); err != nil {
t.Fatal(err)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
command := exec.Command(executable, "-test.run=^TestRoleMenuTemplateValidationIsolation$", "-test.v")
command.Env = append(os.Environ(), "ROLE_TEMPLATE_TEST_CHILD=1", "GF_GCFG_FILE="+configPath)
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("isolated role tests failed: %v\n%s", err, output)
}
t.Log(string(output))
return
}
originalAdapter := g.Cfg().GetAdapter()
adapter, err := gcfg.NewAdapterContent("database:\n default:\n type: sqlite\n link: sqlite::@file(:memory:)\n memorydb:\n link: \"\"\n", "yaml")
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
t.Cleanup(func() { g.Cfg().SetAdapter(originalAdapter) })
if err := gdb.SetConfig(gdb.Config{"default": {{Type: "sqlite", Link: "sqlite::@file(:memory:)"}}}); err != nil {
t.Fatal(err)
}
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 1, TenantId: "000000", DeptId: 100, Roles: []model.Role{{RoleId: consts.SuperAdminRoleId}}}}})
t.Cleanup(func() { _ = g.DB().Close(ctx) })
for _, statement := range []string{
"CREATE TABLE sys_config (tenant_id TEXT, config_key TEXT, config_value TEXT)",
"CREATE TABLE sys_role (role_id INTEGER PRIMARY KEY, tenant_id TEXT, dept_id INTEGER, created_dept INTEGER, status TEXT, is_pass INTEGER, data_scope TEXT, role_name TEXT, role_sort INTEGER, menu_check_strictly INTEGER, dept_check_strictly INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, remark TEXT, deleted_at DATETIME)",
"CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)",
"CREATE TABLE sys_user_role (tenant_id TEXT, user_id INTEGER, role_id INTEGER)",
"CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)",
"INSERT INTO sys_config VALUES ('000000','agent.role.template.role_id','82')",
"INSERT INTO sys_role (role_id,tenant_id,dept_id,created_dept,status,is_pass,data_scope,deleted_at) VALUES (82,'000000',0,100,'0',0,'4',NULL)",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
service := NewSysRole()
t.Run("普通角色新增菜单不受无效模板影响", func(t *testing.T) {
if err := service.RoleMenu(ctx, 200, []int64{10, 11}, nil); err != nil {
t.Fatal(err)
}
count, err := g.DB().Model("sys_role_menu").Where("role_id", 200).Count()
if err != nil || count != 2 {
t.Fatalf("menu count %d: %v", count, err)
}
})
t.Run("普通角色修改菜单不受无效模板影响", func(t *testing.T) {
if err := service.RoleMenu(ctx, 200, []int64{11, 12}, nil); err != nil {
t.Fatal(err)
}
count, err := g.DB().Model("sys_role_menu").Where("role_id", 200).Where("menu_id", 10).Count()
if err != nil || count != 0 {
t.Fatalf("removed menu count %d: %v", count, err)
}
})
t.Run("模板仍禁止全部按钮数据权限", func(t *testing.T) {
err := service.RoleMenu(ctx, 82, []int64{10}, map[int64]string{10: consts.SysRoleDataScopeAll})
if err == nil || !strings.Contains(err.Error(), "模板按钮") {
t.Fatalf("expected template restriction: %v", err)
}
})
t.Run("无效模板仍禁止删除", func(t *testing.T) {
_, err := service.Delete(ctx, &model.SysRoleDeleteParam{RoleId: 82})
if err == nil || !strings.Contains(err.Error(), "模板不能删除") {
t.Fatalf("expected template deletion rejection: %v", err)
}
})
t.Run("批量删除不能包含模板", func(t *testing.T) {
_, err := service.Delete(ctx, &model.SysRoleDeleteParam{RoleIds: []int64{200, 82}})
if err == nil || !strings.Contains(err.Error(), "模板不能删除") {
t.Fatalf("expected batch deletion rejection: %v", err)
}
})
t.Run("非管理员不能绕过无效模板授权", func(t *testing.T) {
limited := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175}}})
if err := service.RoleMenu(limited, 201, []int64{10}, nil); err == nil {
t.Fatal("invalid template must fail closed")
}
count, err := g.DB().Model("sys_role_menu").Where("role_id", 201).Count()
if err != nil || count != 0 {
t.Fatalf("unauthorized menu count %d: %v", count, err)
}
})
t.Run("运行时模板校验仍然严格", func(t *testing.T) {
if _, err := rolepolicy.Template(ctx); err == nil {
t.Fatal("invalid template was accepted")
}
})
t.Run("无关角色不读取模板角色实体", func(t *testing.T) {
template, err := rolepolicy.TemplateForRoles(ctx, 200)
if err != nil || template != nil {
t.Fatalf("unexpected template: %+v %v", template, err)
}
})
t.Run("已删除模板仍保留身份保护", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "DELETE FROM sys_role WHERE role_id=82"); err != nil {
t.Fatal(err)
}
template, err := rolepolicy.TemplateForRoles(ctx, 82)
if err != nil || template == nil || template.RoleId != 82 {
t.Fatalf("template identity lost: %+v %v", template, err)
}
})
t.Run("错误参数不阻断无关角色但不能用于授权", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "UPDATE sys_config SET config_value='invalid'"); err != nil {
t.Fatal(err)
}
if err := service.RoleMenu(ctx, 202, []int64{10}, nil); err != nil {
t.Fatal(err)
}
if _, err := rolepolicy.Template(ctx); err == nil {
t.Fatal("invalid configuration must fail strict validation")
}
})
t.Run("平台管理员权限加载不依赖无效模板", func(t *testing.T) {
for _, statement := range []string{
"UPDATE sys_config SET config_value='82'",
"ALTER TABLE sys_role ADD COLUMN role_key TEXT",
"INSERT INTO sys_role (role_id,tenant_id,dept_id,status,role_key,data_scope) VALUES (1,'000000',100,'0','admin','1'),(102,'000000',175,'0','agent','1')",
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_role INTEGER, status TEXT, ancestors TEXT, dept_type INTEGER, order_num INTEGER DEFAULT 0, deleted_at DATETIME)",
"INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_role,status,deleted_at) VALUES (100,'000000',0,1,'0',NULL),(175,'000000',100,102,'0',NULL)",
"CREATE TABLE sys_menu (menu_id INTEGER PRIMARY KEY, menu_name TEXT, parent_id INTEGER, path TEXT, menu_type TEXT, order_num INTEGER, perms TEXT, status TEXT, deleted_at DATETIME)",
"INSERT INTO sys_menu VALUES (10,'授权功能',0,'allowed','C',1,'system:test:allowed','0',NULL),(11,'受限功能',0,'restricted','C',2,'system:test:restricted','0',NULL)",
"CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, tenant_id TEXT, dept_id INTEGER, user_name TEXT, status TEXT, deleted_at DATETIME)",
"INSERT INTO sys_user VALUES (1,'000000',100,'admin','0',NULL),(2,'000000',175,'agent','0',NULL)",
"CREATE TABLE sys_user_post (tenant_id TEXT, user_id INTEGER, post_id INTEGER)",
"INSERT INTO sys_user_role VALUES ('000000',1,1),('000000',2,102)",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
accessCodes, _, err := NewSysAuth().GetUserAccessCodeList(ctx, 1)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(accessCodes, "cpm:system:test:allowed") {
t.Fatalf("administrator permissions missing: %v", accessCodes)
}
menus, err := NewSysMenu().GetUserMenu(ctx)
if err != nil || len(menus) != 2 {
t.Fatalf("administrator menu loading failed: %v %v", menus, err)
}
})
t.Run("不能仅凭旧令牌声称是平台管理员", func(t *testing.T) {
stale := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175, Roles: []model.Role{{RoleId: consts.SuperAdminRoleId}}}}})
if _, _, err := NewSysAuth().GetUserAccessCodeList(stale, 2); err == nil {
t.Fatal("stale administrator claim bypassed template validation")
}
})
t.Run("停用平台管理员角色不能跳过模板校验", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "UPDATE sys_role SET status='1' WHERE role_id=1"); err != nil {
t.Fatal(err)
}
defer func() {
if _, err := g.DB().Exec(ctx, "UPDATE sys_role SET status='0' WHERE role_id=1"); err != nil {
t.Error(err)
}
}()
if _, _, err := NewSysAuth().GetUserAccessCodeList(ctx, 1); err == nil {
t.Fatal("disabled administrator role bypassed validation")
}
})
t.Run("跨租户管理员标记不能跳过模板校验", func(t *testing.T) {
other := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 1, TenantId: "other", DeptId: 100, Roles: []model.Role{{RoleId: consts.SuperAdminRoleId}}}}})
if _, _, err := NewSysAuth().GetUserAccessCodeList(other, 1); err == nil {
t.Fatal("cross-tenant administrator claim bypassed validation")
}
})
t.Run("模板参数格式异常不阻断平台管理员", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "UPDATE sys_config SET config_value='invalid'"); err != nil {
t.Fatal(err)
}
if _, _, err := NewSysAuth().GetUserAccessCodeList(ctx, 1); err != nil {
t.Fatal(err)
}
})
t.Run("平台单独扩大默认角色权限后菜单及鉴权均生效且不扩大数据范围", func(t *testing.T) {
for _, statement := range []string{
"UPDATE sys_config SET config_value='82'",
"INSERT INTO sys_role (role_id,tenant_id,dept_id,created_dept,status,is_pass,data_scope) VALUES (82,'000000',100,100,'0',1,'4')",
"INSERT INTO sys_role_menu VALUES ('000000',82,10,'0'),('000000',102,10,'0'),('000000',102,11,'0')",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
agent := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175}}})
accessCodes, roleCodes, err := NewSysAuth().GetUserAccessCodeList(agent, 2)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(accessCodes, "cpm:system:test:allowed") || !slices.Contains(accessCodes, "cpm:system:test:restricted") {
t.Fatalf("agent menu boundary violated: %v", accessCodes)
}
menus, err := NewSysMenu().GetUserMenu(agent)
if err != nil || len(menus) != 2 {
t.Fatalf("individual menus missing: %v %v", menus, err)
}
if len(roleCodes) != 2 || !strings.HasSuffix(roleCodes[0], "|4") || !strings.HasSuffix(roleCodes[1], "|4") {
t.Fatalf("agent data scope boundary violated: %v", roleCodes)
}
})
t.Run("平台从角色管理扩大普通角色权限不受默认角色限制", func(t *testing.T) {
for _, statement := range []string{
"DELETE FROM sys_role_menu WHERE role_id=102 AND menu_id=11",
"INSERT INTO sys_role(role_id,tenant_id,dept_id,created_dept,status,data_scope,role_key) VALUES(103,'000000',175,175,'0','4','special')",
"INSERT INTO sys_user VALUES(3,'000000',175,'special','0',NULL)",
"INSERT INTO sys_user_role VALUES('000000',3,103)",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
if _, err := service.Edit(rolesync.DeferRecording(ctx), &model.SysRoleEditParam{RoleId: 103, MenuIds: []int64{11}}); err != nil {
t.Fatal(err)
}
special := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 3, TenantId: "000000", DeptId: 175}}})
codes, _, err := NewSysAuth().GetUserAccessCodeList(special, 3)
if err != nil || !slices.Contains(codes, "cpm:system:test:restricted") {
t.Fatalf("saved permission did not reach API authorization: %v %v", codes, err)
}
menus, err := NewSysMenu().GetUserMenu(special)
if err != nil || len(menus) != 1 || menus[0].MenuId != 11 {
t.Fatalf("saved permission did not reach navigation: %v %v", menus, err)
}
if _, err := g.DB().Exec(ctx, "DELETE FROM sys_role_menu WHERE role_id=103"); err != nil {
t.Fatal(err)
}
codes, _, err = NewSysAuth().GetUserAccessCodeList(special, 3)
if err != nil || slices.Contains(codes, "cpm:system:test:restricted") {
t.Fatalf("revoked role permission remained usable: %v %v", codes, err)
}
})
t.Run("角色详情说明模板和默认角色身份", func(t *testing.T) {
for _, test := range []struct {
id int64
template, defaultRole bool
}{{82, true, false}, {102, false, true}, {103, false, false}} {
role, err := service.View(ctx, &model.SysRoleViewParam{RoleId: test.id})
if err != nil {
t.Fatal(err)
}
if role.IsTemplateRole != test.template || role.IsDeptDefaultRole != test.defaultRole {
t.Fatalf("wrong role metadata: %+v", role)
}
}
})
t.Run("非平台管理员不能单独修改模板菜单或数据范围", func(t *testing.T) {
operator := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175}}})
if err := service.RoleMenu(operator, 82, []int64{10}, nil); err == nil || !strings.Contains(err.Error(), "仅平台超级管理员") {
t.Fatalf("template menu change was not rejected: %v", err)
}
if err := service.EditRoleDataScope(operator, &model.SysRoleDataScopeEditParam{RoleId: 82, DataScope: consts.SysRoleDataScopeDeptAndBelow}); err == nil || !strings.Contains(err.Error(), "仅平台超级管理员") {
t.Fatalf("template data scope change was not rejected: %v", err)
}
})
t.Run("企业入口允许已有下发角色时新增多个普通角色", func(t *testing.T) {
for _, roleName := range []string{"财务", "客服"} {
roleID, err := agent.NewAgentRole().RoleSave(ctx, &model.AgentRoleSaveParam{RoleName: roleName, RoleKey: "staff", MenuIds: []int64{10}, DataScope: consts.SysRoleDataScopePersonal})
if err != nil {
t.Fatal(err)
}
record, err := g.DB().Model("sys_role").Where("role_id", roleID).One()
if err != nil || record["is_pass"].Int() != 0 || record["data_scope"].String() != consts.SysRoleDataScopePersonal {
t.Fatalf("ordinary role was changed: %v %v", record, err)
}
}
})
t.Run("配置平台模板后历史下发标记不限制普通角色维护", func(t *testing.T) {
_, err := agent.NewAgentRole().RoleSave(ctx, &model.AgentRoleSaveParam{RoleName: "历史标记角色", RoleKey: "legacy", IsPass: 1})
if err != nil {
t.Fatal(err)
}
})
t.Run("平台模板由参数识别不依赖隐藏下发标记", func(t *testing.T) {
_, err := agent.NewAgentRole().RoleSave(rolesync.DeferRecording(ctx), &model.AgentRoleSaveParam{RoleId: 82, RoleName: "模板", RoleKey: "template", MenuIds: []int64{10}, IsPass: 0})
if err != nil {
t.Fatal(err)
}
template, err := rolepolicy.Template(ctx)
if err != nil || template == nil || template.RoleId != 82 || template.IsPass != 0 {
t.Fatalf("configured template not recognized: %+v %v", template, err)
}
})
t.Run("企业入口拒绝错误历史传递标记", func(t *testing.T) {
_, err := agent.NewAgentRole().RoleSave(ctx, &model.AgentRoleSaveParam{RoleName: "无效标记", RoleKey: "invalid", IsPass: 2})
if err == nil || !strings.Contains(err.Error(), "传递标记无效") {
t.Fatalf("invalid legacy marker was accepted: %v", err)
}
})
t.Run("配置模板后历史下发标记不阻断普通角色删除", func(t *testing.T) {
for _, statement := range []string{
"INSERT INTO sys_role(role_id,tenant_id,dept_id,created_dept,status,is_pass,data_scope) VALUES(600,'000000',100,100,'0',1,'4')",
"UPDATE sys_role SET created_dept=100 WHERE role_id=102",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
if _, err := service.Delete(ctx, &model.SysRoleDeleteParam{RoleId: 600}); err != nil {
t.Fatal(err)
}
})
t.Run("授权范围不能超出当前操作员有效角色", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,created_dept,status,is_pass,data_scope,role_key) VALUES(501,'000000',175,175,'0',0,'4','staff')"); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, "INSERT INTO sys_user_role VALUES('000000',3,501)"); err != nil {
t.Fatal(err)
}
operator := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 3, TenantId: "000000", DeptId: 175}}})
if err := service.RoleMenu(operator, 501, []int64{10}, nil); err == nil || !strings.Contains(err.Error(), "超出") {
t.Fatalf("operator without permission granted it: %v", err)
}
if _, err := g.DB().Exec(ctx, "INSERT INTO sys_role_menu VALUES('000000',501,10,'0')"); err != nil {
t.Fatal(err)
}
if err := service.RoleMenu(operator, 501, []int64{10}, nil); err != nil {
t.Fatal(err)
}
if _, err := g.DB().Exec(ctx, "UPDATE sys_role SET status='1' WHERE role_id=501"); err != nil {
t.Fatal(err)
}
if err := service.RoleMenu(operator, 501, []int64{10}, nil); err == nil {
t.Fatal("disabled operator role granted permission")
}
})
t.Run("展示祖先兼容旧请求但不写入实际授权", func(t *testing.T) {
for _, statement := range []string{
"INSERT INTO sys_menu VALUES (900,'展示目录',0,'display','M',0,'','0',NULL),(901,'未授权父菜单',900,'parent','C',0,'system:parent:list','0',NULL)",
"UPDATE sys_menu SET parent_id=901 WHERE menu_id=10",
} {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
operator := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175}}})
tree, assignable, err := agent.NewAgentMenu().AllowedMenuTree(operator, 175)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(assignable, 10) || slices.Contains(assignable, 900) || slices.Contains(assignable, 901) {
t.Fatalf("display ancestors leaked into assignable IDs: %v", assignable)
}
if len(tree) != 3 || !slices.ContainsFunc(tree, func(menu *model.SysMenuTreeModel) bool { return menu.MenuId == 901 && menu.ParentId == 900 }) || !slices.ContainsFunc(tree, func(menu *model.SysMenuTreeModel) bool { return menu.MenuId == 10 && menu.ParentId == 901 }) {
t.Fatalf("display hierarchy is incomplete: %+v", tree)
}
if err := service.RoleMenu(operator, 502, []int64{900, 901, 10}, nil); err != nil {
t.Fatal(err)
}
var stored []int64
values, err := g.DB().Model("sys_role_menu").Where("role_id", 502).Array("menu_id")
if err != nil {
t.Fatal(err)
}
for _, value := range values {
stored = append(stored, value.Int64())
}
if len(stored) != 1 || stored[0] != 10 {
t.Fatalf("display-only permissions were persisted: %v", stored)
}
newRole, err := agent.NewAgentRole().RoleSave(rolesync.DeferRecording(operator), &model.AgentRoleSaveParam{
RoleName: "下级测试角色", RoleKey: "scoped-agent", MenuIds: []int64{900, 901, 10},
})
if err != nil {
t.Fatal(err)
}
storedMenus, err := g.DB().Model("sys_role_menu").Where("role_id", newRole).Array("menu_id")
if err != nil || len(storedMenus) != 1 || storedMenus[0].Int64() != 10 {
t.Fatalf("agent save did not preserve the same authorization boundary: %v %v", storedMenus, err)
}
if err := service.RoleMenu(operator, 502, []int64{10, 11}, nil); err == nil {
t.Fatal("unrelated permission was accepted")
}
count, err := g.DB().Model("sys_role_menu").Where("role_id", 502).Count()
if err != nil || count != 1 {
t.Fatalf("failed validation changed permissions: %d %v", count, err)
}
})
t.Run("默认角色缺失明确报错而不是静默返回404", func(t *testing.T) {
if _, err := g.DB().Exec(ctx, "UPDATE sys_dept SET dept_role=0 WHERE dept_id=175"); err != nil {
t.Fatal(err)
}
defer func() { _, _ = g.DB().Exec(ctx, "UPDATE sys_dept SET dept_role=102 WHERE dept_id=175") }()
if _, err := rolepolicy.RestrictMenus(ctx, "000000", 175, []int64{10}); err == nil || !strings.Contains(err.Error(), "未关联默认角色") {
t.Fatalf("missing role must be actionable: %v", err)
}
operator := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 2, TenantId: "000000", DeptId: 175}}})
if _, err := g.DB().Exec(ctx, "UPDATE sys_dept SET dept_type=1 WHERE dept_id=175"); err != nil {
t.Fatal(err)
}
if err := agent.NewAgentDept().EnsureCurrentDeptDefaultRole(operator); err == nil {
t.Fatal("missing default role was not reported")
}
count, err := g.DB().Model("sys_user_role").Where("user_id", 2).Count()
if err != nil || count != 1 {
t.Fatalf("read request changed user roles: %d %v", count, err)
}
})
}
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+1 -1
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
+18 -2
View File
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -16,6 +16,7 @@ import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/bcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/smsgateway"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
@@ -317,6 +318,9 @@ func (l *sSysUser) AddUser(ctx context.Context, req *model.SysUserAddModel) (dat
return nil, gerror.NewCode(consts.CodeDeptNotFound, "部门不存在")
}
if err := rolepolicy.ValidateUserRoleAssignment(ctx, req.TenantId, req.DeptId, req.RoleIds, nil); err != nil {
return nil, err
}
// 判断用户名是否存在(在当前租户下)
var user *entity.SysUser
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, req.UserName).Where(dao.SysUser.Columns().TenantId, req.TenantId).Scan(&user)
@@ -600,6 +604,18 @@ func (l *sSysUser) UpdateUser(ctx context.Context, req *model.SysUserUpdateModel
return gerror.NewCode(consts.CodeUserNotFound, "用户不存在")
}
tenantId := existsUser.TenantId
existingRoles, err := l.GetUserRoleIds(ctx, req.UserId)
if err != nil {
return err
}
deptID := existsUser.DeptId
if req.DeptId != nil && *req.DeptId != deptID {
deptID = *req.DeptId
existingRoles = nil
}
if err := rolepolicy.ValidateUserRoleAssignment(ctx, tenantId, deptID, req.RoleIds, existingRoles); err != nil {
return err
}
req.UpdatedAt = gtime.Now()
req.UpdatedBy = &currUserId
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, req.UserId).Data(req).OmitNil().Update()
@@ -801,7 +817,7 @@ func (l *sSysUser) UpdateLoginInfo(ctx context.Context, userId int64, loginIp st
func (l *sSysUser) GetUserRoleIds(ctx context.Context, userId int64) (roleIds []int64, err error) {
urList := make([]*entity.SysUserRole, 0)
tenantId := contexts.GetTenantId(ctx)
db := dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, userId)
db := dao.SysUserRole.Ctx(ctx).Master().Where(dao.SysUserRole.Columns().UserId, userId)
if tenantId != "" {
db = db.Where(dao.SysUserRole.Columns().TenantId, tenantId)
}
@@ -1,7 +1,7 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
@@ -0,0 +1,51 @@
package system_test
import (
"context"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
)
func TestSysUser_UpdateUserPassword(t *testing.T) {
database, err := gdb.New(gdb.ConfigNode{Type: "sqlite", Link: "sqlite::@file(:memory:)"})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
t.Cleanup(func() { _ = database.Close(ctx) })
if _, err := database.Exec(ctx, "CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, tenant_id TEXT, password TEXT, salt TEXT, updated_by INTEGER, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME)"); err != nil {
t.Fatal(err)
}
for _, userID := range []int64{900001, 900002} {
if _, err := database.Model(dao.SysUser.Table()).Ctx(ctx).Data(do.SysUser{
UserId: userID, TenantId: "test-only", Password: "original", Salt: "original",
}).Insert(); err != nil {
t.Fatal(err)
}
}
password := utility.PasswordEncrypt(utility.Md5("isolated-test-password"), "isolated-test-salt")
if _, err := database.Model(dao.SysUser.Table()).Ctx(ctx).
Where("tenant_id", "test-only").Where("user_id", 900001).
Data(do.SysUser{Password: password, Salt: "isolated-test-salt", UpdatedBy: 900001}).Update(); err != nil {
t.Fatal(err)
}
var updated struct {
Password string
Salt string
}
if err := database.Model(dao.SysUser.Table()).Ctx(ctx).Where("user_id", 900001).Scan(&updated); err != nil {
t.Fatal(err)
}
if updated.Password != password || updated.Salt != "isolated-test-salt" {
t.Fatal("password and salt were not updated in the isolated database")
}
untouched, err := database.Model(dao.SysUser.Table()).Ctx(ctx).Where("user_id", 900002).Value("password")
if err != nil || untouched.String() != "original" {
t.Fatalf("another test user was modified: %v", err)
}
}
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package system
+169
View File
@@ -0,0 +1,169 @@
// // package system_test
// // @Link https://github.com/xiujiecn/xiu-admin
// // @Copyright Copyright (c) 2025 LiXiujie
// // @Author Lxj <xxx@example.com>
// // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system_test
// import (
// "context"
// "encoding/json"
// "fmt"
// "testing"
// "time"
// "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
// "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
// "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/worker"
// "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
// "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/tasks"
// _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
// "github.com/gogf/gf/v2/frame/g"
// "github.com/gogf/gf/v2/os/gctx"
// "github.com/gogf/gf/v2/util/gconv"
// )
// func init() {
// // 数据库只读,不执行
// // g.DB().SetDryRun(true)
// g.DB().SetDebug(true)
// testing.Init()
// }
// func TestTask_runOnce(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
// taskData := tasks.Task{
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
// MethodName: exists.InvokeTarget,
// Params: paramArr,
// Explain: exists.JobName,
// }
// runPayload, _ := json.Marshal(taskData)
// tasks.TasksInstance().Once(
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskTimeout(10),
// worker.WithTaskNow(true),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
// }
// func TestTask_schedule(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
// //获取task目录下是否绑定对应的方法
// actualName, exist := tasks.TasksInstance().CheckFuncName(exists.InvokeTarget)
// if !exist {
// errInfo := fmt.Sprintf("没有绑定对应的方法:%s", exists.InvokeTarget)
// g.Log().Error(ctx, errInfo)
// }
// //传参解析
// paramArr, err := tasks.TasksInstance().ParseParameters(exists.JobParams)
// if err != nil {
// g.Log().Error(ctx, err)
// }
// taskData := tasks.Task{
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
// MethodName: actualName,
// Params: paramArr,
// Explain: exists.JobName,
// }
// runPayload, _ := json.Marshal(taskData)
// _, err2 := tasks.TasksInstance().Cron(
// 1,
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskExpr("0/2 * * * * *"),
// //worker.WithTaskExpr("@every 2s"),
// worker.WithTaskTimeout(10),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
// if err2 != nil {
// g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
// }
// time.Sleep(10 * time.Minute)
// }
// func TestTask_remove(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
// g.Log().Debug(ctx, taskJobId)
// err := tasks.TasksInstance().Remove(taskJobId)
// if err != nil {
// g.Log().Debug(ctx, exists.JobName, "启动停止失败")
// g.Log().Error(ctx, err)
// }
// }
// func TestTask_getTask(t *testing.T) {
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
// task, err := tasks.TasksInstance().GetTask(taskJobId)
// if err != nil {
// g.Log().Debug(ctx, exists.JobName, "获取任务失败")
// g.Log().Error(ctx, err)
// }
// fmt.Println("task", task)
// }