PHP项目如何实现用户积分变动通知?

wen java案例 1

本文目录导读:

PHP项目如何实现用户积分变动通知?

  1. 核心数据模型设计
  2. 业务逻辑封装
  3. 通知实现方式(按场景选择)
  4. 异步处理(大流量优化)
  5. 通用前端通知UI示例(结合多种方式)
  6. 最佳实践建议

在 PHP 项目中实现用户积分变动通知,通常需要根据实时性要求系统规模用户体验来选择不同的技术方案,以下是从简单到复杂、从同步到异步的几种常见实现方式:

核心数据模型设计

无论采用哪种通知方式,首先需要设计好积分变动的记录表,这是所有通知的基础。

-- 积分变动记录表 (Points Log)
CREATE TABLE `points_log` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NOT NULL,
    `points` INT NOT NULL COMMENT '变动数值,正为增加,负为减少',
    `balance` INT NOT NULL COMMENT '变动后的余额',
    `type` VARCHAR(50) NOT NULL COMMENT '变动类型,如: sign_in, purchase, refund, admin_adjust',
    `description` VARCHAR(255) COMMENT '变动原因说明',
    `related_id` VARCHAR(64) COMMENT '关联业务ID,如订单号',
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX `idx_user_id_created` (`user_id`, `created_at` DESC)
);
-- 用户积分汇总表 (Users 表增加积分字段)
ALTER TABLE `users` ADD COLUMN `points_balance` INT NOT NULL DEFAULT 0 COMMENT '积分余额';

业务逻辑封装

建议将积分变动逻辑封装成一个统一的服务类,确保每次变动都能触发通知:

<?php
namespace App\Services;
class PointsService
{
    protected $userModel;
    protected $notifier;
    public function __construct($notifier = null) {
        $this->userModel = new User(); // 你的用户模型
        $this->notifier = $notifier ?? new PointsNotifier();
    }
    /**
     * 执行积分变动并触发通知
     */
    public function adjustPoints(int $userId, int $points, string $type, string $description = '', $relatedId = null): bool
    {
        // 1. 开启数据库事务
        DB::beginTransaction();
        try {
            // 2. 获取用户并锁定行(避免并发问题)
            $user = $this->userModel->lockForUpdate()->find($userId);
            if (!$user) {
                throw new \Exception('用户不存在');
            }
            // 3. 检查积分是否足够(如果是减少)
            if ($points < 0 && $user->points_balance + $points < 0) {
                throw new \Exception('积分不足');
            }
            // 4. 更新用户积分余额
            $oldBalance = $user->points_balance;
            $newBalance = $oldBalance + $points;
            $user->points_balance = $newBalance;
            $user->save();
            // 5. 记录积分变动日志
            $log = new PointsLog();
            $log->user_id = $userId;
            $log->points = $points;
            $log->balance = $newBalance;
            $log->type = $type;
            $log->description = $description;
            $log->related_id = $relatedId;
            $log->save();
            DB::commit();
            // 6. 发送通知(在事务提交后执行)
            $this->notifier->notifyPointsChange($userId, $points, $newBalance, $type, $description);
            return true;
        } catch (\Exception $e) {
            DB::rollBack();
            // 记录错误日志
            Log::error('积分变动失败: ' . $e->getMessage());
            return false;
        }
    }
}

通知实现方式(按场景选择)

站内通知(数据库+前端轮询)—— 推荐基础项目使用

先建站内通知表,然后通过前端轮询或 WebSocket 推送。

CREATE TABLE `user_notifications` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NOT NULL,
    `type` VARCHAR(50) NOT NULL DEFAULT 'points', VARCHAR(255) NOT NULL,
    `content` TEXT,
    `is_read` TINYINT(1) DEFAULT 0,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX `idx_user_read` (`user_id`, `is_read`)
);

通知逻辑:

<?php
namespace App\Services;
class PointsNotifier
{
    public function notifyPointsChange(int $userId, int $points, int $newBalance, string $type, string $description)
    {
        $action = $points > 0 ? '增加' : '减少';
        $absPoints = abs($points);
        $notification = new UserNotification();
        $notification->user_id = $userId;
        $notification->type = 'points';
        $notification->title = "积分{$action}通知";
        $notification->content = "您的积分{$action}了 {$absPoints} 点,当前余额:{$newBalance},原因:{$description}";
        $notification->save();
    }
}

前端轮询示例(JS):

// 每30秒检查一次新通知
setInterval(() => {
    fetch('/api/notifications/unread-count')
        .then(response => response.json())
        .then(data => {
            if (data.count > 0) {
                // 显示红点或弹出通知
                showBadge(data.count);
                // 可选:获取最新通知详情
                fetch('/api/notifications/latest')
                    .then(res => res.json())
                    .then(notifications => {
                        notifications.forEach(n => showToast(n.title, n.content));
                    });
            }
        });
}, 30000);

优点:实现简单,无需额外组件。
缺点:有一定的延迟(取决于轮询间隔),服务器压力随用户数增加。


邮件通知

适合积分变动不频繁、非实时要求的场景。

use Illuminate\Support\Facades\Mail;
class PointsNotifier
{
    public function notifyPointsChange(int $userId, int $points, int $newBalance, string $type, string $description)
    {
        $user = User::find($userId);
        if (!$user || !$user->email) return;
        $action = $points > 0 ? '增加了' : '减少了';
        $absPoints = abs($points);
        Mail::send('emails.points_change', [
            'user' => $user,
            'action' => $action,
            'points' => $absPoints,
            'balance' => $newBalance,
            'description' => $description,
            'type' => $type
        ], function ($message) use ($user) {
            $message->to($user->email)
                    ->subject('积分变动通知');
        });
    }
}

短信通知

适合重要积分变动(如大额提现、充值),使用第三方短信服务(阿里云、腾讯云):

use AlibabaCloud\Client\AlibabaCloud;
class PointsNotifier
{
    public function notifyPointsChange(int $userId, int $points, int $newBalance, string $type, string $description)
    {
        $user = User::find($userId);
        if (!$user || !$user->phone) return;
        // 仅对重要类型发送短信
        $importantTypes = ['withdraw', 'large_deposit', 'admin_adjust'];
        if (!in_array($type, $importantTypes)) return;
        $action = $points > 0 ? '增加' : '减少';
        $absPoints = abs($points);
        // 调用短信SDK
        AlibabaCloud::rpc()
            ->product('Dysmsapi')
            ->scheme('https')
            ->version('2017-05-25')
            ->action('SendSms')
            ->method('POST')
            ->options([
                'query' => [
                    'PhoneNumbers' => $user->phone,
                    'SignName' => '你的短信签名',
                    'TemplateCode' => '积分变动通知模板',
                    'TemplateParam' => json_encode([
                        'action' => $action,
                        'points' => $absPoints,
                        'balance' => $newBalance
                    ])
                ],
            ])
            ->request();
    }
}

WebSocket 实时推送(推荐高实时性场景)

使用 Laravel WebSocket(Laravel + Pusher / soketi) 或 Workerman,以 Laravel 为例:

安装和配置 Laravel WebSocket

composer require beyondcode/laravel-websockets
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
php artisan migrate
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

定义事件

namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
class PointsChanged implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets;
    public $userId;
    public $points;
    public $balance;
    public $type;
    public function __construct($userId, $points, $balance, $type)
    {
        $this->userId = $userId;
        $this->points = $points;
        $this->balance = $balance;
        $this->type = $type;
    }
    public function broadcastOn()
    {
        return new Channel('user.' . $this->userId);
    }
    public function broadcastAs()
    {
        return 'points.updated';
    }
}

在积分变动处触发事件

event(new PointsChanged($userId, $points, $newBalance, $type));

前端监听(Laravel Echo)

import Echo from 'laravel-echo';
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-key',
    wsHost: window.location.hostname,
    wsPort: 6001, // WebSocket 端口
    forceTLS: false,
    disableStats: true,
});
// 监听特定用户的通知
Echo.channel('user.' + userId)
    .listen('.points.updated', (e) => {
        console.log('积分变动:', e.points);
        showNotification(`积分${e.points > 0 ? '增加' : '减少'}`, `当前余额: ${e.balance}`);
    });

优点:实时性高、用户体验好。
缺点:需要额外部署 WebSocket 服务器,运维成本较高。


异步处理(大流量优化)

如果积分变动频繁,建议将通知放入消息队列,不阻塞主流程。

// 使用 Laravel 队列
use App\Jobs\SendPointsNotification;
// 在 PointsService 中,将通知改为异步
dispatch(new SendPointsNotification($userId, $points, $newBalance, $type, $description));
class SendPointsNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    public function __construct(public $userId, public $points, public $balance, public $type, public $description) {}
    public function handle()
    {
        $notifier = new PointsNotifier();
        $notifier->notifyPointsChange($this->userId, $this->points, $this->balance, $this->type, $this->description);
    }
}

通用前端通知UI示例(结合多种方式)

// 统一的积分变动提示函数
function showPointsNotification(points, balance, type, description) {
    const isPositive = points > 0;
    const absPoints = Math.abs(points);
    const message = `积分${isPositive ? '增加' : '减少'} ${absPoints} 点,当前余额 ${balance} 点`;
    // 1. 页面顶部弹窗
    const toast = document.createElement('div');
    toast.className = `points-toast ${isPositive ? 'positive' : 'negative'}`;
    toast.innerHTML = `<span>${message}</span><button onclick="this.parentElement.remove()">×</button>`;
    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), 5000);
    // 2. 更新页面元素的积分显示
    document.querySelectorAll('.points-display').forEach(el => {
        if (el.dataset.userId === userId) {
            el.textContent = balance;
            el.classList.add('points-flash');
            setTimeout(() => el.classList.remove('points-flash'), 1000);
        }
    });
    // 3. 如果是 JS SDK 形式,可以记录到用户的消息中心
    if (window.userNotificationCenter) {
        window.userNotificationCenter.add({
            type: 'points',
            title: '积分变动',
            content: description || message,
            timestamp: Date.now()
        });
    }
}

最佳实践建议

场景 推荐方案
小型项目(用户<1000) 站内通知表 + 前端轮询
中型项目(实时性要求一般) 站内通知 + 邮件(重要变动)
中型项目(高实时性) WebSocket + 站内通知
大型项目/高并发 消息队列 + WebSocket + 多种通知渠道
移动端为主 WebSocket + 配合APP推送

安全注意事项:

  • 积分变动操作要有幂等性(防止重复执行导致积分出错)
  • 使用数据库行锁或乐观锁防止并发积分错误
  • 敏感操作(如管理员手动调整)需要操作日志双人复核避免泄露敏感信息

选择哪种方案取决于你的项目预算、技术栈和用户规模,对于大多数 PHP 项目,站内通知(数据库)+ 前端轮询 是最简单且足够使用的方案;只有当你需要极致的实时体验时,才考虑 WebSocket。

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