feat: add user notification center
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\enum\notice;
|
||||
|
||||
/**
|
||||
* 用户站内消息场景。
|
||||
*
|
||||
* 使用独立的场景编号,避免与验证码等已有通知场景混用。
|
||||
*/
|
||||
class UserNoticeEnum
|
||||
{
|
||||
public const POST_COMMENT = 1001;
|
||||
public const COMMENT_REPLY = 1002;
|
||||
public const FOLLOWED = 1003;
|
||||
public const ADMIN_POINTS = 1004;
|
||||
public const SYSTEM = 1005;
|
||||
|
||||
public static function getSceneIds(): array
|
||||
{
|
||||
return [
|
||||
self::POST_COMMENT,
|
||||
self::COMMENT_REPLY,
|
||||
self::FOLLOWED,
|
||||
self::ADMIN_POINTS,
|
||||
self::SYSTEM,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getMeta(int $sceneId): array
|
||||
{
|
||||
$map = [
|
||||
self::POST_COMMENT => [
|
||||
'type' => 'post_comment',
|
||||
'type_desc' => '帖子评论',
|
||||
'icon' => 'chat',
|
||||
'color' => '#1468f5',
|
||||
],
|
||||
self::COMMENT_REPLY => [
|
||||
'type' => 'comment_reply',
|
||||
'type_desc' => '评论回复',
|
||||
'icon' => 'chat',
|
||||
'color' => '#7c4dff',
|
||||
],
|
||||
self::FOLLOWED => [
|
||||
'type' => 'followed',
|
||||
'type_desc' => '新增关注',
|
||||
'icon' => 'account',
|
||||
'color' => '#10b981',
|
||||
],
|
||||
self::ADMIN_POINTS => [
|
||||
'type' => 'admin_points',
|
||||
'type_desc' => '积分到账',
|
||||
'icon' => 'coupon',
|
||||
'color' => '#f59e0b',
|
||||
],
|
||||
self::SYSTEM => [
|
||||
'type' => 'system',
|
||||
'type_desc' => '系统消息',
|
||||
'icon' => 'bell',
|
||||
'color' => '#64748b',
|
||||
],
|
||||
];
|
||||
|
||||
return $map[$sceneId] ?? [
|
||||
'type' => 'system',
|
||||
'type_desc' => '系统消息',
|
||||
'icon' => 'bell',
|
||||
'color' => '#64748b',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::getSceneIds() as $sceneId) {
|
||||
$meta = self::getMeta($sceneId);
|
||||
$options[] = [
|
||||
'value' => $sceneId,
|
||||
'label' => $meta['type_desc'],
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\notice;
|
||||
|
||||
use app\common\enum\notice\NoticeEnum;
|
||||
use app\common\enum\notice\UserNoticeEnum;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\notice\NoticeRecord;
|
||||
use app\common\model\user\User;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 用户站内消息服务。
|
||||
*/
|
||||
class UserNoticeService
|
||||
{
|
||||
public static function postComment(int $recipientUserId, int $sourceUserId, int $postId, int $commentId, string $content): bool
|
||||
{
|
||||
return self::create(
|
||||
$recipientUserId,
|
||||
UserNoticeEnum::POST_COMMENT,
|
||||
'你的帖子收到了评论',
|
||||
self::sourceName($sourceUserId) . ' 评论了你的帖子:' . self::shortContent($content),
|
||||
[
|
||||
'source_user_id' => $sourceUserId,
|
||||
'target_type' => 'community_post',
|
||||
'target_id' => $postId,
|
||||
'comment_id' => $commentId,
|
||||
'target_url' => "/packages_community/pages/post_detail?id={$postId}&comment_id={$commentId}",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function commentReply(int $recipientUserId, int $sourceUserId, int $postId, int $commentId, string $content): bool
|
||||
{
|
||||
return self::create(
|
||||
$recipientUserId,
|
||||
UserNoticeEnum::COMMENT_REPLY,
|
||||
'你的评论收到了回复',
|
||||
self::sourceName($sourceUserId) . ' 回复了你的评论:' . self::shortContent($content),
|
||||
[
|
||||
'source_user_id' => $sourceUserId,
|
||||
'target_type' => 'community_comment',
|
||||
'target_id' => $commentId,
|
||||
'post_id' => $postId,
|
||||
'target_url' => "/packages_community/pages/post_detail?id={$postId}&comment_id={$commentId}",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function followed(int $recipientUserId, int $sourceUserId): bool
|
||||
{
|
||||
return self::create(
|
||||
$recipientUserId,
|
||||
UserNoticeEnum::FOLLOWED,
|
||||
'你有一位新关注者',
|
||||
self::sourceName($sourceUserId) . ' 关注了你',
|
||||
[
|
||||
'source_user_id' => $sourceUserId,
|
||||
'target_type' => 'user_profile',
|
||||
'target_id' => $sourceUserId,
|
||||
'target_url' => "/packages_community/pages/user_profile?user_id={$sourceUserId}",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function adminPoints(int $recipientUserId, int $points, string $remark = ''): bool
|
||||
{
|
||||
$content = '管理员为你增加了 ' . $points . ' 积分';
|
||||
$remark = self::shortContent($remark, 80);
|
||||
if ($remark !== '') {
|
||||
$content .= ',备注:' . $remark;
|
||||
}
|
||||
|
||||
return self::create(
|
||||
$recipientUserId,
|
||||
UserNoticeEnum::ADMIN_POINTS,
|
||||
'积分到账',
|
||||
$content,
|
||||
[
|
||||
'target_type' => 'points',
|
||||
'target_url' => '/packages/pages/points_log/points_log',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向全部启用用户或指定用户发布后台系统消息。
|
||||
*/
|
||||
public static function sendSystem(string $title, string $content, string $targetUrl = '', array $userIds = []): int
|
||||
{
|
||||
$query = User::where('is_disable', 0);
|
||||
if (!empty($userIds)) {
|
||||
$query->whereIn('id', $userIds);
|
||||
}
|
||||
|
||||
$userIds = array_map('intval', $query->column('id'));
|
||||
if (empty($userIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$extra = self::encodeExtra([
|
||||
'target_type' => 'system',
|
||||
'target_url' => self::sanitizeTargetUrl($targetUrl),
|
||||
]);
|
||||
$sentCount = 0;
|
||||
|
||||
foreach (array_chunk($userIds, 300) as $chunk) {
|
||||
$records = [];
|
||||
foreach ($chunk as $userId) {
|
||||
$records[] = self::recordData($userId, UserNoticeEnum::SYSTEM, $title, $content, $extra, $now);
|
||||
}
|
||||
Db::name('notice_record')->insertAll($records);
|
||||
$sentCount += count($records);
|
||||
}
|
||||
|
||||
return $sentCount;
|
||||
}
|
||||
|
||||
public static function unreadCount(int $userId): int
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return NoticeRecord::where('user_id', $userId)
|
||||
->whereIn('scene_id', UserNoticeEnum::getSceneIds())
|
||||
->where('read', YesNoEnum::NO)
|
||||
->count();
|
||||
}
|
||||
|
||||
public static function decodeExtra($value): array
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode($value, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
public static function sanitizeTargetUrl(string $targetUrl): string
|
||||
{
|
||||
$targetUrl = trim($targetUrl);
|
||||
if ($targetUrl === '' || !str_starts_with($targetUrl, '/') || str_starts_with($targetUrl, '//')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return mb_substr($targetUrl, 0, 200);
|
||||
}
|
||||
|
||||
private static function create(int $recipientUserId, int $sceneId, string $title, string $content, array $extra = []): bool
|
||||
{
|
||||
$sourceUserId = (int) ($extra['source_user_id'] ?? 0);
|
||||
if ($recipientUserId <= 0 || $recipientUserId === $sourceUserId || !in_array($sceneId, UserNoticeEnum::getSceneIds(), true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
NoticeRecord::create(self::recordData(
|
||||
$recipientUserId,
|
||||
$sceneId,
|
||||
$title,
|
||||
$content,
|
||||
self::encodeExtra($extra),
|
||||
$now
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function recordData(int $userId, int $sceneId, string $title, string $content, string $extra, int $now): array
|
||||
{
|
||||
return [
|
||||
'user_id' => $userId,
|
||||
'title' => mb_substr(trim($title), 0, 50),
|
||||
'content' => mb_substr(trim($content), 0, 1000),
|
||||
'scene_id' => $sceneId,
|
||||
'read' => YesNoEnum::NO,
|
||||
'recipient' => 1,
|
||||
'send_type' => NoticeEnum::SYSTEM,
|
||||
'notice_type' => NoticeEnum::BUSINESS_NOTIFICATION,
|
||||
'extra' => $extra,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
private static function encodeExtra(array $extra): string
|
||||
{
|
||||
if (isset($extra['target_url'])) {
|
||||
$extra['target_url'] = self::sanitizeTargetUrl((string) $extra['target_url']);
|
||||
}
|
||||
$encoded = json_encode($extra, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
return is_string($encoded) ? mb_substr($encoded, 0, 255) : '';
|
||||
}
|
||||
|
||||
private static function sourceName(int $userId): string
|
||||
{
|
||||
$name = User::where('id', $userId)->value('nickname');
|
||||
return $name ? (string) $name : '一位用户';
|
||||
}
|
||||
|
||||
private static function shortContent(string $content, int $length = 60): string
|
||||
{
|
||||
$content = trim(preg_replace('/\s+/u', ' ', strip_tags($content)) ?: '');
|
||||
if ($content === '') {
|
||||
return '';
|
||||
}
|
||||
return mb_strlen($content) > $length ? mb_substr($content, 0, $length) . '…' : $content;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user