mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 07:30:54 +08:00
Compare commits
20 Commits
codex/fix-
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7010e8a058 | ||
|
|
38ee37d5be | ||
|
|
e398d8e989 | ||
|
|
85e77c265e | ||
|
|
8abdc73497 | ||
|
|
747d9c07d1 | ||
|
|
09cefbedbf | ||
|
|
d772bc182f | ||
|
|
358c53338d | ||
|
|
2110980797 | ||
|
|
1cd89eaa54 | ||
|
|
1d2e7eb96e | ||
|
|
4428e06f1d | ||
|
|
dddff54556 | ||
|
|
e7f7bbac22 | ||
|
|
37aae4ba5c | ||
|
|
54cfc98336 | ||
|
|
d42d38ff7a | ||
|
|
2b4601bd4b | ||
|
|
5071d9c6d5 |
@@ -246,3 +246,9 @@ https://resend.com/emails 创建账号并登录
|
|||||||
`RESEND_FROM_EMAIL`: **noreply@域名**
|
`RESEND_FROM_EMAIL`: **noreply@域名**
|
||||||
`RESEND_API_KEY`:**刚刚复制的 Key**
|
`RESEND_API_KEY`:**刚刚复制的 Key**
|
||||||

|

|
||||||
|
|
||||||
|
## 开源共建和API文档
|
||||||
|
|
||||||
|
- API文档: https://openisle-docs.netlify.app/docs/openapi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
高效的开源社区前后端平台
|
高效的开源社区前后端平台
|
||||||
<br><br><br>
|
<br><br><br>
|
||||||
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
||||||
|
<br><br><br>
|
||||||
|
<a href="https://hellogithub.com/repository/nagisa77/OpenIsle" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8605546658d94cbab45182af2a02e4c8&claim_uid=p5GNFTtZl6HBAYQ" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 💡 简介
|
## 💡 简介
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ 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的序列化器
|
||||||
|
|||||||
@@ -5,13 +5,21 @@ import io.swagger.v3.oas.models.OpenAPI;
|
|||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
private final SpringDocProperties springDocProperties;
|
||||||
|
|
||||||
@Value("${springdoc.info.title}")
|
@Value("${springdoc.info.title}")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@@ -30,19 +38,23 @@ public class OpenApiConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public OpenAPI openAPI() {
|
public OpenAPI openAPI() {
|
||||||
SecurityScheme securityScheme = new SecurityScheme()
|
SecurityScheme securityScheme = new SecurityScheme()
|
||||||
.type(SecurityScheme.Type.HTTP)
|
.type(SecurityScheme.Type.HTTP)
|
||||||
.scheme(scheme.toLowerCase())
|
.scheme(scheme.toLowerCase())
|
||||||
.bearerFormat("JWT")
|
.bearerFormat("JWT")
|
||||||
.in(SecurityScheme.In.HEADER)
|
.in(SecurityScheme.In.HEADER)
|
||||||
.name(header);
|
.name(header);
|
||||||
|
|
||||||
|
List<Server> servers = springDocProperties.getServers().stream()
|
||||||
|
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
|
.servers(servers)
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title(title)
|
.title(title)
|
||||||
.description(description)
|
.description(description)
|
||||||
.version(version))
|
.version(version))
|
||||||
.components(new Components()
|
.components(new Components().addSecuritySchemes("JWT", securityScheme))
|
||||||
.addSecuritySchemes("JWT", securityScheme))
|
|
||||||
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
.addSecurityItem(new SecurityRequirement().addList("JWT"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ 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.", "://")
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.openisle.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "springdoc.api-docs")
|
||||||
|
public class SpringDocProperties {
|
||||||
|
private List<ServerConfig> servers = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ServerConfig {
|
||||||
|
private String url;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -28,12 +29,15 @@ 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.*;
|
||||||
@@ -80,6 +84,8 @@ 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,
|
||||||
@@ -102,7 +108,8 @@ 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;
|
||||||
@@ -125,6 +132,8 @@ 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)
|
||||||
@@ -201,9 +210,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,
|
// 限制访问次数
|
||||||
java.time.LocalDateTime.now().minusMinutes(5));
|
boolean limitResult = postRateLimit(username);
|
||||||
if (recent >= 1) {
|
if (!limitResult) {
|
||||||
throw new RateLimitException("Too many posts");
|
throw new RateLimitException("Too many posts");
|
||||||
}
|
}
|
||||||
if (tagIds == null || tagIds.isEmpty()) {
|
if (tagIds == null || tagIds.isEmpty()) {
|
||||||
@@ -300,6 +309,23 @@ 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"));
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ rabbitmq.sharding.enabled=true
|
|||||||
# see https://springdoc.org/#springdoc-openapi-core-properties
|
# see https://springdoc.org/#springdoc-openapi-core-properties
|
||||||
springdoc.api-docs.path=/api/v3/api-docs
|
springdoc.api-docs.path=/api/v3/api-docs
|
||||||
springdoc.api-docs.enabled=true
|
springdoc.api-docs.enabled=true
|
||||||
springdoc.api-docs.server-url=${WEBSITE_URL:https://www.open-isle.com}
|
springdoc.api-docs.servers[0].url=${WEBSITE_URL:https://www.open-isle.com}
|
||||||
|
springdoc.api-docs.servers[0].description=正式环境
|
||||||
|
springdoc.api-docs.servers[1].url=https://www.staging.open-isle.com
|
||||||
|
springdoc.api-docs.servers[1].description=预发环境
|
||||||
springdoc.info.title=OpenIsle
|
springdoc.info.title=OpenIsle
|
||||||
springdoc.info.description=OpenIsle Open API Documentation
|
springdoc.info.description=OpenIsle Open API Documentation
|
||||||
springdoc.info.version=0.0.1
|
springdoc.info.version=0.0.1
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ bun dev
|
|||||||
|
|
||||||
使用以下路由:
|
使用以下路由:
|
||||||
|
|
||||||
- `docs/frontend/` 前端技术文档
|
- `frontend/` 前端技术文档
|
||||||
- `docs/backend/` 后端技术文档
|
- `backend/` 后端技术文档
|
||||||
- `docs/openapi/` 后端 API 文档
|
- `openapi/` 后端 API 文档
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function DocsCategory({ url }: { url: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
export default async function Page(props: PageProps<'/[[...slug]]'>) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug);
|
||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
@@ -48,7 +48,7 @@ export async function generateStaticParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
props: PageProps<'/docs/[[...slug]]'>
|
props: PageProps<'/[[...slug]]'>
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const page = source.getPage(params.slug);
|
const page = source.getPage(params.slug);
|
||||||
@@ -28,7 +28,7 @@ function TabTitle({ children }: { children: React.ReactNode }) {
|
|||||||
return <span className="text-[11px]">{children}</span>;
|
return <span className="text-[11px]">{children}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<DocsLayout
|
<DocsLayout
|
||||||
@@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
|||||||
{
|
{
|
||||||
title: 'OpenIsle 前端',
|
title: 'OpenIsle 前端',
|
||||||
description: <TabTitle>前端开发文档</TabTitle>,
|
description: <TabTitle>前端开发文档</TabTitle>,
|
||||||
url: '/docs/frontend',
|
url: '/frontend',
|
||||||
icon: (
|
icon: (
|
||||||
<TabIcon color="#4ca154">
|
<TabIcon color="#4ca154">
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
@@ -50,7 +50,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
|||||||
{
|
{
|
||||||
title: 'OpenIsle 后端',
|
title: 'OpenIsle 后端',
|
||||||
description: <TabTitle>后端开发文档</TabTitle>,
|
description: <TabTitle>后端开发文档</TabTitle>,
|
||||||
url: '/docs/backend',
|
url: '/backend',
|
||||||
icon: (
|
icon: (
|
||||||
<TabIcon color="#1f66f4">
|
<TabIcon color="#1f66f4">
|
||||||
<ServerIcon />
|
<ServerIcon />
|
||||||
@@ -60,7 +60,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
|||||||
{
|
{
|
||||||
title: 'OpenIsle API',
|
title: 'OpenIsle API',
|
||||||
description: <TabTitle>后端 API 文档</TabTitle>,
|
description: <TabTitle>后端 API 文档</TabTitle>,
|
||||||
url: '/docs/openapi',
|
url: '/openapi',
|
||||||
icon: (
|
icon: (
|
||||||
<TabIcon color="#677489">
|
<TabIcon color="#677489">
|
||||||
<CodeXmlIcon />
|
<CodeXmlIcon />
|
||||||
@@ -6,7 +6,7 @@ const inter = Inter({
|
|||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||||
return (
|
return (
|
||||||
<html lang="zh" className={inter.className} suppressHydrationWarning>
|
<html lang="zh" className={inter.className} suppressHydrationWarning>
|
||||||
<body className="flex flex-col min-h-screen">
|
<body className="flex flex-col min-h-screen">
|
||||||
|
|||||||
@@ -40,4 +40,4 @@ backend/
|
|||||||
|
|
||||||
## API 接口
|
## API 接口
|
||||||
|
|
||||||
详细的 API 接口文档请查看 [API 文档](/docs/openapi)。
|
详细的 API 接口文档请查看 [API 文档](/openapi)。
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ OpenIsle 是一个现代化的社区平台,提供完整的社交功能。
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
- [后端开发指南](/docs/backend) - 了解后端架构和开发
|
- [后端开发指南](/backend) - 了解后端架构和开发
|
||||||
- [前端开发指南](/docs/frontend) - 了解前端技术栈和组件
|
- [前端开发指南](/frontend) - 了解前端技术栈和组件
|
||||||
- [API 文档](/docs/openapi) - 查看完整的 API 接口文档
|
- [API 文档](/openapi) - 查看完整的 API 接口文档
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function baseOptions(): BaseLayoutProps {
|
|||||||
githubUrl: 'https://github.com/nagisa77/OpenIsle',
|
githubUrl: 'https://github.com/nagisa77/OpenIsle',
|
||||||
nav: {
|
nav: {
|
||||||
title: 'OpenIsle Docs',
|
title: 'OpenIsle Docs',
|
||||||
url: '/docs',
|
url: '/',
|
||||||
},
|
},
|
||||||
searchToggle: {
|
searchToggle: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as ClientAdapters from './media-adapter.client';
|
|||||||
// See https://fumadocs.vercel.app/docs/headless/source-api for more info
|
// See https://fumadocs.vercel.app/docs/headless/source-api for more info
|
||||||
export const source = loader({
|
export const source = loader({
|
||||||
// it assigns a URL to your pages
|
// it assigns a URL to your pages
|
||||||
baseUrl: '/docs',
|
baseUrl: '/',
|
||||||
source: docs.toFumadocsSource(),
|
source: docs.toFumadocsSource(),
|
||||||
pageTree: {
|
pageTree: {
|
||||||
transformers: [transformerOpenAPI()],
|
transformers: [transformerOpenAPI()],
|
||||||
|
|||||||
Reference in New Issue
Block a user