Разработка личного кабинета покупателя для интернет-магазина
Личный кабинет — точка удержания покупателя. Здесь хранится история заказов, управляются адреса и подписки, доступны бонусы и возвраты. Слабый кабинет вынуждает звонить в поддержку; сильный — снижает нагрузку на поддержку и повышает retention. Разработка полноценного личного кабинета занимает 7–12 рабочих дней.
Структура разделов
Типовой личный кабинет интернет-магазина включает:
| Раздел | URL | Основной функционал |
|---|---|---|
| Обзор | /account |
Краткая сводка: последние заказы, бонусы, уведомления |
| Заказы | /account/orders |
История заказов, фильтры, трекинг |
| Возвраты | /account/returns |
Статус RMA-запросов |
| Адреса | /account/addresses |
CRUD адресов доставки |
| Данные профиля | /account/profile |
Имя, телефон, email, смена пароля |
| Подписки | /account/subscriptions |
Email-рассылки, push-уведомления |
| Список желаний | /account/wishlist |
Сохранённые товары |
| Бонусы | /account/bonuses |
Баланс, история начислений/списаний |
Роутинг и защита
Все маршруты кабинета защищены middleware auth. Для Inertia.js + Laravel:
Route::middleware(['auth', 'verified'])->prefix('account')->name('account.')->group(function () {
Route::get('/', [AccountController::class, 'dashboard'])->name('dashboard');
Route::get('/orders', [OrderController::class, 'index'])->name('orders.index');
Route::get('/orders/{order}', [OrderController::class, 'show'])
->can('view', 'order')
->name('orders.show');
Route::resource('addresses', AddressController::class)->except('index');
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
});
Policy OrderPolicy гарантирует, что пользователь видит только свои заказы: $user->id === $order->user_id.
Dashboard — главный экран
Экран обзора собирает данные из нескольких источников, но делает это одним запросом:
public function dashboard(Request $request): Response
{
$user = $request->user()->load([
'orders' => fn($q) => $q->latest()->limit(3)->with('items.product'),
'bonusAccount',
'activeReturns',
]);
return Inertia::render('Account/Dashboard', [
'recentOrders' => OrderResource::collection($user->orders),
'bonusBalance' => $user->bonusAccount?->balance ?? 0,
'pendingReturns' => $user->activeReturns->count(),
'notifications' => $user->unreadNotifications()->limit(5)->get(),
]);
}
Управление адресами
У пользователя может быть несколько адресов: дом, офис, пункт самовывоза. Один адрес помечается как default:
class Address extends Model
{
protected $fillable = [
'user_id', 'label', 'full_name', 'phone',
'country', 'region', 'city', 'street', 'house',
'apartment', 'postal_code', 'is_default',
];
public function setAsDefault(): void
{
DB::transaction(function () {
$this->user->addresses()->update(['is_default' => false]);
$this->update(['is_default' => true]);
});
}
}
Форма добавления/редактирования адреса с DaData-автодополнением переиспользуется из checkout.
Смена email и двухфакторная верификация
Смена email — чувствительная операция. Процесс:
- Пользователь вводит новый email
- На новый email отправляется письмо с токеном подтверждения
- Только после перехода по ссылке email обновляется в БД
- На старый email отправляется уведомление об изменении
public function updateEmail(Request $request): void
{
$request->validate(['email' => 'required|email|unique:users,email']);
$token = Str::random(64);
Cache::put("email_change:{$token}", [
'user_id' => $request->user()->id,
'new_email' => $request->email,
], now()->addHours(2));
Mail::to($request->email)->send(new EmailChangeConfirmation($token));
}
Управление подписками
Центральное место для управления коммуникационными предпочтениями:
const SubscriptionSettings = () => {
const { preferences, toggle } = useNotificationPreferences();
const options = [
{ key: 'order_updates', label: 'Статус заказов' },
{ key: 'promotions', label: 'Акции и скидки' },
{ key: 'back_in_stock', label: 'Товар снова в наличии' },
{ key: 'price_drops', label: 'Снижение цен на избранное' },
{ key: 'newsletter', label: 'Еженедельная рассылка' },
];
return (
<div className="space-y-3">
{options.map(({ key, label }) => (
<div key={key} className="flex items-center justify-between">
<span>{label}</span>
<Switch checked={preferences[key]} onCheckedChange={() => toggle(key)} />
</div>
))}
</div>
);
};
Безопасность: смена пароля
Смена пароля требует ввода текущего пароля и подтверждения нового:
public function updatePassword(Request $request): void
{
$request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', 'min:8', 'confirmed', Password::defaults()],
]);
$request->user()->update([
'password' => Hash::make($request->password),
]);
// Инвалидируем все другие сессии
Auth::logoutOtherDevices($request->password);
event(new PasswordChanged($request->user()));
}
Удаление аккаунта (GDPR)
По требованиям GDPR/российского 152-ФЗ пользователь должен иметь возможность удалить свой аккаунт. Soft delete с анонимизацией персональных данных:
public function deleteAccount(Request $request): void
{
$user = $request->user();
// Анонимизация вместо физического удаления
$user->update([
'email' => "deleted_{$user->id}@removed.invalid",
'name' => 'Удалённый пользователь',
'phone' => null,
'deleted_at' => now(),
]);
Auth::logout();
}
Заказы при этом сохраняются для бухгалтерской отчётности — только персональные данные анонимизируются.
Мобильная адаптация
Кабинет на мобильных — отдельный layout с нижней навигацией (tab bar) вместо боковой. Критичные операции (смена статуса заказа, просмотр трекинга) должны быть доступны в 2-3 клика от главного экрана.







