mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-20 02:47:25 +08:00
功能追加:显示在线人数
This commit is contained in:
@@ -36,6 +36,8 @@ public class CachingConfig {
|
|||||||
public static final String TAG_CACHE_NAME="openisle_tags";
|
public static final String TAG_CACHE_NAME="openisle_tags";
|
||||||
// 分类缓存名
|
// 分类缓存名
|
||||||
public static final String CATEGORY_CACHE_NAME="openisle_categories";
|
public static final String CATEGORY_CACHE_NAME="openisle_categories";
|
||||||
|
// 在线人数缓存名
|
||||||
|
public static final String ONLINE_CACHE_NAME="openisle_online";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义Redis的序列化器
|
* 自定义Redis的序列化器
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/rss").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.GET, "/api/point-goods").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
||||||
@@ -183,7 +185,8 @@ public class SecurityConfig {
|
|||||||
}
|
}
|
||||||
} else if (!uri.startsWith("/api/auth") && !publicGet
|
} else if (!uri.startsWith("/api/auth") && !publicGet
|
||||||
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")
|
&& !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.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
response.setContentType("application/json");
|
response.setContentType("application/json");
|
||||||
response.getWriter().write("{\"error\": \"Missing token\"}");
|
response.getWriter().write("{\"error\": \"Missing token\"}");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,12 @@
|
|||||||
邀请
|
邀请
|
||||||
<i v-if="isCopying" class="fas fa-spinner fa-spin"></i>
|
<i v-if="isCopying" class="fas fa-spinner fa-spin"></i>
|
||||||
</div>
|
</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">
|
<ToolTip content="复制RSS链接" placement="bottom">
|
||||||
<div class="rss-icon" @click="copyRssLink">
|
<div class="rss-icon" @click="copyRssLink">
|
||||||
<i class="fas fa-rss"></i>
|
<i class="fas fa-rss"></i>
|
||||||
@@ -115,6 +120,46 @@ const userMenu = ref(null)
|
|||||||
const menuBtn = ref(null)
|
const menuBtn = ref(null)
|
||||||
const isCopying = ref(false)
|
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 = () => {
|
const search = () => {
|
||||||
showSearch.value = true
|
showSearch.value = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -262,6 +307,12 @@ onMounted(async () => {
|
|||||||
await updateUnread()
|
await updateUnread()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 新增的在线人数逻辑
|
||||||
|
sendPing()
|
||||||
|
fetchCount()
|
||||||
|
setInterval(sendPing, 120000) // 每 2 分钟发一次心跳
|
||||||
|
setInterval(fetchCount, 60000) // 每 1 分更新 UI
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -452,6 +503,15 @@ onMounted(async () => {
|
|||||||
animation: rss-glow 2s 3;
|
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 {
|
@keyframes rss-glow {
|
||||||
0% {
|
0% {
|
||||||
text-shadow: 0 0 0px var(--primary-color);
|
text-shadow: 0 0 0px var(--primary-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user