$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; } }