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
+86 -53
View File
@@ -42,11 +42,17 @@ func init() {
}
func (s *sAgentDept) DeptList(ctx context.Context, in *model.AgentDeptListParam) (list []*model.AgentEnterpriseCustomerModel, total int, err error) {
if in == nil {
in = &model.AgentDeptListParam{}
}
in.NormalizePagination()
list = make([]*model.AgentEnterpriseCustomerModel, 0)
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return nil, 0, gerror.New("当前用户未绑定组织")
}
m := s.deptModel(ctx).
Where(dao.SysDept.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysDept.Columns().ParentId, currentDeptId).
Where(dao.SysDept.Columns().DeptType, 1)
if in.DeptName != "" {
@@ -56,14 +62,15 @@ func (s *sAgentDept) DeptList(ctx context.Context, in *model.AgentDeptListParam)
m = m.Where(dao.SysDept.Columns().Status, in.Status)
}
err = m.Order(dao.SysDept.Columns().DeptId, "ASC").
Scan(&list)
Page(in.Page, in.PageSize).
ScanAndCount(&list, &total, false)
if err != nil {
return nil, 0, err
}
if err = s.fillEnterpriseDefaultUsers(ctx, list); err != nil {
return nil, 0, err
}
return list, len(list), nil
return list, total, nil
}
func (s *sAgentDept) DeptView(ctx context.Context, deptId int64) (res *model.AgentEnterpriseCustomerModel, err error) {
@@ -124,63 +131,25 @@ func (s *sAgentDept) DeptSave(ctx context.Context, in *model.AgentDeptSaveParam)
return deptId, nil
}
// EnsureCurrentDeptDefaultRole upgrades legacy child enterprises that did not
// receive an inherited administrator role when they were created.
func (s *sAgentDept) EnsureCurrentDeptDefaultRole(ctx context.Context) error {
user := contexts.GetUser(ctx)
if user == nil {
if contexts.GetUser(ctx) == nil {
return gerror.New("登录已过期")
}
deptId := contexts.GetDeptId(ctx)
if deptId == 0 {
deptID := contexts.GetDeptId(ctx)
if deptID <= 0 {
return gerror.New("当前用户未绑定组织")
}
var dept *entity.SysDept
if err := s.deptModel(ctx).
Where(dao.SysDept.Columns().DeptId, deptId).
Scan(&dept); err != nil {
if err := s.deptModel(ctx).Where(dao.SysDept.Columns().DeptId, deptID).Scan(&dept); err != nil {
return err
}
if dept == nil || dept.DeptId == 0 {
if dept == nil {
return gerror.New("组织不存在")
}
if dept.DeptType != 1 || dept.DeptRole != 0 {
return nil
if dept.DeptType == 1 && dept.DeptRole <= 0 {
return gerror.Newf("机构%d未关联默认角色,请联系平台管理员核对机构角色绑定", deptID)
}
sourceRole, menuIds, err := s.passRoleByDept(ctx, dept.ParentId)
if err != nil {
return err
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var current *entity.SysDept
if err := tx.Ctx(ctx).Model(dao.SysDept.Table()).
Where(dao.SysDept.Columns().DeptId, deptId).
Scan(&current); err != nil {
return err
}
if current == nil || current.DeptId == 0 {
return gerror.New("组织不存在")
}
if current.DeptRole != 0 {
return nil
}
roleId, err := s.createEnterpriseRole(ctx, tx, user, deptId, sourceRole, menuIds)
if err != nil {
return err
}
if err := s.updateDeptRole(ctx, tx, deptId, roleId, user.ID); err != nil {
return err
}
// Keep the legacy role for the active session. After re-login, this role
// becomes the department's selected default administrator role.
_, err = tx.Ctx(ctx).Model(dao.SysUserRole.Table()).
Data(do.SysUserRole{UserId: user.ID, RoleId: roleId}).
OmitNil().Insert()
return err
})
return nil
}
func (s *sAgentDept) validateEnterpriseSave(ctx context.Context, tenantId string, in *model.AgentDeptSaveParam) error {
@@ -498,7 +467,10 @@ func (s *sAgentDept) passRoleByDept(ctx context.Context, deptId int64) (*entity.
if err != nil {
return nil, nil, err
}
if template != nil && contexts.GetTenantId(ctx) == template.TenantId {
if template != nil {
if contexts.GetTenantId(ctx) != template.TenantId {
return nil, nil, gerror.New("当前租户不属于平台代理模板授权范围")
}
// 配置了平台模板后,下发来源不再依赖 is_pass=1:
// 一级代理由平台模板推出,其默认角色是复制来的 is_pass=0 角色;
// 各级代理再创建下级时,必须能从本机构默认角色(sys_dept.dept_role)继续传递,
@@ -523,7 +495,7 @@ func (s *sAgentDept) passRoleByDept(ctx context.Context, deptId int64) (*entity.
}
}
var menus []*entity.SysRoleMenu
if err := dao.SysRoleMenu.Ctx(ctx).Where("role_id", source.RoleId).Scan(&menus); err != nil {
if err := dao.SysRoleMenu.Ctx(ctx).Master().Where("tenant_id", template.TenantId).Where("role_id", source.RoleId).Scan(&menus); err != nil {
return nil, nil, err
}
ids := make([]int64, 0, len(menus))
@@ -690,7 +662,11 @@ func (s *sAgentDept) childDeptAncestors(ctx context.Context, parentId int64) (st
if ancestors == "" {
ancestors = "0"
}
return fmt.Sprintf("%s,%d", ancestors, parent.DeptId), nil
// ancestors 必须与平台约定格式一致:以本机构结尾并带尾逗号(如 "0,1,172,")。
// 早期实现返回 "0,1,172"(缺尾逗号),而 GetDeptAndSub 按 '%,id,%' 展开
// 「本机构及以下」数据权限,缺尾逗号时匹配不到,表现为上级机构看不到下级
// (例:一级代理在卡片划拨里选不到下级客户)。
return fmt.Sprintf("%s,%d,", ancestors, parent.DeptId), nil
}
func (s *sAgentDept) nextCustomerDeptKey(ctx context.Context) (string, error) {
@@ -744,11 +720,68 @@ func (s *sAgentDept) ensureUserNameUnique(ctx context.Context, tenantId, userNam
}
func (s *sAgentDept) fillEnterpriseDefaultUsers(ctx context.Context, list []*model.AgentEnterpriseCustomerModel) error {
if len(list) == 0 {
return nil
}
var (
deptIDs = make([]int64, 0, len(list))
roleIDs = make([]int64, 0, len(list))
bindings []struct {
RoleId int64
UserId int64
}
fallbacks []struct {
DeptId int64
UserId int64
}
)
for _, dept := range list {
user, err := s.defaultUserByDept(ctx, dept.DeptId, dept.DeptRole)
if err != nil {
deptIDs = append(deptIDs, dept.DeptId)
if dept.DeptRole > 0 {
roleIDs = append(roleIDs, dept.DeptRole)
}
}
tenantID := contexts.GetTenantId(ctx)
if len(roleIDs) > 0 {
if err := dao.SysUserRole.Ctx(ctx).Where("tenant_id", tenantID).
WhereIn("role_id", roleIDs).Fields("role_id", "MIN(user_id) AS user_id").Group("role_id").Scan(&bindings); err != nil {
return err
}
}
if err := s.userModel(ctx).Where("tenant_id", tenantID).WhereIn("dept_id", deptIDs).
Fields("dept_id", "MIN(user_id) AS user_id").Group("dept_id").Scan(&fallbacks); err != nil {
return err
}
var (
roleUsers = make(map[int64]int64, len(bindings))
deptUsers = make(map[int64]int64, len(fallbacks))
userIDs = make([]int64, 0, len(bindings)+len(fallbacks))
users []*entity.SysUser
)
for _, binding := range bindings {
roleUsers[binding.RoleId] = binding.UserId
userIDs = append(userIDs, binding.UserId)
}
for _, fallback := range fallbacks {
deptUsers[fallback.DeptId] = fallback.UserId
userIDs = append(userIDs, fallback.UserId)
}
if len(userIDs) == 0 {
return nil
}
if err := s.userModel(ctx).Where("tenant_id", tenantID).WhereIn("dept_id", deptIDs).
WhereIn("user_id", userIDs).Fields("user_id", "dept_id", "user_name", "nick_name").Scan(&users); err != nil {
return err
}
usersByID := make(map[int64]*entity.SysUser, len(users))
for _, user := range users {
usersByID[user.UserId] = user
}
for _, dept := range list {
user := usersByID[roleUsers[dept.DeptRole]]
if user == nil || user.DeptId != dept.DeptId {
user = usersByID[deptUsers[dept.DeptId]]
}
if user == nil {
continue
}
@@ -0,0 +1,72 @@
package agent
import (
"context"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"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/service"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
type paginationDeptService struct{ service.ISysDept }
func (paginationDeptService) Model(ctx context.Context, _ ...*handler.Option) *gdb.Model {
return g.DB().Model("sys_dept").Ctx(ctx).Handler(handler.FilterTenant)
}
type paginationUserService struct{ service.ISysUser }
func (paginationUserService) Model(ctx context.Context, _ ...*handler.Option) *gdb.Model {
return g.DB().Model("sys_user").Ctx(ctx).Handler(handler.FilterTenant)
}
func TestEnterpriseCustomerDatabasePaginationAndDefaultUsers(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{TenantId: "000000", DeptId: 1}}})
service.RegisterSysDept(paginationDeptService{})
service.RegisterSysUser(paginationUserService{})
t.Cleanup(func() {
service.RegisterSysDept(nil)
service.RegisterSysUser(nil)
})
statements := []string{
"ALTER TABLE sys_dept ADD COLUMN dept_name TEXT",
"CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, tenant_id TEXT, dept_id INTEGER, user_name TEXT, nick_name TEXT, deleted_at DATETIME)",
"INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role,dept_name) VALUES (2,'000000',1,1,12,'企业A'),(3,'000000',1,1,13,'企业B'),(4,'000000',1,1,14,'企业C'),(5,'000000',2,1,15,'孙级企业'),(6,'other',1,1,16,'其他租户'),(7,'000000',1,0,17,'内部部门'),(8,'000000',1,1,18,'已删除企业'),(9,'000000',1,1,0,'无账号企业')",
"UPDATE sys_dept SET deleted_at=CURRENT_TIMESTAMP WHERE dept_id=8",
"INSERT INTO sys_user VALUES (20,'000000',2,'fallback-a','回退A',NULL),(21,'000000',2,'admin-a','管理员A',NULL),(30,'000000',3,'deleted-admin','已删除',CURRENT_TIMESTAMP),(31,'000000',3,'fallback-b','回退B',NULL),(40,'000000',4,'fallback-c','回退C',NULL),(50,'other',4,'other-user','跨租户',NULL)",
"INSERT INTO sys_user_role (tenant_id,user_id,role_id) VALUES ('000000',21,12),('000000',30,13),('other',50,14)",
}
for _, statement := range statements {
mustExecAgentRoleSyncTest(t, ctx, statement)
}
subject := NewAgentDept()
params := &model.AgentDeptListParam{Page: 1, PageSize: 2}
list, total, err := subject.DeptList(ctx, params)
if err != nil || total != 4 || len(list) != 2 {
t.Fatalf("page 1: total=%d list=%+v err=%v", total, list, err)
}
if list[0].DeptId != 2 || list[0].UserName != "admin-a" || list[1].DeptId != 3 || list[1].UserName != "fallback-b" {
t.Fatalf("unexpected account selection: %+v %+v", list[0], list[1])
}
params.Page = 2
list, total, err = subject.DeptList(ctx, params)
if err != nil || total != 4 || len(list) != 2 || list[0].DeptId != 4 || list[0].UserName != "fallback-c" || list[1].DeptId != 9 || list[1].UserName != "" {
t.Fatalf("page 2: total=%d list=%+v err=%v", total, list, err)
}
params.Page = 3
list, total, err = subject.DeptList(ctx, params)
if err != nil || total != 4 || len(list) != 0 || list == nil {
t.Fatalf("out of range: total=%d list=%+v err=%v", total, list, err)
}
list, total, err = subject.DeptList(ctx, &model.AgentDeptListParam{DeptName: "企业A", Status: "0"})
if err != nil || total != 1 || len(list) != 1 || list[0].DeptId != 2 {
t.Fatalf("filtered list: total=%d list=%+v err=%v", total, list, err)
}
}
@@ -12,9 +12,10 @@ import (
"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/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
)
// 复现生产形态:模板角色已配置(role 18 @ dept 100,is_pass=1),
@@ -56,6 +57,8 @@ func TestPassRoleByDeptWorksForChildAgentWithoutTemplateFlag(t *testing.T) {
func setupPassRoleTestDB(t *testing.T) {
t.Helper()
service.RegisterSysDept(system.NewSysDept())
service.RegisterSysRole(system.NewSysRole())
if err := gdb.SetConfig(gdb.Config{
"default": gdb.ConfigGroup{
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
@@ -69,7 +72,7 @@ func setupPassRoleTestDB(t *testing.T) {
}
ctx := context.Background()
t.Cleanup(func() { _ = g.DB().Close(ctx) })
mustExec(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
mustExec(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_name TEXT, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
mustExec(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
mustExec(t, ctx, `CREATE TABLE sys_role (
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
+169
View File
@@ -0,0 +1,169 @@
package agent
import (
"context"
"encoding/json"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"github.com/gogf/gf/v2/errors/gerror"
)
// agentExportColumns 只允许列表字段,列标题和映射由服务端确定。
func agentExportColumns(raw string, defaults []model.ExportColumn) ([]model.ExportColumn, error) {
if strings.TrimSpace(raw) == "" {
return nil, gerror.New("请选择导出列")
}
var requested []model.ExportColumn
if err := json.Unmarshal([]byte(raw), &requested); err != nil {
return nil, gerror.New("导出列格式错误")
}
allowed := make(map[string]model.ExportColumn, len(defaults))
for _, col := range defaults {
allowed[col.Field] = col
}
result := make([]model.ExportColumn, 0, len(requested))
seen := make(map[string]bool)
for _, col := range requested {
safe, ok := allowed[col.Field]
if !ok {
return nil, gerror.New("包含不允许导出的字段")
}
if !seen[col.Field] {
result = append(result, safe)
seen[col.Field] = true
}
}
if len(result) == 0 {
return nil, gerror.New("请选择导出列")
}
return result, nil
}
func (s *sAgentUser) UserExport(ctx context.Context, in *model.AgentUserListParam) error {
columns, err := agentExportColumns(in.ExportColumnsJson, []model.ExportColumn{
{Field: "userName", Label: "用户名称"}, {Field: "nickName", Label: "用户昵称"},
{Field: "roles", Label: "角色"}, {Field: "phonenumber", Label: "联系电话"},
{Field: "email", Label: "邮箱"}, {Field: "status", Label: "状态"}, {Field: "createdAt", Label: "创建时间"},
})
if err != nil {
return err
}
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "agentUser", BizName: "用户管理", PermCode: "cpm:agent:user:export", QueryParam: in, Columns: columns,
RowFields: agentExportFields(columns),
ToRow: func(value interface{}) []interface{} {
row := value.(map[string]interface{})
values := make([]interface{}, 0, len(columns))
for _, column := range columns {
values = append(values, row[column.Field])
}
return values
},
Tags: func() ([]string, error) {
labels := make([]string, 0, len(columns))
for _, col := range columns {
labels = append(labels, col.Label)
}
return labels, nil
},
Fetch: func(ctx context.Context, page, pageSize int) ([]interface{}, int, error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, total, err := s.UserList(ctx, &query)
if err != nil {
return nil, 0, err
}
rows := make([]interface{}, 0, len(list))
for _, item := range list {
roles := make([]string, 0, len(item.Roles))
for _, role := range item.Roles {
if role != nil && role.RoleName != "" {
roles = append(roles, role.RoleName)
}
}
roleNames := strings.Join(roles, "、")
if roleNames == "" {
roleNames = "-"
}
createdAt := ""
if item.CreatedAt != nil {
createdAt = item.CreatedAt.Format("Y-m-d H:i:s")
}
rows = append(rows, map[string]interface{}{"userName": item.UserName, "nickName": item.NickName, "roles": roleNames, "phonenumber": item.Phonenumber, "email": item.Email, "status": agentExportStatus(item.Status), "createdAt": createdAt})
}
return rows, total, nil
},
})
return err
}
func (s *sAgentDept) DeptExport(ctx context.Context, in *model.AgentDeptListParam) error {
columns, err := agentExportColumns(in.ExportColumnsJson, []model.ExportColumn{
{Field: "deptName", Label: "企业名称"}, {Field: "userName", Label: "登录账号"}, {Field: "nickName", Label: "负责人"},
{Field: "phone", Label: "联系电话"}, {Field: "email", Label: "联系人邮箱"}, {Field: "areaCode", Label: "组织所在地"},
{Field: "address", Label: "详细地址"}, {Field: "createdAt", Label: "创建时间"}, {Field: "status", Label: "状态"},
})
if err != nil {
return err
}
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "agentDept", BizName: "企业客户", PermCode: "cpm:agent:dept:export", QueryParam: in, Columns: columns,
RowFields: agentExportFields(columns),
ToRow: func(value interface{}) []interface{} {
row := value.(map[string]interface{})
values := make([]interface{}, 0, len(columns))
for _, column := range columns {
values = append(values, row[column.Field])
}
return values
},
Tags: func() ([]string, error) {
labels := make([]string, 0, len(columns))
for _, col := range columns {
labels = append(labels, col.Label)
}
return labels, nil
},
Fetch: func(ctx context.Context, page, pageSize int) ([]interface{}, int, error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, total, err := s.DeptList(ctx, &query)
if err != nil {
return nil, 0, err
}
rows := make([]interface{}, 0, len(list))
for _, item := range list {
createdAt := ""
if item.CreatedAt != nil {
createdAt = item.CreatedAt.Format("Y-m-d H:i:s")
}
rows = append(rows, map[string]interface{}{"deptName": item.DeptName, "userName": item.UserName, "nickName": item.NickName, "phone": item.Phone, "email": item.Email, "areaCode": item.AreaCode, "address": item.Address, "createdAt": createdAt, "status": agentExportStatus(item.Status)})
}
return rows, total, nil
},
})
return err
}
func agentExportFields(columns []model.ExportColumn) []string {
fields := make([]string, 0, len(columns))
for _, column := range columns {
fields = append(fields, column.Field)
}
return fields
}
func agentExportStatus(status string) string {
if status == "0" {
return "正常"
}
if status == "1" {
return "停用"
}
return "未知"
}
+21 -67
View File
@@ -46,17 +46,22 @@ func (s *sAgentMenu) AllowedMenuIdsByDeptRole(ctx context.Context, deptId int64)
return []int64{}, nil
}
if dept.DeptRole == consts.SuperAdminRoleId {
return s.allMenuIds(ctx)
menuIds, err = s.allMenuIds(ctx)
} else {
var roleMenus []*entity.SysRoleMenu
roleMenus, err = service.SysRole().GetRoleListMenu(ctx, []int64{dept.DeptRole})
for _, item := range roleMenus {
menuIds = append(menuIds, item.MenuId)
}
}
roleMenus, err := service.SysRole().GetRoleListMenu(ctx, []int64{dept.DeptRole})
if err != nil {
return nil, err
}
for _, item := range roleMenus {
menuIds = append(menuIds, item.MenuId)
}
slices.Sort(menuIds)
menuIds = slices.Compact(menuIds)
if !contexts.IsSuperAdmin(ctx) {
return rolepolicy.RestrictAssignableMenus(ctx, dept.TenantId, dept.DeptId, contexts.GetUserId(ctx), menuIds)
}
return rolepolicy.RestrictMenus(ctx, dept.TenantId, dept.DeptId, menuIds)
}
@@ -83,32 +88,6 @@ func (s *sAgentMenu) getRoleById(ctx context.Context, roleId int64) (role *entit
return role, err
}
// filterMenuIdsByAllowed 筛选出当前机构允许的菜单ID
func (s *sAgentMenu) filterMenuIdsByAllowed(ctx context.Context, deptId int64, menuIds []int64) (res []int64, err error) {
if deptId == 0 {
deptId = contexts.GetDeptId(ctx)
}
allowed, err := s.AllowedMenuIdsByDeptRole(ctx, deptId)
if err != nil {
return nil, err
}
allowed, err = s.withMenuAncestors(ctx, allowed)
if err != nil {
return nil, err
}
allowedMap := make(map[int64]struct{}, len(allowed))
for _, menuId := range allowed {
allowedMap[menuId] = struct{}{}
}
for _, menuId := range menuIds {
if _, ok := allowedMap[menuId]; ok {
res = append(res, menuId)
}
}
slices.Sort(res)
return slices.Compact(res), nil
}
func (s *sAgentMenu) AllowedMenuTree(ctx context.Context, deptId int64) (tree []*model.SysMenuTreeModel, menuIds []int64, err error) {
deptId = contexts.GetDeptId(ctx)
menuIds, err = s.AllowedMenuIdsByDeptRole(ctx, deptId)
@@ -118,13 +97,13 @@ func (s *sAgentMenu) AllowedMenuTree(ctx context.Context, deptId int64) (tree []
if len(menuIds) == 0 && !contexts.IsSuperAdmin(ctx) {
return []*model.SysMenuTreeModel{}, []int64{}, nil
}
menuIds, err = s.withMenuAncestors(ctx, menuIds)
visibleMenuIds, err := rolepolicy.WithMenuAncestors(ctx, menuIds)
if err != nil {
return nil, nil, err
}
mod := service.SysMenu().Model(ctx)
if !contexts.IsSuperAdmin(ctx) {
mod = mod.WhereIn(dao.SysMenu.Columns().MenuId, menuIds)
mod = mod.WhereIn(dao.SysMenu.Columns().MenuId, visibleMenuIds)
}
var menus []*entity.SysMenu
err = mod.Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
@@ -134,43 +113,18 @@ func (s *sAgentMenu) AllowedMenuTree(ctx context.Context, deptId int64) (tree []
return nil, nil, err
}
tree = s.buildMenuList(menus)
activeMenuIds := make([]int64, 0, len(menus))
for _, menu := range menus {
activeMenuIds = append(activeMenuIds, menu.MenuId)
}
if contexts.IsSuperAdmin(ctx) {
menuIds = activeMenuIds
} else {
menuIds = rolepolicy.Intersect(menuIds, activeMenuIds)
}
return tree, menuIds, nil
}
func (s *sAgentMenu) withMenuAncestors(ctx context.Context, menuIds []int64) ([]int64, error) {
seen := make(map[int64]struct{}, len(menuIds))
queue := append([]int64{}, menuIds...)
for _, menuId := range menuIds {
seen[menuId] = struct{}{}
}
for len(queue) > 0 {
var menus []*entity.SysMenu
err := service.SysMenu().Model(ctx).
WhereIn(dao.SysMenu.Columns().MenuId, queue).
Scan(&menus)
if err != nil {
return nil, err
}
queue = queue[:0]
for _, menu := range menus {
if menu.ParentId == 0 {
continue
}
if _, ok := seen[menu.ParentId]; ok {
continue
}
seen[menu.ParentId] = struct{}{}
queue = append(queue, menu.ParentId)
}
}
res := make([]int64, 0, len(seen))
for menuId := range seen {
res = append(res, menuId)
}
slices.Sort(res)
return res, nil
}
func (s *sAgentMenu) buildMenuList(menus []*entity.SysMenu) (list []*model.SysMenuTreeModel) {
for _, menu := range menus {
list = append(list, &model.SysMenuTreeModel{
+3 -198
View File
@@ -2,8 +2,6 @@ package agent
import (
"context"
"slices"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
@@ -112,7 +110,7 @@ func (s *sAgentRole) RoleView(ctx context.Context, roleId int64) (res *model.Age
func (s *sAgentRole) RoleSave(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var saveErr error
roleId, saveErr = s.saveTemplateRole(ctx, in)
roleId, saveErr = s.saveTemplateRole(rolesync.DeferRecording(ctx), in)
if saveErr != nil {
return saveErr
}
@@ -153,24 +151,17 @@ func (s *sAgentRole) saveTemplateRole(ctx context.Context, in *model.AgentRoleSa
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
}
if !contexts.IsSuperAdmin(ctx) {
allowed, err := s.filterMenuIdsByAllowed(ctx, in.DeptId, in.MenuIds)
allowed, err := rolepolicy.NormalizeAssignableMenus(ctx, user.TenantId, in.DeptId, user.ID, in.MenuIds)
if err != nil {
return 0, err
}
for _, id := range in.MenuIds {
if !slices.Contains(allowed, id) {
return 0, gerror.New("角色包含超出本机构权限范围的菜单或按钮")
}
}
in.MenuIds = allowed
switch in.DataScope {
case consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel:
default:
return 0, gerror.New("代理角色数据权限不能超出本机构及下级范围")
}
}
// 只有本次保存的目标角色是平台代理模板时才走模板语义;
// 普通角色(如「运营人员」)不能被强制改写成 is_pass=1,
// 否则任何已配置模板的机构都无法再保存第二个角色。
template, templateErr := rolepolicy.TemplateForRoles(ctx, in.RoleId)
if templateErr != nil {
return 0, templateErr
@@ -181,7 +172,6 @@ func (s *sAgentRole) saveTemplateRole(ctx context.Context, in *model.AgentRoleSa
if template != nil {
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
}
// 未配置平台模板时,保留原有「本级唯一 is_pass=1 角色」下发流程。
legacyPass := in.IsPass == 1
if legacyPass {
configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
@@ -399,188 +389,3 @@ func (s *sAgentRole) inheritedRoleModel(ctx context.Context) *gdb.Model {
}
return m
}
func (s *sAgentRole) SyncInheritedRole(ctx context.Context, message *model.AgentRoleSyncMessage) error {
if message == nil || message.SourceRoleId <= 0 {
return gerror.New("invalid agent role sync message")
}
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return templateErr
}
if template != nil && message.SourceDeptId == template.DeptId && (message.TenantId == template.TenantId || message.TenantId == "") {
return syncTemplateRoles(ctx, template, message.ActorId)
}
var source *entity.SysRole
sourceModel := dao.SysRole.Ctx(ctx).Where(dao.SysRole.Columns().RoleId, message.SourceRoleId)
if message.TenantId != "" {
sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, message.TenantId)
}
if err := sourceModel.Scan(&source); err != nil {
return err
}
if source == nil || source.IsPass != 1 || source.DeletedAt != nil {
return nil
}
if message.SourceDeptId > 0 && message.SourceDeptId != source.DeptId {
return gerror.New("role sync source department changed")
}
var children []*entity.SysDept
if err := dao.SysDept.Ctx(ctx).
Where(dao.SysDept.Columns().TenantId, source.TenantId).
Where(dao.SysDept.Columns().ParentId, source.DeptId).
Where(dao.SysDept.Columns().DeptType, 1).
Scan(&children); err != nil {
return err
}
failed := 0
var firstErr error
for _, child := range children {
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return s.syncInheritedRoleChildrenTx(ctx, tx, message.SourceRoleId, source.TenantId, source.DeptId, message.ActorId, child.DeptId)
})
if err != nil {
failed++
if firstErr == nil {
firstErr = err
}
g.Log().Warningf(ctx, "agent role sync child failed, sourceRoleId:%d, childDeptId:%d, err:%v", source.RoleId, child.DeptId, err)
}
}
if firstErr != nil {
return gerror.Wrapf(firstErr, "agent role sync failed for %d of %d children", failed, len(children))
}
return nil
}
func (s *sAgentRole) syncInheritedRoleChildrenTx(ctx context.Context, tx gdb.TX, sourceRoleID int64, tenantId string, sourceDeptId, actorId, childDeptId int64) error {
if sourceRoleID == 0 {
return nil
}
var source *entity.SysRole
sourceModel := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, sourceRoleID)
if tenantId != "" {
sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, tenantId)
}
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
sourceModel = sourceModel.LockUpdate()
}
if err := sourceModel.Scan(&source); err != nil {
return err
}
if source == nil || source.RoleId == 0 || source.DeletedAt != nil || source.IsPass != 1 {
return nil
}
if sourceDeptId > 0 && source.DeptId != sourceDeptId {
return gerror.New("role sync source department changed")
}
var childDepts []*entity.SysDept
childDeptModel := tx.Ctx(ctx).Model(dao.SysDept.Table()).Where(dao.SysDept.Columns().TenantId, source.TenantId).Where(dao.SysDept.Columns().ParentId, source.DeptId).Where(dao.SysDept.Columns().DeptType, 1).Where(dao.SysDept.Columns().DeptId, childDeptId)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
childDeptModel = childDeptModel.LockUpdate()
}
if err := childDeptModel.Scan(&childDepts); err != nil {
return err
}
var menus []*entity.SysRoleMenu
if err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, sourceRoleID).OrderAsc(dao.SysRoleMenu.Columns().MenuId).Scan(&menus); err != nil {
return err
}
menuIDs := make([]int64, 0, len(menus))
for _, menu := range menus {
menuIDs = append(menuIDs, menu.MenuId)
}
allowed, err := rolepolicy.RestrictMenus(ctx, source.TenantId, source.DeptId, menuIDs)
if err != nil {
return err
}
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return templateErr
}
filtered := make([]*entity.SysRoleMenu, 0, len(menus))
for _, menu := range menus {
if slices.Contains(allowed, menu.MenuId) {
copy := *menu
if template != nil {
copy.DataScope = consts.SysRoleMenuDataScopeRole
}
filtered = append(filtered, &copy)
}
}
menus = filtered
for _, childDept := range childDepts {
var children []*entity.SysRole
if err := tx.Ctx(ctx).Model(dao.SysRole.Table()+" r").
Fields("r.*").
Where("r.tenant_id", source.TenantId).
Where("r.dept_id", childDept.DeptId).
Where("r.created_dept", source.DeptId).
Where("r.role_id", childDept.DeptRole).
Where("r.deleted_at IS NULL").
Scan(&children); err != nil {
return err
}
if len(children) > 1 {
return gerror.Newf("multiple inherited roles in department %d for role %d", childDept.DeptId, source.RoleId)
}
if len(children) == 0 {
var legacy *entity.SysRole
if childDept.DeptRole > 0 {
if err := tx.Ctx(ctx).Model(dao.SysRole.Table()).
Where(dao.SysRole.Columns().RoleId, childDept.DeptRole).
Where(dao.SysRole.Columns().DeptId, childDept.DeptId).
Where(dao.SysRole.Columns().TenantId, source.TenantId).
Scan(&legacy); err != nil {
return err
}
}
if legacy != nil && legacy.RoleId > 0 {
if legacy.CreatedDept != source.DeptId || legacy.RoleKey != source.RoleKey {
return gerror.Newf("cannot identify inherited role for department %d", childDept.DeptId)
}
children = []*entity.SysRole{legacy}
} else {
if childDept.DeptRole > 0 {
return gerror.Newf("missing default role for department %d", childDept.DeptId)
}
roleId, err := s.createInheritedRoleTx(ctx, tx, source, childDept.DeptId, menus, actorId)
if err != nil {
return err
}
children = []*entity.SysRole{{RoleId: roleId}}
}
}
for _, child := range children {
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, child.RoleId).Data(do.SysRole{RoleName: strings.TrimSpace(source.RoleName), RoleKey: source.RoleKey, RoleSort: source.RoleSort, DataScope: source.DataScope, MenuCheckStrictly: source.MenuCheckStrictly, DeptCheckStrictly: source.DeptCheckStrictly, Status: source.Status, Remark: source.Remark, UpdatedBy: actorId, UpdatedAt: gtime.Now()}).OmitNil().Update(); err != nil {
return err
}
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, child.RoleId).Delete(); err != nil {
return err
}
for _, menu := range menus {
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(do.SysRoleMenu{TenantId: source.TenantId, RoleId: child.RoleId, MenuId: menu.MenuId, DataScope: menu.DataScope}).OmitNil().Insert(); err != nil {
return err
}
}
}
}
return nil
}
func (s *sAgentRole) createInheritedRoleTx(ctx context.Context, tx gdb.TX, source *entity.SysRole, deptId int64, menus []*entity.SysRoleMenu, actorId int64) (int64, error) {
if actorId == 0 {
actorId = source.UpdatedBy
}
roleId, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Data(do.SysRole{TenantId: source.TenantId, DeptId: deptId, RoleName: strings.TrimSpace(source.RoleName), RoleKey: source.RoleKey, RoleSort: source.RoleSort, DataScope: source.DataScope, MenuCheckStrictly: source.MenuCheckStrictly, DeptCheckStrictly: source.DeptCheckStrictly, IsPass: 0, Status: source.Status, CreatedDept: source.DeptId, CreatedBy: actorId, CreatedAt: gtime.Now(), UpdatedBy: actorId, UpdatedAt: gtime.Now(), Remark: source.Remark}).OmitNil().InsertAndGetId()
if err != nil {
return 0, err
}
for _, menu := range menus {
if _, err = tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(do.SysRoleMenu{TenantId: source.TenantId, RoleId: roleId, MenuId: menu.MenuId, DataScope: menu.DataScope}).OmitNil().Insert(); err != nil {
return 0, err
}
}
_, err = tx.Ctx(ctx).Model(dao.SysRoleDept.Table()).Data(do.SysRoleDept{RoleId: roleId, DeptId: deptId}).OmitNil().Insert()
return roleId, err
}
@@ -1,83 +0,0 @@
package agent
import (
"context"
"encoding/json"
"time"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/cmd/inithttp"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
localqueue "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/queue"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
type agentRoleSyncQueue struct{}
func init() {
inithttp.RegisterHttpInitFunc("agentRoleSyncQueue", func(ctx context.Context) error {
if rabbitmq.Enabled(ctx) {
if err := rabbitmq.DeclareQueue(ctx, consts.QueueAgentRoleSyncDeadLetter); err != nil {
return err
}
return rabbitmq.ConsumeJSON[model.AgentRoleSyncMessage](ctx, consts.QueueAgentRoleSync, "iotlcm-agent-role-sync", handleAgentRoleSyncMessage)
}
if err := localqueue.NewConsumer(gctx.New(), &agentRoleSyncQueue{}, &localqueue.Config{Workers: 2}); err != nil {
return err
}
return localqueue.NewProducer(gctx.New(), localqueue.QueueTypeMemory, consts.QueueAgentRoleSync, &localqueue.Config{})
})
}
func publishAgentRoleSync(ctx context.Context, message *model.AgentRoleSyncMessage) error {
if rabbitmq.Enabled(ctx) {
return rabbitmq.Publish(ctx, consts.QueueAgentRoleSync, message)
}
return localqueue.Push(ctx, consts.QueueAgentRoleSync, message)
}
func handleAgentRoleSyncMessage(ctx context.Context, message model.AgentRoleSyncMessage) error {
if err := service.AgentRole().SyncInheritedRole(ctx, &message); err != nil {
g.Log().Errorf(ctx, "agent role sync failed, sourceRoleId:%d, retry:%d, err:%v", message.SourceRoleId, message.RetryCount, err)
if message.RetryCount < 3 {
message.RetryCount++
return rabbitmq.PublishDelayed(ctx, consts.QueueAgentRoleSyncRetry, consts.QueueAgentRoleSync, time.Duration(message.RetryCount)*time.Second, message)
}
deadLetter := model.AgentRoleSyncDeadLetterMessage{Message: message, Error: err.Error(), FailedAt: time.Now().Format(time.RFC3339)}
if publishErr := rabbitmq.Publish(ctx, consts.QueueAgentRoleSyncDeadLetter, deadLetter); publishErr != nil {
return publishErr
}
g.Log().Errorf(ctx, "agent role sync moved to dead-letter queue, sourceRoleId:%d, err:%v", message.SourceRoleId, err)
return nil
}
g.Log().Infof(ctx, "agent role sync completed, sourceRoleId:%d", message.SourceRoleId)
return nil
}
func (*agentRoleSyncQueue) GetTopic() string { return consts.QueueAgentRoleSync }
func (*agentRoleSyncQueue) GetType() localqueue.QueueType { return localqueue.QueueTypeMemory }
func (*agentRoleSyncQueue) Handle(ctx context.Context, payload localqueue.Payload) error {
var message model.AgentRoleSyncMessage
if err := json.Unmarshal(payload.Data, &message); err != nil {
return err
}
var err error
for attempt := 0; attempt <= 3; attempt++ {
err = service.AgentRole().SyncInheritedRole(ctx, &message)
if err == nil {
g.Log().Infof(ctx, "agent role sync completed, sourceRoleId:%d", message.SourceRoleId)
return nil
}
if attempt < 3 {
time.Sleep(time.Duration(attempt+1) * time.Second)
}
}
g.Log().Errorf(ctx, "agent role sync failed after retries, sourceRoleId:%d, err:%v", message.SourceRoleId, err)
return err
}
@@ -9,9 +9,9 @@ import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/memorydb"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/system"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
@@ -0,0 +1,94 @@
package agent
import (
"context"
"testing"
"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/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
func TestEnsureRoleNotInheritedAllowsSuperAdmin(t *testing.T) {
ctx := contexts.Set(context.Background(), &model.Context{
User: &model.Identity{BaseClaims: model.BaseClaims{
TenantId: "000000",
Roles: []model.Role{{RoleId: consts.SuperAdminRoleId}},
}},
})
if err := NewAgentRole().ensureRoleNotInherited(ctx, 100); err != nil {
t.Fatalf("super admin should edit inherited roles, got error: %v", err)
}
}
func TestCurrentSuperAdminChecksLiveRoleAssignment(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status) VALUES (1,'000000',1,'0')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_user_role (tenant_id,user_id,role_id) VALUES ('000000',99,1)")
isAdmin, err := rolepolicy.CurrentSuperAdmin(ctx, 99)
if err != nil || !isAdmin {
t.Fatalf("expected active administrator: %v %v", isAdmin, err)
}
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_user_role WHERE user_id=99")
isAdmin, err = rolepolicy.CurrentSuperAdmin(ctx, 99)
if err != nil || isAdmin {
t.Fatalf("revoked administrator must lose access: %v %v", isAdmin, err)
}
}
func TestNoActiveRolesCannotReadFilteredData(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 99, TenantId: "000000", DeptId: 2}}})
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status) VALUES (21,'000000',2,'0')")
count, err := g.DB().Model("sys_role").Ctx(ctx).Handler(handler.FilterAuth).Count()
if err != nil || count != 0 {
t.Fatalf("user without roles must not read filtered data: %d %v", count, err)
}
}
func setupAgentRoleSyncTestDB(t *testing.T) {
t.Helper()
if err := gdb.SetConfig(gdb.Config{
"default": gdb.ConfigGroup{
{Type: "sqlite", Link: "sqlite::@file(:memory:)", Debug: false},
},
}); err != nil {
t.Fatalf("configure sqlite failed: %v", err)
}
ctx := context.Background()
t.Cleanup(func() { _ = g.DB().Close(ctx) })
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, parent_id INTEGER, dept_type INTEGER, status TEXT DEFAULT '0', dept_role INTEGER, deleted_at DATETIME)`)
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_config (config_id INTEGER PRIMARY KEY, tenant_id TEXT, config_name TEXT, config_key TEXT, config_value TEXT, config_type TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role (
role_id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT, dept_id INTEGER, role_name TEXT, role_key TEXT, role_sort INTEGER, data_scope TEXT,
menu_check_strictly INTEGER, dept_check_strictly INTEGER, status TEXT, is_pass INTEGER, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, deleted_by INTEGER, deleted_at DATETIME, remark TEXT)`)
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
mustExecAgentRoleSyncTest(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
}
func mustExecAgentRoleSyncTest(t *testing.T, ctx context.Context, sql string) {
t.Helper()
if _, err := g.DB().Exec(ctx, sql); err != nil {
t.Fatalf("execute sql failed: %v", err)
}
}
func assertAgentRoleSyncCount(t *testing.T, ctx context.Context, sql string, expected int) {
t.Helper()
count, err := g.DB().GetValue(ctx, sql)
if err != nil {
t.Fatalf("query count failed: %v", err)
}
if count.Int() != expected {
t.Fatalf("count = %d, want %d, sql=%s", count.Int(), expected, sql)
}
}
@@ -1,113 +0,0 @@
package agent
import (
"context"
"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/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"strconv"
"strings"
)
func syncTemplateRoles(ctx context.Context, template *entity.SysRole, actorID int64) error {
queue := []int64{template.DeptId}
seen := make(map[int64]bool)
failed := 0
var firstErr error
for len(queue) > 0 {
parentID := queue[0]
queue = queue[1:]
if seen[parentID] {
return gerror.New("机构上下级存在循环")
}
seen[parentID] = true
var children []*entity.SysDept
if err := dao.SysDept.Ctx(ctx).Where("tenant_id", template.TenantId).Where("parent_id", parentID).Where("dept_type", 1).Scan(&children); err != nil {
return err
}
for _, child := range children {
if child.DeptRole <= 0 {
g.Log().Warningf(ctx, "agent template sync skipped missing default role, deptId:%d", child.DeptId)
continue
}
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
lock := tx.Ctx(ctx).Model(dao.SysDept.Table()).Where("dept_id", child.DeptId).Where("tenant_id", template.TenantId)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
lock = lock.LockUpdate()
}
var locked *entity.SysDept
if err := lock.Scan(&locked); err != nil {
return err
}
if locked == nil || locked.DeptRole != child.DeptRole {
return gerror.New("下级机构角色绑定已变化")
}
var current *entity.SysRole
if err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", child.DeptRole).Where("tenant_id", template.TenantId).Where("dept_id", child.DeptId).Scan(&current); err != nil {
return err
}
if current == nil {
g.Log().Warningf(ctx, "agent template sync skipped missing or deleted role, deptId:%d, roleId:%d", child.DeptId, child.DeptRole)
return nil
}
configLock := tx.Ctx(ctx).Model(dao.SysConfig.Table()).Where("tenant_id", template.TenantId).Where("config_key", rolepolicy.TemplateConfigKey)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
configLock = configLock.LockUpdate()
}
var config *entity.SysConfig
if err := configLock.Scan(&config); err != nil {
return err
}
if config == nil || strings.TrimSpace(config.ConfigValue) != strconv.FormatInt(template.RoleId, 10) {
return gerror.New("模板已切换,请重新同步")
}
templateLock := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", template.RoleId)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
templateLock = templateLock.LockUpdate()
}
var latest *entity.SysRole
if err := templateLock.Scan(&latest); err != nil {
return err
}
if latest == nil || latest.Status != "0" {
return gerror.New("模板不可用")
}
var menus []*entity.SysRoleMenu
if err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("tenant_id", template.TenantId).Where("role_id", template.RoleId).Scan(&menus); err != nil {
return err
}
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", current.RoleId).Data(do.SysRole{CreatedDept: template.DeptId, DataScope: consts.SysRoleDataScopeDeptAndBelow, UpdatedBy: actorID}).Update(); err != nil {
return err
}
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("role_id", current.RoleId).Delete(); err != nil {
return err
}
for _, menu := range menus {
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(do.SysRoleMenu{TenantId: template.TenantId, RoleId: current.RoleId, MenuId: menu.MenuId, DataScope: consts.SysRoleMenuDataScopeRole}).Insert(); err != nil {
return err
}
}
return nil
})
if err != nil {
failed++
if firstErr == nil {
firstErr = err
}
g.Log().Errorf(ctx, "agent template sync child failed, deptId:%d, err:%v", child.DeptId, err)
continue
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, child.DeptRole)
}
}
if firstErr != nil {
return gerror.Wrapf(firstErr, "代理模板同步失败企业数:%d", failed)
}
return nil
}
@@ -0,0 +1,64 @@
package agent
import (
"context"
"slices"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"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/service"
)
type templateRoleService struct{ service.ISysRole }
func (templateRoleService) ModelQuery(ctx context.Context, _ ...*handler.Option) *gdb.Model {
return g.DB().Model("sys_role").Ctx(ctx).Handler(handler.FilterTenant)
}
func TestConfiguredTemplateUsesInheritedDefaultAtEveryLevel(t *testing.T) {
setupAgentRoleSyncTestDB(t)
service.RegisterSysDept(paginationDeptService{})
service.RegisterSysRole(templateRoleService{})
t.Cleanup(func() {
service.RegisterSysDept(nil)
service.RegisterSysRole(nil)
})
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 99, TenantId: "000000", DeptId: 2}}})
for _, statement := range []string{
"ALTER TABLE sys_dept ADD COLUMN dept_name TEXT",
"INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,21),(3,'000000',2,1,31)",
"INSERT INTO sys_role(role_id,tenant_id,dept_id,status,is_pass,created_dept,data_scope) VALUES(10,'000000',1,'0',0,1,'4'),(21,'000000',2,'0',0,1,'4'),(31,'000000',3,'0',0,2,'4')",
"INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')",
"INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',10,200,'0'),('000000',21,100,'0'),('000000',21,300,'0'),('000000',31,100,'0')",
} {
mustExecAgentRoleSyncTest(t, ctx, statement)
}
for _, deptID := range []int64{1, 2, 3} {
source, menus, err := NewAgentDept().passRoleByDept(ctx, deptID)
if err != nil || source == nil {
t.Fatalf("source for department %d: %+v %v", deptID, source, err)
}
expected := map[int64][]int64{1: {100, 200}, 2: {100, 300}, 3: {100}}[deptID]
got := make([]int64, 0, len(menus))
for _, menu := range menus {
got = append(got, menu.MenuId)
}
slices.Sort(got)
if !slices.Equal(got, expected) {
t.Fatalf("department %d must copy its current default role including platform expansion: %v, want %v", deptID, got, expected)
}
}
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=0 WHERE dept_id=2")
if _, _, err := NewAgentDept().passRoleByDept(ctx, 2); err == nil {
t.Fatal("missing default role must not fall back to full platform permissions")
}
if err := NewAgentDept().EnsureCurrentDeptDefaultRole(ctx); err == nil {
t.Fatal("read request must report missing default role")
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role", 3)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role", 0)
}
@@ -1,7 +1,7 @@
// Package iot
// @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 agent
+27 -29
View File
@@ -1,29 +1,26 @@
// Package iot
// @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 agent
import (
"context"
"fmt"
"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/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"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"
)
type sAgentTApi struct{}
@@ -86,31 +83,32 @@ func (s *sAgentTApi) List(ctx context.Context, in *model.TApiListParam) (list []
return
}
// Export 导出API集成
// Export 导出API集成(异步:提交任务后到下载中心取文件)
func (s *sAgentTApi) Export(ctx context.Context, in *model.TApiListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TApiExportModel{})
if err != nil {
return
}
var (
fileName = "导出API集成-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TApiExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "tApi",
BizName: "API集成",
PermCode: "cpm:agent:tApi:export",
QueryParam: in,
Tags: func() ([]string, error) {
return convert.GetEntityDescTags(model.TApiListModel{})
},
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, count, err := s.List(ctx, &query)
if err != nil {
return nil, 0, err
}
rows = make([]interface{}, 0, len(list))
for _, item := range list {
rows = append(rows, item)
}
return rows, count, nil
},
})
return err
}
// Edit 修改/新增API集成
+26 -27
View File
@@ -2,21 +2,19 @@ package agent
import (
"context"
"fmt"
"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/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"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"
)
@@ -71,31 +69,32 @@ func (s *sAgentTApiGroup) List(ctx context.Context, in *model.TApiGroupListParam
return
}
// Export 导出开放接口权限组列表。
// Export 导出开放接口权限组(异步:提交任务后到下载中心取文件)
func (s *sAgentTApiGroup) Export(ctx context.Context, in *model.TApiGroupListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(model.TApiGroupExportModel{})
if err != nil {
return
}
fileName := "导出开放接口权限组-" + gctx.CtxId(ctx)
sheetName := fmt.Sprintf(
"搜索条件共%v条 共%v页 当前导出第%v页 本页共%v条",
totalCount,
(totalCount+in.PageSize-1)/in.PageSize,
in.Page,
len(list),
)
var exports []model.TApiGroupExportModel
if err = gconv.Scan(list, &exports); err != nil {
return
}
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "tApiGroup",
BizName: "开放接口权限组",
PermCode: "cpm:agent:tApiGroup:export",
QueryParam: in,
Tags: func() ([]string, error) {
return convert.GetEntityDescTags(model.TApiGroupListModel{})
},
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, count, err := s.List(ctx, &query)
if err != nil {
return nil, 0, err
}
rows = make([]interface{}, 0, len(list))
for _, item := range list {
rows = append(rows, item)
}
return rows, count, nil
},
})
return err
}
// Edit 新增或修改开放接口权限组,并同步维护权限组与接口资源的关联。
+26 -26
View File
@@ -2,22 +2,20 @@ package agent
import (
"context"
"fmt"
"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/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"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/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
@@ -120,30 +118,32 @@ func (s *sAgentTApiResource) List(ctx context.Context, in *model.TApiResourceLis
return
}
// Export 导出开放接口资源列表。
// Export 导出开放接口资源(异步:提交任务后到下载中心取文件)
func (s *sAgentTApiResource) Export(ctx context.Context, in *model.TApiResourceListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(model.TApiResourceExportModel{})
if err != nil {
return
}
fileName := "export-open-api-resource-" + gctx.CtxId(ctx)
totalPage := 0
if in.PageSize > 0 {
totalPage = (totalCount + in.PageSize - 1) / in.PageSize
}
sheetName := fmt.Sprintf("total %v pages %v current %v count %v", totalCount, totalPage, in.Page, len(list))
var exports []*model.TApiResourceExportModel
if err = gconv.Scan(list, &exports); err != nil {
return
}
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
_, err = exportcenter.Submit(ctx, &exportcenter.Request{
BizKey: "tApiResource",
BizName: "开放接口资源",
PermCode: "cpm:agent:tApiResource:export",
QueryParam: in,
Tags: func() ([]string, error) {
return convert.GetEntityDescTags(model.TApiResourceListModel{})
},
Fetch: func(ctx context.Context, page, pageSize int) (rows []interface{}, total int, err error) {
query := *in
query.Page = page
query.PageSize = pageSize
list, count, err := s.List(ctx, &query)
if err != nil {
return nil, 0, err
}
rows = make([]interface{}, 0, len(list))
for _, item := range list {
rows = append(rows, item)
}
return rows, count, nil
},
})
return err
}
// Edit 新增或修改开放接口资源。
@@ -1,7 +1,7 @@
// Package iot
// @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 agent