
本文详解如何在基于 echo 的分页产品列表页面中集成搜索功能,包括安全拼接 like 查询参数、改造 sql 语句、更新路由与处理器逻辑,并避免 sql 注入风险。
在当前的分页实现中,你已通过路径参数 :page 实现了基础分页(如 /product/1),但缺少对用户搜索行为的支持——例如按 prefix 或 usage 模糊查找。直接在 SQL 中使用 ‘%s%’ 占位符并传入原始关键词会导致语法错误或注入漏洞,因此必须重构查询逻辑。
✅ 正确做法:将搜索参数作为独立查询参数传递
首先,修改路由,不再仅依赖路径参数,而是支持带查询参数的 GET 请求(更符合 REST 语义且便于前端表单提交):
// 支持两种访问方式: // - /product?page=1&search=abc (推荐) // - /product/1?search=xyz (兼容旧路由,可选) e.GET("/product", handlers.Product) // 主搜索+分页入口 e.GET("/product/:page", handlers.Product) // 保留原有路径式分页(需解析 query)
然后,在 handlers.Product 中统一处理搜索与分页逻辑:
func Product(c echo.Context) error { ctx := c.Request().Context() // 1. 解析分页参数(优先从 query,fallback 到 path) pageStr := c.QueryParam("page") if pageStr == "" { pageStr = c.Param("page") // 兼容 /product/1 形式 } page, err := strconv.Atoi(pageStr) if err != nil || page < 1 { page = 1 } pageSize := 10 offset := (page - 1) * pageSize // 2. 解析搜索关键词(可为空) search := strings.TrimSpace(c.QueryParam("search")) // 3. 构建安全的 LIKE 模式:前后加 %,由 Go 层完成,而非 SQL 拼接 var searchPattern string if search != "" { searchPattern = "%" + search + "%" } // 4. 执行带搜索条件的分页查询(注意:SQL 中使用 $3、$4 占位符) rows, err := database.WrapQuery( dbconnections.DBPool, ctx, "GetFromProductPaginatedByOffset", pageSize, offset, searchPattern, // ← 传入已加 % 的 pattern searchPattern, // ← 同样用于 usage 字段 ) if err != nil { loggerWithTrace.Error().Err(err).Caller().Msg("paginated query failed") return echo.NewHTTPError(http.StatusInternalServerError, "failed to fetch products") } defer rows.Close() var results []models.Product for rows.Next() { var p models.Product if err := rows.Scan(&p.Prefix, &p.Suffix, &p.Usage); err != nil { loggerWithTrace.Error().Err(err).Caller().Msg("scan product row failed") continue } results = append(results, p) } // 5. 获取总数量(同样需应用搜索条件) var totalItems int err = database.WrapQueryRow( dbconnections.DBPool, ctx, "GetTotalSizeFromProduct", searchPattern, searchPattern, ).Scan(&totalItems) if err != nil { loggerWithTrace.Error().Err(err).Caller().Msg("count query failed") } totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) nextPage := page + 1 if nextPage > totalPages { nextPage = totalPages } prevPage := page - 1 if prevPage < 1 { prevPage = 1 } // 6. 渲染模板,透传 search 值以保持搜索状态 templateDataMap := map[string]interface{}{ "Product": results, "Page": page, "PageSize": pageSize, "TotalItems": totalItems, "TotalPages": totalPages, "NextPage": nextPage, "PrevPage": prevPage, "Search": search, // ← 关键:回填到 input value 中 } return c.Render(http.StatusOK, "product", templateDataMap) }
? SQL 查询语句更新(关键!)
确保你的 SQL 查询模板(如 Q_GET_PAGINATION_FROM_PRODUCT)使用参数化占位符,禁止字符串拼接:
-- ✅ 正确:使用 $3 和 $4 接收 go 传入的 '%keyword%' 字符串 SELECT * FROM PRODUCT WHERE (prefix LIKE $3 OR usage LIKE $4) LIMIT $1 OFFSET $2; -- ✅ 对应总数查询: SELECT COUNT(*) FROM PRODUCT WHERE (prefix LIKE $1 OR usage LIKE $2);
⚠️ 注意:不要写成 LIKE '%' || $1 || '%' 或 LIKE '%'+$1+'%' —— 这不仅降低可读性,还可能因数据库方言差异出错;更严重的是,若前端传入恶意内容(如 %'; DROP table...),虽参数化本身防注入,但手动拼接 % 仍易出错。始终由 Go 层构建 pattern,SQL 只做纯匹配。
?️ 前端模板(html 示例)
在 product.html 中添加搜索表单,并保持当前搜索词与分页链接同步:
✅ 总结要点
- 搜索与分页应解耦:用 ?page=2&search=abc 替代硬编码路径;
- LIKE 模式(%xxx%)必须在 Go 层生成,SQL 中仅使用标准 $n 占位符;
- 所有数据库查询(含总数统计)都需一致应用搜索条件;
- 模板中回显 .Search 值,保障用户体验连续性;
- 避免任何字符串格式化拼接 SQL,坚守参数化查询原则。
这样改造后,你的产品页就拥有了健壮、安全且用户友好的搜索+分页能力。