mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-28 00:50:46 +08:00
Compare commits
1 Commits
codex/modi
...
codex/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d699e68997 |
@@ -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的序列化器
|
||||||
|
|||||||
@@ -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.", "://")
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user