之前的实现存在严重的架构问题:
采用纯后端分页 + 后端筛选的架构:
后端新增DTO类:
// OrderFilterDTO.java
@Data
public class OrderFilterDTO {
private String keyword;
private String orderStatus;
private String shippingStatus;
private String paymentStatus;
private Long channelId;
private Long warehouseId;
}
// ProductFilterDTO.java
@Data
public class ProductFilterDTO {
private String keyword;
private String category;
private String status;
private String brand;
}
优点:
OrdersController.java:
@GetMapping
public PageResponse<OrderListDTO> getOrders(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@ModelAttribute OrderFilterDTO filters) {
Page<Orders> pageResult = ordersService.getPage(page, size, filters);
// ... 返回分页结果
}
ProductController.java:
@GetMapping
public PageResponse<Product> getProducts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@ModelAttribute ProductFilterDTO filters) {
Page<Product> pageResult = productService.getProducts(page, size, filters);
// ... 返回分页结果
}
OrdersService.java:
public Page<Orders> getPage(int page, int size, OrderFilterDTO filters) {
LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
.orderByDesc(Orders::getCreatedAt);
if (filters.getKeyword() != null && !filters.getKeyword().isEmpty()) {
wrapper.and(w -> w.like(Orders::getOrderNo, filters.getKeyword())
.or().like(Orders::getChannelOrderNo, filters.getKeyword())
.or().like(Orders::getBuyer, filters.getKeyword())
.or().like(Orders::getReceiverName, filters.getKeyword()));
}
if (filters.getOrderStatus() != null && !filters.getOrderStatus().isEmpty())
wrapper.eq(Orders::getOrderStatus, filters.getOrderStatus());
if (filters.getShippingStatus() != null && !filters.getShippingStatus().isEmpty())
wrapper.eq(Orders::getShippingStatus, filters.getShippingStatus());
if (filters.getPaymentStatus() != null && !filters.getPaymentStatus().isEmpty())
wrapper.eq(Orders::getPaymentStatus, filters.getPaymentStatus());
if (filters.getChannelId() != null)
wrapper.eq(Orders::getChannelId, filters.getChannelId());
if (filters.getWarehouseId() != null)
wrapper.eq(Orders::getWarehouseId, filters.getWarehouseId());
return mapper.selectPage(new Page<>(page, size), wrapper);
}
ProductService.java:
public Page<Product> getProducts(int page, int size, ProductFilterDTO filters) {
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
.orderByDesc(Product::getCreatedAt);
if (filters.getKeyword() != null && !filters.getKeyword().isEmpty())
wrapper.and(w -> w.like(Product::getTitle, filters.getKeyword())
.or().like(Product::getSpu, filters.getKeyword()));
if (filters.getCategory() != null && !filters.getCategory().isEmpty())
wrapper.eq(Product::getCategoryId, filters.getCategory());
if (filters.getStatus() != null && !filters.getStatus().isEmpty())
wrapper.eq(Product::getStatus, filters.getStatus());
if (filters.getBrand() != null && !filters.getBrand().isEmpty())
wrapper.eq(Product::getBrand, filters.getBrand());
return mapper.selectPage(new Page<>(page, size), wrapper);
}
frontend/src/api/services.ts:
/* Orders */
getOrders: (page?: number, size?: number, filters?: {
keyword?: string;
orderStatus?: string;
shippingStatus?: string;
paymentStatus?: string;
channelId?: number;
warehouseId?: number;
}) => {
const params = new URLSearchParams({
page: String(page || 1),
size: String(size || 20)
});
if (filters?.keyword) params.append('keyword', filters.keyword);
if (filters?.orderStatus) params.append('orderStatus', filters.orderStatus);
if (filters?.shippingStatus) params.append('shippingStatus', filters.shippingStatus);
if (filters?.paymentStatus) params.append('paymentStatus', filters.paymentStatus);
if (filters?.channelId) params.append('channelId', String(filters.channelId));
if (filters?.warehouseId) params.append('warehouseId', String(filters.warehouseId));
return request<{ items: OrderItem[]; totalElements: number }>(
`/api/order/orders?${params.toString()}`
);
},
/* Products */
getProducts: (page?: number, size?: number, filters?: {
keyword?: string;
category?: string;
status?: string;
brand?: string;
}) => {
const params = new URLSearchParams({
page: String(page || 1),
size: String(size || 20)
});
if (filters?.keyword) params.append('keyword', filters.keyword);
if (filters?.category) params.append('category', filters.category);
if (filters?.status) params.append('status', filters.status);
if (filters?.brand) params.append('brand', filters.brand);
return request<{ items: ProductItem[]; totalElements: number }>(
`/api/product/products?${params.toString()}`
);
},
OrderListView.vue - 移除客户端筛选:
// ❌ 删除: 客户端筛选逻辑
const filteredItems = computed(() => {
return items.value.filter(item => {
// ... 筛选逻辑
});
});
const paginatedItems = computed(() => {
const start = (page.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredItems.value.slice(start, end);
});
// ✅ 新增: 直接调用后端API并传递筛选参数
const loadData = async () => {
loading.value = true;
try {
const res = await api.getOrders(page.value, pageSize.value, {
keyword: searchKeyword.value || undefined,
orderStatus: filters.value.orderStatus || undefined,
shippingStatus: filters.value.shippingStatus || undefined,
paymentStatus: filters.value.paymentStatus || undefined
});
items.value = res.items || [];
totalElements.value = res.totalElements || 0;
} finally {
loading.value = false;
}
};
// 监听筛选条件变化,重新加载数据
watch([searchKeyword, () => filters.value, page, pageSize], () => {
loadData();
}, { deep: true });
ProductListView.vue - 同样修改:
const loadData = async () => {
loading.value = true;
try {
const res = await api.getProducts(page.value, pageSize.value, {
keyword: filters.value.keyword || undefined,
category: filters.value.category || undefined,
status: filters.value.status || undefined,
brand: filters.value.brand || undefined
});
items.value = res.items ?? [];
totalElements.value = res.totalElements || 0;
} finally {
loading.value = false;
}
};
表格数据绑定修改:
<!-- ❌ 之前: 使用客户端分页数据 -->
<el-table :data="paginatedItems" ...>
<!-- ✅ 现在: 直接使用后端返回的数据 -->
<el-table :data="items" ...>
<!-- ❌ 之前: 使用客户端筛选后的总数 -->
<el-pagination :total="filteredTotal" ...>
<!-- ✅ 现在: 使用后端返回的总数 -->
<el-pagination :total="totalElements" ...>
backend/src/main/java/com/oms/dto/OrderFilterDTO.java - 新建backend/src/main/java/com/oms/dto/ProductFilterDTO.java - 新建backend/src/main/java/com/oms/controller/OrdersController.java - 使用DTO简化参数backend/src/main/java/com/oms/controller/ProductController.java - 使用DTO简化参数backend/src/main/java/com/oms/service/OrdersService.java - 支持DTO筛选backend/src/main/java/com/oms/service/ProductService.java - 支持DTO筛选frontend/src/api/services.ts - API方法支持筛选参数frontend/src/views/order/OrderListView.vue - 移除客户端筛选,使用后端分页frontend/src/views/product/ProductListView.vue - 移除客户端筛选,使用后端分页性能优化:
可扩展性:
用户体验:
// 订单列表 - 带筛选的分页查询
const res = await api.getOrders(1, 20, {
keyword: 'OMS-',
orderStatus: 'PAID',
shippingStatus: 'SHIPPED'
});
// 商品列表 - 带筛选的分页查询
const res = await api.getProducts(1, 20, {
keyword: '行李箱',
category: 'Luggage',
status: 'LISTED'
});
@GetMapping
public PageResponse<OrderListDTO> getOrders(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@ModelAttribute OrderFilterDTO filters) {
// Spring自动绑定查询参数到DTO
// ?keyword=OMS-&orderStatus=PAID -> filters.setKeyword("OMS-"), filters.setOrderStatus("PAID")
}
这次修复解决了分页架构的根本问题,采用了企业级应用的标准实践:
修复完成后,系统可以正确处理海量数据的分页和筛选,用户体验得到显著提升。
修复时间: 2026-04-21 修复范围: 订单列表、商品列表的分页架构 代码质量: 采用DTO模式,符合企业级应用标准 用户反馈: ✅ 接受使用DTO的建议,代码更简洁