功能追加:显示在线人数

This commit is contained in:
wangshun
2025-09-05 16:24:09 +08:00
parent f89a17f14d
commit dbd322807d
4 changed files with 100 additions and 2 deletions

View File

@@ -36,6 +36,8 @@ public class CachingConfig {
public static final String TAG_CACHE_NAME="openisle_tags";
// 分类缓存名
public static final String CATEGORY_CACHE_NAME="openisle_categories";
// 在线人数缓存名
public static final String ONLINE_CACHE_NAME="openisle_online";
/**
* 自定义Redis的序列化器

View File

@@ -129,6 +129,8 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
.requestMatchers(HttpMethod.GET, "/api/online/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/online/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
@@ -183,7 +185,8 @@ public class SecurityConfig {
}
} else if (!uri.startsWith("/api/auth") && !publicGet
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")
&& !uri.startsWith("/api/v3/api-docs")) {
&& !uri.startsWith("/api/v3/api-docs")
&& !uri.startsWith("/api/online")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Missing token\"}");

View File

@@ -0,0 +1,33 @@
package com.openisle.controller;
import com.openisle.config.CachingConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
/**
* @author smallclover
* @since 2025-09-05
* 统计在线人数
*/
@RestController
@RequestMapping("/api/online")
@RequiredArgsConstructor
public class OnlineController {
private final RedisTemplate redisTemplate;
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":";
@PostMapping("/heartbeat")
public void ping(@RequestParam String userId){
redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150));
}
@GetMapping("/count")
public long count(){
return redisTemplate.keys(ONLINE_KEY+"*").size();
}
}

View File

@@ -37,7 +37,12 @@
邀请
<i v-if="isCopying" class="fas fa-spinner fa-spin"></i>
</div>
<ToolTip v-if="!isMobile" content="当前在线人数" placement="bottom">
<div class="online-count">
<i class="fas fa-people-group"></i>
<span>{{ onlineCount }}</span>
</div>
</ToolTip>
<ToolTip content="复制RSS链接" placement="bottom">
<div class="rss-icon" @click="copyRssLink">
<i class="fas fa-rss"></i>
@@ -115,6 +120,46 @@ const userMenu = ref(null)
const menuBtn = ref(null)
const isCopying = ref(false)
const onlineCount = ref(0)
// 心跳检测
async function sendPing() {
try {
// 已登录就用 userId否则随机生成游客ID
let userId = authState.userId
if (userId) {
// 用户已登录,清理游客 ID
localStorage.removeItem('guestId')
} else {
// 游客模式
let savedId = localStorage.getItem('guestId')
if (!savedId) {
savedId = `guest-${crypto.randomUUID()}`
localStorage.setItem('guestId', savedId)
}
userId = savedId
}
const res = await fetch(`${API_BASE_URL}/api/online/heartbeat?userId=${userId}`, {
method: 'POST',
})
} catch (e) {
console.error("心跳失败", e)
}
}
// 获取在线人数
async function fetchCount() {
try {
const res = await fetch(`${API_BASE_URL}/api/online/count`, {
method: 'GET',
})
onlineCount.value = await res.json()
} catch (e) {
console.error("获取在线人数失败", e)
}
}
const search = () => {
showSearch.value = true
nextTick(() => {
@@ -262,6 +307,12 @@ onMounted(async () => {
await updateUnread()
},
)
// 新增的在线人数逻辑
sendPing()
fetchCount()
setInterval(sendPing, 120000) // 每 2 分钟发一次心跳
setInterval(fetchCount, 60000) // 每 1 分更新 UI
})
</script>
@@ -452,6 +503,15 @@ onMounted(async () => {
animation: rss-glow 2s 3;
}
.online-count {
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
color: var(--primary-color);
cursor: default;
}
@keyframes rss-glow {
0% {
text-shadow: 0 0 0px var(--primary-color);