
本文详解如何在基于 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,坚守参数化查询原则。
这样改造后,你的产品页就拥有了健壮、安全且用户友好的搜索+分页能力。










