Skip to content

分页排序 ​

这篇解决什么 ​

列表接口要带页码、每页条数,有时还要按列排序;导出则要一次拉全量。读完能给新模块写 XxxPageReqVO、在 Mapper 里调 selectPage,并让 VbenVxeGrid 对上 list / total。

条件怎么拼见 MyBatis 数据库。pageNo 校验见 参数校验、时间传参。PageResult 转 RespVO 见 VO 对象转换、数据翻译。

默认端口 48080,管理端前缀 /admin-api。范文走系统管理的租户列表。

组件位置 ​

名称说明仓库路径
PageParam页码、每页条数,默认 1 / 10ruoyi-office/yudao-framework/yudao-common/.../pojo/PageParam.java
PageResult出参:list + total.../pojo/PageResult.java
SortablePageParamPageParam 加 sortingFields.../pojo/SortablePageParam.java
SortingField一列:field + order(asc / desc).../pojo/SortingField.java
BaseMapperX.selectPageIPage 转 PageResult;-1 查全部yudao-spring-boot-starter-mybatis/.../BaseMapperX.java
MyBatisUtils.buildPage组 Page,并校验排序列名.../mybatis/core/util/MyBatisUtils.java
PageUtils内存分页、默认排序列yudao-common/.../util/object/PageUtils.java
分页插件PaginationInnerInterceptorYudaoMybatisAutoConfiguration
范文租户分页 / 导出yudao-module-system 的 TenantController、TenantMapper
前端表格useVbenVxeGrid,读 list / totalruoyi-office-vben/apps/web-antd/src/adapter/vxe-table.ts

YudaoMybatisAutoConfiguration 把分页插件挂进 MybatisPlusInterceptor。列表 SQL 不用自己写 LIMIT。

请求怎么落到表 ​

Controller 只收 VO。Service 转发。Mapper 调 selectPage,再进表。

租户分页请求链路

示意图:管理端租户列表分页落到 TenantMapper,再进 system_tenant。

接口是 GET /admin-api/system/tenant/page。权限标识 system:tenant:query(当前方法上的 @PreAuthorize 被注释掉了,新接口不要照抄这一行)。

java
@GetMapping("/page")
@Operation(summary = "获得租户分页")
public CommonResult<PageResult<TenantRespVO>> getTenantPage(@Valid TenantPageReqVO pageVO) {
    PageResult<TenantDO> pageResult = tenantService.getTenantPage(pageVO);
    return success(BeanUtils.toBean(pageResult, TenantRespVO.class));
}

TenantServiceImpl.getTenantPage 直接 return tenantMapper.selectPage(pageReqVO)。

分页参数 ​

列表查询 VO 继承 PageParam,再写搜索字段。

java
@Schema(description="分页参数")
@Data
public class PageParam implements Serializable {

    public static final Integer PAGE_SIZE_NONE = -1;

    @NotNull(message = "页码不能为空")
    @Min(value = 1, message = "页码最小值为 1")
    private Integer pageNo = 1;

    @NotNull(message = "每页条数不能为空")
    @Min(value = 1, message = "每页条数最小值为 1")
    @Max(value = 200, message = "每页条数最大值为 200")
    private Integer pageSize = 10;
}

TenantPageReqVO 继承它,加上名称、联系人、状态、创建时间区间:

java
@Schema(description = "管理后台 - 租户分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class TenantPageReqVO extends PageParam {

    private String name;
    private String contactName;
    private String contactMobile;
    private Integer status;

    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] createTime;
}

完整字段和 @Schema 在 yudao-module-system-server/.../vo/tenant/TenantPageReqVO.java。区间绑定用 @DateTimeFormat,不要写成 @JsonFormat。

pageSize 上限是 200

校验在入参阶段就拦住。前端把每页条数开到 500 会直接 400。要一次拉全量,走下面的 PAGE_SIZE_NONE,不要改 @Max。

分页结果 ​

PageResult 只有两列。BeanUtils.toBean 能整页转换元素类型。

java
@Schema(description = "分页结果")
@Data
public final class PageResult<T> implements Serializable {

    private Long total;
    private List<T> list;

    public static <T> PageResult<T> empty() {
        return new PageResult<>(0L);
    }
}

空页用 PageResult.empty()。响应仍包在 CommonResult 里:data.list、data.total。前端请求层会拆掉外层 code / msg。

内存里已经有完整 List 时,用 PageUtils.buildPageResult(list, pageParam) 切片,不要再打库。pageSize == PAGE_SIZE_NONE 时它直接返回全量。

Mapper 查询 ​

Mapper 继承 BaseMapperX<T>。条件用 LambdaQueryWrapperX 的 xxxIfPresent,空值不进 SQL。

java
@Mapper
public interface TenantMapper extends BaseMapperX<TenantDO> {

    default PageResult<TenantDO> selectPage(TenantPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<TenantDO>()
                .likeIfPresent(TenantDO::getName, reqVO.getName())
                .likeIfPresent(TenantDO::getContactName, reqVO.getContactName())
                .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile())
                .eqIfPresent(TenantDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(TenantDO::getId));
    }
}

selectPage 内部两路:

名称说明仓库路径
普通分页MyBatisUtils.buildPage 得到 IPage,插件补 LIMIT 和 COUNTBaseMapperX.selectPage
PAGE_SIZE_NONEselectList 全量,total = list.size()同上
联表分页selectJoinPage,结果可投影到别的 classCrmBusinessMapper、WmsItemSkuMapper

BaseMapperX 把 IPage 收成 PageResult

示意图:IPage 在 BaseMapperX 里收成 PageResult。

固定排序写在 Wrapper,例如 .orderByDesc(TenantDO::getId)。联表、xxxIfPresent 细节见 MyBatis 数据库。

导出不分页 ​

导出复用同一套 PageReqVO 和 selectPage。校验通过后再改 pageSize。

java
@GetMapping("/export-excel")
@Operation(summary = "导出租户 Excel")
@PreAuthorize("@ss.hasPermission('system:tenant:export')")
public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO,
                              HttpServletResponse response) throws IOException {
    exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
    List<TenantDO> list = tenantService.getTenantPage(exportReqVO).getList();
    ExcelUtils.write(response, "租户.xls", "数据", TenantRespVO.class,
            BeanUtils.toBean(list, TenantRespVO.class));
}

用户导出、角色导出也是这个写法。前端 exportTenant 只带搜索条件,不传 -1。

不要让浏览器传 pageSize = -1

@Min(1) 会先挡掉。-1 只允许在 Service / Controller 里、@Valid 之后赋值。

导出也要带搜索条件

setPageSize 只关掉分页,name、status 这些条件仍在。表格「导出」应对齐当前筛选项。

动态排序 ​

表头点列排序时,PageReqVO 改继承 SortablePageParam,或接口再收一个 SortablePageParam。

java
@Schema(description = "可排序的分页参数")
@Data
@EqualsAndHashCode(callSuper = true)
public class SortablePageParam extends PageParam {

    @Schema(description = "排序字段")
    private List<SortingField> sortingFields;
}

SortingField.ORDER_ASC 是 "asc",ORDER_DESC 是 "desc"。BaseMapperX.selectPage(sortablePageParam, wrapper) 会把 sortingFields 交给 MyBatisUtils.buildPage。

商品排行把查询条件和分页拆成两个入参,缺省按浏览量倒序:

java
@GetMapping("/rank-page")
@Operation(summary = "获得商品统计排行榜分页(商品维度)")
public CommonResult<PageResult<ProductStatisticsRespVO>> getProductStatisticsRankPage(
        @Valid ProductStatisticsReqVO reqVO,
        @Valid SortablePageParam pageParam) {
    PageResult<ProductStatisticsDO> pageResult =
            productStatisticsService.getProductStatisticsRankPage(reqVO, pageParam);
    // ...
}

ProductStatisticsServiceImpl 里先 PageUtils.buildDefaultSortingField(pageParam, ProductStatisticsDO::getBrowseCount),前端没传排序时补默认列。

MyBatisUtils.buildSafeOrderColumn 会把驼峰改成下划线,并用 ^[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$ 过滤。name desc、name;drop 这类值会被丢掉,不会拼进 ORDER BY。

排序列不要手写拼接

只走 SortingField + MyBatisUtils。不要把前端传来的字符串直接 last("ORDER BY " + field)。LambdaQueryWrapper 在 PAGE_SIZE_NONE 时用 last("ORDER BY …"),同一条 Wrapper 上不要再写别的 last。

前端表格 ​

PC 管理端列表用 useVbenVxeGrid。全局已经约定响应字段:

名称说明仓库路径
proxyConfig.responseresult = list,total = totalapps/web-antd/src/adapter/vxe-table.ts
PageParam / PageResultpageNo / pageSize,list / totalpackages/effects/request/.../types.ts
租户 APIGET /system/tenant/pageapps/web-antd/src/api/system/tenant/index.ts
租户页搜索表单 + 表格apps/web-antd/src/views/system/tenant/index.vue

query 里把 vxe 的 currentPage / pageSize 改成后端字段名:

ts
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getTenantPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
  } as VxeTableGridOptions<SystemTenantApi.Tenant>,
});
ts
export function getTenantPage(params: PageParam) {
  return requestClient.get<PageResult<SystemTenantApi.Tenant>>(
    '/system/tenant/page',
    { params },
  );
}

需要表头远程排序时,打开 proxyConfig.sort,并展开 buildSortingField(sorts):

ts
query: async ({ page, sorts }) => {
  return await getProductStatisticsRankPage({
    pageNo: page.currentPage,
    pageSize: page.pageSize,
    ...buildSortingField(sorts),
  });
},

buildSortingField 在 @vben/request,会生成 sortingFields[0].field、sortingFields[0].order,供 GET 绑定到 List<SortingField>。商品排行页在 views/mall/statistics/product/modules/rank-card.vue。

搜索后回到第一页

vxe 点搜索会重新 query。自己写按钮时先把页码重置再拉数,避免停在空页。

配置与操作 ​

列表入参继承 PageParam,返回 PageResult。前端搜索先重置页码。排序字段走 sortingFields。

开启见 框架层。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版