Compare commits

...

20 Commits

Author SHA1 Message Date
Tim
7010e8a058 feat: allow configuring multiple openapi servers 2025-09-09 15:03:25 +08:00
Tim
38ee37d5be Merge pull request #946 from smallclover/main 2025-09-09 14:29:06 +08:00
Tim
e398d8e989 Merge pull request #949 from nagisa77/codex/remove-/docs/-prefix-from-url-uh7skh
feat(docs): remove /docs URL prefix
2025-09-09 14:03:20 +08:00
Tim
85e77c265e feat(docs): remove /docs prefix 2025-09-09 14:03:04 +08:00
tim
8abdc73497 Revert "feat(docs): remove path prefix"
This reverts commit 09cefbedbf.
2025-09-09 14:02:23 +08:00
Tim
747d9c07d1 Merge pull request #948 from nagisa77/codex/remove-/docs/-prefix-from-url-3n0gdr
feat(docs): serve documentation from root
2025-09-09 13:48:51 +08:00
Tim
09cefbedbf feat(docs): remove path prefix 2025-09-09 13:48:26 +08:00
tim
d772bc182f fix: 允许自建OpenAPI地址 2025-09-09 13:46:25 +08:00
tim
358c53338d Revert "fix: 新增检查"
This reverts commit 1cd89eaa54.
2025-09-09 13:23:30 +08:00
wangshun
2110980797 控制用户发帖频率 2025-09-09 13:23:14 +08:00
tim
1cd89eaa54 fix: 新增检查 2025-09-09 13:16:52 +08:00
tim
1d2e7eb96e Revert "Update deploy-docs.yml"
This reverts commit 4428e06f1d.
2025-09-09 13:10:46 +08:00
Tim
4428e06f1d Update deploy-docs.yml 2025-09-09 13:03:08 +08:00
Tim
dddff54556 Update README.md 2025-09-09 12:18:10 +08:00
Tim
e7f7bbac22 Update README.md 2025-09-09 12:17:49 +08:00
Tim
37aae4ba5c Update README.md 2025-09-09 12:17:24 +08:00
Tim
54cfc98336 Merge pull request #945 from nagisa77/codex/fix-server-url-in-api-docs
Add configurable OpenAPI server URL
2025-09-09 12:12:41 +08:00
Tim
d42d38ff7a Add configurable OpenAPI server URL 2025-09-09 12:12:10 +08:00
Tim
2b4601bd4b Update CONTRIBUTING.md 2025-09-09 11:56:15 +08:00
Tim
5071d9c6d5 Merge pull request #944 from nagisa77/codex/fix-api-docs-base-url-to-use-https
docs: use https for OpenAPI base URL
2025-09-09 11:48:53 +08:00
16 changed files with 105 additions and 31 deletions

View File

@@ -246,3 +246,9 @@ https://resend.com/emails 创建账号并登录
`RESEND_FROM_EMAIL` **noreply@域名** `RESEND_FROM_EMAIL` **noreply@域名**
`RESEND_API_KEY`**刚刚复制的 Key** `RESEND_API_KEY`**刚刚复制的 Key**
![image-20250906151218330](assets/contributing/image-20250906151218330.png) ![image-20250906151218330](assets/contributing/image-20250906151218330.png)
## 开源共建和API文档
- API文档: https://openisle-docs.netlify.app/docs/openapi

View File

@@ -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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p> </p>
## 💡 简介 ## 💡 简介

View File

@@ -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的序列化器

View File

@@ -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"));
} }
} }

View File

@@ -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.", "://")
)); ));

View File

@@ -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;
}
}

View File

@@ -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"));

View File

@@ -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

View File

@@ -16,6 +16,6 @@ bun dev
使用以下路由: 使用以下路由:
- `docs/frontend/` 前端技术文档 - `frontend/` 前端技术文档
- `docs/backend/` 后端技术文档 - `backend/` 后端技术文档
- `docs/openapi/` 后端 API 文档 - `openapi/` 后端 API 文档

View File

@@ -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);

View File

@@ -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 />

View File

@@ -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">

View File

@@ -40,4 +40,4 @@ backend/
## API 接口 ## API 接口
详细的 API 接口文档请查看 [API 文档](/docs/openapi)。 详细的 API 接口文档请查看 [API 文档](/openapi)。

View File

@@ -9,6 +9,6 @@ OpenIsle 是一个现代化的社区平台,提供完整的社交功能。
## 快速开始 ## 快速开始
- [后端开发指南](/docs/backend) - 了解后端架构和开发 - [后端开发指南](/backend) - 了解后端架构和开发
- [前端开发指南](/docs/frontend) - 了解前端技术栈和组件 - [前端开发指南](/frontend) - 了解前端技术栈和组件
- [API 文档](/docs/openapi) - 查看完整的 API 接口文档 - [API 文档](/openapi) - 查看完整的 API 接口文档

View File

@@ -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,

View File

@@ -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()],