本文目录导读:

- 方案一:使用Laravel内置的Notifiable Trait(推荐)
- 方案二:自定义通知表(更灵活)
- 方案三:前端实现示例(Vue/React)
- 方案四:使用缓存优化
- 方案五:实时更新(使用Pusher/Broadcasting)
- 最佳实践建议
在Laravel中实现通知已读/未读标记功能,有几种不同的方案,我来详细介绍最常用和灵活的几种方法:
使用Laravel内置的Notifiable Trait(推荐)
迁移表结构
// 创建通知表迁移 php artisan notifications:table php artisan migrate
模型中使用Notifiable
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
// 获取未读通知
public function unreadNotifications()
{
return $this->notifications()->whereNull('read_at');
}
// 获取已读通知
public function readNotifications()
{
return $this->notifications()->whereNotNull('read_at');
}
}
控制器操作
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
/**
* 获取所有通知
*/
public function index()
{
$notifications = auth()->user()->notifications()
->paginate(20);
return response()->json([
'data' => $notifications,
'unread_count' => auth()->user()->unreadNotifications()->count()
]);
}
/**
* 获取未读通知
*/
public function unread()
{
$notifications = auth()->user()->unreadNotifications()
->paginate(20);
return response()->json($notifications);
}
/**
* 标记单个通知为已读
*/
public function markAsRead($id)
{
$notification = auth()->user()->notifications()
->where('id', $id)
->first();
if ($notification) {
$notification->markAsRead();
return response()->json(['message' => '标记成功']);
}
return response()->json(['message' => '通知不存在'], 404);
}
/**
* 批量标记为已读
*/
public function markAllAsRead()
{
auth()->user()->unreadNotifications()
->update(['read_at' => now()]);
return response()->json(['message' => '全部标记为已读']);
}
/**
* 标记为未读
*/
public function markAsUnread($id)
{
$notification = auth()->user()->notifications()
->where('id', $id)
->first();
if ($notification) {
$notification->markAsUnread();
return response()->json(['message' => '标记为未读成功']);
}
return response()->json(['message' => '通知不存在'], 404);
}
/**
* 删除通知
*/
public function delete($id)
{
auth()->user()->notifications()
->where('id', $id)
->delete();
return response()->json(['message' => '删除成功']);
}
}
路由配置
// routes/api.php 或 routes/web.php
Route::middleware('auth')->group(function () {
Route::get('/notifications', [NotificationController::class, 'index']);
Route::get('/notifications/unread', [NotificationController::class, 'unread']);
Route::put('/notifications/{id}/read', [NotificationController::class, 'markAsRead']);
Route::put('/notifications/read-all', [NotificationController::class, 'markAllAsRead']);
Route::put('/notifications/{id}/unread', [NotificationController::class, 'markAsUnread']);
Route::delete('/notifications/{id}', [NotificationController::class, 'delete']);
});
创建通知类
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class OrderStatusNotification extends Notification implements ShouldQueue
{
use Queueable;
protected $order;
public function __construct($order)
{
$this->order = $order;
}
public function via($notifiable)
{
return ['database', 'mail']; // 支持数据库和邮件
}
public function toDatabase($notifiable)
{
return [
'order_id' => $this->order->id,
'status' => $this->order->status,
'message' => '您的订单状态已更新为:' . $this->order->status,
'url' => route('orders.show', $this->order->id)
];
}
public function toMail($notifiable)
{
return (new MailMessage)
->subject('订单状态更新')
->line('您的订单状态已更新为:' . $this->order->status)
->action('查看订单', url('/orders/' . $this->order->id))
->line('感谢您的支持!');
}
}
发送通知
// 在业务代码中发送通知 use App\Notifications\OrderStatusNotification; // 发送给单个用户 $user->notify(new OrderStatusNotification($order)); // 发送给多个用户 Notification::send($users, new OrderStatusNotification($order)); // 延迟发送 $user->notify((new OrderStatusNotification($order))->delay(now()->addMinutes(5)));
自定义通知表(更灵活)
如果需要更多自定义字段,可以创建自定义的通知表:
// 创建自定义通知表迁移
php artisan make:migration create_custom_notifications_table
// 迁移文件
Schema::create('custom_notifications', function (Blueprint $table) {
$table->id();
$table->morphs('notifiable');
$table->string('type');
$table->morphs('related_model'); // 关联的业务模型
$table->string('title');
$table->text('message');
$table->json('data')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamp('sent_at')->nullable();
$table->timestamps();
$table->index(['notifiable_id', 'read_at']); // 优化查询
});
前端实现示例(Vue/React)
Vue前端实现
<template>
<div class="notification-dropdown">
<button @click="toggleDropdown" class="bell-icon">
<span class="badge" v-if="unreadCount > 0">{{ unreadCount }}</span>
🔔
</button>
<div v-if="isOpen" class="dropdown-menu">
<div class="header">
<h3>通知</h3>
<button @click="markAllAsRead" v-if="unreadCount > 0">全部已读</button>
</div>
<div class="notifications-list">
<div v-for="notification in notifications" :key="notification.id"
:class="['notification-item', { unread: !notification.read_at }]"
@click="readNotification(notification)">
<div class="notification-content">
<div class="title">{{ notification.data.title }}</div>
<div class="message">{{ notification.data.message }}</div>
<div class="time">{{ formatTime(notification.created_at) }}</div>
</div>
<button v-if="notification.read_at" @click.stop="markUnread(notification.id)">标记未读</button>
</div>
</div>
<div v-if="notifications.length === 0" class="empty-state">
暂无通知
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
isOpen: false,
notifications: [],
unreadCount: 0,
polling: null
};
},
mounted() {
this.fetchNotifications();
this.startPolling();
},
beforeDestroy() {
if (this.polling) {
clearInterval(this.polling);
}
},
methods: {
fetchNotifications() {
axios.get('/api/notifications')
.then(response => {
this.notifications = response.data.data;
this.unreadCount = response.data.unread_count;
})
.catch(error => {
console.error('获取通知失败:', error);
});
},
readNotification(notification) {
if (notification.read_at) return;
axios.put(`/api/notifications/${notification.id}/read`)
.then(() => {
notification.read_at = new Date();
this.unreadCount--;
// 如果需要跳转
if (notification.data.url) {
window.location.href = notification.data.url;
}
})
.catch(error => {
console.error('标记已读失败:', error);
});
},
markUnread(id) {
axios.put(`/api/notifications/${id}/unread`)
.then(() => {
const notification = this.notifications.find(n => n.id === id);
notification.read_at = null;
this.unreadCount++;
})
.catch(error => {
console.error('标记未读失败:', error);
});
},
markAllAsRead() {
axios.put('/api/notifications/read-all')
.then(() => {
this.notifications.forEach(n => n.read_at = new Date());
this.unreadCount = 0;
})
.catch(error => {
console.error('全部标记已读失败:', error);
});
},
toggleDropdown() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.fetchNotifications();
}
},
startPolling() {
this.polling = setInterval(() => {
this.fetchNotifications();
}, 30000); // 每30秒轮询一次
},
formatTime(date) {
const d = new Date(date);
const now = new Date();
const diff = now - d;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
return d.toLocaleDateString();
}
}
};
</script>
<style scoped>
/* 样式略 */
</style>
使用缓存优化
// 使用缓存缓存未读数量
class NotificationService
{
public function getUserUnreadCount($userId)
{
return \Cache::remember(
"user_unread_notifications_{$userId}",
60, // 缓存60秒
function () use ($userId) {
return \App\Models\User::find($userId)
->unreadNotifications()
->count();
}
);
}
public function clearUnreadCache($userId)
{
\Cache::forget("user_unread_notifications_{$userId}");
}
}
实时更新(使用Pusher/Broadcasting)
// 在通知模型中添加broadcast channel
class OrderStatusNotification extends Notification
{
use Queueable;
public function via($notifiable)
{
return ['database', 'broadcast'];
}
public function toBroadcast($notifiable)
{
return new BroadcastMessage([
'title' => '订单状态更新',
'message' => '您的订单状态已更新',
'url' => url('/orders/' . $this->order->id)
]);
}
public function broadcastType()
{
return 'order-status';
}
}
// 前端实时监听
// 使用Laravel Echo
window.Echo.private('App.User.' + userId)
.notification((notification) => {
this.notifications.unshift(notification);
this.unreadCount++;
});
最佳实践建议
- 数据库索引:确保
read_at字段有索引 - 批量操作:批量标记已读时使用
update()而不是循环处理 - 缓存优化:频繁查询未读数量时使用缓存
- 分页加载:通知列表使用分页,避免一次性加载太多
- 实时性:根据需求判断是否使用websocket实时推送
- 并发处理:注意处理并发标记已读的情况
- 安全性:确保用户只能操作自己的通知
这个方案涵盖了大多数场景,可以根据具体需求选择使用,如果需要更详细的某个部分实现,请告诉我。