Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
d699e68997 docs: remove docs path prefix 2025-09-09 13:47:57 +08:00
5 changed files with 17 additions and 178 deletions

View File

@@ -42,8 +42,6 @@ public class CachingConfig {
public static final String ONLINE_CACHE_NAME="openisle_online"; public static final String ONLINE_CACHE_NAME="openisle_online";
// 注册验证码 // 注册验证码
public static final String VERIFY_CACHE_NAME="openisle_verify"; public static final String VERIFY_CACHE_NAME="openisle_verify";
// 发帖频率限制
public static final String LIMIT_CACHE_NAME="openisle_limit";
/** /**
* 自定义Redis的序列化器 * 自定义Redis的序列化器

View File

@@ -90,9 +90,6 @@ public class SecurityConfig {
"http://192.168.7.98", "http://192.168.7.98",
"http://192.168.7.98:3000", "http://192.168.7.98:3000",
"https://petstore.swagger.io", "https://petstore.swagger.io",
// 允许自建OpenAPI地址
"https://docs.open-isle.com",
"https://www.docs.open-isle.com",
websiteUrl, websiteUrl,
websiteUrl.replace("://www.", "://") websiteUrl.replace("://www.", "://")
)); ));

View File

@@ -1,6 +1,5 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.Post; import com.openisle.model.Post;
import com.openisle.model.PostStatus; import com.openisle.model.PostStatus;
import com.openisle.model.PostType; import com.openisle.model.PostType;
@@ -29,15 +28,12 @@ import com.openisle.repository.PollVoteRepository;
import com.openisle.model.Role; import com.openisle.model.Role;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import com.openisle.service.EmailSender; import com.openisle.service.EmailSender;
import java.time.Duration;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.*; import java.util.*;
@@ -84,8 +80,6 @@ public class PostService {
@Value("${app.website-url:https://www.open-isle.com}") @Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl; private String websiteUrl;
private final RedisTemplate redisTemplate;
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository, public PostService(PostRepository postRepository,
UserRepository userRepository, UserRepository userRepository,
@@ -108,8 +102,7 @@ public class PostService {
ApplicationContext applicationContext, ApplicationContext applicationContext,
PointService pointService, PointService pointService,
PostChangeLogService postChangeLogService, PostChangeLogService postChangeLogService,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
RedisTemplate redisTemplate) {
this.postRepository = postRepository; this.postRepository = postRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
@@ -132,8 +125,6 @@ public class PostService {
this.pointService = pointService; this.pointService = pointService;
this.postChangeLogService = postChangeLogService; this.postChangeLogService = postChangeLogService;
this.publishMode = publishMode; this.publishMode = publishMode;
this.redisTemplate = redisTemplate;
} }
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
@@ -210,9 +201,9 @@ public class PostService {
LocalDateTime endTime, LocalDateTime endTime,
java.util.List<String> options, java.util.List<String> options,
Boolean multiple) { Boolean multiple) {
// 限制访问次数 long recent = postRepository.countByAuthorAfter(username,
boolean limitResult = postRateLimit(username); java.time.LocalDateTime.now().minusMinutes(5));
if (!limitResult) { if (recent >= 1) {
throw new RateLimitException("Too many posts"); throw new RateLimitException("Too many posts");
} }
if (tagIds == null || tagIds.isEmpty()) { if (tagIds == null || tagIds.isEmpty()) {
@@ -309,23 +300,6 @@ public class PostService {
return post; return post;
} }
/**
* 限制发帖频率
* @param username
* @return
*/
private boolean postRateLimit(String username){
String key = CachingConfig.LIMIT_CACHE_NAME +":posts:"+username;
String result = (String)redisTemplate.opsForValue().get(key);
//最近没有创建过文章
if(StringUtils.isEmpty(result)){
// 限制频率为5分钟
redisTemplate.opsForValue().set(key,"1", Duration.ofMinutes(5));
return true;
}
return false;
}
public void joinLottery(Long postId, String username) { public void joinLottery(Long postId, String username) {
LotteryPost post = lotteryPostRepository.findById(postId) LotteryPost post = lotteryPostRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));

View File

@@ -1,48 +1,23 @@
<template> <template>
<div class="about-page"> <div class="about-page">
<BaseTabs v-model="selectedTab" :tabs="tabs"> <BaseTabs v-model="selectedTab" :tabs="tabs">
<template v-if="selectedTab === 'api'"> <div class="about-loading" v-if="isFetching">
<div class="about-api"> <l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
<div class="about-api-title">调试Token</div> </div>
<div v-if="!authState.loggedIn" class="about-api-login"> <div
<NuxtLink to="/login">登录</NuxtLink>后查看 Token v-else
</div> class="about-content"
<div v-else class="about-api-token"> v-html="renderMarkdown(content)"
<div class="token-row"> @click="handleContentClick"
<span class="token-text">{{ shortToken }}</span> ></div>
<span @click="copyToken"><copy class="copy-icon" /></span>
</div>
<div class="warning-row">
<info-icon class="warning-icon" />
<div class="token-warning">请不要将 Token 泄露给他人</div>
</div>
</div>
<div class="about-api-title">API文档和调试入口</div>
<div class="about-api-link">API Playground <share /></div>
</div>
</template>
<template v-else>
<div class="about-loading" v-if="isFetching">
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
</div>
<div
v-else
class="about-content"
v-html="renderMarkdown(content)"
@click="handleContentClick"
></div>
</template>
</BaseTabs> </BaseTabs>
</div> </div>
</template> </template>
<script> <script>
import { computed, onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from '#imports'
import { authState, getToken } from '~/utils/auth'
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown' import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
import BaseTabs from '~/components/BaseTabs.vue' import BaseTabs from '~/components/BaseTabs.vue'
import { toast } from '~/composables/useToast'
export default { export default {
name: 'AboutPageView', name: 'AboutPageView',
@@ -69,25 +44,11 @@ export default {
label: '隐私政策', label: '隐私政策',
file: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/about/privacy.md', file: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/about/privacy.md',
}, },
{
key: 'api',
label: 'API与调试',
},
] ]
const route = useRoute()
const router = useRouter()
const selectedTab = ref(tabs[0].key) const selectedTab = ref(tabs[0].key)
const content = ref('') const content = ref('')
const token = computed(() => (authState.loggedIn ? getToken() : ''))
const shortToken = computed(() => {
if (!token.value) return ''
if (token.value.length <= 20) return token.value
return `${token.value.slice(0, 20)}...${token.value.slice(-10)}`
})
const loadContent = async (file) => { const loadContent = async (file) => {
if (!file) return
try { try {
isFetching.value = true isFetching.value = true
const res = await fetch(file) const res = await fetch(file)
@@ -104,58 +65,19 @@ export default {
} }
onMounted(() => { onMounted(() => {
const initTab = route.query.tab loadContent(tabs[0].file)
if (initTab && tabs.find((t) => t.key === initTab)) {
selectedTab.value = initTab
const tab = tabs.find((t) => t.key === initTab)
if (tab && tab.file) loadContent(tab.file)
} else {
loadContent(tabs[0].file)
}
}) })
watch(selectedTab, (name) => { watch(selectedTab, (name) => {
const tab = tabs.find((t) => t.key === name) const tab = tabs.find((t) => t.key === name)
if (tab && tab.file) loadContent(tab.file) if (tab) loadContent(tab.file)
router.replace({ query: { ...route.query, tab: name } })
}) })
watch(
() => route.query.tab,
(name) => {
if (name && name !== selectedTab.value && tabs.find((t) => t.key === name)) {
selectedTab.value = name
}
},
)
const copyToken = async () => {
if (import.meta.client && token.value) {
try {
await navigator.clipboard.writeText(token.value)
toast.success('已复制 Token')
} catch (e) {
toast.error('复制失败')
}
}
}
const handleContentClick = (e) => { const handleContentClick = (e) => {
handleMarkdownClick(e) handleMarkdownClick(e)
} }
return { return { tabs, selectedTab, content, renderMarkdown, isFetching, handleContentClick }
tabs,
selectedTab,
content,
renderMarkdown,
isFetching,
handleContentClick,
authState,
token,
copyToken,
shortToken,
}
}, },
} }
</script> </script>
@@ -179,56 +101,6 @@ export default {
height: 200px; height: 200px;
} }
.about-api {
padding: 20px;
}
.about-api-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 30px;
margin-bottom: 15px;
}
.warning-row {
display: flex;
align-items: center;
gap: 4px;
opacity: 0.7;
}
.warning-icon {
font-size: 13px;
}
.token-warning {
font-size: 13px;
}
.token-row {
display: flex;
align-items: center;
gap: 10px;
font: 14px;
margin-bottom: 10px;
word-break: break-all;
}
.copy-btn {
padding: 4px 8px;
cursor: pointer;
}
.about-api-link {
color: var(--primary-color);
cursor: pointer;
}
.about-api-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.about-tabs { .about-tabs {
width: 100vw; width: 100vw;

View File

@@ -77,7 +77,6 @@ import {
Open, Open,
Dislike, Dislike,
CheckOne, CheckOne,
Share,
} from '@icon-park/vue-next' } from '@icon-park/vue-next'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
@@ -158,5 +157,4 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('OpenIcon', Open) nuxtApp.vueApp.component('OpenIcon', Open)
nuxtApp.vueApp.component('Dislike', Dislike) nuxtApp.vueApp.component('Dislike', Dislike)
nuxtApp.vueApp.component('CheckOne', CheckOne) nuxtApp.vueApp.component('CheckOne', CheckOne)
nuxtApp.vueApp.component('Share', Share)
}) })