【SaToken使用】SpringBoot整合SaToken(二)关于数据权限


点击查看上一篇: 【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权

前言

数据范围:1 所有数据、2 所在部门及子部门数据、3 所在部门数据、4 仅本人数据、5 自定义数据


关于数据权限,一直没有一个很好的通用的解决方案。

方式一:

之前有试过参考若依的那个用自定义注解+aop去实现数据权限,然后觉得通用性不高,无法适用于我自己的项目的业务场景,而且我用的mybatisplus,不是联表的话是不会去xml自己写sql语句的,而是直接用QueryWrapper,用这种实现方式是没办法把拼接好的sql放到QueryWrapper中做条件的。

方式二:

然后我又找了种方法,也是用注解和aop,但它是需要解析sql,把数据范围条件拼接到sql中,变成了一条新的sql语句去执行的。尝试了下这种方法,确实可以,不管是在xml自己写的SQL也好,还是用户mybatisplus自带的查询也好,甚至有分页都不用担心。但是面对复杂SQL比如子查询、嵌套查询等,要想把条件给拼接到正确的位置上去,太复杂了,那些group by、where、order by这些关键词位置判断就够呛,要是有子查询、嵌套查询,都有where,那过滤条件放到哪个where里面?这也是比较头疼的事。所以我感觉这种方法也不太行,代码太啰嗦。

方式三:

后面做项目的时候,我上面两种方法都没采用,我的想法是两种方法最后都是在原有的SQL上拼接条件,那为什么不把权限条件像其他条件一样,直接写在SQL里面,而是用注解、拦截器去拼接?就比如查询用户列表,a用户的数据权限是只能查看本部门的数据,所以权限条件是 dept_id=a用户的部门 ,然后列表有个搜索框,搜索姓名的,那么在mybatis中SQL就是

select *
from sys_user
<where>
	<if test="name != null"> and name=#{name} </if>
	<if test="dataScope != null"> and ${dataScope} </if>
</where>

如果不是自己写SQL,而是用QueryWrapper的话,那也简单

public PageInfo<SysUser> page(SysQuery query){
    QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
    if (StrUtil.isNotBlank(query.getName())){
        queryWrapper.eq("name",query.getName());
    }
    String apply = queryApplyScope("dept",null,1); // 获取权限条件
    if (StrUtil.isNotBlank(apply)){
        queryWrapper.apply(apply);
    }
    return new PageInfo<>(baseMapper.selectList(queryWrapper));
}

像这样把权限条件和其他筛选条件一样,直接用不是挺好的嘛?还是说为了做到低侵入、无侵入?不管哪种方法,得看是否适用当前业务场景,不过如果没要求无侵入的话,直接用肯定是比注解+拦截器拼接的方式更灵活的。下面详细说下第二种实现和第三种实现,可以参考下。

方式二实现、自定义注解+拦截器拦截SQL,实现改变原SQL


参考文章:springboot自定义注解+mybatis拦截器实现数据权限

1、自定义注解 DataScope

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    /** 拼接的条件是取 部门id 还是 部门name(1 id 2 name) */
    int type() default 1;
    
    /** 查询的字段名 */
    String fieldName() default "";
    
    /** 查询本人数据时使用的的字段名 */
    String createBy() default "";
}

2、数据过滤处理 DataScopeAspect

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.entity.sys.SysUser;
import com.service.sys.SysUserService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * 数据过滤处理
 */
@Aspect
@Component
public class DataScopeAspect {
	/** 用于存储过滤条件的SQL */
    public static final ThreadLocal DATA_AUTH_THREAD_LOCAL = new ThreadLocal();

    @Resource
    private SysUserService sysUserService;

    // 配置织入点
    @Pointcut("@annotation(DataScope)")
    public void dataScopePointCut() { }

    @Before("dataScopePointCut()")
    public void doBefore(JoinPoint point){
        DATA_AUTH_THREAD_LOCAL.remove();
        handleDataAuthScope(point);
    }

    @After("@annotation(DataScope)")
    public void doAfter(){
        //清空数据权限拼接SQL
        DATA_AUTH_THREAD_LOCAL.remove();
    }

    /**
     * 处理数据权限
     * @param joinPoint     切面
     */
    protected void handleDataAuthScope(final JoinPoint joinPoint) {
        // 获取当前的用户
        SysUser user = (SysUser) StpUtil.getSession().get("user");
        if (null == user) {
            return;
        }
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        DataScope dataScope = method.getAnnotation(DataScope.class);
        // 获取当前用户要拼接的数据权限条件
        String str = sysUserService.queryApplyScope(dataScope.fieldName(), dataScope.createBy(), dataScope.type());
        if (StrUtil.isNotBlank(str)){
            //设置数据权限拼接sql
            DATA_AUTH_THREAD_LOCAL.set(str);
        }
    }
}

3、拦截SQL语句,并将过滤条件拼接到原SQL中

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;

/**
 * 拦截SQL语句,根据处理好的数据权限条件拼接在原SQL后,组成新的SQL语句
 */
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public class DataScopeInterceptor implements Interceptor {

    /** 分组 */
    private static final String GROUP_BY = "GROUP BY";
    /** 排序 */
    private static final String ORDER_BY = "ORDER BY";
    /** 分页 */
    private static final String LIMIT = "LIMIT";
    /** where */
    private static final String WHERE = "WHERE";
    /** where条件 */
    private static final String WHERE_CONDITION = " WHERE 1=1 ";

    /**
     * 拦截sql
     * @param invocation
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        if (SqlCommandType.SELECT == sqlCommandType) {
            // 获取过滤条件
            Object dataAuthSql = DataScopeAspect.DATA_AUTH_THREAD_LOCAL.get();
            //如果添加数据权限
            if (null != dataAuthSql) {
                BoundSql boundSql = (BoundSql) invocation.getArgs()[5];
                StringBuilder newSqlBuilder = new StringBuilder();
                // 获取到原始sql语句
                String mSql = boundSql.getSql();
                System.out.println("原始SQL:\t"+mSql);
                mSql = addWhere(mSql);
                //重写sql语句 前面拼接数据权限语句
                if (mSql.indexOf(GROUP_BY) > 0) {
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(GROUP_BY)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(GROUP_BY), mSql.length()));
                } else if (mSql.indexOf(ORDER_BY) > 0) {
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(ORDER_BY)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(ORDER_BY), mSql.length()));
                } else if (mSql.indexOf(LIMIT) > 0) {
                    newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(LIMIT)))
                            .append(" and ")
                            .append(dataAuthSql.toString())
                            .append(" ")
                            .append(mSql.substring(mSql.lastIndexOf(LIMIT), mSql.length()));
                } else if (mSql.indexOf(WHERE) > 0) {
                    newSqlBuilder.append(mSql)
                            .append(" and ")
                            .append(dataAuthSql.toString());
                } else {
                    newSqlBuilder.append(mSql)
                            .append(WHERE_CONDITION)
                            .append(" and ")
                            .append(dataAuthSql.toString());
                }
                System.out.println("增强后的SQL:\t"+newSqlBuilder.toString());
                //通过反射修改sql语句
                Field field = boundSql.getClass().getDeclaredField("sql");
                field.setAccessible(true);
                field.set(boundSql, newSqlBuilder.toString());
            }
        }
        return invocation.proceed();
    }

    /**
     * 添加where关键字
     * @param sql sql语句
     * @return sql
     */
    private String addWhere(String sql) {
        if (sql.indexOf(WHERE) >= 0) {
            return sql;
        }
        StringBuilder newSqlBuilder = new StringBuilder();
        if (sql.indexOf(GROUP_BY) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(GROUP_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(GROUP_BY), sql.length()));
        } else if (sql.indexOf(ORDER_BY) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(ORDER_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(ORDER_BY), sql.length()));
        } else if (sql.indexOf(LIMIT) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(LIMIT)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(LIMIT), sql.length()));
        } else {
            newSqlBuilder.append(sql).append(" ").append(WHERE_CONDITION);
        }
        return newSqlBuilder.toString();
    }
}

4、用户列表

/**
 * 用户列表
 */
@GetMapping("/list")
@DataScope(fieldName="b.id",createBy="a.id")
@SaCheckPermission("system:user:view")
public ResultVo list(SysQuery query) {
    return ResultUtil.success(service.page(query));
}
<select id="page" resultType="com.entity.sys.SysUser">
    SELECT a.id,user_name,real_name,dept_id,name deptName
    FROM sys_user a
    LEFT JOIN sys_dept b on a.dept_id=b.id
    <where>
        <if test="code != null">
            AND a.user_name like concat('%', #{code}, '%')
        </if>
        <if test="name != null">
            AND a.real_name like concat('%', #{name}, '%')
        </if>
        <if test="parentId != null">
            AND b.id=#{parentId}
        </if>
    </where>
    ORDER BY a.create_time desc
</select>

以上方式亲测单表、联表查询没什么问题,也不影响分页,但是遇上子查询、嵌套查询就不好使了。

方式三实现

关于我上面说的第二种、第三种过滤方式,都是要事先设置好拼接条件,用的时候直接拿就可以了,下面详细说下我的过滤条件是怎么设置的。

1、思路

1、在登录的时候,就根据角色获取查询范围,获取到条件值,然后保存到用户信息中(用户信息是保存到redis中的)。


2、查询的时候就取出当前用户里面的条件值,然后根据你要用哪个字段过滤,把条件值拼接到要过滤的字段中。


3、这样就得到了最终得权限条件,就可以直接把这一串字符之间拼接到查询的SQL语句中。

2、举栗

用户陈怡,她的角色是部门负责人,数据范围是 2(所在部门及子部门数据),当她登录的时候,获取她部门及子部门的所有ID(name),设置到 SysUser 对象的 deptAndSubId 属性中;她登录进去后要查看用户列表,那么这个用户列表要过滤数据是根据哪个字段过滤呢?在执行SQL之前就要设置好要过滤的字段为 dept_id ,然后拿出redis存储她本人用户信息里的 deptAndSubId 属性值,将值拼接到要过滤的字段中:“FIND_IN_SET(dept_id,‘2,3,4,5’)” ,得到这串条件字符后,再拼接到 where 中。

3、实现

实体对象

/**
 * 角色信息表
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_role")
public class SysRole extends BaseEntity {

    /**
     * 角色权限字符
     */
    @TableField("role_key")
    private String roleKey;

    /**
     * 角色名称
     */
    @TableField("role_name")
    private String roleName;

    /**
     * 菜单树是否展开(0折叠 1展开 )
     */
    @TableField("is_open")
    private Boolean isOpen;

    /**
     * 数据范围(1 所有数据 2 所在部门及子部门数据 3 所在部门数据 4 仅本人数据 5 自定义数据)
     */
    @TableField("data_scope")
    private Integer dataScope;

    /**
     * 备注
     */
    @TableField("remark")
    private String remark;

    /**
     * 菜单组
     */
    private transient String [] menuIds;

    /**
     * 部门组
     */
    private transient String [] deptIds;
}

/**
 * 用户信息
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class SysUser extends BaseEntity {

    /**
     * 用户名称
     */
    @TableField("user_name")
    private String userName;

    /**
     * 用户名称
     */
    @TableField("real_name")
    private String realName;

    /**
     * 所属部门
     */
    @TableField("dept_id")
    private String deptId;

    /**
     * 密码
     */
    @TableField("password")
    private String password;

    /**
     * 盐值
     */
    @TableField("salt")
    private String salt;

    /**
     * 头像
     */
    @TableField("picture")
    private String picture;

    /**
     * 最后登录时间
     */
    @TableField("login_date")
    private String loginDate;

    /**
     * 密码更改时间
     */
    @TableField("update_pwd_time")
    private LocalDateTime updatePwdTime;

    private transient String deptName;
    private transient String parentId;

    /** 角色组 */
    private transient String [] roleIds;
    private transient List<SysUserRole.UserRoleVo> roleList;
    /** 查询的数据范围条件 */
    private transient Map<String,String> queryDataScope;
}

/**
 * 用户角色关联
 */
@Data
@TableName("sys_user_role")
public class SysUserRole{

    /**
     * 用户ID
     */
    @TableId("user_id")
    private String userId;

    /**
     * 角色ID
     */
    @TableField("role_id")
    private String roleId;

    public SysUserRole(String userId, String roleId) {
        this.userId = userId;
        this.roleId = roleId;
    }

    public SysUserRole() {}

    @Data
    public static class UserRoleVo{
        private String roleId;
        private String roleName;
        private String roleKey;
        private Integer dataScope;
    }
}

登录

/** 登录 */
@PostMapping("/login")
public ResultVo login(String userName, String password, String code,HttpServletRequest request){
    try {
        // ......省略其他代码
        SysUser user = passwordErrorNum(userName, password); // 校验用户名和密码是否正确
        sysUserService.setDataScope(user); // 设置用户的数据范围查询条件
        StpUtil.login(user.getId());
        String tokenValue = StpUtil.getTokenValue();
        sysLog.setInfo(userName+"登录成功");
        StpUtil.getSession().set("user",user);
        return ResultUtil.success(tokenValue);
    } catch (ExceptionVo e) {
        return ResultUtil.error(e.getCode(),e.getMessage());
    }catch (Exception e) {
        e.printStackTrace();
        return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION);
    }
}

SysUserService.setDataScope(),获取条件值

/**
 * 2、3、4 的数据范围
 */
private void setDataScope(Map<String,String> queryDataScope,List<String> collect,SysUser user){
     if (collect.contains("2")){ // 获取所在部门及子部门的id和name
         Map<String, String> child = sysDeptService.getChildIdAndName(user.getDeptId());
         if (child != null){
             queryDataScope.put("deptAndSubId",child.get("id"));
             queryDataScope.put("deptAndSubName",child.get("name"));
         }
         queryDataScope.put("deptId",user.getDeptId());
         queryDataScope.put("deptName",user.getDeptName());
     }else if (collect.contains("3")){ // 获取所在部门的id和name
         queryDataScope.put("deptId",user.getDeptId());
         queryDataScope.put("deptName",user.getDeptName());
     }else { // 仅本人数据:获取当前用户id,以及获取所在部门的id和name
         queryDataScope.put("createBy",user.getId());
         queryDataScope.put("deptId",user.getDeptId());
         queryDataScope.put("deptName",user.getDeptName());
     }
 }

 /**
  * 设置用户的数据范围查询条件:
  * 问题:关于部门id和name,既然设置了id为什么还要设置name呢?
  * 解释:像用户与部门、角色与部门之间的关联用的是部门id关联。
  *      但是因为我自己这边的业务的原因,有一些表数据的所属部门并不是用部门id来进行关联的,而是直接用部门名称关联,
  *      为了方便以后加业务,拓展,所以我这里把部门名称也一起设置了,这个可以根据实际情况选择要不要设置部门名称,
  */
 public void setDataScope(SysUser user){
     Map<String,String> queryDataScope = new HashMap<>();
     List<SysUserRole.UserRoleVo> roleList = user.getRoleList();// 获取用户所属角色列表
     // 获取所有角色的数据范围
     List<String> collect = roleList.stream().map(t -> String.valueOf(t.getDataScope())).collect(Collectors.toList());
     // 1-4这几个数字里,数字从小到大,数据范围是越来越小的,比如1包含了2、3、4的范围,2包含了3、4的范围,以此类推。
     // 所以只要在用户拥有的数据范围里面,直接取最小的那个数字的范围就行,其他数字的范围就不用取了。
     if (collect.contains("1")) { // 全部数据就不设置过滤条件了
         return;
     }
     if (collect.contains("5")){// 自定义数据:根据角色id获取关联的部门id
         // 包含了5则是取其他数字的数据范围和5的数据范围的并集
         for (int i = 0; i < collect.size(); i++) {
             if ("5".equals(collect.get(i))){
                 String roleId = roleList.get(i).getRoleId();
                 SysQuery query = new SysQuery();
                 query.setId(roleId);
                 List<String> idList = sysDeptService.selectIdListByRoleId(query);   // 根据角色id获取部门id
                 List<SysDept> deptList = sysDeptService.listByIds(idList);  // 根据部门id查询部门列表
                 if (!deptList.isEmpty()) {
                     List<String> c = deptList.stream().map(SysDept::getName).collect(Collectors.toList());
                     queryDataScope.put("customName",StrUtil.join(",", c));
                 }
                 queryDataScope.put("customId",StrUtil.join(",", idList));
             }
         }
         queryDataScope.put("dataScope","5");
         // 其他数字的数据范围
         setDataScope(queryDataScope,collect,user);
     }else {
         setDataScope(queryDataScope,collect,user);
     }
     // 将过滤值设置到用户信息中
     user.setQueryDataScope(queryDataScope);
 }

BaseService.queryApplyScope(),设置查询条件语句

/**
 * 根据 当前用户的数据范围 拼接查询条件语句(这里拼接的查询条件也可以用于单表 QueryWrapper 查询)
 * @param fieldName  查询的字段名
 * @param createBy   查询本人数据时使用的的字段名
 * @param type       拼接的条件是取 部门id 还是 部门name(1 id 2 name)
 */
public String queryApplyScope(String fieldName,String createBy,int type){
    StringBuilder sqlString = new StringBuilder();
    SysUser user = getCurrentUser();
    if (user.getQueryDataScope() != null){
        Map<String, String> scope = user.getQueryDataScope();
        if (type == 1){ // 用部门id字段过滤
            if (StrUtil.isNotBlank(scope.get("dataScope"))){ // 这里不为空,说明数据范围是 5(自定义数据)
            	// 5 和其他数字的范围取并集,用 or 连接,并且它们的外层不要忘了用括号括起来
                sqlString.append(StrUtil.format("(FIND_IN_SET({},'{}')",fieldName, scope.get("customId")));
                if (StrUtil.isNotBlank(scope.get("deptAndSubId"))) {
                    sqlString.append(StrUtil.format(" or FIND_IN_SET({},'{}')",fieldName, scope.get("deptAndSubId")));
                }else if (StrUtil.isNotBlank(scope.get("deptId"))){
                    sqlString.append(StrUtil.format(" or {} = '{}'",fieldName, scope.get("deptId")));
                }
                sqlString.append(")");
            }else {
                // 拼接条件为 部门id时,所在部门及子部门数据用 FIND_IN_SET 查询;所在部门数据直接用等于查询;
                if (StrUtil.isNotBlank(scope.get("deptAndSubId"))) {
                    sqlString.append(StrUtil.format("FIND_IN_SET({},'{}')",fieldName, scope.get("deptAndSubId")));
                }else if (StrUtil.isNotBlank(scope.get("deptId"))){
                    sqlString.append(StrUtil.format("{} = '{}'",fieldName, scope.get("deptId")));
                }
            }
        }else { // 用部门名称字段过滤
            if (StrUtil.isNotBlank(scope.get("dataScope"))){
                sqlString.append(StrUtil.format("(FIND_IN_SET({},'{}')",fieldName, scope.get("customName")));
                if (StrUtil.isNotBlank(scope.get("deptAndSubName"))) {
                    sqlString.append(StrUtil.format(" or FIND_IN_SET({},'{}')",fieldName, scope.get("deptAndSubName")));
                }else if (StrUtil.isNotBlank(scope.get("deptName"))){
                    sqlString.append(StrUtil.format(" or {} = '{}'",fieldName, scope.get("deptName")));
                }
                sqlString.append(")");
            }else {
                // 拼接条件为 部门name时,所在部门及子部门数据用 FIND_IN_SET 查询;所在部门数据直接用等于查询;
                if (StrUtil.isNotBlank(scope.get("deptAndSubName"))){
                    sqlString.append(StrUtil.format("FIND_IN_SET({},'{}')",fieldName, scope.get("deptAndSubName")));
                }else if (StrUtil.isNotBlank(scope.get("deptName"))){
                    sqlString.append(StrUtil.format("{} = '{}'",fieldName, scope.get("deptName")));
                }
            }
        }
        // 这里还要判断 createBy 字段不为空,是因为有的表它不需要根据本人过滤,只用根据部门过滤
        if (StrUtil.isNotBlank(createBy) && StrUtil.isNotBlank(scope.get("createBy"))){
        	// 仅本人数据直接用等于查询;
            if (StrUtil.isNotBlank(sqlString)){
                sqlString.append(StrUtil.format(" and {} = '{}'",createBy, scope.get("createBy")));
            }else {
                sqlString.append(StrUtil.format("{} = '{}'",createBy, scope.get("createBy")));
            }
        }
    }
    return sqlString.toString();
}

用户列表(不使用注解)

@GetMapping("/list")
@SaCheckPermission("system:user:view")
public ResultVo list(SysQuery query) {
    return ResultUtil.success(service.page(query));
}
/**
 * 用户列表分页
 */
public PageInfo<SysUser> page(SysQuery queryVo) {
    PageHelper.startPage(queryVo.getPageNum(),queryVo.getPageSize());
    Map<String, Object> map = sysParams(queryVo);
    String sqlStr = queryApplyScope("b.id","a.id", 1);
    if (StrUtil.isNotBlank(sqlStr)){
        map.put("dataScope",sqlStr);
    }
    List<SysUser> list = baseMapper.page(map);
    return new PageInfo<>(list);
}
<select id="page" resultType="com.entity.sys.SysUser">
    SELECT a.id,user_name,real_name,dept_id,name deptName
    FROM sys_user a
    LEFT JOIN sys_dept b on a.dept_id=b.id
    <where>
        <if test="code != null">
            AND a.user_name like concat('%', #{code}, '%')
        </if>
        <if test="name != null">
            AND a.real_name like concat('%', #{name}, '%')
        </if>
        <if test="parentId != null">
            AND b.id=#{parentId}
        </if>
        <if test="dataScope != null">
            AND ${dataScope}
        </if>
    </where>
    ORDER BY a.create_time desc
</select>

三、源码

完整的数据库和代码都在源码里。如果你觉得我的文章(源码)对你有帮助的话,可以帮我点个赞再走哦~ヾ(•ω•`)o

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

符华-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值