初始化本地仓库
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace core\notify;
|
||||
|
||||
use core\notify\enum\PushClientType;
|
||||
use core\notify\NotificationService;
|
||||
use support\Container;
|
||||
|
||||
|
||||
/**
|
||||
* - 消息通知服务门面类
|
||||
* - 提供静态方法访问通知服务的快捷方式,支持以下功能:
|
||||
* - 消息推送(支持单条/批量)
|
||||
* - 消息记录管理
|
||||
* - 多通道通知分发
|
||||
* @method static array sendAndRecord(PushClientType $clientType, string $businessModule, string $tenantId, string|int|array $userIds, string $event, array $data = [], array $messageData = [], ?string $socketId = null) 推送消息并记录(支持单条/批量)
|
||||
* @method static array batchSend(PushClientType $clientType, string $tenantId, array $messages) 批量推送不同业务消息
|
||||
* @method static array pushOnly(PushClientType $clientType, string $businessModule, string $tenantId, string|int|array $userIds, string $event = 'message', array $data = [], $messages = [], ?string $socketId = null) 仅推送消息(不记录)
|
||||
* @method static array recordOnly(array $userIds, array $messageData) 仅记录消息(不推送)
|
||||
*
|
||||
* @see \core\notify\NotificationService 底层服务实现类
|
||||
*/
|
||||
final class Notification
|
||||
{
|
||||
private static ?NotificationService $instance = null;
|
||||
|
||||
/**
|
||||
* 私有构造方法
|
||||
* 禁止外部实例化,强制使用静态调用方式
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
// 禁止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知服务实例(单例模式)
|
||||
*
|
||||
* @return NotificationService 通知服务实例
|
||||
*/
|
||||
private static function instance(): NotificationService
|
||||
{
|
||||
return self::$instance ??= Container::make(NotificationService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法调用代理
|
||||
*
|
||||
* @param string $method 调用的方法名
|
||||
* @param array $args 方法参数
|
||||
*
|
||||
* @return mixed 方法调用结果
|
||||
*/
|
||||
public static function __callStatic(string $method, array $args)
|
||||
{
|
||||
return self::instance()->$method(...$args);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace core\notify;
|
||||
|
||||
use app\model\message\Message;
|
||||
use core\logger\Logger;
|
||||
use core\notify\enum\PushClientType;
|
||||
use core\uuid\UUIDGenerator;
|
||||
use InvalidArgumentException;
|
||||
use madong\helper\Arr;
|
||||
use Webman\Push\Api;
|
||||
|
||||
final class NotificationService
|
||||
{
|
||||
private const DEFAULT_EXPIRE_DAYS = 30;
|
||||
|
||||
protected ?Api $pushApi = null;
|
||||
protected Message $messageModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$this->pushApi = new Api(
|
||||
$config['api'],
|
||||
$config['app_key'],
|
||||
$config['app_secret']
|
||||
);
|
||||
$this->messageModel = new Message();
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息并记录(支持单条/批量)
|
||||
*
|
||||
* @param PushClientType $clientType 客户端类型枚举值
|
||||
* @param string $businessModule 业务模块标识
|
||||
* @param string $tenantId 租户ID
|
||||
* @param string|int|array $userIds 接收者ID(单个ID或ID数组)
|
||||
* @param string $event 事件类型
|
||||
* @param array $data 推送数据(将直接传递给客户端)
|
||||
* @param array $messageData 消息记录数据,包含:
|
||||
* - 'title' (string): 消息标题
|
||||
* - 'content' (string): 消息内容
|
||||
* - 'type' (string): 消息类型(默认为事件类型)
|
||||
* - 'priority' (int): 消息优先级
|
||||
* - 'related_id' (string|null): 关联ID
|
||||
* - 'expired_at' (int|null): 过期时间戳(默认为null,使用默认过期天数)
|
||||
* - 'message_uuid'(string|null):消息uuid 区别
|
||||
* @param string|null $socketId 指定socket连接ID
|
||||
*
|
||||
* @return array 返回结果数组包含:
|
||||
* - 'push_count' (int): 成功推送的频道数量
|
||||
* - 'messages' (array): 创建的消息对象数组(包含完整消息详情)
|
||||
*/
|
||||
public function sendAndRecord(
|
||||
PushClientType $clientType,
|
||||
string $businessModule,
|
||||
string $tenantId,
|
||||
string|int|array $userIds,
|
||||
string $event = 'message',
|
||||
array $data = [],
|
||||
array $messageData = [],
|
||||
?string $socketId = null
|
||||
): array
|
||||
{
|
||||
|
||||
$userIds = Arr::normalize($userIds);
|
||||
$result = [
|
||||
'push_count' => 0,
|
||||
'messages' => [], // 直接返回创建的消息对象
|
||||
];
|
||||
|
||||
try {
|
||||
// 1. 批量创建消息记录
|
||||
// 使用合并后的消息数据创建记录,包含事件类型、租户ID、频道和过期时间
|
||||
$messages = $this->createMessages(
|
||||
$userIds,
|
||||
array_merge($messageData, [
|
||||
'type' => $event,
|
||||
'tenant_id' => $tenantId,
|
||||
'channel' => $this->buildChannel($clientType, $businessModule, $tenantId, null),
|
||||
'expired_at' => $this->calculateExpireTimestamp($messageData['expired_at'] ?? null),
|
||||
]),
|
||||
);
|
||||
|
||||
// 2. 准备推送数据(直接使用已创建的消息对象)
|
||||
// 将消息格式化为适合推送的格式后附加到推送数据中
|
||||
$pushData = array_merge($data, [
|
||||
'messages' => $this->formatMessagesForPush($messages),
|
||||
]);
|
||||
|
||||
// 3. 执行推送
|
||||
$channels = array_map(
|
||||
fn($userId) => $this->buildChannel($clientType, $businessModule, $tenantId, $userId),
|
||||
$userIds
|
||||
);
|
||||
|
||||
// 批量单条消息批量推送异常等官方修复
|
||||
$this->pushApi->trigger($channels, $event, $pushData, $socketId);
|
||||
|
||||
$result['push_count'] = count($channels);
|
||||
$result['messages'] = $messages;
|
||||
|
||||
Logger::debug("推送成功", [
|
||||
'channels' => $channels,
|
||||
'message_count' => count($messages),
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error("推送失败: " . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'user_ids' => $userIds,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推送不同业务消息(带记录)
|
||||
*
|
||||
* @param PushClientType $clientType 客户端类型枚举值
|
||||
* @param string $tenantId 租户ID
|
||||
* @param array $messages 批量消息数组,每个元素包含以下字段:
|
||||
* - 'module' (string): 业务模块标识
|
||||
* - 'userIds' (string|array): 接收者ID(单个ID或ID数组)
|
||||
* - 'event' (string): 事件类型
|
||||
* - 'data' (array, optional): 推送数据,默认为空数组
|
||||
* - 'title' (string, optional): 消息标题,默认为空字符串
|
||||
* - 'content' (string, optional): 消息内容,默认为空字符串
|
||||
* - 'message_type' (string, optional): 消息类型,默认为事件类型(event)
|
||||
* - 'priority' (int, optional): 消息优先级,默认为0
|
||||
* - 'related_id' (string|null, optional): 关联ID,默认为null
|
||||
* - 'expired_at' (int|null, optional): 过期时间戳,默认为null(使用默认过期天数)
|
||||
* - 'message_uuid'(string|null):消息uuid 区别公共重复推送
|
||||
*
|
||||
* @return array 返回批量推送结果数组,每个元素包含:
|
||||
* - 'push_count' (int): 成功推送数量
|
||||
* - 'message_ids' (array): 创建的消息ID数组
|
||||
* - 'message_details' (array): 完整的消息记录详情
|
||||
*/
|
||||
public function batchSend(
|
||||
PushClientType $clientType,
|
||||
string $tenantId,
|
||||
array $messages
|
||||
): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($messages as $msg) {
|
||||
$results[] = $this->sendAndRecord(
|
||||
$clientType,
|
||||
$msg['module'],
|
||||
$tenantId,
|
||||
$msg['receiver_id'],
|
||||
$msg['event'] ?? 'message',
|
||||
$msg['data'] ?? [],
|
||||
[
|
||||
'title' => $msg['title'] ?? '',
|
||||
'content' => $msg['content'] ?? '',
|
||||
'type' => $msg['message_type'] ?? $msg['event'],
|
||||
'priority' => $msg['priority'] ?? 0,
|
||||
'related_id' => $msg['related_id'] ?? null,
|
||||
'expired_at' => $msg['expired_at'] ?? null,
|
||||
'message_uuid' => $msg['message_uuid'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅推送消息(不记录)
|
||||
*/
|
||||
public function pushOnly(
|
||||
PushClientType $clientType,
|
||||
string $businessModule,
|
||||
string $tenantId,
|
||||
string|int|array $userIds,
|
||||
string $event = 'message',
|
||||
array $data = [],
|
||||
array $messages = [],
|
||||
?string $socketId = null
|
||||
): int
|
||||
{
|
||||
$userIds = Arr::normalize($userIds);
|
||||
$channels = array_map(
|
||||
fn($userId) => $this->buildChannel($clientType, $businessModule, $tenantId, $userId),
|
||||
$userIds
|
||||
);
|
||||
|
||||
$pushData = array_merge($data, [
|
||||
'messages' => $this->formatMessagesForPush($messages),
|
||||
]);
|
||||
|
||||
return $this->pushApi->trigger($channels, $event, $pushData, $socketId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅记录消息(不推送)
|
||||
*/
|
||||
public function recordOnly(
|
||||
array $userIds,
|
||||
array $messageData
|
||||
): array
|
||||
{
|
||||
return $this->createMessages(Arr::normalize($userIds), $messageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建消息记录(返回完整消息对象)
|
||||
*/
|
||||
private function createMessages(array $receiverIds, array $data): array
|
||||
{
|
||||
$defaults = [
|
||||
'status' => 'unread',
|
||||
'priority' => 3,
|
||||
'expired_at' => $data['expired_at'] ?? $this->calculateExpireTimestamp(self::DEFAULT_EXPIRE_DAYS),
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
foreach ($receiverIds as $receiverId) {
|
||||
$message = $this->messageModel->create(array_merge(
|
||||
$defaults,
|
||||
$data,
|
||||
['message_uuid' => $this->generateMessageUuid($data['message_uuid'] ?? null)],
|
||||
['receiver_id' => $receiverId]
|
||||
));
|
||||
$messages[] = $message->toArray(); // 直接返回模型数据
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的 UUID(如果未提供则自动生成)
|
||||
*
|
||||
* @param string|null $uuid 可选的外部 UUID(如果为 null 则自动生成)
|
||||
*
|
||||
* @return string 有效的 UUID
|
||||
* @throws \Exception 如果 UUID 生成失败
|
||||
*/
|
||||
private function generateMessageUuid(?string $uuid = null): string
|
||||
{
|
||||
if (empty($uuid)) {
|
||||
try {
|
||||
return UUIDGenerator::generate(); // 假设返回字符串 UUID
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception("Failed to generate UUID: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算过期时间戳
|
||||
*/
|
||||
private function calculateExpireTimestamp(int|null $expireDays): int
|
||||
{
|
||||
if ($expireDays === null) {
|
||||
$expireDays = self::DEFAULT_EXPIRE_DAYS;
|
||||
}
|
||||
return time() + ($expireDays * 86400); // 86400秒=1天
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息数据用于推送
|
||||
*/
|
||||
private function formatMessagesForPush(array $messages): array
|
||||
{
|
||||
return array_map(function ($message) {
|
||||
return $message;
|
||||
}, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成标准化频道名称
|
||||
*/
|
||||
public function buildChannel(
|
||||
PushClientType $clientType,
|
||||
string $businessModule,
|
||||
string $tenantId,
|
||||
?string $userId = null
|
||||
): string
|
||||
{
|
||||
// 业务模块校验
|
||||
if (!preg_match('/^[a-z0-9_]+$/', $businessModule)) {
|
||||
throw new InvalidArgumentException('业务模块只能包含小写字母、数字和下划线');
|
||||
}
|
||||
|
||||
return implode('-', [
|
||||
$clientType->value,
|
||||
$businessModule,
|
||||
$tenantId,
|
||||
$userId ?: '*',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getConfig(): array
|
||||
{
|
||||
$config = config('core.notify.app.webman-push');
|
||||
if (empty($config)) {
|
||||
throw new \RuntimeException('消息推送配置未定义');
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
'webman-push' => [
|
||||
'websocket' => config('plugin.webman.push.app.websocket'),
|
||||
'api' => config('plugin.webman.push.app.api'),
|
||||
'app_key' => config('plugin.webman.push.app.app_key'),
|
||||
'app_secret' => config('plugin.webman.push.app.app_secret'),
|
||||
'channel_hook' => config('plugin.webman.push.app.channel_hook'),
|
||||
'auth' => config('plugin.webman.push.app.auth'),
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace core\notify\enum;
|
||||
|
||||
use core\interface\IEnum;
|
||||
|
||||
/**
|
||||
* 推送客户端枚举
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
enum PushClientType: string implements IEnum
|
||||
{
|
||||
|
||||
case BACKEND = 'backend'; // 管理后台
|
||||
case APP = 'app'; // 移动应用
|
||||
case MP = 'mp'; // 小程序
|
||||
case WEB = 'web'; // 网页端
|
||||
|
||||
/**
|
||||
* 获取人类识别的标签
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::BACKEND => '管理后台',
|
||||
self::APP => '移动应用',
|
||||
self::MP => '小程序',
|
||||
self::WEB => '网页端',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有枚举值
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user