Bootstrap

通用封装笔记

一.概述

后台管理系统大部分功能都是对数据进行查改删,少部分功能还有增加功能。在企业开发过程中,常常
封装通用的接口来处理此类功能,以提高开发效率,但其封装过程会涉及以下几个关键点需要注意处
理:

  • 权限的控制(访问、数据、操作)
  • 信息安全控制(SQL注入的问题)

对于部分功能需求归纳总结为以下功能点:

  • 新增:能够通用插入授权的数据表数据
  • 修改:能够通用修改指定的字段数据
  • 删除:能够通用删除指定条件的数据
  • 列表:能够通用加载不同数据表的数据,并支持条件查询、分页等
  • 在通用功能中,对于以上功能可做前后置增强业务处理
  • 在通用功能中,可控制不同数据表的操作权限

二.分析

持久层

  • 通过动态SQL或者SQL拼接方式,实现通用的持久层

业务层

  • 定义操作权限类,控制数据表的基本操作权限(是否允许CRUD),每次操作之前需要检查此表
  • 每次操作之前检查当前登录人员是否有此功能操作权限(此步要与RBAC进行基础,此例不做实
    现)
  • 参数校验(比如更新时需要查询条件)
  • 参数合法性校验(放置SQL注入)
  • 前置处理逻辑调用(前置结果可影响程序的继续执行,此例作为练习内容,在此步实现)
  • 数据持久化操作
  • 后置处理逻辑调用(后置结果可影响返回结果)
  • 返回结果

三.接口定义

通用操作功能,主要实现CRUD的功能:

  • 列表接口:用户加载数据表分页数据,并提供字段搜索功能
  • 删除接口:用于删除满足条件的某行数据(考虑安全,每次操作只能删除一条数据)
  • 修改和新增接口:用于新增和修改数据表数据

四.各类的封装

tips:

  • CommonTableEnum:类用于定义哪些数据表可支持通用操作,及其开发的CRUD功能
  • CommonWhereDto:类用于接收前端传入的修改字段或者查询条件参数
  • CommonDto:类用于接收前端传入的接口参数(包括CRUD接口的参数)
  • CommonDao:持久层动态SQL拼接实现
  • CommonServiceimpl:类用于实现CRUD的通用逻辑处理
  • CommonController:实现通用接口的控制层定义
  • BaseCommonFilter:定义后置处理的接口方法(后置增加类的BeanName定义为
  • CommonTableEnum名称即可)

1.CommonTableEnum
创建该类,基本权限的控制定义为枚举类,在后台管理系统中,涉及以下几个功能,要使用到相关数据表,可用通过操作接口实现:

  • 频道/敏感词管理:对平台上的频道进行CRUD操作,因此需要对AD_CHANNEL/ AD_SENSITIVE表允许CRUD操作
  • 爬虫文章审核和自媒体文章审核功能:需要加载人工审核的列表数据,并修改审核状态,所以需要开通CL_NEWS/WM_NEWS的list和修改权限
@Getter
public enum CommonTableEnum {
	AD_CHANNEL("*",true,true,true,true),
	AD_SENSITIVE("*",true,true,true,true),
	// APP用户端
	AP_ARTICLE("*",true,false,false,false),
	AP_ARTICLE_CONFIG("*",true,false,true,false),
	AP_USER("*",true,false,true,false),
	CL_NEWS("*",true,false,true,false),
	WM_NEWS("*",true,false,true,false);

	String filed;
	boolean list;//开启列表权限?
	boolean add;//开启增加权限?
	boolean update;//开启修改权限?
	boolean delete;//开启删除权限?
	
	CommonTableEnum(String filed,boolean list,boolean add,boolean update,boolean delete){
		this.filed = filed;
		this.list = list;
		this.add = add;
		this.update = update;
		this.delete = delete;
	}
}

2.CommonWhereDto
创建该类,条件封装了操作的字段filed、条件的操作类型(eq、like)、字段值value。

@Data
public class CommonWhereDto {
	private String filed;
	private String type="eq";
	private String value;
}

3.CommonDto
创建该类,封装分页、操作模式、操作对象、查询条件、修改字段等参数信息。

@Data
public class CommonDto {
	private Integer size;
	private Integer page;
	// 操作模式add 新增,edit编辑
	private String model;
	// 操作的对象
	private CommonTableEnum name;
	// 查询的条件
	private List<CommonWhereDto> where;
	// 修改的字段
	private List<CommonWhereDto> sets;
}

4.BaseCommonFilter
创建该接口,用于定义后置处理的接口约束和公用默认方法。此定义可增加和扩展通用操作不用数据表的业务功能。

/**
* 通用过滤器的过滤类
*/
public interface BaseCommonFilter {
	void doListAfter(AdUser user, CommonDto dto);
	void doUpdateAfter(AdUser user, CommonDto dto);
	void doInsertAfter(AdUser user, CommonDto dto);
	void doDeleteAfter(AdUser user, CommonDto dto);
	/**
	* 获取更新字段里面的值
	* @param field
	* @param dto
	* @return
	*/
	default CommonWhereDto findUpdateValue(String field, CommonDto dto){
		if(dto!=null){
			for (CommonWhereDto cw : dto.getSets()){
				if(field.equals(cw.getFiled())){
					return cw;
			}
		}
	}
	return null;
}
	/**
	* 获取查询字段里面的值
	* @param field
	* @param dto
	* @return
	*/
	default CommonWhereDto findWhereValue(String field,CommonDto dto){
		if(dto!=null){
			for (CommonWhereDto cw : dto.getWhere()){
				if(field.equals(cw.getFiled())){
					return cw;
				}
			}
		}
		return null;
	}
}

5.CommonDao
创建该类,使用注解方式提供通用的列表查询方法和增删改功能,并使用$拼接字符串方式,生成动态的SQL语句。

/**
* 如果在mycat分库分表的情况下,可以提供多个方法支持不同分片算法的数据CRUD,这里较为常用的查询,既非复合分片的CRUD实现
*/
@Mapper
public interface CommonDao {
	@Select("select * from ${tableName} limit #{start},#{size}")
	@ResultType(HashMap.class)
	List<HashMap> list(@Param("tableName") String tableName, @Param("start") int start, @Param("size") int size);
	
	@Select("select count(*) from ${tableName} ")
	@ResultType(Integer.class)
	int listCount(@Param("tableName") String tableName);
	
	@Select("select * from ${tableName} where 1=1 ${where} limit #{start},#{size}")
	@ResultType(HashMap.class)
	List<HashMap> listForWhere(@Param("tableName") String tableName, @Param("where") String where, @Param("start") int start, @Param("size") int size);
	
	@Select("select count(*) from ${tableName} where 1=1 ${where}")
	@ResultType(Integer.class)
	int listCountForWhere(@Param("tableName") String tableName, @Param("where") String where);
	
	@Update("update ${tableName} set ${sets} where 1=1 ${where}")
	@ResultType(Integer.class)
	int update(@Param("tableName") String tableName, @Param("where") String where, @Param("sets") String sets);
	
	@Insert("insert into ${tableName} (${fileds}) values (${values})")
	@ResultType(Integer.class)
	int insert(@Param("tableName") String tableName, @Param("fileds") String fileds, @Param("values") String values);
	
	@Delete("delete from ${tableName} where 1=1 ${where} limit 1")
	@ResultType(Integer.class)
	int delete(@Param("tableName") String tableName, @Param("where") String where);
}

6.通用的service
创建通用的service接口

public interface CommonService {
	/**
	* 加载通用的数据列表
	* @param dto
	* @return
	*/
	ResponseResult list(CommonDto dto);

	/**
	* 修改通用的数据列表
	* @param dto
	* @return
	*/
	ResponseResult update(CommonDto dto);
	
	/**
	* 删除通用的数据列表
	* @param dto
	* @return
	*/
	ResponseResult delete(CommonDto dto);
}

创建serviceimpl实现类,实现通用CRUD的业务处理。

  • 辅助方法
    • parseValue:方法过滤和替换引起SQL注入的关键字符;注filed和value都需要过滤
    • doFilter:用于依据name查找对应后置增加Bean,如果查得,则执行对应增强的后置处理
    • getWhere:拼接where条件字符串,支持like、between、=等条件查询
    • getSets:拼接修改的set语句的值
    • getInsertSql:拼接新增Sql的字段和值得字符串
  • 接口方法
    • delete:方法必须有查询条件,删除成功之后执行doFilter方法后置处理
    • update:方法通过mode参数判别是新增还是修改,并检查对应的必要参数后调用对应方法
    • addData:判断权限后,调用数据插入方法,插入成功之后再调用后置处理
    • updateData:判断权限后,调用更新方法,修改成功后,再调用后置处理方法
    • list:方法判断权限后,计算分页参数和查询条件,并获取列表和总的及数,最后调用后置方法
/**
* 通用操作类
*/
@Service
public class CommonServiceImpl implements CommonService {
	@Autowired
	CommonDao commonDao;
	@Autowired
	ApplicationContext context;
	
	/**
	* 删除通用的数据列表
	* @param dto
	* @return
	*/
	public ResponseResult delete(CommonDto dto){
		String where = getWhere(dto);
		String tableName =dto.getName().name().toLowerCase();
		if(!dto.getName().isDelete()){
			return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
		}
		if(StringUtils.isEmpty(where)){
			return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "删除条件不合法");
		}
		int temp = commonDao.delete(tableName, where);
		if(temp>0){
			doFilter(dto, "delete");
		}
		return ResponseResult.okResult(temp);
	}
	
	/**
	* 修改通用的数据列表
	* @param dto
	* @return
	*/
	public ResponseResult update(CommonDto dto){
		String model = dto.getModel();
		String where = getWhere(dto);
		String tableName =dto.getName().name().toLowerCase();
		if("add".equals(model)){
			if(StringUtils.isNotEmpty(where)){
				return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "新增数据不能设置条件");
			}else {
				return addData(dto, tableName);
			}
		}else {
			if(StringUtils.isEmpty(where)){
				return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"修改条件不能为空");
			}else {
				return updateData(dto, tableName, where);
			}
		}
	}
	
	/**
	* 插入一条数据
	* @param dto
	* @return
	*/
	private ResponseResult addData(CommonDto dto, String tableName){
		String[] sql = getInsertSql(dto);
		if(!dto.getName().isAdd()){
			return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
		}
		if(sql == null){
			return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "传入的参数值不能为空");
		}
		int temp =commonDao.insert(tableName,sql[0],sql[1]);
		if(temp > 0){
			doFilter(dto, "add");
		}
		return ResponseResult.okResult(temp);
	}
	
	/**
	* 更新一条数据
	* @param dto
	* @return
	*/
	private ResponseResult updateData(CommonDto dto,String tableName,String where){
		String sets = getSets(dto);
		if(!dto.getName().isUpdate()){
			return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
		}
		if(StringUtils.isEmpty(sets)){
			return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "修改的参数值不能为空");
		}
		int temp = commonDao.update(tableName, where, sets);
		if(temp > 0){
			doFilter(dto, "update");
		}
		return ResponseResult.okResult(temp);
	}
	
	/**
	* 通用列表加载方法
	* @param dto
	* @return
	*/
	public ResponseResult list(CommonDto dto){
		if(!dto.getName().isList()){
			return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
		}
		String where = getWhere(dto);
		String tableName =dto.getName().name().toLowerCase();
		List<?> list = null;
		int total = 0;
		int start = (dto.getPage()-1)*dto.getSize();
		if(start<-1)start=0;
		if(StringUtils.isEmpty(where)){
			list = commonDao.list(tableName, start, dto.getSize());
			total = commonDao.listCount(tableName);
		}else{
			list = commonDao.listForWhere(tableName, where, start, dto.getSize());
			total = commonDao.listCountForWhere(tableName, where);
		}
		Map map = Maps.newHashMap();
		map.put("list",list);
		map.put("total",total);
		doFilter(dto,"list");
		return ResponseResult.okResult(map);
	}

	/**
	* 拼接查询条件
	* @param dto
	* @return
	*/
	private String getWhere(CommonDto dto){
		StringBuffer where = new StringBuffer();
		if(dto.getWhere()!=null){
			dto.getWhere().stream().forEach(w->{
				// 字段不为空,并且字段和值不能相等(防止凭借真条件)
				if(StringUtils.isNotEmpty(w.getFiled())&&StringUtils.isNotEmpty(w.getValue())&&!w.getFiled().equalsIgnoreCase(w.getValue())) {
					String tempF = parseValue(w.getFiled());
					String tempV = parseValue(w.getValue());
					if(!tempF.matches("\\d*")&&!tempF.equalsIgnoreCase(tempV)) {
						if ("eq".equals(w.getType())) {
							where.append(" and ").append(tempF).append("=\'").append(tempV).append("\'");
						}
						if ("like".equals(w.getType())) {
							where.append(" and ").append(tempF).append(" like\'%").append(tempV).append("%\'");
						}
						if ("between".equals(w.getType())) {
							String temp[] = tempV.split(",");
							where.append(" and ").append(tempF).append(temp[0]).append(" and ").append(temp[1]);
						}
					}
				}
			});
		}
		return where.toString();
	}
	
	/**
	* 拼接修改条件
	* @param dto
	* @return
	*/
	private String getSets(CommonDto dto){
		StringBuffer sets = new StringBuffer();
		AtomicInteger count = new AtomicInteger();
		if(dto.getSets()!=null){
			dto.getSets().stream().forEach(w->{
				if(StringUtils.isEmpty(w.getValue())){
					count.incrementAndGet();
				}else {
					String tempF = parseValue(w.getFiled());
					String tempV = parseValue(w.getValue());
					if(!tempF.matches("\\d*")&&!tempF.equalsIgnoreCase(tempV)) {
						if (sets.length() > 0) {
							sets.append(",");
						}
						sets.append(tempF).append("=\'").append(tempV).append("\'");
					}
				}
			});
		}
		if(count.get()>0){
			return null;
		}
		return sets.toString();
	}
	
	/**
	* 拼接插入字符串
	* @param dto
	* @return
	*/
	private String[] getInsertSql(CommonDto dto){
		StringBuffer fileds = new StringBuffer();
		StringBuffer values = new StringBuffer();
		AtomicInteger count = new AtomicInteger();
		if(dto.getSets()!=null){
			dto.getSets().stream().forEach(w->{
				if(StringUtils.isEmpty(w.getValue())){
					count.incrementAndGet();
				}else {
					String tempF = parseValue(w.getFiled());
					String tempV = parseValue(w.getValue());
					if(!tempF.matches("\\d*")&&!tempF.equalsIgnoreCase(tempV)) {
						if (fileds.length() > 0) {
							fileds.append(",");
							values.append(",");
						}
						fileds.append(tempF);
						values.append("\'").append(tempV).append("\'");
					}
				}
			});
		}
		if(count.get()>0){
			return null;
		}
		return new String[]{fileds.toString(),values.toString()};
	}
	
	/**
	* SQL 单引号('),分号(;) 和 注释符号(--)
	* @param value
	* @return
	*/
	public String parseValue(String value){
		if(StringUtils.isNotEmpty(value)){
			return value.replaceAll(".*([';#%]+|(--)+).*", "");
		}
		return value;
	}
	
	private void doFilter(CommonDto dto,String name){
		try{
			BaseCommonFilter baseCommonFilter = findFilter(dto);
			if(baseCommonFilter!=null){
				AdUser adUser = AdminThreadLocalUtils.getUser();
				if("insert".equals(name)){
					baseCommonFilter.doInsertAfter(adUser,dto);
				}
				if("update".equals(name)){
					baseCommonFilter.doUpdateAfter(adUser,dto);
				}
				if("list".equals(name)){
					baseCommonFilter.doListAfter(adUser,dto);
				}
				if("delete".equals(name)){
					baseCommonFilter.doDeleteAfter(adUser,dto);
				}
			}
		}catch (Exception e){
			e.printStackTrace();
		}
	}
	
	private BaseCommonFilter findFilter(CommonDto dto){
		String name = dto.getName().name();
			if(context.containsBean(name)) {
				return context.getBean(name, BaseCommonFilter.class);
			}
		return null;
	}
}

7.CommonController
创建该类,实现Service的调用。

@RestController
@RequestMapping("/api/v1/admin/common")
public class CommonController{
	@Autowired
	private CommonService commonService;
	
	@PostMapping("/list")
	public ResponseResult list(@RequestBody CommonDto dto) {
		return commonService.list(dto);
	}
	
	@PostMapping("/update")
	public ResponseResult update(@RequestBody CommonDto dto) {
		return commonService.update(dto);
	}
	
	@PostMapping("/delete")
	public ResponseResult delete(@RequestBody CommonDto dto) {
		return commonService.delete(dto);
	}
}

;