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

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

## 依赖关系
先来看一下基础模块间的依赖关系。
![common](../../.vuepress/public/cloud/common.png)
* 图中蓝色块为`ruoyi-common`下的模块，橙色块为`ruoyi-api`下的模块。
* 图中箭头的含义是：例如`datasource`指向了`security`，说明`datasource`中引入了`security`。  
  根据依赖的传递原则，当您引入了`datasource`后，就无需再引入`security`。  
  `log`和`swagger`真正独立，不引入其它模块，也不被其它模块引入。
* 您在创建可启动的微服务模块时，应当按需引入图中的部分基础模块。可选择引入的基础模块有：`log`、`datasource`、`swagger`、`security`。
  * 其中必须引入的模块是`security`。因为它实现了鉴权、全局路由拦截器、全局过滤器等。
  * 为了打印日志，您也应当引入`log`。
  * `datasource`和`swagger`按需引入。
* 新建的服务模块按照`是否需要数据源`，分为：
  * 需要数据源：您应该引入`datasource`、`log`
  * 不需要数据源：您应该引入`security`、`log`

## 演示模块
`ruoyi-modules-demo`是演示模块。如果您想创建新的服务模块，请参照此演示模块进行创建，您也可以直接改造演示模块。

## 配置自动刷新
所有服务模块的配置文件都在`Nacos`中，以下配置支持自动刷新（autoRefreshed = true）：
* 验证码配置：ruoyi.auth.*
* 文件配置：ruoyi.file.*
* 代码生成配置：ruoyi.gen.*
::: warning 注意
由于 Solon 内部机制的设计，如果您在`Nacos`中配置了数组类型的配置，然后在后端使用数组或List接收，并开启配置自动刷新，当配置变化后，会导致接收的配置参数重复导致混乱。

因此如果您想实现数组类型的配置自动刷新，请配置为字符串形式，并在后端使用字符串切割为数组（参考文件配置或代码生成配置的自动刷新）。
:::

## 常用注解
注解与 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.common.security.interceptor.GlobalRouterInterceptor`

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

## 分页实现
* 前端采用`ElementUI`的分页组件 Pagination。
* 后端采用`Mybatis-Flex`的分页查询，另外集成了轻量级分页插件 [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper)（两种分页方式二选一即可）。
  * `Mybatis-Flex`的分页查询示例
  ```java {3,10}
  @Controller
  @Mapping("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("table")
  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。
  * 后端支持本地文件上传及云存储（`ruoyi-modules-file`模块）。
  ::: warning 注意
  * 文件上传针对文件类型和文件大小在前端和后端都做了限制。
    * 后端文件上传配置示例
    ```yaml
    ruoyi:
      file:
        # 使用的文件服务类型: s3 本地文件存储、aliyun 阿里云对象存储、qiniu 七牛云对象存储、minio
        # 请前往 file 模块的 pom.xml 注释掉不用的文件服务依赖
        type: s3
        # 允许上传的文件大小（单位：MB，设为-1不限制）
        maxSize: 2
        # 允许上传的文件类型
        fileType: jpg, jpeg, png, doc, docx, xls, xlsx, ppt, txt, pdf, zip

    solon:
      cloud:
        # 本地文件 
        file:
          s3:
            file:
              default: local_bucket
              buckets:
                local_bucket:
                  endpoint: C:\Users\ruoyi\Desktop
                  requestPrefix: http://localhost:90/ruoyi-file
                # more_bucket:
                #   regionId: cn-east-1
                #   endpoint: http://s3.ladydaily.com
                #   accessKey: iWeU7cOoPLRokg2Hdat0jGQC
                #   secretKey: ZZIH6mT4VLAy68mVP80F7LiB5SpSEM7N
        # 阿里云 OSS
        aliyun:
          oss:
            file:
              endpoint: oss-cn-xxx.aliyuncs.com
              bucket: world-data-dev
              accessKey: iWeU7cOoPLRokg2Hdat0jGQC
              secretKey: ZZIH6mT4VLAy68mVP80F7LiB5SpSEM7N
        # 七牛云 OSS
        qiniu:
          kodo:
            file:
              regionId: cn-east-1
              endpoint: https://xxx.yyy.zzz
              requestPrefix: https://aaa.bbb.ccc
              bucket: world-data-dev
              accessKey: iWeU7cOoPLRokg2Hdat0jGQC
              secretKey: ZZIH6mT4VLAy68mVP80F7LiB5SpSEM7N
        # minio
        minio:
          file:
            regionId: cn-east-1
            endpoint: https://play.min.io
            bucket: asiatrip
            accessKey: iWeU7cOoPLRokg2Hdat0jGQC
            secretKey: ZZIH6mT4VLAy68mVP80F7LiB5SpSEM7N
    ```
    * 默认使用本地文书上传。如果您想更换为其他上传方式（比如阿里云OSS），请先前往`ruoyi-modules-file`模块的`pom.xml`文件或`build.gradle`文件，打开阿里云OSS的注释，并注释掉其它的；并且修改上方文件上传配置：`ruoyi.file.type: aliyun`，重启`ruoyi-modules-file`服务。
    ```xml
    <!-- Maven 版 -->
    <dependencies>
      <!-- 本地文件 -->
      <!--<dependency>-->
        <!--<groupId>org.noear</groupId>-->
        <!--<artifactId>file-s3-solon-cloud-plugin</artifactId>-->
      <!--</dependency>-->

      <!-- 阿里云 OSS -->
      <dependency>
        <groupId>org.noear</groupId>
        <artifactId>aliyun-oss-solon-cloud-plugin</artifactId>
      </dependency>

      <!-- 七牛云 OSS -->
      <!--<dependency>-->
        <!--<groupId>org.noear</groupId>-->
        <!--<artifactId>qiniu-kodo-solon-cloud-plugin</artifactId>-->
      <!--</dependency>-->

      <!-- minio（minio基于 minio8 sdk，minio7基于 minio7 sdk） -->
      <!--<dependency>-->
        <!--<groupId>org.noear</groupId>-->
        <!--<artifactId>minio-solon-cloud-plugin</artifactId>-->
      <!--</dependency>-->
      <!--<dependency>-->
        <!--<groupId>org.noear</groupId>-->
        <!--<artifactId>minio7-solon-cloud-plugin</artifactId>-->
      <!--</dependency>-->
    </dependencies>
    ```
    ```gradle
    //  Gradle 版
    dependencies {
      ...
      //  implementation 'org.noear:file-s3-solon-cloud-plugin'
      implementation 'org.noear:aliyun-oss-solon-cloud-plugin'
      //  implementation 'org.noear:qiniu-kodo-solon-cloud-plugin'
      //  implementation 'org.noear:minio-solon-cloud-plugin'
      //  implementation 'org.noear:minio7-solon-cloud-plugin'
    }
    ```
  * 如果您使用的是本地文件上传，您需要搭配`代理服务器`来实现本地文件的访问（例如 Nginx）。
    * Nginx配置示例
    ```
    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黑名单（share-config-dev.yml）。
    ```yaml
    ruoyi:
      security:
        ips:
          - 1.1.1.1
    ```
  * 其次，所有的请求，校验登录的用户是否有（字符）权限调用接口（`verifyPermissions()`）。
    ::: warning 注意
    在`verifyPermissions()`方法校验（字符）权限前，先会去校验用户是否登录`verifyLogined()`。  
    只有验证用户已经登录了，才会去校验用户是否有（字符）权限。  
    如果您希望某个接口无需登录即可访问，需要配置放行白名单（share-config-dev.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-cloud?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) {
    ...
  }
  ```

## 熔断限流
使用`sentinel`实现。
* 配置示例（share-config-dev.yml）
```yaml
solon:
  cloud:
    local:
      # 限流
      breaker:
        # 根断路器的阀值（即默认阀值，必须大于0）
        root: 1000
        # global 为断路器名称（不配置或配置0，则取根断路器的阀值）
        # global: 100
```
* 全局限流：`com.ruoyi.common.security.filter.GlobalFilter`
* [Solon熔断限流](https://solon.noear.org/article/160)

## 防重复提交
使用注解`@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/mask.html)，[MyBatis-Flex 字段权限](https://mybatis-flex.com/zh/core/columns-permission.html)

## 定时任务 & 异步
定时任务使用`quartz`实现。
* 具体写法参照`ruoyi-modules-schedule`任务调度模块的`com.ruoyi.schedule.task.RyTask`

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

## 代码生成
提供了基础的增删查改代码生成功能。
* 代码生成配置
  ```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`实现。
* 接口文档配置（位于 nacos 的 share-config-dev.yml 中）
  ```yaml
  knife4j:
    # 开启增强配置
    enable: true
    # 开启生产环境屏蔽
    production: false
    # 签权
    # basic:
    #   enable: true
    #   username: admin
    #   password: 123456
    # 前端UI的个性化配置
    setting:
      enableVersion: true
      enableOpenApi: false
      enableFooter: false
  ```
* 为服务模块引入文档模块
  ```xml
  <!-- 接口文档模块 -->
  <dependency>
      <groupId>com.ruoyi</groupId>
      <artifactId>ruoyi-common-swagger</artifactId>
  </dependency>
  ```
* 接口文档摘要：基于配置文件构建（在服务模块的配置文件中增加下方配置）
  ```yaml
  solon:
    # 接口文档
    docs:
      routes:
        - id: openapi  # 固定值
          # groupName: group-name
          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.system.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);
    }

  }
  ```
* 启动`RuoYiDocsApplication`，登录系统（系统工具 -> 系统接口），使用接口文档配置中的签权的账号密码登录
* 如果不需要某个服务模块出现在文档中，可以在`RuoYiDocsApplication`的配置文件中配置忽略该服务
  ```yaml{15-16}
  solon:
    # 接口文档
    docs:
      # 自动发现配置
      discover:
        # 是否启用自动发现
        enabled: true
        # 目标服务的文档接口路径模式（要么带变量 {service}，要么用统一固定值）
        uriPattern: swagger/v2?group=openapi
        # 同步目标服务上下线状态（如果下线，则文档不显示）
        syncStatus: false
        # 签权
        basicAuth:
          admin: 123456
        # 排除目标服务名
        excludedServices:
          - ruoyi-auth
          - ruoyi-docs
          - ruoyi-file
          - ruoyi-gen
          - ruoyi-schedule
          - ruoyi-monitor
  ```
* 您应当最后启动`RuoYiDocsApplication`

## 系统监控
系统监控分为服务端和客户端，服务端有单独的模块`ruoyi-visual-monitor`，其它每个服务模块作为客户端。
* 您需要先启动`ruoyi-visual-monitor`服务模块
* 打开每个服务模块主启动类上的`@EnableAdminClient`注解的注释
* 监控配置的服务端配置在`ruoyi-monitor-dev.yml`中，客户端配置在`share-config-dev.yml`中。
* 登录系统（系统工具 -> Admin控制台），使用监控配置中的基础签权的账号密码登录
