---
title: 后端手册
prev: false
# next: ./front
---

::: danger 注意
请牢记您是在写 Solon 项目，而不是 SpringBoot 项目。
:::

## 常用注解
注解与 SpringBoot 的注解还是有区别的，详见 [常用注解比较](https://solon.noear.org/article/compare-springboot)。  
常用注解替换：
|  SpringBoot  |  Solon  |
|  :--:  |  :--:  |
|  @Autowired  |  @Inject  |
|  @RestController + @RequestMapping("url")  |  @Controller + @Mapping("url")  |
|  @GetMapping("url")等等  |  @Get等等 + @Mapping("url")  |
|  @Service  |  @Component  |
|  @Value("${key}")  |  @Inject("${key}")  |

## 统一返回结果
项目中的请求（除了 Admin监控 和 接口文档 外），其返回结果都被处理为`org.noear.solon.core.handle.Result<T>`返回给前端。
```json
// 返回结果示例
{
  code: 200,
  description: "操作成功",
  data: null
}
```
详见 全局路由拦截器`com.ruoyi.framework.interceptor.GlobalRouterInterceptor`

## 异常处理
所有的异常统一由`全局过滤器`捕获，并且也会渲染为`Result<T>`返回给前端  
如果您想手动抛出异常，请参照：
```java {2}
if(!StrUtil.isAllNotEmpty(username, password)) {
  throw new ServiceException("账号/密码必须填写");
}
```
详见 全局过滤器`com.ruoyi.framework.filter.GlobalFilter`

## 分页实现
* 前端采用`ElementUI`的分页组件 Pagination。
* 后端采用`Mybatis-Flex`的分页查询，另外集成了轻量级分页插件 [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper)（两种分页方式二选一即可）。
  * `Mybatis-Flex`的分页查询示例
  ```java {3,10}
  @Controller
  @Mapping("system/config")
  public class SysConfigController extends BaseController {

    @Get
    @Mapping("list")
    public PageData list(Page<SysConfig> page, SysConfig sysConfig) {
      QueryWrapper qw = QueryWrapper.create();
      qw.and(SysConfig::getConfigName).like(sysConfig.getConfigName());
      Page<SysConfig> result = sysConfigService.page(page, qw);
      return getPageData(result);
    }

  }
  ```
  * `PageHelper`分页插件示例
  ```java {3,8}
  @Controller
  @Mapping("tool/gen")
  public class GenTableController extends BaseController {

    @Get
    @Mapping("tables")
    public PageData tables(Page<GenTable> page, GenTable genTable) {
      startPage(page);
      List<GenTable> list = genTableService.tables(genTable);
      return getPageData(list);
    }

  }
  ```
  ::: warning 注意
  1、使用分页时，请将控制器`xxxController`继承控制器基类`BaseController`。  
  2、`Pagehelper`只对`XML`的`SQL`分页有效，也就是说需要自己手写`SQL`，不能针对`flex`提供的快捷方法分页，比如`service.list()`。
  :::

## 数据导出
::: warning 注意
数据导出的底层使用的都是`Apache POI`来操作`Office`。  
如果您不需要`poi`功能，可以将相关依赖和代码注释掉，这样打包的体积能减小 17~28 MB。
:::
后端使用`easy-poi`实现实体类数据的导出。在需要被导出的实体类属性添加`@Excel`注解即可。
```java {6,11,15,19}
@Table(value = "sys_config")
public class SysConfig extends BaseEntity {
  private static final long serialVersionUID = 1L;

  /** 参数主键 */
  @Excel(name = "参数主键")
  @Id
  private Long configId;

  /** 参数名称 */
  @Excel(name = "参数名称")
  private String configName;

  /** 系统内置（Y是 N否） */
  @Excel(name = "系统内置", replace = { "是_Y", "否_N" })
  private String configType;

  /** 配置时间 */
  @Excel(name = "配置时间", exportFormat = "yyyy年MM月dd日")
  private Date configTime;

}
```
::: tip 说明
* `name`属性代表列名
* `replace`属性用于值的替换：`configType`的值为`Y`或`N`，导出后将转换为`是`或`否`
* `exportFormat`属性代表导出的时间格式
* 如果需要忽略某一字段的导出，可以删除该字段上的`@Excel`注解，或者在该字段上添加`@ExcelIgnore`注解
* 更多注解属性的使用，请参考`easy-poi`
:::

## 上传下载
* 上传
  * 前端采用`ElementUI`的上传组件 Upload。
  * 后端采用本地文件上传。
  ::: warning 注意
  * 文件上传针对文件类型和文件大小在前端和后端都做了限制。
    * 后端文件上传配置示例（app-xxx.yml）
    ```yaml {8,10}
    ruoyi:
      file:
        # 允许上传的文件大小（单位：MB，设为-1不限制）
        maxSize: 2
        # 允许上传的文件类型
        fileType: [jpg, jpeg, png, doc, docx, xls, xlsx, ppt, txt, pdf, zip]
        # 本地保存路径
        savePath: C:\Users\ruoyi\Desktop
        # 本地文件请求路径前缀
        requestPrefix: http://localhost:90/ruoyi-file
    ```
  * 您需要搭配`代理服务器`来实现本地文件的访问（例如 Nginx）。
    * Nginx配置示例
    ``` {6}
    server {
      listen       90;
      server_name  localhost;

      location /ruoyi-file {
        alias   C:/Users/ruoyi/Desktop/;
        autoindex on;
      }
    }
    ```
  :::
  * 文件上传大小受以下几处限制：
    * 前端上传组件
    * 后端Web服务器的请求体大小：server.request.maxFileSize: 2mb
    * 后端文件上传配置：ruoyi.file.maxSize: 2
    * 前端部署到服务器后（例如 Nginx），Nginx限制的请求体大小
* 下载  
数据导出即采用了文件下载，您可以参照数据导出的实现。

## 参数验证
数据校验能力，详见 [solon-security-validation](https://solon.noear.org/article/225)。
* 控制器基类`BaseController`中已加`@Valid`注解（请将控制器`xxxController`继承控制器基类`BaseController`）
  ```java
  @Valid
  public class BaseController {
  }
  ```
* 在控制器接口的形参前加上`@Validated`注解
  ```java
  @Post
  @Mapping
  public Result add(@Body @Validated SysConfig sysConfig) {
    return toResult(sysConfigService.add(sysConfig));
  }
  ```
* 在实体类的属性上添加相应的校验注解（例如：`@NotBlank(message = "参数名称不能为空")`）
  ```java {5-6}
  @Table(value = "sys_config")
  public class SysConfig extends BaseEntity {
    private static final long serialVersionUID = 1L;

    @NotBlank(message = "参数名称不能为空")
    @Length(max = 100, message = "参数名称不能超过100个字符")
    private String configName;
  }
  ```

## 权限注解
权限采用了 [solon-security-auth](https://solon.noear.org/article/59) & [Sa-Token](https://sa-token.cc/doc.html#/use/jur-auth) 的实现方式。

`solon-security-auth`认证一览：
|  方法  |  描述  |  是否用到  |
|  :--:  |  :--:  |  :--:  |
|  verifyIp()  |  验证客户端IP是否有权访问  |  :heavy_check_mark:  |
|  verifyLogined()  |  验证用户是否登录  |  :heavy_check_mark:  |
|  verifyPath()  |  验证用户是否可访问该路径  |  :x:  |
|  verifyPermissions()  |  验证用户是否具有某字符权限，例如`system:config:list`  |  :heavy_check_mark:  |
|  verifyRoles()  |  验证用户是否具有某角色  |  :x:  |

* 在`SecurityConfig`中定义了鉴权规则，在`AuthProcessorImpl`中定义了鉴权的判定逻辑
  * 首先，所有的请求，校验客户端IP是否在黑名单中（`verifyIp()`）。  
    被拉黑的IP将无法访问任何后端接口。您可以在配置文件中配置IP黑名单（app-xxx.yml）。
    ```yaml
    ruoyi:
      security:
        ips:
          - 1.1.1.1
    ```
  * 其次，所有的请求，校验登录的用户是否有（字符）权限调用接口（`verifyPermissions()`）。
    ::: warning 注意
    在`verifyPermissions()`方法校验（字符）权限前，先会去校验用户是否登录`verifyLogined()`。  
    只有验证用户已经登录了，才会去校验用户是否有（字符）权限。  
    如果您希望某个接口无需登录即可访问，需要配置放行白名单（app-xxx.yml）：
    ```xml {3}
    ruoyi:
      security:
        whites:
          - /captcha
          - /login
          - /logout
          ...
    ```
    :::
* 关于字符权限  
  字符权限形如`system:config:list`，由后台接口上的`@AuthPermissions`注解和菜单中的`perms`权限字符字段共同实现。
  * 带有`@AuthPermissions`注解的接口，说明登录用户需要有此字符权限才能调用该接口
    ```java {1}
    @AuthPermissions("system:config:list")
    @Get
    @Mapping("list")
    public PageData list() {
      ...
    }
    ```
  * 想让某用户可访问上述接口，首先需要在前端创建菜单或按钮并将上述权限字符配置到`perms`权限字符字段  
    而后在角色管理中，给角色A添加该菜单或按钮的权限，最后将角色A赋予此用户即可  
    此用户登录后将只能看到他所拥有的角色所能看到的菜单和按钮

    > 用户登录成功后，已将该用户的所有已有权限放入了登录信息中。  
      由`Sa-Token`的`StpUtil.hasPermission()`方法判定登录用户是否具有接口需要的字符权限。

::: warning 注意
这里的权限指的是用户登录前端后所能看到的菜单和菜单下的操作按钮  
下方的数据权限指的是用户所能查询到的数据范围，比如只能查看自己所在部门的数据
:::

## 数据权限
在实际开发中，需要设置用户只能查看哪些部门的数据，这种情况一般称为数据权限。  
数据权限是通过部门来实现的。
* 在角色管理中设置数据权限，目前支持以下几种：
  * 全部数据权限
  * 本部门数据权限
  * 本部门及以下数据权限
  * 仅本人数据权限
  * 自定义数据权限
* 在需要数据权限控制的控制器接口上添加`@RyDataScope`注解
  ```java {1}
  @RyDataScope(deptAlias = UserConstant.DATA_SCOPE_ALIAS_DEPT, userAlias = UserConstant.DATA_SCOPE_ALIAS_USER)
  @AuthPermissions("system:user:list")
  @Get
  @Mapping("list")
  public PageData list(Page<SysUser> page, SysUser sysUser) {
    startPage(page);
    List<SysUser> list = sysUserService.selectUserList(sysUser);
    return getPageData(list);
  }
  ```
::: tip 提示
* 常量`UserConstant.DATA_SCOPE_ALIAS_DEPT`和`UserConstant.DATA_SCOPE_ALIAS_USER`表示表的别名
* 实体类需要继承基类`BaseEntity`才会处理，处理后的拼接sql语句将存放到`BaseEntity`的`dataScopeSql`字段，然后在`xml`中通过`${参数名.dataScopeSql}`拼接sql语句
* 实现逻辑参考`DataScopeAspect`
:::

## 动态数据源
在实际开发中，经常可能遇到在一个应用中可能需要访问多个数据库的情况，在项目中使用注解来完成此项功能。
* 首先配置文件中已经配置了一个多级数据源
  ```yaml {5,11}
  # mybatis-flex 数据源及 mybatis配置
  mybatis-flex:
    # 动态数据源
    datasource:
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ry-vue?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true
        username: root
        password:
      slave_1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true
        username: root
        password:
  ```
* 在`Controller`控制器层中通过`@UseDataSource`注解指定二级数据源
  ```java {2}
  @AuthPermissions("system:config:list")
  @UseDataSource(DataSourceConstant.DATA_SOURCE_SLAVE_1)
  @Get
  @Mapping("list")
  public PageData list(Page<SysConfig> page, SysConfig sysConfig) {
    ...
  }
  ```

## 限流
使用自定义注解`@RyRateLimit`实现。
* 全局限流：针对于所有的请求（将注解加在全局过滤器的`doIntercept`方法上）
  ```java {3}
  @Component
  public class GlobalRouterInterceptor implements RouterInterceptor {
    @RyRateLimit()  // 全局限流
    @Override
    public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable {
      ...
    }
  }
  ```
* 单接口限流：针对与单个接口（将注解加在控制器上）
  ```java {1}
  @RyRateLimit(count = 10, type = RateLimitType.IP)
  @Get
  @Mapping
  public Result<Map<String, Object>> captcha() {
    return captchaService.getCaptcha();
  }
  ```
::: tip 限流注解属性说明
* `int count() default 100;` 单位时间内允许请求的次数
* `int time() default 60;` 时间，单位：秒
* `RateLimitType type() default RateLimitType.IP;` 限流类型（枚举）：默认、token、ip
* 以上默认配置的效果：从第一次请求开始的60秒内，同一个ip，只能请求100次。60秒内重复请求不会重置限流时间。60秒过后，下一次请求重新计数
:::

## 防重复提交
使用注解`@NoRepeatSubmit`实现。在控制器上添加注解即可。
```java {1}
@NoRepeatSubmit(value = HttpPart.body, message = "请勿重复提交")
@AuthPermissions("system:config:add")
@Post
@Mapping
public Result<Void> add(@Body @Validated SysConfig sysConfig) {
  return toResult(sysConfigService.add(sysConfig));
}
```
::: tip 防重复提交注解属性说明
* `value` 是否是重复提交的判断标准（3种：请求头、请求参数、请求体）
* `seconds` 间隔时间，单位：秒
:::

## 多租户改造
采用指定租户列的方案，基于 [MyBatis-Flex 多租户](https://mybatis-flex.com/zh/core/multi-tenancy.html)。
* 全局配置多租户字段，修改配置文件（app-xxx.yml）
  ```yaml {8-9}
  # mybatis-flex 数据源及 mybatis配置
  mybatis-flex:
    # Mybatis-Flex 的全局配置
    global-config:
      printBanner: true
      # 逻辑删除字段
      logicDeleteColumn: del_flag
      # 租户字段
      tenantColumn: tenant_id
      # 主键配置
      keyConfig:
        # Auto自增
        keyType: Auto
        # value:
  ```
* 在`MybatisFlexConfig`中配置`TenantFactory`
  * `TenantFactory`是用于生产租户id的。实际开发中一般是指定某一字段作为租户的id，所以此处应返回当前租户的id
  * 增删改查的操作将拼接上此处返回的租户id
  ```java
  @Configuration
  public class MybatisFlexConfig {
    ...
    private void globalConfig() {
      ...
      TenantManager.setTenantFactory(new TenantFactory() {
        @Override
        public Object[] getTenantIds() {
          // 通过这里返回当前租户 ID
          return new Object[]{ SecurityUtil.getLoginUser().getTenantId() };
        }
      });
    }
    ...
  }
  ```
::: warning 注意
* 此处在登录用户信息`LoginUser`里设置了租户字段`tenantId`，用户登录的时候给字段赋值租户id
* 这样，使用`IService`和`BaseMapper`提供的增删改查方法时，会在 SQL 中拼接上租户id条件
  * 增：`xxxService.save()`、`xxxMapper.insert()`等方法会自动给租户字段`tenantId`赋值
    ```sql
    INSERT INTO 表名(..., tenant_id, ...) VALUES (..., ?, ...)
    ```
  * 改：`xxxService.update()`、`xxxMapper.update()`等方法会添加租户条件
    ```sql
    UPDATE 表名 SET ... WHERE id = ? AND del_flag = 0 AND tenant_id = ?
    ```
  * 删：`xxxService.remove()`、`xxxMapper.delete()`等方法会添加租户条件
    ```sql
    UPDATE 表名 SET del_flag = 1 WHERE id = ? AND del_flag = 0 AND tenant_id = ?
    ```
  * 查：`xxxService.list()`、`xxxService.page()`、`xxxService.getById()`、`xxxService.count()`、`xxxMapper.select...()`等方法会添加租户条件
    ```sql
    SELECT ... FROM 表名 WHERE tenant_id = ? AND del_flag = 0
    ```
* `TenantFactory`中要求返回一个数组，主要是针对多级租户的情况：  
  假设有如下三级租户：`一级租户 -> 二级租户 -> 三级租户`，二级租户中存的是其所属一级租户的租户id，三级租户存的是其所属二级租户的租户id。  
  那么当一级租户想查询其所拥有的所有三级租户时，查询结果将是空。因为三级租户并没有存一级租户的租户id。  
  因此要想实现，需要先查询一级租户所拥有的所有二级租户的租户id，再用所有二级租户的租户id去查询所有三级租户。  
  所以此时`TenantFactory`返回的应该是`所有二级租户的租户id组成的数组`。此时的 SQL 将变为：
  ```sql
  SELECT ... FROM 表名 WHERE tenant_id IN (?, ?) AND del_flag = 0
  ```
* 如果您需要忽略租户条件，使用`TenantManager.withoutTenantCondition()`：
  ```java
  TenantManager.withoutTenantCondition(() -> xxxService.list(QueryWrapper.create()));

  TenantManager.withoutTenantCondition(() -> xxxMapper.selectListByQuery(QueryWrapper.create()));
  ```
:::
::: danger 注意
:exclamation: 以下情况并不会自动拼接租户条件：
  * 您在`xxxMapper.xml`中手写的 SQL
  * 您在`xxxMapper`中使用 MyBatis 原生功能手写的 SQL（使用`@Select`、`@Insert`、`@Delete`、`@Update`等注解写的 SQL）
:::

## 数据脱敏 & 字段权限
参见 [MyBatis-Flex 数据脱敏](https://mybatis-flex.com/zh/core/mask.html)，[MyBatis-Flex 字段权限](https://mybatis-flex.com/zh/core/columns-permission.html)

## 定时任务 & 异步
定时任务使用`quartz`实现。
* 主启动类添加`@EnableScheduling`和`@EnableAsync`注解
* 具体写法参照`com.ruoyi.schedule.task.RyTask`

定时任务支持持久化：您需要在`ruoyi-admin/src/main/resources/quartz.properties`中配置数据源。
> 注意将`quartz.properties`和`app-dev.yml`中配置为同一个数据库。  
程序（后端）启动时会将`RyTask`中的定时任务写入数据库。  
您可以通过定时任务管理页面对任务进行暂停、启动、删除等操作。
::: warning 注意
* 删除操作仅针对当次启动有效。下次程序（后端）启动时会重新将所有定时任务写入数据库
* 暂停、启动永久生效
* 如果您更改了`cron表达式`并重启，发现不生效，记得清空`quartz`相关的表（以`qrtz_`开头）
:::

使用异步：注入`RyTask`并调用其中的异步方法即可。

## 代码生成
提供了基础的增删查改代码生成功能。
* 代码生成配置
  ```yaml
  ruoyi:
    gen:
      # 作者
      author: ruoyi
      # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
      packageName: com.ruoyi.system
      # 自动去除表前缀，默认是false
      autoRemovePre: false
      # 表前缀（生成类名不会包含表前缀，多个用逗号分隔）
      tablePrefix: demo_
      # 忽略的表前缀（不会查询这些前缀的表）
      ignoreTablePrefix: [qrtz_, gen_, sys_]
  ```
* 登录系统（系统工具 -> 代码生成 -> 导入对应表）
* 代码生成列表中找到需要的表（可预览、编辑、同步、删除生成配置）
* 点击生成代码会得到一个`ruoyi.zip`压缩文件，按照包内目录结构复制到自己的项目中即可

## 系统接口文档
使用`knife4j`实现。
* 接口文档配置（app-xxx.yml）
  ```yaml
  knife4j:
    # 开启增强配置
    enable: true
    # 开启生产环境屏蔽
    production: true
    # 签权
    basic:
      enable: true
      username: admin
      password: 123456
    # 前端UI的个性化配置
    setting:
      enableVersion: true
      enableOpenApi: false
      enableFooter: false
  ```
* 构建文档配置（app-xxx.yml）
  ```yaml
  solon:
    # 接口文档
    docs:
      routes:
        - id: openapi
          groupName: 系统接口
          info:
            title: 接口文档
            description: 在线API文档
  #          termsOfService: https://gitee.com/noear/solon
            contact:
              name: ruoyi
            version: 1.0
          schemes:
            - HTTP
            - HTTPS
          globalResponseInData: true
          globalResult: org.noear.solon.core.handle.Result
          apis:
            - basePackage: com.ruoyi.web.controller
  ```
* 给接口添加注解
  ```java {1,6}
  @Api("系统参数配置")
  @Controller
  @Mapping("system/config")
  public class SysConfigController extends BaseController {

    @ApiOperation("列表查询")
    @Get
    @Mapping("list")
    public PageData list(Page<SysConfig> page, SysConfig sysConfig) {
      QueryWrapper qw = getQW(sysConfig);
      Page<SysConfig> result = sysConfigService.page(page, qw);
      return getPageData(result);
    }

  }
  ```
* 登录系统（系统工具 -> 系统接口），使用接口文档配置中的签权的账号密码登录（knife4j.basic.username/password）

## 系统监控
* 主启动类添加`@EnableAdminServer`和`@EnableAdminClient`注解
* 监控配置（app-xxx.yml）
  ```yaml
  solon:
    # 监控服务端配置
    admin:
      server:
        # 心跳速率，单位：毫秒
        heartbeatInterval: 10000
        # 客户端监控周期，单位：毫秒
        clientMonitorPeriod: 5000
        # 连接超时，单位：毫秒
        connectTimeout: 5000
        # 读取超时，单位：毫秒
        readTimeout: 5000
        # 界面路径（自定义时要以'/'结尾）
        uiPath: /solon-admin/
        # 基础签权（可以多个）
        basicAuth:
          admin: 123456
    # 监控客户端配置
    client:
      # 令牌：监视接口的安全控制
      token: 3C41D632-A070-060C-40D2-6D84B3C07094
      # 连接超时，单位：毫秒
      connectTimeout: 5000
      # 读取超时，单位：毫秒
      readTimeout: 5000
      # 是否向服务端发送敏感信息，如环境变量等
      showSecretInformation: false
    # 监控项配置
    health:
      # 可选: *、disk、cpu、jvm、memory、os、qps（多个用,分隔）
      detector: "*"
  ```
* 登录系统（系统工具 -> Admin控制台），使用监控配置中的基础签权的账号密码登录（solon.admin.server.basicAuth）
