chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)

This commit is contained in:
MiFaZhan
2026-11-02 13:27:59 +08:00
1250 changed files with 154111 additions and 0 deletions
@@ -0,0 +1,145 @@
package agent
import (
"context"
"slices"
"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/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
)
type agentBase struct{}
func newAgentBase() *agentBase {
return &agentBase{}
}
func (s *agentBase) deptModel(ctx context.Context) *gdb.Model {
return service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
}
func (s *agentBase) roleModel(ctx context.Context) *gdb.Model {
return service.SysRole().ModelQuery(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
}
func (s *agentBase) userModel(ctx context.Context) *gdb.Model {
return service.SysUser().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false})
}
// scopeDeptIds 获取当前登录组织本级及下一级组织的部门ID列表
func (s *agentBase) scopeDeptIds(ctx context.Context) (deptIds []int64, err error) {
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return nil, gerror.New("当前用户未绑定组织")
}
deptIds = []int64{currentDeptId}
var children []*entity.SysDept
err = service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
Where(dao.SysDept.Columns().ParentId, currentDeptId).
Order(dao.SysDept.Columns().OrderNum, "ASC").
Scan(&children)
if err != nil {
return nil, err
}
for _, dept := range children {
deptIds = append(deptIds, dept.DeptId)
}
slices.Sort(deptIds)
return slices.Compact(deptIds), nil
}
// ensureDeptInScope 确保部门在当前机构的权限范围内
func (s *agentBase) ensureDeptInScope(ctx context.Context, deptId int64) error {
deptIds, err := s.scopeDeptIds(ctx)
if err != nil {
return err
}
if !slices.Contains(deptIds, deptId) {
return gerror.New("只能操作本级及下一级组织")
}
return nil
}
// ensureRoleInScope 确保角色在当前机构的权限范围内
func (s *agentBase) ensureRoleInScope(ctx context.Context, roleId int64) error {
if roleId == 0 {
return nil
}
deptIds, err := s.scopeDeptIds(ctx)
if err != nil {
return err
}
count, err := s.roleModel(ctx).
Where(dao.SysRole.Columns().RoleId, roleId).
WhereIn(dao.SysRole.Columns().DeptId, deptIds).
Count()
if err != nil {
return err
}
if count == 0 {
return gerror.New("只能绑定本级及下一级组织的角色")
}
return nil
}
// ensureDeptRoleUnchangedForSelf 确保当前机构的权限未被修改
func (s *agentBase) ensureDeptRoleUnchangedForSelf(ctx context.Context, deptId, deptRole int64) error {
currentDeptId := contexts.GetDeptId(ctx)
if deptId == 0 || deptId != currentDeptId {
return nil
}
var dept *entity.SysDept
err := s.deptModel(ctx).
Where(dao.SysDept.Columns().DeptId, currentDeptId).
Scan(&dept)
if err != nil {
return err
}
if dept == nil || dept.DeptId == 0 {
return gerror.New("组织不存在")
}
if dept.DeptRole != deptRole {
return gerror.New("不能修改本级组织的权限")
}
return nil
}
// currentDeptDefaultRoleId 获取当前机构的默认角色ID
func (s *agentBase) currentDeptDefaultRoleId(ctx context.Context) (int64, error) {
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return 0, gerror.New("当前用户未绑定组织")
}
var dept *entity.SysDept
err := s.deptModel(ctx).
Where(dao.SysDept.Columns().DeptId, currentDeptId).
Scan(&dept)
if err != nil {
return 0, err
}
if dept == nil || dept.DeptId == 0 {
return 0, gerror.New("组织不存在")
}
return dept.DeptRole, nil
}
// ensureRoleNotCurrentDeptDefault 确保角色不是当前机构的默认角色
func (s *agentBase) ensureRoleNotCurrentDeptDefault(ctx context.Context, roleId int64) error {
if roleId == 0 {
return nil
}
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
if err != nil {
return err
}
if defaultRoleId != 0 && roleId == defaultRoleId {
return gerror.New("不能修改本级机构绑定的默认角色权限")
}
return nil
}
File diff suppressed because it is too large Load Diff
@@ -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)
}
}
@@ -0,0 +1,188 @@
package agent
import (
"context"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/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"
)
// 复现生产形态:模板角色已配置(role 18 @ dept 100,is_pass=1),
// 一级代理的默认角色是创建代理时复制下来的(is_pass=0)。
// 二级代理(dept 1862, parent 128)要再创建下级时,必须能从本机构默认角色拿到权限。
func TestPassRoleByDeptWorksForChildAgentWithoutTemplateFlag(t *testing.T) {
setupPassRoleTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1,
TenantId: "000000",
DeptId: 128, // 一级代理,要继续创建二级代理
}}})
// 三级结构:平台 100(模板 18,is_pass=1)→ 一级代理 128(默认角色 2321,is_pass=0)→ 二级代理 1862(默认角色 1779,is_pass=0)
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES
(100,'000000',0,1,'0',1),
(128,'000000',100,1,'0',2321),
(1862,'000000',128,1,'0',1779)`)
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
(18,'000000',100,'代理商','dl','0','4',1,100),
(2321,'000000',128,'代理商','dl','0','4',0,128),
(1779,'000000',1862,'代理商','dl','0','4',0,100)`)
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES
('000000',18,100,'0'),('000000',2321,100,'0'),('000000',1779,100,'0')`)
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
service := NewAgentDept()
role, menus, err := service.currentDeptPassRole(ctx)
if err != nil {
t.Fatalf("一级代理创建下级时应能取到本机构默认角色权限,实际报错: %v", err)
}
if role == nil || role.RoleId != 2321 {
t.Fatalf("应以本机构默认角色 2321 作为下发来源,实际: %+v", role)
}
if len(menus) != 1 || menus[0].MenuId != 100 {
t.Fatalf("应复制本机构默认角色的菜单,实际: %+v", menus)
}
}
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},
},
}); err != nil {
t.Fatalf("configure sqlite failed: %v", err)
}
// 走真实部署形态:内存库指向主库(config.yaml 里 link 为空时回落主库)
if err := g.Cfg().GetAdapter().(*gcfg.AdapterFile).Set("database.memorydb.link", ""); err != nil {
t.Fatalf("override memorydb link failed: %v", err)
}
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_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,
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)`)
mustExec(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
mustExec(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
mustExec(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
_ = consts.SysRoleDataScopeDeptAndBelow
}
func mustExec(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\nsql=%s", err, sql)
}
}
// 平台自身(dept 100 = 模板所属机构)仍以模板角色为下发来源。
func TestPassRoleByDeptTemplateOwnerUsesTemplateRole(t *testing.T) {
setupPassRoleTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1, TenantId: "000000", DeptId: 100,
}}})
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
(1,'000000',100,'超级管理员','admin','0','1',0,100),
(18,'000000',100,'代理商','dl','0','4',1,100)`)
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
if err != nil {
t.Fatalf("平台创建一级代理应成功,实际: %v", err)
}
if role.RoleId != 18 {
t.Fatalf("平台下发来源应为模板角色 18,实际: %d", role.RoleId)
}
if len(menus) != 1 || menus[0].MenuId != 100 {
t.Fatalf("应复制模板菜单,实际: %+v", menus)
}
}
// 默认角色缺失/停用/归属不符时必须明确报错,不能静默 0 权限。
func TestPassRoleByDeptRejectsBrokenDefaultRole(t *testing.T) {
cases := []struct {
name string
sql []string
}{
{
name: "默认角色不存在",
sql: []string{
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',9999)`,
},
},
{
name: "默认角色已停用",
sql: []string{
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','1','4',0,128)`,
},
},
{
name: "默认角色归属其它机构",
sql: []string{
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`,
`INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',129,'代理商','dl','0','4',0,100)`,
},
},
{
name: "本机构未绑定默认角色",
sql: []string{
`INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',0)`,
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
setupPassRoleTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1, TenantId: "000000", DeptId: 128,
}}})
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (18,'000000',100,'代理商','dl','0','4',1,100)`)
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
mustExec(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
for _, sql := range tc.sql {
mustExec(t, ctx, sql)
}
if _, _, err := NewAgentDept().currentDeptPassRole(ctx); err == nil {
t.Fatal("默认角色异常时必须报错,不能静默下发空权限")
}
})
}
}
// 未配置平台模板时,保留原 is_pass=1 唯一角色的下发流程。
func TestPassRoleByDeptKeepsLegacyFlowWithoutTemplate(t *testing.T) {
setupPassRoleTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1, TenantId: "000000", DeptId: 128,
}}})
mustExec(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (128,'000000',100,1,'0',2321)`)
mustExec(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES (2321,'000000',128,'代理商','dl','0','4',1,128)`)
mustExec(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',2321,100,'0')`)
role, menus, err := NewAgentDept().currentDeptPassRole(ctx)
if err != nil {
t.Fatalf("未配模板时应走原有 is_pass 流程,实际: %v", err)
}
if role.RoleId != 2321 || len(menus) != 1 {
t.Fatalf("旧流程结果不符: role=%+v menus=%+v", role, menus)
}
}
@@ -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 "未知"
}
@@ -0,0 +1,176 @@
package agent
import (
"context"
"slices"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/util/gconv"
)
type sAgentMenu struct {
*agentBase
}
func NewAgentMenu() *sAgentMenu {
return &sAgentMenu{
agentBase: newAgentBase(),
}
}
func init() {
service.RegisterAgentMenu(NewAgentMenu())
}
// AllowedMenuIdsByDeptRole 获取当前机构允许的菜单ID
func (s *sAgentMenu) AllowedMenuIdsByDeptRole(ctx context.Context, deptId int64) (menuIds []int64, err error) {
if err = s.ensureDeptInScope(ctx, deptId); err != nil {
return nil, err
}
var dept *entity.SysDept
err = service.SysDept().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
Where(dao.SysDept.Columns().DeptId, deptId).
Scan(&dept)
if err != nil {
return nil, err
}
if dept == nil || dept.DeptRole == 0 {
return []int64{}, nil
}
if dept.DeptRole == consts.SuperAdminRoleId {
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)
}
}
if err != nil {
return nil, err
}
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)
}
func (s *sAgentMenu) allMenuIds(ctx context.Context) (menuIds []int64, err error) {
var menus []*entity.SysMenu
err = service.SysMenu().Model(ctx).
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum, "ASC").
Scan(&menus)
if err != nil {
return nil, err
}
for _, item := range menus {
menuIds = append(menuIds, item.MenuId)
}
slices.Sort(menuIds)
return slices.Compact(menuIds), nil
}
func (s *sAgentMenu) getRoleById(ctx context.Context, roleId int64) (role *entity.SysRole, err error) {
err = service.SysRole().Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
Where(dao.SysRole.Columns().RoleId, roleId).
Scan(&role)
return role, err
}
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)
if err != nil {
return nil, nil, err
}
if len(menuIds) == 0 && !contexts.IsSuperAdmin(ctx) {
return []*model.SysMenuTreeModel{}, []int64{}, nil
}
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, visibleMenuIds)
}
var menus []*entity.SysMenu
err = mod.Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum, "ASC").
Scan(&menus)
if err != nil {
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) buildMenuList(menus []*entity.SysMenu) (list []*model.SysMenuTreeModel) {
for _, menu := range menus {
list = append(list, &model.SysMenuTreeModel{
Title: menu.MenuName,
Key: gconv.String(menu.MenuId),
ParentId: menu.ParentId,
MenuId: menu.MenuId,
MenuName: menu.MenuName,
MenuType: menu.MenuType,
Perms: menu.Perms,
})
}
return list
}
func (s *sAgentMenu) buildMenuTree(parentId int64, menus []*entity.SysMenu) (tree []*model.SysMenuTreeModel) {
for _, menu := range menus {
if menu.ParentId != parentId {
continue
}
item := &model.SysMenuTreeModel{
Title: menu.MenuName,
Key: gconv.String(menu.MenuId),
ParentId: menu.ParentId,
MenuId: menu.MenuId,
MenuName: menu.MenuName,
MenuType: menu.MenuType,
Perms: menu.Perms,
}
item.Children = s.buildMenuTree(menu.MenuId, menus)
tree = append(tree, item)
}
if parentId == 0 && len(tree) == 0 {
for _, menu := range menus {
item := &model.SysMenuTreeModel{
Title: menu.MenuName,
Key: gconv.String(menu.MenuId),
ParentId: menu.ParentId,
MenuId: menu.MenuId,
MenuName: menu.MenuName,
MenuType: menu.MenuType,
Perms: menu.Perms,
}
item.Children = s.buildMenuTree(menu.MenuId, menus)
tree = append(tree, item)
}
}
return tree
}
@@ -0,0 +1,391 @@
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/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"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/gtime"
)
type sAgentRole struct {
*sAgentMenu
}
// NewAgentRole 创建代理商角色管理服务实例。
func NewAgentRole() *sAgentRole {
return &sAgentRole{
sAgentMenu: &sAgentMenu{
agentBase: newAgentBase(),
},
}
}
func init() {
service.RegisterAgentRole(NewAgentRole())
}
// RoleList 查询当前登录组织本级角色列表。
func (s *sAgentRole) RoleList(ctx context.Context, in *model.AgentRoleListParam) (list []*model.AgentRoleListModel, total int, err error) {
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return nil, 0, gerror.New("当前用户未绑定组织")
}
if err = service.AgentDept().EnsureCurrentDeptDefaultRole(ctx); err != nil {
return nil, 0, err
}
m := s.roleModel(ctx).Where(dao.SysRole.Columns().DeptId, currentDeptId)
if in.RoleName != "" {
m = m.WhereLike(dao.SysRole.Columns().RoleName, "%"+in.RoleName+"%")
}
if in.RoleKey != "" {
m = m.WhereLike(dao.SysRole.Columns().RoleKey, "%"+in.RoleKey+"%")
}
if in.Status != "" {
m = m.Where(dao.SysRole.Columns().Status, in.Status)
}
if len(in.CreatedAt) == 2 {
begin, end := gtime.NewFromStr(in.CreatedAt[0]), gtime.NewFromStr(in.CreatedAt[1])
m = m.WhereBetween(dao.SysRole.Columns().CreatedAt, begin, end.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Order(dao.SysRole.Columns().RoleId, "ASC").
Scan(&list)
if err != nil {
return nil, 0, err
}
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
if err != nil {
return nil, 0, err
}
for _, role := range list {
role.IsDefaultRole = defaultRoleId != 0 && role.RoleId == defaultRoleId
}
return list, total, nil
}
// RoleView 查询角色详情,并返回当前登录组织可分配的菜单树。
func (s *sAgentRole) RoleView(ctx context.Context, roleId int64) (res *model.AgentRoleViewModel, err error) {
role, err := service.SysRole().View(ctx, &model.SysRoleViewParam{RoleId: roleId})
if err != nil {
return nil, err
}
if role == nil || role.RoleId == 0 {
return nil, gerror.New("角色不存在")
}
if role.DeptId != contexts.GetDeptId(ctx) {
return nil, gerror.New("只能操作本级角色")
}
tree, allowed, err := s.AllowedMenuTree(ctx, 0)
if err != nil {
return nil, err
}
isDefaultRole, err := s.isCurrentDeptDefaultRole(ctx, role.RoleId)
if err != nil {
return nil, err
}
return &model.AgentRoleViewModel{
SysRoleViewModel: *role,
AllowedMenuIds: allowed,
MenuTree: tree,
IsDefaultRole: isDefaultRole,
}, nil
}
// RoleSave 保存代理商角色,按角色ID区分新增和修改流程。
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(rolesync.DeferRecording(ctx), in)
if saveErr != nil {
return saveErr
}
return nil
})
if err != nil {
return 0, err
}
if err := rolesync.EnqueueRole(ctx, roleId, contexts.GetUserId(ctx)); err != nil {
return roleId, err
}
return roleId, nil
}
func (s *sAgentRole) saveTemplateRole(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return 0, gerror.New("登录已过期")
}
if in.DeptId == 0 {
in.DeptId = user.DeptId
}
if in.DeptId != user.DeptId {
return 0, gerror.New("只能维护本级角色")
}
if err = s.ensureRoleNotCurrentDeptDefault(ctx, in.RoleId); err != nil {
return 0, err
}
if in.RoleId > 0 {
if err = s.ensureRoleNotInherited(ctx, in.RoleId); err != nil {
return 0, err
}
}
if in.Status == "" {
in.Status = "0"
}
if in.DataScope == "" {
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
}
if !contexts.IsSuperAdmin(ctx) {
allowed, err := rolepolicy.NormalizeAssignableMenus(ctx, user.TenantId, in.DeptId, user.ID, in.MenuIds)
if err != nil {
return 0, err
}
in.MenuIds = allowed
switch in.DataScope {
case consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel:
default:
return 0, gerror.New("代理角色数据权限不能超出本机构及下级范围")
}
}
template, templateErr := rolepolicy.TemplateForRoles(ctx, in.RoleId)
if templateErr != nil {
return 0, templateErr
}
if in.IsPass != 0 && in.IsPass != 1 {
return 0, gerror.New("传递标记无效")
}
if template != nil {
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
}
legacyPass := in.IsPass == 1
if legacyPass {
configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
if err != nil {
return 0, err
}
legacyPass = configured == 0
}
if legacyPass {
in.DataScope = consts.SysRoleDataScopeDeptAndBelow
var dept *entity.SysDept
deptModel := s.deptModel(ctx).Where(dao.SysDept.Columns().DeptId, in.DeptId)
if dao.SysDept.DB().GetConfig().Type != "sqlite" {
deptModel = deptModel.LockUpdate()
}
if err := deptModel.Scan(&dept); err != nil {
return 0, err
}
if dept == nil {
return 0, gerror.New("当前机构不存在")
}
count, err := s.roleModel(ctx).
Where(dao.SysRole.Columns().DeptId, in.DeptId).
Where("is_pass", 1).
WhereNot(dao.SysRole.Columns().RoleId, in.RoleId).
Count()
if err != nil {
return 0, err
}
if count > 0 {
return 0, gerror.New("每个机构只能配置一个下级管理员角色")
}
}
if in.RoleId == 0 {
return s.addRole(ctx, in)
}
return s.editRole(ctx, in)
}
// addRole 新增代理商角色,并同步角色的数据权限配置。
func (s *sAgentRole) addRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) {
deptId := in.DeptId
role, err := service.SysRole().Add(ctx, &model.SysRoleAddParam{
RoleName: in.RoleName,
RoleKey: in.RoleKey,
RoleSort: in.RoleSort,
DataScope: in.DataScope,
MenuCheckStrictly: in.MenuCheckStrictly,
Status: in.Status,
Remark: in.Remark,
MenuIds: in.MenuIds,
DeptId: &deptId,
})
if err != nil {
return 0, err
}
if role == nil {
return 0, nil
}
if _, err = dao.SysRole.Ctx(ctx).
Where(dao.SysRole.Columns().RoleId, role.RoleId).
Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil {
return 0, err
}
if err = s.updateRoleDataScope(ctx, role.RoleId, in); err != nil {
return 0, err
}
return role.RoleId, nil
}
// editRole 修改代理商角色,并禁止在代理商页面移动角色所属组织。
func (s *sAgentRole) editRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) {
current, err := s.RoleView(ctx, in.RoleId)
if err != nil {
return 0, err
}
if current.DeptId != in.DeptId {
return 0, gerror.New("不能在代理商页面移动角色所属组织")
}
deptId := in.DeptId
roleName := in.RoleName
roleKey := in.RoleKey
roleSort := in.RoleSort
menuCheckStrictly := in.MenuCheckStrictly
status := in.Status
remark := in.Remark
role, err := service.SysRole().Edit(ctx, &model.SysRoleEditParam{
RoleId: in.RoleId,
RoleName: &roleName,
RoleKey: &roleKey,
RoleSort: &roleSort,
MenuCheckStrictly: &menuCheckStrictly,
Status: &status,
Remark: &remark,
MenuIds: in.MenuIds,
DeptId: &deptId,
})
if err != nil {
return 0, err
}
if err = s.updateRoleDataScope(ctx, in.RoleId, in); err != nil {
return 0, err
}
if _, err = dao.SysRole.Ctx(ctx).
Where(dao.SysRole.Columns().RoleId, in.RoleId).
Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil {
return 0, err
}
if role == nil {
return in.RoleId, nil
}
return role.RoleId, nil
}
// updateRoleDataScope 更新角色数据权限和组织树勾选配置。
func (s *sAgentRole) updateRoleDataScope(ctx context.Context, roleId int64, in *model.AgentRoleSaveParam) error {
deptCheckStrictly := in.DeptCheckStrictly
return service.SysRole().EditRoleDataScope(ctx, &model.SysRoleDataScopeEditParam{
RoleId: roleId,
DataScope: in.DataScope,
DeptCheckStrictly: &deptCheckStrictly,
DeptIds: []int64{in.DeptId},
})
}
func (s *sAgentRole) isCurrentDeptDefaultRole(ctx context.Context, roleId int64) (bool, error) {
defaultRoleId, err := s.currentDeptDefaultRoleId(ctx)
if err != nil {
return false, err
}
return defaultRoleId != 0 && roleId == defaultRoleId, nil
}
func (s *sAgentRole) ensureRoleInCurrentDept(ctx context.Context, roleId int64) error {
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return gerror.New("当前用户未绑定组织")
}
count, err := s.roleModel(ctx).
Where(dao.SysRole.Columns().RoleId, roleId).
Where(dao.SysRole.Columns().DeptId, currentDeptId).
Count()
if err != nil {
return err
}
if count == 0 {
return gerror.New("只能操作本级角色")
}
return nil
}
func (s *sAgentRole) ensureRoleNotInherited(ctx context.Context, roleId int64) error {
if roleId <= 0 {
return nil
}
if contexts.IsSuperAdmin(ctx) {
return nil
}
count, err := s.inheritedRoleModel(ctx).
Where("r.role_id", roleId).
Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("上级下发角色由上级角色统一维护,当前组织只能查看")
}
return nil
}
func (s *sAgentRole) ensureRoleNoUsers(ctx context.Context, roleId int64) error {
count, err := dao.SysUserRole.Ctx(ctx).
Where(dao.SysUserRole.Columns().RoleId, roleId).
Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("该角色已绑定用户,不能删除")
}
return nil
}
// RoleDelete 删除代理商角色,删除前校验本级机构默认角色不可被删除。
func (s *sAgentRole) RoleDelete(ctx context.Context, roleIds []int64) (err error) {
for _, roleId := range roleIds {
if err = s.ensureRoleInCurrentDept(ctx, roleId); err != nil {
return err
}
if err = s.ensureRoleNotCurrentDeptDefault(ctx, roleId); err != nil {
return err
}
if err = s.ensureRoleNotInherited(ctx, roleId); err != nil {
return err
}
if err = s.ensureRoleNoUsers(ctx, roleId); err != nil {
return err
}
}
_, err = service.SysRole().Delete(ctx, &model.SysRoleDeleteParam{RoleIds: roleIds})
return err
}
func (s *sAgentRole) inheritedRoleModel(ctx context.Context) *gdb.Model {
m := g.Model(dao.SysRole.Table()+" r").
LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id").
Where("r.deleted_at IS NULL").
Where("d.deleted_at IS NULL").
Where("r.created_dept > 0").
Where("r.dept_id <> r.created_dept").
Where("d.parent_id = r.created_dept")
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
m = m.Where("r.tenant_id", tenantId)
}
return m
}
@@ -0,0 +1,127 @@
package agent
import (
"context"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/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"
)
// 复现生产形态:dept 100 同时存在模板角色 18(is_pass=1)和普通角色 79(运营人员,is_pass=0)。
// 给 79 勾权限保存时,旧逻辑强制把 in.IsPass 置 1 再查重,必然报
// 「每个机构只能配置一个下级管理员角色」,导致普通角色永远改不了。
func TestSaveTemplateRoleAllowsOrdinaryRoleWithTemplateConfigured(t *testing.T) {
setupRoleSaveTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1,
TenantId: "000000",
DeptId: 100,
Roles: []model.Role{{RoleId: 1}},
}}})
// 模板已配置为 18,机构 100 已有 is_pass=1 的 18
mustExecRole(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (100,'000000',0,1,'0',1)`)
mustExecRole(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
(1,'000000',100,'超级管理员','admin','0','1',0,100),
(18,'000000',100,'代理商','dl','0','4',1,100),
(79,'000000',100,'运营人员','yunying','0','4',0,100)`)
mustExecRole(t, ctx, `INSERT INTO sys_role_menu (tenant_id, role_id, menu_id, data_scope) VALUES ('000000',18,100,'0')`)
mustExecRole(t, ctx, `INSERT INTO sys_config (config_id, tenant_id, config_key, config_value) VALUES (1,'000000','agent.role.template.role_id','18')`)
// 编辑普通角色 79:勾一个菜单
_, err := NewAgentRole().saveTemplateRole(ctx, &model.AgentRoleSaveParam{
RoleId: 79,
DeptId: 100,
RoleName: "运营人员",
RoleKey: "yunying",
Status: "0",
DataScope: "4",
MenuIds: []int64{100},
})
if err != nil {
t.Fatalf("已配置模板时普通角色必须能保存,实际报错: %v", err)
}
// 关键断言:79 不能被悄悄改成 is_pass=1
var isPass int
v, qErr := g.DB().GetValue(ctx, `SELECT is_pass FROM sys_role WHERE role_id = 79`)
if qErr != nil {
t.Fatalf("query is_pass failed: %v", qErr)
}
isPass = v.Int()
if isPass != 0 {
t.Fatalf("普通角色 79 的 is_pass 被改成了 %d,应保持 0", isPass)
}
// 模板 18 的 is_pass 也不能被动到
v18, _ := g.DB().GetValue(ctx, `SELECT is_pass FROM sys_role WHERE role_id = 18`)
if v18.Int() != 1 {
t.Fatalf("模板角色 18 的 is_pass 变成了 %d,应保持 1", v18.Int())
}
}
// 未配置平台模板时,原有「本级唯一 is_pass=1 角色」限制必须保留。
func TestSaveTemplateRoleKeepsLegacyUniquenessWithoutTemplate(t *testing.T) {
setupRoleSaveTestDB(t)
ctx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{
ID: 1, TenantId: "000000", DeptId: 200,
}}})
mustExecRole(t, ctx, `INSERT INTO sys_dept (dept_id, tenant_id, parent_id, dept_type, status, dept_role) VALUES (200,'000000',0,1,'0',1)`)
mustExecRole(t, ctx, `INSERT INTO sys_role (role_id, tenant_id, dept_id, role_name, role_key, status, data_scope, is_pass, created_dept) VALUES
(1,'000000',200,'超级管理员','admin','0','1',0,200),
(90,'000000',200,'已有的下发角色','dl','0','4',1,200)`)
// 没有 sys_config 模板参数 → 走 legacy 分支
_, err := NewAgentRole().saveTemplateRole(ctx, &model.AgentRoleSaveParam{
RoleId: 0,
DeptId: 200,
RoleName: "第二个下发角色",
RoleKey: "dl2",
Status: "0",
DataScope: "4",
MenuIds: []int64{},
IsPass: 1,
})
if err == nil {
t.Fatal("未配置模板时,第二个 is_pass=1 角色必须被拒绝")
}
if err.Error() != "每个机构只能配置一个下级管理员角色" {
t.Fatalf("错误信息不符,实际: %v", err)
}
}
func setupRoleSaveTestDB(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)
}
_ = g.Cfg().GetAdapter().(*gcfg.AdapterFile).Set("database.memorydb.link", "")
ctx := context.Background()
t.Cleanup(func() { _ = g.DB().Close(ctx) })
mustExecRole(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, order_num INTEGER DEFAULT 0, ancestors TEXT, dept_name TEXT, deleted_at DATETIME)`)
mustExecRole(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)`)
mustExecRole(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)`)
mustExecRole(t, ctx, `CREATE TABLE sys_role_menu (tenant_id TEXT, role_id INTEGER, menu_id INTEGER, data_scope TEXT)`)
mustExecRole(t, ctx, `CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)`)
mustExecRole(t, ctx, `CREATE TABLE sys_user_role (tenant_id TEXT DEFAULT '000000', user_id INTEGER, role_id INTEGER)`)
mustExecRole(t, ctx, `CREATE TABLE sys_menu (menu_id INTEGER PRIMARY KEY, menu_name TEXT, parent_id INTEGER, level INTEGER, tree TEXT, order_num INTEGER DEFAULT 0, path TEXT, component TEXT, query_param TEXT, is_frame INTEGER, is_cache INTEGER, menu_type TEXT, visible TEXT, status TEXT DEFAULT '0', perms TEXT, icon TEXT, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME, remark TEXT)`)
mustExecRole(t, ctx, `INSERT INTO sys_menu (menu_id, menu_name, parent_id, status) VALUES (100,'卡片信息',0,'0')`)
}
func mustExecRole(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\nsql=%s", err, sql)
}
}
@@ -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)
}
}
@@ -0,0 +1,25 @@
package agent
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
)
type sAgentScope struct {
*agentBase
}
func NewAgentScope() *sAgentScope {
return &sAgentScope{
agentBase: newAgentBase(),
}
}
func init() {
service.RegisterAgentScope(NewAgentScope())
}
func (s *sAgentScope) ScopeDeptIds(ctx context.Context) (deptIds []int64, err error) {
return s.scopeDeptIds(ctx)
}
@@ -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)
}
@@ -0,0 +1,340 @@
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/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sAgentUser struct {
*agentBase
}
func NewAgentUser() *sAgentUser {
return &sAgentUser{
agentBase: newAgentBase(),
}
}
func init() {
service.RegisterAgentUser(NewAgentUser())
}
// UserList 查询用户列表
func (s *sAgentUser) UserList(ctx context.Context, in *model.AgentUserListParam) (list []*model.AgentUserListModel, total int, err error) {
deptId := contexts.GetDeptId(ctx)
if deptId == 0 {
return nil, 0, gerror.New("当前用户未绑定组织")
}
if in.Page <= 0 {
in.Page = 1
}
if in.PageSize <= 0 {
in.PageSize = 20
}
m := s.userModel(ctx).Where(dao.SysUser.Columns().DeptId, deptId)
if in.UserName != "" {
m = m.WhereLike(dao.SysUser.Columns().UserName, "%"+in.UserName+"%")
}
if in.NickName != "" {
m = m.WhereLike(dao.SysUser.Columns().NickName, "%"+in.NickName+"%")
}
if in.Email != "" {
m = m.WhereLike(dao.SysUser.Columns().Email, "%"+in.Email+"%")
}
if in.Phonenumber != "" {
m = m.WhereLike(dao.SysUser.Columns().Phonenumber, "%"+in.Phonenumber+"%")
}
if in.Status != "" {
m = m.Where(dao.SysUser.Columns().Status, in.Status)
}
if len(in.CreatedAt) == 2 {
begin, end := gtime.NewFromStr(in.CreatedAt[0]), gtime.NewFromStr(in.CreatedAt[1])
m = m.WhereBetween(dao.SysUser.Columns().CreatedAt, begin, end.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
var sysList []*model.SysUserListModel
err = m.Order(dao.SysUser.Columns().UserId, "ASC").WithAll().Page(in.Page, in.PageSize).Scan(&sysList)
if err != nil {
return nil, 0, err
}
if err = s.fillRolesAndDefaultFlag(ctx, sysList); err != nil {
return nil, 0, err
}
if err = gconv.Structs(sysList, &list); err != nil {
return nil, 0, err
}
return list, total, nil
}
// UserView 查看用户详情
func (s *sAgentUser) UserView(ctx context.Context, userId int64) (res *model.AgentUserViewModel, err error) {
sysUser, err := service.SysUser().GetUserById(ctx, userId)
if err != nil {
return nil, err
}
if sysUser == nil || sysUser.UserId == 0 {
return nil, gerror.New("用户不存在")
}
if sysUser.DeptId != contexts.GetDeptId(ctx) {
return nil, gerror.New("只能操作本级用户")
}
res = &model.AgentUserViewModel{}
if err = gconv.Struct(sysUser, res); err != nil {
return nil, err
}
res.IsDefaultUser, err = s.isDefaultUser(ctx, userId)
if err != nil {
return nil, err
}
return res, nil
}
// filterRoleIdsByDept 过滤角色ID,只保留本级及下一级角色
func (s *sAgentUser) filterRoleIdsByDept(ctx context.Context, deptId int64, roleIds []int64) (res []int64, err error) {
if len(roleIds) == 0 {
return roleIds, nil
}
var roles []*entity.SysRole
err = s.roleModel(ctx).
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
Where(dao.SysRole.Columns().DeptId, deptId).
Where("is_pass", 0).
Scan(&roles)
if err != nil {
return nil, err
}
for _, role := range roles {
res = append(res, role.RoleId)
}
return res, nil
}
// fillRolesAndDefaultFlag 填充用户角色和默认标志
func (s *sAgentUser) fillRolesAndDefaultFlag(ctx context.Context, list []*model.SysUserListModel) error {
if len(list) == 0 {
return nil
}
userIds := make([]int64, 0, len(list))
deptIds := make([]int64, 0, len(list))
userById := make(map[int64]*model.SysUserListModel, len(list))
for _, user := range list {
userIds = append(userIds, user.UserId)
deptIds = append(deptIds, user.DeptId)
userById[user.UserId] = user
}
var userRoles []*entity.SysUserRole
err := dao.SysUserRole.Ctx(ctx).
WhereIn(dao.SysUserRole.Columns().UserId, userIds).
Scan(&userRoles)
if err != nil {
return err
}
roleIds := make([]int64, 0, len(userRoles))
userRoleMap := make(map[int64][]int64, len(userRoles))
for _, userRole := range userRoles {
roleIds = append(roleIds, userRole.RoleId)
userRoleMap[userRole.UserId] = append(userRoleMap[userRole.UserId], userRole.RoleId)
}
if len(roleIds) > 0 {
var roles []*entity.SysRole
if err = s.roleModel(ctx).
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
Scan(&roles); err != nil {
return err
}
roleMap := make(map[int64]*entity.SysRole, len(roles))
for _, role := range roles {
roleMap[role.RoleId] = role
}
for _, userRole := range userRoles {
role := roleMap[userRole.RoleId]
user := userById[userRole.UserId]
if role == nil || user == nil {
continue
}
user.Roles = append(user.Roles, &model.SysRoleMiniModel{
RoleId: role.RoleId,
RoleName: role.RoleName,
DataScope: role.DataScope,
})
}
}
var depts []*entity.SysDept
if err = s.deptModel(ctx).
WhereIn(dao.SysDept.Columns().DeptId, deptIds).
Scan(&depts); err != nil {
return err
}
deptRoleMap := make(map[int64]int64, len(depts))
for _, dept := range depts {
if dept.DeptRole > 0 {
deptRoleMap[dept.DeptId] = dept.DeptRole
}
}
for _, user := range list {
deptRoleId := deptRoleMap[user.DeptId]
if deptRoleId == 0 {
continue
}
for _, roleId := range userRoleMap[user.UserId] {
if roleId == deptRoleId {
user.IsDefaultUser = true
break
}
}
}
return nil
}
// isDefaultUser 判断用户是否为默认用户
func (s *sAgentUser) isDefaultUser(ctx context.Context, userId int64) (bool, error) {
var user *entity.SysUser
if err := s.userModel(ctx).
Where(dao.SysUser.Columns().UserId, userId).
Scan(&user); err != nil {
return false, err
}
if user == nil || user.UserId == 0 {
return false, gerror.New("用户不存在")
}
var dept *entity.SysDept
if err := s.deptModel(ctx).
Where(dao.SysDept.Columns().DeptId, user.DeptId).
Scan(&dept); err != nil {
return false, err
}
if dept == nil || dept.DeptRole == 0 {
return false, nil
}
count, err := dao.SysUserRole.Ctx(ctx).
Where(dao.SysUserRole.Columns().UserId, userId).
Where(dao.SysUserRole.Columns().RoleId, dept.DeptRole).
Count()
return count > 0, err
}
// UserSave 新增/修改用户
func (s *sAgentUser) UserSave(ctx context.Context, in *model.SysUserAddModel) (res *model.AgentUserViewModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.New("登录已过期")
}
in.TenantId = user.TenantId
in.DeptId = user.DeptId
in.UserType = consts.SysUserTypeSys
if in.DeptId == 0 {
return nil, gerror.New("当前用户未绑定组织")
}
if len(in.RoleIds) == 0 {
return nil, gerror.New("请选择角色")
}
in.RoleIds, err = s.filterRoleIdsByDept(ctx, in.DeptId, in.RoleIds)
if err != nil {
return nil, err
}
if len(in.RoleIds) == 0 {
return nil, gerror.New("请选择本级角色")
}
// 验证角色的DeptId和用户的deptId一致
if err = s.ensureRoleDeptInScope(ctx, in.DeptId, in.RoleIds); err != nil {
return nil, err
}
sysUser, err := service.SysUser().AddUser(ctx, in)
if err != nil {
return nil, err
}
res = &model.AgentUserViewModel{}
if err = gconv.Struct(sysUser, res); err != nil {
return nil, err
}
res.IsDefaultUser, err = s.isDefaultUser(ctx, res.UserId)
if err != nil {
return nil, err
}
return res, nil
}
// UserUpdate 更新用户
func (s *sAgentUser) UserUpdate(ctx context.Context, in *model.SysUserUpdateModel) (err error) {
current, err := s.UserView(ctx, in.UserId)
if err != nil {
return err
}
currentDeptId := contexts.GetDeptId(ctx)
if currentDeptId == 0 {
return gerror.New("当前用户未绑定组织")
}
if current.DeptId != currentDeptId {
return gerror.New("只能修改本级用户")
}
in.DeptId = &current.DeptId
if len(in.RoleIds) == 0 {
return gerror.New("请选择角色")
}
in.RoleIds, err = s.filterRoleIdsByDept(ctx, *in.DeptId, in.RoleIds)
if err != nil {
return err
}
if len(in.RoleIds) == 0 {
return gerror.New("请选择本级角色")
}
userType := consts.SysUserTypeSys
in.UserType = &userType
// 验证角色的DeptId和用户的deptId一致
if err = s.ensureRoleDeptInScope(ctx, *in.DeptId, in.RoleIds); err != nil {
return err
}
return service.SysUser().UpdateUser(ctx, in)
}
// UserDelete 删除用户
func (s *sAgentUser) UserDelete(ctx context.Context, userIds []int64) (err error) {
for _, userId := range userIds {
if _, err = s.UserView(ctx, userId); err != nil {
return err
}
}
return service.SysUser().DeleteUser(ctx, userIds)
}
// UserResetPassword 重置用户密码
func (s *sAgentUser) UserResetPassword(ctx context.Context, userId int64, password string) (err error) {
if _, err = s.UserView(ctx, userId); err != nil {
return err
}
return service.SysUser().ResetPassword(ctx, userId, password)
}
// 验证角色部门和用户部门是否一致
func (s *sAgentUser) ensureRoleDeptInScope(ctx context.Context, deptId int64, roleIds []int64) (err error) {
// 验证角色的DeptId和用户的deptId一致
var roles []*entity.SysRole
err = s.roleModel(ctx).
WhereIn(dao.SysRole.Columns().RoleId, roleIds).
Scan(&roles)
if err != nil {
return err
}
for _, role := range roles {
if role.DeptId != deptId {
return gerror.New("角色部门与用户部门不一致")
}
}
return nil
}
@@ -0,0 +1,170 @@
// Package iot
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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"
)
type sAgentEnterprise struct{}
func NewAgentEnterprise() *sAgentEnterprise {
return &sAgentEnterprise{}
}
func init() {
service.RegisterAgentEnterprise(NewAgentEnterprise())
}
// Model 企业客户单位信息ORM模型
func (s *sAgentEnterprise) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TAgentEnterprise.Ctx(ctx), option...)
}
// List 获取企业客户单位信息列表
func (s *sAgentEnterprise) List(ctx context.Context, in *model.TAgentEnterpriseListParam) (list []*model.TAgentEnterpriseListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TAgentEnterpriseListModel{})
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TAgentEnterprise.Columns().Id, in.Id)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TAgentEnterprise.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TAgentEnterprise.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取企业客户单位信息列表失败,请稍后重试!")
return
}
return
}
// Export 导出企业客户单位信息
func (s *sAgentEnterprise) Export(ctx context.Context, in *model.TAgentEnterpriseListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TAgentEnterpriseExportModel{})
if err != nil {
return
}
var (
fileName = "导出企业客户单位信息-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TAgentEnterpriseExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增企业客户单位信息
func (s *sAgentEnterprise) Edit(ctx context.Context, in *model.TAgentEnterpriseEditParam) (err error) {
// 验证'LegalPersonIdCard'唯一
if err = xgorm.IsUnique(ctx, &dao.TAgentEnterprise, g.Map{dao.TAgentEnterprise.Columns().LegalPersonIdCard: in.LegalPersonIdCard}, "法人身份证号已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
userId := contexts.GetUserId(ctx)
in.UpdatedBy = &userId
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TAgentEnterpriseUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改企业客户单位信息失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
deptId := contexts.GetDeptId(ctx)
in.CreatedDept = &deptId
userId := contexts.GetUserId(ctx)
in.CreatedBy = &userId
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TAgentEnterpriseInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增企业客户单位信息失败,请稍后重试!")
}
return
})
}
// Delete 删除企业客户单位信息
func (s *sAgentEnterprise) Delete(ctx context.Context, in *model.TAgentEnterpriseDeleteParam) (err error) {
if _, err = s.Model(ctx).Where(dao.TAgentEnterprise.Columns().DeptId, in.DeptId).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除企业客户单位信息失败,请稍后重试!")
return
}
return
}
// View 获取企业客户单位信息指定信息
func (s *sAgentEnterprise) View(ctx context.Context, in *model.TAgentEnterpriseViewParam) (res *model.TAgentEnterpriseViewModel, err error) {
if err = s.Model(ctx).Where(dao.TAgentEnterprise.Columns().DeptId, in.DeptId).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取企业客户单位信息信息,请稍后重试!")
return
}
return
}
@@ -0,0 +1,167 @@
// Package iot
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package agent
import (
"context"
"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"
"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/gtime"
)
type sAgentTApi struct{}
func NewAgentTApi() *sAgentTApi {
return &sAgentTApi{}
}
func init() {
service.RegisterAgentTApi(NewAgentTApi())
}
// Model API集成ORM模型
func (s *sAgentTApi) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TApi.Ctx(ctx), option...)
}
// List 获取API集成列表
func (s *sAgentTApi) List(ctx context.Context, in *model.TApiListParam) (list []*model.TApiListModel, totalCount int, err error) {
mod := s.Model(ctx)
tApiTable := dao.TApi.Table()
sysDeptTable := dao.SysDept.Table()
// 字段过滤
mod = mod.FieldsPrefix(tApiTable, model.TApiListModel{})
mod = mod.Fields(
sysDeptTable+"."+dao.SysDept.Columns().DeptKey+" AS deptKey",
sysDeptTable+"."+dao.SysDept.Columns().DeptName+" AS deptName",
)
mod = mod.LeftJoin(
sysDeptTable,
sysDeptTable+"."+dao.SysDept.Columns().DeptId+"="+tApiTable+"."+dao.TApi.Columns().DeptId,
)
// 查询APPKey
if in.AppKey != "" {
mod = mod.Where(dao.TApi.Columns().AppKey+" like ?", "%"+in.AppKey+"%")
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TApi.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取API集成列表失败,请稍后重试!")
return
}
return
}
// Export 导出API集成(异步:提交任务后到下载中心取文件)
func (s *sAgentTApi) Export(ctx context.Context, in *model.TApiListParam) (err error) {
_, 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集成
func (s *sAgentTApi) Edit(ctx context.Context, in *model.TApiEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
userId := contexts.GetUserId(ctx)
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
in.UpdatedBy = &userId
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TApiUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改API集成失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
deptId := contexts.GetDeptId(ctx)
in.CreatedDept = &deptId
userId := contexts.GetUserId(ctx)
in.CreatedBy = &userId
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TApiInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增API集成失败,请稍后重试!")
}
return
})
}
// Delete 删除API集成
func (s *sAgentTApi) Delete(ctx context.Context, in *model.TApiDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除API集成失败,请稍后重试!")
return
}
return
}
// View 获取API集成指定信息
func (s *sAgentTApi) View(ctx context.Context, in *model.TApiViewParam) (res *model.TApiViewModel, err error) {
if err = s.Model(ctx).Where(dao.TApi.Columns().AppKey, in.AppKey).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取API集成信息,请稍后重试!")
return
}
return
}
@@ -0,0 +1,238 @@
package agent
import (
"context"
"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"
"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/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sAgentTApiGroup struct{}
// NewAgentTApiGroup 创建开放接口权限组后台服务实例。
func NewAgentTApiGroup() *sAgentTApiGroup {
return &sAgentTApiGroup{}
}
func init() {
service.RegisterAgentTApiGroup(NewAgentTApiGroup())
}
// Model 返回开放接口权限组 ORM 模型,默认启用租户和数据权限过滤。
func (s *sAgentTApiGroup) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TApiGroup.Ctx(ctx), option...)
}
// List 分页查询开放接口权限组列表。
func (s *sAgentTApiGroup) List(ctx context.Context, in *model.TApiGroupListParam) (list []*model.TApiGroupListModel, totalCount int, err error) {
mod := s.Model(ctx).Fields(model.TApiGroupListModel{})
if in.Id > 0 {
mod = mod.Where(dao.TApiGroup.Columns().Id, in.Id)
}
if in.Status > 0 {
mod = mod.Where(dao.TApiGroup.Columns().Status, in.Status)
}
if in.GroupName != "" {
mod = mod.Where(dao.TApiGroup.Columns().GroupName+" LIKE ?", "%"+in.GroupName+"%")
}
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
mod = mod.WhereBetween(dao.TApiGroup.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PageSize).
OrderDesc(dao.TApiGroup.Columns().Id).
Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取开放接口权限组列表失败,请稍后重试!")
}
return
}
// Export 导出开放接口权限组(异步:提交任务后到下载中心取文件)
func (s *sAgentTApiGroup) Export(ctx context.Context, in *model.TApiGroupListParam) (err error) {
_, 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 新增或修改开放接口权限组,并同步维护权限组与接口资源的关联。
func (s *sAgentTApiGroup) Edit(ctx context.Context, in *model.TApiGroupEditParam) (err error) {
if in.GroupCode != nil {
if err = xgorm.IsUnique(ctx, &dao.TApiGroup, g.Map{dao.TApiGroup.Columns().GroupCode: *in.GroupCode}, "权限组编码已存在", in.Id); err != nil {
return
}
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
groupId := in.Id
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
if _, err = s.Model(ctx).
Fields(model.TApiGroupUpdateFields{}).
WherePri(in.Id).
Data(in).
Update(); err != nil {
return gerror.Wrap(err, "修改开放接口权限组失败,请稍后重试!")
}
return s.replaceGroupResources(ctx, tx, groupId, in.ApiIds)
}
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
groupId, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TApiGroupInsertFields{}).
Data(in).
InsertAndGetId()
if err != nil {
return gerror.Wrap(err, "新增开放接口权限组失败,请稍后重试!")
}
return s.replaceGroupResources(ctx, tx, groupId, in.ApiIds)
})
}
// Delete 删除开放接口权限组,并删除该权限组下的资源关联。
func (s *sAgentTApiGroup) Delete(ctx context.Context, in *model.TApiGroupDeleteParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
groupIds := gconv.Int64s(in.Id)
if len(groupIds) > 0 {
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
WhereIn(dao.TApiGroupResource.Columns().GroupId, groupIds).
Delete(); err != nil {
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
}
}
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
return gerror.Wrap(err, "删除开放接口权限组失败,请稍后重试!")
}
return nil
})
}
// View returns the API group detail with bound resource IDs and resource list.
func (s *sAgentTApiGroup) View(ctx context.Context, in *model.TApiGroupViewParam) (res *model.TApiGroupViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "get open api group failed")
}
if res == nil {
return
}
apiIds, err := dao.TApiGroupResource.Ctx(ctx).
Fields(dao.TApiGroupResource.Columns().ApiId).
Where(dao.TApiGroupResource.Columns().GroupId, in.Id).
Array()
if err != nil {
return nil, gerror.Wrap(err, "get open api group resource relations failed")
}
res.ApiIds = gconv.Int64s(apiIds)
res.Apis = []*model.TApiResourceListModel{}
if len(res.ApiIds) > 0 {
if err = handler.Model(dao.TApiResource.Ctx(ctx), &handler.Option{FilterTenant: true, FilterAuth: false}).
Fields(model.TApiResourceListModel{}).
WhereIn(dao.TApiResource.Columns().Id, res.ApiIds).
OrderAsc(dao.TApiResource.Columns().Id).
Hook(hook.MemberSummary).
Scan(&res.Apis); err != nil {
return nil, gerror.Wrap(err, "get open api group resources failed")
}
}
return
}
// Status 更新开放接口权限组启停状态。
func (s *sAgentTApiGroup) Status(ctx context.Context, in *model.TApiGroupStatusParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TApiGroup.Columns().Status: in.Status,
dao.TApiGroup.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.TApiGroup.Columns().UpdatedAt: gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新开放接口权限组状态失败,请稍后重试!")
}
return
}
// replaceGroupResources 替换权限组绑定的接口资源,先清空旧关联再批量写入新关联。
func (s *sAgentTApiGroup) replaceGroupResources(ctx context.Context, tx gdb.TX, groupId int64, apiIds []int64) (err error) {
if groupId <= 0 {
return gerror.New("权限组ID不能为空")
}
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
Where(dao.TApiGroupResource.Columns().GroupId, groupId).
Delete(); err != nil {
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
}
seen := make(map[int64]struct{}, len(apiIds))
data := g.List{}
for _, apiId := range apiIds {
if apiId <= 0 {
continue
}
if _, ok := seen[apiId]; ok {
continue
}
seen[apiId] = struct{}{}
data = append(data, g.Map{
dao.TApiGroupResource.Columns().GroupId: groupId,
dao.TApiGroupResource.Columns().ApiId: apiId,
})
}
if len(data) == 0 {
return nil
}
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "保存开放接口权限组资源关联失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,239 @@
package agent
import (
"context"
"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"
"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/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
type sAgentTApiResource struct{}
// NewAgentTApiResource 创建开放接口资源后台服务实例。
func NewAgentTApiResource() *sAgentTApiResource {
return &sAgentTApiResource{}
}
func init() {
service.RegisterAgentTApiResource(NewAgentTApiResource())
}
// Model 返回开放接口资源 ORM 模型,默认启用租户和数据权限过滤。
func (s *sAgentTApiResource) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TApiResource.Ctx(ctx), option...)
}
// List 分页查询开放接口资源列表。
func (s *sAgentTApiResource) List(ctx context.Context, in *model.TApiResourceListParam) (list []*model.TApiResourceListModel, totalCount int, err error) {
deptId := contexts.GetDeptId(ctx)
if deptId <= 0 {
return []*model.TApiResourceListModel{}, 0, nil
}
mod := s.Model(ctx, &handler.Option{FilterTenant: true, FilterAuth: false}).
Fields(model.TApiResourceListModel{})
isTop, dept, err := service.AgentDept().IsTopDept(ctx)
if err != nil {
return nil, 0, err
}
if dept != nil && (dept.DeptId == 100 || dept.ParentId == 100) {
isTop = true
}
if !isTop {
var apiInfo struct {
GroupId int64 `orm:"group_id"`
}
if err = dao.TApi.Ctx(ctx).
Fields(dao.TApi.Columns().GroupId).
Where(dao.TApi.Columns().DeptId, deptId).
Where(dao.TApi.Columns().TenantId, contexts.GetTenantId(ctx)).
OrderDesc(dao.TApi.Columns().Id).
Scan(&apiInfo); err != nil {
return nil, 0, gerror.Wrap(err, "get current dept open api group failed")
}
if apiInfo.GroupId <= 0 {
return []*model.TApiResourceListModel{}, 0, nil
}
apiIds, err := dao.TApiGroupResource.Ctx(ctx).
Fields(dao.TApiGroupResource.Columns().ApiId).
Where(dao.TApiGroupResource.Columns().GroupId, apiInfo.GroupId).
Array()
if err != nil {
return nil, 0, gerror.Wrap(err, "get open api group resources failed")
}
if len(apiIds) == 0 {
return []*model.TApiResourceListModel{}, 0, nil
}
mod = mod.WhereIn(dao.TApiResource.Columns().Id, gconv.Int64s(apiIds))
}
if in.Id > 0 {
mod = mod.Where(dao.TApiResource.Columns().Id, in.Id)
}
if in.Status > 0 {
mod = mod.Where(dao.TApiResource.Columns().Status, in.Status)
}
if in.ApiName != "" {
mod = mod.Where(dao.TApiResource.Columns().ApiName+" LIKE ?", "%"+in.ApiName+"%")
}
if in.Version != "" {
mod = mod.Where(dao.TApiResource.Columns().Version+" LIKE ?", "%"+in.Version+"%")
}
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
mod = mod.WhereBetween(dao.TApiResource.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PageSize).
OrderDesc(dao.TApiResource.Columns().Id).
Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取开放接口资源列表失败,请稍后重试!")
}
return
}
// Export 导出开放接口资源(异步:提交任务后到下载中心取文件)
func (s *sAgentTApiResource) Export(ctx context.Context, in *model.TApiResourceListParam) (err error) {
_, 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 新增或修改开放接口资源。
func (s *sAgentTApiResource) Edit(ctx context.Context, in *model.TApiResourceEditParam) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if in.Status == nil {
status := 0
in.Status = &status
}
if in.Method != nil {
method := *in.Method
in.Method = &method
}
if in.ApiCode != nil && gstr.Trim(*in.ApiCode) != "" {
if err = xgorm.IsUnique(ctx, &dao.TApiResource, g.Map{dao.TApiResource.Columns().ApiCode: gstr.Trim(*in.ApiCode)}, "接口权限标识已存在", in.Id); err != nil {
return
}
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
if _, err = s.Model(ctx).
Fields(model.TApiResourceUpdateFields{}).
WherePri(in.Id).
Data(in).
Update(); err != nil {
return gerror.Wrap(err, "修改开放接口资源失败,请稍后重试!")
}
return nil
}
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TApiResourceInsertFields{}).
Data(in).
Insert(); err != nil {
return gerror.Wrap(err, "新增开放接口资源失败,请稍后重试!")
}
return nil
})
}
// Delete 删除开放接口资源,并清理权限组资源关联。
func (s *sAgentTApiResource) Delete(ctx context.Context, in *model.TApiResourceDeleteParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
apiIds := gconv.Int64s(in.Id)
if len(apiIds) > 0 {
if _, err = tx.Ctx(ctx).Model(dao.TApiGroupResource.Table()).
WhereIn(dao.TApiGroupResource.Columns().ApiId, apiIds).
Delete(); err != nil {
return gerror.Wrap(err, "删除开放接口权限组资源关联失败,请稍后重试!")
}
}
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
return gerror.Wrap(err, "删除开放接口资源失败,请稍后重试!")
}
return nil
})
}
// View 查询开放接口资源详情。
func (s *sAgentTApiResource) View(ctx context.Context, in *model.TApiResourceViewParam) (res *model.TApiResourceViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取开放接口资源信息失败,请稍后重试!")
}
return
}
// Status 更新开放接口资源启停状态。
func (s *sAgentTApiResource) Status(ctx context.Context, in *model.TApiResourceStatusParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TApiResource.Columns().Status: in.Status,
dao.TApiResource.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.TApiResource.Columns().UpdatedAt: gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新开放接口资源状态失败,请稍后重试!")
}
return
}
@@ -0,0 +1,182 @@
// Package iot
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @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/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 sAgentTCustWithdrawAccount struct{}
func NewAgentTCustWithdrawAccount() *sAgentTCustWithdrawAccount {
return &sAgentTCustWithdrawAccount{}
}
func init() {
service.RegisterAgentTCustWithdrawAccount(NewAgentTCustWithdrawAccount())
}
// Model 客户提现账户表ORM模型
func (s *sAgentTCustWithdrawAccount) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.TCustWithdrawAccount.Ctx(ctx), option...)
}
// List 获取客户提现账户表列表
func (s *sAgentTCustWithdrawAccount) List(ctx context.Context, in *model.TCustWithdrawAccountListParam) (list []*model.TCustWithdrawAccountListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TCustWithdrawAccountListModel{})
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TCustWithdrawAccount.Columns().Id, in.Id)
}
// 查询状态:0-禁用 1-正常
if in.Status > 0 {
mod = mod.Where(dao.TCustWithdrawAccount.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TCustWithdrawAccount.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.TCustWithdrawAccount.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取客户提现账户表列表失败,请稍后重试!")
return
}
return
}
// Export 导出客户提现账户表
func (s *sAgentTCustWithdrawAccount) Export(ctx context.Context, in *model.TCustWithdrawAccountListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TCustWithdrawAccountExportModel{})
if err != nil {
return
}
var (
fileName = "导出客户提现账户表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.TCustWithdrawAccountExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增客户提现账户表
func (s *sAgentTCustWithdrawAccount) Edit(ctx context.Context, in *model.TCustWithdrawAccountEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
userId := contexts.GetUserId(ctx)
in.UpdatedBy = &userId
in.UpdatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TCustWithdrawAccountUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改客户提现账户表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
deptId := contexts.GetDeptId(ctx)
in.CreatedDept = &deptId
userId := contexts.GetUserId(ctx)
in.CreatedBy = &userId
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.TCustWithdrawAccountInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增客户提现账户表失败,请稍后重试!")
}
return
})
}
// Delete 删除客户提现账户表
func (s *sAgentTCustWithdrawAccount) Delete(ctx context.Context, in *model.TCustWithdrawAccountDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除客户提现账户表失败,请稍后重试!")
return
}
return
}
// View 获取客户提现账户表指定信息
func (s *sAgentTCustWithdrawAccount) View(ctx context.Context, in *model.TCustWithdrawAccountViewParam) (res *model.TCustWithdrawAccountViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取客户提现账户表信息,请稍后重试!")
return
}
return
}
// Status 更新客户提现账户表状态
func (s *sAgentTCustWithdrawAccount) Status(ctx context.Context, in *model.TCustWithdrawAccountStatusParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TCustWithdrawAccount.Columns().Status: in.Status,
dao.TCustWithdrawAccount.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新客户提现账户表状态失败,请稍后重试!")
return
}
return
}