JeeSite 4.x

Spring Boot 最好的快速开发平台

业务逻辑层、数据权限、数据事务处理、通用基类

引言

对于业务逻辑层的开发重复代码很多,尽管有代码生成器,但从代码量总的来说还是比较多,所以就有了以下抽象类及工具,对一些常用操作进行封装。

对通用新增、删除、编辑、查询,代码操作进行封装简化。你只需要写你的业务逻辑代码就可以了。

对特有树状结构特有字段如(所有父级编码、所有排序号编码、是否是叶子节点、当前节点层次)进行更新,比如,通过所有父级编码可快速查询到所有子级的数据;通过所有排序号,可快速对整个树结构进行排序;通过是否叶子节点快速得知是否有下级;根据当前层次快速知道当前节点在树中的级别。

对通用数据权限进行简化封装,将颗粒度降到人员身上,支持人员与数据,角色与数据权限定制(角色上的数据权限与人员身上的数据权限为或者关系)。数据权限不仅仅支持公司、部门、角色,还可以通过配置支持你的业务字段数据信息过滤,如订单的区域、内容管理的栏目、故障单类型等等。

对事务处理使用Spring事务@Transactional注解,进行方法级别的事务控制,不用单独处理事务及回滚。如配置传播行为,进行事务继承,子事务,事务回滚行为等,配置隔离级别读取未提交的数据等。

数据权限

相对于旧版本,本次对数据权限这块进行了全面的升级,让数据权限颗粒度细化到人员身上。

支持人员与权限角色与权限:主要是这两张表:js_sys_role_data_scope、js_sys_user_data_scope

权限表中的关键字段的含义,如下:

  • 控制类型:Office:部门;Company:公司、Role:角色;
  • 控制数据:被控制数据权限的数据主键编号,业务表的主键编号;
  • 控制权限:1:拥有的权限(DataScope.CTRL_PERMI_HAVE);2:管理的权限(DataScope.CTRL_PERMI_MANAGE)。

你可以根据需要,扩展控制类型的权限,如:行业、区域、分类;也可以扩展控制权限,如:查询权限、修改权限。

用户管理的数据权限被定义为管理的权限(所以需要从二级管理员里授权设置),如果采用无限级授权(即只能创建本部门的用户,只能分配自己有用的角色,只能分配自己拥有的菜单),这时你需要将用户管理的权限改为拥有的权限,设置(user.adminCtrlPermi=1 v4.1.5+)即可。

角色数据范围:

指定数据权限范围类型,多个角色同时指定,之间为或者关系;人员权限与角色权限之间同样为或者关系,多个位置设置权限,依照最大权限为主。

  • 未设置:忽略这个角色的数据源权限设置
  • 全部数据:可以查看全部数据,无需控制权限
  • 自定义数据:打对勾,跨部门、跨机构的情况设置数据权限
  • 本部门数据:仅控制当前用户所在部门(所在机构)的数据权限
  • 本公司数据:仅控制当前用户所在公司的数据权限
  • 本部门和本公司:控制当前用户所在部门和所在公司的数据权限

控制业务范围:

在 v4.1.6 中新增的功能,当你使用 addFilter 权限过滤的时候可以指定适应的业务范围,不指定代表所有生效。

举例如:有的功能可以看本部门,有的功能可以看本公司,有的功能可以看全部数据,分功能去控制权限。

新增的业务范围可以从字典管理中找到 sys_role_biz_scope 类型进行添加。

第一步

/**
 * 添加数据权限过滤条件
 */
public void addDataScopeFilter(T entity){
	// 举例1:公司数据权限过滤,实体类@Table注解extWhereKeys="dsf"
	company.getSqlMap().getDataScope().addFilter("dsf", "Company",
			"a.company_code", DataScope.CTRL_PERMI_HAVE);
	// 举例2:部门数据权限过滤,实体类@Table注解extWhereKeys="dsf"
	office.getSqlMap().getDataScope().addFilter("dsf", "Office",
			"a.office_code", DataScope.CTRL_PERMI_HAVE);
	// 举例3:角色数据权限过滤,实体类@Table注解extWhereKeys="dsf"
	role.getSqlMap().getDataScope().addFilter("dsf", "Role",
			"a.role_code", DataScope.CTRL_PERMI_HAVE);
	// 举例4:用户、员工(自己创建的)数据权限根据部门过滤,实体类@Table注解extWhereKeys="dsfOffice"
	empUser.getSqlMap().getDataScope().addFilter("dsfOffice", "Office", 
			"e.office_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);
	// 举例5:用户、员工(自己创建的)数据权限根据公司过滤,实体类@Table注解extWhereKeys="dsfCompany"
	empUser.getSqlMap().getDataScope().addFilter("dsfCompany", "Company", 
			"e.company_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);
}

注意:在调用 findList 或 findPage 之前去手动调用 addDataScopeFilter 方法,才可生效。例如:

@RequiresPermissions("user")
@RequestMapping(value = "listData")
@ResponseBody
public Page<EmpUser> listData(EmpUser empUser, HttpServletRequest request, HttpServletResponse response) {
	empUser.setPage(new Page<>(request, response));
	empUserService.addDataScopeFilter(empUser); 	// 调用数据权限过滤方法
	Page<EmpUser> page = empUserService.findPage(empUser);
	return page;
}

第二步

1)在 \@Table 注解中调用如下:

  1. 采用 EXISTS方式调用 : @Table(extWhereKeys="dsf")
  2. 采用JOIN方式调用 : @Table(extFromKeys="dsf",extWhereKeys="dsf")

2)MyBatis Mapper 中调用如下两种方式:

  1. 采用 EXISTS方式调用 : 将 ${sqlMap.dsf} 放在 Where
  2. 采用JOIN方式调用 : 将 ${sqlMap.dsfFrom} 放在 From 后 ,将 ${sqlMap.dsfWhere} 放在 Where

调用 API 接口

/**
 * 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
 * @param sqlMapKey sqlMap的键值,举例:如设置“dsf”数据范围过滤,则:<br>
 * 		exists方式对应:sqlMap.dsf; join方式对应:sqlMap.dsfFrom 和 sqlMap.dsfWhere
 * @param ctrlTypes 控制类型,多个用“,”隔开,举例:<br>
 *		控制角色:Role<br>
 *		控制部门:Office<br>
 *		控制公司:Company
 * @param bizCtrlDataFields 业务表对应过滤表别名和加权字段,多个使用“,”分隔。<br>
 * 		长度必须与tableTypes保持一致,举例:<br>
 * 		业务表控制角色:a.role_code<br>
 * 		业务表控制部门:a.office_code<br>
 * 		业务表控制公司:a.company_code
 * @param bizCtrlUserField 业务表对应过滤表别名和用户字段,用于过滤只可以查看本人数据。<br>
 * 		不设置的话,如果没有范围权限,则查不到任何数据,举例:<br>
 * 		业务表:a.create_by
 * @param ctrlPermi 拥有的数据权限:DataScope.CTRL_PERMI_HAVE、可管理的数据权限:DataScope.CTRL_PERMI_HAVE
 * @param bizScope 业务范围  v4.1.6
 * @see example
 * 		1)在Service中调用如下两种方式:<br>
 * 			// 添加数据权限过滤条件(控制角色)<br>
 * 			entity.getSqlMap().getDataScope().addFilter("dsf", "Role", <br>
 * 		 			"a.role_code", DataScope.CTRL_PERMI_HAVE);<br>
 * 			// 添加数据权限过滤条件(控制部门)<br>
 * 			entity.getSqlMap().getDataScope().addFilter("dsf", "Office", <br>
 * 					"a.office_code", DataScope.CTRL_PERMI_HAVE);<br>
 * 			// 添加数据权限过滤条件(控制公司)<br>
 * 			entity.getSqlMap().getDataScope().addFilter("dsf", "Company", <br>
 * 					"a.company_code", DataScope.CTRL_PERMI_HAVE);<br>
 * 			// 添加数据权限过滤条件(如果没有部门权限,则控制当前用户)<br>
 * 			entity.getSqlMap().getDataScope().addFilter("dsf", "Office", <br>
 * 					"a.office_code", "a.create_by", DataScope.CTRL_PERMI_HAVE);<br>
 * 		2)在 \@Table 注解中调用如下:<br>
 * 			采用 EXISTS 方式调用 : \@Table(extWhereKeys="dsf")<br>
 * 			采用JOIN 方式调用	 : \@Table(extFromKeys="dsf", extWhereKeys="dsf")
 * 		3)MyBatis Mapper 中调用如下两种方式:<br>
 * 			采用 EXISTS 方式调用 : 将  ${sqlMap.dsf} 放在Where语句里<br>
 * 			采用 JOIN 方式调用	 : 将  ${sqlMap.dsfFrom} 放在From后 ,将  ${sqlMap.dsfWhere} 放在Where语句里
 */
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes, String bizCtrlDataFields, 
			String bizCtrlUserField, String ctrlPermi, String bizScope);

/**
 * 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
 * 详见:{@link QueryDataScope#addFilter(String, String, String, String, String)}
 */
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes,
			String bizCtrlDataFields, String ctrlPermi) {
	addFilter(sqlMapKey, ctrlTypes, bizCtrlDataFields, null, ctrlPermi);
	return this;
}

/**
 * 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
 * 详见:{@link QueryDataScope#addFilter(String, String, String, String, String, String)}
 */
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes,
			String bizCtrlDataFields, String bizCtrlUserField, String ctrlPermi) {
	addFilter(sqlMapKey, ctrlTypes, bizCtrlDataFields, bizCtrlUserField, ctrlPermi, null);
	return this;
}

/**
 * 添加到数据范围过滤条件(如果之前sqlMapKey已经存在,则使用OR增加到该条件)
 * @param sqlMapKey sqlMap的键值,举例:如设置“dsf”数据范围过滤,则:exists方式对应:sqlMap.dsf
 * @param sqlWhere 具体的Where子句。
 */
public QueryDataScope addFilter(String sqlMapKey, String sqlWhere);

/**
 * 清理数据范围过滤条件
 * @param sqlMapKey 要清理的数据过滤条件
 */
public QueryDataScope clearFilter(String sqlMapKey);

数据库事务

事务管理对于企业应用来说是至关重要的,当出现异常情况,它也可以保证数据的一致性。

JeeSite主要使用Spring的@Transactional注解,也称声明式事务管理,是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需通过基于@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。

JeeSite支持分布式事务,在 yml 配置文件里将 jdbc.jta.enabled 设置为 true 即可。

注解属性

属性 类型 描述
传播性(propagation) 枚举型 可选的传播性设置(默认值:Propagation.REQUIRED)
隔离性(isolation) 枚举型 可选的隔离性级别(默认值:Isolation.ISOLATION_DEFAULT)
只读性(readOnly) 布尔型 读写型事务 vs. 只读型事务
超时(timeout) int型 事务超时(以秒为单位)
回滚异常类(rollbackFor) Class 类的实例,必须是Throwable 的子类 异常类,遇到时必须进行回滚。默认情况下checked exceptions不进行回滚,仅unchecked exceptions(即RuntimeException的子类)才进行事务回滚。
不回滚异常类(noRollbackFor) Class 类的实例,必须是Throwable的子类 异常类,遇到时必须不回滚。

事务传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在Propagation定义中包括了如下几个表示传播行为的常量:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务,这是默认值。
  • Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。Isolation 接口中定义了五个表示隔离级别的常量:

  • Isolation.DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是Isolation.READ_COMMITTED。
  • Isolation.READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
  • Isolation.READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  • Isolation.REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
  • Isolation.SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

基类及接口的继承关系

TreeService -> TreeQueryService -> CrudService -> QueryService -> BaseService

TreeDao -> CrudDao -> QueryDao -> BaseDao

TreeEntity -> DataEntity -> BaseEntity

QueryService 查询抽象基类

/**
 * 新建实体对象
 * @return
 */
protected T newEntity();

/**
 * 新建实体对象(带一个String构造参数)
 * @return
 */
protected T newEntity(String id);

/**
 * 获取单条数据
 * @param id 主键
 * @return
 */
public T get(String id);

/**
 * 获取单条数据
 * @param entity
 * @return
 */
public T get(T entity);

/**
 * 获取单条数据,如果获取不到,则实例化一个空实体
 * @param id 主键编号
 * @param isNewRecord 如果是新记录,则验证主键编号是否存在。
 * 					     如果存在抛出ValidationException异常。
 * @return
 */
public T get(String id, boolean isNewRecord);

/**
 * 获取单条数据,如果获取不到,则实例化一个空实体(多个主键情况下调用)
 * @param pkClass 主键类型数组
 * @param pkValue 主键数据值数组
 * @param isNewRecord 如果是新记录,则验证主键编号是否存在。
 * 					     如果存在抛出ValidationException异常。
 * @return
 */
public T get(Class<?>[] pkClass, Object[] pkValue, boolean isNewRecord);

/**
 * 列表查询数据
 * @param entity
 * @return
 */
public List<T> findList(T entity);

/**
 * 分页查询数据
 * @param page 分页对象
 * @param entity
 * @return
 */
public Page<T> findPage(Page<T> page, T entity);

/**
 * 查询列表总数
 * @param entity
 * @return
 */
public long findCount(T entity);

CrudService 增删改抽象基类

该类继承QueryService抽象类

/**
 * 保存数据(插入或更新)
 * @param entity
 */
@Transactional(readOnly = false)
public void save(T entity)

/**
 * 插入数据
 * @param entity
 */
@Transactional(readOnly = false)
public void insert(T entity);

/**
 * 更新数据
 * @param entity
 */
@Transactional(readOnly = false)
public void update(T entity);

/**
 * 更新状态(级联更新子节点)
 * @param entity
 */
@Transactional(readOnly = false)
public void updateStatus(T entity);

/**
 * 删除数据
 * @param entity
 */
@Transactional(readOnly = false)
public void delete(T entity);

TreeService 树结构抽象基类

该类继承CrudService抽象类

/**
 * 根据父节点获取子节点最后一条记录
 */
public T getLastByParentCode(T entity);

/**
 * 保存数据(插入或更新)
 * 实现自动保存字段:所有父级编号、所有排序号、是否是叶子节点、节点的层次级别等数据
 * 实现级联更新所有子节点数据:同父级自动保存字段
 */
@Transactional(readOnly = false)
public void save(T entity);

/**
 * 更新parent_codes、tree_sorts、tree_level字段值
 */
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateParentCodes(T entity);

/**
 * 更新当前节点排序号
 */
@Transactional(readOnly = false)
public void updateTreeSort(T entity);

/**
 * 更新tree_leaf字段值
 */
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateTreeLeaf(T entity);

/**
 * 修正本表树结构的所有父级编号
 * 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段
 */
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes();

/**
 * 按父级编码修正树结构的所有父级编号
 * 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段
 */
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes(String parentCode);

/**
 * 预留接口事件,更新子节点
 * @param childEntity 当前操作节点的子节点
 * @param parentEntity 当前操作节点
 */
protected void updateChildNode(T childEntity, T parentEntity);

/**
 * 更新状态(级联更新子节点)
 * @param entity
 */
@Transactional(readOnly = false)
public void updateStatus(T entity);

/**
 * 删除数据(级联删除子节点)
 * @param entity
 */
@Transactional(readOnly = false)
public void delete(T entity);

/**
 * 将不同级别无序的树列表进行排序,前提是sourcelist每一级是有序的<br>
 * 举例如下:<br>
 * 	List<T> targetList = Lists.newArrayList();<br>
 * 	List<T> sourcelist = service.findList(category);<br>
 * 	service.execTreeSort(targetList, sourcelist, "0");<br>
 * @param sourceList 源数据列表
 * @param targetList 目标数据列表
 * @param parentCode 目标数据列表的顶级节点
 */
public void execTreeSort(List<T> sourceList, List<T> targetList, String parentCode);

/**
 * 将简单列表code,parentCode转换为嵌套列表形式code,childList[code,childList[...]]<br>
 * 举例如下:<br>
 * 	List<T> targetList = Lists.newArrayList();<br>
 * 	List<T> sourcelist = service.findList(category);<br>
 * 	service.execChildListBulid(targetList, sourcelist, "0");<br>
 * @param sourceList 源数据列表
 * @param targetList 目标数据列表
 * @param parentCode 目标数据列表的顶级节点
 */
public void execChildListBulid(List<T> sourceList, List<T> targetList, String parentCode);

其它使用技巧

分页逻辑说明

Entity部分:

public Custom extends BaseEntity{
	private String code;
	private String name;	
}

Dao部分:

public List<Custom> findListByCodeAndName(Custom custom);

Service部分:

Page<Custom> page = new Page<Custom>();
Custom custom = new Custom();
custom.setCode("code");
custom.setName("name");
custom.setPage(page); // 给实体设置 page 参数即可自动分页
page.setList(dao.findListByCodeAndName(custom));
System.out.println(page.getList());	 // 获取当前页数据
System.out.println(page.getCount()); // 获取总条数

MAP参数分页

Dao部分:

public List<Map<String, Object>> findListForMap(Map<String, Object> params);

Mapper部分:

<select id="findListForMap" resultType="map">
	SELECT * FROM test_data a
	<where>
		<if test="testInput != null and testInput != ''">
			AND a.test_input = #{testInput}
		</if>
	</where>
	<if test="page != null and page.orderBy != null and page.orderBy != ''">
		ORDER BY ${page.orderBy}
	</if>
</select>

Service部分:

Page<Map<String, Object>> pageMap = new Page<>();
Map<String, Object> params = MapUtils.newHashMap();
params.put("testInput", "123");
params.put("page", pageMap); // 给 Map 设置 page 参数即可自动分页
pageMap.setList(dao.findListForMap(params));
System.out.println(pageMap.getList());	// 获取当前页数据
System.out.println(pageMap.getCount()); // 获取总条数

覆写内置Service

@Service
@Transactional(readOnly=true)
public class UserServiceImpl extends UserServiceSupport{
	
	public UserServiceImpl() {
		super.setEntityClass(User.class);
	}
	
	/**
	 * 更新个人信息
	 */
	@Override
	@Transactional(readOnly=false)
	public void updateUserInfo(User user){
		String avatarBase64 = user.getAvatarBase64();
		if (StringUtils.isNotBlank(avatarBase64)){
			if ("EMPTY".equals(avatarBase64)){
				user.setAvatar(StringUtils.EMPTY);
			}else{
				String imageUrl = "avatar/"+user.getCorpCode()+"/"
						+user.getUserType()+"/"+user.getUserCode()
						+"."+FileUtils.getFileExtensionByImageBase64(avatarBase64);
				String fileName = Global.getUserfilesBaseDir(imageUrl);
				FileUtils.writeToFileByImageBase64(fileName, avatarBase64);
				user.setAvatar(Global.USERFILES_BASE_URL + imageUrl);
			}
		}
		super.updateUserInfo(user);
	}
	
}

关注 JeeSite 公众号,了解最新动态