本文详解如何解决「分类/子分类筛选 + 分页」场景下常见的数据丢失、页码跳变、总数错乱等问题,核心在于服务端统一维护过滤状态与分页上下文,避免前端页码变量与后端查询逻辑脱节。
本文详解如何解决「分类/子分类筛选 + 分页」场景下常见的数据丢失、页码跳变、总数错乱等问题,核心在于服务端统一维护过滤状态与分页上下文,避免前端页码变量与后端查询逻辑脱节。
在你提供的 JavaScript 代码中,currentPage 是前端维护的全局变量,但问题根源在于:每次点击“下一页”时,前端仅传递 page 参数,却未同步携带当前激活的 subcategory_ids 或 category_ids 过滤条件。导致 get_products.php 或 get_product_category.php 在第2页请求时(如 ?page=2)实际执行的是无过滤的原始查询,从而返回空结果或错误数据集——这正是“点下一页显示0条、再返回首页也空白”的根本原因。
分页不是前端计数器游戏,而是带状态的数据库查询过程。必须确保:
<?php// get_products.php —— 支持子分类过滤的健壮分页接口require 'db.php'; // 假设已配置 PDO 连接 $pdo// 1. 安全获取并校验参数$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;$page = max(1, $page);$limit = 20; // 固定每页条数,禁止用户篡改$offset = ($page - 1) * $limit;// 2. 解析子分类过滤条件(支持逗号分隔,如 ?subcategory_ids=5,8,12)$subcategoryIds = [];if ($idsStr = filter_input(INPUT_GET, 'subcategory_ids', FILTER_SANITIZE_STRING)) { $ids = array_filter(array_map('intval', explode(',', $idsStr))); if (!empty($ids)) { $subcategoryIds = $ids; }}// 3. 构建动态 WHERE 条件(防SQL注入!使用预处理)$whereClause = '';$params = [];if (!empty($subcategoryIds)) { $placeholders = str_repeat('?,', count($subcategoryIds) - 1) . '?'; $whereClause = "WHERE subcategory_id IN ($placeholders)"; $params = $subcategoryIds;}// 4. 查询总记录数(关键!必须与数据查询条件完全一致)$countSql = "SELECT COUNT(*) FROM products $whereClause";$stmt = $pdo->prepare($countSql);$stmt->execute($params);$totalCount = (int)$stmt->fetchColumn();// 5. 查询当前页数据$sql = "SELECT id, fabric, design_no FROM products $whereClause ORDER BY id DESC LIMIT :limit OFFSET :offset";$stmt = $pdo->prepare($sql);$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);$stmt->execute($params);$products = $stmt->fetchAll(PDO::FETCH_ASSOC);// 6. 返回结构化响应echo json_encode([ 'products' => $products, 'total_count' => $totalCount, 'current_page' => $page, 'per_page' => $limit, 'last_page' => ceil($totalCount / $limit)]);
⚠️ 关键注意事项:
- 绝不拼接 SQL 字符串:所有用户输入(尤其是 ID 列表)必须通过 PDO::prepare() + bindValue() 绑定,杜绝 SQL 注入。
- total_count 与数据查询必须共用同一 WHERE 条件:这是分页正确的数学基础。若总数查全表,而数据查过滤后,必然导致页码错乱。
- 固定 $limit:避免恶意请求 ?limit=100000 导致 DoS;如需可配置,应在服务端白名单内校验(如仅允许 10/20/50)。
你当前的 JS 在翻页时丢失了过滤参数。应重构 updateProductList(),使其始终携带最新选中的 ID 列表:
立即学习“PHP免费学习笔记(深入)”;
function buildUrl() { const selectedSubIds = Array.from( document.querySelectorAll(".subcategory-checkbox:checked") ).map(cb => cb.getAttribute("data-subcategory-id")); const params = new URLSearchParams(); params.set('page', currentPage); if (selectedSubIds.length > 0) { params.set('subcategory_ids', selectedSubIds.join(',')); } return `get_products.php?${params.toString()}`;}function updateProductList() { fetch(buildUrl()) // ✅ 每次请求都含完整过滤参数 .then(r => r.json()) .then(data => { totalProducts = data.total_count; renderProducts(data.products); // 动态更新分页按钮状态 const totalPages = Math.ceil(totalProducts / 20); nextPageButton.disabled = currentPage >= totalPages; prevPageButton.disabled = currentPage <= 1; pageElement.textContent = `Page ${currentPage}`; productCountElement.textContent = totalProducts; });}
若商品表数据量极大(>50万行),传统 OFFSET 分页将严重退化:
-- 第一页SELECT * FROM products WHERE subcategory_id IN (5,8) ORDER BY id DESC LIMIT 20;-- 下一页(取上一页最后一条的 id = 10234)SELECT * FROM products WHERE subcategory_id IN (5,8) AND id < 10234 ORDER BY id DESC LIMIT 20;
此方式性能恒定,但需前端保存 cursor 值(如 last_id),且不支持跳转任意页。
| 环节 | 正确做法 | 常见错误 |
|---|---|---|
| 参数传递 | 每次请求(含翻页)都携带全部过滤参数 | 仅首页传过滤,翻页时丢失 |
| 总数统计 | COUNT(*) 与数据查询使用完全相同的 WHERE 条件 | 总数查全表,数据查过滤后 |
| SQL 安全 | 全部用户输入用预处理绑定 | 字符串拼接 ID 列表 |
| 页码校验 | 后端强制 max(1, min($page, $lastPage)) | 信任前端 page 值直接计算 offset |
| 性能兜底 | 数据量 >10万行时启用游标分页 | 死守 LIMIT OFFSET 直至超时 |
遵循以上原则,你的分类筛选与分页将彻底解耦、稳定可靠——无论用户勾选1个子类还是10个,无论翻到第1页还是第200页,数据始终连贯、准确、高效。