Laravel三级关联下经纬度距离过滤功能失效问题求助
解决Laravel三级关联(hasManyThrough)下经纬度距离过滤失效问题
我看到你遇到的问题了:用hasManyThrough关联到第三层模型(PartnerOutlet),想要根据第三层的经纬度做距离过滤和排序,但当前的whereHas写法无法生效,主要原因有两个:
whereHas只负责筛选主模型,不传递关联字段到主查询:你在whereHas闭包里计算的distance和做的orderBy,都是作用于关联模型的子查询里,不会影响主模型的结果集排序,也不会把distance字段带回主模型。hasManyThrough的whereHas闭包是针对目标模型(PartnerOutlet),但你需要把距离条件和排序整合到主查询中。
下面给你两种可行的解决方案:
方案一:手动Join关联表(最直接高效)
既然hasManyThrough本质是两次表关联,我们可以手动Join中间表(PartnerOutletPromotion)和目标表(PartnerOutlet),直接在主查询里计算距离、筛选和排序:
if ($request->filled(['latitude', 'longitude'])) { $latitude = $request->latitude; $longitude = $request->longitude; // 替换成你的主模型表名和对应的外键 $mainModelTable = 'your_main_model_table'; // 比如如果主模型是Product,这里是products $mainModelForeignKey = 'product_id'; // 主模型在partner_outlet_promotions表中的外键 $list = $list // Join中间表 ->join('partner_outlet_promotions', "{$mainModelTable}.id", '=', "partner_outlet_promotions.{$mainModelForeignKey}") // Join目标表(存经纬度的表) ->join('partner_outlets', 'partner_outlet_promotions.partner_outlet_id', '=', 'partner_outlets.id') // 计算距离,用参数绑定避免SQL注入 ->selectRaw("{$mainModelTable}.*, 6371 * acos(cos(radians(?)) * cos(radians(partner_outlets.latitude)) * cos(radians(partner_outlets.longitude) - radians(?)) + sin(radians(?)) * sin(radians(partner_outlets.latitude))) AS distance", [$latitude, $longitude, $latitude]) // 筛选距离小于100000的记录 ->having('distance', '<', 100000) // 按距离排序 ->orderBy('distance', 'asc') // 去重,避免主模型因多个关联outlet而重复 ->distinct(); } return response()->json($list->get());
注意事项:
- 一定要替换
$mainModelTable和$mainModelForeignKey为你实际的表名和外键。 - 使用
selectRaw的参数绑定可以避免SQL注入风险,比直接拼接字符串更安全。 distinct()用来防止主模型记录因为关联多个符合条件的PartnerOutlet而重复出现。
方案二:使用子查询计算距离(适合不想用Join的场景)
如果你更倾向于使用Laravel的关联语法而不是手动Join,可以用selectSub把距离计算作为子查询加入主查询,同时嵌套whereHas做筛选:
首先,确保你的PartnerOutletPromotion模型有一个到PartnerOutlet的关联:
// 在PartnerOutletPromotion模型中 public function partnerOutlet() { return $this->belongsTo(PartnerOutlet::class); }
然后修改查询代码:
if ($request->filled(['latitude', 'longitude'])) { $latitude = $request->latitude; $longitude = $request->longitude; // 子查询:获取当前主模型关联的最近outlet的距离 $distanceSubquery = PartnerOutlet::selectRaw('6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))', [$latitude, $longitude, $latitude]) ->whereColumn('partner_outlets.id', 'partner_outlet_promotions.partner_outlet_id') ->whereRaw('6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude))) < 100000', [$latitude, $longitude, $latitude]) ->limit(1); $list = $list // 筛选存在符合距离条件的关联记录 ->whereHas('partnerOutletPromotion', function ($query) use ($latitude, $longitude) { $query->whereHas('partnerOutlet', function ($q) use ($latitude, $longitude) { $q->selectRaw('*, 6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude))) AS distance', [$latitude, $longitude, $latitude]) ->having('distance', '<', 100000); }); }) // 将距离作为子查询字段加入主查询 ->selectSub($distanceSubquery, 'distance') // 按距离排序 ->orderBy('distance', 'asc'); } return response()->json($list->get());
为什么原来的代码失效?
再回头看你原来的写法:
whereHas('partnerOutletPromotion', ...)只是筛选出存在符合条件的PartnerOutlet的主模型记录,但不会把distance字段带到主查询中。- 你在闭包里的
orderBy('distance', 'asc')是对关联模型的子查询排序,不会影响主模型结果的顺序。 - 最终返回的
$list->get()是主模型的集合,根本没有distance字段,所以排序完全不生效。
用上面的两种方案,就能把距离计算整合到主查询中,实现正确的过滤和排序。
内容的提问来源于stack exchange,提问作者Muhammad Mehdi
相关产品推荐
相关产品推荐

