PHP项目Laravel通知已读未读标记

wen PHP项目 3

本文目录导读:

PHP项目Laravel通知已读未读标记

  1. 方案一:使用Laravel内置的Notifiable Trait(推荐)
  2. 方案二:自定义通知表(更灵活)
  3. 方案三:前端实现示例(Vue/React)
  4. 方案四:使用缓存优化
  5. 方案五:实时更新(使用Pusher/Broadcasting)
  6. 最佳实践建议

在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++;
    });

最佳实践建议

  1. 数据库索引:确保read_at字段有索引
  2. 批量操作:批量标记已读时使用update()而不是循环处理
  3. 缓存优化:频繁查询未读数量时使用缓存
  4. 分页加载:通知列表使用分页,避免一次性加载太多
  5. 实时性:根据需求判断是否使用websocket实时推送
  6. 并发处理:注意处理并发标记已读的情况
  7. 安全性:确保用户只能操作自己的通知

这个方案涵盖了大多数场景,可以根据具体需求选择使用,如果需要更详细的某个部分实现,请告诉我。

抱歉,评论功能暂时关闭!