sync: 同步 v1.0.4 开源代码并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:52:43 +08:00
parent 32e4316ee4
commit 6893e21f07
136 changed files with 9851 additions and 436 deletions
+208
View File
@@ -0,0 +1,208 @@
package rolesync
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"slices"
"strings"
)
func SyncInheritedRole(ctx context.Context, message *model.AgentRoleSyncMessage) error {
if message == nil || message.SourceRoleId <= 0 || message.SourceDeptId <= 0 || message.TenantId == "" {
return gerror.New("invalid agent role sync message")
}
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return templateErr
}
if message.TemplateRole && (template == nil || message.SourceRoleId != template.RoleId) {
message.Superseded = true
return nil
}
if template != nil && message.SourceRoleId == template.RoleId && message.SourceDeptId == template.DeptId && (message.TenantId == template.TenantId || message.TenantId == "") {
return syncTemplateRoles(ctx, template, message)
}
if template != nil {
message.Superseded = true
return nil
}
var source *entity.SysRole
sourceModel := dao.SysRole.Ctx(ctx).Master().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 {
message.Superseded = true
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).Master().
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
if len(message.ChildDeptIds) == 0 {
message.TotalCount = len(children)
message.SuccessCount = 0
}
failedDeptIds := make([]int64, 0)
var firstErr error
for _, child := range children {
if err := ctx.Err(); err != nil {
return err
}
if len(message.ChildDeptIds) > 0 && !slices.Contains(message.ChildDeptIds, child.DeptId) {
continue
}
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return syncInheritedRoleChildrenTx(ctx, tx, message.SourceRoleId, source.TenantId, source.DeptId, message.ActorId, child.DeptId)
})
if err != nil {
failed++
failedDeptIds = append(failedDeptIds, child.DeptId)
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)
} else {
message.SuccessCount++
}
}
message.ChildDeptIds = failedDeptIds
message.FailedCount = len(failedDeptIds)
if firstErr != nil {
return gerror.Wrapf(firstErr, "agent role sync failed for %d of %d children", failed, len(children))
}
return nil
}
func 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).
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)
}
return gerror.Newf("missing default role for department %d", childDept.DeptId)
}
}
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}).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
}
menuData := make([]*do.SysRoleMenu, 0, len(menus))
for _, menu := range menus {
menuData = append(menuData, &do.SysRoleMenu{TenantId: source.TenantId, RoleId: child.RoleId, MenuId: menu.MenuId, DataScope: menu.DataScope})
}
if len(menuData) > 0 {
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(menuData).OmitNil().Batch(200).Insert(); err != nil {
return err
}
}
}
}
return nil
}
+283
View File
@@ -0,0 +1,283 @@
package rolesync
import (
"context"
"fmt"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"reflect"
"testing"
)
func TestTemplateSwitchOverwritesDefaultRolesAndPreservesOtherRoles(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, "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),(4,'000000',0,1,41)")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,status,data_scope,is_pass,created_dept) VALUES (10,'000000',1,'模板','template','0','4',0,1),(11,'000000',1,'新模板','next','0','4',0,1),(21,'000000',2,'管理员','admin','0','4',0,1),(31,'000000',3,'三级管理员','admin','0','4',0,2),(22,'000000',2,'财务','finance','0','3',0,2),(41,'000000',4,'其他机构','other','0','4',0,4)")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES ('000000','agent.role.template.role_id','10')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'0'),('000000',10,200,'0'),('000000',11,200,'0'),('000000',22,100,'3'),('000000',41,100,'0')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_user_role(user_id,role_id) VALUES(99,21)")
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
for attempt := 0; attempt < 2; attempt++ {
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatal(err)
}
}
if message.TotalCount != 2 || message.SuccessCount != 2 {
t.Fatalf("unexpected hierarchy counts: %+v", message)
}
for _, roleID := range []int64{21, 31} {
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id IN(100,200)", roleID), 2)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=22 AND menu_id=200", 0)
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_config SET config_value='11'")
ids, err := rolepolicy.RestrictMenus(ctx, "000000", 3, []int64{100, 200})
if err != nil || !reflect.DeepEqual(ids, []int64{100, 200}) {
t.Fatalf("template switch must wait for confirmed distribution: %v %v", ids, err)
}
if err := SyncInheritedRole(ctx, message); err != nil || !message.Superseded {
t.Fatalf("obsolete template must not propagate: %+v %v", message, err)
}
next := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 11, SourceDeptId: 1, TemplateRole: true}
if err := SyncInheritedRole(ctx, next); err != nil {
t.Fatal(err)
}
for _, roleID := range []int64{21, 31} {
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id=100", roleID), 0)
assertAgentRoleSyncCount(t, ctx, fmt.Sprintf("SELECT COUNT(*) FROM sys_role_menu WHERE role_id=%d AND menu_id=200", roleID), 1)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id=31 AND created_dept=2", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=22 AND menu_id=100", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=41 AND menu_id=100", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role WHERE user_id=99 AND role_id=21", 1)
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role(role_id,tenant_id,dept_id,is_pass,status,data_scope) VALUES(25,'000000',2,1,'0','4')")
legacy := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 25, SourceDeptId: 2}
if err := SyncInheritedRole(ctx, legacy); err != nil || !legacy.Superseded {
t.Fatalf("legacy local templates must not overwrite configured hierarchy: %+v %v", legacy, err)
}
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_config SET config_value='invalid'")
if _, err := rolepolicy.RestrictMenus(ctx, "000000", 3, []int64{200}); err == nil {
t.Fatal("invalid template must deny")
}
}
func TestSyncInheritedRoleOnlyUpdatesDirectEnterpriseChildren(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `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, 0),
(4, '000000', 1, 0, 0)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role
(role_id, tenant_id, dept_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, is_pass, created_dept, created_by, updated_by, remark)
VALUES
(10, '000000', 1, '企业客户', 'enterpriseCustomer', 1, '4', 1, 1, '0', 1, 1, 1, 1, 'source'),
(20, '000000', 2, '下级自建角色', 'localRole', 5, '3', 1, 1, '0', 0, 2, 2, 2, 'local'),
(21, '000000', 2, '旧下发角色', 'enterpriseCustomer', 1, '4', 1, 1, '0', 0, 1, 1, 1, 'legacy')`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000', 10, 100, '7')`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000', 21, 200, '7')`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_user_role (user_id, role_id) VALUES (99, 21)`)
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, ActorId: 9}
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatalf("first sync failed: %v", err)
}
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatalf("second sync failed: %v", err)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 2 AND created_dept = 1", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 21 AND created_dept = 1", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role WHERE user_id = 99 AND role_id = 21", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id = 21 AND menu_id = 200", 0)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 3 AND created_dept = 1", 0)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 4 AND created_dept = 1", 0)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 20 AND role_name = '下级自建角色' AND created_dept = 2", 1)
menuScope, err := g.DB().GetValue(ctx, `SELECT rm.data_scope FROM sys_role_menu rm JOIN sys_role r ON r.role_id = rm.role_id WHERE r.dept_id = 2 AND r.created_dept = 1 AND rm.menu_id = 100`)
if err != nil {
t.Fatalf("query inherited menu data scope failed: %v", err)
}
if menuScope.String() != "7" {
t.Fatalf("inherited menu data scope = %q, want 7", menuScope.String())
}
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET role_name = '停止下发后的名称', is_pass = 0 WHERE role_id = 10`)
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatalf("disabled source sync failed: %v", err)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE dept_id = 2 AND created_dept = 1 AND role_name = '企业客户'", 1)
}
func TestSyncInheritedRoleRejectsUnrelatedDefaultRole(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `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)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, is_pass, created_dept) VALUES (10, '000000', 1, '下发角色', 'source', 1, 1), (21, '000000', 2, '自建默认角色', 'local', 0, 2)`)
err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1})
if err == nil {
t.Fatal("expected ambiguous default role to be rejected")
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role WHERE role_id = 21 AND created_dept = 2 AND role_name = '自建默认角色'", 1)
}
func TestTemplateSyncReportsMissingRoles(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,10),(2,'000000',1,1,0),(3,'000000',1,1,31)")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status,data_scope,is_pass) VALUES (10,'000000',1,'0','4',1),(31,'000000',3,'0','4',0)")
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=31")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config (config_id,tenant_id,config_key,config_value) VALUES (1,'000000','agent.role.template.role_id','10')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,200,'0')")
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
t.Fatal("missing default role must report partial failure")
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=200", 1)
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,status,data_scope) VALUES (21,'000000',2,'0','4')")
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=21")
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=21 WHERE dept_id=2")
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err != nil {
t.Fatal(err)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=200", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200", 1)
}
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)
}
}
func TestRoleSyncRetriesOnlyFailedDepartments(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `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',1,1,31)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role (role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept) VALUES (10,'000000',1,'source','source',1,1),(21,'000000',2,'before','source',0,1),(31,'000000',3,'local','local',0,3)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'7')`)
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}
if err := SyncInheritedRole(ctx, message); err == nil {
t.Fatal("expected invalid department failure")
}
if !reflect.DeepEqual(message.ChildDeptIds, []int64{3}) {
t.Fatalf("unexpected failures: %v", message.ChildDeptIds)
}
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET role_name='already-successful' WHERE role_id=21`)
mustExecAgentRoleSyncTest(t, ctx, `UPDATE sys_role SET created_dept=1,role_key='source' WHERE role_id=31`)
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatal(err)
}
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role WHERE role_id=21 AND role_name='already-successful'`, 1)
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=100`, 1)
if len(message.ChildDeptIds) != 0 {
t.Fatal("failure list must clear after success")
}
}
func TestObsoleteTemplateTaskDoesNotPropagateAsNormalRole(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept (dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES (1,'000000',0,1,10),(2,'000000',1,1,21)")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role (role_id,tenant_id,dept_id,role_key,is_pass,created_dept) VALUES (10,'000000',1,'source',1,1),(21,'000000',2,'source',0,1)")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu (tenant_id,role_id,menu_id,data_scope) VALUES ('000000',10,100,'7'),('000000',21,200,'7')")
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatal(err)
}
if !message.Superseded {
t.Fatal("obsolete template task must be marked superseded")
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200", 1)
}
func TestMissingDefaultRoleDoesNotCreateOrphanRole(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,0)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept) VALUES(10,'000000',1,'source','source',1,1)`)
for attempt := 0; attempt < 2; attempt++ {
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
t.Fatal("missing role must fail")
}
}
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role WHERE dept_id=2`, 0)
}
func TestTemplateChildRevalidatesCurrentBindingAndSource(t *testing.T) {
for _, change := range []string{
"UPDATE sys_dept SET parent_id=99 WHERE dept_id=2",
"UPDATE sys_dept SET dept_type=0 WHERE dept_id=2",
"UPDATE sys_role SET status='1' WHERE role_id=10",
"UPDATE sys_role_menu SET data_scope='1' WHERE role_id=10",
} {
t.Run(change, func(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `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)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,status,data_scope,is_pass) VALUES(10,'000000',1,'0','4',1),(21,'000000',2,'0','3',0)`)
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_role SET created_dept=1 WHERE role_id=21")
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,200,'0')`)
mustExecAgentRoleSyncTest(t, ctx, change)
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return syncTemplateChildTx(ctx, tx, &entity.SysRole{RoleId: 10, TenantId: "000000", DeptId: 1}, &entity.SysDept{DeptId: 2, DeptRole: 21, ParentId: 1}, 7)
})
if err == nil {
t.Fatal("changed source or binding must fail")
}
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200`, 1)
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=100`, 0)
})
}
}
func TestDeletedDefaultRoleIsNotRestoredBySynchronization(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, `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)`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role(role_id,tenant_id,dept_id,role_name,role_key,is_pass,created_dept,deleted_at) VALUES(10,'000000',1,'source','source',1,1,NULL),(21,'000000',2,'deleted','source',0,1,'2026-10-13 00:00:00')`)
mustExecAgentRoleSyncTest(t, ctx, `INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,200,'0')`)
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
t.Fatal("deleted default role must fail")
}
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=200`, 1)
assertAgentRoleSyncCount(t, ctx, `SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=100`, 0)
}
+109
View File
@@ -0,0 +1,109 @@
package rolesync
import (
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"strings"
"time"
)
func Start(ctx context.Context) (func(), error) {
if !g.Cfg().MustGet(ctx, "roleSync.enabled", false).Bool() {
return func() {}, nil
}
if !rabbitmq.Enabled(ctx) {
return nil, gerror.New("权限同步 Worker 必须启用 RabbitMQ 并配置连接")
}
workerCtx, cancel := context.WithCancel(ctx)
go superviseConsumer(workerCtx)
g.Log().Info(ctx, "role sync Worker started")
return cancel, nil
}
func handleMessage(ctx context.Context, message model.AgentRoleSyncMessage) error {
if err := syncRoleMessage(ctx, &message); err != nil {
g.Log().Errorf(ctx, "role sync failed, taskId:%s, roleId:%d, attempt:%d, err:%+v", message.TaskId, message.SourceRoleId, message.RetryCount, err)
message.RetryCount++
body, marshalErr := json.Marshal(message)
if marshalErr != nil {
return marshalErr
}
if message.RetryCount > 5 {
deadLetter, marshalErr := json.Marshal(model.AgentRoleSyncDeadLetterMessage{
Message: message, Error: err.Error(), FailedAt: time.Now().Format(time.RFC3339),
})
if marshalErr != nil {
return marshalErr
}
return publishRoleDeadLetter(ctx, deadLetter)
}
delay := time.Duration(1<<uint(message.RetryCount-1)) * 5 * time.Second
if publishErr := publishRoleRetry(ctx, delay, body); publishErr != nil {
return gerror.Wrap(publishErr, fmt.Sprintf("role sync retry publish failed: %v", err))
}
return nil
}
return nil
}
var consumeRoleSync = rabbitmq.ConsumeWithDone
var syncRoleMessage = SyncInheritedRole
var publishRoleRetry = func(ctx context.Context, delay time.Duration, body []byte) error {
return rabbitmq.PublishDelayedBytesWithHeaders(ctx, consts.QueueAgentRoleSync+".retry", consts.QueueAgentRoleSync, delay, body, nil)
}
var publishRoleDeadLetter = func(ctx context.Context, body []byte) error {
return rabbitmq.PublishBytesWithHeaders(ctx, consts.QueueAgentRoleSync+".dlq", body, nil)
}
func superviseConsumer(ctx context.Context) {
for ctx.Err() == nil {
done, err := consumeRoleSync(ctx, consts.QueueAgentRoleSync, "iotlcm-worker-agent-role-sync", func(ctx context.Context, body []byte) error {
var message model.AgentRoleSyncMessage
invalid := json.Unmarshal(body, &message)
if invalid != nil || message.SourceRoleId <= 0 || message.SourceDeptId <= 0 || strings.TrimSpace(message.TenantId) == "" || message.RetryCount < 0 {
return quarantineRoleSync(ctx, body)
}
if err := handleMessage(ctx, message); err != nil {
select {
case <-ctx.Done():
case <-time.After(time.Second):
}
return err
}
return nil
})
if err == nil {
select {
case <-ctx.Done():
return
case <-done:
}
}
if ctx.Err() != nil {
return
}
g.Log().Warningf(ctx, "role sync consumer disconnected, restarting: %v", err)
select {
case <-ctx.Done():
return
case <-time.After(3 * time.Second):
}
}
}
var quarantineRoleSync = func(ctx context.Context, body []byte) error {
if err := publishRoleDeadLetter(ctx, body); err != nil {
return gerror.Wrap(err, "保存无效权限消息到死信队列失败")
}
g.Log().Warning(ctx, "无效权限消息已隔离到死信队列")
return nil
}
+161
View File
@@ -0,0 +1,161 @@
package rolesync
import (
"context"
"encoding/json"
"errors"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"testing"
"time"
)
func TestMessageRetryAndDeadLetter(t *testing.T) {
originalSync, originalRetry, originalDead := syncRoleMessage, publishRoleRetry, publishRoleDeadLetter
t.Cleanup(func() {
syncRoleMessage, publishRoleRetry, publishRoleDeadLetter = originalSync, originalRetry, originalDead
})
syncRoleMessage = func(_ context.Context, message *model.AgentRoleSyncMessage) error {
message.ChildDeptIds = []int64{200}
return errors.New("child failed")
}
for attempt := 0; attempt <= 5; attempt++ {
retries, deadLetters := 0, 0
publishRoleRetry = func(_ context.Context, delay time.Duration, body []byte) error {
retries++
var message model.AgentRoleSyncMessage
if err := json.Unmarshal(body, &message); err != nil {
t.Fatal(err)
}
if message.RetryCount != attempt+1 || len(message.ChildDeptIds) != 1 || message.ChildDeptIds[0] != 200 || delay != time.Duration(1<<uint(attempt))*5*time.Second {
t.Fatalf("invalid retry: %+v, delay=%s", message, delay)
}
return nil
}
publishRoleDeadLetter = func(_ context.Context, body []byte) error {
deadLetters++
var dead model.AgentRoleSyncDeadLetterMessage
if err := json.Unmarshal(body, &dead); err != nil {
t.Fatal(err)
}
if dead.Error != "child failed" || dead.FailedAt == "" || dead.Message.RetryCount != 6 {
t.Fatalf("invalid dead letter: %+v", dead)
}
return nil
}
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{RetryCount: attempt}); err != nil {
t.Fatal(err)
}
if (attempt < 5 && (retries != 1 || deadLetters != 0)) || (attempt == 5 && (retries != 0 || deadLetters != 1)) {
t.Fatalf("unexpected destinations: retry=%d, dead=%d", retries, deadLetters)
}
}
brokerError := errors.New("broker offline")
publishRoleRetry = func(context.Context, time.Duration, []byte) error { return brokerError }
publishRoleDeadLetter = func(context.Context, []byte) error { return brokerError }
for _, attempt := range []int{0, 5} {
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{RetryCount: attempt}); err == nil {
t.Fatal("publication failure must prevent acknowledgement")
}
}
syncRoleMessage = func(context.Context, *model.AgentRoleSyncMessage) error { return nil }
if err := handleMessage(context.Background(), model.AgentRoleSyncMessage{}); err != nil {
t.Fatalf("successful sync should not publish retries: %v", err)
}
}
func TestRuntimeConfiguration(t *testing.T) {
original := g.Cfg().GetAdapter()
t.Cleanup(func() { g.Cfg().SetAdapter(original) })
for _, test := range []struct {
content string
wantError bool
}{
{"roleSync:\n enabled: false\n", false},
{"roleSync:\n enabled: true\nrabbitmq:\n enabled: false\n", true},
} {
adapter, err := gcfg.NewAdapterContent(test.content)
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
stop, err := Start(context.Background())
if (err != nil) != test.wantError {
t.Fatalf("unexpected initialization: %v", err)
}
if stop != nil {
stop()
}
}
}
func TestConsumerReconnectsAfterConnectionFailureAndChannelClose(t *testing.T) {
original := consumeRoleSync
t.Cleanup(func() { consumeRoleSync = original })
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reconnected := make(chan struct{})
calls := 0
consumeRoleSync = func(context.Context, string, string, rabbitmq.Handler) (<-chan struct{}, error) {
calls++
if calls == 1 {
return nil, errors.New("broker offline")
}
if calls == 2 {
closed := make(chan struct{})
close(closed)
return closed, nil
}
close(reconnected)
return make(chan struct{}), nil
}
finished := make(chan struct{})
go func() { defer close(finished); superviseConsumer(ctx) }()
select {
case <-reconnected:
case <-time.After(10 * time.Second):
cancel()
<-finished
t.Fatal("consumer did not reconnect")
}
cancel()
<-finished
if calls != 3 {
t.Fatalf("unexpected restarts: %d", calls)
}
}
func TestInvalidMessagesAreQuarantinedAndPublicationFailureIsRetried(t *testing.T) {
originalConsume, originalQuarantine := consumeRoleSync, quarantineRoleSync
t.Cleanup(func() { consumeRoleSync = originalConsume; quarantineRoleSync = originalQuarantine })
for _, body := range []string{`broken json`, `{"sourceRoleId":18}`} {
for _, publishFails := range []bool{false, true} {
ctx, cancel := context.WithCancel(context.Background())
calls := 0
quarantineRoleSync = func(context.Context, []byte) error {
calls++
if publishFails {
return errors.New("broker offline")
}
return nil
}
consumeRoleSync = func(ctx context.Context, queue, consumer string, handler rabbitmq.Handler) (<-chan struct{}, error) {
cancel()
err := handler(ctx, []byte(body))
if (err != nil) != publishFails {
t.Fatalf("unexpected quarantine result: %v", err)
}
done := make(chan struct{})
close(done)
return done, nil
}
superviseConsumer(ctx)
cancel()
if calls != 1 {
t.Fatalf("invalid message not isolated: %d", calls)
}
}
}
}
+185
View File
@@ -0,0 +1,185 @@
package rolesync
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"slices"
"strconv"
"strings"
)
func syncTemplateRoles(ctx context.Context, template *entity.SysRole, message *model.AgentRoleSyncMessage) error {
departments, err := templateDescendants(ctx, template)
if err != nil {
return err
}
retrying := len(message.ChildDeptIds) > 0
if !retrying {
message.TotalCount = len(departments)
message.SuccessCount = 0
}
pending := make(map[int64]bool, len(message.ChildDeptIds))
for _, deptID := range message.ChildDeptIds {
pending[deptID] = true
}
failed := make(map[int64]bool)
failedDeptIDs := make([]int64, 0)
var firstErr error
for _, child := range departments {
if err := ctx.Err(); err != nil {
return err
}
if retrying && !pending[child.DeptId] {
continue
}
delete(pending, child.DeptId)
var syncErr error
if failed[child.ParentId] {
syncErr = gerror.Newf("上级机构%d尚未同步成功", child.ParentId)
} else {
syncErr = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
return syncTemplateChildTx(ctx, tx, template, child, message.ActorId)
})
}
if syncErr != nil {
failed[child.DeptId] = true
failedDeptIDs = append(failedDeptIDs, child.DeptId)
if firstErr == nil {
firstErr = syncErr
}
g.Log().Errorf(ctx, "agent template sync child failed, deptId:%d, err:%v", child.DeptId, syncErr)
continue
}
message.SuccessCount++
}
for deptID := range pending {
failedDeptIDs = append(failedDeptIDs, deptID)
if firstErr == nil {
firstErr = gerror.Newf("待同步机构%d已不在当前模板的下级范围,请核对机构关系", deptID)
}
}
slices.Sort(failedDeptIDs)
message.ChildDeptIds = failedDeptIDs
message.FailedCount = len(failedDeptIDs)
if firstErr != nil {
return gerror.Wrapf(firstErr, "代理模板同步失败企业数:%d", len(failedDeptIDs))
}
return nil
}
func templateDescendants(ctx context.Context, template *entity.SysRole) ([]*entity.SysDept, error) {
queue := []int64{template.DeptId}
seen := map[int64]bool{template.DeptId: true}
departments := make([]*entity.SysDept, 0)
for len(queue) > 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
parentID := queue[0]
queue = queue[1:]
var children []*entity.SysDept
if err := dao.SysDept.Ctx(ctx).Master().Where("tenant_id", template.TenantId).Where("parent_id", parentID).Where("dept_type", 1).OrderAsc("dept_id").Scan(&children); err != nil {
return nil, err
}
for _, child := range children {
if seen[child.DeptId] {
return nil, gerror.New("机构上下级存在循环")
}
seen[child.DeptId] = true
queue = append(queue, child.DeptId)
departments = append(departments, child)
}
}
return departments, nil
}
func syncTemplateChildTx(ctx context.Context, tx gdb.TX, template *entity.SysRole, child *entity.SysDept, actorID int64) error {
if child.DeptRole <= 0 {
return gerror.New("下级机构缺失默认角色")
}
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 || locked.ParentId != child.ParentId || locked.DeptType != 1 {
return gerror.New("下级机构角色绑定已变化")
}
currentLock := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", child.DeptRole).Where("tenant_id", template.TenantId).Where("dept_id", child.DeptId)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
currentLock = currentLock.LockUpdate()
}
var current *entity.SysRole
if err := currentLock.Scan(&current); err != nil {
return err
}
if current == nil || current.Status != "0" {
return gerror.New("下级机构默认角色不存在或已删除")
}
if current.CreatedDept != child.ParentId {
return gerror.New("下级机构默认角色非直属上级继承角色,请核对角色来源")
}
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" || latest.TenantId != template.TenantId || latest.DeptId != template.DeptId || latest.DataScope != consts.SysRoleDataScopeDeptAndBelow || (latest.CreatedDept > 0 && latest.CreatedDept != latest.DeptId) {
return gerror.New("模板不可用")
}
menuLock := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("tenant_id", template.TenantId).Where("role_id", template.RoleId)
if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") {
menuLock = menuLock.LockUpdate()
}
var menus []*entity.SysRoleMenu
if err := menuLock.Scan(&menus); err != nil {
return err
}
// Every linked default role receives the same template snapshot. Individual
// changes are intentionally replaced; unrelated roles are never updated.
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("tenant_id", template.TenantId).Where("role_id", current.RoleId).Data(do.SysRole{DataScope: consts.SysRoleDataScopeDeptAndBelow, UpdatedBy: actorID}).Update(); err != nil {
return err
}
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where("tenant_id", template.TenantId).Where("role_id", current.RoleId).Delete(); err != nil {
return err
}
menuData := make([]*do.SysRoleMenu, 0, len(menus))
for _, menu := range menus {
if menu.DataScope == consts.SysRoleDataScopeAll || menu.DataScope == consts.SysRoleDataScopeCustom {
return gerror.New("模板按钮数据范围已变化,请重新同步")
}
menuData = append(menuData, &do.SysRoleMenu{TenantId: template.TenantId, RoleId: current.RoleId, MenuId: menu.MenuId, DataScope: consts.SysRoleMenuDataScopeRole})
}
if len(menuData) > 0 {
if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(menuData).Batch(200).Insert(); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,97 @@
package rolesync
import (
"context"
"reflect"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
)
func TestTemplateHierarchyRetriesFailedParentAndDescendants(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
for _, statement := range []string{
"INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',0,1,0),(2,'000000',1,1,0),(3,'000000',2,1,31),(4,'000000',1,1,41),(5,'other',1,1,51),(6,'000000',1,0,61)",
"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'),(41,'000000',4,'0',0,1,'4'),(51,'other',5,'0',0,1,'4'),(61,'000000',6,'0',0,1,'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',31,999,'0')",
} {
mustExecAgentRoleSyncTest(t, ctx, statement)
}
message := &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}
if err := SyncInheritedRole(ctx, message); err == nil {
t.Fatal("missing parent default role must fail")
}
if !reflect.DeepEqual(message.ChildDeptIds, []int64{2, 3}) || message.TotalCount != 3 || message.SuccessCount != 1 || message.FailedCount != 2 {
t.Fatalf("unexpected failed hierarchy: %+v", message)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=999", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(51,61)", 0)
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET dept_role=21 WHERE dept_id=2")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',41,777,'0')")
if err := SyncInheritedRole(ctx, message); err != nil {
t.Fatal(err)
}
if message.SuccessCount != 3 || message.FailedCount != 0 || len(message.ChildDeptIds) != 0 {
t.Fatalf("retry counts: %+v", message)
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=100", 1)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=31 AND menu_id=999", 0)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=41 AND menu_id=777", 1)
message.ChildDeptIds = []int64{3}
mustExecAgentRoleSyncTest(t, ctx, "UPDATE sys_dept SET parent_id=99 WHERE dept_id=3")
if err := SyncInheritedRole(ctx, message); err == nil || !reflect.DeepEqual(message.ChildDeptIds, []int64{3}) {
t.Fatalf("removed retry target must not be reported as successful: %+v %v", message, err)
}
}
func TestTemplateHierarchyRejectsCycleBeforeWriting(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_dept(dept_id,tenant_id,parent_id,dept_type,dept_role) VALUES(1,'000000',2,1,0),(2,'000000',1,1,21)")
mustExecAgentRoleSyncTest(t, ctx, "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')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu(tenant_id,role_id,menu_id,data_scope) VALUES('000000',10,100,'0'),('000000',21,999,'0')")
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1}); err == nil {
t.Fatal("cycle must fail before updating roles")
}
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id=21 AND menu_id=999", 1)
}
func TestTemplateOverwriteResetsIndividualDefaultsOnly(t *testing.T) {
setupAgentRoleSyncTestDB(t)
ctx := context.Background()
for _, statement := range []string{
"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'),(22,'000000',2,'0',0,2,'3'),(32,'000000',3,'0',0,3,'5')",
"INSERT INTO sys_config(tenant_id,config_key,config_value) VALUES('000000','agent.role.template.role_id','10')",
"INSERT INTO sys_role_menu VALUES('000000',10,100,'0'),('000000',10,200,'0'),('000000',22,999,'3'),('000000',32,888,'5')",
"INSERT INTO sys_user_role(user_id,role_id) VALUES(99,21),(100,31),(101,22)",
} {
mustExecAgentRoleSyncTest(t, ctx, statement)
}
sync := func() {
t.Helper()
if err := SyncInheritedRole(ctx, &model.AgentRoleSyncMessage{TenantId: "000000", SourceRoleId: 10, SourceDeptId: 1, TemplateRole: true}); err != nil {
t.Fatal(err)
}
}
sync()
// Platform expands one role, removes a default permission from another, and
// gives unrelated staff roles independent permissions.
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',21,999,'0'),('000000',31,888,'0')")
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_role_menu WHERE role_id=21 AND menu_id=200")
mustExecAgentRoleSyncTest(t, ctx, "DELETE FROM sys_role_menu WHERE role_id=10 AND menu_id=100")
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',10,300,'0')")
sync()
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31)", 4)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31) AND menu_id IN(200,300)", 4)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31) AND menu_id IN(100,888,999)", 0)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE (role_id=22 AND menu_id=999 AND data_scope='3') OR (role_id=32 AND menu_id=888 AND data_scope='5')", 2)
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_user_role", 3)
// A repeated distribution must also replace individual default-role edits.
mustExecAgentRoleSyncTest(t, ctx, "INSERT INTO sys_role_menu VALUES('000000',31,999,'0')")
sync()
assertAgentRoleSyncCount(t, ctx, "SELECT COUNT(*) FROM sys_role_menu WHERE role_id IN(21,31)", 4)
}