为Sails.js API添加自定义搜索参数:半径查询路由丢失基础功能
我来帮你搞定这个问题——你现在的核心痛点是直接用原生SQL查询绕过了Waterline(Sails的ORM)的核心能力,导致丢失了默认索引路由自带的过滤、分页、排序、关联填充等功能。下面是几个既能实现半径地理查询,又能完整保留Sails默认API特性的可行方案:
方案一:自定义模型方法,结合Waterline查询
在你的Cheer模型里新增一个自定义方法,把地理查询逻辑和Waterline的标准查询能力结合起来,这样就能复用所有默认路由的功能:
// api/models/Cheer.js module.exports = { attributes: { lat: { type: 'number', required: true }, lng: { type: 'number', required: true }, // 你的其他模型属性... }, // 自定义方法:支持半径查询 + 所有Waterline标准查询参数 findNearby: async function(lat, lng, radiusInMeters, queryOpts = {}) { // 初始化基础Waterline查询 let baseQuery = Cheer.find(queryOpts); // 添加参数化的地理查询条件(避免SQL注入) baseQuery.where({ sql: "earth_box(ll_to_earth($1, $2), $3) @> ll_to_earth(lat, lng)", values: [lat, lng, radiusInMeters] }); // 执行查询,自动继承分页、排序、关联填充等功能 return await baseQuery; } };
然后在控制器里调用这个方法,同时提取并传递所有默认查询参数:
// api/controllers/CheerController.js const _ = require('lodash'); // 用lodash过滤参数,也可以自己手写逻辑 module.exports = { nearby: async function(req, res) { try { // 提取地理查询参数 const { lat, lng, rad: radiusInMeters } = req.query; // 过滤掉地理参数,剩下的交给Waterline处理 const standardQueryOpts = _.omit(req.query, ['lat', 'lng', 'rad']); const results = await Cheer.findNearby(lat, lng, radiusInMeters, standardQueryOpts); return res.ok(results); } catch (err) { return res.serverError(err); } } };
现在你的/cheer/nearby路由就能支持所有默认功能了,比如:GET /cheer/nearby?lat=40.7128&lng=-74.0060&rad=5000&limit=10&sort=createdAt DESC&populate=author
方案二:原生SQL筛选ID + Waterline查询详情
如果需要更复杂的原生地理逻辑,可以先通过原生查询拿到符合条件的记录ID,再用Waterline查询这些ID的详情,同时应用所有标准查询参数:
// api/controllers/CheerController.js const _ = require('lodash'); module.exports = { nearby: async function(req, res) { try { const { lat, lng, rad: radiusInMeters } = req.query; const standardQueryOpts = _.omit(req.query, ['lat', 'lng', 'rad']); // 第一步:用原生SQL获取符合半径条件的记录ID const rawResult = await sails.sendNativeQuery( "SELECT id FROM cheer WHERE earth_box(ll_to_earth($1, $2), $3) @> ll_to_earth(lat, lng);", [lat, lng, radiusInMeters] ); const validIds = rawResult.rows.map(row => row.id); if (validIds.length === 0) { return res.ok([]); } // 第二步:用Waterline查询详情,自动支持分页、排序、关联等 const results = await Cheer.find({ ...standardQueryOpts, id: validIds }); return res.ok(results); } catch (err) { return res.serverError(err); } } };
这个方法的优势是把地理筛选和数据查询解耦,原生SQL只负责最核心的地理判断,剩下的所有复杂逻辑都交给Waterline处理。
方案三:生命周期钩子扩展默认路由(高级用法)
如果想让默认的/cheer索引路由直接支持半径查询,可以在模型的beforeFind钩子中添加逻辑,自动识别并处理地理参数:
// api/models/Cheer.js module.exports = { attributes: { lat: { type: 'number', required: true }, lng: { type: 'number', required: true }, // 你的其他模型属性... }, beforeFind: function(options) { const { lat, lng, radius } = options.where || {}; // 如果请求中包含地理参数,添加半径查询条件 if (lat && lng && radius) { options.where = { ...options.where, // 添加参数化的地理查询 sql: "earth_box(ll_to_earth($1, $2), $3) @> ll_to_earth(lat, lng)", values: [lat, lng, radius] }; // 移除临时的地理参数,避免Waterline尝试查询不存在的字段 delete options.where.lat; delete options.where.lng; delete options.where.radius; } } };
现在你直接用默认路由就能实现半径查询,同时保留所有原生功能:GET /cheer?lat=40.7128&lng=-74.0060&radius=5000&limit=20&sort=createdAt DESC
关键注意事项
- 确保PostgreSQL扩展已启用:
CREATE EXTENSION IF NOT EXISTS cube; CREATE EXTENSION IF NOT EXISTS earthdistance; - 添加性能索引:为了提升地理查询速度,建议创建GIN索引:
CREATE INDEX cheer_earth_idx ON cheer USING GIN (ll_to_earth(lat, lng)); - 始终使用参数化查询:避免直接拼接SQL,防止SQL注入风险。
内容的提问来源于stack exchange,提问作者WalksAway

