本文目录导读:

在 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。