# lleice-framework **Repository Path**: lleice/lleice-framework ## Basic Information - **Project Name**: lleice-framework - **Description**: springboot轻量级RBAC手脚架,接外包神器 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2020-12-25 - **Last Updated**: 2021-01-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项目背景 因公司内部需要一套单体的RBAC手脚架作为新项目的基础框架,所以才有了这个项目,这个项目目的是能让开发拿到项目后迅速的进入业务开发环节,不用再费时间去搭一套基础的权限框架,也是业余时间外包接单神器. # 运行环境 **JDK8** **Maven 3.5+** **Mysql 5.7.31** **redis 5.0.4** **kafka2.3.1(可选,异步请求日志)** # 项目结构 ![Image text](https://gitee.com/lleice/lleice-framework/raw/master/img/project_structure.jpg) # 功能 + 项目包含一般常用的工具类,包括redis工具类,redisson工具类(redis分布式锁),jackson工具类(替代fastjson),树结构工具类,excel导出导入工具类等等.. + 这个项目包含用户管理,角色管理,资源管理,机构管理,支持一人多角色多部门绑定 + 页面,接口权限通过用户和角色关联 + 数据权限通过用户和机构关联,支持设置数据权限为 全部数据,本部门数据,本部门及其子部门数据,和本人数据 + 数据权限通过切面实现,在查询的时候动态修改sql ~~~Java @Component @Aspect @Order(9) public class DataScopeAspect { @Autowired private ISysDepartmentService iSysDepartmentService; @Pointcut(ScanPackageConstant.DATA_SCOPE_POINTCUT) public void dataScope() { } @Before("dataScope()") public void getDataScope(JoinPoint pjp) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); DataScope annotation = signature.getMethod().getAnnotation(DataScope.class); //部门别名 String deptAlias = annotation.deptAlias(); String userAlias = annotation.userAlias(); //service的第一个查询参数 CommonReq req = (CommonReq) pjp.getArgs()[0]; //从requestHolder获取当前线程用户 UserDto user = RequestHolder.getCurrentUser(); //校验部门id的数据权限 depDataSource(deptAlias, req, user); //校验用户id的数据权限 userDataSource(userAlias, req, user); } /** * 校验用户id的数据权限 * @param userAlias 关联用户id的表别名 * @param req 查询的service的第一个参数 * @param user 用户 * @throws BusinessException */ private void userDataSource(String userAlias, CommonReq req, UserDto user) throws BusinessException { if(!StrUtil.isBlank(userAlias)) { if(null == user) { throw new BusinessException(ExceptionEnum.QXDL); } if(user.getDataScope().equals(Constant.DATA_SCOPE_SELF)) { //本人的数据 StringBuilder builder = new StringBuilder(); builder.append(userAlias).append(".user_id = ").append(user.getId()); req.setSql(builder.toString()); } } } /** * 校验部门id的数据权限 * @param deptAlias 关联部门id的表别名 * @param req 查询的service的第一个参数 * @param user 用户 * @throws BusinessException * @throws NoSuchFieldException * @throws IllegalAccessException */ private void depDataSource(String deptAlias, CommonReq req, UserDto user) throws Exception { if(!StrUtil.isBlank(deptAlias)) { if(null == user) { throw new BusinessException(ExceptionEnum.QXDL); } if(user.getDataScope().equals(Constant.DATA_SCOPE_ALL)) { //全部数据权限,不做限制 req.setSql("1=1"); }else if(user.getDataScope().equals(Constant.DATA_SCOPE_SELF_DEP)) { //部门数据 if(user.getDepartmentList().size() == 0) { //如果没有绑定部门,则查询不到数据 req.setSql("1 = 0"); }else { StringBuilder builder = new StringBuilder(); builder.append(deptAlias).append(".dep_id IN("). append(user.getDepartmentList().stream().map(i -> String.valueOf(i)).collect(Collectors.joining(","))).append(")"); req.setSql(builder.toString()); } }else if(user.getDataScope().equals(Constant.DATA_SCOPE_SELF_CHILD_DEP)) { //部门及以下数据 if(user.getDepartmentList().size() == 0) { //如果没有绑定部门,则查询不到数据 req.setSql("1 = 0"); }else { List departmentList = iSysDepartmentService.getAllDepartmentWithUser(user.getId()); List departmentNodeList = new ArrayList(); getNodes(departmentNodeList, departmentList, false); StringBuilder builder = new StringBuilder(); builder.append(deptAlias).append(".dep_id IN("). append(departmentNodeList.stream().collect(Collectors.joining(","))).append(")"); req.setSql(builder.toString()); } } } } /** * 递归获取所有父子节点 * @param permissionList * @param resourceList */ private void getNodes(List departmentNodeList, List resourceList,Boolean isChild) { for(DepartmentTreeParentDto d : resourceList) { if(d.getIsShow().equals(1) && !isChild) { departmentNodeList.add(d.getId()); }else if(isChild) { departmentNodeList.add(d.getId()); } if(d.getChildren().size()!=0) { getNodes(departmentNodeList, d.getChildren(),d.getIsShow().equals(1)?true:false); } } } } ~~~ # 使用说明 * [编译&启动](#编译) * [启动前修改配置](#启动前要修改的配置) * [登录授权](#登录授权) * [一键改包名生成项目](#修改包名) * [代码生成器](#代码生成器) * [数据库文档生成器](#数据库文档生成器) * [xxl-job定时任务调度器](#xxl-job定时任务调度器) * [系统健康监控&druid监控](#监控) * [多数据源配置](#多数据源) * [常用功能demo](#常用功能demo) * [编码规范](#编码规范) # 编译 项目clone下来后先编译single-parent 再编译common,再编译web-service,最后编译web,因为项目加入了mapstruct,如果在mapstruct增加了对应的映射,在启动项目前要编译一下web-service这个项目,编译后会在target/generated-sources/annotations生成对应的class文件,否则运行的时候会报错找不到对应的实现类 # 启动前要修改的配置 启动项目前需要修改 * application.dev.yml的logging.path * Kafka,mysql,redis,redisson的连接配置 * 若修改了包名,则需要在WebApplication.java 的@ComponentScan加上修改后的包路径,否则不会扫描到新的包路径 # 登录授权 swagger访问地址: http://localhost:8666/doc.html 调用接口前先需要登录,登录前需要调用获取验证码接口 把验证码接口返回的captchaKey和控制台打印的验证码对应登录接口的captchaKey字段和captcha字段。 登录后回返回token,所有有@Permission注解的接口都要在请求头加上 key为Authorization value为登录返回的的token。 __项目常用注解__ * @DataScope(数据权限注解) * @Permission(接口访问权限注解) * @RequestLog(请求日志打印注解,默认使用kafka做异步入库,如不想使用,在配置文件kafka.enable=false即可,则会自动转换成同步插入数据库) # 代码生成器 数据库代码生成器,路径在framework-web-serivce 的/src/test/MybatisGenerator下,运行main方法按提示填入模块名和表名即可。 # 数据库文档生成器 数据库文档生成器,路径在framework-web-serivce 的/src/test/DbDocumentGenerator下,修改输出路径,运行main方法即可。 # xxl-job定时任务调度器 使用前先修改xxl-job-admin-2.2.0.jar的application.properties的配置和logback.xml的日志路径 修改命令: jar xf xxl-job-admin-2.2.0.jar BOOT-INF/classes/application.properties(解压出配置文件) 修改完后,jar uf xxl-job-admin-2.2.0.jar BOOT-INF/classes/application.properties(将修改完的替换原来的) Logback.xml同理。 修改部署xxl-job-admin-2.2.0.jar即可。 * 访问地址:http://localhost:port/xxl-job-admin * 账号密码 admin/123456 * xxl-job默认禁用,如需使用 则修改web端的配置文件,xxl.isEnable = true即可,修改web端的xxl.job.executor.logpath和xxl.job.admin.addresses * 其他使用文档请看https://www.xuxueli.com/xxl-job/ # 监控 系统集成了springboot-monitor 和druid连接池 springboot-monitor * 访问地址:http://localhost:8666/monitor * 访问账号密码 root/root (可在application.yml修改) druid连接池监控后台 * 访问地址:http://localhost:8666/druid * 访问账号密码root/root(可在application-profiles.active.yml修改) # 多数据源 支持一主多从,多主多从,混合多库,例子在SysUserServiceImpl @DS注解上,使用了事务的必须在master上,不能切换数据源,因为该框架是基于springAop的方案来进行多数据源的管理和切换的,要想保证多个库的整体事务则需要分布式事务(如阿里Seata和基于rocketmq的分布式事务)。 在项目的application.dev.yml配置了一主2从的配置(暂时注释了,具体按实际开发需要) 详情文档请看https://dynamic-datasource.com/ # 常用功能demo * 在web端的/controller/other/ExampleController 包含redis分布式锁,excel导出导入,httpClient的使用demo * 在web端的/controller/other/BusinessDatascopeExampleController 包含了数据权限查询的demo * 在web端的/test/java DataSourceEnc 有数据库配置敏感字段加密使用方法 * 在web端的/test/java JacksonDemo 有jackson工具类基本使用demo # 编码规范 项目已经通过阿里代码规范扫描,后续开发尽量按照下面的规范 * 数据库主键统一用bigint类型 (__项目已经解决id过长返回给前端精度丢失问题__) * 使用mapStruct替换BeanUtil.copyProperties(),因为BeanUtil底层是通过大量的反射去做转换的,mapstruct是在编译时生成对应的class文件,直接映射,性能比BeanUtil高很多,缺点是新增一个映射接口时需要重新编译一下否则运行对应的方法时会报找不到实现类的错误 ​ __mapstruct生成的类如下,所以性能比BeanUtil高__ ~~~Java package com.chinaunicom.framework.web.business.mapstruct; import com.chinaunicom.framework.web.business.dto.sys.UserDto; import com.chinaunicom.framework.web.business.entity.sys.SysUser; import javax.annotation.Generated; @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2020-12-17T09:24:33+0800", comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_201 (Oracle Corporation)" ) public class SysUserConverMapperImpl implements SysUserConverMapper { @Override public UserDto sysUserToUserDto(SysUser u) { if ( u == null ) { return null; } UserDto userDto = new UserDto(); userDto.setId( u.getId() ); userDto.setCreateBy( u.getCreateBy() ); userDto.setCreateTime( u.getCreateTime() ); userDto.setUpdateBy( u.getUpdateBy() ); userDto.setUpdateTime( u.getUpdateTime() ); userDto.setIsEnable( u.getIsEnable() ); userDto.setIsDelete( u.getIsDelete() ); userDto.setUserAccount( u.getUserAccount() ); userDto.setUserName( u.getUserName() ); userDto.setUserPassword( u.getUserPassword() ); userDto.setUserSalt( u.getUserSalt() ); userDto.setDataScope( u.getDataScope() ); return userDto; } } ~~~ * 上线打包前不能跳过单元测试,项目的单元测试用例都写在了web端/test/java 下。__单元测试类一定要加@Transactional(rollbackFor = Exception.class)回滚,否则有脏数据!__ * 尽量不要使用fastjson,因为fastjson漏洞较多,而且在jdk1.8后的substring方法做了改写,jackson和fastjson性能差别不大,建议用jackson替换fastjson做json相关的操作(jackson工具类常用demo在test/java/JacksonDemo下) * 上线前需要通过阿里代码规范扫描 * 其他规范请看阿里java开发手册 # 修改包名 __为方便用这个手脚架的同事进行新项目开发,这个功能可以修改包名并且在指定路径生成全新项目。__ * step1 在web-service的/src/test/PackageGenerator类修改对应的信息 ![Image text](https://gitee.com/lleice/lleice-framework/raw/master/img/part1.jpg) * step2 修改完后运行main方法,即可在刚才配置的路径生成包名修改后的新项目 ![Image text](https://gitee.com/lleice/lleice-framework/raw/master/img/part2.jpg) * step3 导入生成的项目 依次编译parent->common->service->web * step4 编译后即可运行 ![Image text](https://gitee.com/lleice/lleice-framework/raw/master/img/part3.jpg)