← Назад к вопросам
Сервис консультаций для абитуриентов
2.0 Middle🔥 71 комментариев
#API и веб-протоколы#Очереди и брокеры сообщений#Фреймворки
Условие
Приёмная комиссия проводит индивидуальные и групповые консультации для абитуриентов (очно и онлайн). Нужно сделать сервис для записи на консультации.
Функциональность
- Хранение списка консультаций
- Запись абитуриентов на консультации
- Контроль количества мест
- API для внешних сервисов
Сущности
Консультация:
- Название, дата/время, формат (очно/онлайн), макс. количество мест
Запись:
- ФИО абитуриента, email, телефон, консультация
API
- GET /api/consultations - список доступных консультаций
- POST /api/consultations/{id}/register - запись на консультацию
- DELETE /api/registrations/{id} - отмена записи
Требования
- Нельзя записаться, если мест нет
- Email уведомление при записи
- Напоминание за день до консультации
Комментарии (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("consultations", function (Blueprint $table) {
$table->id();
$table->string("title", 255);
$table->text("description")->nullable();
$table->dateTime("start_at");
$table->dateTime("end_at");
$table->enum("format", ["offline", "online"]);
$table->integer("max_participants");
$table->string("location", 255)->nullable(); // Для очных
$table->string("meeting_link", 500)->nullable(); // Для онлайн
$table->boolean("is_active")->default(true);
$table->timestamps();
$table->index("start_at");
$table->index("is_active");
});
// Таблица записей на консультации
Schema::create("registrations", function (Blueprint $table) {
$table->id();
$table->foreignId("consultation_id")->constrained("consultations")->cascadeOnDelete();
$table->string("full_name", 255);
$table->string("email", 255);
$table->string("phone", 20);
$table->text("comment")->nullable();
$table->enum("status", ["active", "cancelled"])->default("active");
$table->timestamp("registered_at");
$table->timestamp("cancelled_at")->nullable();
$table->timestamps();
$table->unique(["consultation_id", "email"]);
$table->index("consultation_id");
$table->index("email");
$table->index("status");
});
}
public function down(): void {
Schema::dropIfExists("registrations");
Schema::dropIfExists("consultations");
}
};
2. Модели
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Consultation extends Model {
protected $fillable = [
"title",
"description",
"start_at",
"end_at",
"format",
"max_participants",
"location",
"meeting_link",
"is_active",
];
protected $casts = [
"start_at" => "datetime",
"end_at" => "datetime",
"is_active" => "boolean",
];
public function registrations(): HasMany {
return $this->hasMany(Registration::class);
}
public function getAvailableSpotsAttribute(): int {
$active = $this->registrations()
->where("status", "active")
->count();
return $this->max_participants - $active;
}
public function isAvailable(): bool {
return $this->is_active
&& $this->start_at > now()
&& $this->getAvailableSpotsAttribute() > 0;
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Registration extends Model {
protected $fillable = [
"consultation_id",
"full_name",
"email",
"phone",
"comment",
"status",
"registered_at",
"cancelled_at",
];
protected $casts = [
"registered_at" => "datetime",
"cancelled_at" => "datetime",
];
public function consultation(): BelongsTo {
return $this->belongsTo(Consultation::class);
}
}
3. Mail классы
<?php
namespace App\Mail;
use App\Models\Registration;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class RegistrationConfirmation extends Mailable {
use Queueable, SerializesModels;
public function __construct(private Registration $registration) {}
public function envelope(): Envelope {
return new Envelope(
subject: "Подтверждение записи на консультацию: {$this->registration->consultation->title}",
);
}
public function content(): Content {
return new Content(
view: "emails.registration_confirmation",
with: [
"registration" => $this->registration,
"consultation" => $this->registration->consultation,
],
);
}
}
<?php
namespace App\Mail;
use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ConsultationReminder extends Mailable {
use Queueable, SerializesModels;
public function __construct(private Consultation $consultation) {}
public function envelope(): Envelope {
return new Envelope(
subject: "Напоминание: завтра консультация '{$this->consultation->title}'",
);
}
public function content(): Content {
return new Content(
view: "emails.consultation_reminder",
with: ["consultation" => $this->consultation],
);
}
}
4. Service для управления записями
<?php
namespace App\Services;
use App\Mail\ConsultationReminder;
use App\Mail\RegistrationConfirmation;
use App\Models\Consultation;
use App\Models\Registration;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\ValidationException;
class ConsultationService {
/**
* Записать абитуриента на консультацию
*/
public function register(Consultation $consultation, array $data): Registration {
// Проверка доступности
if (!$consultation->isAvailable()) {
throw ValidationException::withMessages([
"consultation" => "К сожалению, мест на эту консультацию больше нет."
]);
}
// Проверка дублирования
$exists = Registration::where("consultation_id", $consultation->id)
->where("email", $data["email"])
->where("status", "active")
->exists();
if ($exists) {
throw ValidationException::withMessages([
"email" => "Вы уже записаны на эту консультацию."
]);
}
// Создание записи
$registration = Registration::create([
"consultation_id" => $consultation->id,
"full_name" => $data["full_name"],
"email" => $data["email"],
"phone" => $data["phone"],
"comment" => $data["comment"] ?? null,
"registered_at" => now(),
]);
// Отправка email подтверждения
Mail::queue(new RegistrationConfirmation($registration));
return $registration;
}
/**
* Отменить запись
*/
public function cancel(Registration $registration): void {
$registration->update([
"status" => "cancelled",
"cancelled_at" => now(),
]);
}
}
5. API контроллер
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\RegisterConsultationRequest;
use App\Models\Consultation;
use App\Models\Registration;
use App\Services\ConsultationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ConsultationController extends Controller {
protected $service;
public function __construct(ConsultationService $service) {
$this->service = $service;
}
/**
* GET /api/consultations - список доступных консультаций
*/
public function index(Request $request): JsonResponse {
$query = Consultation::where("is_active", true)
->where("start_at", ">", now())
->orderBy("start_at");
// Фильтр по формату
if ($request->has("format")) {
$query->where("format", $request->get("format"));
}
// Фильтр по датам
if ($request->has("from")) {
$query->where("start_at", ">", $request->get("from"));
}
if ($request->has("to")) {
$query->where("start_at", "<", $request->get("to"));
}
$consultations = $query->paginate(20);
return response()->json($consultations->map(function (Consultation $c) {
return [
"id" => $c->id,
"title" => $c->title,
"description" => $c->description,
"start_at" => $c->start_at,
"end_at" => $c->end_at,
"format" => $c->format,
"location" => $c->location,
"meeting_link" => $c->meeting_link,
"available_spots" => $c->available_spots,
"max_participants" => $c->max_participants,
];
}));
}
/**
* POST /api/consultations/{id}/register - запись на консультацию
*/
public function register(RegisterConsultationRequest $request, Consultation $consultation): JsonResponse {
$registration = $this->service->register($consultation, $request->validated());
return response()->json([
"message" => "Вы успешно записались на консультацию",
"registration" => [
"id" => $registration->id,
"consultation" => $consultation->title,
"date" => $consultation->start_at->format("d.m.Y H:i"),
"format" => $consultation->format,
"confirmation_email" => $registration->email,
],
], 201);
}
/**
* DELETE /api/registrations/{id} - отмена записи
*/
public function cancel(Registration $registration): JsonResponse {
$this->service->cancel($registration);
return response()->json([
"message" => "Ваша запись отменена",
]);
}
/**
* GET /api/registrations/{id} - информация о записи
*/
public function show(Registration $registration): JsonResponse {
return response()->json([
"id" => $registration->id,
"full_name" => $registration->full_name,
"email" => $registration->email,
"phone" => $registration->phone,
"consultation" => [
"id" => $registration->consultation->id,
"title" => $registration->consultation->title,
"start_at" => $registration->consultation->start_at,
"format" => $registration->consultation->format,
],
"status" => $registration->status,
"registered_at" => $registration->registered_at,
]);
}
}
6. Form Request валидация
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterConsultationRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
"full_name" => "required|string|max:255",
"email" => "required|email|max:255",
"phone" => "required|string|max:20|regex:/^[+\\d\\s\\-\\(\\)]+$/",
"comment" => "nullable|string|max:1000",
];
}
public function messages(): array {
return [
"full_name.required" => "ФИО обязательно",
"email.required" => "Email обязателен",
"phone.required" => "Телефон обязателен",
"phone.regex" => "Некорректный формат телефона",
];
}
}
7. Маршруты
<?php
use App\Http\Controllers\Api\ConsultationController;
use Illuminate\Support\Facades\Route;
Route::get("/consultations", [ConsultationController::class, "index"]);
Route::post("/consultations/{consultation}/register", [ConsultationController::class, "register"]);
Route::get("/registrations/{registration}", [ConsultationController::class, "show"]);
Route::delete("/registrations/{registration}", [ConsultationController::class, "cancel"]);
8. Scheduled job для напоминаний
<?php
namespace App\Console\Commands;
use App\Mail\ConsultationReminder;
use App\Models\Consultation;
use App\Models\Registration;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendConsultationReminders extends Command {
protected $signature = "consultation:send-reminders";
protected $description = "Send reminders for consultations tomorrow";
public function handle(): void {
// Консультации на завтра
$tomorrow = now()->addDay()->startOfDay();
$tomorrowEnd = now()->addDay()->endOfDay();
$consultations = Consultation::whereBetween("start_at", [$tomorrow, $tomorrowEnd])
->get();
foreach ($consultations as $consultation) {
// Отправляем напоминание всем, кто записан
$registrations = Registration::where("consultation_id", $consultation->id)
->where("status", "active")
->get();
foreach ($registrations as $registration) {
Mail::queue(new ConsultationReminder($consultation));
}
}
$this->info("Reminders sent successfully");
}
}
9. Конфигурация планировщика (app/Console/Kernel.php)
<?php
public function schedule(Schedule $schedule) {
// Отправка напоминаний каждый день в 10:00
$schedule->command("consultation:send-reminders")->dailyAt("10:00");
}
10. Email шаблоны
{{-- resources/views/emails/registration_confirmation.blade.php --}}
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h2>Спасибо за запись!</h2>
<p>{{ $registration->full_name }}, вы успешно записались на консультацию.</p>
<h3>Детали:</h3>
<ul>
<li><strong>Консультация:</strong> {{ $consultation->title }}</li>
<li><strong>Дата и время:</strong> {{ $consultation->start_at->format('d.m.Y H:i') }}</li>
<li><strong>Формат:</strong> {{ $consultation->format === 'offline' ? 'Очно' : 'Онлайн' }}</li>
@if($consultation->format === 'offline')
<li><strong>Место:</strong> {{ $consultation->location }}</li>
@else
<li><strong>Ссылка на встречу:</strong> {{ $consultation->meeting_link }}</li>
@endif
</ul>
<p>До скорого встречи!</p>
</body>
</html>
11. Тесты
<?php
namespace Tests\Feature;
use App\Models\Consultation;
use App\Models\Registration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ConsultationApiTest extends TestCase {
use RefreshDatabase;
public function test_can_see_available_consultations(): void {
Consultation::factory()->create(["start_at" => now()->addDay()]);
$response = $this->getJson("/api/consultations");
$response->assertStatus(200);
}
public function test_can_register_for_consultation(): void {
$consultation = Consultation::factory()->create();
$response = $this->postJson("/api/consultations/{$consultation->id}/register", [
"full_name" => "John Doe",
"email" => "john@example.com",
"phone" => "+7 (999) 123-45-67",
]);
$response->assertStatus(201);
$this->assertDatabaseHas("registrations", ["email" => "john@example.com"]);
}
public function test_cannot_register_without_available_spots(): void {
$consultation = Consultation::factory()->create(["max_participants" => 1]);
Registration::factory()->create(["consultation_id" => $consultation->id]);
$response = $this->postJson("/api/consultations/{$consultation->id}/register", [
"full_name" => "Jane Doe",
"email" => "jane@example.com",
"phone" => "+7 (999) 987-65-43",
]);
$response->assertStatus(422);
}
public function test_can_cancel_registration(): void {
$registration = Registration::factory()->create();
$response = $this->deleteJson("/api/registrations/{$registration->id}");
$response->assertStatus(200);
$registration->refresh();
$this->assertEquals("cancelled", $registration->status);
}
}
Это полное решение с API, email уведомлениями, проверкой доступности и напоминаниями за день до консультации.