本文从本人博客搬运,原文格式更加美观,可以移步原文阅读:若依系统用户权限模型分析

用户-角色-菜单

1.基本使用

这是一个经典的用户-角色-权限的模型,其中菜单就代表了权限(因为权限就代表能否访问某个资源,菜单可以代表资源),它们互为多对多关系

新增菜单,需要选择上级菜单。菜单分为三类:目录、菜单、按钮

目录表示外层,有一个下拉箭头,点击可以列出子菜单

目录存储到数据库中有如下特点:

  • parent_id为0
  • component组件路径为空
  • perms权限标识为空

菜单表示目录下可点击的模块。其中组件路径代表点击菜单后访问的路径(前端路由使用),权限标识代表访问菜单需要的权限字符串。默认情况下菜单的权限都会包含list,因为点击菜单后默认来到数据列表页面,需要调用后端数据列表查询接口

按钮表示菜单下的资源,没有路由地址和组件路径(因为按钮在页面内部,不涉及前端路由),权限标识代表点击按钮生效需要的权限字符串

用默认用户登录会拥有所有权限,我们尝试新增一个角色,让其只拥有部分菜单权限

此时数据库sys_role表会保存这个角色的基本信息

同时sys_role_menu会保存这个角色与菜单的关联关系,可以发现只要有至少一个子项被选中,父项就会被添加到角色可用菜单中。比如角色管理下只选中了角色查询,那么其父菜单角色管理也会被附带添加

然后再新增一个用户,赋予其测试角色

此时除了在sys_user表保存用户信息,还会在sys_user_role表保存用户所关联的角色。这样通过用户->角色->菜单这样的关系,就可以定义用户所有的权限

然后用新创建的用户登录,可以发现只能显示系统管理下的用户管理和角色管理

并且角色管理页面中只有查询相关的按钮,没有增删改相关的按钮

2.原理分析

当用户登录成功后,redis中会保存用户的信息,其中就包含了角色和权限字符串的信息

{"@type": "com.ruoyi.common.core.domain.model.LoginUser","accountNonExpired": true,"accountNonLocked": true,"browser": "Chrome 8","credentialsNonExpired": true,"enabled": true,"expireTime": 1610099630922,"ipaddr": "127.0.0.1","loginLocation": "内网IP","loginTime": 1610097830922,"os": "Windows 10","password": "$2a$10$QJAQWU5OYLWE609iM5Tr5O1KXjbLMX8TqU6wp5kqf0/UjE58HWpQ6","permissions": [  // 用户关联的所有菜单的权限字符串"system:user:resetPwd","system:user:export","system:user:list","system:user:remove","system:role:list","system:user:import","system:user:edit","system:role:query","system:user:query","system:user:add"],"token": "08c70ed8-3283-4bcb-8e14-d239ae93f7d2","user": {"admin": false,"avatar": "","createBy": "admin","createTime": 1599702165000,"delFlag": "0","dept": {"children": [],"deptId": 103,"deptName": "研发部门","leader": "若依","orderNum": "1","params": {},"parentId": 101,"status": "0"},"deptId": 103,"email": "","loginIp": "","nickName": "baobao","params": {},"password": "$2a$10$QJAQWU5OYLWE609iM5Tr5O1KXjbLMX8TqU6wp5kqf0/UjE58HWpQ6","phonenumber": "","roles": [  // 用户所拥有的角色{"admin": false,"dataScope": "2","flag": false,"params": {},"roleId": 3,"roleKey": "test","roleName": "测试","roleSort": "2","status": "0"}],"sex": "0","status": "0","userId": 3,"userName": "包包"},"username": "包包"
}

登录成功来到首页时,会发起/getInfo请求,获取用户信息,包含了权限和角色信息

/*** 获取用户信息* * @return 用户信息*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{// 从请求头中获取token,解析token后获得uuid,根据uuid从redis中查询关联的用户信息LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());SysUser user = loginUser.getUser();// 跳转1:获取用户角色集合Set<String> roles = permissionService.getRolePermission(user);// 跳转2:获取用户权限集合Set<String> permissions = permissionService.getMenuPermission(user);AjaxResult ajax = AjaxResult.success();ajax.put("user", user);ajax.put("roles", roles);ajax.put("permissions", permissions);return ajax;
}// 跳转1:获取用户角色集合
public Set<String> getRolePermission(SysUser user)
{Set<String> roles = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){roles.add("admin");}else{// 跳转3:从数据库查询该用户的所有角色roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));}return roles;
}// 跳转3:从数据库查询该用户的所有角色
@Override
public Set<String> selectRolePermissionByUserId(Long userId)
{// 查询用户关联的所有角色List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);Set<String> permsSet = new HashSet<>();// 遍历角色,将所有角色字符串收集成一个Set返回for (SysRole perm : perms){if (StringUtils.isNotNull(perm)){permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));}}return permsSet;
}// 跳转2:获取用户权限集合
public Set<String> getMenuPermission(SysUser user)
{Set<String> perms = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()){perms.add("*:*:*");}else{// 跳转4:查询该用户所有权限perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));}return perms;
}// 跳转4:查询该用户所有权限
@Override
public Set<String> selectMenuPermsByUserId(Long userId)
{// 查询该用户的所有权限字符串List<String> perms = menuMapper.selectMenuPermsByUserId(userId);Set<String> permsSet = new HashSet<>();for (String perm : perms){if (StringUtils.isNotEmpty(perm)){permsSet.addAll(Arrays.asList(perm.trim().split(",")));}}return permsSet;
}

然后会调用/getRouters查询该用户有访问权限的目录菜单的路由数据(不包含按钮,因为按钮只有点击进入具体的菜单后才会决定显示或不显示),根据获取的路由数据动态展示该用户有权限的目录与菜单

/*** 获取路由信息* * @return 路由信息*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{// 解析请求头的token,从redis中获取用户信息LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 用户信息SysUser user = loginUser.getUser();// 跳转1:查询用户关联的所有菜单List<SysMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId());// 跳转5:根据菜单树型结构构建返回给前端的路由return AjaxResult.success(menuService.buildMenus(menus));
}// 跳转1:查询用户关联的所有菜单
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId)
{List<SysMenu> menus = null;// 如果是admin,直接获取所有菜单if (SecurityUtils.isAdmin(userId)){// 获取所有目录和菜单的集合(不包含按钮)/*select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms,              m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timefrom sys_menu m where m.menu_type in ('M', 'C') and m.status = 0order by m.parent_id, m.order_num*/menus = menuMapper.selectMenuTreeAll();}else{// 获取指定用户拥有的目录和菜单的集合/*select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms,                m.is_frame, m.menu_type, m.icon, m.order_num, m.create_timefrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idleft join sys_user_role ur on rm.role_id = ur.role_idleft join sys_role ro on ur.role_id = ro.role_idleft join sys_user u on ur.user_id = u.user_idwhere u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0order by m.parent_id, m.order_num*/menus = menuMapper.selectMenuTreeByUserId(userId);}// 跳转2:获取每个目录或菜单的子项,从一级目录开始构建树型结构return getChildPerms(menus, 0);
}// 跳转2:获取每个目录或菜单的子项,构建树型结构
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId)
{List<SysMenu> returnList = new ArrayList<SysMenu>();for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();){SysMenu t = (SysMenu) iterator.next();// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点if (t.getParentId() == parentId){// 跳转3:递归构建每个节点的子节点recursionFn(list, t);returnList.add(t);}}return returnList;
}// 跳转3:递归构建每个节点的子节点
private void recursionFn(List<SysMenu> list, SysMenu t)
{// 跳转4:得到t的子节点列表List<SysMenu> childList = getChildList(list, t);// 设置t的子节点列表t.setChildren(childList);// 遍历子节点,递归构建子节点的子节点for (SysMenu tChild : childList){// 如果有子节点if (hasChild(list, tChild)){recursionFn(list, tChild);}}
}// 跳转4:得到t的子节点列表
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t)
{List<SysMenu> tlist = new ArrayList<SysMenu>();Iterator<SysMenu> it = list.iterator();while (it.hasNext()){SysMenu n = (SysMenu) it.next();if (n.getParentId().longValue() == t.getMenuId().longValue()){tlist.add(n);}}return tlist;
}// 跳转5:根据菜单树型结构构建返回给前端的路由
@Override
public List<RouterVo> buildMenus(List<SysMenu> menus)
{List<RouterVo> routers = new LinkedList<RouterVo>();for (SysMenu menu : menus){RouterVo router = new RouterVo();// 设置是否可见router.setHidden("1".equals(menu.getVisible()));// 跳转6:设置路由名称router.setName(getRouteName(menu));// 跳转7:设置路径router.setPath(getRouterPath(menu));// 跳转8:设置组件路径router.setComponent(getComponent(menu));// 设置路由元数据router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));List<SysMenu> cMenus = menu.getChildren();// 如果菜单类型是目录M,并且子节点大于0if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())){router.setAlwaysShow(true);router.setRedirect("noRedirect");// 设置路由子节点router.setChildren(buildMenus(cMenus));}// 如果是最外层,并且类型是菜单C,说明它不应该包含子节点else if (isMeunFrame(menu)){List<RouterVo> childrenList = new ArrayList<RouterVo>();RouterVo children = new RouterVo();children.setPath(menu.getPath());children.setComponent(menu.getComponent());children.setName(StringUtils.capitalize(menu.getPath()));children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));childrenList.add(children);// 直接将该节点作为路由子节点router.setChildren(childrenList);}routers.add(router);}return routers;
}// 跳转6:设置路由名称
public String getRouteName(SysMenu menu)
{// 获取菜单的path字段String routerName = StringUtils.capitalize(menu.getPath());// 非外链并且是一级菜单(类型为菜单)if (isMeunFrame(menu)){routerName = StringUtils.EMPTY;}return routerName;
}// 跳转7:设置路径
public String getRouterPath(SysMenu menu)
{String routerPath = menu.getPath();// 非外链并且是一级目录(类型为目录)if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())&& UserConstants.NO_FRAME.equals(menu.getIsFrame())){routerPath = "/" + menu.getPath();}// 非外链并且是一级目录(类型为菜单)else if (isMeunFrame(menu)){routerPath = "/";}return routerPath;
}// 跳转8:设置组件路径
public String getComponent(SysMenu menu)
{String component = UserConstants.LAYOUT;if (StringUtils.isNotEmpty(menu.getComponent()) && !isMeunFrame(menu)){component = menu.getComponent();}return component;
}

点击具体的目录下的菜单时,前端会根据已经获取的用户权限信息,动态判断菜单中的按钮是否要显示

当然,以上只是做了前端的权限功能,无法防止绕过前端调用无权限的接口。后端的权限校验在每个接口上用@PreAuthorize实现

@PreAuthorize中用了自定义类来校验权限。原理是获取redis中已登录用户信息,判断用户是否有该接口上标注的权限或者角色

/*** RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母* * @author ruoyi*/
@Service("ss")
public class PermissionService
{/** 所有权限标识 */private static final String ALL_PERMISSION = "*:*:*";/** 管理员角色权限标识 */private static final String SUPER_ADMIN = "admin";private static final String ROLE_DELIMETER = ",";private static final String PERMISSION_DELIMETER = ",";@Autowiredprivate TokenService tokenService;/*** 验证用户是否具备某权限* * @param permission 权限字符串* @return 用户是否具备某权限*/public boolean hasPermi(String permission){if (StringUtils.isEmpty(permission)){return false;}LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否不具备某权限,与 hasPermi逻辑相反** @param permission 权限字符串* @return 用户是否不具备某权限*/public boolean lacksPermi(String permission){return hasPermi(permission) != true;}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/public boolean hasAnyPermi(String permissions){if (StringUtils.isEmpty(permissions)){return false;}LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(PERMISSION_DELIMETER)){if (permission != null && hasPermissions(authorities, permission)){return true;}}return false;}/*** 判断用户是否拥有某个角色* * @param role 角色字符串* @return 用户是否具备某角色*/public boolean hasRole(String role){if (StringUtils.isEmpty(role)){return false;}LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (SysRole sysRole : loginUser.getUser().getRoles()){String roleKey = sysRole.getRoleKey();if (SUPER_ADMIN.contains(roleKey) || roleKey.contains(StringUtils.trim(role))){return true;}}return false;}/*** 验证用户是否不具备某角色,与 isRole逻辑相反。** @param role 角色名称* @return 用户是否不具备某角色*/public boolean lacksRole(String role){return hasRole(role) != true;}/*** 验证用户是否具有以下任意一个角色** @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表* @return 用户是否具有以下任意一个角色*/public boolean hasAnyRoles(String roles){if (StringUtils.isEmpty(roles)){return false;}LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (String role : roles.split(ROLE_DELIMETER)){if (hasRole(role)){return true;}}return false;}/*** 判断是否包含权限* * @param permissions 权限列表* @param permission 权限字符串* @return 用户是否具备某权限*/private boolean hasPermissions(Set<String> permissions, String permission){return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}
}

用户-部门

多对1关系,用户表中有dept_id字段指向部门

新增用户的时候需要选择部门

用户-岗位

多对多关系

新增用户时可以选择多个岗位

用户-角色-部门

1.使用演示

这里的部门不是指某个用户所属的部门,而是说角色有能访问到哪些部门的数据的权限,它们是多对多对多关系

在角色管理中,可以对角色关联的部门做配置

可以看到,已经预先定义好了一些部门的数据权限范围,如果想精细化配置,可以选择自定义数据权限,这样可以自由选择角色拥有哪些部门数据权限

我们给测试角色选择研发部门权限,那么再用测试用户登录,就只能看到研发部门的数据了

2.原理分析

在用户修改角色对应的数据权限后,角色表中会保存对应的数据权限枚举标识

并且角色与部门的中间表中也会保存对应数据

然后在用户管理页面,查询用户列表或者部门树型结构时,会根据用户对应的角色的数据权限过滤查询的结果。以查询用户列表为例,首先在Service中会添加@DataScope表示需要根据数据权限对数据进行过滤,注解支持的参数如下:

参数 类型 默认值 描述
deptAlias String 部门表的别名
userAlias String 用户表的别名
/*** 获取用户列表*/
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{startPage();List<SysUser> list = userService.selectUserList(user);return getDataTable(list);
}/*** 根据条件分页查询用户列表* * @param user 用户信息* @return 用户信息集合信息*/
@Override
@DataScope(deptAlias = "d", userAlias = "u") // 部门及用户权限注解,其中d和u用来表示表的别名
public List<SysUser> selectUserList(SysUser user)
{return userMapper.selectUserList(user);
}

然后在对应的数据权限切面类DataScopeAspect中会添加部门数据过滤逻辑:

  1. 获取注解@DataScope信息

  2. 获取当前登录用户的角色信息

  3. 根据角色信息的数据权限类型来判断需要如何过滤,支持以下5种权限类型:

    • 全部数据权限
    • 自定数据权限
    • 部门数据权限
    • 部门及以下数据权限
    • 仅本人数据权限
  4. 将权限过滤的条件sql语句拼接好,然后获取切入方法的参数,将其强转为BaseEntity,然后将权限过滤的sql语句赋值给BaseEntityparams参数

@Aspect
@Component
public class DataScopeAspect
{/*** 全部数据权限*/public static final String DATA_SCOPE_ALL = "1";/*** 自定数据权限*/public static final String DATA_SCOPE_CUSTOM = "2";/*** 部门数据权限*/public static final String DATA_SCOPE_DEPT = "3";/*** 部门及以下数据权限*/public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";/*** 仅本人数据权限*/public static final String DATA_SCOPE_SELF = "5";/*** 数据权限过滤关键字*/public static final String DATA_SCOPE = "dataScope";// 配置织入点:切入到所有标有@DataScope注解的方法@Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)")public void dataScopePointCut(){}// 方法执行前进行增强@Before("dataScopePointCut()")public void doBefore(JoinPoint point) throws Throwable{// 处理增强逻辑handleDataScope(point);}protected void handleDataScope(final JoinPoint joinPoint){// 获得@DataScope注解DataScope controllerDataScope = getAnnotationLog(joinPoint);if (controllerDataScope == null){return;}// 获取当前的用户LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());SysUser currentUser = loginUser.getUser();if (currentUser != null){// 如果是超级管理员,则不过滤数据if (!currentUser.isAdmin()){// 生成过滤部门的条件sql语句,传入@DataScope中定义的用户、部门表别名dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),controllerDataScope.userAlias());}}}/*** 数据范围过滤** @param joinPoint 切点* @param user 用户* @param userAlias 别名*/public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias){StringBuilder sqlString = new StringBuilder();// 遍历用户的所有角色for (SysRole role : user.getRoles()){// 获取角色的数据权限String dataScope = role.getDataScope();// 1.全部数据权限if (DATA_SCOPE_ALL.equals(dataScope)){sqlString = new StringBuilder();break;}// 2.自定数据权限else if (DATA_SCOPE_CUSTOM.equals(dataScope)){// 从角色部门中间表查询要过滤的部门sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,role.getRoleId()));}// 3.部门数据权限else if (DATA_SCOPE_DEPT.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));}// 4.部门及以下数据权限else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",deptAlias, user.getDeptId(), user.getDeptId()));}// 5.仅本人数据权限else if (DATA_SCOPE_SELF.equals(dataScope)){if (StringUtils.isNotBlank(userAlias)){sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));}else{// 数据权限为仅本人且没有userAlias别名不查询任何数据sqlString.append(" OR 1=0 ");}}}if (StringUtils.isNotBlank(sqlString.toString())){// 获取切入方法的参数Object params = joinPoint.getArgs()[0];// 如果参数不为空,并且是BaseEntity的实例if (StringUtils.isNotNull(params) && params instanceof BaseEntity){// 强转为BaseEntityBaseEntity baseEntity = (BaseEntity) params;// 将过滤部门的条件sql语句存入BaseEntity的Map类型的属性params中,key为dataScopebaseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");}}}/*** 是否存在注解,如果存在就获取*/private DataScope getAnnotationLog(JoinPoint joinPoint){Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();if (method != null){return method.getAnnotation(DataScope.class);}return null;}
}

经过上述切面处理后,Service中方法的参数中就保存了过滤部门的sql语句,然后在mapper中实际进行查询时,查询语句的最后取出参数中的sql语句拼接上即可:${params.dataScope}

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user uleft join sys_dept d on u.dept_id = d.dept_idwhere u.del_flag = '0'<if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if><if test="status != null and status != ''">AND u.status = #{status}</if><if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if><if test="beginTime != null and beginTime != ''"><!-- 开始时间检索 -->AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{beginTime},'%y%m%d')</if><if test="endTime != null and endTime != ''"><!-- 结束时间检索 -->AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')</if><if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET (#{deptId},ancestors) ))</if><!-- 数据范围过滤 -->${params.dataScope}
</select>

从上面的原理分析中可以得出结论:

仅实体继承BaseEntity才会进行处理,SQL语句会存放到BaseEntity对象中的params属性中供xml参数params.dataScope获取

若依系统用户权限模型分析相关推荐

  1. 医药采购系统用户管理模型分析

    3 用户管理模型 3.1 模型分析 业务是什么?业务就是用户需求. 系统用户角色:卫生局.卫生院.卫生室.供货商.系统管理员 用户: 登陆系统进行业务操作. 实体分析: 系统用户表SYSUSER: 记 ...

  2. SAAS--03HRM系统用户权限设计概述

    第3章-SaaS系统用户权限设计 学习目标: 理解RBAC模型的基本概念及设计思路 了解SAAS-HRM中权限控制的需求及表结构分析 完成组织机构的基本CRUD操作 完成用户管理的基本CRUD操作 完 ...

  3. 数据分析实战项目3-用Excel做RFM模型用户分层模型分析

    本文数据集来源:忘记了,私聊发数据源 本次目的是将一份用户订单表做RFM模型分析,做好8个维度的用户分层,可以方便运营和销售有目的去跟进重点和非重点客户. 数据源字段如图所示,但本次是订单表,客户有重 ...

  4. 第3章-SaaS-HRM系统用户权限设计

    学习目标: 理解RBAC模型的基本概念及设计思路 了解SAAS-HRM中权限控制的需求及表结构分析 完成组织机构的基本CRUD操作 完成用户管理的基本CRUD操作 完成角色管理的基本CRUD操作 1 ...

  5. SaaS-HRM--第3章-SaaS系统用户权限设计

    学习目标: 理解RBAC模型的基本概念及设计思路 了解SAAS-HRM中权限控制的需求及表结构分析 完成组织机构的基本CRUD操作 完成用户管理的基本CRUD操作 完成角色管理的基本CRUD操作 1组 ...

  6. SAP系统用户权限及岗位(复合)角色对应关系清单输出实例

    近期遇到一个由审计引出的需求,需要在系统中导出用户与其所分配的角色权限的对应关系.笔者公司建立了以岗位为管理单位的复合角色.需要定期输出并检查用户对应的岗位权限是否合理.比如,一个用户有总账会计角色权 ...

  7. 开发标准化软件组件能让程序员在大城市过上体面的生活 -- 多系统用户权限管理标准件开发销售心得体会...

    其实很多人都有顾虑,选择程序员这个行业是否有前途?是否可以长久?我是78年出生的,现在算算已经35岁了,虽然在同学里算不上最成功的,但是也足够不是最差的.生活中该有的都有了,虽然身体没往日那么强壮,但 ...

  8. 电商Sass平台-商城运营后台原型-仓储管理-订单管理-店铺运营-采购管理-数据分析-交易分析-留存分析-客户管理-用户运营-围栏管理-商品管理-流量分析-电商erp后台管理-用户权限-销量分析

    axure作品内容介绍:电商Sass平台-商城运营后台原型-仓储管理-订单管理-平台运营-采购管理-数据分析-交易分析-留存分析-客户管理-用户运营-围栏管理-商品管理-店铺装修-门店管理-商品档案- ...

  9. 计算机化系统用户权限,GMP附录(2015):计算机化系统.doc

    GMP附录(2015):计算机化系统.doc 附件1计算机化系统第一章 范 围第一条 本附录适用于在药品生产质量管理过程中应用的计算机化系统.计算机化系统由一系列硬件和软件组成,以满足特定的功能.第二 ...

最新文章

  1. CSS外边距折叠引发的问题
  2. linux 下解压缩rar文件
  3. python时间序列函数_python时间日期函数与利用pandas进行时间序列处理详解
  4. 数据库保护(数据库备份)Sql Server2012 图形界面操作
  5. Quartz + spring 定时任务常见错误总结
  6. Spring+Quartz实现定时任务
  7. CoNEXT 2018:在Facebook上部署IETF QUIC
  8. java word批注_编写Java批注
  9. 【Python】2.x与3​​.x版本的选用版本间的区别
  10. mongodb基本概念
  11. 量化风控学习:原来评分卡模型的概率是这么校准的!
  12. python全局变量被覆盖的问题
  13. 可能促使您决定创建自定义数据绑定控件的一些原因:
  14. JsonView 使用方法
  15. EAccessViolation
  16. 软件实施人员具备的技能和素养
  17. 购物网站的商品推荐算法有哪些?
  18. 词法解析器 | 从零实现一门语言
  19. 多线程 环形缓冲区_使用环形缓冲区有效登录多线程应用程序
  20. 铁头乔:开源社区那些事

热门文章

  1. 设计分享|基于单片机的矩阵电子琴(汇编)
  2. 毕业之后从事前端工作月薪大概多少?
  3. 2020.10.08丨全长转录组之参考基因组比对
  4. Linux目录、文件管理详解与vi编辑器
  5. 用python解决数学问题
  6. [现代诗]情诗——给网恋中人
  7. biblatex中参考文献期刊名缩写的实现
  8. 做程序界中的死神,获取自己的灵力修养
  9. 基于matlab的运动目标检测,基于matlab的运动目标检测.doc
  10. 【C++实战 】标准库