← Назад к вопросам
Справочник организаций с геолокацией
3.0 Senior🔥 71 комментариев
#API и веб-протоколы#Базы данных и SQL#Фреймворки
Условие
Реализовать мини-справочник с адресами, компаниями и зданиями.
Структура данных
- Здания: адрес, координаты (lat, lng)
- Компании: название, телефон, часы работы
- Связь: компания может находиться в нескольких зданиях
API требования
- CRUD для зданий и компаний
- Поиск компаний по названию
- Выборка компаний по радиусу от заданной точки
- Выборка компаний в заданном квадрате координат
Пример запроса
GET /api/companies?lat=55.75&lng=37.62&radius=1000
GET /api/companies?lat1=55.7&lng1=37.5&lat2=55.8&lng2=37.7
Технологии
PHP 8+, Laravel, PostgreSQL с PostGIS или MySQL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
1. Миграции для таблиц
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
// Таблица зданий
Schema::create("buildings", function (Blueprint $table) {
$table->id();
$table->string("address", 500);
$table->string("city", 100);
$table->string("postal_code", 20)->nullable();
$table->decimal("latitude", 10, 8);
$table->decimal("longitude", 11, 8);
$table->text("description")->nullable();
$table->timestamps();
// Индекс для быстрого поиска по координатам
$table->spatialIndex(["latitude", "longitude"]);
});
// Таблица компаний
Schema::create("companies", function (Blueprint $table) {
$table->id();
$table->string("name", 255);
$table->string("phone", 20)->nullable();
$table->string("email", 255)->nullable();
$table->time("opening_time")->nullable();
$table->time("closing_time")->nullable();
$table->text("description")->nullable();
$table->boolean("is_active")->default(true);
$table->timestamps();
$table->index("name");
$table->index("is_active");
});
// Таблица связи компаний и зданий (many-to-many)
Schema::create("company_building", function (Blueprint $table) {
$table->foreignId("company_id")->constrained("companies")->cascadeOnDelete();
$table->foreignId("building_id")->constrained("buildings")->cascadeOnDelete();
$table->string("office_number", 50)->nullable();
$table->timestamps();
$table->primary(["company_id", "building_id"]);
});
}
public function down(): void {
Schema::dropIfExists("company_building");
Schema::dropIfExists("companies");
Schema::dropIfExists("buildings");
}
};
2. Модели
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Building extends Model {
protected $fillable = [
"address",
"city",
"postal_code",
"latitude",
"longitude",
"description",
];
protected $casts = [
"latitude" => "float",
"longitude" => "float",
];
public function companies(): BelongsToMany {
return $this->belongsToMany(Company::class, "company_building")
->withPivot("office_number")
->withTimestamps();
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Company extends Model {
protected $fillable = [
"name",
"phone",
"email",
"opening_time",
"closing_time",
"description",
"is_active",
];
protected $casts = [
"opening_time" => "datetime:H:i",
"closing_time" => "datetime:H:i",
"is_active" => "boolean",
];
public function buildings(): BelongsToMany {
return $this->belongsToMany(Building::class, "company_building")
->withPivot("office_number")
->withTimestamps();
}
}
3. Service для геолокации
<?php
namespace App\Services;
use App\Models\Building;
use App\Models\Company;
use Illuminate\Database\Query\Builder;
class GeoLocationService {
/**
* Константа для конвертирования координат в метры на экваторе
*/
const EARTH_RADIUS_M = 6371000;
/**
* Вычисляет расстояние между двумя точками в метрах
* Использует формулу Хаверсина
*/
public function getDistance(float $lat1, float $lon1, float $lat2, float $lon2): float {
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return self::EARTH_RADIUS_M * $c;
}
/**
* Поиск компаний в радиусе от заданной точки
*
* @param float $lat Широта
* @param float $lng Долгота
* @param int $radiusM Радиус в метрах
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findCompaniesByRadius(float $lat, float $lng, int $radiusM = 1000) {
// Примерный расчёт границ для предварительной фильтрации
$latOffset = ($radiusM / self::EARTH_RADIUS_M) * (180 / M_PI);
$lngOffset = ($radiusM / self::EARTH_RADIUS_M) * (180 / M_PI) / cos(deg2rad($lat));
$companies = Company::where("is_active", true)
->with("buildings")
->get()
->filter(function (Company $company) use ($lat, $lng, $radiusM) {
// Проверяем каждое здание компании
foreach ($company->buildings as $building) {
$distance = $this->getDistance(
$lat,
$lng,
$building->latitude,
$building->longitude
);
if ($distance <= $radiusM) {
return true;
}
}
return false;
})
->values();
return $companies;
}
/**
* Поиск компаний в квадрате координат
*
* @param float $lat1 Широта верхнего левого угла
* @param float $lng1 Долгота верхнего левого угла
* @param float $lat2 Широта нижнего правого угла
* @param float $lng2 Долгота нижнего правого угла
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findCompaniesByBoundingBox(float $lat1, float $lng1, float $lat2, float $lng2) {
$minLat = min($lat1, $lat2);
$maxLat = max($lat1, $lat2);
$minLng = min($lng1, $lng2);
$maxLng = max($lng1, $lng2);
$companies = Company::where("is_active", true)
->with([
"buildings" => function ($query) use ($minLat, $maxLat, $minLng, $maxLng) {
$query->whereBetween("latitude", [$minLat, $maxLat])
->whereBetween("longitude", [$minLng, $maxLng]);
}
])
->get()
->filter(fn($c) => $c->buildings->count() > 0)
->values();
return $companies;
}
/**
* Поиск компаний по названию
*/
public function searchCompaniesByName(string $query) {
return Company::where("is_active", true)
->where("name", "like", "%{$query}%")
->with("buildings")
->get();
}
}
4. API контроллеры
<?php
namespace App\Http\Controllers\Api;
use App\Models\Building;
use App\Models\Company;
use App\Services\GeoLocationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CompanyController extends Controller {
protected $geoService;
public function __construct(GeoLocationService $geoService) {
$this->geoService = $geoService;
}
/**
* Поиск компаний по разным критериям
*/
public function index(Request $request): JsonResponse {
// Поиск по радиусу
if ($request->has(["lat", "lng"])) {
$radius = $request->get("radius", 1000);
$companies = $this->geoService->findCompaniesByRadius(
(float) $request->get("lat"),
(float) $request->get("lng"),
(int) $radius
);
return response()->json([
"type" => "radius_search",
"lat" => $request->get("lat"),
"lng" => $request->get("lng"),
"radius_m" => $radius,
"count" => $companies->count(),
"data" => $companies,
]);
}
// Поиск по квадрату координат
if ($request->has(["lat1", "lng1", "lat2", "lng2"])) {
$companies = $this->geoService->findCompaniesByBoundingBox(
(float) $request->get("lat1"),
(float) $request->get("lng1"),
(float) $request->get("lat2"),
(float) $request->get("lng2")
);
return response()->json([
"type" => "bounding_box_search",
"bounds" => [
"lat1" => $request->get("lat1"),
"lng1" => $request->get("lng1"),
"lat2" => $request->get("lat2"),
"lng2" => $request->get("lng2"),
],
"count" => $companies->count(),
"data" => $companies,
]);
}
// Поиск по названию
if ($request->has("search")) {
$companies = $this->geoService->searchCompaniesByName($request->get("search"));
return response()->json([
"type" => "name_search",
"query" => $request->get("search"),
"count" => $companies->count(),
"data" => $companies,
]);
}
// Все компании
$companies = Company::where("is_active", true)->with("buildings")->paginate(20);
return response()->json($companies);
}
public function show(Company $company): JsonResponse {
return response()->json($company->load("buildings"));
}
public function store(Request $request): JsonResponse {
$validated = $request->validate([
"name" => "required|string|max:255",
"phone" => "nullable|string|max:20",
"email" => "nullable|email",
"opening_time" => "nullable|date_format:H:i",
"closing_time" => "nullable|date_format:H:i",
"description" => "nullable|string",
]);
$company = Company::create($validated);
return response()->json($company, 201);
}
public function update(Request $request, Company $company): JsonResponse {
$validated = $request->validate([
"name" => "string|max:255",
"phone" => "nullable|string|max:20",
"email" => "nullable|email",
"opening_time" => "nullable|date_format:H:i",
"closing_time" => "nullable|date_format:H:i",
"description" => "nullable|string",
]);
$company->update($validated);
return response()->json($company);
}
public function destroy(Company $company): JsonResponse {
$company->delete();
return response()->json(["message" => "Company deleted"]);
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Models\Building;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BuildingController extends Controller {
public function index(): JsonResponse {
$buildings = Building::with("companies")->paginate(20);
return response()->json($buildings);
}
public function show(Building $building): JsonResponse {
return response()->json($building->load("companies"));
}
public function store(Request $request): JsonResponse {
$validated = $request->validate([
"address" => "required|string|max:500",
"city" => "required|string|max:100",
"postal_code" => "nullable|string|max:20",
"latitude" => "required|numeric|between:-90,90",
"longitude" => "required|numeric|between:-180,180",
"description" => "nullable|string",
]);
$building = Building::create($validated);
return response()->json($building, 201);
}
public function update(Request $request, Building $building): JsonResponse {
$validated = $request->validate([
"address" => "string|max:500",
"city" => "string|max:100",
"postal_code" => "nullable|string|max:20",
"latitude" => "numeric|between:-90,90",
"longitude" => "numeric|between:-180,180",
"description" => "nullable|string",
]);
$building->update($validated);
return response()->json($building);
}
public function destroy(Building $building): JsonResponse {
$building->delete();
return response()->json(["message" => "Building deleted"]);
}
public function attachCompany(Request $request, Building $building): JsonResponse {
$validated = $request->validate([
"company_id" => "required|exists:companies,id",
"office_number" => "nullable|string|max:50",
]);
$building->companies()->attach($validated["company_id"], [
"office_number" => $validated["office_number"] ?? null,
]);
return response()->json(["message" => "Company attached"]);
}
}
5. Маршруты API
<?php
use App\Http\Controllers\Api\BuildingController;
use App\Http\Controllers\Api\CompanyController;
use Illuminate\Support\Facades\Route;
Route::get("/companies", [CompanyController::class, "index"]);
Route::get("/companies/{company}", [CompanyController::class, "show"]);
Route::post("/companies", [CompanyController::class, "store"]);
Route::put("/companies/{company}", [CompanyController::class, "update"]);
Route::delete("/companies/{company}", [CompanyController::class, "destroy"]);
Route::get("/buildings", [BuildingController::class, "index"]);
Route::get("/buildings/{building}", [BuildingController::class, "show"]);
Route::post("/buildings", [BuildingController::class, "store"]);
Route::put("/buildings/{building}", [BuildingController::class, "update"]);
Route::delete("/buildings/{building}", [BuildingController::class, "destroy"]);
Route::post("/buildings/{building}/attach-company", [BuildingController::class, "attachCompany"]);
6. Примеры API запросов
# Поиск компаний в радиусе 1000 метров от точки (55.75, 37.62)
GET /api/companies?lat=55.75&lng=37.62&radius=1000
# Поиск компаний в квадрате координат
GET /api/companies?lat1=55.7&lng1=37.5&lat2=55.8&lng2=37.7
# Поиск по названию
GET /api/companies?search=Москва
# Создание здания
POST /api/buildings
{"address": "Красная площадь, 1", "city": "Москва", "latitude": 55.7539, "longitude": 37.6208}
# Присоединение компании к зданию
POST /api/buildings/1/attach-company
{"company_id": 1, "office_number": "101"}
Это полное решение с геолокацией, поиском по радиусу, по квадрату координат и по названию.