一.概述
后台管理系统大部分功能都是对数据进行查改删,少部分功能还有增加功能。在企业开发过程中,常常
封装通用的接口来处理此类功能,以提高开发效率,但其封装过程会涉及以下几个关键点需要注意处
理:
- 权限的控制(访问、数据、操作)
- 信息安全控制(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);
}
}